From 62cc8acb87446f2b80c19201c7a7d903ee981414 Mon Sep 17 00:00:00 2001 From: Valeriy Date: Thu, 25 Oct 2018 03:34:47 +0300 Subject: [PATCH] test(curriculum): use Mocha for tests --- curriculum/addAssertsToTapTest.js | 55 -- curriculum/package-lock.json | 263 ++++----- curriculum/package.json | 6 +- curriculum/test-challenges.js | 510 ------------------ curriculum/test/test-challenges.js | 423 +++++++++++++++ .../{ => test/utils}/challengeTitles.js | 0 curriculum/{ => test/utils}/mongoIds.js | 0 7 files changed, 534 insertions(+), 723 deletions(-) delete mode 100644 curriculum/addAssertsToTapTest.js delete mode 100644 curriculum/test-challenges.js create mode 100644 curriculum/test/test-challenges.js rename curriculum/{ => test/utils}/challengeTitles.js (100%) rename curriculum/{ => test/utils}/mongoIds.js (100%) diff --git a/curriculum/addAssertsToTapTest.js b/curriculum/addAssertsToTapTest.js deleted file mode 100644 index 5de7da2695..0000000000 --- a/curriculum/addAssertsToTapTest.js +++ /dev/null @@ -1,55 +0,0 @@ -let _ = require('lodash'); - -function createIsAssert(tapTest, isThing) { - const { assert } = tapTest; - return function() { - const args = [...arguments]; - args[0] = isThing(args[0]); - assert.apply(tapTest, args); - }; -} - -function addAssertsToTapTest(tapTest) { - const assert = tapTest.assert; - - assert.isTrue = createIsAssert(tapTest, v => v === true); - assert.isFalse = createIsAssert(tapTest, v => v === false); - assert.isArray = createIsAssert(tapTest, _.isArray); - assert.isBoolean = createIsAssert(tapTest, _.isBoolean); - assert.isString = createIsAssert(tapTest, _.isString); - assert.isNumber = createIsAssert(tapTest, _.isNumber); - assert.isUndefined = createIsAssert(tapTest, _.isUndefined); - assert.isNaN = createIsAssert(tapTest, _.isNaN); - - assert.deepEqual = tapTest.deepEqual; - assert.equal = tapTest.equal; - assert.strictEqual = tapTest.equal; - assert.sameMembers = function sameMembers() { - const [ first, second, ...args] = arguments; - assert.apply( - tapTest, - [ - _.difference(first, second).length === 0 && - _.difference(second, first).length === 0 - ].concat(args) - ); - }; - assert.includeMembers = function includeMembers() { - const [ first, second, ...args] = arguments; - assert.apply(tapTest, - [ - _.difference(second, first).length === 0 - ].concat(args)); - }; - assert.match = function match() { - const [value, regex, ...args] = arguments; - assert.apply(tapTest, - [ - regex.test(value) - ].concat(args)); - }; - - return assert; -} - -module.exports = addAssertsToTapTest; diff --git a/curriculum/package-lock.json b/curriculum/package-lock.json index abb6bcf51c..ef7c6fa946 100644 --- a/curriculum/package-lock.json +++ b/curriculum/package-lock.json @@ -3056,9 +3056,9 @@ } }, "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, "browserify": { @@ -3386,17 +3386,17 @@ } }, "chai": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", - "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", "dev": true, "requires": { - "assertion-error": "^1.0.1", - "check-error": "^1.0.1", - "deep-eql": "^3.0.0", + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", "get-func-name": "^2.0.0", - "pathval": "^1.0.0", - "type-detect": "^4.0.0" + "pathval": "^1.1.0", + "type-detect": "^4.0.5" } }, "chalk": { @@ -4395,9 +4395,9 @@ } }, "diff": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", - "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, "diffie-hellman": { @@ -6308,7 +6308,8 @@ "version": "1.1.0", "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", @@ -6320,7 +6321,8 @@ "version": "1.1.0", "resolved": false, "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -6451,7 +6453,8 @@ "version": "2.0.3", "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -6465,6 +6468,7 @@ "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6604,7 +6608,8 @@ "version": "1.0.1", "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -6755,6 +6760,7 @@ "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7589,9 +7595,9 @@ "dev": true }, "growl": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", - "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, "gulp": { @@ -10570,29 +10576,24 @@ } }, "mocha": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", - "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", "dev": true, "requires": { - "browser-stdout": "1.3.0", - "commander": "2.11.0", + "browser-stdout": "1.3.1", + "commander": "2.15.1", "debug": "3.1.0", - "diff": "3.3.1", + "diff": "3.5.0", "escape-string-regexp": "1.0.5", "glob": "7.1.2", - "growl": "1.10.3", + "growl": "1.10.5", "he": "1.1.1", + "minimatch": "3.0.4", "mkdirp": "0.5.1", - "supports-color": "4.4.0" + "supports-color": "5.4.0" }, "dependencies": { - "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", - "dev": true - }, "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", @@ -10602,19 +10603,13 @@ "ms": "2.0.0" } }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, "supports-color": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "dev": true, "requires": { - "has-flag": "^2.0.0" + "has-flag": "^3.0.0" } } } @@ -11640,6 +11635,74 @@ "got": "^8.0.1", "mkdirp": "^0.5.1", "mocha": "^4.0.1" + }, + "dependencies": { + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "diff": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", + "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", + "dev": true + }, + "growl": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "dev": true + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "mocha": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", + "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.3.1", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + } + }, + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "dev": true, + "requires": { + "has-flag": "^2.0.0" + } + } } }, "parse-filepath": { @@ -13999,116 +14062,6 @@ } } }, - "tap-out": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tap-out/-/tap-out-2.1.0.tgz", - "integrity": "sha512-LJE+TBoVbOWhwdz4+FQk40nmbIuxJLqaGvj3WauQw3NYYU5TdjoV3C0x/yq37YAvVyi+oeBXmWnxWSjJ7IEyUw==", - "dev": true, - "requires": { - "re-emitter": "1.1.3", - "readable-stream": "2.2.9", - "split": "1.0.0", - "trim": "0.0.1" - }, - "dependencies": { - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true - }, - "readable-stream": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz", - "integrity": "sha1-z3jsb0ptHrQ9JkiMrJfwQudLf8g=", - "dev": true, - "requires": { - "buffer-shims": "~1.0.0", - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~1.0.0", - "util-deprecate": "~1.0.1" - } - }, - "split": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.0.tgz", - "integrity": "sha1-xDlc5oOrzSVLwo/h2rtuXCfc/64=", - "dev": true, - "requires": { - "through": "2" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "tap-spec": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tap-spec/-/tap-spec-5.0.0.tgz", - "integrity": "sha512-zMDVJiE5I6Y4XGjlueGXJIX2YIkbDN44broZlnypT38Hj/czfOXrszHNNJBF/DXR8n+x6gbfSx68x04kIEHdrw==", - "dev": true, - "requires": { - "chalk": "^1.0.0", - "duplexer": "^0.1.1", - "figures": "^1.4.0", - "lodash": "^4.17.10", - "pretty-ms": "^2.1.0", - "repeat-string": "^1.5.2", - "tap-out": "^2.1.0", - "through2": "^2.0.0" - }, - "dependencies": { - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" - } - } - } - }, - "tape": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/tape/-/tape-4.9.1.tgz", - "integrity": "sha512-6fKIXknLpoe/Jp4rzHKFPpJUHDHDqn8jus99IfPnHIjyz78HYlefTGD3b5EkbQzuLfaEvmfPK3IolLgq2xT3kw==", - "dev": true, - "requires": { - "deep-equal": "~1.0.1", - "defined": "~1.0.0", - "for-each": "~0.3.3", - "function-bind": "~1.1.1", - "glob": "~7.1.2", - "has": "~1.0.3", - "inherits": "~2.0.3", - "minimist": "~1.2.0", - "object-inspect": "~1.6.0", - "resolve": "~1.7.1", - "resumer": "~0.0.0", - "string.prototype.trim": "~1.1.2", - "through": "~2.3.8" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, "tar": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", diff --git a/curriculum/package.json b/curriculum/package.json index b6f5dce0a5..7676e7eb2c 100644 --- a/curriculum/package.json +++ b/curriculum/package.json @@ -21,7 +21,7 @@ "prepare": "npm run build", "repack": "babel-node ./repack.js", "semantic-release": "semantic-release", - "test": "node ./test-challenges.js | tap-spec", + "test": "mocha --delay --reporter progress", "unpack": "babel-node ./unpack.js" }, "dependencies": { @@ -45,6 +45,7 @@ "babel-preset-stage-3": "^6.24.1", "babel-standalone": "^6.26.0", "browserify": "^16.2.2", + "chai": "4.2.0", "enzyme": "^3.7.0", "enzyme-adapter-react-16": "^1.6.0", "eslint": "^4.19.1", @@ -61,6 +62,7 @@ "jsdom": "^12.2.0", "lint-staged": "^7.2.0", "lodash": "^4.17.10", + "mocha": "5.2.0", "node-sass": "4.9.4", "prettier": "^1.13.5", "prettier-package-json": "^1.6.0", @@ -74,8 +76,6 @@ "rework-visit":"1.0.0", "rx": "^4.1.0", "semantic-release": "^15.6.0", - "tap-spec": "^5.0.0", - "tape": "^4.9.1", "validator": "^10.4.0" }, "keywords": [ diff --git a/curriculum/test-challenges.js b/curriculum/test-challenges.js deleted file mode 100644 index fb3e53aef6..0000000000 --- a/curriculum/test-challenges.js +++ /dev/null @@ -1,510 +0,0 @@ -/* 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(`
${solution}
`); - 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(`
${solution}
`); - 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, - solution, - assert, - required, - files, - test, - tapTest -) { - 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 ``; - } - if (link) { - return ``; - } - return ''; - }) - .reduce((head, required) => head.concat(required), ''); - - const scripts = ` - - - ${links} - - `; - - solution = transformSass(solution); - solution = replaceColorNames(solution); - - const jsdom = new JSDOM(` - - - ${scripts} - ${head} - ${solution} - ${tail} - - `, options); - - // jQuery used by tests - jQuery(jsdom.window); - - 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); - if (typeof testResult === 'function') { - const __result = testResult(() => code); - if (isPromise(__result)) { - await __result; - } - } - })();`; - const script = new vm.Script(scriptString); - jsdom.runVMScript(script); - jsdom.window.close(); - } catch (e) { - tapTest.fail(e); - } -} - -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(` - - - -
- - - `); - - 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) - ); diff --git a/curriculum/test/test-challenges.js b/curriculum/test/test-challenges.js new file mode 100644 index 0000000000..4a4f758d8b --- /dev/null +++ b/curriculum/test/test-challenges.js @@ -0,0 +1,423 @@ +const assert = require('chai').assert; + +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 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('./utils/mongoIds'); +const ChallengeTitles = require('./utils/challengeTitles'); +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 { JSDOM } = jsdom; + +const babelOptions = { + plugins: ['transform-runtime'], + presets: [presetEnv, presetReact] +}; + +const jQueryScript = fs.readFileSync( + path.resolve('./node_modules/jquery/dist/jquery.slim.min.js') +); + +(async function() { + 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)]; + }, []) + )); + + describe('Check challenges tests', async function() { + this.timeout(200000); + + allChallenges.forEach(challenge => { + describe(challenge.title || 'No title', async function() { + + it('Common checks', function() { + const result = validateChallenge(challenge); + if (result.error) { + console.log(result.value); + throw new Error(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 + ) { + return; + } + + let { tests } = challenge; + tests = tests.filter(test => !!test.testString); + if (tests.length === 0) { + it.skip('Check tests syntax. No tests.'); + return; + } + + describe('Check tests syntax', function() { + tests.forEach(test => { + it(`Check for: ${test.text}`, function() { + assert.doesNotThrow( + () => new vm.Script(test.testString) + ); + }); + }); + }); + + let { solutions } = challenge; + const noSolution = new RegExp('// solution required'); + solutions = solutions.filter(solution => ( + !!solution && !noSolution.test(solution) + )); + + if (solutions.length === 0) { + it.skip('Check tests against solutions. No solutions'); + return; + } + + 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, + 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 { + it.skip('Check tests against solutions. Unknown file type.'); + 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 + }); + }); + }); + }); + }); + }); + }); + }); + }); + + run(); + +})(); + +// 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 transformSass(solution) { + const fragment = JSDOM.fragment(`
${solution}
`); + 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 => { + 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(`
${solution}
`); + 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, + solution, + required = [], + files, + test +}) { + + const code = solution; + const { head = '', tail = '' } = files.html; + + const options = { + resources: 'usable', + runScripts: 'dangerously', + virtualConsole: new jsdom.VirtualConsole() + }; + + 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 ``; + } + if (link) { + return ``; + } + return ''; + }) + .reduce((head, required) => head.concat(required), ''); + + const scripts = ` + + + ${links} + + `; + + solution = transformSass(solution); + solution = replaceColorNames(solution); + + const dom = new JSDOM(` + + + ${scripts} + ${head} + ${solution} + ${tail} + + `, options); + + if (links || challengeType === challengeTypes.modern) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + dom.window.code = code; + runTestInJsdom(dom, test.testString); +} + +async function evaluateJsTest({ + solution, + files, + test +}) { + + const virtualConsole = new jsdom.VirtualConsole(); + const dom = new JSDOM('', { runScripts: 'dangerously', virtualConsole }); + dom.window.code = solution; + + const { head = '', tail = '' } = files.js; + const scriptString = head + '\n' + solution + '\n' + tail + '\n'; + + await runTestInJsdom(dom, test.testString, scriptString); +} + +async function evaluateReactReduxTest({ + solution, + files, + test +}) { + + const code = solution; + /* 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; + + 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'; + + const virtualConsole = new jsdom.VirtualConsole(); + // Mock DOM document for ReactDOM.render method + const dom = new JSDOM(` + + + +
+ + + `, { + runScripts: 'dangerously', + virtualConsole + }); + + 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, 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.isPromise = isPromise; + + dom.window.__test = testString; + scriptString += `; + (async () => { + try { + const testResult = eval(__test); + if (typeof testResult === 'function') { + const __result = testResult(() => code); + if (isPromise(__result)) { + await __result; + } + } + }catch (e) { + window.__error = e; + } + })();`; + const script = new vm.Script(scriptString); + dom.runVMScript(script); + if (dom.window.__error) { + throw dom.window.__error; + } +} diff --git a/curriculum/challengeTitles.js b/curriculum/test/utils/challengeTitles.js similarity index 100% rename from curriculum/challengeTitles.js rename to curriculum/test/utils/challengeTitles.js diff --git a/curriculum/mongoIds.js b/curriculum/test/utils/mongoIds.js similarity index 100% rename from curriculum/mongoIds.js rename to curriculum/test/utils/mongoIds.js