feat(seed): "unpack" and "repack" scripts
add "npm run seed" as alias to "node seed" unpack tests and solution into HTML file; add titles and help text; style unpacked file enable running unpacked assert tests in browser Using browserify, compile "tape", "lodash", jQuery into "unpacked-bundle.js" for use during in-browser unpacked tests feat(seed): diff after repacking feat(seed): unpacked tests use Browser TAP chrome dev tool if available
This commit is contained in:
committed by
Mrugesh Mohapatra
parent
c754880476
commit
590f646263
13
README.md
13
README.md
@ -10,8 +10,17 @@ For each challenge section, there is a JSON file (fields documented below) conta
|
||||
|---|---|
|
||||
| `npm run test-challenges` | run all challenge tests (for each challenge JSON file, run all `tests` against all `solutions`) |
|
||||
| `npm run test` | run all JS tests in the system, including client, server, lint and challenge tests |
|
||||
| `node seed` | parses all the challenge JSON files and saves them into MongoDB (code is inside [index.js](index.js)) |
|
||||
| `npm run seed` <br> (<small>or</small> `node seed`) | parses all the challenge JSON files and saves them into MongoDB (code is inside [index.js](index.js)) |
|
||||
| `npm run commit` | interactive tool to help you build a good commit message |
|
||||
| `npm run unpack` | extract challenges from `seed/challenges` into `unpacked` subdirectory, one HTML page per challenge |
|
||||
| `npm run repack` | repack challenges from `unpacked` subdirectory into `seed/challenges` |
|
||||
|
||||
### unpack and repack
|
||||
|
||||
`npm run unpack` extracts challenges into separate files for easier viewing and editing. The files are `.gitignore`d and will *not* be checked in, and all mongo seed importing will keep using the existing system; this is essentially a tool for editing `challenge.json` files. These HTML files are self-contained and run their own tests -- open a browser JS console to see the test results.
|
||||
|
||||
`npm run repack` gathers up the unpacked/edited HTML files into challenge-block JSON files. Use `git diff` to see the changes
|
||||
|
||||
|
||||
## Links
|
||||
|
||||
@ -24,7 +33,7 @@ For each challenge section, there is a JSON file (fields documented below) conta
|
||||
|
||||
## Challenge Template
|
||||
|
||||
```json
|
||||
```
|
||||
{
|
||||
"id": "unique identifier (alphanumerical, mongodb id)",
|
||||
"title": "Challenge Title",
|
||||
|
52
addAssertsToTapTest.js
Normal file
52
addAssertsToTapTest.js
Normal file
@ -0,0 +1,52 @@
|
||||
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.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.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;
|
@ -4,64 +4,54 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const hiddenFile = /(^(\.|\/\.))|(.md$)/g;
|
||||
|
||||
function getFilesFor(dir) {
|
||||
return fs.readdirSync(path.join(__dirname, '/' + dir))
|
||||
let targetDir = path.join(__dirname, dir);
|
||||
return fs.readdirSync(targetDir)
|
||||
.filter(file => !hiddenFile.test(file))
|
||||
.map(function(file) {
|
||||
let superBlock;
|
||||
if (fs.statSync(path.join(__dirname, dir + '/' + file)).isFile()) {
|
||||
return { file: file };
|
||||
if (fs.statSync(path.join(targetDir, file)).isFile()) {
|
||||
return {file: file};
|
||||
}
|
||||
superBlock = file;
|
||||
return getFilesFor(dir + '/' + superBlock)
|
||||
return getFilesFor(path.join(dir, superBlock))
|
||||
.map(function(data) {
|
||||
return {
|
||||
file: superBlock + '/' + data.file,
|
||||
file: path.join(superBlock, data.file),
|
||||
superBlock: superBlock
|
||||
};
|
||||
});
|
||||
})
|
||||
.reduce(function(files, file) {
|
||||
if (!Array.isArray(file)) {
|
||||
files.push(file);
|
||||
return files;
|
||||
}
|
||||
return files.concat(file);
|
||||
.reduce(function(files, entry) {
|
||||
return files.concat(entry);
|
||||
}, []);
|
||||
}
|
||||
|
||||
function getSupOrder(filePath) {
|
||||
const order = parseInt((filePath || '').split('-')[0], 10);
|
||||
// check for NaN
|
||||
if (order !== order) {
|
||||
return 0;
|
||||
function superblockInfo(filePath) {
|
||||
let parts = (filePath || '').split('-');
|
||||
let order = parseInt(parts[0], 10);
|
||||
if (isNaN(order)) {
|
||||
return {order: 0, name: filePath};
|
||||
} else {
|
||||
return {
|
||||
order: order,
|
||||
name: parts.splice(1).join('-')
|
||||
};
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
function getSupName(filePath) {
|
||||
const order = parseInt((filePath || '').split('-')[0], 10);
|
||||
// check for NaN
|
||||
if (order !== order) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
return (filePath || '').split('-').splice(1).join('-');
|
||||
}
|
||||
|
||||
module.exports = function getChallenges() {
|
||||
try {
|
||||
return getFilesFor('challenges')
|
||||
.map(function(data) {
|
||||
const challengeSpec = require('./challenges/' + data.file);
|
||||
challengeSpec.fileName = data.file;
|
||||
challengeSpec.superBlock = getSupName(data.superBlock);
|
||||
challengeSpec.superOrder = getSupOrder(data.superBlock);
|
||||
|
||||
return challengeSpec;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('error: ', e);
|
||||
return [];
|
||||
module.exports = function getChallenges(challengesDir) {
|
||||
if (!challengesDir) {
|
||||
challengesDir = 'challenges';
|
||||
}
|
||||
return getFilesFor(challengesDir)
|
||||
.map(function(data) {
|
||||
const challengeSpec = require('./' + challengesDir + '/' + data.file);
|
||||
let superInfo = superblockInfo(data.superBlock);
|
||||
challengeSpec.fileName = data.file;
|
||||
challengeSpec.superBlock = superInfo.name;
|
||||
challengeSpec.superOrder = superInfo.order;
|
||||
return challengeSpec;
|
||||
});
|
||||
};
|
||||
|
24
mongoIds.js
Normal file
24
mongoIds.js
Normal file
@ -0,0 +1,24 @@
|
||||
import _ from 'lodash';
|
||||
import { isMongoId } from 'validator';
|
||||
|
||||
class MongoIds {
|
||||
constructor() {
|
||||
this.knownIds = [];
|
||||
}
|
||||
check(id, title) {
|
||||
if (!isMongoId(id)) {
|
||||
throw new Error(`Expected a valid ObjectId for ${title}, but got ${id}`);
|
||||
}
|
||||
const idIndex = _.findIndex(this.knownIds, existing => id === existing);
|
||||
if (idIndex !== -1) {
|
||||
throw new Error(`
|
||||
All challenges must have a unique id.
|
||||
|
||||
The id for ${title} is already assigned
|
||||
`);
|
||||
}
|
||||
this.knownIds = [ ...this.knownIds, id ];
|
||||
}
|
||||
}
|
||||
|
||||
export default MongoIds;
|
@ -52,16 +52,16 @@ function createNewTranslations(challenge) {
|
||||
newTranslation = {};
|
||||
newTranslation[matches[1]] = challenge[oldKey];
|
||||
translations[tag] = translations[tag] ?
|
||||
Object.assign({}, translations[tag], newTranslation) :
|
||||
Object.assign({}, newTranslation);
|
||||
({...translations[tag], ...newTranslation}) :
|
||||
({...newTranslation});
|
||||
return translations;
|
||||
}
|
||||
matches = oldKey.match(oldNameRegex);
|
||||
tag = normalizeLangTag(matches[1]);
|
||||
newTranslation = { title: challenge[oldKey] };
|
||||
translations[tag] = translations[tag] ?
|
||||
Object.assign({}, translations[tag], newTranslation) :
|
||||
Object.assign({}, newTranslation);
|
||||
({...translations[tag], ...newTranslation}) :
|
||||
({...newTranslation});
|
||||
return translations;
|
||||
}, {});
|
||||
}
|
||||
@ -71,11 +71,10 @@ function normalizeChallenge(challenge) {
|
||||
challenge.translations = challenge.translations || {};
|
||||
var hasOldTranslations = keys.some(hasOldTranslation);
|
||||
if (hasOldTranslations) {
|
||||
challenge.translations = Object.assign(
|
||||
{},
|
||||
challenge.translations,
|
||||
createNewTranslations(challenge)
|
||||
);
|
||||
challenge.translations = ({
|
||||
...challenge.translations,
|
||||
...createNewTranslations(challenge)
|
||||
});
|
||||
}
|
||||
challenge.translations = sortTranslationsKeys(challenge.translations);
|
||||
// remove old translations from the top level
|
||||
|
76
repack.js
Normal file
76
repack.js
Normal file
@ -0,0 +1,76 @@
|
||||
/* eslint-disable no-eval, no-process-exit */
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import {UnpackedChallenge, ChallengeFile} from './unpackedChallenge';
|
||||
|
||||
const jsdiff = require('diff');
|
||||
|
||||
// Repack all challenges from all
|
||||
// seed/unpacked/00-foo/bar/000-id.html files
|
||||
// into
|
||||
// seed/challenges/00-foo/bar.json files
|
||||
|
||||
let unpackedRoot = path.join(__dirname, 'unpacked');
|
||||
let seedChallengesRoot = path.join(__dirname, 'challenges');
|
||||
|
||||
function directoriesIn(parentDir) {
|
||||
return fs.readdirSync(parentDir)
|
||||
.filter(entry => fs.statSync(path.join(parentDir, entry)).isDirectory());
|
||||
}
|
||||
|
||||
let superBlocks = directoriesIn(unpackedRoot);
|
||||
console.log(superBlocks);
|
||||
|
||||
function diffFiles(originalFilePath, changedFilePath) {
|
||||
// todo: async
|
||||
console.log(`diffing ${originalFilePath} and ${changedFilePath}`);
|
||||
let original = fs.readFileSync(originalFilePath).toString();
|
||||
let repacked = fs.readFileSync(changedFilePath).toString();
|
||||
|
||||
let changes = jsdiff.diffLines(original, repacked, { newlineIsToken: true });
|
||||
changes.forEach((change) => {
|
||||
if (change.added || change.removed) {
|
||||
console.log(JSON.stringify(change, null, 2));
|
||||
}
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
superBlocks.forEach(superBlock => {
|
||||
let superBlockPath = path.join(unpackedRoot, superBlock);
|
||||
let blocks = directoriesIn(superBlockPath);
|
||||
blocks.forEach(blockName => {
|
||||
let blockPath = path.join(superBlockPath, blockName);
|
||||
let blockFilePath = path.join(blockPath, blockName + '.json');
|
||||
let block = require(blockFilePath);
|
||||
let index = 0;
|
||||
block.challenges.forEach(challengeJson => {
|
||||
let unpackedChallenge =
|
||||
new UnpackedChallenge(blockPath, challengeJson, index);
|
||||
let unpackedFile = unpackedChallenge.challengeFile();
|
||||
let chunks = unpackedFile.readChunks();
|
||||
|
||||
Object.assign(block.challenges[ index ], chunks);
|
||||
|
||||
index += 1;
|
||||
});
|
||||
|
||||
let outputFilePath =
|
||||
path.join(seedChallengesRoot, superBlock, blockName + '.json');
|
||||
// todo: async
|
||||
fs.writeFileSync(outputFilePath, JSON.stringify(block, null, 2));
|
||||
|
||||
// todo: make this a command-line option instead
|
||||
let doDiff = false;
|
||||
if (doDiff) {
|
||||
diffFiles(blockFilePath, outputFilePath);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// let challenges = getChallenges();
|
||||
// challenges.forEach(challengeBlock => {
|
||||
// console.log()
|
||||
// });
|
@ -1,76 +1,106 @@
|
||||
/* eslint-disable no-eval, no-process-exit */
|
||||
import _ from 'lodash';
|
||||
import { Observable } from 'rx';
|
||||
/* eslint-disable no-eval, no-process-exit, no-unused-vars */
|
||||
|
||||
import {Observable} from 'rx';
|
||||
import tape from 'tape';
|
||||
import { isMongoId } from 'validator';
|
||||
|
||||
import getChallenges from './getChallenges';
|
||||
import { modern } from '../common/app/utils/challengeTypes';
|
||||
import MongoIds from './mongoIds';
|
||||
import addAssertsToTapTest from './addAssertsToTapTest';
|
||||
|
||||
const notMongoId = id => !isMongoId(id);
|
||||
let mongoIds = new MongoIds();
|
||||
|
||||
let existingIds = [];
|
||||
function evaluateTest(solution, assert,
|
||||
react, redux, reactRedux,
|
||||
head, tail,
|
||||
test, tapTest) {
|
||||
|
||||
let code = solution;
|
||||
|
||||
/* NOTE: Provide dependencies for React/Redux challenges
|
||||
* and configure testing environment
|
||||
*/
|
||||
let React,
|
||||
ReactDOM,
|
||||
Redux,
|
||||
ReduxThunk,
|
||||
ReactRedux,
|
||||
Enzyme,
|
||||
document;
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
if (react || redux || reactRedux) {
|
||||
// Provide dependencies, just provide all of them
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
Redux = require('redux');
|
||||
ReduxThunk = require('redux-thunk');
|
||||
ReactRedux = require('react-redux');
|
||||
Enzyme = require('enzyme');
|
||||
const Adapter15 = require('enzyme-adapter-react-15');
|
||||
Enzyme.configure({ adapter: new Adapter15() });
|
||||
|
||||
/* Transpile ALL the code
|
||||
* (we may use JSX in head or tail or tests, too): */
|
||||
const transform = require('babel-standalone').transform;
|
||||
const options = { presets: [ 'es2015', 'react' ] };
|
||||
|
||||
head = transform(head, options).code;
|
||||
solution = transform(solution, options).code;
|
||||
tail = transform(tail, options).code;
|
||||
test = transform(test, options).code;
|
||||
|
||||
const { JSDOM } = require('jsdom');
|
||||
// Mock DOM document for ReactDOM.render method
|
||||
const jsdom = new JSDOM(`<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="challenge-node"></div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
const { window } = jsdom;
|
||||
|
||||
// Mock DOM for ReactDOM tests
|
||||
document = window.document;
|
||||
global.window = window;
|
||||
global.document = window.document;
|
||||
|
||||
function validateObjectId(id, title) {
|
||||
if (notMongoId(id)) {
|
||||
throw new Error(`Expected a vaild ObjectId for ${title}, got ${id}`);
|
||||
}
|
||||
const idIndex = _.findIndex(existingIds, existing => id === existing);
|
||||
if (idIndex !== -1) {
|
||||
throw new Error(`
|
||||
All challenges must have a unique id.
|
||||
|
||||
The id for ${title} is already assigned
|
||||
`);
|
||||
/* eslint-enable no-unused-vars */
|
||||
try {
|
||||
(() => {
|
||||
return eval(
|
||||
head + '\n;;' +
|
||||
solution + '\n;;' +
|
||||
tail + '\n;;' +
|
||||
test
|
||||
);
|
||||
})();
|
||||
} catch (e) {
|
||||
tapTest.fail(e);
|
||||
}
|
||||
existingIds = [ ...existingIds, id ];
|
||||
return;
|
||||
}
|
||||
|
||||
function createIsAssert(t, isThing) {
|
||||
const { assert } = t;
|
||||
return function() {
|
||||
const args = [...arguments];
|
||||
args[0] = isThing(args[0]);
|
||||
assert.apply(t, args);
|
||||
};
|
||||
}
|
||||
|
||||
function fillAssert(t) {
|
||||
const assert = t.assert;
|
||||
|
||||
assert.isArray = createIsAssert(t, _.isArray);
|
||||
assert.isBoolean = createIsAssert(t, _.isBoolean);
|
||||
assert.isString = createIsAssert(t, _.isString);
|
||||
assert.isNumber = createIsAssert(t, _.isNumber);
|
||||
assert.isUndefined = createIsAssert(t, _.isUndefined);
|
||||
|
||||
assert.deepEqual = t.deepEqual;
|
||||
assert.equal = t.equal;
|
||||
assert.strictEqual = t.equal;
|
||||
|
||||
assert.sameMembers = function sameMembers() {
|
||||
const [ first, second, ...args] = arguments;
|
||||
assert.apply(
|
||||
t,
|
||||
[
|
||||
_.difference(first, second).length === 0 &&
|
||||
_.difference(second, first).length === 0
|
||||
].concat(args)
|
||||
);
|
||||
};
|
||||
|
||||
assert.includeMembers = function includeMembers() {
|
||||
const [ first, second, ...args] = arguments;
|
||||
assert.apply(t, [_.difference(second, first).length === 0].concat(args));
|
||||
};
|
||||
|
||||
assert.match = function match() {
|
||||
const [value, regex, ...args] = arguments;
|
||||
assert.apply(t, [regex.test(value)].concat(args));
|
||||
};
|
||||
|
||||
return assert;
|
||||
}
|
||||
|
||||
function createTest({
|
||||
@ -84,7 +114,8 @@ function createTest({
|
||||
redux = false,
|
||||
reactRedux = false
|
||||
}) {
|
||||
validateObjectId(id, title);
|
||||
mongoIds.check(id, title);
|
||||
|
||||
solutions = solutions.filter(solution => !!solution);
|
||||
tests = tests.filter(test => !!test);
|
||||
|
||||
@ -92,7 +123,10 @@ function createTest({
|
||||
const isAsync = s => s.includes('(async () => ');
|
||||
if (isAsync(tests.join(''))) {
|
||||
console.log(`Replacing Async Tests for Challenge ${title}`);
|
||||
tests = tests.map(t => isAsync(t) ? "assert(true, 'message: great');" : t);
|
||||
tests = tests.map(challengeTestSource =>
|
||||
isAsync(challengeTestSource) ?
|
||||
"assert(true, 'message: great');" :
|
||||
challengeTestSource);
|
||||
}
|
||||
|
||||
head = head.join('\n');
|
||||
@ -106,116 +140,26 @@ function createTest({
|
||||
}
|
||||
|
||||
return Observable.fromCallback(tape)(title)
|
||||
.doOnNext(t => solutions.length ? t.plan(plan) : t.end())
|
||||
.flatMap(t => {
|
||||
.doOnNext(tapTest =>
|
||||
solutions.length ? tapTest.plan(plan) : tapTest.end())
|
||||
.flatMap(tapTest => {
|
||||
if (solutions.length <= 0) {
|
||||
t.comment('No solutions for ' + title);
|
||||
tapTest.comment('No solutions for ' + title);
|
||||
return Observable.just({
|
||||
title,
|
||||
type: 'missing'
|
||||
});
|
||||
}
|
||||
|
||||
return Observable.just(t)
|
||||
.map(fillAssert)
|
||||
return Observable.just(tapTest)
|
||||
.map(addAssertsToTapTest)
|
||||
/* eslint-disable no-unused-vars */
|
||||
// assert and code used within the eval
|
||||
.doOnNext(assert => {
|
||||
solutions.forEach(solution => {
|
||||
// Original code string
|
||||
const originalCode = solution;
|
||||
tests.forEach(test => {
|
||||
let code = solution;
|
||||
|
||||
/* NOTE: Provide dependencies for React/Redux challenges
|
||||
* and configure testing environment
|
||||
*/
|
||||
|
||||
let React,
|
||||
ReactDOM,
|
||||
Redux,
|
||||
ReduxThunk,
|
||||
ReactRedux,
|
||||
Enzyme,
|
||||
document;
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
if (react || redux || reactRedux) {
|
||||
// Provide dependencies, just provide all of them
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
Redux = require('redux');
|
||||
ReduxThunk = require('redux-thunk');
|
||||
ReactRedux = require('react-redux');
|
||||
Enzyme = require('enzyme');
|
||||
const Adapter15 = require('enzyme-adapter-react-15');
|
||||
Enzyme.configure({ adapter: new Adapter15() });
|
||||
|
||||
/* Transpile ALL the code
|
||||
* (we may use JSX in head or tail or tests, too): */
|
||||
const transform = require('babel-standalone').transform;
|
||||
const options = { presets: [ 'es2015', 'react' ] };
|
||||
|
||||
head = transform(head, options).code;
|
||||
solution = transform(solution, options).code;
|
||||
tail = transform(tail, options).code;
|
||||
test = transform(test, options).code;
|
||||
|
||||
const { JSDOM } = require('jsdom');
|
||||
// Mock DOM document for ReactDOM.render method
|
||||
const jsdom = new JSDOM(`<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<div id="challenge-node"></div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
const { window } = jsdom;
|
||||
|
||||
// Mock DOM for ReactDOM tests
|
||||
document = window.document;
|
||||
global.window = window;
|
||||
global.document = window.document;
|
||||
|
||||
}
|
||||
|
||||
const editor = {
|
||||
getValue() { return code; },
|
||||
getOriginalCode() { return originalCode; }
|
||||
};
|
||||
/* eslint-enable no-unused-vars */
|
||||
try {
|
||||
(() => {
|
||||
return eval(
|
||||
head + '\n;;' +
|
||||
solution + '\n;;' +
|
||||
tail + '\n;;' +
|
||||
test
|
||||
);
|
||||
})();
|
||||
} catch (e) {
|
||||
t.fail(e);
|
||||
}
|
||||
evaluateTest(solution, assert, react, redux, reactRedux,
|
||||
head, tail, test, tapTest);
|
||||
});
|
||||
});
|
||||
})
|
||||
@ -244,10 +188,12 @@ Observable.from(getChallenges())
|
||||
if (noSolutions) {
|
||||
console.log(
|
||||
'# These challenges have no solutions\n- [ ] ' +
|
||||
noSolutions.join('\n- [ ] ')
|
||||
noSolutions.join('\n- [ ] ')
|
||||
);
|
||||
}
|
||||
},
|
||||
err => { throw err; },
|
||||
err => {
|
||||
throw err;
|
||||
},
|
||||
() => process.exit(0)
|
||||
);
|
||||
|
87
unpack.js
Normal file
87
unpack.js
Normal file
@ -0,0 +1,87 @@
|
||||
/* eslint-disable no-eval, no-process-exit */
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import browserify from 'browserify';
|
||||
import getChallenges from './getChallenges';
|
||||
import {UnpackedChallenge, ChallengeFile} from './unpackedChallenge';
|
||||
|
||||
// Unpack all challenges
|
||||
// from all seed/challenges/00-foo/bar.json files
|
||||
// into seed/unpacked/00-foo/bar/000-id.html files
|
||||
//
|
||||
// todo: unpack translations too
|
||||
// todo: use common/app/routes/Challenges/utils/index.js:15 maps
|
||||
// to determine format/style for non-JS tests
|
||||
// todo: figure out embedded images etc. served from elsewhere in the project
|
||||
// todo: prettier/clearer CSS
|
||||
|
||||
// bundle up the test-running JS
|
||||
function createUnpackedBundle() {
|
||||
let unpackedFile = path.join(__dirname, 'unpacked.js');
|
||||
let b = browserify(unpackedFile).bundle();
|
||||
b.on('error', console.error);
|
||||
let unpackedBundleFile =
|
||||
path.join(__dirname, 'unpacked', 'unpacked-bundle.js');
|
||||
const bundleFileStream = fs.createWriteStream(unpackedBundleFile);
|
||||
bundleFileStream.on('finish', () => {
|
||||
console.log('Wrote bundled JS into ' + unpackedBundleFile);
|
||||
});
|
||||
bundleFileStream.on('pipe', () => {
|
||||
console.log('Writing bundled JS...');
|
||||
});
|
||||
bundleFileStream.on('error', console.error);
|
||||
b.pipe(bundleFileStream);
|
||||
// bundleFileStream.end(); // do not do this prematurely!
|
||||
}
|
||||
|
||||
let currentlyUnpackingDir = null;
|
||||
|
||||
function unpackChallengeBlock(challengeBlock) {
|
||||
let challengeBlockPath = path.parse(challengeBlock.fileName);
|
||||
let unpackedChallengeBlockDir = path.join(
|
||||
__dirname,
|
||||
'unpacked',
|
||||
challengeBlockPath.dir,
|
||||
challengeBlockPath.name
|
||||
);
|
||||
fs.mkdirp(unpackedChallengeBlockDir, (err) => {
|
||||
if (err && err.code !== 'EEXIST') {
|
||||
console.log(err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (currentlyUnpackingDir !== challengeBlockPath.dir) {
|
||||
currentlyUnpackingDir = challengeBlockPath.dir;
|
||||
console.log(`Unpacking into ${currentlyUnpackingDir}:`);
|
||||
}
|
||||
console.log(` ${challengeBlock.name}`);
|
||||
|
||||
// write a copy of the challenge block into unpacked dir
|
||||
delete challengeBlock.fileName;
|
||||
delete challengeBlock.superBlock;
|
||||
delete challengeBlock.superOrder;
|
||||
let challengeBlockCopy =
|
||||
new ChallengeFile(
|
||||
unpackedChallengeBlockDir,
|
||||
challengeBlockPath.name,
|
||||
'.json');
|
||||
challengeBlockCopy.write(JSON.stringify(challengeBlock, null, 2));
|
||||
|
||||
// unpack each challenge into an HTML file
|
||||
let index = 0;
|
||||
challengeBlock.challenges.forEach(challenge => {
|
||||
new UnpackedChallenge(
|
||||
unpackedChallengeBlockDir,
|
||||
challenge,
|
||||
index
|
||||
).unpack();
|
||||
index += 1;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createUnpackedBundle();
|
||||
let challenges = getChallenges();
|
||||
challenges.forEach(challengeBlock => {
|
||||
unpackChallengeBlock(challengeBlock);
|
||||
});
|
22
unpacked.css
Normal file
22
unpacked.css
Normal file
@ -0,0 +1,22 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
script.unpacked, pre.unpacked {
|
||||
display: block;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
white-space: pre;
|
||||
border: 1px solid blue;
|
||||
background: #EFEFEF;
|
||||
padding: .5em 1em;
|
||||
margin: 1em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
div.unpacked {
|
||||
border: 1px solid black;
|
||||
padding: .5em 1em;
|
||||
margin: 1em;
|
||||
overflow: auto;
|
||||
}
|
18
unpacked.js
Normal file
18
unpacked.js
Normal file
@ -0,0 +1,18 @@
|
||||
/* eslint-disable no-unused-vars,max-len */
|
||||
window._ = require('lodash');
|
||||
window.test = require('tape').test;
|
||||
|
||||
// check for Browser TAP chrome extension, available here:
|
||||
// https://chrome.google.com/webstore/detail/browser-tap/ncfblaiipckncgeipgmpdioedcdmofei?hl=en
|
||||
if (window.tapExtension) {
|
||||
window.test = window.tapExtension(window.test);
|
||||
}
|
||||
|
||||
window.addAssertsToTapTest = require('./addAssertsToTapTest');
|
||||
window.$ = require('jquery');
|
||||
|
||||
test('framework', function(t) {
|
||||
t.plan(1);
|
||||
t.equal(1, 1, 'one equals one');
|
||||
});
|
||||
|
258
unpackedChallenge.js
Normal file
258
unpackedChallenge.js
Normal file
@ -0,0 +1,258 @@
|
||||
/* eslint-disable no-inline-comments */
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import _ from 'lodash';
|
||||
|
||||
const jsonLinePrefix = '//--JSON:';
|
||||
|
||||
class ChallengeFile {
|
||||
constructor(dir, name, suffix) {
|
||||
this.dir = dir;
|
||||
this.name = name;
|
||||
this.suffix = suffix;
|
||||
}
|
||||
|
||||
filePath() {
|
||||
return path.join(this.dir, this.name + this.suffix);
|
||||
}
|
||||
|
||||
write(contents) {
|
||||
if (_.isArray(contents)) {
|
||||
contents = contents.join('\n');
|
||||
}
|
||||
fs.writeFile(this.filePath(), contents, err => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
readChunks() {
|
||||
// todo: make this work async
|
||||
// todo: make sure it works with encodings
|
||||
let data = fs.readFileSync(this.filePath());
|
||||
let lines = data.toString().split(/(?:\r\n|\r|\n)/g);
|
||||
let chunks = {};
|
||||
let readingChunk = null;
|
||||
lines.forEach(line => {
|
||||
let chunkEnd = /(<!|\/\*)--end--/;
|
||||
let chunkStart = /(<!|\/\*)--(\w+)--/;
|
||||
|
||||
line = line.toString();
|
||||
|
||||
if (chunkEnd.test(line)) {
|
||||
if (!readingChunk) {
|
||||
throw 'Encountered --end-- without being in a chunk';
|
||||
}
|
||||
readingChunk = null;
|
||||
} else if (chunkStart.test(line)) {
|
||||
let chunkName = line.match(chunkStart)[ 2 ];
|
||||
if (readingChunk) {
|
||||
throw `Encountered chunk ${chunkName} start `
|
||||
+ `while already reading ${readingChunk}:
|
||||
${line}`;
|
||||
}
|
||||
readingChunk = chunkName;
|
||||
} else if (readingChunk) {
|
||||
if (line.startsWith(jsonLinePrefix)) {
|
||||
line = JSON.parse(line.slice(jsonLinePrefix.length));
|
||||
}
|
||||
if (!chunks[readingChunk]) {
|
||||
chunks[readingChunk] = [];
|
||||
}
|
||||
// don't push empty top lines
|
||||
if (!(!line && chunks[readingChunk].length === 0)) {
|
||||
chunks[ readingChunk ].push(line);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// hack to deal with solutions field being an array of a single string
|
||||
// instead of an array of lines like other fields
|
||||
if (chunks.solutions) {
|
||||
chunks.solutions = [chunks.solutions.join('\n')];
|
||||
}
|
||||
|
||||
// console.log(JSON.stringify(chunks, null, 2));
|
||||
return chunks;
|
||||
}
|
||||
}
|
||||
|
||||
export {ChallengeFile};
|
||||
|
||||
class UnpackedChallenge {
|
||||
constructor(targetDir, challengeJson, index) {
|
||||
this.targetDir = targetDir;
|
||||
this.index = index;
|
||||
|
||||
// todo: merge challengeJson properties into this object?
|
||||
this.challenge = challengeJson;
|
||||
|
||||
// extract names of block and superblock from path
|
||||
// note: this is a bit redundant with the
|
||||
// fileName,superBlock,superOrder fields
|
||||
// that getChallenges() adds to the challenge JSON
|
||||
let targetDirPath = path.parse(targetDir);
|
||||
let parentDirPath = path.parse(targetDirPath.dir);
|
||||
// superBlockName e.g. "03-front-end-libraries"
|
||||
this.superBlockName = parentDirPath.base;
|
||||
// challengeBlockName e.g. "bootstrap"
|
||||
this.challengeBlockName = targetDirPath.base;
|
||||
}
|
||||
|
||||
unpack() {
|
||||
this.challengeFile()
|
||||
.write(this.unpackedHTML());
|
||||
}
|
||||
|
||||
challengeFile() {
|
||||
return new ChallengeFile(this.targetDir, this.baseName(), '.html');
|
||||
}
|
||||
|
||||
baseName() {
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
let prefix = ((this.index < 10) ? '00' : (this.index < 100) ? '0' : '')
|
||||
+ this.index;
|
||||
return `${prefix}-${this.challenge.id}`;
|
||||
}
|
||||
|
||||
expandedDescription(description) {
|
||||
let out = [];
|
||||
description.forEach(part => {
|
||||
if (_.isString(part)) {
|
||||
out.push(part.toString());
|
||||
} else {
|
||||
// Descriptions are weird since sometimes they're text and sometimes
|
||||
// they're "steps" which appear one at a time with optional pix and
|
||||
// captions and links, or "questions" with choices and expanations...
|
||||
// For now we preserve non-string descriptions via JSON but this is
|
||||
// not a great solution.
|
||||
// It would be better if "steps" and "description" were separate fields.
|
||||
// For the record, the (unnamed) fields in step are:
|
||||
// 0: image URL
|
||||
// 1: caption
|
||||
// 2: text
|
||||
// 3: link URL
|
||||
out.push(jsonLinePrefix + JSON.stringify(part));
|
||||
}
|
||||
});
|
||||
// indent by 2
|
||||
return out;
|
||||
}
|
||||
|
||||
expandedTests(tests) {
|
||||
if (!tests) {
|
||||
return [];
|
||||
}
|
||||
let out = [];
|
||||
tests.forEach(test => {
|
||||
if (_.isString(test)) {
|
||||
out.push(test);
|
||||
} else {
|
||||
// todo: figure out what to do about these id-title challenge links
|
||||
out.push(jsonLinePrefix + JSON.stringify(test));
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
unpackedHTML() {
|
||||
let text = [];
|
||||
text.push('<html>');
|
||||
text.push('<head>');
|
||||
text.push('<link rel="stylesheet" href="../../../unpacked.css">');
|
||||
text.push('<!-- shim to enable running the tests in-browser -->');
|
||||
text.push('<script src="../../unpacked-bundle.js"></script>');
|
||||
text.push('</head>');
|
||||
text.push('<body>');
|
||||
text.push(`<h1>${this.challenge.title}</h1>`);
|
||||
text.push(`<p>This is the <b>unpacked</b> version of
|
||||
<code>${this.superBlockName}/${this.challengeBlockName}</code>
|
||||
(challenge id <code>${this.challenge.id}</code>).</p>`);
|
||||
text.push('<p>Open the JavaScript console to see test results.</p>');
|
||||
|
||||
// text.push(`<p>Edit this HTML file (between <!--s only!)
|
||||
// and run <code>npm repack ???</code>
|
||||
// to incorporate your changes into the challenge database.</p>`);
|
||||
|
||||
text.push('');
|
||||
text.push('<h2>Description</h2>');
|
||||
text.push('<div class="unpacked description">');
|
||||
text.push('<!--description-->');
|
||||
text.push(this.expandedDescription(this.challenge.description).join('\n'));
|
||||
text.push('<!--end-->');
|
||||
text.push('</div>');
|
||||
|
||||
text.push('');
|
||||
text.push('<h2>Seed</h2>');
|
||||
text.push('<!--seed--><pre class="unpacked">');
|
||||
if (this.challenge.seed) {
|
||||
text.push(text, this.challenge.seed.join('\n'));
|
||||
}
|
||||
text.push('<!--end-->');
|
||||
text.push('</pre>');
|
||||
|
||||
// Q: What is the difference between 'seed' and 'challengeSeed' ?
|
||||
text.push('');
|
||||
text.push('<h2>Challenge Seed</h2>');
|
||||
text.push('<!--challengeSeed--><pre class="unpacked">');
|
||||
if (this.challenge.challengeSeed) {
|
||||
text.push(text, this.challenge.challengeSeed.join('\n'));
|
||||
}
|
||||
text.push('<!--end-->');
|
||||
text.push('</pre>');
|
||||
|
||||
text.push('');
|
||||
text.push('<h2>Head</h2>');
|
||||
text.push('<!--head--><script class="unpacked head">');
|
||||
if (this.challenge.head) {
|
||||
text.push(text, this.challenge.head.join('\n'));
|
||||
}
|
||||
text.push('</script><!--end-->');
|
||||
|
||||
text.push('');
|
||||
text.push('<h2>Solution</h2>');
|
||||
text.push(
|
||||
'<!--solutions--><script class="unpacked solution" id="solution">'
|
||||
);
|
||||
// Note: none of the challenges have more than one solution
|
||||
// todo: should we deal with multiple solutions or not?
|
||||
if (this.challenge.solutions && this.challenge.solutions.length > 0) {
|
||||
let solution = this.challenge.solutions[0];
|
||||
text.push(solution);
|
||||
}
|
||||
text.push('</script><!--end-->');
|
||||
|
||||
text.push('');
|
||||
text.push('<h2>Tail</h2>');
|
||||
text.push('<!--tail--><script class="unpacked tail">');
|
||||
if (this.challenge.tail) {
|
||||
text.push(text, this.challenge.tail.join('\n'));
|
||||
}
|
||||
text.push('</script><!--end-->');
|
||||
|
||||
text.push('');
|
||||
text.push('<h2>Tests</h2>');
|
||||
text.push('<script class="unpacked tests">');
|
||||
text.push(`test(\'${this.challenge.title} challenge tests\', ` +
|
||||
'function(t) {');
|
||||
text.push('let assert = addAssertsToTapTest(t);');
|
||||
text.push('let code = document.getElementById(\'solution\').innerText;');
|
||||
text.push('t.plan(' +
|
||||
(this.challenge.tests ? this.challenge.tests.length : 0) +
|
||||
');');
|
||||
text.push('/*--tests--*/');
|
||||
text.push(this.expandedTests(this.challenge.tests).join('\n'));
|
||||
text.push('/*--end--*/');
|
||||
text.push('});')
|
||||
text.push('</script>');
|
||||
|
||||
text.push('');
|
||||
text.push('</body>');
|
||||
text.push('</html>');
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
export {UnpackedChallenge};
|
||||
|
Reference in New Issue
Block a user