test(curriculum): use Mocha for tests
This commit is contained in:
committed by
mrugesh mohapatra
parent
73485119f4
commit
62cc8acb87
@ -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;
|
|
263
curriculum/package-lock.json
generated
263
curriculum/package-lock.json
generated
@ -3056,9 +3056,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"browser-stdout": {
|
"browser-stdout": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
|
||||||
"integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=",
|
"integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"browserify": {
|
"browserify": {
|
||||||
@ -3386,17 +3386,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chai": {
|
"chai": {
|
||||||
"version": "4.1.2",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
|
||||||
"integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=",
|
"integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"assertion-error": "^1.0.1",
|
"assertion-error": "^1.1.0",
|
||||||
"check-error": "^1.0.1",
|
"check-error": "^1.0.2",
|
||||||
"deep-eql": "^3.0.0",
|
"deep-eql": "^3.0.1",
|
||||||
"get-func-name": "^2.0.0",
|
"get-func-name": "^2.0.0",
|
||||||
"pathval": "^1.0.0",
|
"pathval": "^1.1.0",
|
||||||
"type-detect": "^4.0.0"
|
"type-detect": "^4.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chalk": {
|
"chalk": {
|
||||||
@ -4395,9 +4395,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"diff": {
|
"diff": {
|
||||||
"version": "3.3.1",
|
"version": "3.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
|
||||||
"integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==",
|
"integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"diffie-hellman": {
|
"diffie-hellman": {
|
||||||
@ -6308,7 +6308,8 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": false,
|
"resolved": false,
|
||||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
@ -6320,7 +6321,8 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": false,
|
"resolved": false,
|
||||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
|
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@ -6451,7 +6453,8 @@
|
|||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": false,
|
"resolved": false,
|
||||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
|
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
@ -6465,6 +6468,7 @@
|
|||||||
"resolved": false,
|
"resolved": false,
|
||||||
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
@ -6604,7 +6608,8 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": false,
|
"resolved": false,
|
||||||
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
|
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
@ -6755,6 +6760,7 @@
|
|||||||
"resolved": false,
|
"resolved": false,
|
||||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
@ -7589,9 +7595,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"growl": {
|
"growl": {
|
||||||
"version": "1.10.3",
|
"version": "1.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
|
||||||
"integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==",
|
"integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"gulp": {
|
"gulp": {
|
||||||
@ -10570,29 +10576,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mocha": {
|
"mocha": {
|
||||||
"version": "4.1.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
|
||||||
"integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==",
|
"integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"browser-stdout": "1.3.0",
|
"browser-stdout": "1.3.1",
|
||||||
"commander": "2.11.0",
|
"commander": "2.15.1",
|
||||||
"debug": "3.1.0",
|
"debug": "3.1.0",
|
||||||
"diff": "3.3.1",
|
"diff": "3.5.0",
|
||||||
"escape-string-regexp": "1.0.5",
|
"escape-string-regexp": "1.0.5",
|
||||||
"glob": "7.1.2",
|
"glob": "7.1.2",
|
||||||
"growl": "1.10.3",
|
"growl": "1.10.5",
|
||||||
"he": "1.1.1",
|
"he": "1.1.1",
|
||||||
|
"minimatch": "3.0.4",
|
||||||
"mkdirp": "0.5.1",
|
"mkdirp": "0.5.1",
|
||||||
"supports-color": "4.4.0"
|
"supports-color": "5.4.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": {
|
|
||||||
"version": "2.11.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
|
|
||||||
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||||
@ -10602,19 +10603,13 @@
|
|||||||
"ms": "2.0.0"
|
"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": {
|
"supports-color": {
|
||||||
"version": "4.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz",
|
||||||
"integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==",
|
"integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"has-flag": "^2.0.0"
|
"has-flag": "^3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -11640,6 +11635,74 @@
|
|||||||
"got": "^8.0.1",
|
"got": "^8.0.1",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"mocha": "^4.0.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": {
|
"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": {
|
"tar": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
"prepare": "npm run build",
|
"prepare": "npm run build",
|
||||||
"repack": "babel-node ./repack.js",
|
"repack": "babel-node ./repack.js",
|
||||||
"semantic-release": "semantic-release",
|
"semantic-release": "semantic-release",
|
||||||
"test": "node ./test-challenges.js | tap-spec",
|
"test": "mocha --delay --reporter progress",
|
||||||
"unpack": "babel-node ./unpack.js"
|
"unpack": "babel-node ./unpack.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -45,6 +45,7 @@
|
|||||||
"babel-preset-stage-3": "^6.24.1",
|
"babel-preset-stage-3": "^6.24.1",
|
||||||
"babel-standalone": "^6.26.0",
|
"babel-standalone": "^6.26.0",
|
||||||
"browserify": "^16.2.2",
|
"browserify": "^16.2.2",
|
||||||
|
"chai": "4.2.0",
|
||||||
"enzyme": "^3.7.0",
|
"enzyme": "^3.7.0",
|
||||||
"enzyme-adapter-react-16": "^1.6.0",
|
"enzyme-adapter-react-16": "^1.6.0",
|
||||||
"eslint": "^4.19.1",
|
"eslint": "^4.19.1",
|
||||||
@ -61,6 +62,7 @@
|
|||||||
"jsdom": "^12.2.0",
|
"jsdom": "^12.2.0",
|
||||||
"lint-staged": "^7.2.0",
|
"lint-staged": "^7.2.0",
|
||||||
"lodash": "^4.17.10",
|
"lodash": "^4.17.10",
|
||||||
|
"mocha": "5.2.0",
|
||||||
"node-sass": "4.9.4",
|
"node-sass": "4.9.4",
|
||||||
"prettier": "^1.13.5",
|
"prettier": "^1.13.5",
|
||||||
"prettier-package-json": "^1.6.0",
|
"prettier-package-json": "^1.6.0",
|
||||||
@ -74,8 +76,6 @@
|
|||||||
"rework-visit":"1.0.0",
|
"rework-visit":"1.0.0",
|
||||||
"rx": "^4.1.0",
|
"rx": "^4.1.0",
|
||||||
"semantic-release": "^15.6.0",
|
"semantic-release": "^15.6.0",
|
||||||
"tap-spec": "^5.0.0",
|
|
||||||
"tape": "^4.9.1",
|
|
||||||
"validator": "^10.4.0"
|
"validator": "^10.4.0"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
@ -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(`<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,
|
|
||||||
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 `<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);
|
|
||||||
|
|
||||||
const jsdom = new JSDOM(`
|
|
||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
${scripts}
|
|
||||||
${head}
|
|
||||||
${solution}
|
|
||||||
${tail}
|
|
||||||
</html>
|
|
||||||
`, 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(`
|
|
||||||
<!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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
);
|
|
423
curriculum/test/test-challenges.js
Normal file
423
curriculum/test/test-challenges.js
Normal file
@ -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(`<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 => {
|
||||||
|
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,
|
||||||
|
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 `<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);
|
||||||
|
|
||||||
|
const dom = new JSDOM(`
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
${scripts}
|
||||||
|
${head}
|
||||||
|
${solution}
|
||||||
|
${tail}
|
||||||
|
</html>
|
||||||
|
`, 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(`
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<div id="root"><div id="challenge-node"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`, {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user