feat: execute js challenges saga
parent
2d3c2efa2a
commit
a4fd935b8b
|
@ -0,0 +1,35 @@
|
|||
/* global chai, importScripts */
|
||||
importScripts('https://cdnjs.cloudflare.com/ajax/libs/chai/4.2.0/chai.min.js');
|
||||
|
||||
const oldLog = self.console.log.bind(self.console);
|
||||
self.console.log = function proxyConsole(...args) {
|
||||
self.__logs = [...self.__logs, ...args];
|
||||
return oldLog(...args);
|
||||
};
|
||||
|
||||
onmessage = async e => {
|
||||
self.__logs = [];
|
||||
const { script: __test, code } = e.data;
|
||||
/* eslint-disable no-unused-vars */
|
||||
const assert = chai.assert;
|
||||
// Fake Deep Equal dependency
|
||||
const DeepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
||||
/* eslint-enable no-unused-vars */
|
||||
try {
|
||||
// eslint-disable-next-line no-eval
|
||||
const testResult = eval(__test);
|
||||
if (typeof testResult === 'function') {
|
||||
await testResult(() => code);
|
||||
}
|
||||
self.postMessage({ pass: true, logs: self.__logs.map(String) });
|
||||
} catch (err) {
|
||||
self.postMessage({
|
||||
err: {
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
isAssertionError: err instanceof chai.AssertionError
|
||||
},
|
||||
logs: self.__logs.map(String)
|
||||
});
|
||||
}
|
||||
};
|
|
@ -21,24 +21,31 @@ import WorkerExecutor from '../utils/worker-executor';
|
|||
const protectTimeout = 100;
|
||||
Babel.registerPlugin('loopProtection', protect(protectTimeout));
|
||||
|
||||
const babelOptions = {
|
||||
const babelOptionsJSX = {
|
||||
plugins: ['loopProtection'],
|
||||
presets: [presetEnv, presetReact]
|
||||
};
|
||||
const babelTransformCode = code => Babel.transform(code, babelOptions).code;
|
||||
|
||||
const babelOptionsJS = {
|
||||
presets: [presetEnv]
|
||||
};
|
||||
|
||||
const babelTransformCode = options => code =>
|
||||
Babel.transform(code, options).code;
|
||||
|
||||
// const sourceReg =
|
||||
// /(<!-- fcc-start-source -->)([\s\S]*?)(?=<!-- fcc-end-source -->)/g;
|
||||
const NBSPReg = new RegExp(String.fromCharCode(160), 'g');
|
||||
|
||||
const isJS = matchesProperty('ext', 'js');
|
||||
const testJS = matchesProperty('ext', 'js');
|
||||
const testJSX = matchesProperty('ext', 'jsx');
|
||||
const testHTML = matchesProperty('ext', 'html');
|
||||
const testHTMLJS = overSome(isJS, testHTML);
|
||||
export const testJS$JSX = overSome(isJS, matchesProperty('ext', 'jsx'));
|
||||
const testHTML$JS$JSX = overSome(testHTML, testJS, testJSX);
|
||||
export const testJS$JSX = overSome(testJS, testJSX);
|
||||
|
||||
export const replaceNBSP = cond([
|
||||
[
|
||||
testHTMLJS,
|
||||
testHTML$JS$JSX,
|
||||
partial(vinyl.transformContents, contents =>
|
||||
contents.replace(NBSPReg, ' ')
|
||||
)
|
||||
|
@ -63,11 +70,20 @@ function tryTransform(wrap = identity) {
|
|||
|
||||
export const babelTransformer = cond([
|
||||
[
|
||||
testJS$JSX,
|
||||
testJS,
|
||||
flow(
|
||||
partial(
|
||||
vinyl.transformHeadTailAndContents,
|
||||
tryTransform(babelTransformCode)
|
||||
tryTransform(babelTransformCode(babelOptionsJS))
|
||||
)
|
||||
)
|
||||
],
|
||||
[
|
||||
testJSX,
|
||||
flow(
|
||||
partial(
|
||||
vinyl.transformHeadTailAndContents,
|
||||
tryTransform(babelTransformCode(babelOptionsJSX))
|
||||
),
|
||||
partial(vinyl.setExt, 'js')
|
||||
)
|
||||
|
@ -82,12 +98,12 @@ const htmlSassTransformCode = file => {
|
|||
div.innerHTML = file.contents;
|
||||
const styleTags = div.querySelectorAll('style[type="text/sass"]');
|
||||
if (styleTags.length > 0) {
|
||||
return Promise.all([].map.call(styleTags, async style => {
|
||||
style.type = 'text/css';
|
||||
style.innerHTML = await sassWorker.execute(style.innerHTML, 2000);
|
||||
})).then(() => (
|
||||
vinyl.transformContents(() => div.innerHTML, file)
|
||||
));
|
||||
return Promise.all(
|
||||
[].map.call(styleTags, async style => {
|
||||
style.type = 'text/css';
|
||||
style.innerHTML = await sassWorker.execute(style.innerHTML, 2000);
|
||||
})
|
||||
).then(() => vinyl.transformContents(() => div.innerHTML, file));
|
||||
}
|
||||
return vinyl.transformContents(() => div.innerHTML, file);
|
||||
};
|
||||
|
|
|
@ -44,6 +44,13 @@ const executeDebounceTimeout = 750;
|
|||
function updateMainEpic(action$, state$, { document }) {
|
||||
return action$.pipe(
|
||||
ofType(types.updateFile, types.challengeMounted),
|
||||
filter(() => {
|
||||
const { challengeType } = challengeMetaSelector(state$.value);
|
||||
return (
|
||||
challengeType !== challengeTypes.js &&
|
||||
challengeType !== challengeTypes.bonfire
|
||||
);
|
||||
}),
|
||||
debounceTime(executeDebounceTimeout),
|
||||
switchMap(() => {
|
||||
const frameMain = createMainFramer(document, state$);
|
||||
|
@ -77,6 +84,8 @@ function executeChallengeEpic(action$, state$, { document }) {
|
|||
consoleProxy
|
||||
);
|
||||
const challengeResults = frameReady.pipe(
|
||||
// Delay for jQuery ready code, in jQuery challenges
|
||||
delay(250),
|
||||
pluck('checkChallengePayload'),
|
||||
map(checkChallengePayload => ({
|
||||
checkChallengePayload,
|
||||
|
@ -103,6 +112,13 @@ 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
|
||||
);
|
||||
}),
|
||||
debounceTime(executeDebounceTimeout),
|
||||
filter(() => isJSEnabledSelector(state$.value)),
|
||||
switchMap(() => {
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import { takeEvery, put, select, call } from 'redux-saga/effects';
|
||||
|
||||
import {
|
||||
challengeMetaSelector,
|
||||
challengeTestsSelector,
|
||||
initConsole,
|
||||
updateConsole,
|
||||
initLogs,
|
||||
updateLogs,
|
||||
logsToConsole,
|
||||
checkChallenge,
|
||||
updateTests,
|
||||
challengeFilesSelector
|
||||
} from './';
|
||||
|
||||
import { buildJSFromFiles } from '../utils/build';
|
||||
|
||||
import { challengeTypes } from '../../../../utils/challengeTypes';
|
||||
|
||||
import WorkerExecutor from '../utils/worker-executor';
|
||||
|
||||
const testWorker = new WorkerExecutor('test-evaluator');
|
||||
const testTimeout = 5000;
|
||||
|
||||
function* ExecuteJSChallengeSaga() {
|
||||
const { challengeType } = yield select(challengeMetaSelector);
|
||||
const { js, bonfire } = challengeTypes;
|
||||
if (challengeType !== js && challengeType !== bonfire) {
|
||||
return;
|
||||
}
|
||||
yield put(initLogs());
|
||||
yield put(initConsole('// running tests'));
|
||||
try {
|
||||
const files = yield select(challengeFilesSelector);
|
||||
const { code, solution } = yield call(buildJSFromFiles, files);
|
||||
const tests = yield select(challengeTestsSelector);
|
||||
const testResults = [];
|
||||
for (const { text, testString } of tests) {
|
||||
const newTest = { text, testString };
|
||||
const { pass, err, logs } = yield call(
|
||||
testWorker.execute,
|
||||
{ script: solution + '\n' + testString, code },
|
||||
testTimeout
|
||||
);
|
||||
if (pass) {
|
||||
newTest.pass = true;
|
||||
} else {
|
||||
const { message, stack, isAssertionError } = err;
|
||||
newTest.err = message + '\n' + stack;
|
||||
newTest.stack = stack;
|
||||
newTest.message = text.replace(/<code>(.*?)<\/code>/g, '$1');
|
||||
yield put(updateConsole(newTest.message));
|
||||
if (!isAssertionError) {
|
||||
console.warn(message);
|
||||
}
|
||||
}
|
||||
testResults.push(newTest);
|
||||
for (const log of logs) {
|
||||
yield put(updateLogs(log));
|
||||
}
|
||||
// kill worker for independent tests
|
||||
yield call(testWorker.killWorker);
|
||||
}
|
||||
yield put(updateTests(testResults));
|
||||
yield put(updateConsole('// tests completed'));
|
||||
yield put(logsToConsole('// console output'));
|
||||
yield put(checkChallenge());
|
||||
} catch (e) {
|
||||
if (e === 'timeout') {
|
||||
yield put(updateConsole('Test timed out'));
|
||||
} else {
|
||||
yield put(updateConsole(e));
|
||||
}
|
||||
} finally {
|
||||
yield call(testWorker.killWorker);
|
||||
}
|
||||
}
|
||||
|
||||
export function createExecuteChallengeSaga(types) {
|
||||
return [takeEvery(types.executeChallenge, ExecuteJSChallengeSaga)];
|
||||
}
|
|
@ -14,6 +14,7 @@ import codeStorageEpic from './code-storage-epic';
|
|||
import currentChallengeEpic from './current-challenge-epic';
|
||||
|
||||
import { createIdToNameMapSaga } from './id-to-name-map-saga';
|
||||
import { createExecuteChallengeSaga } from './execute-challenge-saga';
|
||||
|
||||
export const ns = 'challenge';
|
||||
export const backendNS = 'backendChallenge';
|
||||
|
@ -88,7 +89,10 @@ export const epics = [
|
|||
currentChallengeEpic
|
||||
];
|
||||
|
||||
export const sagas = [...createIdToNameMapSaga(types)];
|
||||
export const sagas = [
|
||||
...createIdToNameMapSaga(types),
|
||||
...createExecuteChallengeSaga(types)
|
||||
];
|
||||
|
||||
export const createFiles = createAction(types.createFiles, challengeFiles =>
|
||||
Object.keys(challengeFiles)
|
||||
|
|
|
@ -83,6 +83,37 @@ export function buildFromFiles(state) {
|
|||
return concatHtml(finalRequires, template, finalFiles);
|
||||
}
|
||||
|
||||
export function buildJSFromFiles(files) {
|
||||
const pipeLine = flow(
|
||||
applyFunctions(throwers),
|
||||
applyFunctions(transformers)
|
||||
);
|
||||
const finalFiles = Object.keys(files)
|
||||
.map(key => files[key])
|
||||
.map(pipeLine);
|
||||
const sourceMap = Promise.all(finalFiles).then(files =>
|
||||
files.reduce((sources, file) => {
|
||||
sources[file.name] = file.source || file.contents;
|
||||
return sources;
|
||||
}, {})
|
||||
);
|
||||
const body = Promise.all(finalFiles).then(files =>
|
||||
files
|
||||
.reduce(
|
||||
(body, file) => [
|
||||
...body,
|
||||
file.head + '\n' + file.contents + '\n' + file.tail
|
||||
],
|
||||
[]
|
||||
)
|
||||
.join('/n')
|
||||
);
|
||||
return Promise.all([body, sourceMap]).then(([body, sources]) => ({
|
||||
solution: body,
|
||||
code: sources && 'index' in sources ? sources['index'] : ''
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildBackendChallenge(state) {
|
||||
const {
|
||||
solution: { value: url }
|
||||
|
|
|
@ -2,6 +2,10 @@ export default class WorkerExecutor {
|
|||
constructor(workerName) {
|
||||
this.workerName = workerName;
|
||||
this.worker = null;
|
||||
|
||||
this.execute = this.execute.bind(this);
|
||||
this.killWorker = this.killWorker.bind(this);
|
||||
this.getWorker = this.getWorker.bind(this);
|
||||
}
|
||||
|
||||
getWorker() {
|
||||
|
|
|
@ -6,7 +6,8 @@ module.exports = (env = {}) => {
|
|||
mode: __DEV__ ? 'development' : 'production',
|
||||
entry: {
|
||||
'frame-runner': './src/client/frame-runner.js',
|
||||
'sass-compile': './src/client/workers/sass-compile.js'
|
||||
'sass-compile': './src/client/workers/sass-compile.js',
|
||||
'test-evaluator': './src/client/workers/test-evaluator.js'
|
||||
},
|
||||
devtool: __DEV__ ? 'inline-source-map' : 'source-map',
|
||||
output: {
|
||||
|
|
Loading…
Reference in New Issue