diff --git a/lib/assert.js b/lib/assert.js index cce42f324..c16e11a1a 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -2,13 +2,11 @@ const coreAssert = require('core-assert'); const deepEqual = require('lodash.isequal'); const observableToPromise = require('observable-to-promise'); -const indentString = require('indent-string'); const isObservable = require('is-observable'); const isPromise = require('is-promise'); -const jestSnapshot = require('jest-snapshot'); +const jestDiff = require('jest-diff'); const enhanceAssert = require('./enhance-assert'); const formatAssertError = require('./format-assert-error'); -const snapshotState = require('./snapshot-state'); class AssertionError extends Error { constructor(opts) { @@ -251,21 +249,21 @@ function wrapAssertions(callbacks) { } }, - snapshot(actual, optionalMessage) { - const result = snapshot(this, actual, optionalMessage); + snapshot(actual, message) { + const state = this._test.getSnapshotState(); + const result = state.match(this.title, actual); if (result.pass) { pass(this); } else { - const diff = formatAssertError.formatDiff(actual, result.expected); - const values = diff ? [diff] : [ - formatAssertError.formatWithLabel('Actual:', actual), - formatAssertError.formatWithLabel('Must be deeply equal to:', result.expected) - ]; - + const diff = jestDiff(result.expected.trim(), result.actual.trim(), {expand: true}) + // Remove annotation + .split('\n') + .slice(3) + .join('\n'); fail(this, new AssertionError({ assertion: 'snapshot', - message: result.message, - values + message: message || 'Did not match snapshot', + values: [{label: 'Difference:', formatted: diff}] })); } } @@ -378,50 +376,3 @@ function wrapAssertions(callbacks) { return Object.assign(assertions, enhancedAssertions); } exports.wrapAssertions = wrapAssertions; - -function snapshot(executionContext, tree, optionalMessage, match, snapshotStateGetter) { - // Set defaults - this allows tests to mock deps easily - const toMatchSnapshot = match || jestSnapshot.toMatchSnapshot; - const getState = snapshotStateGetter || snapshotState.get; - - const state = getState(); - - const context = { - dontThrow() {}, - currentTestName: executionContext.title, - snapshotState: state - }; - - // Symbols can't be serialized and saved in a snapshot, that's why tree - // is saved in the `__ava_react_jsx` prop, so that JSX can be detected later - const serializedTree = tree.$$typeof === Symbol.for('react.test.json') ? {__ava_react_jsx: tree} : tree; // eslint-disable-line camelcase - const result = toMatchSnapshot.call(context, JSON.stringify(serializedTree)); - - let message = 'Please check your code or --update-snapshots'; - - if (optionalMessage) { - message += '\n\n' + indentString(optionalMessage, 2); - } - - state.save(); - - let expected; - - if (result.expected) { - // JSON in a snapshot is surrounded with `"`, because jest-snapshot - // serializes snapshot values too, so it ends up double JSON encoded - expected = JSON.parse(result.expected.slice(1).slice(0, -1)); - // Define a `$$typeof` symbol, so that pretty-format detects it as React tree - if (expected.__ava_react_jsx) { // eslint-disable-line camelcase - expected = expected.__ava_react_jsx; // eslint-disable-line camelcase - Object.defineProperty(expected, '$$typeof', {value: Symbol.for('react.test.json')}); - } - } - - return { - pass: result.pass, - expected, - message - }; -} -exports.snapshot = snapshot; diff --git a/lib/main.js b/lib/main.js index 607233f20..52618e8b7 100644 --- a/lib/main.js +++ b/lib/main.js @@ -7,10 +7,12 @@ const Runner = require('./runner'); const opts = globals.options; const runner = new Runner({ + bail: opts.failFast, failWithoutAssertions: opts.failWithoutAssertions, + file: opts.file, + match: opts.match, serial: opts.serial, - bail: opts.failFast, - match: opts.match + updateSnapshots: opts.updateSnapshots }); worker.setRunner(runner); @@ -51,7 +53,7 @@ function exit() { // Reference the IPC channel now that tests have finished running. adapter.ipcChannel.ref(); - const stats = runner._buildStats(); + const stats = runner.buildStats(); adapter.send('results', {stats}); } @@ -78,7 +80,11 @@ globals.setImmediate(() => { adapter.ipcChannel.unref(); runner.run(options) - .then(exit) + .then(() => { + runner.saveSnapshotState(); + + return exit(); + }) .catch(err => { process.emit('uncaughtException', err); }); @@ -89,9 +95,9 @@ globals.setImmediate(() => { }); }); -module.exports = runner.test; +module.exports = runner.chain; // TypeScript imports the `default` property for // an ES2015 default import (`import test from 'ava'`) // See: https://github.com/Microsoft/TypeScript/issues/2242#issuecomment-83694181 -module.exports.default = runner.test; +module.exports.default = runner.chain; diff --git a/lib/runner.js b/lib/runner.js index b090db491..5f0edacb2 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -1,6 +1,8 @@ 'use strict'; const EventEmitter = require('events'); -const Promise = require('bluebird'); +const path = require('path'); +const Bluebird = require('bluebird'); +const jestSnapshot = require('jest-snapshot'); const optionChain = require('option-chain'); const matcher = require('matcher'); const TestCollection = require('./test-collection'); @@ -45,18 +47,61 @@ class Runner extends EventEmitter { options = options || {}; + this.file = options.file; + this.match = options.match || []; + this.serial = options.serial; + this.updateSnapshots = options.updateSnapshots; + + this.hasStarted = false; this.results = []; + this.snapshotState = null; this.tests = new TestCollection({ bail: options.bail, - failWithoutAssertions: options.failWithoutAssertions + failWithoutAssertions: options.failWithoutAssertions, + getSnapshotState: () => this.getSnapshotState() + }); + + this.chain = optionChain(chainableMethods, (opts, args) => { + let title; + let fn; + let macroArgIndex; + + if (this.hasStarted) { + throw new Error('All tests and hooks must be declared synchronously in your ' + + 'test file, and cannot be nested within other tests or hooks.'); + } + + if (typeof args[0] === 'string') { + title = args[0]; + fn = args[1]; + macroArgIndex = 2; + } else { + fn = args[0]; + title = null; + macroArgIndex = 1; + } + + if (this.serial) { + opts.serial = true; + } + + if (args.length > macroArgIndex) { + args = args.slice(macroArgIndex); + } else { + args = null; + } + + if (Array.isArray(fn)) { + fn.forEach(fn => { + this.addTest(title, opts, fn, args); + }); + } else { + this.addTest(title, opts, fn, args); + } }); - this.hasStarted = false; - this._serial = options.serial; - this._match = options.match || []; - this._addTestResult = this._addTestResult.bind(this); - this._buildStats = this._buildStats.bind(this); } - _addTest(title, opts, fn, args) { + + addTest(title, metadata, fn, args) { if (args) { if (fn.title) { title = fn.title.apply(fn, [title || ''].concat(args)); @@ -65,22 +110,23 @@ class Runner extends EventEmitter { fn = wrapFunction(fn, args); } - if (opts.type === 'test' && this._match.length > 0) { - opts.exclusive = title !== null && matcher([title], this._match).length === 1; + if (metadata.type === 'test' && this.match.length > 0) { + metadata.exclusive = title !== null && matcher([title], this.match).length === 1; } - const validationError = validateTest(title, fn, opts); + const validationError = validateTest(title, fn, metadata); if (validationError !== null) { throw new TypeError(validationError); } this.tests.add({ - metadata: opts, + metadata, fn, title }); } - _addTestResult(result) { + + addTestResult(result) { const test = result.result; const props = { duration: test.duration, @@ -95,103 +141,81 @@ class Runner extends EventEmitter { this.results.push(result); this.emit('test', props); } - _buildStats() { + + buildStats() { const stats = { - testCount: 0, + failCount: 0, + knownFailureCount: 0, + passCount: 0, skipCount: 0, + testCount: 0, todoCount: 0 }; - this.results - .map(result => { - return result.result; - }) - .filter(test => { - return test.metadata.type === 'test'; - }) - .forEach(test => { + for (const result of this.results) { + if (!result.passed) { + // Includes hooks + stats.failCount++; + } + + const metadata = result.result.metadata; + if (metadata.type === 'test') { stats.testCount++; - if (test.metadata.skipped) { + if (metadata.skipped) { stats.skipCount++; - } - - if (test.metadata.todo) { + } else if (metadata.todo) { stats.todoCount++; + } else if (result.passed) { + if (metadata.failing) { + stats.knownFailureCount++; + } else { + stats.passCount++; + } } - }); + } + } - stats.failCount = this.results - .filter(result => { - return result.passed === false; - }) - .length; + return stats; + } - stats.knownFailureCount = this.results - .filter(result => { - return result.passed === true && result.result.metadata.failing; - }) - .length; + getSnapshotState() { + if (this.snapshotState) { + return this.snapshotState; + } - stats.passCount = stats.testCount - stats.failCount - stats.skipCount - stats.todoCount; + const name = path.basename(this.file) + '.snap'; + const dir = path.dirname(this.file); - return stats; + const snapshotPath = path.join(dir, '__snapshots__', name); + const testPath = this.file; + const update = this.updateSnapshots; + + const state = jestSnapshot.initializeSnapshotState(testPath, update, snapshotPath); + this.snapshotState = state; + return state; } + + saveSnapshotState() { + if (this.snapshotState) { + this.snapshotState.save(this.updateSnapshots); + } + } + run(options) { if (options.runOnlyExclusive && !this.tests.hasExclusive) { return Promise.resolve(null); } - this.tests.on('test', this._addTestResult); - this.hasStarted = true; - - return Promise.resolve(this.tests.build().run()).then(this._buildStats); + this.tests.on('test', result => { + this.addTestResult(result); + }); + return Bluebird.try(() => this.tests.build().run()); } attributeLeakedError(err) { return this.tests.attributeLeakedError(err); } } -optionChain(chainableMethods, function (opts, args) { - let title; - let fn; - let macroArgIndex; - - if (this.hasStarted) { - throw new Error('All tests and hooks must be declared synchronously in your ' + - 'test file, and cannot be nested within other tests or hooks.'); - } - - if (typeof args[0] === 'string') { - title = args[0]; - fn = args[1]; - macroArgIndex = 2; - } else { - fn = args[0]; - title = null; - macroArgIndex = 1; - } - - if (this._serial) { - opts.serial = true; - } - - if (args.length > macroArgIndex) { - args = args.slice(macroArgIndex); - } else { - args = null; - } - - if (Array.isArray(fn)) { - fn.forEach(function (fn) { - this._addTest(title, opts, fn, args); - }, this); - } else { - this._addTest(title, opts, fn, args); - } -}, Runner.prototype); - -Runner._chainableMethods = chainableMethods.chainableMethods; - module.exports = Runner; diff --git a/lib/snapshot-state.js b/lib/snapshot-state.js deleted file mode 100644 index c7b942d07..000000000 --- a/lib/snapshot-state.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; -const path = require('path'); -const jestSnapshot = require('jest-snapshot'); -const globals = require('./globals'); - -const x = module.exports; - -x.get = (initializeState, globalsOptions) => { - if (!x.state) { - // Set defaults - this allows tests to mock deps easily - const options = globalsOptions || globals.options; - const initializeSnapshotState = initializeState || jestSnapshot.initializeSnapshotState; - - const filename = options.file; - const dirname = path.dirname(filename); - const snapshotFileName = path.basename(filename) + '.snap'; - const snapshotsFolder = path.join(dirname, '__snapshots__', snapshotFileName); - - x.state = initializeSnapshotState( - filename, - options.updateSnapshots, - snapshotsFolder, - true - ); - } - - return x.state; -}; - -x.state = null; diff --git a/lib/test-collection.js b/lib/test-collection.js index fedf9f93e..5404cb119 100644 --- a/lib/test-collection.js +++ b/lib/test-collection.js @@ -11,6 +11,7 @@ class TestCollection extends EventEmitter { this.bail = options.bail; this.failWithoutAssertions = options.failWithoutAssertions; + this.getSnapshotState = options.getSnapshotState; this.hasExclusive = false; this.testCount = 0; @@ -132,6 +133,7 @@ class TestCollection extends EventEmitter { contextRef, failWithoutAssertions: false, fn: hook.fn, + getSnapshotState: this.getSnapshotState, metadata: hook.metadata, onResult: this._emitTestResult, title @@ -148,6 +150,7 @@ class TestCollection extends EventEmitter { contextRef, failWithoutAssertions: this.failWithoutAssertions, fn: test.fn, + getSnapshotState: this.getSnapshotState, metadata: test.metadata, onResult: this._emitTestResult, title: test.title diff --git a/lib/test.js b/lib/test.js index 026285cb0..9d7f0623a 100644 --- a/lib/test.js +++ b/lib/test.js @@ -98,6 +98,7 @@ class Test { this.contextRef = options.contextRef; this.failWithoutAssertions = options.failWithoutAssertions; this.fn = isGeneratorFn(options.fn) ? co.wrap(options.fn) : options.fn; + this.getSnapshotState = options.getSnapshotState; this.metadata = options.metadata; this.onResult = options.onResult; this.title = options.title; diff --git a/package.json b/package.json index ee945df7f..e21d83e65 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,8 @@ "is-obj": "^1.0.0", "is-observable": "^0.2.0", "is-promise": "^2.1.0", - "jest-snapshot": "^18.1.0", + "jest-diff": "19.0.0", + "jest-snapshot": "19.0.2", "js-yaml": "^3.8.2", "last-line-stream": "^1.0.0", "lodash.debounce": "^4.0.3", diff --git a/test/assert.js b/test/assert.js index b8b832aeb..95d53aeff 100644 --- a/test/assert.js +++ b/test/assert.js @@ -1,6 +1,7 @@ 'use strict'; +const path = require('path'); +const jestSnapshot = require('jest-snapshot'); const test = require('tap').test; -const sinon = require('sinon'); const assert = require('../lib/assert'); const formatValue = require('../lib/format-assert-error').formatValue; @@ -573,74 +574,50 @@ test('.ifError()', t => { t.end(); }); -test('snapshot makes a snapshot using a library and global options', t => { - const saveSpy = sinon.spy(); - const state = {save: saveSpy}; - const stateGetter = sinon.stub().returns(state); - const matchStub = sinon.stub().returns({pass: true}); - - const test = { - title: 'Test name' +test('.snapshot()', t => { + // Set to `true` to update the snapshot, then run: + // "$(npm bin)"/tap --no-cov -R spec test/assert.js + // + // Ignore errors and make sure not to run tests with the `-b` (bail) option. + const update = false; + + const state = jestSnapshot.initializeSnapshotState(__filename, update, path.join(__dirname, 'fixture', 'assert.snap')); + const executionContext = { + _test: { + getSnapshotState() { + return state; + } + }, + title: '' }; - t.plan(4); - - t.doesNotThrow(() => { - assert.snapshot(test, 'tree', undefined, matchStub, stateGetter); - }); - - t.ok(stateGetter.called); - - t.match(matchStub.firstCall.thisValue, { - currentTestName: 'Test name', - snapshotState: state + passes(t, () => { + executionContext.title = 'passes'; + assertions.snapshot.call(executionContext, {foo: 'bar'}); }); - t.ok(saveSpy.calledOnce); - t.end(); -}); - -test('snapshot handles jsx tree', t => { - const saveSpy = sinon.spy(); - const state = {save: saveSpy}; - const stateGetter = sinon.stub().returns(state); - const matchStub = sinon.stub().returns({pass: true}); - - const test = { - title: 'Test name' - }; - - t.plan(5); - - t.doesNotThrow(() => { - const tree = { - type: 'h1', - children: ['Hello'], - props: {} - }; - - Object.defineProperty(tree, '$$typeof', {value: Symbol.for('react.test.json')}); - - assert.snapshot(test, tree, undefined, matchStub, stateGetter); + failsWith(t, () => { + executionContext.title = 'fails'; + assertions.snapshot.call(executionContext, {foo: update ? 'bar' : 'not bar'}); + }, { + assertion: 'snapshot', + message: 'Did not match snapshot', + values: [{label: 'Difference:', formatted: 'Object {\n- "foo": "bar",\n+ "foo": "not bar",\n }'}] }); - t.ok(stateGetter.called); - - const savedTree = JSON.parse(matchStub.firstCall.args[0]); - t.deepEqual(savedTree, { - __ava_react_jsx: { // eslint-disable-line camelcase - type: 'h1', - children: ['Hello'], - props: {} - } + failsWith(t, () => { + executionContext.title = 'fails'; + assertions.snapshot.call(executionContext, {foo: update ? 'bar' : 'not bar'}, 'my message'); + }, { + assertion: 'snapshot', + message: 'my message', + values: [{label: 'Difference:', formatted: 'Object {\n- "foo": "bar",\n+ "foo": "not bar",\n }'}] }); - t.match(matchStub.firstCall.thisValue, { - currentTestName: 'Test name', - snapshotState: state - }); + if (update) { + state.save(true); + } - t.ok(saveSpy.calledOnce); t.end(); }); diff --git a/test/cli.js b/test/cli.js index 7073ecdf6..6aedf396f 100644 --- a/test/cli.js +++ b/test/cli.js @@ -504,3 +504,24 @@ test('promise tests fail if event loop empties before they\'re resolved', t => { t.end(); }); }); + +test('snapshots work', t => { + try { + fs.unlinkSync(path.join(__dirname, 'fixture', 'snapshots', '__snapshots__', 'test.snap')); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } + + // Test should pass, and a snapshot gets written + execCli(['--update-snapshots', 'test.js'], {dirname: 'fixture/snapshots'}, err => { + t.ifError(err); + + // Test should pass, and the snapshot gets used + execCli(['test.js'], {dirname: 'fixture/snapshots'}, err => { + t.ifError(err); + t.end(); + }); + }); +}); diff --git a/test/fixture/assert.snap b/test/fixture/assert.snap new file mode 100644 index 000000000..5420a5126 --- /dev/null +++ b/test/fixture/assert.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`fails 1`] = ` +Object { + "foo": "bar", +} +`; + +exports[`fails 2`] = ` +Object { + "foo": "bar", +} +`; + +exports[`passes 1`] = ` +Object { + "foo": "bar", +} +`; diff --git a/test/fixture/snapshots/.gitignore b/test/fixture/snapshots/.gitignore new file mode 100644 index 000000000..b05c2dfa7 --- /dev/null +++ b/test/fixture/snapshots/.gitignore @@ -0,0 +1 @@ +__snapshots__ diff --git a/test/fixture/snapshots/test.js b/test/fixture/snapshots/test.js new file mode 100644 index 000000000..70e1b67db --- /dev/null +++ b/test/fixture/snapshots/test.js @@ -0,0 +1,5 @@ +import test from '../../..'; + +test('snapshot', t => { + t.snapshot({foo: 'bar'}); +}); diff --git a/test/hooks.js b/test/hooks.js index e45c9e3bd..c0a2f3931 100644 --- a/test/hooks.js +++ b/test/hooks.js @@ -34,11 +34,11 @@ test('before', t => { const runner = new Runner(); const arr = []; - runner.before(() => { + runner.chain.before(() => { arr.push('a'); }); - runner.test(a => { + runner.chain.test(a => { a.pass(); arr.push('b'); }); @@ -54,16 +54,17 @@ test('after', t => { const runner = new Runner(); const arr = []; - runner.after(() => { + runner.chain.after(() => { arr.push('b'); }); - runner.test(a => { + runner.chain.test(a => { a.pass(); arr.push('a'); }); - return runner.run({}).then(stats => { + return runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.passCount, 1); t.is(stats.failCount, 0); t.strictDeepEqual(arr, ['a', 'b']); @@ -77,14 +78,15 @@ test('after not run if test failed', t => { const runner = new Runner(); const arr = []; - runner.after(() => { + runner.chain.after(() => { arr.push('a'); }); - runner.test(() => { + runner.chain.test(() => { throw new Error('something went wrong'); }); - return runner.run({}).then(stats => { + return runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.passCount, 0); t.is(stats.failCount, 1); t.strictDeepEqual(arr, []); @@ -98,14 +100,15 @@ test('after.always run even if test failed', t => { const runner = new Runner(); const arr = []; - runner.after.always(() => { + runner.chain.after.always(() => { arr.push('a'); }); - runner.test(() => { + runner.chain.test(() => { throw new Error('something went wrong'); }); - return runner.run({}).then(stats => { + return runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.passCount, 0); t.is(stats.failCount, 1); t.strictDeepEqual(arr, ['a']); @@ -119,11 +122,11 @@ test('after.always run even if before failed', t => { const runner = new Runner(); const arr = []; - runner.before(() => { + runner.chain.before(() => { throw new Error('something went wrong'); }); - runner.after.always(() => { + runner.chain.after.always(() => { arr.push('a'); }); @@ -139,15 +142,15 @@ test('stop if before hooks failed', t => { const runner = new Runner(); const arr = []; - runner.before(() => { + runner.chain.before(() => { arr.push('a'); }); - runner.before(() => { + runner.chain.before(() => { throw new Error('something went wrong'); }); - runner.test(a => { + runner.chain.test(a => { a.pass(); arr.push('b'); a.end(); @@ -167,20 +170,20 @@ test('before each with concurrent tests', t => { let i = 0; let k = 0; - runner.beforeEach(() => { + runner.chain.beforeEach(() => { arr[i++].push('a'); }); - runner.beforeEach(() => { + runner.chain.beforeEach(() => { arr[k++].push('b'); }); - runner.test(a => { + runner.chain.test(a => { a.pass(); arr[0].push('c'); }); - runner.test(a => { + runner.chain.test(a => { a.pass(); arr[1].push('d'); }); @@ -197,20 +200,20 @@ test('before each with serial tests', t => { const runner = new Runner(); const arr = []; - runner.beforeEach(() => { + runner.chain.beforeEach(() => { arr.push('a'); }); - runner.beforeEach(() => { + runner.chain.beforeEach(() => { arr.push('b'); }); - runner.serial(a => { + runner.chain.serial(a => { a.pass(); arr.push('c'); }); - runner.serial(a => { + runner.chain.serial(a => { a.pass(); arr.push('d'); }); @@ -227,17 +230,18 @@ test('fail if beforeEach hook fails', t => { const runner = new Runner(); const arr = []; - runner.beforeEach(a => { + runner.chain.beforeEach(a => { arr.push('a'); a.fail(); }); - runner.test(a => { + runner.chain.test(a => { arr.push('b'); a.pass(); }); - return runner.run({}).then(stats => { + return runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.failCount, 1); t.strictDeepEqual(arr, ['a']); t.end(); @@ -252,20 +256,20 @@ test('after each with concurrent tests', t => { let i = 0; let k = 0; - runner.afterEach(() => { + runner.chain.afterEach(() => { arr[i++].push('a'); }); - runner.afterEach(() => { + runner.chain.afterEach(() => { arr[k++].push('b'); }); - runner.test(a => { + runner.chain.test(a => { a.pass(); arr[0].push('c'); }); - runner.test(a => { + runner.chain.test(a => { a.pass(); arr[1].push('d'); }); @@ -282,20 +286,20 @@ test('after each with serial tests', t => { const runner = new Runner(); const arr = []; - runner.afterEach(() => { + runner.chain.afterEach(() => { arr.push('a'); }); - runner.afterEach(() => { + runner.chain.afterEach(() => { arr.push('b'); }); - runner.serial(a => { + runner.chain.serial(a => { a.pass(); arr.push('c'); }); - runner.serial(a => { + runner.chain.serial(a => { a.pass(); arr.push('d'); }); @@ -312,11 +316,11 @@ test('afterEach not run if concurrent tests failed', t => { const runner = new Runner(); const arr = []; - runner.afterEach(() => { + runner.chain.afterEach(() => { arr.push('a'); }); - runner.test(() => { + runner.chain.test(() => { throw new Error('something went wrong'); }); @@ -332,11 +336,11 @@ test('afterEach not run if serial tests failed', t => { const runner = new Runner(); const arr = []; - runner.afterEach(() => { + runner.chain.afterEach(() => { arr.push('a'); }); - runner.serial(() => { + runner.chain.serial(() => { throw new Error('something went wrong'); }); @@ -352,11 +356,11 @@ test('afterEach.always run even if concurrent tests failed', t => { const runner = new Runner(); const arr = []; - runner.afterEach.always(() => { + runner.chain.afterEach.always(() => { arr.push('a'); }); - runner.test(() => { + runner.chain.test(() => { throw new Error('something went wrong'); }); @@ -372,11 +376,11 @@ test('afterEach.always run even if serial tests failed', t => { const runner = new Runner(); const arr = []; - runner.afterEach.always(() => { + runner.chain.afterEach.always(() => { arr.push('a'); }); - runner.serial(() => { + runner.chain.serial(() => { throw new Error('something went wrong'); }); @@ -392,16 +396,16 @@ test('afterEach.always run even if beforeEach failed', t => { const runner = new Runner(); const arr = []; - runner.beforeEach(() => { + runner.chain.beforeEach(() => { throw new Error('something went wrong'); }); - runner.test(a => { + runner.chain.test(a => { a.pass(); arr.push('a'); }); - runner.afterEach.always(() => { + runner.chain.afterEach.always(() => { arr.push('b'); }); @@ -417,23 +421,23 @@ test('ensure hooks run only around tests', t => { const runner = new Runner(); const arr = []; - runner.beforeEach(() => { + runner.chain.beforeEach(() => { arr.push('beforeEach'); }); - runner.before(() => { + runner.chain.before(() => { arr.push('before'); }); - runner.afterEach(() => { + runner.chain.afterEach(() => { arr.push('afterEach'); }); - runner.after(() => { + runner.chain.after(() => { arr.push('after'); }); - runner.test(a => { + runner.chain.test(a => { a.pass(); arr.push('test'); }); @@ -449,30 +453,31 @@ test('shared context', t => { const runner = new Runner(); - runner.before(a => { + runner.chain.before(a => { a.is(a.context, null); }); - runner.after(a => { + runner.chain.after(a => { a.is(a.context, null); }); - runner.beforeEach(a => { + runner.chain.beforeEach(a => { a.context.arr = ['a']; }); - runner.test(a => { + runner.chain.test(a => { a.pass(); a.context.arr.push('b'); a.deepEqual(a.context.arr, ['a', 'b']); }); - runner.afterEach(a => { + runner.chain.afterEach(a => { a.context.arr.push('c'); a.deepEqual(a.context.arr, ['a', 'b', 'c']); }); - return runner.run({}).then(stats => { + return runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.failCount, 0); t.end(); }); @@ -483,16 +488,17 @@ test('shared context of any type', t => { const runner = new Runner(); - runner.beforeEach(a => { + runner.chain.beforeEach(a => { a.context = 'foo'; }); - runner.test(a => { + runner.chain.test(a => { a.pass(); a.is(a.context, 'foo'); }); - return runner.run({}).then(stats => { + return runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.failCount, 0); t.end(); }); diff --git a/test/runner.js b/test/runner.js index 9742d3df1..50ce2e7a3 100644 --- a/test/runner.js +++ b/test/runner.js @@ -10,9 +10,9 @@ test('nested tests and hooks aren\'t allowed', t => { const runner = new Runner(); - runner.test(a => { + runner.chain.test(a => { t.throws(() => { - runner.test(noop); + runner.chain.test(noop); }, {message: 'All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.'}); a.pass(); }); @@ -27,7 +27,7 @@ test('tests must be declared synchronously', t => { const runner = new Runner(); - runner.test(a => { + runner.chain.test(a => { a.pass(); return Promise.resolve(); }); @@ -35,7 +35,7 @@ test('tests must be declared synchronously', t => { runner.run({}); t.throws(() => { - runner.test(noop); + runner.chain.test(noop); }, {message: 'All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.'}); t.end(); @@ -44,7 +44,7 @@ test('tests must be declared synchronously', t => { test('runner emits a "test" event', t => { const runner = new Runner(); - runner.test('foo', a => { + runner.chain.test('foo', a => { a.pass(); }); @@ -62,17 +62,17 @@ test('run serial tests before concurrent ones', t => { const runner = new Runner(); const arr = []; - runner.test(a => { + runner.chain.test(a => { arr.push('c'); a.end(); }); - runner.serial(a => { + runner.chain.serial(a => { arr.push('a'); a.end(); }); - runner.serial(a => { + runner.chain.serial(a => { arr.push('b'); a.end(); }); @@ -94,23 +94,23 @@ test('anything can be skipped', t => { }; } - runner.after(pusher('after')); - runner.after.skip(pusher('after.skip')); + runner.chain.after(pusher('after')); + runner.chain.after.skip(pusher('after.skip')); - runner.afterEach(pusher('afterEach')); - runner.afterEach.skip(pusher('afterEach.skip')); + runner.chain.afterEach(pusher('afterEach')); + runner.chain.afterEach.skip(pusher('afterEach.skip')); - runner.before(pusher('before')); - runner.before.skip(pusher('before.skip')); + runner.chain.before(pusher('before')); + runner.chain.before.skip(pusher('before.skip')); - runner.beforeEach(pusher('beforeEach')); - runner.beforeEach.skip(pusher('beforeEach.skip')); + runner.chain.beforeEach(pusher('beforeEach')); + runner.chain.beforeEach.skip(pusher('beforeEach.skip')); - runner.test(pusher('concurrent')); - runner.test.skip(pusher('concurrent.skip')); + runner.chain.test(pusher('concurrent')); + runner.chain.test.skip(pusher('concurrent.skip')); - runner.serial(pusher('serial')); - runner.serial.skip(pusher('serial.skip')); + runner.chain.serial(pusher('serial')); + runner.chain.serial.skip(pusher('serial.skip')); runner.run({}).then(() => { // Note that afterEach and beforeEach run twice because there are two actual tests - "serial" and "concurrent" @@ -131,20 +131,20 @@ test('anything can be skipped', t => { test('include skipped tests in results', t => { const runner = new Runner(); - runner.before('before', noop); - runner.before.skip('before.skip', noop); + runner.chain.before('before', noop); + runner.chain.before.skip('before.skip', noop); - runner.beforeEach('beforeEach', noop); - runner.beforeEach.skip('beforeEach.skip', noop); + runner.chain.beforeEach('beforeEach', noop); + runner.chain.beforeEach.skip('beforeEach.skip', noop); - runner.test.serial('test', a => a.pass()); - runner.test.serial.skip('test.skip', noop); + runner.chain.test.serial('test', a => a.pass()); + runner.chain.test.serial.skip('test.skip', noop); - runner.after('after', noop); - runner.after.skip('after.skip', noop); + runner.chain.after('after', noop); + runner.chain.after.skip('after.skip', noop); - runner.afterEach('afterEach', noop); - runner.afterEach.skip('afterEach.skip', noop); + runner.chain.afterEach('afterEach', noop); + runner.chain.afterEach.skip('afterEach.skip', noop); const titles = []; @@ -182,11 +182,11 @@ test('test types and titles', t => { } const runner = new Runner(); - runner.before(named); - runner.beforeEach(fn); - runner.after(fn); - runner.afterEach(named); - runner.test('test', fn); + runner.chain.before(named); + runner.chain.beforeEach(fn); + runner.chain.after(fn); + runner.chain.afterEach(named); + runner.chain.test('test', fn); // See https://github.com/avajs/ava/issues/1027 const supportsFunctionNames = noop.name === 'noop'; @@ -229,20 +229,21 @@ test('skip test', t => { const runner = new Runner(); const arr = []; - runner.test(a => { + runner.chain.test(a => { arr.push('a'); a.pass(); }); - runner.skip(() => { + runner.chain.skip(() => { arr.push('b'); }); t.throws(() => { - runner.skip('should be a todo'); + runner.chain.skip('should be a todo'); }, new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.')); - runner.run({}).then(stats => { + runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.testCount, 2); t.is(stats.passCount, 1); t.is(stats.skipCount, 1); @@ -257,7 +258,7 @@ test('test throws when given no function', t => { const runner = new Runner(); t.throws(() => { - runner.test(); + runner.chain.test(); }, new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.')); }); @@ -267,22 +268,23 @@ test('todo test', t => { const runner = new Runner(); const arr = []; - runner.test(a => { + runner.chain.test(a => { arr.push('a'); a.pass(); }); - runner.todo('todo'); + runner.chain.todo('todo'); t.throws(() => { - runner.todo('todo', () => {}); + runner.chain.todo('todo', () => {}); }, new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.')); t.throws(() => { - runner.todo(); + runner.chain.todo(); }, new TypeError('`todo` tests require a title')); - runner.run({}).then(stats => { + runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.testCount, 2); t.is(stats.passCount, 1); t.is(stats.todoCount, 1); @@ -297,17 +299,18 @@ test('only test', t => { const runner = new Runner(); const arr = []; - runner.test(a => { + runner.chain.test(a => { arr.push('a'); a.pass(); }); - runner.only(a => { + runner.chain.only(a => { arr.push('b'); a.pass(); }); - runner.run({}).then(stats => { + runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.testCount, 1); t.is(stats.passCount, 1); t.strictDeepEqual(arr, ['b']); @@ -319,7 +322,7 @@ test('throws if you try to set a hook as exclusive', t => { const runner = new Runner(); t.throws(() => { - runner.beforeEach.only('', noop); + runner.chain.beforeEach.only('', noop); }, new TypeError('`only` is only for tests and cannot be used with hooks')); t.end(); @@ -329,7 +332,7 @@ test('throws if you try to set a before hook as always', t => { const runner = new Runner(); t.throws(() => { - runner.before.always('', noop); + runner.chain.before.always('', noop); }, new TypeError('`always` can only be used with `after` and `afterEach`')); t.end(); @@ -339,7 +342,7 @@ test('throws if you try to set a test as always', t => { const runner = new Runner(); t.throws(() => { - runner.test.always('', noop); + runner.chain.test.always('', noop); }, new TypeError('`always` can only be used with `after` and `afterEach`')); t.end(); @@ -349,7 +352,7 @@ test('throws if you give a function to todo', t => { const runner = new Runner(); t.throws(() => { - runner.test.todo('todo with function', noop); + runner.chain.test.todo('todo with function', noop); }, new TypeError('`todo` tests are not allowed to have an implementation. Use ' + '`test.skip()` for tests with an implementation.')); @@ -360,7 +363,7 @@ test('throws if todo has no title', t => { const runner = new Runner(); t.throws(() => { - runner.test.todo(); + runner.chain.test.todo(); }, new TypeError('`todo` tests require a title')); t.end(); @@ -373,15 +376,15 @@ test('throws if todo has failing, skip, or only', t => { ' used with `skip`, `only`, or `failing`'; t.throws(() => { - runner.test.failing.todo('test'); + runner.chain.test.failing.todo('test'); }, new TypeError(errorMessage)); t.throws(() => { - runner.test.skip.todo('test'); + runner.chain.test.skip.todo('test'); }, new TypeError(errorMessage)); t.throws(() => { - runner.test.only.todo('test'); + runner.chain.test.only.todo('test'); }, new TypeError(errorMessage)); t.end(); @@ -394,19 +397,19 @@ test('throws if todo isn\'t a test', t => { ' cannot be used with hooks'; t.throws(() => { - runner.before.todo('test'); + runner.chain.before.todo('test'); }, new TypeError(errorMessage)); t.throws(() => { - runner.beforeEach.todo('test'); + runner.chain.beforeEach.todo('test'); }, new TypeError(errorMessage)); t.throws(() => { - runner.after.todo('test'); + runner.chain.after.todo('test'); }, new TypeError(errorMessage)); t.throws(() => { - runner.afterEach.todo('test'); + runner.chain.afterEach.todo('test'); }, new TypeError(errorMessage)); t.end(); @@ -416,7 +419,7 @@ test('throws if test has skip and only', t => { const runner = new Runner(); t.throws(() => { - runner.test.only.skip('test', noop); + runner.chain.test.only.skip('test', noop); }, new TypeError('`only` tests cannot be skipped')); t.end(); @@ -428,19 +431,19 @@ test('throws if failing is used on non-tests', t => { const errorMessage = '`failing` is only for tests and cannot be used with hooks'; t.throws(() => { - runner.beforeEach.failing('', noop); + runner.chain.beforeEach.failing('', noop); }, new TypeError(errorMessage)); t.throws(() => { - runner.before.failing('', noop); + runner.chain.before.failing('', noop); }, new TypeError(errorMessage)); t.throws(() => { - runner.afterEach.failing('', noop); + runner.chain.afterEach.failing('', noop); }, new TypeError(errorMessage)); t.throws(() => { - runner.after.failing('', noop); + runner.chain.after.failing('', noop); }, new TypeError(errorMessage)); t.end(); @@ -452,19 +455,19 @@ test('throws if only is used on non-tests', t => { const errorMessage = '`only` is only for tests and cannot be used with hooks'; t.throws(() => { - runner.beforeEach.only(noop); + runner.chain.beforeEach.only(noop); }, new TypeError(errorMessage)); t.throws(() => { - runner.before.only(noop); + runner.chain.before.only(noop); }, new TypeError(errorMessage)); t.throws(() => { - runner.afterEach.only(noop); + runner.chain.afterEach.only(noop); }, new TypeError(errorMessage)); t.throws(() => { - runner.after.only(noop); + runner.chain.after.only(noop); }, new TypeError(errorMessage)); t.end(); @@ -475,9 +478,10 @@ test('validate accepts skipping failing tests', t => { const runner = new Runner(); - runner.test.skip.failing('skip failing', noop); + runner.chain.test.skip.failing('skip failing', noop); - runner.run({}).then(stats => { + runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.testCount, 1); t.is(stats.skipCount, 1); t.end(); @@ -491,7 +495,7 @@ test('runOnlyExclusive option test', t => { const options = {runOnlyExclusive: true}; const arr = []; - runner.test(() => { + runner.chain.test(() => { arr.push('a'); }); @@ -507,7 +511,7 @@ test('options.serial forces all tests to be serial', t => { const runner = new Runner({serial: true}); const arr = []; - runner.cb(a => { + runner.chain.cb(a => { setTimeout(() => { arr.push(1); a.end(); @@ -515,7 +519,7 @@ test('options.serial forces all tests to be serial', t => { a.pass(); }); - runner.cb(a => { + runner.chain.cb(a => { setTimeout(() => { arr.push(2); a.end(); @@ -523,7 +527,7 @@ test('options.serial forces all tests to be serial', t => { a.pass(); }); - runner.test(a => { + runner.chain.test(a => { a.pass(); t.strictDeepEqual(arr, [1, 2]); t.end(); @@ -537,12 +541,12 @@ test('options.bail will bail out', t => { const runner = new Runner({bail: true}); - runner.test(a => { + runner.chain.test(a => { t.pass(); a.fail(); }); - runner.test(() => { + runner.chain.test(() => { t.fail(); }); @@ -557,7 +561,7 @@ test('options.bail will bail out (async)', t => { const runner = new Runner({bail: true}); const tests = []; - runner.cb(a => { + runner.chain.cb(a => { setTimeout(() => { tests.push(1); a.fail(); @@ -566,7 +570,7 @@ test('options.bail will bail out (async)', t => { a.pass(); }); - runner.cb(a => { + runner.chain.cb(a => { setTimeout(() => { tests.push(2); a.end(); @@ -594,7 +598,7 @@ test('options.bail + serial - tests will never happen (async)', t => { }); const tests = []; - runner.cb(a => { + runner.chain.cb(a => { setTimeout(() => { tests.push(1); a.fail(); @@ -602,7 +606,7 @@ test('options.bail + serial - tests will never happen (async)', t => { }, 100); }); - runner.cb(a => { + runner.chain.cb(a => { setTimeout(() => { tests.push(2); a.end(); @@ -625,27 +629,28 @@ test('options.match will not run tests with non-matching titles', t => { match: ['*oo', '!foo'] }); - runner.test('mhm. grass tasty. moo', a => { + runner.chain.test('mhm. grass tasty. moo', a => { t.pass(); a.pass(); }); - runner.test('juggaloo', a => { + runner.chain.test('juggaloo', a => { t.pass(); a.pass(); }); - runner.test('foo', a => { + runner.chain.test('foo', a => { t.fail(); a.pass(); }); - runner.test(a => { + runner.chain.test(a => { t.fail(); a.pass(); }); - runner.run({}).then(stats => { + runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.skipCount, 0); t.is(stats.passCount, 2); t.is(stats.testCount, 2); @@ -662,16 +667,17 @@ test('options.match hold no effect on hooks with titles', t => { let actual; - runner.before('before hook with title', () => { + runner.chain.before('before hook with title', () => { actual = 'foo'; }); - runner.test('after', a => { + runner.chain.test('after', a => { t.is(actual, 'foo'); a.pass(); }); - runner.run({}).then(stats => { + runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.skipCount, 0); t.is(stats.passCount, 1); t.is(stats.testCount, 1); @@ -686,17 +692,18 @@ test('options.match overrides .only', t => { match: ['*oo'] }); - runner.test('moo', a => { + runner.chain.test('moo', a => { t.pass(); a.pass(); }); - runner.test.only('boo', a => { + runner.chain.test.only('boo', a => { t.pass(); a.pass(); }); - runner.run({}).then(stats => { + runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.skipCount, 0); t.is(stats.passCount, 2); t.is(stats.testCount, 2); @@ -709,12 +716,13 @@ test('macros: Additional args will be spread as additional args on implementatio const runner = new Runner(); - runner.test('test1', function (a) { + runner.chain.test('test1', function (a) { t.deepEqual(slice.call(arguments, 1), ['foo', 'bar']); a.pass(); }, 'foo', 'bar'); - runner.run({}).then(stats => { + runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.passCount, 1); t.is(stats.testCount, 1); t.end(); @@ -746,11 +754,12 @@ test('macros: Customize test names attaching a `title` function', t => { const runner = new Runner(); - runner.test(macroFn, 'A'); - runner.test('supplied', macroFn, 'B'); - runner.test(macroFn, 'C'); + runner.chain.test(macroFn, 'A'); + runner.chain.test('supplied', macroFn, 'B'); + runner.chain.test(macroFn, 'C'); - runner.run({}).then(stats => { + runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.passCount, 3); t.is(stats.testCount, 3); t.end(); @@ -771,10 +780,11 @@ test('match applies to macros', t => { match: ['foobar'] }); - runner.test(macroFn, 'foo'); - runner.test(macroFn, 'bar'); + runner.chain.test(macroFn, 'foo'); + runner.chain.test(macroFn, 'bar'); - runner.run({}).then(stats => { + runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.passCount, 1); t.is(stats.testCount, 1); t.end(); @@ -806,12 +816,13 @@ test('arrays of macros', t => { const runner = new Runner(); - runner.test([macroFnA, macroFnB], 'A'); - runner.test([macroFnA, macroFnB], 'B'); - runner.test(macroFnA, 'C'); - runner.test(macroFnB, 'D'); + runner.chain.test([macroFnA, macroFnB], 'A'); + runner.chain.test([macroFnA, macroFnB], 'B'); + runner.chain.test(macroFnA, 'C'); + runner.chain.test(macroFnB, 'D'); - runner.run({}).then(stats => { + runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.passCount, 6); t.is(stats.testCount, 6); t.is(expectedArgsA.length, 0); @@ -846,10 +857,11 @@ test('match applies to arrays of macros', t => { match: ['foobar'] }); - runner.test([fooMacro, barMacro, bazMacro], 'foo'); - runner.test([fooMacro, barMacro, bazMacro], 'bar'); + runner.chain.test([fooMacro, barMacro, bazMacro], 'foo'); + runner.chain.test([fooMacro, barMacro, bazMacro], 'bar'); - runner.run({}).then(stats => { + runner.run({}).then(() => { + const stats = runner.buildStats(); t.is(stats.passCount, 1); t.is(stats.testCount, 1); t.end(); diff --git a/test/snapshot-state.js b/test/snapshot-state.js deleted file mode 100644 index 3e324cc30..000000000 --- a/test/snapshot-state.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; -const path = require('path'); -const test = require('tap').test; -const sinon = require('sinon'); -const snapshotState = require('../lib/snapshot-state'); - -test('snapshot state gets created and returned', t => { - const stateStub = sinon.stub().returns('state'); - - t.plan(3); - - t.doesNotThrow(() => { - const result = snapshotState.get(stateStub, { - file: path.join('hello', 'world.test.js'), - updateSnapshots: false - }); - - t.is(result, 'state'); - }); - - t.ok(stateStub.calledWith( - path.join('hello', 'world.test.js'), - false, - path.join('hello', '__snapshots__', 'world.test.js.snap'), - true - )); - - t.end(); -}); - -test('snapshot state is returned immediately if it already exists', t => { - const stateSpy = sinon.spy(); - - t.plan(3); - - snapshotState.state = 'already made state'; - - t.doesNotThrow(() => { - const result = snapshotState.get(stateSpy); - t.is(result, 'already made state'); - }); - - t.false(stateSpy.called); - - t.end(); -}); diff --git a/types/make.js b/types/make.js index ad086897f..46990fa88 100644 --- a/types/make.js +++ b/types/make.js @@ -15,14 +15,14 @@ const path = require('path'); const fs = require('fs'); const isArraySorted = require('is-array-sorted'); -const runner = require('../lib/runner'); +const Runner = require('../lib/runner'); const arrayHas = parts => part => parts.indexOf(part) !== -1; const base = fs.readFileSync(path.join(__dirname, 'base.d.ts'), 'utf8'); // All suported function names -const allParts = Object.keys(runner._chainableMethods).filter(name => name !== 'test'); +const allParts = Object.keys(new Runner({}).chain).filter(name => name !== 'test'); // The output consists of the base declarations, the actual 'test' function declarations, // and the namespaced chainable methods.