2018-10-23 16:21:53 +03:00
|
|
|
/* eslint-disable no-process-exit, no-unused-vars */
|
2018-01-19 14:03:17 -05:00
|
|
|
|
2018-10-23 16:21:53 +03:00
|
|
|
const { Observable } = require('rx');
|
|
|
|
|
const tape = require('tape');
|
|
|
|
|
const { flatten } = require('lodash');
|
|
|
|
|
const vm = require('vm');
|
|
|
|
|
const path = require('path');
|
2018-10-24 14:15:00 +03:00
|
|
|
const fs = require('fs');
|
2018-10-23 16:21:53 +03:00
|
|
|
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
|
2018-01-29 16:11:27 +00:00
|
|
|
|
2018-10-24 14:15:00 +03:00
|
|
|
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 rework = require('rework');
|
|
|
|
|
const visit = require('rework-visit');
|
|
|
|
|
|
2018-10-23 16:21:53 +03:00
|
|
|
const { getChallengesForLang } = require('./getChallenges');
|
2018-05-21 14:56:49 +01:00
|
|
|
|
2018-10-23 16:21:53 +03:00
|
|
|
const MongoIds = require('./mongoIds');
|
|
|
|
|
const ChallengeTitles = require('./challengeTitles');
|
|
|
|
|
const addAssertsToTapTest = require('./addAssertsToTapTest');
|
|
|
|
|
const { validateChallenge } = require('./schema/challengeSchema');
|
|
|
|
|
const { challengeTypes } = require('../client/utils/challengeTypes');
|
2018-05-21 14:56:49 +01:00
|
|
|
|
2018-10-24 14:15:00 +03:00
|
|
|
const { LOCALE: lang = 'english' } = process.env;
|
|
|
|
|
|
2018-01-19 14:03:17 -05:00
|
|
|
let mongoIds = new MongoIds();
|
2018-04-11 05:23:36 +09:00
|
|
|
let challengeTitles = new ChallengeTitles();
|
2018-01-29 16:11:27 +00:00
|
|
|
|
2018-10-24 14:15:00 +03:00
|
|
|
const babelOptions = {
|
|
|
|
|
plugins: ['transform-runtime'],
|
|
|
|
|
presets: [presetEnv, presetReact]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const jQueryScript = fs.readFileSync(
|
|
|
|
|
path.resolve('./node_modules/jquery/dist/jquery.slim.min.js')
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function isPromise(value) {
|
|
|
|
|
return (
|
|
|
|
|
value &&
|
|
|
|
|
typeof value.subscribe !== 'function' &&
|
|
|
|
|
typeof value.then === 'function'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-23 16:21:53 +03:00
|
|
|
function checkSyntax(test, tapTest) {
|
|
|
|
|
try {
|
|
|
|
|
// eslint-disable-next-line
|
|
|
|
|
new vm.Script(test.testString);
|
|
|
|
|
tapTest.pass(test.text);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
tapTest.fail(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-24 14:15:00 +03:00
|
|
|
async function runScript(scriptString, sandbox) {
|
|
|
|
|
const context = vm.createContext(sandbox);
|
|
|
|
|
scriptString += `;
|
|
|
|
|
(async () => {
|
|
|
|
|
const testResult = eval(test);
|
|
|
|
|
if (typeof testResult === 'function') {
|
|
|
|
|
const __result = testResult(() => code);
|
|
|
|
|
if (isPromise(__result)) {
|
|
|
|
|
await __result;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})();`;
|
|
|
|
|
const script = new vm.Script(scriptString);
|
|
|
|
|
script.runInContext(context);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const colors = {
|
|
|
|
|
red: 'rgb(255, 0, 0)',
|
|
|
|
|
green: 'rgb(0, 255, 0)',
|
|
|
|
|
blue: 'rgb(0, 0, 255)',
|
|
|
|
|
black: 'rgb(0, 0, 0)',
|
|
|
|
|
gray: 'rgb(128, 128, 128)',
|
|
|
|
|
yellow: 'rgb(255, 255, 0)'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function replaceColorNamesPlugin(style) {
|
|
|
|
|
visit(style, (declarations, node) => {
|
|
|
|
|
declarations
|
|
|
|
|
.filter(decl => decl.type === 'declaration')
|
|
|
|
|
.forEach(decl => {
|
|
|
|
|
if (colors[decl.value]) {
|
|
|
|
|
decl.value = colors[decl.value];
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// JSDOM uses CSSStyleDeclaration, which does not convert color keywords
|
|
|
|
|
// to 'rgb()' https://github.com/jsakas/CSSStyleDeclaration/issues/48.
|
|
|
|
|
// It's a workaround.
|
|
|
|
|
function replaceColorNames(solution) {
|
|
|
|
|
const fragment = JSDOM.fragment(`<div>${solution}</div>`);
|
|
|
|
|
const styleTags = fragment.querySelectorAll('style');
|
|
|
|
|
if (styleTags.length > 0) {
|
|
|
|
|
styleTags.forEach(styleTag => {
|
|
|
|
|
styleTag.innerHTML = rework(styleTag.innerHTML)
|
|
|
|
|
.use(replaceColorNamesPlugin)
|
|
|
|
|
.toString();
|
|
|
|
|
});
|
|
|
|
|
return fragment.children[0].innerHTML;
|
|
|
|
|
}
|
|
|
|
|
return solution;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function evaluateHtmlTest(
|
|
|
|
|
challengeType,
|
2018-05-21 14:56:49 +01:00
|
|
|
solution,
|
|
|
|
|
assert,
|
2018-10-24 14:15:00 +03:00
|
|
|
required,
|
2018-10-23 16:21:53 +03:00
|
|
|
files,
|
2018-05-21 14:56:49 +01:00
|
|
|
test,
|
|
|
|
|
tapTest
|
|
|
|
|
) {
|
2018-10-24 14:15:00 +03:00
|
|
|
try {
|
|
|
|
|
const code = solution;
|
|
|
|
|
const { head = '', tail = '' } = files.html;
|
|
|
|
|
|
|
|
|
|
const options = {
|
|
|
|
|
resources: 'usable',
|
|
|
|
|
runScripts: 'dangerously'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const links = required
|
|
|
|
|
.map(({ link, src }) => {
|
|
|
|
|
if (link && src) {
|
|
|
|
|
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>
|
|
|
|
|
<script>${jQueryScript}</script>
|
|
|
|
|
${links}
|
|
|
|
|
</head>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
solution = transformSass(solution);
|
|
|
|
|
solution = replaceColorNames(solution);
|
|
|
|
|
|
2018-10-23 16:21:53 +03:00
|
|
|
const jsdom = new JSDOM(`
|
|
|
|
|
<!doctype html>
|
|
|
|
|
<html>
|
2018-10-24 14:15:00 +03:00
|
|
|
${scripts}
|
2018-10-23 16:21:53 +03:00
|
|
|
${head}
|
|
|
|
|
${solution}
|
|
|
|
|
${tail}
|
|
|
|
|
</html>
|
2018-10-24 14:15:00 +03:00
|
|
|
`, options);
|
2018-10-23 16:21:53 +03:00
|
|
|
|
2018-10-24 14:15:00 +03:00
|
|
|
// jQuery used by tests
|
|
|
|
|
jQuery(jsdom.window);
|
2018-01-19 14:03:17 -05:00
|
|
|
|
2018-10-24 14:15:00 +03:00
|
|
|
if (links || challengeType === challengeTypes.modern) {
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
jsdom.window.assert = assert;
|
|
|
|
|
jsdom.window.code = code;
|
|
|
|
|
jsdom.window.DeepEqual = DeepEqual;
|
|
|
|
|
jsdom.window.DeepFreeze = DeepFreeze;
|
|
|
|
|
jsdom.window.isPromise = isPromise;
|
|
|
|
|
jsdom.window.__test = test.testString;
|
|
|
|
|
const scriptString = `;
|
|
|
|
|
(async () => {
|
|
|
|
|
const testResult = eval(__test);
|
2018-10-23 16:21:53 +03:00
|
|
|
if (typeof testResult === 'function') {
|
2018-10-24 14:15:00 +03:00
|
|
|
const __result = testResult(() => code);
|
|
|
|
|
if (isPromise(__result)) {
|
|
|
|
|
await __result;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})();`;
|
2018-10-23 16:21:53 +03:00
|
|
|
const script = new vm.Script(scriptString);
|
2018-10-24 14:15:00 +03:00
|
|
|
jsdom.runVMScript(script);
|
|
|
|
|
jsdom.window.close();
|
2018-01-19 14:03:17 -05:00
|
|
|
} catch (e) {
|
|
|
|
|
tapTest.fail(e);
|
|
|
|
|
}
|
2015-11-01 17:20:03 -08:00
|
|
|
}
|
|
|
|
|
|
2018-10-24 14:15:00 +03:00
|
|
|
async function evaluateJsTest(
|
|
|
|
|
challengeType,
|
|
|
|
|
solution,
|
|
|
|
|
assert,
|
|
|
|
|
required,
|
|
|
|
|
files,
|
|
|
|
|
test,
|
|
|
|
|
tapTest
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let sandbox = {
|
|
|
|
|
assert,
|
|
|
|
|
code: solution,
|
|
|
|
|
DeepEqual,
|
|
|
|
|
DeepFreeze,
|
|
|
|
|
isPromise,
|
|
|
|
|
test: test.testString
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const { head = '', tail = '' } = files.js;
|
|
|
|
|
const scriptString = head + '\n' + solution + '\n' + tail + '\n';
|
|
|
|
|
|
|
|
|
|
runScript(scriptString, sandbox);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
tapTest.fail(e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function evaluateReactReduxTest(
|
|
|
|
|
challengeType,
|
|
|
|
|
solution,
|
|
|
|
|
assert,
|
|
|
|
|
required,
|
|
|
|
|
files,
|
|
|
|
|
test,
|
|
|
|
|
tapTest
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const code = solution;
|
|
|
|
|
let sandbox = {
|
|
|
|
|
assert,
|
|
|
|
|
code,
|
|
|
|
|
DeepEqual,
|
|
|
|
|
DeepFreeze,
|
|
|
|
|
isPromise
|
|
|
|
|
};
|
|
|
|
|
/* Transpile ALL the code
|
|
|
|
|
* (we may use JSX in head or tail or tests, too): */
|
|
|
|
|
solution = Babel.transform(solution, babelOptions).code;
|
|
|
|
|
const testString = Babel.transform(test.testString, babelOptions).code;
|
|
|
|
|
|
|
|
|
|
sandbox = {
|
|
|
|
|
...sandbox,
|
|
|
|
|
test: testString
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let head = '', tail = '';
|
|
|
|
|
if (files.js) {
|
|
|
|
|
const { head: headJs = '', tail: tailJs = '' } = files.js;
|
|
|
|
|
head += Babel.transform(headJs, babelOptions).code + '\n';
|
|
|
|
|
tail += Babel.transform(tailJs, babelOptions).code + '\n';
|
|
|
|
|
}
|
|
|
|
|
if (files.jsx) {
|
|
|
|
|
const { head: headJsx = '', tail: tailJsx = '' } = files.jsx;
|
|
|
|
|
head += Babel.transform(headJsx, babelOptions).code + '\n';
|
|
|
|
|
tail += Babel.transform(tailJsx, babelOptions).code + '\n';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const scriptString = head + '\n' + solution + '\n' + tail + '\n';
|
|
|
|
|
|
|
|
|
|
// Mock DOM document for ReactDOM.render method
|
|
|
|
|
const jsdom = new JSDOM(`
|
|
|
|
|
<!doctype html>
|
|
|
|
|
<html>
|
|
|
|
|
<body>
|
|
|
|
|
<div id="root"><div id="challenge-node"></div>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
const { window } = jsdom;
|
|
|
|
|
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);
|
|
|
|
|
// copyProps(window, global);
|
|
|
|
|
|
|
|
|
|
// Provide dependencies, just provide all of them
|
|
|
|
|
const React = require('react');
|
|
|
|
|
const ReactDOM = require('react-dom');
|
|
|
|
|
const PropTypes = require('prop-types');
|
|
|
|
|
const Redux = require('redux');
|
|
|
|
|
const ReduxThunk = require('redux-thunk');
|
|
|
|
|
const ReactRedux = require('react-redux');
|
|
|
|
|
const Enzyme = require('enzyme');
|
|
|
|
|
const Adapter16 = require('enzyme-adapter-react-16');
|
|
|
|
|
Enzyme.configure({ adapter: new Adapter16() });
|
|
|
|
|
|
|
|
|
|
sandbox = {
|
|
|
|
|
...sandbox,
|
|
|
|
|
require,
|
|
|
|
|
setTimeout,
|
|
|
|
|
window,
|
|
|
|
|
document,
|
|
|
|
|
React,
|
|
|
|
|
ReactDOM,
|
|
|
|
|
PropTypes,
|
|
|
|
|
Redux,
|
|
|
|
|
ReduxThunk,
|
|
|
|
|
ReactRedux,
|
|
|
|
|
Enzyme,
|
|
|
|
|
editor: {
|
|
|
|
|
getValue() {
|
|
|
|
|
return code;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
runScript(scriptString, sandbox);
|
|
|
|
|
jsdom.window.close();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
tapTest.fail(e);
|
|
|
|
|
}
|
2018-10-23 16:21:53 +03:00
|
|
|
}
|
|
|
|
|
|
2015-12-28 22:54:02 -08:00
|
|
|
function createTest({
|
|
|
|
|
title,
|
2018-01-29 16:11:27 +00:00
|
|
|
id = '',
|
2018-10-23 16:21:53 +03:00
|
|
|
challengeType,
|
2018-10-24 14:15:00 +03:00
|
|
|
required = [],
|
2015-12-28 22:54:02 -08:00
|
|
|
tests = [],
|
|
|
|
|
solutions = [],
|
2018-10-23 16:21:53 +03:00
|
|
|
files = []
|
2015-12-28 22:54:02 -08:00
|
|
|
}) {
|
2018-01-19 14:03:17 -05:00
|
|
|
mongoIds.check(id, title);
|
2018-04-11 05:23:36 +09:00
|
|
|
challengeTitles.check(title);
|
2018-01-19 14:03:17 -05:00
|
|
|
|
2018-10-23 16:21:53 +03:00
|
|
|
// if title starts with [word] [number], for example `Problem 5`,
|
|
|
|
|
// tap-spec does not recognize it as test suite.
|
|
|
|
|
const titleRe = new RegExp('^([a-z]+\\s+)(\\d+.*)$', 'i');
|
|
|
|
|
const match = titleRe.exec(title);
|
|
|
|
|
if (match) {
|
|
|
|
|
title = `${match[1]}#${match[2]}`;
|
|
|
|
|
}
|
Feat: react redux migration (#16200)
* feat: crudely enables test to run solution code against React challenge (and passes!)
* feat: Updates comment
* feat: Adds React 2 and 3, validates challenges in app
* feat: Adds React 4, validates tests
* feat: Adds Peter's migrated challenge seed files for all challenges
* feat: Adds redux, react-redux imports, adds tests for React 7,
* feat: Adds tests for React 08
* fix(challenges): wrap reserved words in <code> and add tests
* feat: complete first two tests for React 9
* feat: modifies tests in React 09
* feat: Adds working tests for React 37, including async setState tests
* feat: Escape hatch to avoid async tests in automated test suite
* feat: Updates React 15 with working tests
* feat: build passes, yay
* feat: Provisions original code string in challenges and adds tests for React Redux 01
* fix(tests): add self-closing tags challenge, other small fixes
* fix(challenge): add react_10, some other stuff
* fix(challenges): update react 22, add react 23
* fix(challenges): react 5 and react 8
* feat: removes dependencies that will break in browser, will replace later
* feat: fix build
* feat: add redux 1
* fix(challenge): add react 24 tests
* feat: partial implemented Redux 2
* feat: migrate redux 3
* feat: Adds React-Redux 04 with working tests under npm test
* feat: Updates automated test runner, just provide all the dependencies. Adds Redux-Thunk.
* feat: Adds working tests for React Redux 07
* feat: redux challenge 4
* feat: migrate redux 5
* feat: redux 6
* feat: migrate Redux test 7
* fix(challenge): add react 25 tests
* feat: Adds tests for React 48, npm test does not pass...
* feat: Migrate Redux test 8
* fix(challenges): skip 26, add react 27 tests
* fix(challenges): add react 28 tests, replace function w/ => throughout, fix linter warnings
* feat: fixes (patches) hard to understand problem with automated test suite
* feat: updates async tests patch
* feat: adds converted tests for React 47
* feat: adds converted tests for React 46
* feat: Partially adds tests for React 43
* docs: adds TO-DO tests for React 43
* feat: migrates tests for React 42
* feat: migrates tests for React 41
* feat: migrates tests for React 39
* feat: Migrates tests for React 38, automated test script fails again!
* feat: migrates tests for React 32
* feat: QAs more React Redux challenge in FCC UI
* feat: Updates tests for React 7
* feat: Migrates React-Redux 3 tests and hardcodes deep-freeze dependency
* feat: migrates React Redux 05 tests
* feat: migrates React Redux 06 tests
* feat: Migrates React Redux 10
* feat: Migrates tests for React 16
* feat: Migrates React 17 tests
* feat: Migrates React 18 tests
* feat: Migrates React 19 tests
* feat: Migrates React 19 tests
* feat: fixing usage of code, replace with editor.getOriginalCode
* feat: Migrates React 21 tests
* feat: Finishes migration of React 09
* fix(challenges): add react 45 tests 💀
* feat: Adds React 11 tests
* feat: Migrates React 50 tests
* feat: Re-enables original code in FCC editor, QAs challenges blocked by original code
* feat: hacks head tail code in editor test environment
* feat: updates React 20 head code
* feat: QAs React Redux 07 in UI
* fix(challenges): add React 29 tests
* fix(challenges): add React 30 tests
* feat: updates async tests
* feat: Migrates React 12, gets ReactDOM challenges working and QAs them
* feat: Migrates React 13 tests
* feat: Migrates tests for React 14 and updates challenge description formatting
* feat: Refactors 2nd test for Redux 02
* feat: Migrates React 33
* feat: Removes React 26 and 43
* feat: Adds React 34 from Kevin
* fix(challenges): add React 31 & 35 tests (thanks Kevin)
* feat: Migrate Redux challenge 10 - pass both UI QA and terminal test
* fix(challenge): add react 40 tests
* feat: Migrates React Redux 02 tests
* feat: Migrates React Redux 08 and fixes async syntax in React challenge
* fix(challenge): add react 49 tests with caveat
* feat: fixes React 49 tests and adds first tests for React Redux 09
* feat: Migrate Redux 11 - pass both terminal test and UI test
* feat: Migrate Redux 12 - passing both UI test and terminal test
* feat: Migrate Redux 13 - passing both terminal and UI tests
* feat: Adding in code tags for previous redux challenges - terminal and UI tests pass
* feat: Migrates React Redux 09 and React 44 (thanks Kevin)
* feat: fix code tag issues - passed UI and terminal tests
* feat: Migrates Redux 14 tests
* feat: Migrates Redux 14
* feat: Migrates Redux 15
* feat: Migrates Redux 17
* feat: Final migration and QA of Redux, except for Redux 9
* feat: migrates React 36 and QAs
* feat: Rewrites Redux 09 and migrates
* feat: refactors pull request and cleans up code
* style(challenges): QA React challenges
* style(challenges): QA react challenges
* fix(challenges): fix react 41 and 45 tests
* style(challenges): QA redux challenges
* style(challenges): QA react and redux challenges
* fix(seed/react): Move head/tail to files
* fix(seed/redux): Move head/tail to file level
* chore(packages): Move jsdom to dev deps
* fix(seed/react/redux): Async funcs
make async func defined
* fix(seed): %s/editor.getUserCode/getUserInput/gc
* fix(Challenges/build): Make sure head/tail is bundled and transformed
* feat(Challenges.react): Add tail to render component
* chore(seed): Disable modern challenge testing for now
We will put these on beta while we update the auto testing framework
2017-12-18 13:04:03 -08:00
|
|
|
|
2018-10-23 16:21:53 +03:00
|
|
|
const testSuite = Observable.fromCallback(tape)(title);
|
|
|
|
|
|
|
|
|
|
tests = tests.filter(test => !!test.testString);
|
|
|
|
|
if (tests.length === 0) {
|
|
|
|
|
return testSuite.flatMap(tapTest => {
|
|
|
|
|
tapTest.end();
|
|
|
|
|
return Observable.just(title);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const noSolution = new RegExp('// solution required');
|
|
|
|
|
solutions = solutions.filter(solution => (
|
|
|
|
|
!!solution && !noSolution.test(solution)
|
|
|
|
|
));
|
|
|
|
|
|
2018-10-24 14:15:00 +03:00
|
|
|
const skipTests =
|
|
|
|
|
challengeType !== challengeTypes.html &&
|
2018-10-23 16:21:53 +03:00
|
|
|
challengeType !== challengeTypes.js &&
|
|
|
|
|
challengeType !== challengeTypes.bonfire &&
|
2018-10-24 14:15:00 +03:00
|
|
|
challengeType !== challengeTypes.modern;
|
2018-10-23 16:21:53 +03:00
|
|
|
|
|
|
|
|
// For problems without a solution, check only the syntax of the tests.
|
|
|
|
|
if (solutions.length === 0 || skipTests) {
|
|
|
|
|
return testSuite.flatMap(tapTest => {
|
|
|
|
|
tapTest.plan(tests.length);
|
|
|
|
|
tests.forEach(test => {
|
|
|
|
|
checkSyntax(test, tapTest);
|
|
|
|
|
});
|
|
|
|
|
return Observable.just(title);
|
|
|
|
|
});
|
2018-06-13 19:19:37 +05:30
|
|
|
}
|
2018-10-23 16:21:53 +03:00
|
|
|
|
|
|
|
|
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(
|
2018-05-21 14:56:49 +01:00
|
|
|
(result, file) => ({
|
2018-10-24 14:15:00 +03:00
|
|
|
head: result.head + '\n' + file.head,
|
|
|
|
|
tail: result.tail + '\n' + file.tail
|
2018-05-21 14:56:49 +01:00
|
|
|
}),
|
|
|
|
|
{ head: '', tail: '' }
|
|
|
|
|
);
|
2018-10-23 16:21:53 +03:00
|
|
|
return {
|
|
|
|
|
...result,
|
|
|
|
|
[ext]: file
|
|
|
|
|
};
|
|
|
|
|
}, {});
|
2015-12-28 22:54:02 -08:00
|
|
|
|
2018-10-24 14:15:00 +03:00
|
|
|
let evaluateTest;
|
|
|
|
|
if (challengeType === challengeTypes.modern &&
|
|
|
|
|
(groupedFiles.js || groupedFiles.jsx)) {
|
|
|
|
|
evaluateTest = evaluateReactReduxTest;
|
|
|
|
|
} else if (groupedFiles.html) {
|
|
|
|
|
evaluateTest = evaluateHtmlTest;
|
|
|
|
|
} else if (groupedFiles.js) {
|
|
|
|
|
evaluateTest = evaluateJsTest;
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(`Unknown challenge type ${title}`);
|
|
|
|
|
}
|
2018-10-23 16:21:53 +03:00
|
|
|
const plan = tests.length * solutions.length;
|
|
|
|
|
return testSuite
|
2018-01-19 14:03:17 -05:00
|
|
|
.flatMap(tapTest => {
|
2018-10-23 16:21:53 +03:00
|
|
|
tapTest.plan(plan);
|
2018-06-13 19:19:37 +05:30
|
|
|
return (
|
|
|
|
|
Observable.just(tapTest)
|
|
|
|
|
.map(addAssertsToTapTest)
|
2018-10-24 14:15:00 +03:00
|
|
|
.flatMap(assert =>
|
|
|
|
|
Observable.from(solutions)
|
|
|
|
|
.flatMap(solution =>
|
|
|
|
|
Observable.from(tests)
|
|
|
|
|
.flatMap(test => evaluateTest(
|
|
|
|
|
challengeType,
|
|
|
|
|
solution,
|
|
|
|
|
assert,
|
|
|
|
|
required,
|
|
|
|
|
groupedFiles,
|
|
|
|
|
test,
|
|
|
|
|
tapTest
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
)
|
2018-10-23 16:21:53 +03:00
|
|
|
.ignoreElements()
|
2018-06-13 19:19:37 +05:30
|
|
|
);
|
2015-11-01 17:20:03 -08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-24 14:15:00 +03:00
|
|
|
Observable.fromPromise(getChallengesForLang(lang))
|
2018-10-23 16:21:53 +03:00
|
|
|
.flatMap(curriculum => {
|
|
|
|
|
const allChallenges = Object.keys(curriculum)
|
|
|
|
|
.map(key => curriculum[key].blocks)
|
|
|
|
|
.reduce((challengeArray, superBlock) => {
|
|
|
|
|
const challengesForBlock = Object.keys(superBlock).map(
|
|
|
|
|
key => superBlock[key].challenges
|
|
|
|
|
);
|
|
|
|
|
return [...challengeArray, ...flatten(challengesForBlock)];
|
|
|
|
|
}, []);
|
|
|
|
|
return Observable.from(allChallenges);
|
2018-05-22 13:43:14 +01:00
|
|
|
})
|
2018-10-23 16:21:53 +03:00
|
|
|
.do(challenge => {
|
|
|
|
|
const result = validateChallenge(challenge);
|
|
|
|
|
if (result.error) {
|
|
|
|
|
console.log(result.value);
|
|
|
|
|
throw new Error(result.error);
|
|
|
|
|
}
|
2015-11-01 17:20:03 -08:00
|
|
|
})
|
|
|
|
|
.flatMap(challenge => {
|
|
|
|
|
return createTest(challenge);
|
|
|
|
|
})
|
|
|
|
|
.toArray()
|
|
|
|
|
.subscribe(
|
2018-06-13 19:19:37 +05:30
|
|
|
noSolutions => {
|
2018-01-29 16:11:27 +00:00
|
|
|
if (noSolutions) {
|
2018-01-12 04:09:37 +05:30
|
|
|
console.log(
|
2018-10-23 16:21:53 +03:00
|
|
|
`# These challenges have no solutions (${noSolutions.length})\n` +
|
|
|
|
|
'- [ ] ' + noSolutions.join('\n- [ ] ')
|
2018-01-12 04:09:37 +05:30
|
|
|
);
|
|
|
|
|
}
|
2015-11-01 17:20:03 -08:00
|
|
|
},
|
2018-01-19 14:03:17 -05:00
|
|
|
err => {
|
|
|
|
|
throw err;
|
|
|
|
|
},
|
2015-11-01 17:20:03 -08:00
|
|
|
() => process.exit(0)
|
|
|
|
|
);
|