/* global browser, page */ const { assert, AssertionError } = require('chai'); const Mocha = require('mocha'); const { flatten } = require('lodash'); const path = require('path'); const fs = require('fs'); require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }); const vm = require('vm'); 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 MongoIds = require('./utils/mongoIds'); const ChallengeTitles = require('./utils/challengeTitles'); const { challengeSchemaValidator } = require('../schema/challengeSchema'); const { challengeTypes } = require('../../client/utils/challengeTypes'); const { supportedLangs } = require('../utils'); const oldRunnerFail = Mocha.Runner.prototype.fail; Mocha.Runner.prototype.fail = function(test, err) { if (err instanceof AssertionError) { const errMessage = String(err.message || ''); const assertIndex = errMessage.indexOf(': expected'); if (assertIndex !== -1) { err.message = errMessage.slice(0, assertIndex); } // Don't show stacktrace for assertion errors. if (err.stack) { delete err.stack; } } return oldRunnerFail.call(this, test, err); }; const { JSDOM } = jsdom; const babelOptions = { plugins: ['transform-runtime'], presets: [presetEnv, presetReact] }; const jQueryScript = fs.readFileSync( path.resolve('./node_modules/jquery/dist/jquery.slim.min.js'), 'utf8' ); runTests(); async function runTests() { let testLangs = [...supportedLangs]; if (process.env.TEST_CHALLENGES_FOR_LANGS) { const filterLangs = process.env.TEST_CHALLENGES_FOR_LANGS.split(',').map( lang => lang.trim().toLowerCase() ); testLangs = testLangs.filter(lang => filterLangs.includes(lang)); } await Promise.all(testLangs.map(lang => populateTestsForLang(lang))); run(); } async function populateTestsForLang(lang) { const allChallenges = await getChallengesForLang(lang).then(curriculum => 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)]; }, []) ); const mongoIds = new MongoIds(); const challengeTitles = new ChallengeTitles(); const validateChallenge = challengeSchemaValidator(lang); describe(`Check challenges (${lang})`, async 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); allChallenges.forEach(challenge => { describe(challenge.title || 'No title', async function() { it('Common checks', function() { const result = validateChallenge(challenge); if (result.error) { throw new AssertionError(result.error); } const { id, title } = challenge; mongoIds.check(id, title); challengeTitles.check(title); }); const { challengeType } = challenge; if ( challengeType !== challengeTypes.html && challengeType !== challengeTypes.js && challengeType !== challengeTypes.bonfire && challengeType !== challengeTypes.modern && challengeType !== challengeTypes.backend ) { return; } let { tests = [] } = challenge; tests = tests.filter(test => !!test.testString); if (tests.length === 0) { it('Check tests. No tests.'); return; } describe('Check tests syntax', function() { tests.forEach(test => { it(`Check for: ${test.text}`, function() { assert.doesNotThrow(() => new vm.Script(test.testString)); }); }); }); const { files = [], required = [] } = 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; if ( challengeType === challengeTypes.modern && (groupedFiles.js || groupedFiles.jsx) ) { evaluateTest = evaluateReactReduxTest; } else if (groupedFiles.html) { evaluateTest = evaluateHtmlTest; } else if (groupedFiles.js) { evaluateTest = evaluateJsTest; } else { it('Check tests. Unknown file type.'); return; } it('Test suite must fail on the initial contents', async function() { this.timeout(20000); // suppress errors in the console. const oldConsoleError = console.error; console.error = () => {}; let fails = (await Promise.all( tests.map(async function(test) { try { await evaluateTest({ challengeType, required, files: groupedFiles, test }); return false; } catch (e) { return true; } }) )).some(v => v); console.error = oldConsoleError; assert(fails, 'Test suit does not fail on the initial contents'); }); let { solutions = [] } = challenge; const noSolution = new RegExp('// solution required'); solutions = solutions.filter( solution => !!solution && !noSolution.test(solution) ); if (solutions.length === 0) { it('Check tests. No solutions'); return; } describe('Check tests against solutions', async function() { solutions.forEach((solution, index) => { describe(`Solution ${index + 1}`, async function() { tests.forEach(test => { it(test.text, async function() { await evaluateTest({ challengeType, solution, required, files: groupedFiles, test }); }); }); }); }); }); }); }); }); } // 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 transformSass(solution) { const fragment = JSDOM.fragment(`