diff --git a/docs/02-execution-context.md b/docs/02-execution-context.md index 2a255b2c4..71b312a2c 100644 --- a/docs/02-execution-context.md +++ b/docs/02-execution-context.md @@ -22,6 +22,10 @@ The test title. Contains shared state from hooks. +## `t.passed` + +Whether a test has passed. This value is only accurate in the `test.afterEach()` and `test.afterEach.always()` hooks. + ## `t.plan(count)` Plan how many assertion there are in the test. The test will fail if the actual assertion count doesn't match the number of planned assertions. See [assertion planning](./03-assertions.md#assertion-planning). diff --git a/index.d.ts b/index.d.ts index 04bcb5482..87999fb14 100644 --- a/index.d.ts +++ b/index.d.ts @@ -308,6 +308,9 @@ export interface ExecutionContext extends Assertions { /** Title of the test or hook. */ readonly title: string; + /** Whether the test has passed. Only accurate in afterEach hooks. */ + readonly passed: boolean; + log: LogFn; plan: PlanFn; timeout: TimeoutFn; diff --git a/lib/runner.js b/lib/runner.js index 98ef38cb5..80f76791e 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -266,7 +266,7 @@ class Runner extends Emittery { return result; } - async runHooks(tasks, contextRef, titleSuffix) { + async runHooks(tasks, contextRef, titleSuffix, testPassed) { const hooks = tasks.map(task => new Runnable({ contextRef, experiments: this.experiments, @@ -278,7 +278,8 @@ class Runner extends Emittery { updateSnapshots: this.updateSnapshots, metadata: task.metadata, powerAssert: this.powerAssert, - title: `${task.title}${titleSuffix || ''}` + title: `${task.title}${titleSuffix || ''}`, + testPassed })); const outcome = await this.runMultiple(hooks, this.serial); for (const result of outcome.storedResults) { @@ -304,10 +305,11 @@ class Runner extends Emittery { } async runTest(task, contextRef) { - let hooksAndTestOk = false; - const hookSuffix = ` for ${task.title}`; - if (await this.runHooks(this.tasks.beforeEach, contextRef, hookSuffix)) { + let hooksOk = await this.runHooks(this.tasks.beforeEach, contextRef, hookSuffix); + + let testOk = false; + if (hooksOk) { // Only run the test if all `beforeEach` hooks passed. const test = new Runnable({ contextRef, @@ -325,7 +327,9 @@ class Runner extends Emittery { }); const result = await this.runSingle(test); - if (result.passed) { + testOk = result.passed; + + if (testOk) { this.emit('stateChange', { type: 'test-passed', title: result.title, @@ -333,7 +337,8 @@ class Runner extends Emittery { knownFailing: result.metadata.failing, logs: result.logs }); - hooksAndTestOk = await this.runHooks(this.tasks.afterEach, contextRef, hookSuffix); + + hooksOk = await this.runHooks(this.tasks.afterEach, contextRef, hookSuffix, testOk); } else { this.emit('stateChange', { type: 'test-failed', @@ -347,8 +352,8 @@ class Runner extends Emittery { } } - const alwaysOk = await this.runHooks(this.tasks.afterEachAlways, contextRef, hookSuffix); - return hooksAndTestOk && alwaysOk; + const alwaysOk = await this.runHooks(this.tasks.afterEachAlways, contextRef, hookSuffix, testOk); + return alwaysOk && hooksOk && testOk; } async start() { diff --git a/lib/test.js b/lib/test.js index d9bdee34c..99e7bfb4a 100644 --- a/lib/test.js +++ b/lib/test.js @@ -174,6 +174,10 @@ class ExecutionContext extends assert.Assertions { testMap.get(this).contextRef.set(context); } + get passed() { + return testMap.get(this).testPassed; + } + _throwsArgStart(assertion, file, line) { testMap.get(this).trackThrows({assertion, file, line}); } @@ -192,6 +196,7 @@ class Test { this.metadata = options.metadata; this.powerAssert = options.powerAssert; this.title = options.title; + this.testPassed = options.testPassed; this.registerUniqueTitle = options.registerUniqueTitle; this.logs = []; diff --git a/test-tap/hooks.js b/test-tap/hooks.js index ac67f14a1..00be44408 100644 --- a/test-tap/hooks.js +++ b/test-tap/hooks.js @@ -371,6 +371,90 @@ test('afterEach.always run even if beforeEach failed', t => { }); }); +test('afterEach: property `passed` of execution-context is false when test failed and true when test passed', t => { + t.plan(1); + + const passed = []; + let i; + return promiseEnd(new Runner(), runner => { + runner.chain.afterEach(a => { + passed[i] = a.passed; + }); + + runner.chain('failure', () => { + i = 0; + throw new Error('something went wrong'); + }); + runner.chain('pass', a => { + i = 1; + a.pass(); + }); + }).then(() => { + t.strictDeepEqual(passed, [undefined, true]); + }); +}); + +test('afterEach.always: property `passed` of execution-context is false when test failed and true when test passed', t => { + t.plan(1); + + const passed = []; + let i; + return promiseEnd(new Runner(), runner => { + runner.chain.afterEach.always(a => { + passed[i] = a.passed; + }); + + runner.chain('failure', () => { + i = 0; + throw new Error('something went wrong'); + }); + runner.chain('pass', a => { + i = 1; + a.pass(); + }); + }).then(() => { + t.strictDeepEqual(passed, [false, true]); + }); +}); + +test('afterEach.always: property `passed` of execution-context is false when before hook failed', t => { + t.plan(1); + + let passed; + return promiseEnd(new Runner(), runner => { + runner.chain.before(() => { + throw new Error('something went wrong'); + }); + runner.chain.afterEach.always(a => { + passed = a.passed; + }); + runner.chain('pass', a => { + a.pass(); + }); + }).then(() => { + t.false(passed); + }); +}); + +test('afterEach.always: property `passed` of execution-context is true when test passed and afterEach hook failed', t => { + t.plan(1); + + let passed; + return promiseEnd(new Runner(), runner => { + runner.chain.afterEach(() => { + throw new Error('something went wrong'); + }); + runner.chain.afterEach.always(a => { + passed = a.passed; + }); + runner.chain('pass', a => { + a.pass(); + }); + }).then(() => { + t.true(passed); + }); +}); + test('ensure hooks run only around tests', t => { t.plan(1);