Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 50 additions & 19 deletions packages/schematics/angular/refactor/jasmine-vitest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
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']);
Expand Down Expand Up @@ -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}.`,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
Expand All @@ -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.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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').",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,12 @@ export function transformJasmineToVitest(
transformSpies,
transformCreateSpyObj,
transformSpyReset,
transformFocusedAndSkippedTests,
transformSpyCallInspection,
transformPending,
transformDoneCallback,
transformtoHaveBeenCalledBefore,
transformToHaveClass,
transformTimerMocks,
transformFocusedAndSkippedTests,
transformPending,
transformDoneCallback,
transformGlobalFunctions,
transformUnsupportedJasmineCalls,
];
Expand Down