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