From 4605a5bc8a462edbb2efe0b6998adddfab624e4e Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:07:33 -0400 Subject: [PATCH 1/2] feat(@schematics/angular): add `include` option to jasmine-to-vitest schematic This commit introduces a new `--include` option to the `jasmine-to-vitest` refactoring schematic. This option allows users to scope the transformation to a specific file or directory within a project. Previously, the schematic would always process every test file in the entire project. With the `include` option, users can now run the refactoring on a smaller subset of files, which is useful for incremental migrations or for targeting specific areas of a codebase. The implementation handles both file and directory paths, normalizes Windows-style path separators, and includes error handling for invalid or non-existent paths. --- .../angular/refactor/jasmine-vitest/index.ts | 69 ++++++++++---- .../refactor/jasmine-vitest/index_spec.ts | 90 ++++++++++++++++++- .../refactor/jasmine-vitest/schema.json | 4 + 3 files changed, 143 insertions(+), 20 deletions(-) diff --git a/packages/schematics/angular/refactor/jasmine-vitest/index.ts b/packages/schematics/angular/refactor/jasmine-vitest/index.ts index 96e4ebbabdc5..fb545b8bcbca 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/index.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/index.ts @@ -13,37 +13,37 @@ import { SchematicsException, Tree, } from '@angular-devkit/schematics'; +import { join, normalize } from 'node:path/posix'; import { ProjectDefinition, getWorkspace } from '../../utility/workspace'; import { Schema } from './schema'; import { transformJasmineToVitest } from './test-file-transformer'; import { RefactorReporter } from './utils/refactor-reporter'; -async function getProjectRoot(tree: Tree, projectName: string | undefined): Promise { +async function getProject( + tree: Tree, + projectName: string | undefined, +): Promise<{ project: ProjectDefinition; name: string }> { const workspace = await getWorkspace(tree); - let project: ProjectDefinition | undefined; if (projectName) { - project = workspace.projects.get(projectName); + const project = workspace.projects.get(projectName); if (!project) { throw new SchematicsException(`Project "${projectName}" not found.`); } - } else { - if (workspace.projects.size === 1) { - project = workspace.projects.values().next().value; - } else { - const projectNames = Array.from(workspace.projects.keys()); - throw new SchematicsException( - `Multiple projects found: [${projectNames.join(', ')}]. Please specify a project name.`, - ); - } + + return { project, name: projectName }; } - if (!project) { - // This case should theoretically not be hit due to the checks above, but it's good for type safety. - throw new SchematicsException('Could not determine a project.'); + if (workspace.projects.size === 1) { + const [name, project] = Array.from(workspace.projects.entries())[0]; + + return { project, name }; } - return project.root; + const projectNames = Array.from(workspace.projects.keys()); + throw new SchematicsException( + `Multiple projects found: [${projectNames.join(', ')}]. Please specify a project name.`, + ); } const DIRECTORIES_TO_SKIP = new Set(['node_modules', '.git', 'dist', '.angular']); @@ -74,14 +74,45 @@ function findTestFiles(directory: DirEntry, fileSuffix: string): string[] { export default function (options: Schema): Rule { return async (tree: Tree, context: SchematicContext) => { const reporter = new RefactorReporter(context.logger); - const projectRoot = await getProjectRoot(tree, options.project); + const { project, name: projectName } = await getProject(tree, options.project); + const projectRoot = project.root; const fileSuffix = options.fileSuffix ?? '.spec.ts'; - const files = findTestFiles(tree.getDir(projectRoot), fileSuffix); + let files: string[]; + let searchScope: string; + + if (options.include) { + const normalizedInclude = options.include.replace(/\\/g, '/'); + const includePath = normalize(join(projectRoot, normalizedInclude)); + searchScope = options.include; + + let dirEntry: DirEntry | null = null; + try { + dirEntry = tree.getDir(includePath); + } catch { + // Path is not a directory. + } + + // Approximation of a directory exists check + if (dirEntry && (dirEntry.subdirs.length > 0 || dirEntry.subfiles.length > 0)) { + // It is a directory + files = findTestFiles(dirEntry, fileSuffix); + } else if (tree.exists(includePath)) { + // It is a file + files = [includePath]; + } else { + throw new SchematicsException( + `The specified include path '${options.include}' does not exist.`, + ); + } + } else { + searchScope = `project '${projectName}'`; + files = findTestFiles(tree.getDir(projectRoot), fileSuffix); + } if (files.length === 0) { throw new SchematicsException( - `No files ending with '${fileSuffix}' found in project '${options.project}'.`, + `No files ending with '${fileSuffix}' found in ${searchScope}.`, ); } diff --git a/packages/schematics/angular/refactor/jasmine-vitest/index_spec.ts b/packages/schematics/angular/refactor/jasmine-vitest/index_spec.ts index 70f0a3fd0abd..194c4be4298d 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/index_spec.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/index_spec.ts @@ -123,6 +123,94 @@ describe('Jasmine to Vitest Schematic', () => { expect(logs.some((log) => log.includes('Transformed `spyOn` to `vi.spyOn`'))).toBe(true); }); + describe('with `include` option', () => { + beforeEach(() => { + // Create a nested structure for testing directory-specific inclusion + appTree.create( + 'projects/bar/src/app/nested/nested.spec.ts', + `describe('Nested', () => { it('should work', () => { spyOn(window, 'confirm'); }); });`, + ); + appTree.overwrite( + 'projects/bar/src/app/app.spec.ts', + `describe('App', () => { it('should work', () => { spyOn(window, 'alert'); }); });`, + ); + }); + + it('should only transform the specified file', async () => { + const tree = await schematicRunner.runSchematic( + 'jasmine-to-vitest', + { project: 'bar', include: 'src/app/nested/nested.spec.ts' }, + appTree, + ); + + const changedContent = tree.readContent('projects/bar/src/app/nested/nested.spec.ts'); + expect(changedContent).toContain(`vi.spyOn(window, 'confirm');`); + + const unchangedContent = tree.readContent('projects/bar/src/app/app.spec.ts'); + expect(unchangedContent).toContain(`spyOn(window, 'alert');`); + }); + + it('should handle a Windows-style path', async () => { + const tree = await schematicRunner.runSchematic( + 'jasmine-to-vitest', + { project: 'bar', include: 'src\\app\\nested\\nested.spec.ts' }, + appTree, + ); + + const changedContent = tree.readContent('projects/bar/src/app/nested/nested.spec.ts'); + expect(changedContent).toContain(`vi.spyOn(window, 'confirm');`); + + const unchangedContent = tree.readContent('projects/bar/src/app/app.spec.ts'); + expect(unchangedContent).toContain(`spyOn(window, 'alert');`); + }); + + it('should only transform files in the specified directory', async () => { + appTree.create( + 'projects/bar/src/other/other.spec.ts', + `describe('Other', () => { it('should work', () => { spyOn(window, 'close'); }); });`, + ); + + const tree = await schematicRunner.runSchematic( + 'jasmine-to-vitest', + { project: 'bar', include: 'src/app' }, + appTree, + ); + + const changedAppContent = tree.readContent('projects/bar/src/app/app.spec.ts'); + expect(changedAppContent).toContain(`vi.spyOn(window, 'alert');`); + + const changedNestedContent = tree.readContent('projects/bar/src/app/nested/nested.spec.ts'); + expect(changedNestedContent).toContain(`vi.spyOn(window, 'confirm');`); + + const unchangedContent = tree.readContent('projects/bar/src/other/other.spec.ts'); + expect(unchangedContent).toContain(`spyOn(window, 'close');`); + }); + + it('should process all files if `include` is not provided', async () => { + const tree = await schematicRunner.runSchematic( + 'jasmine-to-vitest', + { project: 'bar' }, + appTree, + ); + + const changedAppContent = tree.readContent('projects/bar/src/app/app.spec.ts'); + expect(changedAppContent).toContain(`vi.spyOn(window, 'alert');`); + + const changedNestedContent = tree.readContent('projects/bar/src/app/nested/nested.spec.ts'); + expect(changedNestedContent).toContain(`vi.spyOn(window, 'confirm');`); + }); + + it('should throw if the include path does not exist', async () => { + await expectAsync( + schematicRunner.runSchematic( + 'jasmine-to-vitest', + { project: 'bar', include: 'src/non-existent' }, + appTree, + ), + ).toBeRejectedWithError(`The specified include path 'src/non-existent' does not exist.`); + }); + }); + it('should print a summary report after running', async () => { const specFilePath = 'projects/bar/src/app/app.spec.ts'; const content = ` @@ -138,7 +226,7 @@ describe('Jasmine to Vitest Schematic', () => { const logs: string[] = []; schematicRunner.logger.subscribe((entry) => logs.push(entry.message)); - await schematicRunner.runSchematic('jasmine-to-vitest', { include: specFilePath }, appTree); + await schematicRunner.runSchematic('jasmine-to-vitest', {}, appTree); expect(logs).toContain('Jasmine to Vitest Refactoring Summary:'); expect(logs).toContain('- 1 test file(s) scanned.'); diff --git a/packages/schematics/angular/refactor/jasmine-vitest/schema.json b/packages/schematics/angular/refactor/jasmine-vitest/schema.json index bb60a4f2862f..5756b1f68ce9 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/schema.json +++ b/packages/schematics/angular/refactor/jasmine-vitest/schema.json @@ -5,6 +5,10 @@ "type": "object", "description": "Refactors a Jasmine test file to use Vitest.", "properties": { + "include": { + "type": "string", + "description": "A path to a specific file or directory to refactor. If not provided, all test files in the project will be refactored." + }, "fileSuffix": { "type": "string", "description": "The file suffix to identify test files (e.g., '.spec.ts', '.test.ts').", From 3dbed6fcd7db681385c61ef8ee37de8a7830c214 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:14:58 -0400 Subject: [PATCH 2/2] refactor(@schematics/angular): remove duplicate jasmine-to-vitest transformers This commit removes duplicate entries from the array of transformer functions within the `jasmine-to-vitest` schematic. This is a minor cleanup to improve code readability and maintainability without changing the schematics behavior. --- .../angular/refactor/jasmine-vitest/test-file-transformer.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.ts b/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.ts index b23dc3583b6c..c159f9bc3de2 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.ts @@ -82,16 +82,12 @@ export function transformJasmineToVitest( transformSpies, transformCreateSpyObj, transformSpyReset, - transformFocusedAndSkippedTests, transformSpyCallInspection, transformPending, transformDoneCallback, transformtoHaveBeenCalledBefore, transformToHaveClass, transformTimerMocks, - transformFocusedAndSkippedTests, - transformPending, - transformDoneCallback, transformGlobalFunctions, transformUnsupportedJasmineCalls, ];