diff --git a/client/frame-runner.js b/client/frame-runner.js index 7d399590cd..d12e88d47f 100644 --- a/client/frame-runner.js +++ b/client/frame-runner.js @@ -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); }); diff --git a/client/sagas/execute-challenge-saga.js b/client/sagas/execute-challenge-saga.js index 0f5ffb8986..3ec206ce60 100644 --- a/client/sagas/execute-challenge-saga.js +++ b/client/sagas/execute-challenge-saga.js @@ -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}`); + + return Observable.combineLatest(head$, frameRunner$) + .map(([ head, frameRunner ]) => { + return head + `${source}` + 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); + }); }); } diff --git a/client/sagas/frame-saga.js b/client/sagas/frame-saga.js index 3402d4f6a0..04fa3bd3bf 100644 --- a/client/sagas/frame-saga.js +++ b/client/sagas/frame-saga.js @@ -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) => ` + +`; + 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$ + ); } diff --git a/common/app/routes/challenges/components/classic/Classic.jsx b/common/app/routes/challenges/components/classic/Classic.jsx index fe89974e37..df9004daa9 100644 --- a/common/app/routes/challenges/components/classic/Classic.jsx +++ b/common/app/routes/challenges/components/classic/Classic.jsx @@ -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; diff --git a/common/app/routes/challenges/components/classic/Test-Suite.jsx b/common/app/routes/challenges/components/classic/Test-Suite.jsx index 9599e3352a..dff602ab4b 100644 --- a/common/app/routes/challenges/components/classic/Test-Suite.jsx +++ b/common/app/routes/challenges/components/classic/Test-Suite.jsx @@ -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 ( diff --git a/common/app/routes/challenges/redux/actions.js b/common/app/routes/challenges/redux/actions.js index 5a364899b4..4de4cff4c7 100644 --- a/common/app/routes/challenges/redux/actions.js +++ b/common/app/routes/challenges/redux/actions.js @@ -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); diff --git a/common/app/routes/challenges/redux/reducer.js b/common/app/routes/challenges/redux/reducer.js index 5e0d8d64fe..badd11be39 100644 --- a/common/app/routes/challenges/redux/reducer.js +++ b/common/app/routes/challenges/redux/reducer.js @@ -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 = ''}) => ({ diff --git a/common/app/routes/challenges/redux/types.js b/common/app/routes/challenges/redux/types.js index 5a786d0391..8db8d3a409 100644 --- a/common/app/routes/challenges/redux/types.js +++ b/common/app/routes/challenges/redux/types.js @@ -21,7 +21,11 @@ export default createTypes([ // rechallenge 'executeChallenge', + 'updateMain', + 'runTests', 'frameMain', 'frameOutput', - 'frameTests' + 'frameTests', + 'updateOutput', + 'updateTests' ], 'challenges'); diff --git a/common/app/routes/challenges/utils.js b/common/app/routes/challenges/utils.js index c5f6c3673f..f5084b2021 100644 --- a/common/app/routes/challenges/utils.js +++ b/common/app/routes/challenges/utils.js @@ -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 })); }