diff --git a/packages/jest-circus/src/__tests__/__snapshots__/hooks.test.ts.snap b/packages/jest-circus/src/__tests__/__snapshots__/hooks.test.ts.snap index 277e3c17e2f7..ee7828c378f2 100644 --- a/packages/jest-circus/src/__tests__/__snapshots__/hooks.test.ts.snap +++ b/packages/jest-circus/src/__tests__/__snapshots__/hooks.test.ts.snap @@ -1,5 +1,104 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`async cleanup functions are properly awaited 1`] = ` +"start_describe_definition: describe +add_hook: beforeEach +add_hook: afterEach +add_test: test +finish_describe_definition: describe +start_describe_definition: describe with beforeAll +add_hook: beforeAll +add_hook: afterAll +add_test: test +finish_describe_definition: describe with beforeAll +run_start +run_describe_start: ROOT_DESCRIBE_BLOCK +run_describe_start: describe +test_start: test +test_started: test +hook_start: beforeEach +> beforeEach value: 1 +hook_success: beforeEach +test_fn_start: test +> test value: 1 +test_fn_success: test +hook_start: afterEach +> cleanup value: 0 +hook_success: afterEach +hook_start: afterEach +> afterEach value: 0 +hook_success: afterEach +test_done: test +run_describe_finish: describe +run_describe_start: describe with beforeAll +hook_start: beforeAll +> beforeAll value: 2 +hook_success: beforeAll +test_start: test +test_started: test +test_fn_start: test +> test value: 2 +test_fn_success: test +test_done: test +hook_start: afterAll +> cleanup value: 0 +hook_success: afterAll +hook_start: afterAll +> afterAll value: 0 +hook_success: afterAll +run_describe_finish: describe with beforeAll +run_describe_finish: ROOT_DESCRIBE_BLOCK +run_finish + +unhandledErrors: 0" +`; + +exports[`beforeAll can return a cleanup function, run in the correct order relative to afterAll hooks 1`] = ` +"start_describe_definition: describe +add_hook: afterAll +add_hook: beforeAll +add_hook: afterAll +add_hook: beforeAll +add_hook: afterAll +add_test: test +finish_describe_definition: describe +run_start +run_describe_start: ROOT_DESCRIBE_BLOCK +run_describe_start: describe +hook_start: beforeAll +> beforeAll 1 +hook_success: beforeAll +hook_start: beforeAll +> beforeAll 2 +hook_success: beforeAll +test_start: test +test_started: test +test_fn_start: test +> test +test_fn_success: test +test_done: test +hook_start: afterAll +> afterAll 1 (defined first) +hook_success: afterAll +hook_start: afterAll +> cleanup 1 +hook_success: afterAll +hook_start: afterAll +> afterAll 2 +hook_success: afterAll +hook_start: afterAll +> cleanup 2 +hook_success: afterAll +hook_start: afterAll +> afterAll 3 (defined last) +hook_success: afterAll +run_describe_finish: describe +run_describe_finish: ROOT_DESCRIBE_BLOCK +run_finish + +unhandledErrors: 0" +`; + exports[`beforeAll is exectued correctly 1`] = ` "start_describe_definition: describe 1 add_hook: beforeAll @@ -46,6 +145,52 @@ run_finish unhandledErrors: 0" `; +exports[`beforeEach can return a cleanup function, run in the correct order relative to afterEach hooks 1`] = ` +"start_describe_definition: describe +add_hook: afterEach +add_hook: beforeEach +add_hook: afterEach +add_hook: beforeEach +add_hook: afterEach +add_test: test +finish_describe_definition: describe +run_start +run_describe_start: ROOT_DESCRIBE_BLOCK +run_describe_start: describe +test_start: test +test_started: test +hook_start: beforeEach +> beforeEach 1 +hook_success: beforeEach +hook_start: beforeEach +> beforeEach 2 +hook_success: beforeEach +test_fn_start: test +> test +test_fn_success: test +hook_start: afterEach +> afterEach 1 (defined first) +hook_success: afterEach +hook_start: afterEach +> cleanup 1 +hook_success: afterEach +hook_start: afterEach +> afterEach 2 +hook_success: afterEach +hook_start: afterEach +> cleanup 2 +hook_success: afterEach +hook_start: afterEach +> afterEach 3 (defined last) +hook_success: afterEach +test_done: test +run_describe_finish: describe +run_describe_finish: ROOT_DESCRIBE_BLOCK +run_finish + +unhandledErrors: 0" +`; + exports[`beforeEach is executed before each test in current/child describe blocks 1`] = ` "start_describe_definition: describe add_hook: beforeEach @@ -136,6 +281,60 @@ run_finish unhandledErrors: 0" `; +exports[`cleanup functions run in the correct order with nested describes 1`] = ` +"start_describe_definition: outer describe +add_hook: afterAll +add_hook: beforeAll +add_hook: afterAll +start_describe_definition: inner describe +add_hook: afterAll +add_hook: beforeAll +add_hook: afterAll +add_test: test +finish_describe_definition: inner describe +finish_describe_definition: outer describe +run_start +run_describe_start: ROOT_DESCRIBE_BLOCK +run_describe_start: outer describe +hook_start: beforeAll +> outer beforeAll +hook_success: beforeAll +run_describe_start: inner describe +hook_start: beforeAll +> inner beforeAll +hook_success: beforeAll +test_start: test +test_started: test +test_fn_start: test +> test +test_fn_success: test +test_done: test +hook_start: afterAll +> inner afterAll 1 (defined first) +hook_success: afterAll +hook_start: afterAll +> inner cleanup +hook_success: afterAll +hook_start: afterAll +> inner afterAll 2 (defined last) +hook_success: afterAll +run_describe_finish: inner describe +hook_start: afterAll +> outer afterAll 1 (defined first) +hook_success: afterAll +hook_start: afterAll +> outer cleanup +hook_success: afterAll +hook_start: afterAll +> outer afterAll 2 (defined last) +hook_success: afterAll +run_describe_finish: outer describe +run_describe_finish: ROOT_DESCRIBE_BLOCK +run_finish + +unhandledErrors: 0" +`; + exports[`multiple before each hooks in one describe are executed in the right order 1`] = ` "start_describe_definition: describe 1 add_hook: beforeEach diff --git a/packages/jest-circus/src/__tests__/hooks.test.ts b/packages/jest-circus/src/__tests__/hooks.test.ts index 0db8d2f53d50..94bf759f3e7c 100644 --- a/packages/jest-circus/src/__tests__/hooks.test.ts +++ b/packages/jest-circus/src/__tests__/hooks.test.ts @@ -55,6 +55,45 @@ test('multiple before each hooks in one describe are executed in the right order expect(stdout).toMatchSnapshot(); }); +test('beforeEach can return a cleanup function, run in the correct order relative to afterEach hooks', () => { + const {stdout} = runTest(` + describe('describe', () => { + afterEach(() => { + console.log('> afterEach 1 (defined first)'); + }); + + beforeEach(() => { + console.log('> beforeEach 1'); + return () => { + console.log('> cleanup 1'); + }; + }); + + afterEach(() => { + console.log('> afterEach 2'); + }); + + beforeEach(() => { + console.log('> beforeEach 2'); + return () => { + console.log('> cleanup 2'); + }; + }); + + afterEach(() => { + console.log('> afterEach 3 (defined last)'); + }); + + test('test', () => { + console.log('> test'); + }); + }); + `); + + expect(stdout).toMatchSnapshot(); +}); + + test('beforeAll is exectued correctly', () => { const {stdout} = runTest(` describe('describe 1', () => { @@ -71,3 +110,132 @@ test('beforeAll is exectued correctly', () => { expect(stdout).toMatchSnapshot(); }); + +test('beforeAll can return a cleanup function, run in the correct order relative to afterAll hooks', () => { + const {stdout} = runTest(` + describe('describe', () => { + afterAll(() => { + console.log('> afterAll 1 (defined first)'); + }); + + beforeAll(() => { + console.log('> beforeAll 1'); + return () => { + console.log('> cleanup 1'); + }; + }); + + afterAll(() => { + console.log('> afterAll 2'); + }); + + beforeAll(() => { + console.log('> beforeAll 2'); + return () => { + console.log('> cleanup 2'); + }; + }); + + afterAll(() => { + console.log('> afterAll 3 (defined last)'); + }); + + test('test', () => { + console.log('> test'); + }); + }); + `); + + expect(stdout).toMatchSnapshot(); +}); + +test('cleanup functions run in the correct order with nested describes', () => { + const {stdout} = runTest(` + describe('outer describe', () => { + afterAll(() => { + console.log('> outer afterAll 1 (defined first)'); + }); + + beforeAll(() => { + console.log('> outer beforeAll'); + return () => { + console.log('> outer cleanup'); + }; + }); + + afterAll(() => { + console.log('> outer afterAll 2 (defined last)'); + }); + + describe('inner describe', () => { + afterAll(() => { + console.log('> inner afterAll 1 (defined first)'); + }); + + beforeAll(() => { + console.log('> inner beforeAll'); + return () => { + console.log('> inner cleanup'); + }; + }); + + afterAll(() => { + console.log('> inner afterAll 2 (defined last)'); + }); + + test('test', () => { + console.log('> test'); + }); + }); + }); + `); + + expect(stdout).toMatchSnapshot(); +}); + +test('async cleanup functions are properly awaited', () => { + const {stdout} = runTest(` + let value = 0; + describe('describe', () => { + beforeEach(() => { + value += 1; + console.log('> beforeEach value:', value); + return async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + value -= 1; + console.log('> cleanup value:', value); + }; + }); + + afterEach(() => { + console.log('> afterEach value:', value); + }); + + test('test', () => { + console.log('> test value:', value); + }); + }); + + describe('describe with beforeAll', () => { + beforeAll(() => { + value += 2; + console.log('> beforeAll value:', value); + return async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + value -= 2; + console.log('> cleanup value:', value); + }; + }); + + afterAll(() => { + console.log('> afterAll value:', value); + }); + + test('test', () => { + console.log('> test value:', value); + }); + }); + `); + + expect(stdout).toMatchSnapshot(); +}); \ No newline at end of file diff --git a/packages/jest-circus/src/eventHandler.ts b/packages/jest-circus/src/eventHandler.ts index da66a6bde13e..921587093e19 100644 --- a/packages/jest-circus/src/eventHandler.ts +++ b/packages/jest-circus/src/eventHandler.ts @@ -45,6 +45,7 @@ const eventHandler: Circus.EventHandler = (event, state) => { const describeBlock = makeDescribe(blockName, currentDescribeBlock, mode); currentDescribeBlock.children.push(describeBlock); + currentDescribeBlock.childrenWithHooks.push(describeBlock); state.currentDescribeBlock = describeBlock; break; } @@ -110,14 +111,16 @@ const eventHandler: Circus.EventHandler = (event, state) => { } const parent = currentDescribeBlock; - currentDescribeBlock.hooks.push({ + const hook: Circus.Hook = { asyncError, fn, parent, seenDone: false, timeout, type, - }); + } + currentDescribeBlock.hooks.push(hook); + currentDescribeBlock.childrenWithHooks.push(hook); break; } case 'add_test': { @@ -162,6 +165,7 @@ const eventHandler: Circus.EventHandler = (event, state) => { state.hasFocusedTests = true; } currentDescribeBlock.children.push(test); + currentDescribeBlock.childrenWithHooks.push(test); currentDescribeBlock.tests.push(test); break; } diff --git a/packages/jest-circus/src/run.ts b/packages/jest-circus/src/run.ts index e4e87b06bf7c..4d96008b0593 100644 --- a/packages/jest-circus/src/run.ts +++ b/packages/jest-circus/src/run.ts @@ -51,13 +51,30 @@ const _runTestsForDescribeBlock = async ( isRootBlock = false, ) => { await dispatch({describeBlock, name: 'run_describe_start'}); - const {beforeAll, afterAll} = getAllHooksForDescribe(describeBlock); + const {hooks} = getAllHooksForDescribe(describeBlock); + const afterAll: Array = []; const isSkipped = describeBlock.mode === 'skip'; if (!isSkipped) { - for (const hook of beforeAll) { - await _callCircusHook({describeBlock, hook}); + for (const hook of hooks) { + if (hook.type === 'beforeAll') { + const returnedValue = await _callCircusHook({describeBlock, hook}); + if (typeof returnedValue === 'function') { + // Add cleanup function to run after this beforeAll + afterAll.push({ + type: 'afterAll', + asyncError: hook.asyncError, + fn: returnedValue as Circus.TestFn, + parent: hook.parent, + seenDone: false, + timeout: hook.timeout, + }); + } + } else { + // Add afterAll hook + afterAll.push(hook); + } } } @@ -240,15 +257,34 @@ const _runTest = async ( await dispatch({name: 'test_started', test}); - const {afterEach, beforeEach} = getEachHooksForTest(test); + const {hooks} = getEachHooksForTest(test); - for (const hook of beforeEach) { - if (test.errors.length > 0) { - // If any of the before hooks failed already, we don't run any - // hooks after that. - break; + // Run hooks in order and collect cleanup functions + const afterEach: Array = []; + + for (const hook of hooks) { + if (hook.type === 'beforeEach') { + if (test.errors.length > 0) { + // If any of the before hooks failed already, we don't run any + // hooks after that. + break; + } + const returnedValue = await _callCircusHook({hook, test, testContext}); + if (typeof returnedValue === 'function') { + // Add cleanup function to run after this beforeEach + afterEach.push({ + type: 'afterEach', + asyncError: hook.asyncError, + fn: returnedValue as Circus.TestFn, + parent: hook.parent, + seenDone: false, + timeout: hook.timeout, + }); + } + } else { + // Add afterEach hook + afterEach.push(hook); } - await _callCircusHook({hook, test, testContext}); } await _callCircusTest(test, testContext); @@ -273,16 +309,19 @@ const _callCircusHook = async ({ describeBlock?: Circus.DescribeBlock; test?: Circus.TestEntry; testContext?: Circus.TestContext; -}): Promise => { +}): Promise => { await dispatch({hook, name: 'hook_start'}); const timeout = hook.timeout || getState().testTimeout; try { - await callAsyncCircusFn(hook, testContext, { + const result = await callAsyncCircusFn(hook, testContext, { isHook: true, timeout, }); await dispatch({describeBlock, hook, name: 'hook_success', test}); + if (typeof result === 'function') { + return result as Circus.TestFn; + } } catch (error) { await dispatch({describeBlock, error, hook, name: 'hook_failure', test}); } diff --git a/packages/jest-circus/src/utils.ts b/packages/jest-circus/src/utils.ts index 389c8beddf50..60d4c3593817 100644 --- a/packages/jest-circus/src/utils.ts +++ b/packages/jest-circus/src/utils.ts @@ -52,6 +52,7 @@ export const makeDescribe = ( type: 'describeBlock', // eslint-disable-next-line sort-keys children: [], hooks: [], + childrenWithHooks: [], mode: _mode, name: convertDescriptorToString(name), parent, @@ -105,27 +106,18 @@ const hasEnabledTest = (describeBlock: Circus.DescribeBlock): boolean => { }; type DescribeHooks = { - beforeAll: Array; - afterAll: Array; + hooks: Array; }; export const getAllHooksForDescribe = ( describe: Circus.DescribeBlock, ): DescribeHooks => { - const result: DescribeHooks = { - afterAll: [], - beforeAll: [], - }; + const result: DescribeHooks = {hooks: []}; if (hasEnabledTest(describe)) { for (const hook of describe.hooks) { - switch (hook.type) { - case 'beforeAll': - result.beforeAll.push(hook); - break; - case 'afterAll': - result.afterAll.push(hook); - break; + if (hook.type === 'beforeAll' || hook.type === 'afterAll') { + result.hooks.push(hook); } } } @@ -134,35 +126,44 @@ export const getAllHooksForDescribe = ( }; type TestHooks = { - beforeEach: Array; - afterEach: Array; + hooks: Array; }; export const getEachHooksForTest = (test: Circus.TestEntry): TestHooks => { - const result: TestHooks = {afterEach: [], beforeEach: []}; + const result: TestHooks = {hooks: []}; if (test.concurrent) { // *Each hooks are not run for concurrent tests return result; } + // First collect hooks from all blocks in parent-first order let block: Circus.DescribeBlock | undefined | null = test.parent; + let current: Circus.TestEntry | Circus.DescribeBlock = test; do { - const beforeEachForCurrentBlock = []; - for (const hook of block.hooks) { - switch (hook.type) { - case 'beforeEach': - beforeEachForCurrentBlock.push(hook); - break; - case 'afterEach': - result.afterEach.push(hook); - break; + const currentIndexInBlock = block.childrenWithHooks.indexOf(current); + + // Add hooks in their original order within each block + const hooksBeforeCurrent: Array = []; + + for (let i = 0; i < currentIndexInBlock; i++) { + const child = block?.childrenWithHooks[i]; + if (child.type === 'beforeEach' || child.type === 'afterEach') { + hooksBeforeCurrent.push(child); + } + } + + for (let i = currentIndexInBlock + 1; i < block?.children.length; i++) { + const child = block.childrenWithHooks[i]; + if (child.type === 'beforeEach' || child?.type === 'afterEach') { + // Parent block hooks go at the start + result.hooks.push(child); } } - // 'beforeEach' hooks are executed from top to bottom, the opposite of the - // way we traversed it. - result.beforeEach.unshift(...beforeEachForCurrentBlock); - } while ((block = block.parent)); + + result.hooks.unshift(...hooksBeforeCurrent); + } while (((current = block), (block = block.parent))); + return result; }; @@ -203,7 +204,7 @@ export const callAsyncCircusFn = ( const {fn, asyncError} = testOrHook; const doneCallback = takesDoneCallback(fn); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { timeoutID = setTimeout( () => reject(_makeTimeoutMessage(timeout, isHook, doneCallback)), timeout, @@ -260,7 +261,7 @@ export const callAsyncCircusFn = ( throw errorAsErrorObject; } - return reason ? reject(errorAsErrorObject) : resolve(); + return reason ? reject(errorAsErrorObject) : resolve(void undefined); }); }; @@ -282,7 +283,10 @@ export const callAsyncCircusFn = ( } if (isPromise(returnedValue)) { - returnedValue.then(() => resolve(), reject); + returnedValue.then( + result => (isHook ? resolve(result) : resolve(void undefined)), + reject, + ); return; } @@ -298,9 +302,13 @@ export const callAsyncCircusFn = ( return; } + if (isHook) { + resolve(returnedValue); + } + // Otherwise this test is synchronous, and if it didn't throw it means // it passed. - resolve(); + resolve(void undefined); }).finally(() => { completed = true; // If timeout is not cleared/unrefed the node process won't exit until diff --git a/packages/jest-types/src/Circus.ts b/packages/jest-types/src/Circus.ts index dbd87e6f8780..24a35791ac58 100644 --- a/packages/jest-types/src/Circus.ts +++ b/packages/jest-types/src/Circus.ts @@ -256,6 +256,11 @@ export type DescribeBlock = { type: 'describeBlock'; children: Array; hooks: Array; + /** + * An array with both children and hooks in the original order. + * This is used to determine the relative order of beforeEach and afterEach hooks. + */ + childrenWithHooks: Array; mode: BlockMode; name: BlockName; parent?: DescribeBlock;