Run tests in iframe displays results to user

This commit is contained in:
Berkeley Martinez
2016-05-25 18:28:20 -07:00
parent e3e9f67e40
commit 256182836a
9 changed files with 131 additions and 38 deletions

View File

@ -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);
});

View File

@ -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);
});
});
}

View File

@ -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$
);
}

View File

@ -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;

View File

@ -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 (

View File

@ -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);

View File

@ -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 = ''}) => ({

View File

@ -21,7 +21,11 @@ export default createTypes([
// rechallenge
'executeChallenge',
'updateMain',
'runTests',
'frameMain',
'frameOutput',
'frameTests'
'frameTests',
'updateOutput',
'updateTests'
], 'challenges');

View File

@ -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
}));
}