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:
Alex Chaffee
2018-01-19 14:03:17 -05:00
committed by Mrugesh Mohapatra
parent c754880476
commit 590f646263
11 changed files with 698 additions and 217 deletions

View File

@ -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>&nbsp;&nbsp;(<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
View 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;

View File

@ -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()) {
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;
module.exports = function getChallenges(challengesDir) {
if (!challengesDir) {
challengesDir = 'challenges';
}
return (filePath || '').split('-').splice(1).join('-');
}
module.exports = function getChallenges() {
try {
return getFilesFor('challenges')
return getFilesFor(challengesDir)
.map(function(data) {
const challengeSpec = require('./challenges/' + data.file);
const challengeSpec = require('./' + challengesDir + '/' + data.file);
let superInfo = superblockInfo(data.superBlock);
challengeSpec.fileName = data.file;
challengeSpec.superBlock = getSupName(data.superBlock);
challengeSpec.superOrder = getSupOrder(data.superBlock);
challengeSpec.superBlock = superInfo.name;
challengeSpec.superOrder = superInfo.order;
return challengeSpec;
});
} catch (e) {
console.error('error: ', e);
return [];
}
};

24
mongoIds.js Normal file
View 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;

View File

@ -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
View 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()
// });

View File

@ -1,136 +1,25 @@
/* eslint-disable no-eval, no-process-exit */
import _ from 'lodash';
/* 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) {
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
`);
}
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({
title,
id = '',
tests = [],
solutions = [],
head = [],
tail = [],
react = false,
redux = false,
reactRedux = false
}) {
validateObjectId(id, title);
solutions = solutions.filter(solution => !!solution);
tests = tests.filter(test => !!test);
// No support for async tests
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);
}
head = head.join('\n');
tail = tail.join('\n');
const plan = tests.length;
if (!plan) {
return Observable.just({
title,
type: 'missing'
});
}
return Observable.fromCallback(tape)(title)
.doOnNext(t => solutions.length ? t.plan(plan) : t.end())
.flatMap(t => {
if (solutions.length <= 0) {
t.comment('No solutions for ' + title);
return Observable.just({
title,
type: 'missing'
});
}
return Observable.just(t)
.map(fillAssert)
/* 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,
@ -199,10 +88,6 @@ function createTest({
}
const editor = {
getValue() { return code; },
getOriginalCode() { return originalCode; }
};
/* eslint-enable no-unused-vars */
try {
(() => {
@ -214,8 +99,67 @@ function createTest({
);
})();
} catch (e) {
t.fail(e);
tapTest.fail(e);
}
}
function createTest({
title,
id = '',
tests = [],
solutions = [],
head = [],
tail = [],
react = false,
redux = false,
reactRedux = false
}) {
mongoIds.check(id, title);
solutions = solutions.filter(solution => !!solution);
tests = tests.filter(test => !!test);
// No support for async tests
const isAsync = s => s.includes('(async () => ');
if (isAsync(tests.join(''))) {
console.log(`Replacing Async Tests for Challenge ${title}`);
tests = tests.map(challengeTestSource =>
isAsync(challengeTestSource) ?
"assert(true, 'message: great');" :
challengeTestSource);
}
head = head.join('\n');
tail = tail.join('\n');
const plan = tests.length;
if (!plan) {
return Observable.just({
title,
type: 'missing'
});
}
return Observable.fromCallback(tape)(title)
.doOnNext(tapTest =>
solutions.length ? tapTest.plan(plan) : tapTest.end())
.flatMap(tapTest => {
if (solutions.length <= 0) {
tapTest.comment('No solutions for ' + title);
return Observable.just({
title,
type: 'missing'
});
}
return Observable.just(tapTest)
.map(addAssertsToTapTest)
/* eslint-disable no-unused-vars */
// assert and code used within the eval
.doOnNext(assert => {
solutions.forEach(solution => {
tests.forEach(test => {
evaluateTest(solution, assert, react, redux, reactRedux,
head, tail, test, tapTest);
});
});
})
@ -248,6 +192,8 @@ Observable.from(getChallenges())
);
}
},
err => { throw err; },
err => {
throw err;
},
() => process.exit(0)
);

87
unpack.js Normal file
View 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
View 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
View 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
View 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 &lt;!--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};