From 8beba0c2baf10fb3af7f812eac2c64cabdfc7bfb Mon Sep 17 00:00:00 2001 From: Younes Jaaidi Date: Tue, 17 Mar 2026 14:21:39 +0100 Subject: [PATCH 1/2] feat(@schematics/angular): add `refactor-fake-async` migration --- packages/schematics/angular/collection.json | 6 + .../angular/refactor/fake-async/index.ts | 43 +++++++ .../angular/refactor/fake-async/index_spec.ts | 112 ++++++++++++++++++ .../refactor/fake-async/refactor-context.ts | 17 +++ .../angular/refactor/fake-async/schema.json | 15 +++ .../fake-async/transform-fake-async.ts | 79 ++++++++++++ .../refactor/fake-async/transformers/flush.ts | 22 ++++ .../transformers/setup-fake-timers.ts | 73 ++++++++++++ .../refactor/fake-async/transformers/tick.ts | 28 +++++ .../angular/refactor/jasmine-vitest/index.ts | 26 +--- .../jasmine-vitest/utils/ast-helpers.ts | 12 +- .../angular/utility/find-test-files.ts | 26 ++++ 12 files changed, 427 insertions(+), 32 deletions(-) create mode 100644 packages/schematics/angular/refactor/fake-async/index.ts create mode 100644 packages/schematics/angular/refactor/fake-async/index_spec.ts create mode 100644 packages/schematics/angular/refactor/fake-async/refactor-context.ts create mode 100644 packages/schematics/angular/refactor/fake-async/schema.json create mode 100644 packages/schematics/angular/refactor/fake-async/transform-fake-async.ts create mode 100644 packages/schematics/angular/refactor/fake-async/transformers/flush.ts create mode 100644 packages/schematics/angular/refactor/fake-async/transformers/setup-fake-timers.ts create mode 100644 packages/schematics/angular/refactor/fake-async/transformers/tick.ts create mode 100644 packages/schematics/angular/utility/find-test-files.ts diff --git a/packages/schematics/angular/collection.json b/packages/schematics/angular/collection.json index 29b361ccafbb..89724322d59b 100755 --- a/packages/schematics/angular/collection.json +++ b/packages/schematics/angular/collection.json @@ -138,6 +138,12 @@ "private": true, "description": "[INTERNAL] Adds tailwind to a project. Intended for use for ng new/add." }, + "refactor-fake-async": { + "factory": "./refactor/fake-async", + "schema": "./refactor/fake-async/schema.json", + "description": "[EXPERIMENTAL] Refactors Angular fakeAsync to Vitest fake timers.", + "hidden": true + }, "refactor-jasmine-vitest": { "factory": "./refactor/jasmine-vitest", "schema": "./refactor/jasmine-vitest/schema.json", diff --git a/packages/schematics/angular/refactor/fake-async/index.ts b/packages/schematics/angular/refactor/fake-async/index.ts new file mode 100644 index 000000000000..d5b84ce2410a --- /dev/null +++ b/packages/schematics/angular/refactor/fake-async/index.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { Rule, SchematicContext, SchematicsException, Tree } from '@angular-devkit/schematics'; +import { findTestFiles } from '../../utility/find-test-files'; +import { getWorkspace } from '../../utility/workspace'; +import { Schema as FakeAsyncOptions } from './schema'; +import { transformFakeAsync } from './transform-fake-async'; + +export default function (options: FakeAsyncOptions): Rule { + return async (tree: Tree, _context: SchematicContext) => { + const workspace = await getWorkspace(tree); + const project = workspace.projects.get(options.project); + + if (!project) { + throw new SchematicsException(`Project "${options.project}" does not exist.`); + } + + const projectRoot = project.root; + const fileSuffix = '.spec.ts'; + const files = findTestFiles(tree.getDir(projectRoot), fileSuffix); + + if (files.length === 0) { + throw new SchematicsException( + `No files ending with '${fileSuffix}' found in ${projectRoot}.`, + ); + } + + for (const file of files) { + const content = tree.readText(file); + const newContent = transformFakeAsync(file, content); + + if (content !== newContent) { + tree.overwrite(file, newContent); + } + } + }; +} diff --git a/packages/schematics/angular/refactor/fake-async/index_spec.ts b/packages/schematics/angular/refactor/fake-async/index_spec.ts new file mode 100644 index 000000000000..7fc1676346c6 --- /dev/null +++ b/packages/schematics/angular/refactor/fake-async/index_spec.ts @@ -0,0 +1,112 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { format } from 'prettier'; +import { Schema as ApplicationOptions } from '../../application/schema'; +import { Schema as WorkspaceOptions } from '../../workspace/schema'; + +describe('Angular `fakeAsync` to Vitest Fake Timers Schematic', () => { + it("should replace `fakeAsync` with an async test using Vitest's fake timers", async () => { + const { transformContent } = await setUp(); + + const newContent = await transformContent(` +it("should work", fakeAsync(() => { + expect(1).toBe(1); +})); +`); + + expect(newContent).toBe(`\ +import { onTestFinished, vi } from "vitest"; +it("should work", async () => { + vi.useFakeTimers(); + onTestFinished(() => { + vi.useRealTimers(); + }); + expect(1).toBe(1); +}); +`); + }); + + it('should replace `tick` with `vi.advanceTimersByTimeAsync`', async () => { + const { transformContent } = await setUp(); + + const newContent = await transformContent(` +it("should work", fakeAsync(() => { + tick(100); +})); +`); + + expect(newContent).toContain(`await vi.advanceTimersByTimeAsync(100);`); + }); + + it('should replace `tick()` with `vi.advanceTimersByTimeAsync(0)`', async () => { + const { transformContent } = await setUp(); + + const newContent = await transformContent(` +it("should work", fakeAsync(() => { + tick(); +})); +`); + + expect(newContent).toContain(`await vi.advanceTimersByTimeAsync(0);`); + }); + + it('should replace `flush` with `vi.advanceTimersByTimeAsync`', async () => { + const { transformContent } = await setUp(); + + const newContent = await transformContent(` +it("should work", fakeAsync(() => { + flush(); +})); +`); + + expect(newContent).toContain(`await vi.runAllTimersAsync();`); + }); +}); + +async function setUp() { + const schematicRunner = new SchematicTestRunner( + '@schematics/angular', + require.resolve('../../collection.json'), + ); + + const workspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '21.0.0', + }; + + const appOptions: ApplicationOptions = { + name: 'bar', + inlineStyle: false, + inlineTemplate: false, + routing: false, + skipTests: false, + skipPackageJson: false, + }; + + let appTree = await schematicRunner.runSchematic('workspace', workspaceOptions); + appTree = await schematicRunner.runSchematic('application', appOptions, appTree); + + return { + transformContent: async (content: string): Promise => { + const specFilePath = 'projects/bar/src/app/app.spec.ts'; + + appTree.overwrite(specFilePath, content); + + const tree = await schematicRunner.runSchematic( + 'refactor-fake-async', + { project: 'bar' }, + appTree, + ); + + return format(tree.readContent(specFilePath), { parser: 'typescript' }); + }, + }; +} diff --git a/packages/schematics/angular/refactor/fake-async/refactor-context.ts b/packages/schematics/angular/refactor/fake-async/refactor-context.ts new file mode 100644 index 000000000000..d6ac08e78a3c --- /dev/null +++ b/packages/schematics/angular/refactor/fake-async/refactor-context.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export interface RefactorContext { + importedVitestIdentifiers: Set; +} + +export function createRefactorContext(): RefactorContext { + return { + importedVitestIdentifiers: new Set(), + }; +} diff --git a/packages/schematics/angular/refactor/fake-async/schema.json b/packages/schematics/angular/refactor/fake-async/schema.json new file mode 100644 index 000000000000..abc32a4e5441 --- /dev/null +++ b/packages/schematics/angular/refactor/fake-async/schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "title": "Angular `fakeAsync` to Vitest's Fake Timers", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "projectName" + } + } + }, + "required": ["project"] +} diff --git a/packages/schematics/angular/refactor/fake-async/transform-fake-async.ts b/packages/schematics/angular/refactor/fake-async/transform-fake-async.ts new file mode 100644 index 000000000000..479f7083d229 --- /dev/null +++ b/packages/schematics/angular/refactor/fake-async/transform-fake-async.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be found + * in the LICENSE file at https://angular.dev/license + */ + +import ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript'; +import { createRefactorContext } from './refactor-context'; +import { transformFlush } from './transformers/flush'; +import { transformSetupFakeTimers } from './transformers/setup-fake-timers'; +import { transformTick } from './transformers/tick'; + +/** + * Transforms Angular's `fakeAsync` to Vitest's fake timers. + * Replaces `it('...', fakeAsync(() => { ... }))` with an async test that uses + * `vi.useFakeTimers()` and `onTestFinished` for cleanup. + */ +export function transformFakeAsync(filePath: string, content: string): string { + const sourceFile = ts.createSourceFile( + filePath, + content, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS, + ); + + const refactorContext = createRefactorContext(); + + const transformer: ts.TransformerFactory = (context) => { + const visit = (node: ts.Node): ts.Node => { + if (ts.isCallExpression(node)) { + for (const transformer of callExpressionTransformers) { + node = transformer(node, refactorContext); + } + } + + return ts.visitEachChild(node, visit, context); + }; + + return (sf) => ts.visitNode(sf, visit, ts.isSourceFile); + }; + + const result = ts.transform(sourceFile, [transformer]); + let transformedSourceFile = result.transformed[0]; + + if (refactorContext.importedVitestIdentifiers.size > 0) { + const indentifiers = Array.from(refactorContext.importedVitestIdentifiers); + const vitestImport = ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + undefined, + undefined, + ts.factory.createNamedImports( + indentifiers.map((identifier) => + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier(identifier), + ), + ), + ), + ), + ts.factory.createStringLiteral('vitest', true), + ); + + transformedSourceFile = ts.factory.updateSourceFile(transformedSourceFile, [ + vitestImport, + ...transformedSourceFile.statements, + ]); + } + + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + + return printer.printFile(transformedSourceFile); +} + +const callExpressionTransformers = [transformSetupFakeTimers, transformTick, transformFlush]; diff --git a/packages/schematics/angular/refactor/fake-async/transformers/flush.ts b/packages/schematics/angular/refactor/fake-async/transformers/flush.ts new file mode 100644 index 000000000000..f83f186fdc14 --- /dev/null +++ b/packages/schematics/angular/refactor/fake-async/transformers/flush.ts @@ -0,0 +1,22 @@ +import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript'; + +export function transformFlush(node: ts.Node): ts.Node { + if ( + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === 'flush' + ) { + return ts.factory.createAwaitExpression( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('vi'), + 'runAllTimersAsync', + ), + undefined, + [], + ), + ); + } + + return node; +} diff --git a/packages/schematics/angular/refactor/fake-async/transformers/setup-fake-timers.ts b/packages/schematics/angular/refactor/fake-async/transformers/setup-fake-timers.ts new file mode 100644 index 000000000000..05a87652e728 --- /dev/null +++ b/packages/schematics/angular/refactor/fake-async/transformers/setup-fake-timers.ts @@ -0,0 +1,73 @@ +import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript'; +import { RefactorContext } from '../refactor-context'; + +export function transformSetupFakeTimers(node: ts.Node, ctx: RefactorContext): ts.Node { + if ( + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === 'fakeAsync' && + node.arguments.length >= 1 && + (ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0])) + ) { + ctx.importedVitestIdentifiers.add('onTestFinished'); + ctx.importedVitestIdentifiers.add('vi'); + + const callback = node.arguments[0]; + const callbackBody = ts.isBlock(callback.body) + ? callback.body + : ts.factory.createBlock([ts.factory.createExpressionStatement(callback.body)]); + + // Generate the following code: + // vi.useFakeTimers(); + // onTestFinished(() => { + // vi.useRealTimers(); + // }); + const setupStatements: ts.Statement[] = [ + ts.factory.createExpressionStatement( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('vi'), + 'useFakeTimers', + ), + undefined, + [], + ), + ), + ts.factory.createExpressionStatement( + ts.factory.createCallExpression(ts.factory.createIdentifier('onTestFinished'), undefined, [ + ts.factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createBlock([ + ts.factory.createExpressionStatement( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('vi'), + 'useRealTimers', + ), + undefined, + [], + ), + ), + ]), + ), + ]), + ), + ...callbackBody.statements, + ]; + + return ts.factory.createArrowFunction( + [ts.factory.createModifier(ts.SyntaxKind.AsyncKeyword)], + undefined, + [], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createBlock(setupStatements), + ); + } + + return node; +} diff --git a/packages/schematics/angular/refactor/fake-async/transformers/tick.ts b/packages/schematics/angular/refactor/fake-async/transformers/tick.ts new file mode 100644 index 000000000000..3dd785f4e695 --- /dev/null +++ b/packages/schematics/angular/refactor/fake-async/transformers/tick.ts @@ -0,0 +1,28 @@ +import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript'; +import { RefactorContext } from '../refactor-context'; + +export function transformTick(node: ts.Node): ts.Node { + if ( + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === 'tick' + ) { + const durationNumericLiteral = + node.arguments.length > 0 && ts.isNumericLiteral(node.arguments[0]) + ? node.arguments[0] + : ts.factory.createNumericLiteral(0); + + return ts.factory.createAwaitExpression( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('vi'), + 'advanceTimersByTimeAsync', + ), + undefined, + [durationNumericLiteral], + ), + ); + } + + return node; +} diff --git a/packages/schematics/angular/refactor/jasmine-vitest/index.ts b/packages/schematics/angular/refactor/jasmine-vitest/index.ts index 4ae4077a7be4..e9385fcc4521 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/index.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/index.ts @@ -18,6 +18,7 @@ import { ProjectDefinition, getWorkspace } from '../../utility/workspace'; import { Schema } from './schema'; import { transformJasmineToVitest } from './test-file-transformer'; import { RefactorReporter } from './utils/refactor-reporter'; +import { findTestFiles } from '../../utility/find-test-files'; async function getProject( tree: Tree, @@ -46,31 +47,6 @@ async function getProject( ); } -const DIRECTORIES_TO_SKIP = new Set(['node_modules', '.git', 'dist', '.angular']); - -function findTestFiles(directory: DirEntry, fileSuffix: string): string[] { - const files: string[] = []; - const stack: DirEntry[] = [directory]; - - let current: DirEntry | undefined; - while ((current = stack.pop())) { - for (const path of current.subfiles) { - if (path.endsWith(fileSuffix)) { - files.push(current.path + '/' + path); - } - } - - for (const path of current.subdirs) { - if (DIRECTORIES_TO_SKIP.has(path)) { - continue; - } - stack.push(current.dir(path)); - } - } - - return files; -} - export default function (options: Schema): Rule { return async (tree: Tree, context: SchematicContext) => { const reporter = new RefactorReporter(context.logger); diff --git a/packages/schematics/angular/refactor/jasmine-vitest/utils/ast-helpers.ts b/packages/schematics/angular/refactor/jasmine-vitest/utils/ast-helpers.ts index f6f363df1643..4119d3e0c2c7 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/utils/ast-helpers.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/utils/ast-helpers.ts @@ -48,15 +48,13 @@ export function getVitestAutoImports( allSpecifiers.sort((a, b) => a.name.text.localeCompare(b.name.text)); - const importClause = ts.factory.createImportClause( - isClauseTypeOnly, // Set isTypeOnly on the clause if only type imports - undefined, - ts.factory.createNamedImports(allSpecifiers), - ); - return ts.factory.createImportDeclaration( undefined, - importClause, + ts.factory.createImportClause( + isClauseTypeOnly, // Set isTypeOnly on the clause if only type imports + undefined, + ts.factory.createNamedImports(allSpecifiers), + ), ts.factory.createStringLiteral('vitest'), ); } diff --git a/packages/schematics/angular/utility/find-test-files.ts b/packages/schematics/angular/utility/find-test-files.ts new file mode 100644 index 000000000000..dc6ccb71a91a --- /dev/null +++ b/packages/schematics/angular/utility/find-test-files.ts @@ -0,0 +1,26 @@ +import { DirEntry } from '@angular-devkit/schematics'; + +const DIRECTORIES_TO_SKIP = new Set(['node_modules', '.git', 'dist', '.angular']); + +export function findTestFiles(directory: DirEntry, fileSuffix: string): string[] { + const files: string[] = []; + const stack: DirEntry[] = [directory]; + + let current: DirEntry | undefined; + while ((current = stack.pop())) { + for (const path of current.subfiles) { + if (path.endsWith(fileSuffix)) { + files.push(current.path + '/' + path); + } + } + + for (const path of current.subdirs) { + if (DIRECTORIES_TO_SKIP.has(path)) { + continue; + } + stack.push(current.dir(path)); + } + } + + return files; +} From 191de45a6bf84bc623f97dfeb792751312b7ac85 Mon Sep 17 00:00:00 2001 From: Younes Jaaidi Date: Tue, 17 Mar 2026 17:29:22 +0100 Subject: [PATCH 2/2] feat(@schematics/angular): do not transform flush & tick if used outside fakeAsync --- .../angular/refactor/fake-async/index_spec.ts | 24 ++++++++++++ .../refactor/fake-async/refactor-context.ts | 2 + .../fake-async/transform-fake-async.ts | 11 +++++- .../refactor/fake-async/transformers/flush.ts | 12 +++++- .../transformers/setup-fake-timers.ts | 38 +++++++++++++------ .../refactor/fake-async/transformers/tick.ts | 11 +++++- 6 files changed, 84 insertions(+), 14 deletions(-) diff --git a/packages/schematics/angular/refactor/fake-async/index_spec.ts b/packages/schematics/angular/refactor/fake-async/index_spec.ts index 7fc1676346c6..24d433b446e9 100644 --- a/packages/schematics/angular/refactor/fake-async/index_spec.ts +++ b/packages/schematics/angular/refactor/fake-async/index_spec.ts @@ -68,6 +68,30 @@ it("should work", fakeAsync(() => { expect(newContent).toContain(`await vi.runAllTimersAsync();`); }); + + it('should not transform `tick` if not inside `fakeAsync`', async () => { + const { transformContent } = await setUp(); + + const newContent = await transformContent(` +it("should work", () => { + tick(100); +})); +`); + + expect(newContent).toContain(`tick(100);`); + }); + + it('should not transform `flush` if not inside `fakeAsync`', async () => { + const { transformContent } = await setUp(); + + const newContent = await transformContent(` +it("should work", () => { + flush(); +})); +`); + + expect(newContent).toContain(`flush();`); + }); }); async function setUp() { diff --git a/packages/schematics/angular/refactor/fake-async/refactor-context.ts b/packages/schematics/angular/refactor/fake-async/refactor-context.ts index d6ac08e78a3c..5f43df449e21 100644 --- a/packages/schematics/angular/refactor/fake-async/refactor-context.ts +++ b/packages/schematics/angular/refactor/fake-async/refactor-context.ts @@ -8,10 +8,12 @@ export interface RefactorContext { importedVitestIdentifiers: Set; + isInsideFakeAsync: boolean; } export function createRefactorContext(): RefactorContext { return { importedVitestIdentifiers: new Set(), + isInsideFakeAsync: false, }; } diff --git a/packages/schematics/angular/refactor/fake-async/transform-fake-async.ts b/packages/schematics/angular/refactor/fake-async/transform-fake-async.ts index 479f7083d229..caaab73ccd9d 100644 --- a/packages/schematics/angular/refactor/fake-async/transform-fake-async.ts +++ b/packages/schematics/angular/refactor/fake-async/transform-fake-async.ts @@ -9,7 +9,10 @@ import ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript'; import { createRefactorContext } from './refactor-context'; import { transformFlush } from './transformers/flush'; -import { transformSetupFakeTimers } from './transformers/setup-fake-timers'; +import { + isFakeAsyncCallExpression, + transformSetupFakeTimers, +} from './transformers/setup-fake-timers'; import { transformTick } from './transformers/tick'; /** @@ -31,9 +34,15 @@ export function transformFakeAsync(filePath: string, content: string): string { const transformer: ts.TransformerFactory = (context) => { const visit = (node: ts.Node): ts.Node => { if (ts.isCallExpression(node)) { + const isFakeAsync = isFakeAsyncCallExpression(node); + for (const transformer of callExpressionTransformers) { node = transformer(node, refactorContext); } + + if (isFakeAsync) { + refactorContext.isInsideFakeAsync = true; + } } return ts.visitEachChild(node, visit, context); diff --git a/packages/schematics/angular/refactor/fake-async/transformers/flush.ts b/packages/schematics/angular/refactor/fake-async/transformers/flush.ts index f83f186fdc14..6d17a4f0c9f7 100644 --- a/packages/schematics/angular/refactor/fake-async/transformers/flush.ts +++ b/packages/schematics/angular/refactor/fake-async/transformers/flush.ts @@ -1,7 +1,17 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript'; +import { RefactorContext } from '../refactor-context'; -export function transformFlush(node: ts.Node): ts.Node { +export function transformFlush(node: ts.Node, ctx: RefactorContext): ts.Node { if ( + ctx.isInsideFakeAsync && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'flush' diff --git a/packages/schematics/angular/refactor/fake-async/transformers/setup-fake-timers.ts b/packages/schematics/angular/refactor/fake-async/transformers/setup-fake-timers.ts index 05a87652e728..ea6e5ac5b8b8 100644 --- a/packages/schematics/angular/refactor/fake-async/transformers/setup-fake-timers.ts +++ b/packages/schematics/angular/refactor/fake-async/transformers/setup-fake-timers.ts @@ -1,16 +1,19 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript'; import { RefactorContext } from '../refactor-context'; export function transformSetupFakeTimers(node: ts.Node, ctx: RefactorContext): ts.Node { - if ( - ts.isCallExpression(node) && - ts.isIdentifier(node.expression) && - node.expression.text === 'fakeAsync' && - node.arguments.length >= 1 && - (ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0])) - ) { + if (isFakeAsyncCallExpression(node)) { ctx.importedVitestIdentifiers.add('onTestFinished'); ctx.importedVitestIdentifiers.add('vi'); + ctx.isInsideFakeAsync = true; const callback = node.arguments[0]; const callbackBody = ts.isBlock(callback.body) @@ -18,10 +21,10 @@ export function transformSetupFakeTimers(node: ts.Node, ctx: RefactorContext): t : ts.factory.createBlock([ts.factory.createExpressionStatement(callback.body)]); // Generate the following code: - // vi.useFakeTimers(); - // onTestFinished(() => { - // vi.useRealTimers(); - // }); + // > vi.useFakeTimers(); + // > onTestFinished(() => { + // > vi.useRealTimers(); + // > }); const setupStatements: ts.Statement[] = [ ts.factory.createExpressionStatement( ts.factory.createCallExpression( @@ -71,3 +74,16 @@ export function transformSetupFakeTimers(node: ts.Node, ctx: RefactorContext): t return node; } + +export function isFakeAsyncCallExpression(node: ts.Node): node is ts.CallExpression & { + expression: ts.Identifier; + arguments: [ts.ArrowFunction | ts.FunctionExpression]; +} { + return ( + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === 'fakeAsync' && + node.arguments.length >= 1 && + (ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0])) + ); +} diff --git a/packages/schematics/angular/refactor/fake-async/transformers/tick.ts b/packages/schematics/angular/refactor/fake-async/transformers/tick.ts index 3dd785f4e695..2cd221463b62 100644 --- a/packages/schematics/angular/refactor/fake-async/transformers/tick.ts +++ b/packages/schematics/angular/refactor/fake-async/transformers/tick.ts @@ -1,8 +1,17 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript'; import { RefactorContext } from '../refactor-context'; -export function transformTick(node: ts.Node): ts.Node { +export function transformTick(node: ts.Node, ctx: RefactorContext): ts.Node { if ( + ctx.isInsideFakeAsync && ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'tick'