diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9ab9d8f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceFolder}/sample.test.js" + } + ] +} \ No newline at end of file diff --git a/gunner/lib/constants.js b/gunner/lib/constants.js deleted file mode 100644 index dc5fb3c..0000000 --- a/gunner/lib/constants.js +++ /dev/null @@ -1,6 +0,0 @@ -const constants = { - pass: 'pass', - fail: 'fail', -}; - -module.exports = constants; diff --git a/gunner/lib/runTests.js b/gunner/lib/runTests.js deleted file mode 100644 index 2a22fc7..0000000 --- a/gunner/lib/runTests.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; - -const { isPromise } = require('../../util/helpers'); -const { pass, fail } = require('./constants'); - -const runTests = instance => { - - const beforeAll = () => Promise.map( - instance.__hooks__.before['@start'], - hook => hook.run(), - ); - - const beforeEvery = () => Promise.mapSeries( - instance.__hooks__.before['*'] || [], - hook => hook.run(), - ); - - const runner = () => Promise.mapSeries(instance.__tests__, each => { - - const beforeThis = () => Promise.mapSeries( - instance.__hooks__.before[each.description] || [], - hook => hook.run(), - ); - - const afterThis = () => Promise.mapSeries( - instance.__hooks__.after[each.description] || [], - hook => hook.run(), - ); - - return beforeEvery().then(() => beforeThis()).then(() => { - - const pred = each.test(); - - /* There are 4 different cases at play: - 1. A plain expect() is returned. - 2. An array of [ expect() ] is returned - 3. A plain expect() is wrapped in a promise - 4. An array of [ expect() ] is wrapped in a promise. - Here we normalise all of them into something we can process */ - - if (!isPromise(pred) && !(pred && isPromise(pred[0]))) - throw new Error(`Malformed test '${each.description}'`); - const toTest = Array.isArray(pred) - ? Promise.all(pred) - : pred.then(x => Array.isArray(x) ? Promise.all(x) : x); - - return toTest - .then(() => ({ description: each.description, result: pass })) - .catch(e => ({ description: each.description, result: fail, error: e })); - - }) - .then(result => afterThis().then(() => result)); - - }); - - const afterAll = () => Promise.mapSeries( - instance.__hooks__.before['@end'], - hook => hook.run(), - ); - - return beforeAll() - .then(() => runner()) - .then(results => afterAll().then(() => results)); - -}; - -module.exports = runTests; diff --git a/index.js b/index.js index cca91cd..1192c07 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -module.exports = require('./gunner'); \ No newline at end of file +module.exports = require('./src/gunner'); \ No newline at end of file diff --git a/package.json b/package.json index 6c21e20..065e1d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@klenty/gunner", - "version": "0.3.0", + "version": "0.3.1", "description": "Zero magic, fast test-runner and assertion framework. No magic globals.", "main": "index.js", "repository": { @@ -22,7 +22,9 @@ "homepage": "https://github.com/klenty/gunner#readme", "dependencies": { "@codefeathers/iseq": "^1.2.1", + "@codefeathers/promise.object": "^0.9.5", "bluebird": "^3.5.1", + "chalk": "^2.4.1", "eslint": "^5.2.0", "json-stringify-safe": "^5.0.1" } diff --git a/sample.test.js b/sample.test.js index 2285f5c..73fef4b 100644 --- a/sample.test.js +++ b/sample.test.js @@ -1,7 +1,17 @@ -const Gunner = require('./gunner'); +/** + * This file contains random tests + * used during development + */ + +const Gunner = require('./index.js'); const gunner = new Gunner({ name: 'sample tests' }); const a = 1; +gunner.before(Gunner.Start, () => console.log('Started tests!')); +gunner.before(Gunner.End, () => console.log('Ended tests!')); +let runCount = 1; +gunner.before('*', () => console.log(`Running test ${runCount++}`)); + gunner.test('should automatically pass', expect => expect().done()); gunner.test(`should be equal`, expect => expect(1).equal(1)); gunner.test(`objects are deep equal`, expect => expect({ a: 1 }).deepEqual({ a: 1 })); @@ -16,6 +26,16 @@ gunner.test('should be a Promise (rejected)', expect => gunner.test('should resolve to 5', expect => expect(Promise.resolve(5)).resolvesTo(5)); +gunner.before( + 'file must have hello as content', + () => console.log('>> starting test! file must have hello as content'), +); + +gunner.after( + 'file must have hello as content', + () => console.log('>> finished test! file must have hello as content'), +); + gunner.test('file must have hello as content', async expect => { const { readFile } = require('fs').promises; const file = await readFile('./hello.txt', { encoding: 'utf8' }); @@ -25,6 +45,12 @@ gunner.test('file must have hello as content', async expect => { ]; }); +gunner.test('(should fail) Value is not a Promise', expect => + expect(5).isPromise()); + +gunner.test('(should fail) Error is not a Promise', expect => + expect(flamethrower()).isPromise()); + gunner.test(`(should fail) objects aren't deeply equal`, expect => expect({a : 1}).deepEqual({ a: 2 })); gunner.test('(should fail) promise must reject', expect => @@ -52,7 +78,9 @@ gunner.test('(should fail) should catch error', expect => { }); gunner.test('(should fail) should not resolve to 5', expect => - expect(Promise.resolve({})).resolvesTo(5)); + expect(Promise.resolve()).resolvesTo(5)); +const trace = process.argv.slice(2).indexOf('--trace') !== -1; +const log = process.argv.slice(2).indexOf('--log') !== -1; -gunner.run({ trace: true}); +gunner.run({ trace, log }); diff --git a/shrinkwrap.yaml b/shrinkwrap.yaml index 229829c..5d831af 100644 --- a/shrinkwrap.yaml +++ b/shrinkwrap.yaml @@ -1,6 +1,8 @@ dependencies: '@codefeathers/iseq': 1.2.1 + '@codefeathers/promise.object': 0.9.5 bluebird: 3.5.1 + chalk: 2.4.1 eslint: 5.2.0 json-stringify-safe: 5.0.1 packages: @@ -8,6 +10,10 @@ packages: dev: false resolution: integrity: sha1-zUHiKGdKZQlWBfKVacbdVtlodsw= + /@codefeathers/promise.object/0.9.5: + dev: false + resolution: + integrity: sha1-YBDLXpC4vhz12WrDHJKr/F8K/zA= /acorn-jsx/4.1.1: dependencies: acorn: 5.7.1 @@ -1043,6 +1049,8 @@ shrinkwrapMinorVersion: 8 shrinkwrapVersion: 3 specifiers: '@codefeathers/iseq': ^1.2.1 + '@codefeathers/promise.object': ^0.9.5 bluebird: ^3.5.1 + chalk: ^2.4.1 eslint: ^5.2.0 json-stringify-safe: ^5.0.1 diff --git a/gunner/index.js b/src/gunner.js similarity index 80% rename from gunner/index.js rename to src/gunner.js index 1e74ab2..c0b8ace 100644 --- a/gunner/index.js +++ b/src/gunner.js @@ -1,21 +1,24 @@ 'use strict'; const { log } = console; +const chalk = require('chalk'); Promise = require('bluebird'); +Promise.object = require('@codefeathers/promise.object'); const _runTests = require('./lib/runTests'); const _expect = require('./lib/expect'); -const { stringify, hasProp } = require('../util/helpers'); +const { stringify, hasProp } = require('./util'); +const symbols = require('./util/symbols'); class Gunner { constructor (options = {}) { this.__hooks__ = { before: { - '@start': [], - '@end': [], + [symbols.Start]: [], + [symbols.Stop]: [], '*': [], }, after: { @@ -37,9 +40,9 @@ class Gunner { this.__tests__.push({ description, - test: () => { + test: (state) => { try { - return test(_expect); + return test(_expect, state); } catch (e) { // If errors are thrown, reject them return Promise.reject(e); @@ -85,7 +88,7 @@ class Gunner { results.passing = success.length; const successPercent = Math.floor(success.length/results.length * 100); log( - `\n${success.length} tests passed of ${results.length}`, + chalk`\n{green ${success.length}} tests passed of ${results.length}`, `[${successPercent}% success]\n` ); results.forEach(r => { @@ -93,7 +96,7 @@ class Gunner { ? `\n Traceback:\n ${stringify(r.error)}` : ''; - log(`${r.result === 'pass' ? '✅' : '❌'} ::`, + log(`${r.result === 'pass' ? chalk`{green ✅}` : chalk`{red ❌}`} ::`, `${r.description}`, `${trace}`); }); @@ -115,3 +118,5 @@ class Gunner { module.exports = Gunner; module.exports.expect = _expect; +module.exports.Start = symbols.Start; +module.exports.End = symbols.End; diff --git a/gunner/lib/assertPromise.js b/src/lib/assertPromise.js similarity index 100% rename from gunner/lib/assertPromise.js rename to src/lib/assertPromise.js diff --git a/gunner/lib/expect.js b/src/lib/expect.js similarity index 95% rename from gunner/lib/expect.js rename to src/lib/expect.js index cec83c6..bc8afae 100644 --- a/gunner/lib/expect.js +++ b/src/lib/expect.js @@ -1,9 +1,8 @@ const isEq = require('@codefeathers/iseq'); -const { liftPromise, stringify, isPromise } = require('../../util/helpers'); +const { liftPromise, stringify, isPromise } = require('../util'); const _assertPromise = require('./assertPromise'); - const expectPromise = (pred, statement, options = {}) => toTest => (...testValues) => diff --git a/src/lib/runTests.js b/src/lib/runTests.js new file mode 100644 index 0000000..1fb331f --- /dev/null +++ b/src/lib/runTests.js @@ -0,0 +1,74 @@ +'use strict'; + +const { isPromise } = require('../util'); +const constants = require('../util/symbols'); + +const runTests = instance => { + + const beforeAll = () => Promise.map( + instance.__hooks__.before[constants.Start] || [], + hook => hook.run(), + ); + + const beforeEvery = state => Promise.mapSeries( + instance.__hooks__.before['*'] || [], + hook => hook.run(state), + ); + + const runner = state => Promise.mapSeries(instance.__tests__, each => { + + const beforeThis = state => Promise.mapSeries( + instance.__hooks__.before[each.description] || [], + hook => hook.run(state), + ); + + const afterThis = state => Promise.mapSeries( + instance.__hooks__.after[each.description] || [], + hook => hook.run(state), + ); + + return beforeEvery(state) + .then(newState => ({ ...state, '@every': newState })) + .then(state => Promise.object({ ...state, '@this': beforeThis() })) + .then(state => { + + const pred = each.test(state); + + /* There are 4 different cases at play: + 1. A plain expect() is returned. + 2. An array of [ expect() ] is returned + 3. A plain expect() is wrapped in a promise + 4. An array of [ expect() ] is wrapped in a promise. + Here we normalise all of them into something we can process */ + + if (!isPromise(pred) && !(pred && isPromise(pred[0]))) + throw new Error(`Malformed test '${each.description}'`); + const toTest = Array.isArray(pred) + ? Promise.all(pred) + : pred.then(x => Array.isArray(x) ? Promise.all(x) : x); + + return [ + state, + toTest + .then(() => ({ description: each.description, result: constants.pass })) + .catch(e => ({ description: each.description, result: constants.fail, error: e })), + ]; + + }) + .spread((state, result) => afterThis(state).then(() => result)); + + }); + + const afterAll = state => Promise.mapSeries( + instance.__hooks__.before[constants.End] || [], + hook => hook.run(state, state['@results']), + ); + + return Promise.object({ '@start': beforeAll() }) + .then(state => Promise.object({ ...state, '@results': runner(state)})) + .then(state => Promise.object({ ...state, '@end': afterAll(state) })) + .then(state => state['@results']); + +}; + +module.exports = runTests; diff --git a/util/helpers.js b/src/util/index.js similarity index 90% rename from util/helpers.js rename to src/util/index.js index fabe93d..9491444 100644 --- a/util/helpers.js +++ b/src/util/index.js @@ -1,9 +1,12 @@ const stringify = require('json-stringify-safe'); +/* Returns true if a promise is passed */ +const isPromise = prom => prom && (typeof prom.then === 'function'); + module.exports = { /* Returns true if a promise is passed */ - isPromise : prom => prom && (typeof prom.then === 'function'), + isPromise, /* Flattens an array of arrays to an array */ flatten : arrData => [].concat.apply([], arrData), @@ -26,7 +29,7 @@ module.exports = { /* Pipe a value or promise through any number of unary functions */ pipe: (...fns) => arg => fns.reduce((acc, fn) => - typeof acc.then === 'function' + isPromise(acc) ? acc.then(fn) : fn(acc), arg), @@ -43,7 +46,7 @@ module.exports = { /* Lift promises into a function */ liftPromise : (fn, thing) => - typeof thing.then === 'function' + isPromise(thing) ? thing.then(fn) : fn(thing), diff --git a/src/util/symbols.js b/src/util/symbols.js new file mode 100644 index 0000000..602537c --- /dev/null +++ b/src/util/symbols.js @@ -0,0 +1,9 @@ +module.exports = { + + Start : Symbol('Start'), + End : Symbol('End'), + + pass: 'pass', + fail: 'fail', + +}; \ No newline at end of file