feat(client): execute DOM challenge saga
This commit is contained in:
@ -4,31 +4,23 @@ import jQuery from 'jquery';
|
||||
|
||||
window.$ = jQuery;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const {
|
||||
timeout,
|
||||
catchError,
|
||||
map,
|
||||
toArray,
|
||||
mergeMap,
|
||||
of,
|
||||
from,
|
||||
throwError
|
||||
} = document.__deps__.rx;
|
||||
const testId = 'fcc-test-frame';
|
||||
if (window.frameElement && window.frameElement.id === testId) {
|
||||
document.addEventListener('DOMContentLoaded', initTestFrame);
|
||||
}
|
||||
|
||||
function initTestFrame() {
|
||||
// window.__logs = [];
|
||||
// const oldLog = window.console.log.bind(window.console);
|
||||
// window.console.log = function proxyConsole(...args) {
|
||||
// window.__logs = [...window.__logs, ...args];
|
||||
// return oldLog(...args);
|
||||
// };
|
||||
|
||||
const frameReady = document.__frameReady;
|
||||
const source = document.__source;
|
||||
const __getUserInput = document.__getUserInput || (x => x);
|
||||
const checkChallengePayload = document.__checkChallengePayload;
|
||||
|
||||
const fiveSeconds = 5000;
|
||||
|
||||
function isPromise(value) {
|
||||
return (
|
||||
value &&
|
||||
typeof value.subscribe !== 'function' &&
|
||||
typeof value.then === 'function'
|
||||
);
|
||||
}
|
||||
// Fake Deep Equal dependency
|
||||
/* eslint-disable no-unused-vars */
|
||||
const DeepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
||||
@ -49,11 +41,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
return o;
|
||||
};
|
||||
|
||||
const assert = chai.assert;
|
||||
const getUserInput = __getUserInput;
|
||||
|
||||
if (document.Enzyme) {
|
||||
window.Enzyme = document.Enzyme;
|
||||
}
|
||||
|
||||
document.__runTests = function runTests(tests = []) {
|
||||
document.__runTest = async function runTests(testString) {
|
||||
window.__logs = [];
|
||||
/* eslint-disable no-unused-vars */
|
||||
const code = source.slice(0);
|
||||
const editor = {
|
||||
@ -61,71 +57,36 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
return source;
|
||||
}
|
||||
};
|
||||
const assert = chai.assert;
|
||||
const getUserInput = __getUserInput;
|
||||
// Iterate through the test one at a time
|
||||
// on new stacks
|
||||
const results = from(tests).pipe(
|
||||
mergeMap(function runOneTest({ text, testString }) {
|
||||
const newTest = { text, testString };
|
||||
let test;
|
||||
let __result;
|
||||
// uncomment the following line to inspect
|
||||
// the frame-runner as it runs tests
|
||||
// make sure the dev tools console is open
|
||||
// debugger;
|
||||
try {
|
||||
/* eslint-disable no-eval */
|
||||
// eval test string to actual JavaScript
|
||||
// This return can be a function
|
||||
// i.e. function() { assert(true, 'happy coding'); }
|
||||
test = eval(testString);
|
||||
/* eslint-enable no-eval */
|
||||
if (typeof test === 'function') {
|
||||
// all async tests must return a promise or observable
|
||||
// sync tests can return Any type
|
||||
__result = test(getUserInput);
|
||||
if (isPromise(__result)) {
|
||||
// resolve the promise before continuing
|
||||
__result = from(__result);
|
||||
}
|
||||
}
|
||||
|
||||
if (!__result || typeof __result.subscribe !== 'function') {
|
||||
// make sure result is an observable
|
||||
__result = of(null);
|
||||
}
|
||||
} catch (e) {
|
||||
__result = throwError(e);
|
||||
}
|
||||
return __result.pipe(
|
||||
timeout(fiveSeconds),
|
||||
map(() => {
|
||||
// if we are here, then the assert passed
|
||||
// mark test as passing
|
||||
newTest.pass = true;
|
||||
return newTest;
|
||||
}),
|
||||
catchError(err => {
|
||||
const { message, stack } = err;
|
||||
// we catch the error here to prevent the error from bubbling up
|
||||
// and collapsing the pipe
|
||||
newTest.err = message + '\n' + stack;
|
||||
newTest.stack = stack;
|
||||
newTest.message = text.replace(/<code>(.*?)<\/code>/g, '$1');
|
||||
if (!(err instanceof chai.AssertionError)) {
|
||||
console.error(err);
|
||||
}
|
||||
// RxJS catch expects an observable as a return
|
||||
return of(newTest);
|
||||
})
|
||||
);
|
||||
}),
|
||||
toArray()
|
||||
);
|
||||
return results;
|
||||
/* eslint-disable no-unused-vars */
|
||||
// uncomment the following line to inspect
|
||||
// the frame-runner as it runs tests
|
||||
// make sure the dev tools console is open
|
||||
// debugger;
|
||||
try {
|
||||
/* eslint-disable no-eval */
|
||||
// eval test string to actual JavaScript
|
||||
// This return can be a function
|
||||
// i.e. function() { assert(true, 'happy coding'); }
|
||||
const test = eval(testString);
|
||||
/* eslint-enable no-eval */
|
||||
if (typeof test === 'function') {
|
||||
await test(getUserInput);
|
||||
}
|
||||
return { pass: true, logs: window.__logs.map(String) };
|
||||
} catch (err) {
|
||||
if (!(err instanceof chai.AssertionError)) {
|
||||
console.error(err);
|
||||
}
|
||||
return {
|
||||
err: {
|
||||
message: err.message,
|
||||
stack: err.stack
|
||||
},
|
||||
logs: window.__logs.map(String)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// notify that the window methods are ready to run
|
||||
frameReady.next({ checkChallengePayload });
|
||||
});
|
||||
frameReady();
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { from } from 'rxjs';
|
||||
import {
|
||||
cond,
|
||||
flow,
|
||||
@ -102,7 +101,7 @@ A required file can not have both a src and a link: src = ${src}, link = ${link}
|
||||
const frameRunner =
|
||||
'<script src="/js/frame-runner.js" type="text/javascript"></script>';
|
||||
|
||||
return from(
|
||||
return (
|
||||
Promise.all([head, body, frameRunner, sourceMap]).then(
|
||||
([head, body, frameRunner, sourceMap]) => ({
|
||||
build: head + frameRunner + body,
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
startWith,
|
||||
delay
|
||||
} from 'rxjs/operators';
|
||||
import { ofType, combineEpics } from 'redux-observable';
|
||||
import { ofType } from 'redux-observable';
|
||||
import { overEvery, isString } from 'lodash';
|
||||
|
||||
import {
|
||||
@ -31,51 +31,12 @@ import {
|
||||
isJSEnabledSelector
|
||||
} from './';
|
||||
import { buildFromFiles, buildBackendChallenge } from '../utils/build';
|
||||
import {
|
||||
runTestsInTestFrame,
|
||||
createTestFramer,
|
||||
createMainFramer
|
||||
} from '../utils/frame.js';
|
||||
import { runTestsInTestFrame, createTestFramer } from '../utils/frame.js';
|
||||
|
||||
import { challengeTypes } from '../../../../utils/challengeTypes';
|
||||
|
||||
const executeDebounceTimeout = 750;
|
||||
|
||||
function updateMainEpic(action$, state$, { document }) {
|
||||
return action$.pipe(
|
||||
ofType(
|
||||
types.updateFile,
|
||||
types.previewMounted,
|
||||
types.challengeMounted,
|
||||
types.resetChallenge
|
||||
),
|
||||
filter(() => {
|
||||
const { challengeType } = challengeMetaSelector(state$.value);
|
||||
return (
|
||||
challengeType !== challengeTypes.js &&
|
||||
challengeType !== challengeTypes.bonfire
|
||||
);
|
||||
}),
|
||||
debounceTime(executeDebounceTimeout),
|
||||
switchMap(() => {
|
||||
const frameMain = createMainFramer(document, state$);
|
||||
return buildFromFiles(state$.value).pipe(
|
||||
map(frameMain),
|
||||
ignoreElements(),
|
||||
startWith(initConsole('')),
|
||||
catchError((...err) => {
|
||||
console.error(err);
|
||||
return of(disableJSOnError(err.message));
|
||||
})
|
||||
);
|
||||
}),
|
||||
catchError(err => {
|
||||
console.error(err);
|
||||
return of(disableJSOnError(err.message));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function executeChallengeEpic(action$, state$, { document }) {
|
||||
return of(document).pipe(
|
||||
filter(Boolean),
|
||||
@ -117,13 +78,7 @@ function executeChallengeEpic(action$, state$, { document }) {
|
||||
);
|
||||
const buildAndFrameChallenge = action$.pipe(
|
||||
ofType(types.executeChallenge),
|
||||
filter(() => {
|
||||
const { challengeType } = challengeMetaSelector(state$.value);
|
||||
return (
|
||||
challengeType !== challengeTypes.js &&
|
||||
challengeType !== challengeTypes.bonfire
|
||||
);
|
||||
}),
|
||||
filter(() => false),
|
||||
debounceTime(executeDebounceTimeout),
|
||||
filter(() => isJSEnabledSelector(state$.value)),
|
||||
switchMap(() => {
|
||||
@ -151,4 +106,4 @@ function executeChallengeEpic(action$, state$, { document }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default combineEpics(updateMainEpic, executeChallengeEpic);
|
||||
export default executeChallengeEpic;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { put, select, call, takeLatest } from 'redux-saga/effects';
|
||||
import { put, select, call, takeLatest, race } from 'redux-saga/effects';
|
||||
import { delay } from 'redux-saga';
|
||||
|
||||
import {
|
||||
challengeMetaSelector,
|
||||
@ -12,11 +13,16 @@ import {
|
||||
challengeFilesSelector
|
||||
} from './';
|
||||
|
||||
import { buildJSFromFiles } from '../utils/build';
|
||||
import { buildJSFromFiles, buildFromFiles } from '../utils/build';
|
||||
|
||||
import { challengeTypes } from '../../../../utils/challengeTypes';
|
||||
|
||||
import WorkerExecutor from '../utils/worker-executor';
|
||||
import {
|
||||
createMainFramer,
|
||||
createTestFramer,
|
||||
runTestInTestFrame
|
||||
} from '../utils/frame.js';
|
||||
|
||||
const testWorker = new WorkerExecutor('test-evaluator');
|
||||
const testTimeout = 5000;
|
||||
@ -26,8 +32,8 @@ function* ExecuteChallengeSaga() {
|
||||
const { js, bonfire, backend } = challengeTypes;
|
||||
const { challengeType } = yield select(challengeMetaSelector);
|
||||
|
||||
// TODO: ExecuteBackendChallengeSaga and ExecuteDOMChallengeSaga
|
||||
if (challengeType !== js && challengeType !== bonfire) {
|
||||
// TODO: ExecuteBackendChallengeSaga
|
||||
if (challengeType === backend) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -45,7 +51,7 @@ function* ExecuteChallengeSaga() {
|
||||
// yield ExecuteBackendChallengeSaga();
|
||||
break;
|
||||
default:
|
||||
// yield ExecuteDOMChallengeSaga();
|
||||
testResults = yield ExecuteDOMChallengeSaga(tests);
|
||||
}
|
||||
|
||||
yield put(updateTests(testResults));
|
||||
@ -96,6 +102,94 @@ function* ExecuteJSChallengeSaga(tests) {
|
||||
return testResults;
|
||||
}
|
||||
|
||||
export function createExecuteChallengeSaga(types) {
|
||||
return [takeLatest(types.executeChallenge, ExecuteChallengeSaga)];
|
||||
function createTestFrame(state, ctx, proxyLogger) {
|
||||
return new Promise(resolve => {
|
||||
const frameTest = createTestFramer(document, state, resolve, proxyLogger);
|
||||
frameTest(ctx);
|
||||
}).then(() => console.log('Frame ready'));
|
||||
}
|
||||
|
||||
function* proxyLogger() {
|
||||
let args = yield;
|
||||
while (true) {
|
||||
args = yield put(updateLogs(args));
|
||||
}
|
||||
}
|
||||
|
||||
function* ExecuteDOMChallengeSaga(tests) {
|
||||
const testResults = [];
|
||||
const state = yield select();
|
||||
const ctx = yield call(buildFromFiles, state);
|
||||
const proxy = proxyLogger();
|
||||
proxy.next('1');
|
||||
proxy.next('2');
|
||||
proxy.next('3');
|
||||
yield call(createTestFrame, state, ctx, proxy);
|
||||
|
||||
for (const { text, testString } of tests) {
|
||||
const newTest = { text, testString };
|
||||
try {
|
||||
const [{ pass, err, logs }, timeout] = yield race([
|
||||
call(runTestInTestFrame, document, testString),
|
||||
delay(testTimeout, 'timeout')
|
||||
]);
|
||||
if (timeout) {
|
||||
throw timeout;
|
||||
}
|
||||
for (const log of logs) {
|
||||
yield put(updateLogs(log));
|
||||
}
|
||||
if (pass) {
|
||||
newTest.pass = true;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} catch (err) {
|
||||
newTest.message = text.replace(/<code>(.*?)<\/code>/g, '$1');
|
||||
if (err === 'timeout') {
|
||||
newTest.err = 'Test timed out';
|
||||
newTest.message = `${newTest.message} (${newTest.err})`;
|
||||
} else {
|
||||
const { message, stack } = err;
|
||||
newTest.err = message + '\n' + stack;
|
||||
newTest.stack = stack;
|
||||
}
|
||||
console.error(err);
|
||||
yield put(updateConsole(newTest.message));
|
||||
} finally {
|
||||
testResults.push(newTest);
|
||||
}
|
||||
}
|
||||
return testResults;
|
||||
}
|
||||
|
||||
function* updateMainSaga() {
|
||||
try {
|
||||
const { html, modern } = challengeTypes;
|
||||
const { challengeType } = yield select(challengeMetaSelector);
|
||||
if (challengeType !== html && challengeType !== modern) {
|
||||
return;
|
||||
}
|
||||
const state = yield select();
|
||||
const frameMain = yield call(createMainFramer, document, state);
|
||||
const ctx = yield call(buildFromFiles, state);
|
||||
yield call(frameMain, ctx);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
export function createExecuteChallengeSaga(types) {
|
||||
return [
|
||||
takeLatest(types.executeChallenge, ExecuteChallengeSaga),
|
||||
takeLatest(
|
||||
[
|
||||
types.updateFile,
|
||||
types.previewMounted,
|
||||
types.challengeMounted,
|
||||
types.resetChallenge
|
||||
],
|
||||
updateMainSaga
|
||||
)
|
||||
];
|
||||
}
|
||||
|
@ -1,14 +1,4 @@
|
||||
import { toString, flow } from 'lodash';
|
||||
import { defer, of, from, Observable, throwError, queueScheduler } from 'rxjs';
|
||||
import {
|
||||
tap,
|
||||
map,
|
||||
toArray,
|
||||
delay,
|
||||
mergeMap,
|
||||
timeout,
|
||||
catchError
|
||||
} from 'rxjs/operators';
|
||||
import { configure, shallow, mount } from 'enzyme';
|
||||
import Adapter16 from 'enzyme-adapter-react-16';
|
||||
import { setConfig } from 'react-hot-loader';
|
||||
@ -43,16 +33,15 @@ const createHeader = (id = mainId) => `
|
||||
</script>
|
||||
`;
|
||||
|
||||
export const runTestsInTestFrame = (document, tests) =>
|
||||
defer(() => {
|
||||
const { contentDocument: frame } = document.getElementById(testId);
|
||||
// Enable Stateless Functional Component. Otherwise, enzyme-adapter-react-16
|
||||
// does not work correctly.
|
||||
setConfig({ pureSFC: true });
|
||||
return frame
|
||||
.__runTests(tests)
|
||||
.pipe(tap(() => setConfig({ pureSFC: false })));
|
||||
});
|
||||
export const runTestInTestFrame = async function(document, tests) {
|
||||
const { contentDocument: frame } = document.getElementById(testId);
|
||||
// Enable Stateless Functional Component. Otherwise, enzyme-adapter-react-16
|
||||
// does not work correctly.
|
||||
setConfig({ pureSFC: true });
|
||||
const result = await frame.__runTest(tests);
|
||||
setConfig({ pureSFC: false });
|
||||
return result;
|
||||
};
|
||||
|
||||
const createFrame = (document, state, id) => ctx => {
|
||||
const isJSEnabled = isJSEnabledSelector(state);
|
||||
@ -67,14 +56,14 @@ const createFrame = (document, state, id) => ctx => {
|
||||
};
|
||||
};
|
||||
|
||||
const hiddenFrameClassname = 'hide-test-frame';
|
||||
const hiddenFrameClassName = 'hide-test-frame';
|
||||
const mountFrame = document => ({ element, ...rest }) => {
|
||||
const oldFrame = document.getElementById(element.id);
|
||||
if (oldFrame) {
|
||||
element.className = oldFrame.className || hiddenFrameClassname;
|
||||
element.className = oldFrame.className || hiddenFrameClassName;
|
||||
oldFrame.parentNode.replaceChild(element, oldFrame);
|
||||
} else {
|
||||
element.className = hiddenFrameClassname;
|
||||
element.className = hiddenFrameClassName;
|
||||
document.body.appendChild(element);
|
||||
}
|
||||
return {
|
||||
@ -85,33 +74,6 @@ const mountFrame = document => ({ element, ...rest }) => {
|
||||
};
|
||||
};
|
||||
|
||||
const addDepsToDocument = ctx => {
|
||||
ctx.document.__deps__ = {
|
||||
rx: {
|
||||
of,
|
||||
from,
|
||||
Observable,
|
||||
throwError,
|
||||
queueScheduler,
|
||||
tap,
|
||||
map,
|
||||
toArray,
|
||||
delay,
|
||||
mergeMap,
|
||||
timeout,
|
||||
catchError
|
||||
},
|
||||
log: (...things) => console.log('from test frame', ...things)
|
||||
};
|
||||
// using require here prevents nodejs issues as loop-protect
|
||||
// is added to the window object by webpack and not available to
|
||||
// us server side.
|
||||
/* eslint-disable import/no-unresolved */
|
||||
ctx.document.loopProtect = require('loop-protect');
|
||||
/* eslint-enable import/no-unresolved */
|
||||
return ctx;
|
||||
};
|
||||
|
||||
const buildProxyConsole = proxyLogger => ctx => {
|
||||
const oldLog = ctx.window.console.log.bind(ctx.window.console);
|
||||
ctx.window.console.log = function proxyConsole(...args) {
|
||||
@ -122,7 +84,7 @@ const buildProxyConsole = proxyLogger => ctx => {
|
||||
};
|
||||
|
||||
const writeTestDepsToDocument = frameReady => ctx => {
|
||||
const { sources, checkChallengePayload } = ctx;
|
||||
const { sources } = ctx;
|
||||
// add enzyme
|
||||
// TODO: do programatically
|
||||
// TODO: webpack lazyload this
|
||||
@ -133,7 +95,6 @@ const writeTestDepsToDocument = frameReady => ctx => {
|
||||
ctx.document.__source = sources && 'index' in sources ? sources['index'] : '';
|
||||
// provide the file name and get the original source
|
||||
ctx.document.__getUserInput = fileName => toString(sources[fileName]);
|
||||
ctx.document.__checkChallengePayload = checkChallengePayload;
|
||||
ctx.document.__frameReady = frameReady;
|
||||
return ctx;
|
||||
};
|
||||
@ -150,19 +111,17 @@ const writeContentToFrame = ctx => {
|
||||
return ctx;
|
||||
};
|
||||
|
||||
export const createMainFramer = (document, state$) =>
|
||||
export const createMainFramer = (document, state) =>
|
||||
flow(
|
||||
createFrame(document, state$.value, mainId),
|
||||
createFrame(document, state, mainId),
|
||||
mountFrame(document),
|
||||
addDepsToDocument,
|
||||
writeContentToFrame
|
||||
);
|
||||
|
||||
export const createTestFramer = (document, state$, frameReady, proxyConsole) =>
|
||||
export const createTestFramer = (document, state, frameReady, proxyConsole) =>
|
||||
flow(
|
||||
createFrame(document, state$.value, testId),
|
||||
createFrame(document, state, testId),
|
||||
mountFrame(document),
|
||||
addDepsToDocument,
|
||||
writeTestDepsToDocument(frameReady),
|
||||
buildProxyConsole(proxyConsole),
|
||||
writeContentToFrame
|
||||
|
Reference in New Issue
Block a user