/* eslint-disable no-process-exit, no-unused-vars */
const { Observable } = require('rx');
const tape = require('tape');
const { flatten } = require('lodash');
const vm = require('vm');
const path = require('path');
const fs = require('fs');
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
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');
const { getChallengesForLang } = require('./getChallenges');
const MongoIds = require('./mongoIds');
const ChallengeTitles = require('./challengeTitles');
const addAssertsToTapTest = require('./addAssertsToTapTest');
const { validateChallenge } = require('./schema/challengeSchema');
const { challengeTypes } = require('../client/utils/challengeTypes');
const { LOCALE: lang = 'english' } = process.env;
let mongoIds = new MongoIds();
let challengeTitles = new ChallengeTitles();
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'
);
}
function checkSyntax(test, tapTest) {
try {
// eslint-disable-next-line
new vm.Script(test.testString);
tapTest.pass(test.text);
} catch (e) {
tapTest.fail(e);
}
}
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(`
`);
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);
}
}
function createTest({
title,
id = '',
challengeType,
required = [],
tests = [],
solutions = [],
files = []
}) {
mongoIds.check(id, title);
challengeTitles.check(title);
// 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]}`;
}
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)
));
const skipTests =
challengeType !== challengeTypes.html &&
challengeType !== challengeTypes.js &&
challengeType !== challengeTypes.bonfire &&
challengeType !== challengeTypes.modern;
// 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);
});
}
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,
tail: result.tail + '\n' + file.tail
}),
{ head: '', tail: '' }
);
return {
...result,
[ext]: file
};
}, {});
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}`);
}
const plan = tests.length * solutions.length;
return testSuite
.flatMap(tapTest => {
tapTest.plan(plan);
return (
Observable.just(tapTest)
.map(addAssertsToTapTest)
.flatMap(assert =>
Observable.from(solutions)
.flatMap(solution =>
Observable.from(tests)
.flatMap(test => evaluateTest(
challengeType,
solution,
assert,
required,
groupedFiles,
test,
tapTest
)
)
)
)
.ignoreElements()
);
});
}
Observable.fromPromise(getChallengesForLang(lang))
.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);
})
.do(challenge => {
const result = validateChallenge(challenge);
if (result.error) {
console.log(result.value);
throw new Error(result.error);
}
})
.flatMap(challenge => {
return createTest(challenge);
})
.toArray()
.subscribe(
noSolutions => {
if (noSolutions) {
console.log(
`# These challenges have no solutions (${noSolutions.length})\n` +
'- [ ] ' + noSolutions.join('\n- [ ] ')
);
}
},
err => {
throw err;
},
() => process.exit(0)
);