feat: execute js challenges saga

pull/25153/head^2
Valeriy 2018-11-26 02:17:38 +03:00 committed by Stuart Taylor
parent 2d3c2efa2a
commit a4fd935b8b
8 changed files with 204 additions and 16 deletions

View File

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

View File

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

View 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(() => {

View File

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

View File

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

View File

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

View File

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

View File

@ -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: {