diff --git a/integration_tests/__tests__/jasmine_async-test.js b/integration_tests/__tests__/jasmine_async-test.js index aed80cbb68f2..c11c871e6fec 100644 --- a/integration_tests/__tests__/jasmine_async-test.js +++ b/integration_tests/__tests__/jasmine_async-test.js @@ -21,8 +21,8 @@ describe('async jasmine', () => { expect(json.numPendingTests).toBe(0); const {message} = json.testResults[0]; - expect(message).toMatch('with failing timeout'); - expect(message).toMatch('Async callback was not invoked within timeout'); + expect(message).toMatch('with failing async'); + expect(message).toMatch('timeout'); }); it('works with beforeEach', () => { diff --git a/integration_tests/__tests__/test-hooks-test.js b/integration_tests/__tests__/test-hooks-test.js new file mode 100644 index 000000000000..b1572f2ed5fb --- /dev/null +++ b/integration_tests/__tests__/test-hooks-test.js @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +'use strict'; + +const path = require('path'); +const skipOnWindows = require('skipOnWindows'); +const {extractSummary, cleanup, writeFiles} = require('../utils'); +const runJest = require('../runJest'); + +const DIR = path.resolve(__dirname, '../test-hooks'); + +skipOnWindows.suite(); + +beforeEach(() => cleanup(DIR)); +afterAll(() => cleanup(DIR)); + +// Blocked by https://github.com/facebook/jest/issues/3785 +test.skip('fails because of the error in afterAll hook', () => { + writeFiles(DIR, { + '__tests__/hooks-test.js': ` + // keep the counter to make sure we execute the hook only once. + let count = 0; + beforeAll(() => { throw new Error('afterAll error ' + count++); }); + + test('one', () => {}); + test('two', () => {}); + `, + 'package.json': '{}', + }); + + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false']); + const {rest, summary} = extractSummary(stderr); + expect(rest).toMatchSnapshot(); + expect(summary).toMatchSnapshot(); + expect(status).toBe(1); +}); diff --git a/integration_tests/jasmine_async/__tests__/promise_beforeAll-test.js b/integration_tests/jasmine_async/__tests__/promise_beforeAll-test.js index 931facf8240d..8758e5265134 100644 --- a/integration_tests/jasmine_async/__tests__/promise_beforeAll-test.js +++ b/integration_tests/jasmine_async/__tests__/promise_beforeAll-test.js @@ -9,11 +9,13 @@ 'use strict'; describe('promise beforeAll', () => { + let flag; + beforeAll(() => { return new Promise(resolve => { process.nextTick(resolve); }).then(() => { - this.flag = 1; + flag = 1; }); }); @@ -23,14 +25,14 @@ describe('promise beforeAll', () => { // passing tests it('runs tests after beforeAll asynchronously completes', () => { - expect(this.flag).toBe(1); + expect(flag).toBe(1); }); - describe('with failing timeout', () => { + describe('with failing async', () => { // failing before hook beforeAll(() => { return new Promise(resolve => setTimeout(resolve, 100)); - }, 10); + }, 11); it('fails', () => {}); }); diff --git a/packages/jest-circus/src/__mocks__/test_event_handler.js b/packages/jest-circus/src/__mocks__/test_event_handler.js new file mode 100644 index 000000000000..e7b461fd67a2 --- /dev/null +++ b/packages/jest-circus/src/__mocks__/test_event_handler.js @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @noflow + */ + +'use strict'; + +const testEventHandler = (event, state) => { + switch (event.name) { + case 'start_describe_definition': + case 'finish_describe_definition': { + console.log(event.name + ':', event.blockName); + break; + } + case 'run_describe_start': + case 'run_describe_finish': { + console.log(event.name + ':', event.describeBlock.name); + break; + } + case 'test_start': + case 'test_done': { + console.log(event.name + ':', event.test.name); + break; + } + + case 'add_test': { + console.log(event.name + ':', event.testName); + break; + } + + case 'test_fn_start': + case 'test_fn_success': + case 'test_fn_failure': { + console.log(event.name + ':', event.test.name); + break; + } + + case 'add_hook': { + console.log(event.name + ':', event.hookType); + break; + } + + case 'hook_start': + case 'hook_success': + case 'hook_failure': { + console.log(event.name + ':', event.hook.type); + break; + } + + default: { + console.log(event.name); + } + } + + if (event.name === 'run_finish') { + console.log(''); + console.log(`unhandledErrors: ${String(state.unhandledErrors.length)}`); + } +}; + +module.exports = testEventHandler; diff --git a/packages/jest-circus/src/__mocks__/test_utils.js b/packages/jest-circus/src/__mocks__/test_utils.js new file mode 100644 index 000000000000..2ded61256d40 --- /dev/null +++ b/packages/jest-circus/src/__mocks__/test_utils.js @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ + +'use strict'; + +import os from 'os'; +import path from 'path'; +import {spawnSync} from 'child_process'; +import fs from 'fs'; + +const CIRCUS_PATH = path.resolve(__dirname, '../../build/index'); +const CIRCUS_RUN_PATH = path.resolve(__dirname, '../../build/run'); +const CIRCUS_STATE_PATH = path.resolve(__dirname, '../../build/state'); +const TEST_EVENT_HANDLER_PATH = path.resolve(__dirname, './test_event_handler'); + +const runTest = (source: string) => { + const tmpFilename = path.join(os.tmpdir(), 'circus-test-file.js'); + + const content = ` + const circus = require('${CIRCUS_PATH}'); + global.test = circus.test; + global.describe = circus.describe; + global.beforeEach = circus.beforeEach; + global.afterEach = circus.afterEach; + global.beforeAll = circus.beforeAll; + global.afterAll = circus.afterAll; + + const testEventHandler = require('${TEST_EVENT_HANDLER_PATH}'); + const addEventHandler = require('${CIRCUS_STATE_PATH}').addEventHandler; + addEventHandler(testEventHandler); + + ${source}; + + const run = require('${CIRCUS_RUN_PATH}'); + + run(); + `; + + fs.writeFileSync(tmpFilename, content); + const result = spawnSync('node', [tmpFilename], {cwd: process.cwd()}); + + if (result.status !== 0) { + const message = ` + STDOUT: ${result.stdout && result.stdout.toString()} + STDERR: ${result.stderr && result.stderr.toString()} + STATUS: ${result.status} + ERROR: ${String(result.error)} + `; + throw new Error(message); + } + + result.stdout = String(result.stdout); + result.stderr = String(result.stderr); + + if (result.stderr) { + throw new Error( + ` + Unexpected stderr: + ${result.stderr} + `, + ); + } + return result; +}; + +module.exports = { + runTest, +}; diff --git a/packages/jest-circus/src/__tests__/__snapshots__/after_all-test.js.snap b/packages/jest-circus/src/__tests__/__snapshots__/after_all-test.js.snap new file mode 100644 index 000000000000..62f1c5843d32 --- /dev/null +++ b/packages/jest-circus/src/__tests__/__snapshots__/after_all-test.js.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`tests are not marked done until their parent afterAll runs 1`] = ` +"start_describe_definition: describe +add_hook: afterAll +add_test: one +add_test: two +start_describe_definition: 2nd level describe +add_hook: afterAll +add_test: 2nd level test +start_describe_definition: 3rd level describe +add_test: 3rd level test +add_test: 3rd level test#2 +finish_describe_definition: 3rd level describe +finish_describe_definition: 2nd level describe +finish_describe_definition: describe +start_describe_definition: 2nd describe +add_hook: afterAll +add_test: 2nd describe test +finish_describe_definition: 2nd describe +run_start +run_describe_start: ROOT_DESCRIBE_BLOCK +run_describe_start: describe +test_start: one +test_fn_start: one +test_fn_success: one +test_done: one +test_start: two +test_fn_start: two +test_fn_success: two +test_done: two +run_describe_start: 2nd level describe +test_start: 2nd level test +test_fn_start: 2nd level test +test_fn_success: 2nd level test +test_done: 2nd level test +run_describe_start: 3rd level describe +test_start: 3rd level test +test_fn_start: 3rd level test +test_fn_success: 3rd level test +test_done: 3rd level test +test_start: 3rd level test#2 +test_fn_start: 3rd level test#2 +test_fn_success: 3rd level test#2 +test_done: 3rd level test#2 +run_describe_finish: 3rd level describe +hook_start: afterAll +hook_success: afterAll +run_describe_finish: 2nd level describe +hook_start: afterAll +hook_success: afterAll +run_describe_finish: describe +run_describe_start: 2nd describe +test_start: 2nd describe test +test_fn_start: 2nd describe test +test_fn_success: 2nd describe test +test_done: 2nd describe test +hook_start: afterAll +hook_failure: afterAll +run_describe_finish: 2nd describe +run_describe_finish: ROOT_DESCRIBE_BLOCK +run_finish + +unhandledErrors: 1 +" +`; diff --git a/packages/jest-circus/src/__tests__/__snapshots__/base_test-test.js.snap b/packages/jest-circus/src/__tests__/__snapshots__/base_test-test.js.snap new file mode 100644 index 000000000000..d4a4cb2b86a5 --- /dev/null +++ b/packages/jest-circus/src/__tests__/__snapshots__/base_test-test.js.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`failures 1`] = ` +"start_describe_definition: describe +add_hook: beforeEach +add_hook: afterEach +add_test: one +add_test: two +finish_describe_definition: describe +run_start +run_describe_start: ROOT_DESCRIBE_BLOCK +run_describe_start: describe +test_start: one +hook_start: beforeEach +hook_success: beforeEach +test_fn_start: one +test_fn_failure: one +hook_start: afterEach +hook_failure: afterEach +test_done: one +test_start: two +hook_start: beforeEach +hook_success: beforeEach +test_fn_start: two +test_fn_success: two +hook_start: afterEach +hook_failure: afterEach +test_done: two +run_describe_finish: describe +run_describe_finish: ROOT_DESCRIBE_BLOCK +run_finish + +unhandledErrors: 0 +" +`; + +exports[`simple test 1`] = ` +"start_describe_definition: describe +add_hook: beforeEach +add_hook: afterEach +add_test: one +add_test: two +finish_describe_definition: describe +run_start +run_describe_start: ROOT_DESCRIBE_BLOCK +run_describe_start: describe +test_start: one +hook_start: beforeEach +hook_success: beforeEach +test_fn_start: one +test_fn_success: one +hook_start: afterEach +hook_success: afterEach +test_done: one +test_start: two +hook_start: beforeEach +hook_success: beforeEach +test_fn_start: two +test_fn_success: two +hook_start: afterEach +hook_success: afterEach +test_done: two +run_describe_finish: describe +run_describe_finish: ROOT_DESCRIBE_BLOCK +run_finish + +unhandledErrors: 0 +" +`; diff --git a/packages/jest-circus/src/__tests__/after_all-test.js b/packages/jest-circus/src/__tests__/after_all-test.js new file mode 100644 index 000000000000..6c5e959e8854 --- /dev/null +++ b/packages/jest-circus/src/__tests__/after_all-test.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +'use strict'; + +import {runTest} from '../__mocks__/test_utils'; + +test('tests are not marked done until their parent afterAll runs', () => { + const {stdout} = runTest(` + describe('describe', () => { + afterAll(() => {}); + test('one', () => {}); + test('two', () => {}); + describe('2nd level describe', () => { + afterAll(() => {}); + test('2nd level test', () => {}); + + describe('3rd level describe', () => { + test('3rd level test', () => {}); + test('3rd level test#2', () => {}); + }); + }); + }) + + describe('2nd describe', () => { + afterAll(() => { throw new Error('alabama'); }); + test('2nd describe test', () => {}); + }) + `); + + expect(stdout).toMatchSnapshot(); +}); diff --git a/packages/jest-circus/src/__tests__/base_test-test.js b/packages/jest-circus/src/__tests__/base_test-test.js new file mode 100644 index 000000000000..fd0b2b300c5d --- /dev/null +++ b/packages/jest-circus/src/__tests__/base_test-test.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +'use strict'; + +import {runTest} from '../__mocks__/test_utils'; + +test('simple test', () => { + const {stdout} = runTest(` + describe('describe', () => { + beforeEach(() => {}); + afterEach(() => {}); + test('one', () => {}); + test('two', () => {}); + }) + `); + + expect(stdout).toMatchSnapshot(); +}); + +test('failures', () => { + const {stdout} = runTest(` + describe('describe', () => { + beforeEach(() => {}); + afterEach(() => { throw new Error('banana')}); + test('one', () => { throw new Error('kentucky')}); + test('two', () => {}); + }) + `); + + expect(stdout).toMatchSnapshot(); +}); diff --git a/packages/jest-circus/src/eventHandler.js b/packages/jest-circus/src/eventHandler.js index 789c399a209d..75b36b021968 100644 --- a/packages/jest-circus/src/eventHandler.js +++ b/packages/jest-circus/src/eventHandler.js @@ -10,7 +10,13 @@ import type {EventHandler} from '../types'; -import {makeDescribe, getTestDuration, makeTest} from './utils'; +import { + addErrorToEachTestUnderDescribe, + makeDescribe, + getTestDuration, + invariant, + makeTest, +} from './utils'; // To pass this value from Runtime object to state we need to use global[sym] const TEST_TIMEOUT_SYMBOL = Symbol.for('TEST_TIMEOUT_SYMBOL'); @@ -30,11 +36,7 @@ const handler: EventHandler = (event, state): void => { } case 'finish_describe_definition': { const {currentDescribeBlock} = state; - if (!currentDescribeBlock) { - throw new Error( - `"currentDescribeBlock" has to be there since we're finishing its definition.`, - ); - } + invariant(currentDescribeBlock, `currentDescribeBlock mest to be there`); if (currentDescribeBlock.parent) { state.currentDescribeBlock = currentDescribeBlock.parent; } @@ -42,35 +44,51 @@ const handler: EventHandler = (event, state): void => { } case 'add_hook': { const {currentDescribeBlock} = state; - const {fn, hookType: type} = event; - currentDescribeBlock.hooks.push({fn, type}); + const {fn, hookType: type, timeout} = event; + const parent = currentDescribeBlock; + currentDescribeBlock.hooks.push({fn, parent, timeout, type}); break; } case 'add_test': { const {currentDescribeBlock} = state; - const {fn, mode, testName: name} = event; - const test = makeTest(fn, mode, name, currentDescribeBlock); + const {fn, mode, testName: name, timeout} = event; + const test = makeTest(fn, mode, name, currentDescribeBlock, timeout); test.mode === 'only' && (state.hasFocusedTests = true); currentDescribeBlock.tests.push(test); break; } - case 'test_start': { - event.test.startedAt = Date.now(); + case 'hook_failure': { + const {test, describeBlock, error, hook} = event; + const {type} = hook; + + if (type === 'beforeAll') { + invariant(describeBlock, 'always present for `*All` hooks'); + addErrorToEachTestUnderDescribe(describeBlock, error); + } else if (type === 'afterAll') { + // Attaching `afterAll` errors to each test makes execution flow + // too complicated, so we'll consider them to be global. + state.unhandledErrors.push(error); + } else { + invariant(test, 'always present for `*Each` hooks'); + test.errors.push(error); + } break; } case 'test_skip': { event.test.status = 'skip'; break; } - case 'test_failure': { - event.test.status = 'fail'; + case 'test_done': { event.test.duration = getTestDuration(event.test); - event.test.errors.push(event.error); + event.test.status = 'done'; break; } - case 'test_success': { - event.test.status = 'pass'; - event.test.duration = getTestDuration(event.test); + case 'test_start': { + event.test.startedAt = Date.now(); + break; + } + case 'test_fn_failure': { + event.test.errors.push(event.error); break; } case 'run_start': { @@ -78,6 +96,10 @@ const handler: EventHandler = (event, state): void => { (state.testTimeout = global[TEST_TIMEOUT_SYMBOL]); break; } + case 'error': { + state.unhandledErrors.push(event.error); + break; + } } }; diff --git a/packages/jest-circus/src/formatNodeAssertErrors.js b/packages/jest-circus/src/formatNodeAssertErrors.js index ed4d022afb02..c17f848df000 100644 --- a/packages/jest-circus/src/formatNodeAssertErrors.js +++ b/packages/jest-circus/src/formatNodeAssertErrors.js @@ -41,8 +41,7 @@ const humanReadableOperators = { module.exports = (event: Event, state: State) => { switch (event.name) { - case 'test_failure': - case 'test_success': { + case 'test_done': { event.test.errors = event.test.errors.map(error => { return error instanceof require('assert').AssertionError ? assertionErrorMessage(error, {expand: state.expand}) diff --git a/packages/jest-circus/src/index.js b/packages/jest-circus/src/index.js index 0b2b1e6f911d..61a9bd900c85 100644 --- a/packages/jest-circus/src/index.js +++ b/packages/jest-circus/src/index.js @@ -19,6 +19,8 @@ import type { } from '../types'; import {dispatch} from './state'; +type THook = (fn: HookFn, timeout?: number) => void; + const describe = (blockName: BlockName, blockFn: BlockFn) => _dispatchDescribe(blockFn, blockName); describe.only = (blockName: BlockName, blockFn: BlockFn) => @@ -29,23 +31,23 @@ describe.skip = (blockName: BlockName, blockFn: BlockFn) => const _dispatchDescribe = (blockFn, blockName, mode?: BlockMode) => { dispatch({blockName, mode, name: 'start_describe_definition'}); blockFn(); - dispatch({name: 'finish_describe_definition'}); + dispatch({blockName, mode, name: 'finish_describe_definition'}); }; -const _addHook = (fn: HookFn, hookType: HookType) => - dispatch({fn, hookType, name: 'add_hook'}); -const beforeEach = (fn: HookFn) => _addHook(fn, 'beforeEach'); -const beforeAll = (fn: HookFn) => _addHook(fn, 'beforeAll'); -const afterEach = (fn: HookFn) => _addHook(fn, 'afterEach'); -const afterAll = (fn: HookFn) => _addHook(fn, 'afterAll'); +const _addHook = (fn: HookFn, hookType: HookType, timeout: ?number) => + dispatch({fn, hookType, name: 'add_hook', timeout}); +const beforeEach: THook = (fn, timeout) => _addHook(fn, 'beforeEach', timeout); +const beforeAll: THook = (fn, timeout) => _addHook(fn, 'beforeAll', timeout); +const afterEach: THook = (fn, timeout) => _addHook(fn, 'afterEach', timeout); +const afterAll: THook = (fn, timeout) => _addHook(fn, 'afterAll', timeout); -const test = (testName: TestName, fn?: TestFn) => - dispatch({fn, name: 'add_test', testName}); +const test = (testName: TestName, fn?: TestFn, timeout?: number) => + dispatch({fn, name: 'add_test', testName, timeout}); const it = test; -test.skip = (testName: TestName, fn?: TestFn) => - dispatch({fn, mode: 'skip', name: 'add_test', testName}); -test.only = (testName: TestName, fn: TestFn) => - dispatch({fn, mode: 'only', name: 'add_test', testName}); +test.skip = (testName: TestName, fn?: TestFn, timeout?: number) => + dispatch({fn, mode: 'skip', name: 'add_test', testName, timeout}); +test.only = (testName: TestName, fn: TestFn, timeout?: number) => + dispatch({fn, mode: 'only', name: 'add_test', testName, timeout}); module.exports = { afterAll, diff --git a/packages/jest-circus/src/legacy_code_todo_rewrite/jest-adapter-init.js b/packages/jest-circus/src/legacy_code_todo_rewrite/jest-adapter-init.js index 3df550157496..f629e5334932 100644 --- a/packages/jest-circus/src/legacy_code_todo_rewrite/jest-adapter-init.js +++ b/packages/jest-circus/src/legacy_code_todo_rewrite/jest-adapter-init.js @@ -43,6 +43,32 @@ const initialize = ({ global.fit = global.it.only; global.fdescribe = global.describe.only; + global.test.concurrent = ( + testName: string, + testFn: () => Promise, + timeout?: number, + ) => { + // For concurrent tests we first run the function that returns promise, and then register a + // nomral test that will be waiting on the returned promise (when we start the test, the promise + // will already be in the process of execution). + // Unfortunately at this stage there's no way to know if there are any `.only` tests in the suite + // that will result in this test to be skipped, so we'll be executing the promise function anyway, + // even if it ends up being skipped. + const promise = testFn(); + global.test(testName, () => promise, timeout); + }; + + global.test.concurrent.only = ( + testName: string, + testFn: () => Promise, + timeout?: number, + ) => { + const promise = testFn(); + global.test.only(testName, () => promise, timeout); + }; + + global.test.concurrent.skip = global.test.skip; + addEventHandler(eventHandler); // Jest tests snapshotSerializers in order preceding built-in serializers. @@ -74,32 +100,17 @@ const runAndTransformResultsToJestFormat = async ({ let numPassingTests = 0; let numPendingTests = 0; - for (const testResult of result) { - switch (testResult.status) { - case 'fail': - numFailingTests += 1; - break; - case 'pass': - numPassingTests += 1; - break; - case 'skip': - numPendingTests += 1; - break; - } - } - const assertionResults = result.map(testResult => { let status: Status; - switch (testResult.status) { - case 'fail': - status = 'failed'; - break; - case 'pass': - status = 'passed'; - break; - case 'skip': - status = 'pending'; - break; + if (testResult.status === 'skip') { + status = 'pending'; + numPendingTests += 1; + } else if (testResult.errors.length) { + status = 'failed'; + numFailingTests += 1; + } else { + status = 'passed'; + numPassingTests += 1; } const ancestorTitles = testResult.testPath.filter( @@ -107,7 +118,6 @@ const runAndTransformResultsToJestFormat = async ({ ); const title = ancestorTitles.pop(); - // $FlowFixMe Types are slightly incompatible and need to be refactored return { ancestorTitles, duration: testResult.duration, @@ -158,8 +168,7 @@ const eventHandler = (event: Event) => { setState({currentTestName: getTestID(event.test)}); break; } - case 'test_success': - case 'test_failure': { + case 'test_done': { _addSuppressedErrors(event.test); _addExpectedAssertionErrors(event.test); break; @@ -169,7 +178,6 @@ const eventHandler = (event: Event) => { const _addExpectedAssertionErrors = (test: TestEntry) => { const errors = extractExpectedAssertionsErrors(); - errors.length && (test.status = 'fail'); test.errors = test.errors.concat(errors); }; @@ -180,7 +188,6 @@ const _addSuppressedErrors = (test: TestEntry) => { const {suppressedErrors} = getState(); setState({suppressedErrors: []}); if (suppressedErrors.length) { - test.status = 'fail'; test.errors = test.errors.concat(suppressedErrors); } }; diff --git a/packages/jest-circus/src/run.js b/packages/jest-circus/src/run.js index 19854ba33038..026499903316 100644 --- a/packages/jest-circus/src/run.js +++ b/packages/jest-circus/src/run.js @@ -21,6 +21,7 @@ import { callAsyncFn, getAllHooksForDescribe, getEachHooksForTest, + invariant, makeTestResults, } from './utils'; @@ -37,22 +38,24 @@ const _runTestsForDescribeBlock = async (describeBlock: DescribeBlock) => { const {beforeAll, afterAll} = getAllHooksForDescribe(describeBlock); for (const hook of beforeAll) { - _callHook(hook); + await _callHook({describeBlock, hook}); } for (const test of describeBlock.tests) { await _runTest(test); } + for (const child of describeBlock.children) { await _runTestsForDescribeBlock(child); } for (const hook of afterAll) { - _callHook(hook); + await _callHook({describeBlock, hook}); } dispatch({describeBlock, name: 'run_describe_finish'}); }; const _runTest = async (test: TestEntry): Promise => { + dispatch({name: 'test_start', test}); const testContext = Object.create(null); const isSkipped = @@ -67,38 +70,62 @@ const _runTest = async (test: TestEntry): Promise => { const {afterEach, beforeEach} = getEachHooksForTest(test); for (const hook of beforeEach) { - await _callHook(hook, testContext); + if (test.errors.length) { + // If any of the before hooks failed already, we don't run any + // hooks after that. + break; + } + await _callHook({hook, test, testContext}); } await _callTest(test, testContext); for (const hook of afterEach) { - await _callHook(hook, testContext); + await _callHook({hook, test, testContext}); } + + // `afterAll` hooks should not affect test status (pass or fail), because if + // we had a global `afterAll` hook it would block all existing tests until + // this hook is executed. So we dispatche `test_done` right away. + dispatch({name: 'test_done', test}); }; -const _callHook = (hook: Hook, testContext?: TestContext): Promise => { +const _callHook = ({ + hook, + test, + describeBlock, + testContext, +}: { + hook: Hook, + describeBlock?: DescribeBlock, + test?: TestEntry, + testContext?: TestContext, +}): Promise => { dispatch({hook, name: 'hook_start'}); - const {testTimeout: timeout} = getState(); + const timeout = hook.timeout || getState().testTimeout; return callAsyncFn(hook.fn, testContext, {isHook: true, timeout}) - .then(() => dispatch({hook, name: 'hook_success'})) - .catch(error => dispatch({error, hook, name: 'hook_failure'})); + .then(() => dispatch({describeBlock, hook, name: 'hook_success', test})) + .catch(error => + dispatch({describeBlock, error, hook, name: 'hook_failure', test}), + ); }; const _callTest = async ( test: TestEntry, testContext: TestContext, -): Promise => { - dispatch({name: 'test_start', test}); - const {testTimeout: timeout} = getState(); +): Promise => { + dispatch({name: 'test_fn_start', test}); + const timeout = test.timeout || getState().testTimeout; + invariant(test.fn, `Tests with no 'fn' should have 'mode' set to 'skipped'`); - if (!test.fn) { - throw Error(`Tests with no 'fn' should have 'mode' set to 'skipped'`); + if (test.errors.length) { + // We don't run the test if there's already an error in before hooks. + return; } - return callAsyncFn(test.fn, testContext, {isHook: false, timeout}) - .then(() => dispatch({name: 'test_success', test})) - .catch(error => dispatch({error, name: 'test_failure', test})); + await callAsyncFn(test.fn, testContext, {isHook: false, timeout}) + .then(() => dispatch({name: 'test_fn_success', test})) + .catch(error => dispatch({error, name: 'test_fn_failure', test})); }; module.exports = run; diff --git a/packages/jest-circus/src/state.js b/packages/jest-circus/src/state.js index 20b0dbe272fa..a5353b720c46 100644 --- a/packages/jest-circus/src/state.js +++ b/packages/jest-circus/src/state.js @@ -29,6 +29,7 @@ const INITIAL_STATE: State = { hasFocusedTests: false, rootDescribeBlock: ROOT_DESCRIBE_BLOCK, testTimeout: 5000, + unhandledErrors: [], }; global[STATE_SYM] = INITIAL_STATE; diff --git a/packages/jest-circus/src/utils.js b/packages/jest-circus/src/utils.js index 94f10c3e4fee..715067084913 100644 --- a/packages/jest-circus/src/utils.js +++ b/packages/jest-circus/src/utils.js @@ -48,6 +48,7 @@ const makeTest = ( mode: TestMode, name: TestName, parent: DescribeBlock, + timeout: ?number, ): TestEntry => { if (!fn) { mode = 'skip'; // skip test if no fn passed @@ -65,6 +66,7 @@ const makeTest = ( parent, startedAt: null, status: null, + timeout, }; }; @@ -244,12 +246,33 @@ const _formatError = (error: ?Exception): string => { } }; +const addErrorToEachTestUnderDescribe = ( + describeBlock: DescribeBlock, + error: Exception, +) => { + for (const test of describeBlock.tests) { + test.errors.push(error); + } + + for (const child of describeBlock.children) { + addErrorToEachTestUnderDescribe(child, error); + } +}; + +const invariant = (condition: *, message: string) => { + if (!condition) { + throw new Error(message); + } +}; + module.exports = { + addErrorToEachTestUnderDescribe, callAsyncFn, getAllHooksForDescribe, getEachHooksForTest, getTestDuration, getTestID, + invariant, makeDescribe, makeTest, makeTestResults, diff --git a/packages/jest-circus/types.js b/packages/jest-circus/types.js index 91c006f51c44..308c09dd9330 100644 --- a/packages/jest-circus/types.js +++ b/packages/jest-circus/types.js @@ -19,10 +19,15 @@ export type HookFn = (done?: DoneFn) => ?Promise; export type AsyncFn = TestFn | HookFn; export type SharedHookType = 'afterAll' | 'beforeAll'; export type HookType = SharedHookType | 'afterEach' | 'beforeEach'; -export type Hook = {fn: HookFn, type: HookType}; export type TestContext = Object; export type Exception = any; // Since in JS anything can be thrown as an error. export type FormattedError = string; // String representation of error. +export type Hook = { + fn: HookFn, + type: HookType, + parent: DescribeBlock, + timeout: ?number, +}; export type EventHandler = (event: Event, state: State) => void; @@ -33,18 +38,22 @@ export type Event = blockName: BlockName, |} | {| + mode: BlockMode, name: 'finish_describe_definition', + blockName: BlockName, |} | {| name: 'add_hook', hookType: HookType, fn: HookFn, + timeout: ?number, |} | {| name: 'add_test', testName: TestName, fn?: TestFn, mode?: TestMode, + timeout: ?number, |} | {| name: 'hook_start', @@ -52,30 +61,48 @@ export type Event = |} | {| name: 'hook_success', + describeBlock: ?DescribeBlock, + test: ?TestEntry, hook: Hook, |} | {| name: 'hook_failure', error: string | Exception, + describeBlock: ?DescribeBlock, + test: ?TestEntry, hook: Hook, |} | {| - name: 'test_start', + name: 'test_fn_start', test: TestEntry, |} | {| - name: 'test_success', + name: 'test_fn_success', test: TestEntry, |} | {| - name: 'test_failure', + name: 'test_fn_failure', error: Exception, test: TestEntry, |} + | {| + // the `test` in this case is all hooks + it/test function, not just the + // function passed to `it/test` + name: 'test_start', + test: TestEntry, + |} | {| name: 'test_skip', test: TestEntry, |} + | {| + // test failure is defined by presence of errors in `test.errors`, + // `test_done` indicates that the test and all its hooks were run, + // and nothing else will change it's state in the future. (except third + // party extentions/plugins) + name: 'test_done', + test: TestEntry, + |} | {| name: 'run_describe_start', describeBlock: DescribeBlock, @@ -89,9 +116,15 @@ export type Event = |} | {| name: 'run_finish', + |} + | {| + // Any unhandled error that happened outside of test/hooks (unless it is + // an `afterAll` hook) + name: 'error', + error: Exception, |}; -export type TestStatus = 'pass' | 'fail' | 'skip'; +export type TestStatus = 'skip' | 'done'; export type TestResult = {| duration: ?number, errors: Array, @@ -107,6 +140,7 @@ export type State = {| rootDescribeBlock: DescribeBlock, testTimeout: number, expand?: boolean, // expand error messages + unhandledErrors: Array, |}; export type DescribeBlock = {| @@ -126,5 +160,6 @@ export type TestEntry = {| parent: DescribeBlock, startedAt: ?number, duration: ?number, - status: ?TestStatus, + status: ?TestStatus, // whether the test has been skipped or run already + timeout: ?number, |};