Skip to content

Commit 6b46b7d

Browse files
committed
feat(@schematics/angular): migrate fake async to Vitest fake timers
1 parent 3663f80 commit 6b46b7d

17 files changed

+740
-47
lines changed

packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
*/
1515

1616
import ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
17+
import { transformFakeAsyncFlush } from './transformers/fake-async-flush';
18+
import { transformFakeAsyncFlushMicrotasks } from './transformers/fake-async-flush-microtasks';
19+
import { transformFakeAsyncTest } from './transformers/fake-async-test';
20+
import { transformFakeAsyncTick } from './transformers/fake-async-tick';
1721
import {
1822
transformDoneCallback,
1923
transformFocusedAndSkippedTests,
@@ -46,7 +50,11 @@ import {
4650
transformSpyReset,
4751
} from './transformers/jasmine-spy';
4852
import { transformJasmineTypes } from './transformers/jasmine-type';
49-
import { addVitestValueImport, getVitestAutoImports } from './utils/ast-helpers';
53+
import {
54+
addVitestValueImport,
55+
getVitestAutoImports,
56+
removeImportSpecifiers,
57+
} from './utils/ast-helpers';
5058
import { RefactorContext } from './utils/refactor-context';
5159
import { RefactorReporter } from './utils/refactor-reporter';
5260

@@ -121,6 +129,10 @@ const callExpressionTransformers = [
121129
transformSpyCallInspection,
122130
transformtoHaveBeenCalledBefore,
123131
transformToHaveClass,
132+
transformFakeAsyncTest,
133+
transformFakeAsyncTick,
134+
transformFakeAsyncFlush,
135+
transformFakeAsyncFlushMicrotasks,
124136

125137
// **Stage 3: Global Functions & Cleanup**
126138
// These handle global Jasmine functions and catch-alls for unsupported APIs.
@@ -179,6 +191,7 @@ export function transformJasmineToVitest(
179191

180192
const pendingVitestValueImports = new Set<string>();
181193
const pendingVitestTypeImports = new Set<string>();
194+
const pendingImportSpecifierRemovals = new Map<string, Set<string>>();
182195

183196
const transformer: ts.TransformerFactory<ts.SourceFile> = (context) => {
184197
const refactorCtx: RefactorContext = {
@@ -187,6 +200,7 @@ export function transformJasmineToVitest(
187200
tsContext: context,
188201
pendingVitestValueImports,
189202
pendingVitestTypeImports,
203+
pendingImportSpecifierRemovals,
190204
};
191205

192206
const visitor: ts.Visitor = (node) => {
@@ -240,16 +254,25 @@ export function transformJasmineToVitest(
240254

241255
const hasPendingValueImports = pendingVitestValueImports.size > 0;
242256
const hasPendingTypeImports = pendingVitestTypeImports.size > 0;
257+
const hasPendingImportSpecifierRemovals = pendingImportSpecifierRemovals.size > 0;
243258

244259
if (
245260
transformedSourceFile === sourceFile &&
246261
!reporter.hasTodos &&
247262
!hasPendingValueImports &&
248-
!hasPendingTypeImports
263+
!hasPendingTypeImports &&
264+
!hasPendingImportSpecifierRemovals
249265
) {
250266
return content;
251267
}
252268

269+
if (hasPendingImportSpecifierRemovals) {
270+
transformedSourceFile = removeImportSpecifiers(
271+
transformedSourceFile,
272+
pendingImportSpecifierRemovals,
273+
);
274+
}
275+
253276
if (hasPendingTypeImports || (options.addImports && hasPendingValueImports)) {
254277
const vitestImport = getVitestAutoImports(
255278
options.addImports ? pendingVitestValueImports : new Set(),

packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer_add-imports_spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,26 @@ describe('Jasmine to Vitest Transformer - addImports option', () => {
116116
`;
117117
await expectTransformation(input, expected, true);
118118
});
119+
120+
it('should add imports for `onTestFinished` and `vi` when addImports is true', async () => {
121+
const input = `
122+
import { fakeAsync } from '@angular/core/testing';
123+
124+
it('works', fakeAsync(() => {
125+
expect(1).toBe(1);
126+
}));
127+
`;
128+
const expected = `
129+
import { expect, it, onTestFinished, vi } from 'vitest';
130+
131+
it('works', async () => {
132+
vi.useFakeTimers();
133+
onTestFinished(() => {
134+
vi.useRealTimers();
135+
});
136+
expect(1).toBe(1);
137+
});
138+
`;
139+
await expectTransformation(input, expected, true);
140+
});
119141
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
10+
import { isNamedImportFrom } from '../utils/ast-helpers';
11+
import { ANGULAR_CORE_TESTING } from '../utils/constants';
12+
import { RefactorContext } from '../utils/refactor-context';
13+
import { addImportSpecifierRemoval, createViCallExpression } from '../utils/refactor-helpers';
14+
15+
export function transformFakeAsyncFlushMicrotasks(node: ts.Node, ctx: RefactorContext): ts.Node {
16+
if (
17+
!(
18+
ts.isCallExpression(node) &&
19+
ts.isIdentifier(node.expression) &&
20+
node.expression.text === 'flushMicrotasks' &&
21+
isNamedImportFrom(ctx.sourceFile, 'flushMicrotasks', ANGULAR_CORE_TESTING)
22+
)
23+
) {
24+
return node;
25+
}
26+
27+
ctx.reporter.reportTransformation(
28+
ctx.sourceFile,
29+
node,
30+
`Transformed \`flushMicrotasks\` to \`await vi.advanceTimersByTimeAsync(0)\`.`,
31+
);
32+
33+
addImportSpecifierRemoval(ctx, 'flushMicrotasks', ANGULAR_CORE_TESTING);
34+
35+
return ts.factory.createAwaitExpression(
36+
createViCallExpression(ctx, 'advanceTimersByTimeAsync', [ts.factory.createNumericLiteral(0)]),
37+
);
38+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { expectTransformation } from '../test-helpers';
10+
11+
describe('transformFakeAsyncFlushMicrotasks', () => {
12+
const testCases = [
13+
{
14+
description: 'should replace `flushMicrotasks` with `await vi.advanceTimersByTimeAsync(0)`',
15+
input: `
16+
import { flushMicrotasks } from '@angular/core/testing';
17+
18+
flushMicrotasks();
19+
`,
20+
expected: `await vi.advanceTimersByTimeAsync(0);`,
21+
},
22+
{
23+
description:
24+
'should not replace `flushMicrotasks` if not imported from `@angular/core/testing`',
25+
input: `
26+
import { flushMicrotasks } from './my-flush-microtasks';
27+
28+
flushMicrotasks();
29+
`,
30+
expected: `
31+
import { flushMicrotasks } from './my-flush-microtasks';
32+
33+
flushMicrotasks();
34+
`,
35+
},
36+
];
37+
38+
testCases.forEach(({ description, input, expected }) => {
39+
it(description, async () => {
40+
await expectTransformation(input, expected);
41+
});
42+
});
43+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
10+
import { isNamedImportFrom } from '../utils/ast-helpers';
11+
import { addTodoComment } from '../utils/comment-helpers';
12+
import { ANGULAR_CORE_TESTING } from '../utils/constants';
13+
import { RefactorContext } from '../utils/refactor-context';
14+
import { addImportSpecifierRemoval, createViCallExpression } from '../utils/refactor-helpers';
15+
16+
export function transformFakeAsyncFlush(node: ts.Node, ctx: RefactorContext): ts.Node {
17+
if (
18+
!(
19+
ts.isCallExpression(node) &&
20+
ts.isIdentifier(node.expression) &&
21+
node.expression.text === 'flush' &&
22+
isNamedImportFrom(ctx.sourceFile, 'flush', ANGULAR_CORE_TESTING)
23+
)
24+
) {
25+
return node;
26+
}
27+
28+
ctx.reporter.reportTransformation(
29+
ctx.sourceFile,
30+
node,
31+
`Transformed \`flush\` to \`await vi.runAllTimersAsync()\`.`,
32+
);
33+
34+
addImportSpecifierRemoval(ctx, 'flush', ANGULAR_CORE_TESTING);
35+
36+
if (node.arguments.length > 0) {
37+
ctx.reporter.recordTodo('flush-max-turns', ctx.sourceFile, node);
38+
addTodoComment(node, 'flush-max-turns');
39+
}
40+
41+
const awaitRunAllTimersAsync = ts.factory.createAwaitExpression(
42+
createViCallExpression(ctx, 'runAllTimersAsync'),
43+
);
44+
45+
if (ts.isExpressionStatement(node.parent)) {
46+
return awaitRunAllTimersAsync;
47+
} else {
48+
// If `flush` is not used as its own statement, then the return value is probably used.
49+
// Therefore, we replace it with nullish coalescing that returns 0:
50+
// > await vi.runAllTimersAsync() ?? 0;
51+
ctx.reporter.recordTodo('flush-return-value', ctx.sourceFile, node);
52+
addTodoComment(node, 'flush-return-value');
53+
54+
return ts.factory.createBinaryExpression(
55+
awaitRunAllTimersAsync,
56+
ts.SyntaxKind.QuestionQuestionToken,
57+
ts.factory.createNumericLiteral(0),
58+
);
59+
}
60+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { expectTransformation } from '../test-helpers';
10+
11+
describe('transformFakeAsyncFlush', () => {
12+
const testCases = [
13+
{
14+
description: 'should replace `flush` with `await vi.runAllTimersAsync()`',
15+
input: `
16+
import { flush } from '@angular/core/testing';
17+
18+
flush();
19+
`,
20+
expected: `await vi.runAllTimersAsync();`,
21+
},
22+
{
23+
description: 'should add TODO comment when flush is called with maxTurns',
24+
input: `
25+
import { flush } from '@angular/core/testing';
26+
27+
flush(42);
28+
`,
29+
expected: `
30+
// TODO: vitest-migration: flush(maxTurns) was called but maxTurns parameter is not migrated. Please migrate manually.
31+
await vi.runAllTimersAsync();
32+
`,
33+
},
34+
{
35+
description: 'should add TODO comment when flush return value is used',
36+
input: `
37+
import { flush } from '@angular/core/testing';
38+
39+
const turns = flush();
40+
`,
41+
expected: `
42+
// TODO: vitest-migration: flush() return value is not migrated. Please migrate manually.
43+
const turns = await vi.runAllTimersAsync() ?? 0;
44+
`,
45+
},
46+
{
47+
description: 'should add TODO comment when flush return value is used in a return statement',
48+
input: `
49+
import { flush } from '@angular/core/testing';
50+
51+
async function myFlushWrapper() {
52+
return flush();
53+
}
54+
`,
55+
expected: `
56+
async function myFlushWrapper() {
57+
// TODO: vitest-migration: flush() return value is not migrated. Please migrate manually.
58+
return await vi.runAllTimersAsync() ?? 0;
59+
}
60+
`,
61+
},
62+
{
63+
description: 'should not replace `flush` if not imported from `@angular/core/testing`',
64+
input: `
65+
import { flush } from './my-flush';
66+
67+
flush();
68+
`,
69+
expected: `
70+
import { flush } from './my-flush';
71+
72+
flush();
73+
`,
74+
},
75+
{
76+
description: 'should keep other imported symbols from `@angular/core/testing`',
77+
input: `
78+
import { TestBed, flush } from '@angular/core/testing';
79+
80+
flush();
81+
`,
82+
expected: `
83+
import { TestBed } from '@angular/core/testing';
84+
85+
await vi.runAllTimersAsync();
86+
`,
87+
},
88+
{
89+
description: 'should keep imported types from `@angular/core/testing`',
90+
input: `
91+
import { flush, type ComponentFixture } from '@angular/core/testing';
92+
93+
flush();
94+
`,
95+
expected: `
96+
import { type ComponentFixture } from '@angular/core/testing';
97+
98+
await vi.runAllTimersAsync();
99+
`,
100+
},
101+
];
102+
103+
testCases.forEach(({ description, input, expected }) => {
104+
it(description, async () => {
105+
await expectTransformation(input, expected);
106+
});
107+
});
108+
});

0 commit comments

Comments
 (0)