Run tests in iframe displays results to user
This commit is contained in:
@ -1,8 +1,12 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var common = parent.__common;
|
||||
var Rx = parent.Rx;
|
||||
var frameId = window.__frameId;
|
||||
var frameReady = common[frameId + 'Ready$'] || { onNext() {} };
|
||||
var Rx = document.Rx;
|
||||
var chai = parent.chai;
|
||||
var source = document.__source;
|
||||
|
||||
common.getJsOutput = function evalJs(source = '') {
|
||||
document.__getJsOutput = function getJsOutput() {
|
||||
if (window.__err || !common.shouldRun()) {
|
||||
return window.__err || 'source disabled';
|
||||
}
|
||||
@ -12,13 +16,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
output = eval(source);
|
||||
/* eslint-enable no-eval */
|
||||
} catch (e) {
|
||||
output = e.message;
|
||||
window.__err = e;
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
common.runTests$ = function runTests$({ tests = [], source }) {
|
||||
document.__runTests$ = function runTests$(tests = []) {
|
||||
/* eslint-disable no-unused-vars */
|
||||
const editor = { getValue() { return source; } };
|
||||
const code = source;
|
||||
/* eslint-enable no-unused-vars */
|
||||
if (window.__err) {
|
||||
return Rx.Observable.throw(window.__err);
|
||||
}
|
||||
@ -27,8 +35,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// on new stacks
|
||||
return Rx.Observable.from(tests, null, null, Rx.Scheduler.default)
|
||||
// add delay here for firefox to catch up
|
||||
.delay(100)
|
||||
.delay(200)
|
||||
/* eslint-disable no-unused-vars */
|
||||
.map(({ text, testString }) => {
|
||||
const assert = chai.assert;
|
||||
/* eslint-enable no-unused-vars */
|
||||
const newTest = { text, testString };
|
||||
let test;
|
||||
try {
|
||||
@ -46,7 +57,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
newTest.err = e.message.split(':').shift();
|
||||
newTest.err = e.message + '\n' + e.stack;
|
||||
}
|
||||
if (!newTest.err) {
|
||||
newTest.pass = true;
|
||||
}
|
||||
return newTest;
|
||||
})
|
||||
@ -55,7 +69,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
};
|
||||
|
||||
// used when updating preview without running tests
|
||||
common.checkPreview$ = function checkPreview$(args) {
|
||||
document.__checkPreview$ = function checkPreview$(args) {
|
||||
if (window.__err) {
|
||||
return Rx.Observable.throw(window.__err);
|
||||
}
|
||||
@ -66,5 +80,5 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// we set the subject to true
|
||||
// this will let the updatePreview
|
||||
// script now that we are ready.
|
||||
common.testFrameReady$.onNext(true);
|
||||
frameReady.onNext(null);
|
||||
});
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { Observable } from 'rx';
|
||||
import { Scheduler, Observable } from 'rx';
|
||||
|
||||
import { ajax$ } from '../../common/utils/ajax-stream';
|
||||
import throwers from '../rechallenge/throwers';
|
||||
import transformers from '../rechallenge/transformers';
|
||||
import types from '../../common/app/routes/challenges/redux/types';
|
||||
import {
|
||||
frameMain
|
||||
frameMain,
|
||||
frameTests,
|
||||
frameOutput
|
||||
} from '../../common/app/routes/challenges/redux/actions';
|
||||
import { setExt, updateContents } from '../../common/utils/polyvinyl';
|
||||
|
||||
@ -34,7 +36,6 @@ function cacheScript({ src } = {}) {
|
||||
}
|
||||
const script$ = ajax$(src)
|
||||
.doOnNext(res => {
|
||||
console.log('status', res.status);
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Request errror: ' + res.status);
|
||||
}
|
||||
@ -55,9 +56,12 @@ const jsCatch = '\n;/* */';
|
||||
|
||||
export default function executeChallengeSaga(action$, getState) {
|
||||
return action$
|
||||
.filter(({ type }) => type === types.executeChallenge)
|
||||
.filter(({ type }) => (
|
||||
type === types.executeChallenge ||
|
||||
type === types.updateMain
|
||||
))
|
||||
.debounce(750)
|
||||
.flatMapLatest(() => {
|
||||
.flatMapLatest(({ type }) => {
|
||||
const { files, required = [ jQuery ] } = getState().challengesApp;
|
||||
return createFileStream(files)
|
||||
::throwers()
|
||||
@ -81,18 +85,31 @@ export default function executeChallengeSaga(action$, getState) {
|
||||
return build + finalFile.contents + htmlCatch;
|
||||
}, ''))
|
||||
// add required scripts and links here
|
||||
.flatMap(build => {
|
||||
const header$ = Observable.from(required)
|
||||
.flatMap(source => {
|
||||
const head$ = Observable.from(required)
|
||||
.flatMap(required => {
|
||||
if (required.script) {
|
||||
return cacheScript(required);
|
||||
}
|
||||
return Observable.just('');
|
||||
})
|
||||
.reduce((header, required) => header + required, '');
|
||||
return Observable.combineLatest(header$, frameRunner$)
|
||||
.map(([ header, frameRunner ]) => header + build + frameRunner);
|
||||
.reduce((head, required) => head + required, '')
|
||||
.map(head => `<head>${head}</head>`);
|
||||
|
||||
return Observable.combineLatest(head$, frameRunner$)
|
||||
.map(([ head, frameRunner ]) => {
|
||||
return head + `<body>${source}</body>` + frameRunner;
|
||||
})
|
||||
.map(build => ({ source, build }));
|
||||
})
|
||||
.map(build => frameMain(build));
|
||||
.flatMap(payload => {
|
||||
const actions = [];
|
||||
actions.push(frameMain(payload));
|
||||
if (type !== types.updateMain) {
|
||||
actions.push(frameTests(payload));
|
||||
actions.push(frameOutput(payload));
|
||||
}
|
||||
return Observable.from(actions, null, null, Scheduler.default);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,12 +1,24 @@
|
||||
import { BehaviorSubject } from 'rx';
|
||||
import Rx, { Observable, Subject } from 'rx';
|
||||
import tape from 'tape';
|
||||
import types from '../../common/app/routes/challenges/redux/types';
|
||||
import {
|
||||
updateOutput
|
||||
} from '../../common/app/routes/challenges/redux/types';
|
||||
import {
|
||||
updateTests
|
||||
} from '../../common/app/routes/challenges/redux/actions';
|
||||
|
||||
// we use three different frames to make them all essentially pure functions
|
||||
const mainId = 'fcc-main-frame';
|
||||
/*
|
||||
const outputId = 'fcc-output-frame';
|
||||
const testId = 'fcc-test-frame';
|
||||
*/
|
||||
const outputId = 'fcc-output-frame';
|
||||
|
||||
const createHeader = (id = mainId) => `
|
||||
<script>
|
||||
window.__frameId = '${id}';
|
||||
</script>
|
||||
`;
|
||||
|
||||
|
||||
function createFrame(document, id = mainId) {
|
||||
const frame = document.createElement('iframe');
|
||||
@ -25,27 +37,58 @@ function getFrameDocument(document, id = mainId) {
|
||||
let frame = document.getElementById(id);
|
||||
if (!frame) {
|
||||
frame = createFrame(document, id);
|
||||
} else {
|
||||
refreshFrame(frame);
|
||||
}
|
||||
return frame.contentDocument || frame.contentWindow.document;
|
||||
}
|
||||
|
||||
function frameMain(build, document) {
|
||||
function frameMain({ build } = {}, document) {
|
||||
const main = getFrameDocument(document);
|
||||
refreshFrame(main);
|
||||
main.open();
|
||||
main.write(build);
|
||||
main.write(createHeader() + build);
|
||||
main.close();
|
||||
}
|
||||
|
||||
function frameTests({ build, source } = {}, document) {
|
||||
const tests = getFrameDocument(document, testId);
|
||||
refreshFrame(tests);
|
||||
tests.Rx = Rx;
|
||||
tests.tape = tape;
|
||||
tests.__source = source;
|
||||
tests.open();
|
||||
tests.write(createHeader(testId) + build);
|
||||
tests.close();
|
||||
}
|
||||
|
||||
export default function frameSaga(actions$, getState, { window, document }) {
|
||||
window.__common = {};
|
||||
window.__common.outputFrameReady$ = new BehaviorSubject(false);
|
||||
window.__common.testFrameReady$ = new BehaviorSubject(false);
|
||||
return actions$
|
||||
.filter(({ type }) => type === types.frameMain)
|
||||
const runTests$ = window.__common[testId + 'Ready$'] =
|
||||
new Subject();
|
||||
const updateOutput$ = window.__common[outputId + 'Ready$'] =
|
||||
new Subject();
|
||||
window.__common.shouldRun = () => true;
|
||||
const result$ = actions$
|
||||
.filter(({ type }) => (
|
||||
type === types.frameMain ||
|
||||
type === types.frameTests
|
||||
))
|
||||
.map(action => {
|
||||
frameMain(action.payload, document);
|
||||
if (action.type === types.frameMain) {
|
||||
return frameMain(action.payload, document);
|
||||
}
|
||||
if (action.type === types.frameTests) {
|
||||
return frameTests(action.payload, document);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
return Observable.merge(
|
||||
updateOutput$.map(updateOutput),
|
||||
runTests$.flatMap(() => {
|
||||
const frame = getFrameDocument(document, testId);
|
||||
const { tests } = getState().challengesApp;
|
||||
return frame.__runTests$(tests).map(updateTests);
|
||||
}),
|
||||
result$
|
||||
);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import Editor from './Editor.jsx';
|
||||
import SidePanel from './Side-Panel.jsx';
|
||||
import Preview from './Preview.jsx';
|
||||
import { challengeSelector } from '../../redux/selectors';
|
||||
import { executeChallenge, updateFile } from '../../redux/actions';
|
||||
import { executeChallenge, updateMain, updateFile } from '../../redux/actions';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
challengeSelector,
|
||||
@ -24,7 +24,7 @@ const mapStateToProps = createSelector(
|
||||
})
|
||||
);
|
||||
|
||||
const bindableActions = { executeChallenge, updateFile };
|
||||
const bindableActions = { executeChallenge, updateFile, updateMain };
|
||||
|
||||
export class Challenge extends PureComponent {
|
||||
static displayName = 'Challenge';
|
||||
@ -34,9 +34,13 @@ export class Challenge extends PureComponent {
|
||||
content: PropTypes.string,
|
||||
mode: PropTypes.string,
|
||||
updateFile: PropTypes.func,
|
||||
executeChallenge: PropTypes.func
|
||||
executeChallenge: PropTypes.func,
|
||||
updateMain: PropTypes.func
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.updateMain();
|
||||
}
|
||||
renderPreview(showPreview) {
|
||||
if (!showPreview) {
|
||||
return null;
|
||||
|
@ -14,8 +14,8 @@ export default class extends PureComponent {
|
||||
return tests.map(({ err, text = '' }, index)=> {
|
||||
const iconClass = classnames({
|
||||
'big-icon': true,
|
||||
'ion-close-circled error-icon': !refresh && !err,
|
||||
'ion-checkmark-circled success-icon': !refresh && err,
|
||||
'ion-close-circled error-icon': !refresh && err,
|
||||
'ion-checkmark-circled success-icon': !refresh && !err,
|
||||
'ion-refresh refresh-icon': refresh
|
||||
});
|
||||
return (
|
||||
|
@ -44,6 +44,10 @@ export const updateFiles = createAction(types.updateFiles);
|
||||
|
||||
// rechallenge
|
||||
export const executeChallenge = createAction(types.executeChallenge);
|
||||
export const updateMain = createAction(types.updateMain);
|
||||
export const frameMain = createAction(types.frameMain);
|
||||
export const frameOutput = createAction(types.frameOutput);
|
||||
export const frameTests = createAction(types.frameTests);
|
||||
export const runTests = createAction(types.runTests);
|
||||
export const updateOutput = createAction(types.updateOutput);
|
||||
export const updateTests = createAction(types.updateTests);
|
||||
|
@ -33,6 +33,12 @@ const mainReducer = handleActions(
|
||||
key: getFileKey(challenge),
|
||||
tests: createTests(challenge)
|
||||
}),
|
||||
[types.updateTests]: (state, { payload: tests }) => ({
|
||||
...state,
|
||||
refresh: false,
|
||||
tests
|
||||
}),
|
||||
[types.executeChallenge]: state => ({ ...state, refresh: true }),
|
||||
|
||||
// map
|
||||
[types.updateFilter]: (state, { payload = ''}) => ({
|
||||
|
@ -21,7 +21,11 @@ export default createTypes([
|
||||
|
||||
// rechallenge
|
||||
'executeChallenge',
|
||||
'updateMain',
|
||||
'runTests',
|
||||
'frameMain',
|
||||
'frameOutput',
|
||||
'frameTests'
|
||||
'frameTests',
|
||||
'updateOutput',
|
||||
'updateTests'
|
||||
], 'challenges');
|
||||
|
@ -60,6 +60,7 @@ export function getFileKey({ challengeType }) {
|
||||
export function createTests({ tests = [] }) {
|
||||
return tests
|
||||
.map(test => ({
|
||||
text: test.split('message: ').pop().replace(/\'\);/g, '')
|
||||
text: test.split('message: ').pop().replace(/\'\);/g, ''),
|
||||
testString: test
|
||||
}));
|
||||
}
|
||||
|
Reference in New Issue
Block a user