Skip to content

Commit d227e69

Browse files
yjaaidiclydin
authored andcommitted
feat(@schematics/angular): migrate fake async to Vitest fake timers
1 parent 7fb59ea commit d227e69

21 files changed

Lines changed: 1165 additions & 46 deletions

packages/schematics/angular/refactor/jasmine-vitest/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export default function (options: Schema): Rule {
122122
const newContent = transformJasmineToVitest(file, content, reporter, {
123123
addImports: !!options.addImports,
124124
browserMode: !!options.browerMode,
125+
fakeAsync: !!options.fakeAsync,
125126
});
126127

127128
if (content !== newContent) {

packages/schematics/angular/refactor/jasmine-vitest/schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
"description": "Whether the tests are intended to run in browser mode. If true, the `toHaveClass` assertions are left as is because Vitest browser mode has such an assertion. Otherwise they're migrated to an equivalent assertion.",
3737
"default": false
3838
},
39+
"fakeAsync": {
40+
"type": "boolean",
41+
"description": "Whether to transform `fakeAsync` tests to Vitest fake timers.",
42+
"default": false
43+
},
3944
"report": {
4045
"type": "boolean",
4146
"description": "Whether to generate a summary report file (jasmine-vitest-<date>.md) in the project root.",

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

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { RefactorReporter } from './utils/refactor-reporter';
1414
async function expectTransformation(
1515
input: string,
1616
expected: string,
17-
options: { addImports: boolean; browserMode: boolean } = {
17+
options: { addImports: boolean; browserMode: boolean; fakeAsync?: boolean } = {
1818
addImports: false,
1919
browserMode: false,
2020
},
@@ -534,4 +534,66 @@ describe('Jasmine to Vitest Transformer - Integration Tests', () => {
534534

535535
await expectTransformation(jasmineCode, vitestCode);
536536
});
537+
538+
it('should not transform `fakeAsync`', async () => {
539+
const jasmineCode = `
540+
import { fakeAsync, flush, flushMicrotasks, tick } from '@angular/core/testing';
541+
542+
describe('My fakeAsync suite', () => {
543+
it('works', fakeAsync(() => {
544+
flush();
545+
flushMicrotasks();
546+
tick(100);
547+
}));
548+
});
549+
`;
550+
const vitestCode = `
551+
import { fakeAsync, flush, flushMicrotasks, tick } from '@angular/core/testing';
552+
553+
describe('My fakeAsync suite', () => {
554+
it('works', fakeAsync(() => {
555+
flush();
556+
flushMicrotasks();
557+
tick(100);
558+
}));
559+
});
560+
`;
561+
562+
await expectTransformation(jasmineCode, vitestCode);
563+
});
564+
565+
it('should transform `fakeAsync` if `fakeAsync` option is true', async () => {
566+
const jasmineCode = `
567+
import { fakeAsync, flush, flushMicrotasks, tick } from '@angular/core/testing';
568+
569+
describe('My fakeAsync suite', () => {
570+
it('works', fakeAsync(() => {
571+
flush();
572+
flushMicrotasks();
573+
tick(100);
574+
}));
575+
});
576+
`;
577+
const vitestCode = `
578+
describe('My fakeAsync suite', () => {
579+
beforeAll(() => {
580+
vi.useFakeTimers({ advanceTimeDelta: 1, shouldAdvanceTime: true });
581+
});
582+
afterAll(() => {
583+
vi.useRealTimers();
584+
});
585+
it('works', async () => {
586+
await vi.runAllTimersAsync();
587+
await vi.advanceTimersByTimeAsync(0);
588+
await vi.advanceTimersByTimeAsync(100);
589+
});
590+
});
591+
`;
592+
593+
await expectTransformation(jasmineCode, vitestCode, {
594+
addImports: false,
595+
browserMode: false,
596+
fakeAsync: true,
597+
});
598+
});
537599
});

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

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

1616
import ts from '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,
@@ -48,7 +52,11 @@ import {
4852
transformSpyReset,
4953
} from './transformers/jasmine-spy';
5054
import { transformJasmineTypes } from './transformers/jasmine-type';
51-
import { addVitestValueImport, getVitestAutoImports } from './utils/ast-helpers';
55+
import {
56+
addVitestValueImport,
57+
getVitestAutoImports,
58+
removeImportSpecifiers,
59+
} from './utils/ast-helpers';
5260
import { RefactorContext } from './utils/refactor-context';
5361
import { RefactorReporter } from './utils/refactor-reporter';
5462

@@ -129,6 +137,10 @@ const callExpressionTransformers = [
129137
transformToHaveBeenCalledBefore,
130138
transformToHaveClass,
131139
transformToBeNullish,
140+
transformFakeAsyncTest,
141+
transformFakeAsyncTick,
142+
transformFakeAsyncFlush,
143+
transformFakeAsyncFlushMicrotasks,
132144

133145
// **Stage 3: Global Functions & Cleanup**
134146
// These handle global Jasmine functions and catch-alls for unsupported APIs.
@@ -173,8 +185,10 @@ export function transformJasmineToVitest(
173185
filePath: string,
174186
content: string,
175187
reporter: RefactorReporter,
176-
options: { addImports: boolean; browserMode: boolean },
188+
options: { addImports: boolean; browserMode: boolean; fakeAsync?: boolean },
177189
): string {
190+
options.fakeAsync ??= false;
191+
178192
const contentWithPlaceholders = preserveBlankLines(content);
179193

180194
const sourceFile = ts.createSourceFile(
@@ -187,6 +201,7 @@ export function transformJasmineToVitest(
187201

188202
const pendingVitestValueImports = new Set<string>();
189203
const pendingVitestTypeImports = new Set<string>();
204+
const pendingImportSpecifierRemovals = new Map<string, Set<string>>();
190205

191206
const transformer: ts.TransformerFactory<ts.SourceFile> = (context) => {
192207
const refactorCtx: RefactorContext = {
@@ -195,6 +210,7 @@ export function transformJasmineToVitest(
195210
tsContext: context,
196211
pendingVitestValueImports,
197212
pendingVitestTypeImports,
213+
pendingImportSpecifierRemovals,
198214
};
199215

200216
const visitor: ts.Visitor = (node) => {
@@ -211,7 +227,18 @@ export function transformJasmineToVitest(
211227
}
212228

213229
for (const transformer of callExpressionTransformers) {
214-
if (!(options.browserMode && transformer === transformToHaveClass)) {
230+
if (
231+
!(
232+
(options.browserMode && transformer === transformToHaveClass) ||
233+
(options.fakeAsync === false &&
234+
[
235+
transformFakeAsyncFlush,
236+
transformFakeAsyncFlushMicrotasks,
237+
transformFakeAsyncTick,
238+
transformFakeAsyncTest,
239+
].includes(transformer))
240+
)
241+
) {
215242
transformedNode = transformer(transformedNode, refactorCtx);
216243
}
217244
}
@@ -249,16 +276,25 @@ export function transformJasmineToVitest(
249276

250277
const hasPendingValueImports = pendingVitestValueImports.size > 0;
251278
const hasPendingTypeImports = pendingVitestTypeImports.size > 0;
279+
const hasPendingImportSpecifierRemovals = pendingImportSpecifierRemovals.size > 0;
252280

253281
if (
254282
transformedSourceFile === sourceFile &&
255283
!reporter.hasTodos &&
256284
!hasPendingValueImports &&
257-
!hasPendingTypeImports
285+
!hasPendingTypeImports &&
286+
!hasPendingImportSpecifierRemovals
258287
) {
259288
return content;
260289
}
261290

291+
if (hasPendingImportSpecifierRemovals) {
292+
transformedSourceFile = removeImportSpecifiers(
293+
transformedSourceFile,
294+
pendingImportSpecifierRemovals,
295+
);
296+
}
297+
262298
if (hasPendingTypeImports || (options.addImports && hasPendingValueImports)) {
263299
const vitestImport = getVitestAutoImports(
264300
options.addImports ? pendingVitestValueImports : new Set(),

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,32 @@ describe('Jasmine to Vitest Transformer - addImports option', () => {
150150
true,
151151
);
152152
});
153+
154+
it('should add imports for `vi` when addImports is true', async () => {
155+
const input = `
156+
import { fakeAsync } from '@angular/core/testing';
157+
158+
describe('My fakeAsync suite', () => {
159+
it('works', fakeAsync(() => {
160+
expect(1).toBe(1);
161+
}));
162+
});
163+
`;
164+
const expected = `
165+
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
166+
167+
describe('My fakeAsync suite', () => {
168+
beforeAll(() => {
169+
vi.useFakeTimers({ advanceTimeDelta: 1, shouldAdvanceTime: true });
170+
});
171+
afterAll(() => {
172+
vi.useRealTimers();
173+
});
174+
it('works', async () => {
175+
expect(1).toBe(1);
176+
});
177+
});
178+
`;
179+
await expectTransformation(input, expected, true);
180+
});
153181
});

packages/schematics/angular/refactor/jasmine-vitest/test-helpers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export async function expectTransformation(
3333
const transformed = transformJasmineToVitest('spec.ts', input, reporter, {
3434
addImports,
3535
browserMode: false,
36+
fakeAsync: true,
3637
});
3738
const formattedTransformed = await format(transformed, { parser: 'typescript' });
3839
const formattedExpected = await format(expected, { parser: 'typescript' });
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 '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 '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+
}

0 commit comments

Comments
 (0)