Skip to content
Open
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
5 changes: 5 additions & 0 deletions packages/schematics/angular/refactor/jasmine-vitest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ export default function (options: Schema): Rule {
}
}

if (options.report) {
const reportContent = reporter.generateReportContent();
tree.create(`jasmine-vitest-${new Date().toISOString()}.md`, reportContent);
}

reporter.printSummary(options.verbose);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
"type": "boolean",
"description": "Whether to add imports for the Vitest API. The Angular `unit-test` system automatically uses the Vitest globals option, which means explicit imports for global APIs like `describe`, `it`, `expect`, and `vi` are often not strictly necessary unless Vitest has been configured not to use globals.",
"default": false
},
"report": {
"type": "boolean",
"description": "Whether to generate a summary report file (jasmine-vitest-<date>.md) in the project root.",
"default": true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function transformPending(
'Converted `pending()` to a skipped test (`it.skip`).',
);
const category = 'pending';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, bodyNode);
addTodoComment(replacement, category);
ts.addSyntheticLeadingComment(
replacement,
Expand Down Expand Up @@ -412,7 +412,7 @@ export function transformDoneCallback(node: ts.Node, refactorCtx: RefactorContex
`Found unhandled usage of \`${doneIdentifier.text}\` callback. Skipping transformation.`,
);
const category = 'unhandled-done-usage';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(node, category);

return node;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ export function transformSyntacticSugarMatchers(

if (matcherName === 'toHaveSpyInteractions') {
const category = 'toHaveSpyInteractions';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(node, category);

return node;
}

if (matcherName === 'toThrowMatching') {
const category = 'toThrowMatching';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(node, category, { name: matcherName });

return node;
Expand Down Expand Up @@ -304,11 +304,11 @@ export function transformExpectAsync(
if (matcherName) {
if (matcherName === 'toBePending') {
const category = 'toBePending';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(node, category);
} else {
const category = 'unsupported-expect-async-matcher';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(node, category, { name: matcherName });
}
}
Expand Down Expand Up @@ -418,7 +418,7 @@ export function transformArrayWithExactContents(

if (!ts.isArrayLiteralExpression(argument.arguments[0])) {
const category = 'arrayWithExactContents-dynamic-variable';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(node, category);

return node;
Expand Down Expand Up @@ -455,7 +455,7 @@ export function transformArrayWithExactContents(
const containingStmt = ts.factory.createExpressionStatement(containingCall);

const category = 'arrayWithExactContents-check';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(lengthStmt, category);

return [lengthStmt, containingStmt];
Expand Down Expand Up @@ -615,7 +615,7 @@ export function transformExpectNothing(

reporter.reportTransformation(sourceFile, node, 'Removed `expect().nothing()` statement.');
const category = 'expect-nothing';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(replacement, category);
ts.addSyntheticLeadingComment(
replacement,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export function transformGlobalFunctions(
`Found unsupported global function \`${functionName}\`.`,
);
const category = 'unsupported-global-function';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(node, category, { name: functionName });
}

Expand Down Expand Up @@ -179,7 +179,7 @@ export function transformUnsupportedJasmineCalls(
node,
`Found unsupported call \`jasmine.${methodName}\`.`,
);
reporter.recordTodo(methodName);
reporter.recordTodo(methodName, sourceFile, node);
addTodoComment(node, methodName);
}

Expand Down Expand Up @@ -230,7 +230,7 @@ export function transformUnknownJasmineProperties(
`Found unknown jasmine property \`jasmine.${propName}\`.`,
);
const category = 'unknown-jasmine-property';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(node, category, { name: propName });
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.
}
default: {
const category = 'unsupported-spy-strategy';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(node, category, { name: strategyName });
break;
}
Expand Down Expand Up @@ -202,7 +202,7 @@ export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.
'Found unsupported `jasmine.spyOnAllFunctions()`.',
);
const category = 'spyOnAllFunctions';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(node, category);

return node;
Expand Down Expand Up @@ -235,7 +235,7 @@ export function transformCreateSpyObj(

if (node.arguments.length < 2) {
const category = 'createSpyObj-single-argument';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(node, category);

return node;
Expand All @@ -247,7 +247,7 @@ export function transformCreateSpyObj(
properties = createSpyObjWithObject(methods, baseName);
} else {
const category = 'createSpyObj-dynamic-variable';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(node, category);

return node;
Expand All @@ -258,7 +258,7 @@ export function transformCreateSpyObj(
properties.push(...(propertiesArg.properties as unknown as ts.PropertyAssignment[]));
} else {
const category = 'createSpyObj-dynamic-property-map';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(node, category);
}
}
Expand Down Expand Up @@ -485,7 +485,7 @@ export function transformSpyCallInspection(node: ts.Node, refactorCtx: RefactorC
node.parent.name.text !== 'args'
) {
const category = 'mostRecent-without-args';
reporter.recordTodo(category);
reporter.recordTodo(category, sourceFile, node);
addTodoComment(node, category);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.dev/license
*/

import { logging } from '@angular-devkit/core';
import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { TodoCategory } from './todo-notes';

Expand All @@ -15,8 +14,9 @@ export class RefactorReporter {
private filesTransformed = 0;
private readonly todos = new Map<string, number>();
private readonly verboseLogs = new Map<string, string[]>();
private readonly fileTodos = new Map<string, { category: TodoCategory; line: number }[]>();

constructor(private logger: logging.LoggerApi) {}
constructor(private logger: { info(message: string): void; warn(message: string): void }) {}

get hasTodos(): boolean {
return this.todos.size > 0;
Expand All @@ -30,14 +30,27 @@ export class RefactorReporter {
this.filesTransformed++;
}

recordTodo(category: TodoCategory): void {
recordTodo(category: TodoCategory, sourceFile: ts.SourceFile, node: ts.Node): void {
this.todos.set(category, (this.todos.get(category) ?? 0) + 1);

const { line } = ts.getLineAndCharacterOfPosition(
sourceFile,
ts.getOriginalNode(node).getStart(sourceFile),
);
const filePath = sourceFile.fileName;

let fileTodos = this.fileTodos.get(filePath);
if (!fileTodos) {
fileTodos = [];
this.fileTodos.set(filePath, fileTodos);
}
fileTodos.push({ category, line: line + 1 });
}

reportTransformation(sourceFile: ts.SourceFile, node: ts.Node, message: string): void {
const { line } = ts.getLineAndCharacterOfPosition(
sourceFile,
ts.getOriginalNode(node).getStart(),
ts.getOriginalNode(node).getStart(sourceFile),
);
const filePath = sourceFile.fileName;

Expand All @@ -49,6 +62,79 @@ export class RefactorReporter {
logs.push(`L${line + 1}: ${message}`);
}

generateReportContent(): string {
const lines: string[] = [];
lines.push('# Jasmine to Vitest Refactoring Report');
lines.push('');
lines.push(`Date: ${new Date().toISOString()}`);
lines.push('');

const summaryEntries = [
{ label: 'Files Scanned', value: this.filesScanned },
{ label: 'Files Transformed', value: this.filesTransformed },
{ label: 'Files Skipped', value: this.filesScanned - this.filesTransformed },
{ label: 'Total TODOs', value: [...this.todos.values()].reduce((a, b) => a + b, 0) },
];

const firstColPad = Math.max(...summaryEntries.map(({ label }) => label.length));
const secondColPad = 5;

lines.push('## Summary');
lines.push('');
lines.push(`| ${' '.padEnd(firstColPad)} | ${'Count'.padStart(secondColPad)} |`);
lines.push(`|:${'-'.repeat(firstColPad + 1)}|${'-'.repeat(secondColPad + 1)}:|`);
for (const { label, value } of summaryEntries) {
lines.push(`| ${label.padEnd(firstColPad)} | ${String(value).padStart(secondColPad)} |`);
}
lines.push('');

if (this.todos.size > 0) {
lines.push('## TODO Overview');
lines.push('');
const todoEntries = [...this.todos.entries()];
const firstColPad = Math.max(
'Category'.length,
...todoEntries.map(([category]) => category.length),
);
const secondColPad = 5;

lines.push(`| ${'Category'.padEnd(firstColPad)} | ${'Count'.padStart(secondColPad)} |`);
lines.push(`|:${'-'.repeat(firstColPad + 1)}|${'-'.repeat(secondColPad + 1)}:|`);
for (const [category, count] of todoEntries) {
lines.push(`| ${category.padEnd(firstColPad)} | ${String(count).padStart(secondColPad)} |`);
}
lines.push('');
}

if (this.fileTodos.size > 0) {
lines.push('## Files Requiring Manual Attention');
lines.push('');
// Sort files alphabetically
const sortedFiles = [...this.fileTodos.keys()].sort();

for (const filePath of sortedFiles) {
const relativePath = filePath.startsWith('/') ? filePath.substring(1) : filePath;
lines.push(`### [\`${relativePath}\`](./${relativePath})`);
const todos = this.fileTodos.get(filePath);
if (todos) {
// Sort todos by line number
todos.sort((a, b) => a.line - b.line);

for (const todo of todos) {
lines.push(`- [L${todo.line}](./${relativePath}#L${todo.line}): ${todo.category}`);
}
}
lines.push('');
}
} else {
lines.push('## No Manual Changes Required');
lines.push('');
lines.push('All identified patterns were successfully transformed.');
}

return lines.join('\n');
}

printSummary(verbose = false): void {
if (verbose && this.verboseLogs.size > 0) {
this.logger.info('Detailed Transformation Log:');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,23 @@
*/

import { logging } from '@angular-devkit/core';
import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { RefactorReporter } from './refactor-reporter';

describe('RefactorReporter', () => {
let logger: logging.LoggerApi;
let reporter: RefactorReporter;
let sourceFile: ts.SourceFile;
let node: ts.Node;

beforeEach(() => {
logger = {
info: jasmine.createSpy('info'),
warn: jasmine.createSpy('warn'),
} as unknown as logging.LoggerApi;
reporter = new RefactorReporter(logger);
sourceFile = ts.createSourceFile('/test.spec.ts', 'statement;', ts.ScriptTarget.Latest);
node = sourceFile.statements[0];
});

it('should correctly increment scanned and transformed files', () => {
Expand All @@ -34,9 +39,9 @@ describe('RefactorReporter', () => {
});

it('should record and count todos by category', () => {
reporter.recordTodo('pending');
reporter.recordTodo('spyOnAllFunctions');
reporter.recordTodo('pending');
reporter.recordTodo('pending', sourceFile, node);
reporter.recordTodo('spyOnAllFunctions', sourceFile, node);
reporter.recordTodo('pending', sourceFile, node);
reporter.printSummary();

expect(logger.warn).toHaveBeenCalledWith('- 3 TODO(s) added for manual review:');
Expand All @@ -48,4 +53,25 @@ describe('RefactorReporter', () => {
reporter.printSummary();
expect(logger.warn).not.toHaveBeenCalled();
});

it('should generate a markdown report with TODOs', () => {
reporter.incrementScannedFiles();
reporter.recordTodo('pending', sourceFile, node);

const report = reporter.generateReportContent();

expect(report).toContain('# Jasmine to Vitest Refactoring Report');
expect(report).toContain('## Summary');
expect(report).toContain('| | Count |');
expect(report).toContain('|:------------------|------:|');
expect(report).toContain('| Files Scanned | 1 |');
expect(report).toContain('| Total TODOs | 1 |');
expect(report).toContain('## TODO Overview');
expect(report).toContain('| Category | Count |');
expect(report).toContain('|:---------|------:|');
expect(report).toContain('| pending | 1 |');
expect(report).toContain('## Files Requiring Manual Attention');
expect(report).toContain('### [`test.spec.ts`](./test.spec.ts)');
expect(report).toContain('- [L1](./test.spec.ts#L1): pending');
});
});