diff --git a/index.d.ts b/index.d.ts index 1d43ec91b..a9580e251 100644 --- a/index.d.ts +++ b/index.d.ts @@ -69,9 +69,9 @@ export interface TestInterface { (title: string, macro: Macro | Macro[], ...args: Array): void; (macro: Macro | Macro[], ...args: Array): void; - after: AfterInterface; + after: AfterInterface; afterEach: AfterInterface; - before: BeforeInterface; + before: BeforeInterface; beforeEach: BeforeInterface; cb: CbInterface; failing: FailingInterface; @@ -187,9 +187,9 @@ declare const test: TestInterface; export default test; export {test}; -export const after: AfterInterface; +export const after: AfterInterface; export const afterEach: AfterInterface; -export const before: BeforeInterface; +export const before: BeforeInterface; export const beforeEach: BeforeInterface; export const cb: CbInterface; export const failing: FailingInterface; diff --git a/index.js.flow b/index.js.flow index 118307b29..42a578e6e 100644 --- a/index.js.flow +++ b/index.js.flow @@ -72,9 +72,9 @@ export interface TestInterface { (title: string, macro: Macro | Macro[], ...args: Array): void; (macro: Macro | Macro[], ...args: Array): void; - after: AfterInterface; + after: AfterInterface; afterEach: AfterInterface; - before: BeforeInterface; + before: BeforeInterface; beforeEach: BeforeInterface; cb: CbInterface; failing: FailingInterface; @@ -189,9 +189,9 @@ export type TodoDeclaration = {(title: string): void}; declare export default TestInterface<>; declare export var test: TestInterface<>; -declare export var after: AfterInterface; +declare export var after: AfterInterface<>; declare export var afterEach: AfterInterface<>; -declare export var before: BeforeInterface; +declare export var before: BeforeInterface<>; declare export var beforeEach: BeforeInterface<>; declare export var cb: CbInterface<>; declare export var failing: FailingInterface<>; diff --git a/lib/test-collection.js b/lib/test-collection.js index 46183d252..0569e88e6 100644 --- a/lib/test-collection.js +++ b/lib/test-collection.js @@ -1,9 +1,48 @@ 'use strict'; const EventEmitter = require('events'); +const clone = require('lodash.clone'); const Concurrent = require('./concurrent'); const Sequence = require('./sequence'); const Test = require('./test'); +class ContextRef { + constructor() { + this.value = {}; + } + + get() { + return this.value; + } + + set(newValue) { + this.value = newValue; + } + + copy() { + return new LateBinding(this); // eslint-disable-line no-use-before-define + } +} + +class LateBinding extends ContextRef { + constructor(ref) { + super(); + this.ref = ref; + this.bound = false; + } + + get() { + if (!this.bound) { + this.set(clone(this.ref.get())); + } + return super.get(); + } + + set(newValue) { + this.bound = true; + super.set(newValue); + } +} + class TestCollection extends EventEmitter { constructor(options) { super(); @@ -98,9 +137,9 @@ class TestCollection extends EventEmitter { this.emit('test', result); } - _buildHooks(hooks, testTitle, context) { + _buildHooks(hooks, testTitle, contextRef) { return hooks.map(hook => { - const test = this._buildHook(hook, testTitle, context); + const test = this._buildHook(hook, testTitle, contextRef); if (hook.metadata.skipped || hook.metadata.todo) { return this._skippedTest(test); @@ -117,10 +156,6 @@ class TestCollection extends EventEmitter { title += ` for ${testTitle}`; } - if (!contextRef) { - contextRef = null; - } - const test = new Test({ contextRef, failWithoutAssertions: false, @@ -135,10 +170,6 @@ class TestCollection extends EventEmitter { } _buildTest(test, contextRef) { - if (!contextRef) { - contextRef = null; - } - test = new Test({ contextRef, failWithoutAssertions: this.failWithoutAssertions, @@ -152,26 +183,26 @@ class TestCollection extends EventEmitter { return test; } - _buildTestWithHooks(test) { + _buildTestWithHooks(test, contextRef) { if (test.metadata.skipped || test.metadata.todo) { return new Sequence([this._skippedTest(this._buildTest(test))], true); } - const context = {context: {}}; + const copiedRef = contextRef.copy(); - const beforeHooks = this._buildHooks(this.hooks.beforeEach, test.title, context); - const afterHooks = this._buildHooks(this.hooks.afterEach, test.title, context); + const beforeHooks = this._buildHooks(this.hooks.beforeEach, test.title, copiedRef); + const afterHooks = this._buildHooks(this.hooks.afterEach, test.title, copiedRef); - let sequence = new Sequence([].concat(beforeHooks, this._buildTest(test, context), afterHooks), true); + let sequence = new Sequence([].concat(beforeHooks, this._buildTest(test, copiedRef), afterHooks), true); if (this.hooks.afterEachAlways.length > 0) { - const afterAlwaysHooks = new Sequence(this._buildHooks(this.hooks.afterEachAlways, test.title, context)); + const afterAlwaysHooks = new Sequence(this._buildHooks(this.hooks.afterEachAlways, test.title, copiedRef)); sequence = new Sequence([sequence, afterAlwaysHooks], false); } return sequence; } - _buildTests(tests) { - return tests.map(test => this._buildTestWithHooks(test)); + _buildTests(tests, contextRef) { + return tests.map(test => this._buildTestWithHooks(test, contextRef)); } _hasUnskippedTests() { @@ -182,22 +213,24 @@ class TestCollection extends EventEmitter { } build() { - const serialTests = new Sequence(this._buildTests(this.tests.serial), this.bail); - const concurrentTests = new Concurrent(this._buildTests(this.tests.concurrent), this.bail); + const contextRef = new ContextRef(); + + const serialTests = new Sequence(this._buildTests(this.tests.serial, contextRef), this.bail); + const concurrentTests = new Concurrent(this._buildTests(this.tests.concurrent, contextRef), this.bail); const allTests = new Sequence([serialTests, concurrentTests]); let finalTests; // Only run before and after hooks when there are unskipped tests if (this._hasUnskippedTests()) { - const beforeHooks = new Sequence(this._buildHooks(this.hooks.before)); - const afterHooks = new Sequence(this._buildHooks(this.hooks.after)); + const beforeHooks = new Sequence(this._buildHooks(this.hooks.before, null, contextRef)); + const afterHooks = new Sequence(this._buildHooks(this.hooks.after, null, contextRef)); finalTests = new Sequence([beforeHooks, allTests, afterHooks], true); } else { finalTests = new Sequence([allTests], true); } if (this.hooks.afterAlways.length > 0) { - const afterAlwaysHooks = new Sequence(this._buildHooks(this.hooks.afterAlways)); + const afterAlwaysHooks = new Sequence(this._buildHooks(this.hooks.afterAlways, null, contextRef)); finalTests = new Sequence([finalTests, afterAlwaysHooks], false); } diff --git a/lib/test.js b/lib/test.js index 839101b40..b3bbe873a 100644 --- a/lib/test.js +++ b/lib/test.js @@ -53,19 +53,11 @@ class ExecutionContext { } get context() { - const contextRef = this._test.contextRef; - return contextRef && contextRef.context; + return this._test.contextRef.get(); } set context(context) { - const contextRef = this._test.contextRef; - - if (!contextRef) { - this._test.saveFirstError(new Error(`\`t.context\` is not available in ${this._test.metadata.type} tests`)); - return; - } - - contextRef.context = context; + this._test.contextRef.set(context); } _throwsArgStart(assertion, file, line) { diff --git a/package-lock.json b/package-lock.json index 18852f01c..a4f1cabff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4845,6 +4845,11 @@ "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "dev": true }, + "lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=" + }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", diff --git a/package.json b/package.json index 7f5d1d3d8..e7211fdd4 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "is-observable": "^1.1.0", "is-promise": "^2.1.0", "last-line-stream": "^1.0.0", + "lodash.clone": "^4.5.0", "lodash.clonedeepwith": "^4.5.0", "lodash.debounce": "^4.0.3", "lodash.difference": "^4.3.0", diff --git a/test/hooks.js b/test/hooks.js index 28c176732..ee731b4c1 100644 --- a/test/hooks.js +++ b/test/hooks.js @@ -454,26 +454,36 @@ test('shared context', t => { const runner = new Runner(); runner.chain.before(a => { - a.is(a.context, null); + a.deepEqual(a.context, {}); + a.context.arr = ['a']; + a.context.prop = 'before'; }); runner.chain.after(a => { - a.is(a.context, null); + a.deepEqual(a.context.arr, ['a', 'b', 'c', 'd']); + a.is(a.context.prop, 'before'); }); runner.chain.beforeEach(a => { - a.context.arr = ['a']; + a.deepEqual(a.context.arr, ['a']); + a.context.arr.push('b'); + a.is(a.context.prop, 'before'); + a.context.prop = 'beforeEach'; }); runner.chain('test', a => { a.pass(); - a.context.arr.push('b'); a.deepEqual(a.context.arr, ['a', 'b']); + a.context.arr.push('c'); + a.is(a.context.prop, 'beforeEach'); + a.context.prop = 'test'; }); runner.chain.afterEach(a => { - a.context.arr.push('c'); a.deepEqual(a.context.arr, ['a', 'b', 'c']); + a.context.arr.push('d'); + a.is(a.context.prop, 'test'); + a.context.prop = 'afterEach'; }); return runner.run({}).then(() => { diff --git a/test/test.js b/test/test.js index 68ff0c43f..72b9500b9 100644 --- a/test/test.js +++ b/test/test.js @@ -8,9 +8,21 @@ const Test = require('../lib/test'); const failingTestHint = 'Test was expected to fail, but succeeded, you should stop marking the test as failing'; const noop = () => {}; +class ContextRef { + constructor() { + this.value = {}; + } + get() { + return this.value; + } + set(newValue) { + this.value = newValue; + } +} + function ava(fn, contextRef, onResult) { return new Test({ - contextRef, + contextRef: contextRef || new ContextRef(), failWithoutAssertions: true, fn, metadata: {type: 'test', callback: false}, @@ -21,7 +33,7 @@ function ava(fn, contextRef, onResult) { ava.failing = (fn, contextRef, onResult) => { return new Test({ - contextRef, + contextRef: contextRef || new ContextRef(), failWithoutAssertions: true, fn, metadata: {type: 'test', callback: false, failing: true}, @@ -32,7 +44,7 @@ ava.failing = (fn, contextRef, onResult) => { ava.cb = (fn, contextRef, onResult) => { return new Test({ - contextRef, + contextRef: contextRef || new ContextRef(), failWithoutAssertions: true, fn, metadata: {type: 'test', callback: true}, @@ -43,7 +55,7 @@ ava.cb = (fn, contextRef, onResult) => { ava.cb.failing = (fn, contextRef, onResult) => { return new Test({ - contextRef, + contextRef: contextRef || new ContextRef(), failWithoutAssertions: true, fn, metadata: {type: 'test', callback: true, failing: true}, @@ -615,7 +627,11 @@ test('no crash when adding assertions after the test has ended', t => { test('contextRef', t => { new Test({ - contextRef: {context: {foo: 'bar'}}, + contextRef: { + get() { + return {foo: 'bar'}; + } + }, failWithoutAssertions: true, fn(a) { a.pass(); @@ -628,21 +644,6 @@ test('contextRef', t => { }).run(); }); -test('it is an error to set context in a hook', t => { - let result; - const avaTest = ava(a => { - a.context = 'foo'; - }, null, r => { - result = r; - }); - avaTest.metadata.type = 'foo'; - - const passed = avaTest.run(); - t.is(passed, false); - t.match(result.reason.message, /`t\.context` is not available in foo tests/); - t.end(); -}); - test('failing tests should fail', t => { const passed = ava.failing('foo', a => { a.fail();