diff --git a/packages/schematics/angular/refactor/jasmine-vitest/transformers/fake-async-test.ts b/packages/schematics/angular/refactor/jasmine-vitest/transformers/fake-async-test.ts index 723d3a66c4b7..0ba618005c6e 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/transformers/fake-async-test.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/fake-async-test.ts @@ -107,13 +107,30 @@ function _transformFakeAsyncCall( `Transformed \`fakeAsync\` to \`vi.useFakeTimers\`.`, ); + let statements = callbackBody.statements; + + // Append `vi.runOnlyPendingTimersAsync()` as the last statement of `beforeEach` to mimic flush behavior. + if ( + ts.isCallExpression(node.parent) && + ts.isIdentifier(node.parent.expression) && + node.parent.expression.text === 'beforeEach' && + !_isFakeAsyncFlushDisabled(node) + ) { + statements = ts.factory.createNodeArray([ + ...statements, + ts.factory.createExpressionStatement( + ts.factory.createAwaitExpression(createViCallExpression(ctx, 'runOnlyPendingTimersAsync')), + ), + ]); + } + return ts.factory.createArrowFunction( [ts.factory.createModifier(ts.SyntaxKind.AsyncKeyword)], fakeAsyncCallback.typeParameters, fakeAsyncCallback.parameters, undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.factory.createBlock(callbackBody.statements), + ts.factory.createBlock(statements), ); } @@ -177,6 +194,25 @@ function _createFakeTimersHookStatements(ctx: RefactorContext): ts.Statement[] { ]; } +/** + * Detects if the `flush` option is set to false in the `fakeAsync` call expression. + * e.g. `fakeAsync(() => { ... }, { flush: false })` + */ +function _isFakeAsyncFlushDisabled(fakeAsyncCallExpression: ts.CallExpression): boolean { + const options = fakeAsyncCallExpression.arguments[1]; + + return ( + options && + ts.isObjectLiteralExpression(options) && + options.properties.some( + (property) => + ts.isPropertyAssignment(property) && + property.name.getText() === 'flush' && + property.initializer.getText() === 'false', + ) + ); +} + interface CurrentOutermostDescribeContext { isUsingFakeAsync: boolean; } diff --git a/packages/schematics/angular/refactor/jasmine-vitest/transformers/fake-async-test_spec.ts b/packages/schematics/angular/refactor/jasmine-vitest/transformers/fake-async-test_spec.ts index 9ac3e8f5f9bc..9a0579d2d146 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/transformers/fake-async-test_spec.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/fake-async-test_spec.ts @@ -154,28 +154,79 @@ describe('transformFakeAsyncTest', () => { }, { description: - 'should transform fakeAsync test to `vi.useFakeTimers()` in `beforeEach`, `afterEach`, `beforeAll`, `afterAll`', + 'should transform fakeAsync test to `vi.useFakeTimers()` in `beforeEach` and preserve flush behavior', input: ` import { fakeAsync } from '@angular/core/testing'; describe('My fakeAsync suite', () => { - beforeAll(fakeAsync(() => { - console.log('beforeAll'); + + let count = 0; + beforeEach(fakeAsync(() => { + setTimeout(() => ++count, 100); })); - afterAll(fakeAsync(() => { - console.log('afterAll'); + it('works', fakeAsync(() => { + expect(count).toBe(1); })); + }); + `, + expected: ` + describe('My fakeAsync suite', () => { + beforeEach(() => { + vi.useFakeTimers({ advanceTimeDelta: 1, shouldAdvanceTime: true }); + }); + afterEach(() => { + vi.useRealTimers(); + }); - beforeEach(fakeAsync(() => { - console.log('beforeEach'); - })); + let count = 0; + beforeEach(async () => { + setTimeout(() => ++count, 100); + await vi.runOnlyPendingTimersAsync(); + }); + + it('works', async () => { + expect(count).toBe(1); + }); + }); + `, + }, + { + description: 'should transform fakeAsync test to `vi.useFakeTimers()` in `afterEach`', + input: ` + import { fakeAsync } from '@angular/core/testing'; + describe('My fakeAsync suite', () => { afterEach(fakeAsync(() => { console.log('afterEach'); })); }); `, + expected: ` + describe('My fakeAsync suite', () => { + beforeEach(() => { + vi.useFakeTimers({ advanceTimeDelta: 1, shouldAdvanceTime: true }); + }); + afterEach(() => { + vi.useRealTimers(); + }); + afterEach(async () => { + console.log('afterEach'); + }); + }); + `, + }, + { + description: 'should transform fakeAsync test to `vi.useFakeTimers()` in `beforeAll`', + input: ` + import { fakeAsync } from '@angular/core/testing'; + + describe('My fakeAsync suite', () => { + beforeAll(fakeAsync(() => { + console.log('beforeAll'); + })); + }); + `, expected: ` describe('My fakeAsync suite', () => { beforeEach(() => { @@ -187,17 +238,30 @@ describe('transformFakeAsyncTest', () => { beforeAll(async () => { console.log('beforeAll'); }); + }); + `, + }, + { + description: 'should transform fakeAsync test to `vi.useFakeTimers()` in `afterAll`', + input: ` + import { fakeAsync } from '@angular/core/testing'; - afterAll(async () => { + describe('My fakeAsync suite', () => { + afterAll(fakeAsync(() => { console.log('afterAll'); + })); + }); + `, + expected: ` + describe('My fakeAsync suite', () => { + beforeEach(() => { + vi.useFakeTimers({ advanceTimeDelta: 1, shouldAdvanceTime: true }); }); - - beforeEach(async () => { - console.log('beforeEach'); + afterEach(() => { + vi.useRealTimers(); }); - - afterEach(async () => { - console.log('afterEach'); + afterAll(async () => { + console.log('afterAll'); }); }); `, @@ -240,6 +304,65 @@ describe('transformFakeAsyncTest', () => { }); `, }, + { + description: 'should not append `vi.runOnlyPendingTimersAsync()` in `test` or `afterEach`', + input: ` + import { fakeAsync } from '@angular/core/testing'; + + describe('My fakeAsync suite', () => { + afterEach(fakeAsync(() => { + console.log('afterEach'); + })); + + it('works', fakeAsync(() => { + expect(1).toBe(1); + })); + }); + `, + expected: ` + describe('My fakeAsync suite', () => { + beforeEach(() => { + vi.useFakeTimers({ advanceTimeDelta: 1, shouldAdvanceTime: true }); + }); + afterEach(() => { + vi.useRealTimers(); + }); + afterEach(async () => { + console.log('afterEach'); + }); + + it('works', async () => { + expect(1).toBe(1); + }); + }); + `, + }, + { + description: + 'should not append `vi.runOnlyPendingTimersAsync()` if `flush` option is set to false', + input: ` + import { fakeAsync } from '@angular/core/testing'; + + describe('My fakeAsync suite', () => { + beforeEach(fakeAsync(() => { + console.log('beforeEach'); + }, {flush: false})); + }); + `, + expected: ` + describe('My fakeAsync suite', () => { + beforeEach(() => { + vi.useFakeTimers({ advanceTimeDelta: 1, shouldAdvanceTime: true }); + }); + afterEach(() => { + vi.useRealTimers(); + }); + beforeEach(async () => { + console.log('beforeEach'); + }); + }); + `, + }, ]; testCases.forEach(({ description, input, expected }) => {