feat(tests): use client build pipeline and test runners for testing curriculum
This commit is contained in:
parent
08cfd986c4
commit
e063686fca
@ -1,4 +1,3 @@
|
|||||||
import { throwers } from '../rechallenge/throwers';
|
|
||||||
import { transformers } from '../rechallenge/transformers';
|
import { transformers } from '../rechallenge/transformers';
|
||||||
import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js';
|
import { cssToHtml, jsToHtml, concatHtml } from '../rechallenge/builders.js';
|
||||||
|
|
||||||
@ -27,9 +26,8 @@ const applyFunction = fn =>
|
|||||||
return newFile;
|
return newFile;
|
||||||
}
|
}
|
||||||
return file;
|
return file;
|
||||||
} catch {
|
} catch (error) {
|
||||||
// return { error };
|
return { ...file, error };
|
||||||
return file;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -55,7 +53,7 @@ export function buildDOMChallenge(files, meta = {}) {
|
|||||||
const { required = [], template = '' } = meta;
|
const { required = [], template = '' } = meta;
|
||||||
const finalRequires = [...globalRequires, ...required, ...frameRunner];
|
const finalRequires = [...globalRequires, ...required, ...frameRunner];
|
||||||
const toHtml = [jsToHtml, cssToHtml];
|
const toHtml = [jsToHtml, cssToHtml];
|
||||||
const pipeLine = composeFunctions(...throwers, ...transformers, ...toHtml);
|
const pipeLine = composeFunctions(...transformers, ...toHtml);
|
||||||
const finalFiles = Object.keys(files)
|
const finalFiles = Object.keys(files)
|
||||||
.map(key => files[key])
|
.map(key => files[key])
|
||||||
.map(pipeLine);
|
.map(pipeLine);
|
||||||
@ -68,7 +66,7 @@ export function buildDOMChallenge(files, meta = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildJSChallenge(files) {
|
export function buildJSChallenge(files) {
|
||||||
const pipeLine = composeFunctions(...throwers, ...transformers);
|
const pipeLine = composeFunctions(...transformers);
|
||||||
const finalFiles = Object.keys(files)
|
const finalFiles = Object.keys(files)
|
||||||
.map(key => files[key])
|
.map(key => files[key])
|
||||||
.map(pipeLine);
|
.map(pipeLine);
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { homeLocation } from '../../../../config/env.json';
|
|
||||||
|
|
||||||
class WorkerExecutor {
|
class WorkerExecutor {
|
||||||
constructor(workerName) {
|
constructor(workerName, location) {
|
||||||
this.workerName = workerName;
|
this.workerName = workerName;
|
||||||
this.worker = null;
|
this.worker = null;
|
||||||
this.observers = {};
|
this.observers = {};
|
||||||
|
this.location = location;
|
||||||
|
|
||||||
this.execute = this.execute.bind(this);
|
this.execute = this.execute.bind(this);
|
||||||
this.killWorker = this.killWorker.bind(this);
|
this.killWorker = this.killWorker.bind(this);
|
||||||
@ -13,7 +12,7 @@ class WorkerExecutor {
|
|||||||
|
|
||||||
getWorker() {
|
getWorker() {
|
||||||
if (this.worker === null) {
|
if (this.worker === null) {
|
||||||
this.worker = new Worker(`${homeLocation}/js/${this.workerName}.js`);
|
this.worker = new Worker(`${this.location}${this.workerName}.js`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.worker;
|
return this.worker;
|
||||||
@ -72,6 +71,6 @@ class WorkerExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createWorkerExecutor(workerName) {
|
export default function createWorkerExecutor(workerName, location = '/js/') {
|
||||||
return new WorkerExecutor(workerName);
|
return new WorkerExecutor(workerName, location);
|
||||||
}
|
}
|
||||||
|
4739
curriculum/package-lock.json
generated
4739
curriculum/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -19,6 +19,7 @@
|
|||||||
"lint": "eslint ./**/*.js --fix",
|
"lint": "eslint ./**/*.js --fix",
|
||||||
"repack": "babel-node ./repack.js",
|
"repack": "babel-node ./repack.js",
|
||||||
"semantic-release": "semantic-release",
|
"semantic-release": "semantic-release",
|
||||||
|
"pretest": "cd ../client && npm run build:frame-runner",
|
||||||
"test": "mocha --delay --reporter progress --bail",
|
"test": "mocha --delay --reporter progress --bail",
|
||||||
"unpack": "babel-node ./unpack.js"
|
"unpack": "babel-node ./unpack.js"
|
||||||
},
|
},
|
||||||
@ -26,6 +27,10 @@
|
|||||||
"invariant": "^2.2.4"
|
"invariant": "^2.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.2.2",
|
||||||
|
"@babel/register": "^7.0.0",
|
||||||
|
"@babel/polyfill": "^7.2.5",
|
||||||
|
"@babel/preset-env": "^7.2.3",
|
||||||
"@commitlint/cli": "^7.0.0",
|
"@commitlint/cli": "^7.0.0",
|
||||||
"@commitlint/config-conventional": "^7.0.1",
|
"@commitlint/config-conventional": "^7.0.1",
|
||||||
"@commitlint/travis-cli": "^7.0.0",
|
"@commitlint/travis-cli": "^7.0.0",
|
||||||
@ -44,8 +49,6 @@
|
|||||||
"babel-standalone": "^6.26.0",
|
"babel-standalone": "^6.26.0",
|
||||||
"browserify": "^16.2.2",
|
"browserify": "^16.2.2",
|
||||||
"chai": "4.2.0",
|
"chai": "4.2.0",
|
||||||
"enzyme": "^3.7.0",
|
|
||||||
"enzyme-adapter-react-16": "^1.6.0",
|
|
||||||
"eslint": "^4.19.1",
|
"eslint": "^4.19.1",
|
||||||
"eslint-config-freecodecamp": "^1.1.1",
|
"eslint-config-freecodecamp": "^1.1.1",
|
||||||
"eslint-plugin-import": "^2.11.0",
|
"eslint-plugin-import": "^2.11.0",
|
||||||
@ -59,18 +62,13 @@
|
|||||||
"js-yaml": "^3.12.0",
|
"js-yaml": "^3.12.0",
|
||||||
"jsdom": "^12.2.0",
|
"jsdom": "^12.2.0",
|
||||||
"lint-staged": "^7.2.0",
|
"lint-staged": "^7.2.0",
|
||||||
|
"live-server": "^1.2.1",
|
||||||
"lodash": "^4.17.10",
|
"lodash": "^4.17.10",
|
||||||
"mocha": "5.2.0",
|
"mocha": "5.2.0",
|
||||||
"node-sass": "4.9.4",
|
|
||||||
"prettier": "^1.13.5",
|
"prettier": "^1.13.5",
|
||||||
"prettier-package-json": "^1.6.0",
|
"prettier-package-json": "^1.6.0",
|
||||||
"puppeteer": "1.11.0",
|
"puppeteer": "1.11.0",
|
||||||
"react": "^16.5.2",
|
|
||||||
"react-dom": "^16.5.2",
|
|
||||||
"react-redux": "^5.0.7",
|
|
||||||
"readdirp-walk": "^1.6.0",
|
"readdirp-walk": "^1.6.0",
|
||||||
"redux": "^4.0.1",
|
|
||||||
"redux-thunk": "^2.3.0",
|
|
||||||
"rx": "^4.1.0",
|
"rx": "^4.1.0",
|
||||||
"semantic-release": "^15.6.0",
|
"semantic-release": "^15.6.0",
|
||||||
"validator": "^10.4.0"
|
"validator": "^10.4.0"
|
||||||
|
0
curriculum/test/stubs/index.html
Normal file
0
curriculum/test/stubs/index.html
Normal file
@ -1,23 +1,38 @@
|
|||||||
/* global browser, page */
|
const path = require('path');
|
||||||
|
const liveServer = require('live-server');
|
||||||
|
|
||||||
|
const spinner = require('ora')();
|
||||||
|
|
||||||
|
const clientPath = path.resolve(__dirname, '../../client');
|
||||||
|
require('@babel/polyfill');
|
||||||
|
require('@babel/register')({
|
||||||
|
root: clientPath,
|
||||||
|
babelrc: false,
|
||||||
|
presets: ['@babel/preset-env'],
|
||||||
|
ignore: [/node_modules/],
|
||||||
|
only: [clientPath]
|
||||||
|
});
|
||||||
|
|
||||||
|
const createPseudoWorker = require('./utils/pseudo-worker');
|
||||||
|
const {
|
||||||
|
default: createWorker
|
||||||
|
} = require('../../client/src/templates/Challenges/utils/worker-executor');
|
||||||
|
|
||||||
const { assert, AssertionError } = require('chai');
|
const { assert, AssertionError } = require('chai');
|
||||||
const Mocha = require('mocha');
|
const Mocha = require('mocha');
|
||||||
|
|
||||||
const { flatten } = require('lodash');
|
const { flatten } = require('lodash');
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
const jsdom = require('jsdom');
|
||||||
|
|
||||||
|
const dom = new jsdom.JSDOM('');
|
||||||
|
global.document = dom.window.document;
|
||||||
|
|
||||||
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
|
require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
|
||||||
|
|
||||||
const vm = require('vm');
|
const vm = require('vm');
|
||||||
|
|
||||||
const puppeteer = require('puppeteer');
|
const puppeteer = require('puppeteer');
|
||||||
|
|
||||||
const jsdom = require('jsdom');
|
|
||||||
const jQuery = require('jquery');
|
|
||||||
const Sass = require('node-sass');
|
|
||||||
const Babel = require('babel-standalone');
|
|
||||||
const presetEnv = require('babel-preset-env');
|
|
||||||
const presetReact = require('babel-preset-react');
|
|
||||||
|
|
||||||
const { getChallengesForLang } = require('../getChallenges');
|
const { getChallengesForLang } = require('../getChallenges');
|
||||||
|
|
||||||
const MongoIds = require('./utils/mongoIds');
|
const MongoIds = require('./utils/mongoIds');
|
||||||
@ -27,6 +42,15 @@ const { challengeTypes } = require('../../client/utils/challengeTypes');
|
|||||||
|
|
||||||
const { supportedLangs } = require('../utils');
|
const { supportedLangs } = require('../utils');
|
||||||
|
|
||||||
|
const {
|
||||||
|
buildDOMChallenge,
|
||||||
|
buildJSChallenge
|
||||||
|
} = require('../../client/src/templates/Challenges/utils/build');
|
||||||
|
|
||||||
|
const {
|
||||||
|
createPoly
|
||||||
|
} = require('../../client/src/templates/Challenges/utils/polyvinyl');
|
||||||
|
|
||||||
const oldRunnerFail = Mocha.Runner.prototype.fail;
|
const oldRunnerFail = Mocha.Runner.prototype.fail;
|
||||||
Mocha.Runner.prototype.fail = function(test, err) {
|
Mocha.Runner.prototype.fail = function(test, err) {
|
||||||
if (err instanceof AssertionError) {
|
if (err instanceof AssertionError) {
|
||||||
@ -43,17 +67,18 @@ Mocha.Runner.prototype.fail = function(test, err) {
|
|||||||
return oldRunnerFail.call(this, test, err);
|
return oldRunnerFail.call(this, test, err);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { JSDOM } = jsdom;
|
async function newPageContext(browser) {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
// it's needed for workers as context.
|
||||||
|
await page.goto('http://127.0.0.1:8080/index.html');
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
const babelOptions = {
|
spinner.start();
|
||||||
plugins: ['transform-runtime'],
|
spinner.text = 'Populate tests.';
|
||||||
presets: [presetEnv, presetReact]
|
|
||||||
};
|
|
||||||
|
|
||||||
const jQueryScript = fs.readFileSync(
|
let browser;
|
||||||
path.resolve('./node_modules/jquery/dist/jquery.slim.min.js'),
|
let page;
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
|
|
||||||
runTests();
|
runTests();
|
||||||
|
|
||||||
@ -66,13 +91,47 @@ async function runTests() {
|
|||||||
testLangs = testLangs.filter(lang => filterLangs.includes(lang));
|
testLangs = testLangs.filter(lang => filterLangs.includes(lang));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(testLangs.map(lang => populateTestsForLang(lang)));
|
const challenges = await Promise.all(
|
||||||
|
testLangs.map(lang => getChallenges(lang))
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('Check challenges', function() {
|
||||||
|
before(async function() {
|
||||||
|
spinner.text = 'Testing';
|
||||||
|
this.timeout(50000);
|
||||||
|
liveServer.start({
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: '8080',
|
||||||
|
root: path.resolve(__dirname, 'stubs'),
|
||||||
|
mount: [['/js', path.join(clientPath, 'static/js')]],
|
||||||
|
open: false,
|
||||||
|
logLevel: 0
|
||||||
|
});
|
||||||
|
browser = await puppeteer.launch({
|
||||||
|
args: ['--no-sandbox']
|
||||||
|
// dumpio: true
|
||||||
|
});
|
||||||
|
global.Worker = createPseudoWorker(await newPageContext(browser));
|
||||||
|
page = await newPageContext(browser);
|
||||||
|
await page.setViewport({ width: 300, height: 150 });
|
||||||
|
});
|
||||||
|
after(async function() {
|
||||||
|
this.timeout(30000);
|
||||||
|
if (browser) {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
liveServer.shutdown();
|
||||||
|
spinner.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
challenges.forEach(populateTestsForLang);
|
||||||
|
});
|
||||||
|
|
||||||
run();
|
run();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function populateTestsForLang(lang) {
|
async function getChallenges(lang) {
|
||||||
const allChallenges = await getChallengesForLang(lang).then(curriculum =>
|
const challenges = await getChallengesForLang(lang).then(curriculum =>
|
||||||
Object.keys(curriculum)
|
Object.keys(curriculum)
|
||||||
.map(key => curriculum[key].blocks)
|
.map(key => curriculum[key].blocks)
|
||||||
.reduce((challengeArray, superBlock) => {
|
.reduce((challengeArray, superBlock) => {
|
||||||
@ -82,28 +141,19 @@ async function populateTestsForLang(lang) {
|
|||||||
return [...challengeArray, ...flatten(challengesForBlock)];
|
return [...challengeArray, ...flatten(challengesForBlock)];
|
||||||
}, [])
|
}, [])
|
||||||
);
|
);
|
||||||
|
return { lang, challenges };
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateTestsForLang({ lang, challenges }) {
|
||||||
const mongoIds = new MongoIds();
|
const mongoIds = new MongoIds();
|
||||||
const challengeTitles = new ChallengeTitles();
|
const challengeTitles = new ChallengeTitles();
|
||||||
const validateChallenge = challengeSchemaValidator(lang);
|
const validateChallenge = challengeSchemaValidator(lang);
|
||||||
|
|
||||||
describe(`Check challenges (${lang})`, async function() {
|
describe(`Check challenges (${lang})`, function() {
|
||||||
before(async function() {
|
|
||||||
this.timeout(30000);
|
|
||||||
global.browser = await puppeteer.launch({ args: ['--no-sandbox'] });
|
|
||||||
global.page = await browser.newPage();
|
|
||||||
await page.setViewport({ width: 300, height: 150 });
|
|
||||||
});
|
|
||||||
after(async function() {
|
|
||||||
if (global.browser) {
|
|
||||||
await browser.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.timeout(5000);
|
this.timeout(5000);
|
||||||
|
|
||||||
allChallenges.forEach(challenge => {
|
challenges.forEach(challenge => {
|
||||||
describe(challenge.title || 'No title', async function() {
|
describe(challenge.title || 'No title', function() {
|
||||||
it('Common checks', function() {
|
it('Common checks', function() {
|
||||||
const result = validateChallenge(challenge);
|
const result = validateChallenge(challenge);
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
@ -140,38 +190,26 @@ async function populateTestsForLang(lang) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const { files = [], required = [] } = challenge;
|
let { files = [] } = challenge;
|
||||||
const exts = Array.from(new Set(files.map(({ ext }) => ext)));
|
|
||||||
const groupedFiles = exts.reduce((result, ext) => {
|
|
||||||
const file = files.filter(file => file.ext === ext).reduce(
|
|
||||||
(result, file) => ({
|
|
||||||
head: result.head + '\n' + file.head,
|
|
||||||
contents: result.contents + '\n' + file.contents,
|
|
||||||
tail: result.tail + '\n' + file.tail
|
|
||||||
}),
|
|
||||||
{ head: '', contents: '', tail: '' }
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
[ext]: file
|
|
||||||
};
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
let evaluateTest;
|
let evaluateTest;
|
||||||
if (
|
if (challengeType === challengeTypes.backend) {
|
||||||
challengeType === challengeTypes.modern &&
|
it('Check tests is not implemented.');
|
||||||
(groupedFiles.js || groupedFiles.jsx)
|
return;
|
||||||
|
} else if (
|
||||||
|
challengeType === challengeTypes.js ||
|
||||||
|
challengeType === challengeTypes.bonfire
|
||||||
) {
|
) {
|
||||||
evaluateTest = evaluateReactReduxTest;
|
|
||||||
} else if (groupedFiles.html) {
|
|
||||||
evaluateTest = evaluateHtmlTest;
|
|
||||||
} else if (groupedFiles.js) {
|
|
||||||
evaluateTest = evaluateJsTest;
|
evaluateTest = evaluateJsTest;
|
||||||
|
} else if (files.length === 1) {
|
||||||
|
evaluateTest = evaluateHtmlTest;
|
||||||
} else {
|
} else {
|
||||||
it('Check tests. Unknown file type.');
|
it('Check tests.', () => {
|
||||||
|
throw new Error('Seed file should be only the one.');
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
files = files.map(createPoly);
|
||||||
it('Test suite must fail on the initial contents', async function() {
|
it('Test suite must fail on the initial contents', async function() {
|
||||||
this.timeout(20000);
|
this.timeout(20000);
|
||||||
// suppress errors in the console.
|
// suppress errors in the console.
|
||||||
@ -180,12 +218,14 @@ async function populateTestsForLang(lang) {
|
|||||||
let fails = (await Promise.all(
|
let fails = (await Promise.all(
|
||||||
tests.map(async function(test) {
|
tests.map(async function(test) {
|
||||||
try {
|
try {
|
||||||
await evaluateTest({
|
await evaluateTest(
|
||||||
challengeType,
|
{
|
||||||
required,
|
...challenge,
|
||||||
files: groupedFiles,
|
files,
|
||||||
test
|
test
|
||||||
});
|
},
|
||||||
|
page
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return true;
|
return true;
|
||||||
@ -207,18 +247,20 @@ async function populateTestsForLang(lang) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Check tests against solutions', async function() {
|
describe('Check tests against solutions', function() {
|
||||||
solutions.forEach((solution, index) => {
|
solutions.forEach((solution, index) => {
|
||||||
describe(`Solution ${index + 1}`, async function() {
|
describe(`Solution ${index + 1}`, function() {
|
||||||
tests.forEach(test => {
|
tests.forEach(test => {
|
||||||
it(test.text, async function() {
|
it(test.text, async function() {
|
||||||
await evaluateTest({
|
await evaluateTest(
|
||||||
challengeType,
|
{
|
||||||
solution,
|
...challenge,
|
||||||
required,
|
files,
|
||||||
files: groupedFiles,
|
solution,
|
||||||
test
|
test
|
||||||
});
|
},
|
||||||
|
page
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -229,314 +271,68 @@ async function populateTestsForLang(lang) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fake Deep Equal dependency
|
async function evaluateHtmlTest(
|
||||||
const DeepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
{ solution, required = [], template, files, test },
|
||||||
|
context
|
||||||
// Hardcode Deep Freeze dependency
|
) {
|
||||||
const DeepFreeze = o => {
|
if (solution) {
|
||||||
Object.freeze(o);
|
files[0].contents = solution;
|
||||||
Object.getOwnPropertyNames(o).forEach(function(prop) {
|
|
||||||
if (
|
|
||||||
o.hasOwnProperty(prop) &&
|
|
||||||
o[prop] !== null &&
|
|
||||||
(typeof o[prop] === 'object' || typeof o[prop] === 'function') &&
|
|
||||||
!Object.isFrozen(o[prop])
|
|
||||||
) {
|
|
||||||
DeepFreeze(o[prop]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return o;
|
|
||||||
};
|
|
||||||
|
|
||||||
function transformSass(solution) {
|
|
||||||
const fragment = JSDOM.fragment(`<div>${solution}</div>`);
|
|
||||||
const styleTags = fragment.querySelectorAll('style[type="text/sass"]');
|
|
||||||
if (styleTags.length > 0) {
|
|
||||||
styleTags.forEach(styleTag => {
|
|
||||||
styleTag.innerHTML = Sass.renderSync({ data: styleTag.innerHTML }).css;
|
|
||||||
styleTag.type = 'text/css';
|
|
||||||
});
|
|
||||||
return fragment.children[0].innerHTML;
|
|
||||||
}
|
}
|
||||||
return solution;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function evaluateHtmlTest({ solution, required, files, test }) {
|
const loadEnzyme = files[0].ext === 'jsx';
|
||||||
const { head = '', contents = '', tail = '' } = files.html;
|
|
||||||
if (!solution) {
|
|
||||||
solution = contents;
|
|
||||||
}
|
|
||||||
const code = solution;
|
|
||||||
|
|
||||||
const links = required
|
const { build, sources } = await buildDOMChallenge(files, {
|
||||||
.map(({ link, src }) => {
|
required,
|
||||||
if (link && src) {
|
template
|
||||||
throw new Error(`
|
|
||||||
A required file can not have both a src and a link: src = ${src}, link = ${link}
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
if (src) {
|
|
||||||
return `<script src='${src}' type='text/javascript'></script>`;
|
|
||||||
}
|
|
||||||
if (link) {
|
|
||||||
return `<link href='${link}' rel='stylesheet' />`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
})
|
|
||||||
.reduce((head, required) => head.concat(required), '');
|
|
||||||
|
|
||||||
const scripts = `
|
|
||||||
<head>
|
|
||||||
${links}
|
|
||||||
</head>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const sandbox = { solution, transformSass };
|
|
||||||
const context = vm.createContext(sandbox);
|
|
||||||
vm.runInContext('solution = transformSass(solution);', context, {
|
|
||||||
timeout: 2000
|
|
||||||
});
|
});
|
||||||
solution = sandbox.solution;
|
|
||||||
|
|
||||||
await preparePageToTest();
|
await context.reload();
|
||||||
|
await context.setContent(build);
|
||||||
|
|
||||||
await page.setContent(
|
const result = await context.evaluate(
|
||||||
`
|
async(sources, testString, loadEnzyme) => {
|
||||||
<!doctype html>
|
document.__source = sources && 'index' in sources ? sources['index'] : '';
|
||||||
<html>
|
document.__getUserInput = fileName => sources[fileName];
|
||||||
${scripts}
|
document.__frameReady = () => {};
|
||||||
${head}
|
document.__loadEnzyme = loadEnzyme;
|
||||||
${solution}
|
await document.__initTestFrame();
|
||||||
${tail}
|
|
||||||
</html>
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
await runTestInBrowser(code, test.testString);
|
const { pass, err } = await document.__runTest(testString);
|
||||||
}
|
if (pass) {
|
||||||
|
return true;
|
||||||
async function preparePageToTest() {
|
} else {
|
||||||
await page.reload();
|
return { ...err };
|
||||||
await page.addScriptTag({
|
|
||||||
url: 'https://cdnjs.cloudflare.com/ajax/libs/chai/4.2.0/chai.min.js'
|
|
||||||
});
|
|
||||||
await page.evaluate(() => {
|
|
||||||
window.assert = window.chai.assert;
|
|
||||||
});
|
|
||||||
await page.evaluate(jQueryScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runTestInBrowser(code, testString) {
|
|
||||||
const result = await page.evaluate(
|
|
||||||
async function(code, testString) {
|
|
||||||
/* eslint-disable no-unused-vars */
|
|
||||||
// Fake Deep Equal dependency
|
|
||||||
const DeepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
||||||
|
|
||||||
// Hardcode Deep Freeze dependency
|
|
||||||
const DeepFreeze = o => {
|
|
||||||
Object.freeze(o);
|
|
||||||
Object.getOwnPropertyNames(o).forEach(function(prop) {
|
|
||||||
if (
|
|
||||||
o.hasOwnProperty(prop) &&
|
|
||||||
o[prop] !== null &&
|
|
||||||
(typeof o[prop] === 'object' || typeof o[prop] === 'function') &&
|
|
||||||
!Object.isFrozen(o[prop])
|
|
||||||
) {
|
|
||||||
DeepFreeze(o[prop]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return o;
|
|
||||||
};
|
|
||||||
|
|
||||||
const editor = {
|
|
||||||
getValue() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/* eslint-enable no-unused-vars */
|
|
||||||
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-eval
|
|
||||||
const test = eval(testString);
|
|
||||||
if (typeof test === 'function') {
|
|
||||||
await test(() => code);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
message: e.message,
|
|
||||||
isAssertion: e instanceof window.chai.AssertionError
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
code,
|
sources,
|
||||||
testString
|
test.testString,
|
||||||
|
loadEnzyme
|
||||||
);
|
);
|
||||||
if (result !== true) {
|
if (result !== true) {
|
||||||
throw result.isAssertion
|
throw AssertionError(result.message);
|
||||||
? new AssertionError(result.message)
|
|
||||||
: new Error(result.message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function evaluateJsTest({ solution, files, test }) {
|
async function evaluateJsTest({ solution, files, test }) {
|
||||||
const virtualConsole = new jsdom.VirtualConsole();
|
if (solution) {
|
||||||
const dom = new JSDOM('', { runScripts: 'dangerously', virtualConsole });
|
files[0].contents = solution;
|
||||||
|
|
||||||
const { head = '', contents = '', tail = '' } = files.js;
|
|
||||||
let scriptString = '';
|
|
||||||
if (!solution) {
|
|
||||||
solution = contents;
|
|
||||||
try {
|
|
||||||
scriptString =
|
|
||||||
Babel.transform(head, babelOptions).code +
|
|
||||||
'\n' +
|
|
||||||
Babel.transform(contents, babelOptions).code +
|
|
||||||
'\n' +
|
|
||||||
Babel.transform(tail, babelOptions).code +
|
|
||||||
'\n';
|
|
||||||
} catch (e) {
|
|
||||||
scriptString = '';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
scriptString =
|
|
||||||
Babel.transform(head, babelOptions).code +
|
|
||||||
'\n' +
|
|
||||||
Babel.transform(solution, babelOptions).code +
|
|
||||||
'\n' +
|
|
||||||
Babel.transform(tail, babelOptions).code +
|
|
||||||
'\n';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dom.window.require = require;
|
const { build, sources } = await buildJSChallenge(files);
|
||||||
dom.window.code = solution;
|
const code = sources && 'index' in sources ? sources['index'] : '';
|
||||||
await runTestInJsdom(dom, test.testString, scriptString);
|
const script = build + '\n' + test.testString;
|
||||||
}
|
|
||||||
|
|
||||||
async function evaluateReactReduxTest({ solution, files, test }) {
|
const testWorker = createWorker('test-evaluator');
|
||||||
let head = '',
|
|
||||||
tail = '';
|
|
||||||
if (files.js) {
|
|
||||||
const { head: headJs = '', tail: tailJs = '' } = files.js;
|
|
||||||
head += headJs + '\n';
|
|
||||||
tail += tailJs + '\n';
|
|
||||||
}
|
|
||||||
if (files.jsx) {
|
|
||||||
const { head: headJsx = '', tail: tailJsx = '' } = files.jsx;
|
|
||||||
head += headJsx + '\n';
|
|
||||||
tail += tailJsx + '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Transpile ALL the code
|
try {
|
||||||
* (we may use JSX in head or tail or tests, too): */
|
const { pass, err } = await testWorker.execute(
|
||||||
|
{ script, code, sources },
|
||||||
let scriptString = '';
|
5000
|
||||||
if (!solution) {
|
);
|
||||||
const contents =
|
if (!pass) {
|
||||||
(files.js ? files.js.contents || '' : '') +
|
throw new AssertionError(err.message);
|
||||||
(files.jsx ? files.jsx.contents || '' : '');
|
|
||||||
solution = contents;
|
|
||||||
try {
|
|
||||||
scriptString =
|
|
||||||
Babel.transform(head, babelOptions).code +
|
|
||||||
'\n' +
|
|
||||||
Babel.transform(contents, babelOptions).code +
|
|
||||||
'\n' +
|
|
||||||
Babel.transform(tail, babelOptions).code +
|
|
||||||
'\n';
|
|
||||||
} catch (e) {
|
|
||||||
scriptString = '';
|
|
||||||
}
|
}
|
||||||
} else {
|
} finally {
|
||||||
scriptString =
|
testWorker.killWorker();
|
||||||
Babel.transform(head, babelOptions).code +
|
|
||||||
'\n' +
|
|
||||||
Babel.transform(solution, babelOptions).code +
|
|
||||||
'\n' +
|
|
||||||
Babel.transform(tail, babelOptions).code +
|
|
||||||
'\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = solution;
|
|
||||||
|
|
||||||
const virtualConsole = new jsdom.VirtualConsole();
|
|
||||||
// Mock DOM document for ReactDOM.render method
|
|
||||||
const dom = new JSDOM(
|
|
||||||
`
|
|
||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<div id="root"><div id="challenge-node"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`,
|
|
||||||
{
|
|
||||||
runScripts: 'dangerously',
|
|
||||||
virtualConsole,
|
|
||||||
url: 'http://localhost'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { window } = dom;
|
|
||||||
const document = window.document;
|
|
||||||
|
|
||||||
global.window = window;
|
|
||||||
global.document = document;
|
|
||||||
|
|
||||||
global.navigator = {
|
|
||||||
userAgent: 'node.js'
|
|
||||||
};
|
|
||||||
global.requestAnimationFrame = callback => setTimeout(callback, 0);
|
|
||||||
global.cancelAnimationFrame = id => clearTimeout(id);
|
|
||||||
|
|
||||||
// Provide dependencies, just provide all of them
|
|
||||||
dom.window.React = require('react');
|
|
||||||
dom.window.ReactDOM = require('react-dom');
|
|
||||||
dom.window.PropTypes = require('prop-types');
|
|
||||||
dom.window.Redux = require('redux');
|
|
||||||
dom.window.ReduxThunk = require('redux-thunk');
|
|
||||||
dom.window.ReactRedux = require('react-redux');
|
|
||||||
dom.window.Enzyme = require('enzyme');
|
|
||||||
const Adapter16 = require('enzyme-adapter-react-16');
|
|
||||||
dom.window.Enzyme.configure({ adapter: new Adapter16() });
|
|
||||||
|
|
||||||
dom.window.require = require;
|
|
||||||
dom.window.code = code;
|
|
||||||
dom.window.editor = {
|
|
||||||
getValue() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await runTestInJsdom(dom, test.testString, scriptString);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runTestInJsdom(dom, testString, scriptString = '') {
|
|
||||||
// jQuery used by tests
|
|
||||||
jQuery(dom.window);
|
|
||||||
|
|
||||||
dom.window.assert = assert;
|
|
||||||
dom.window.DeepEqual = DeepEqual;
|
|
||||||
dom.window.DeepFreeze = DeepFreeze;
|
|
||||||
|
|
||||||
dom.window.__test = testString;
|
|
||||||
scriptString += `;
|
|
||||||
window.__result =
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const testResult = eval(__test);
|
|
||||||
if (typeof testResult === 'function') {
|
|
||||||
await testResult(() => code);
|
|
||||||
}
|
|
||||||
}catch (e) {
|
|
||||||
window.__error = e;
|
|
||||||
}
|
|
||||||
})();`;
|
|
||||||
const script = new vm.Script(scriptString);
|
|
||||||
dom.runVMScript(script, { timeout: 5000 });
|
|
||||||
await dom.window.__result;
|
|
||||||
if (dom.window.__error) {
|
|
||||||
throw dom.window.__error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
85
curriculum/test/utils/pseudo-worker.js
Normal file
85
curriculum/test/utils/pseudo-worker.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
function createPseudoWorker(context) {
|
||||||
|
class PseudoWorker {
|
||||||
|
constructor(path) {
|
||||||
|
this.terminated = false;
|
||||||
|
this.worker = context.evaluateHandle(path => new Worker(path), path);
|
||||||
|
this.listenToWorker('onmessage');
|
||||||
|
this.listenToWorker('onerror');
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate() {
|
||||||
|
this.terminated = true;
|
||||||
|
this.worker.then(worker =>
|
||||||
|
context.evaluate(worker => worker.terminate(), worker)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
listenToWorker(eventName) {
|
||||||
|
this.worker.then(async worker => {
|
||||||
|
const producer = await context.evaluateHandle(
|
||||||
|
(worker, eventName) => {
|
||||||
|
let callback;
|
||||||
|
const queue = [];
|
||||||
|
function send(event) {
|
||||||
|
if (!queue.length && callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
queue.push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
worker[eventName] = e => send(e);
|
||||||
|
|
||||||
|
const resolver = resolve => (callback = resolve);
|
||||||
|
async function* produce() {
|
||||||
|
while (true) {
|
||||||
|
while (queue.length) {
|
||||||
|
yield queue.shift();
|
||||||
|
}
|
||||||
|
await new Promise(resolver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return produce();
|
||||||
|
},
|
||||||
|
worker,
|
||||||
|
eventName
|
||||||
|
);
|
||||||
|
while (!this.terminated) {
|
||||||
|
try {
|
||||||
|
const data = await context.evaluate(
|
||||||
|
producer =>
|
||||||
|
producer
|
||||||
|
.next()
|
||||||
|
.then(({ value: { data, message } }) => ({ data, message })),
|
||||||
|
producer
|
||||||
|
);
|
||||||
|
if (this[eventName]) {
|
||||||
|
this[eventName](data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async postMessage(msg) {
|
||||||
|
if (this.terminated) {
|
||||||
|
throw new Error('Worker is terminated.');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.worker.then(worker =>
|
||||||
|
worker
|
||||||
|
.executionContext()
|
||||||
|
.evaluate((worker, msg) => worker.postMessage(msg), worker, msg)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (this.onerror) {
|
||||||
|
this.onerror({ message: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PseudoWorker;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createPseudoWorker;
|
Loading…
x
Reference in New Issue
Block a user