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