Skip to content

Commit d2932cd

Browse files
committed
refactor(@schematics/angular): implement type-safe TODO notes in jasmine-to-vitest
This commit refactors the `jasmine-to-vitest` schematic to use a centralized and type-safe system for generating "TODO" comments. Previously, TODO messages were hardcoded strings scattered across various transformer files. This new system improves maintainability, consistency, and developer experience. Key changes include: - A new `utils/todo-notes.ts` file acts as a single source of truth for all TODO messages, categories, and optional documentation URLs. - Advanced mapped types (`TodoCategory`, `TodoContextMap`) are used to infer types directly from the configuration, ensuring that all calls are type-safe and preventing runtime errors. - The `addTodoComment` helper is now overloaded to handle both static and dynamic (context-aware) messages, ensuring that specific details (like an unsupported function name) are included in the comment. - All transformers have been updated to use this new system, resulting in cleaner, more consistent, and more maintainable code.
1 parent b7982bd commit d2932cd

File tree

13 files changed

+287
-129
lines changed

13 files changed

+287
-129
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ describe('Jasmine to Vitest Transformer - Integration Tests', () => {
362362
});
363363
364364
it.skip('should handle pending tests', () => {
365-
// TODO: vitest-migration: The pending() function was converted to a skipped test (\`it.skip\`).
365+
// TODO: vitest-migration: The pending() function was converted to a skipped test (\`it.skip\`). See: https://vitest.dev/api/vi.html#it-skip
366366
// pending('This test is not yet implemented.');
367367
});
368368
@@ -422,7 +422,7 @@ describe('Jasmine to Vitest Transformer - Integration Tests', () => {
422422
const vitestCode = `
423423
describe('Unsupported Features', () => {
424424
beforeAll(() => {
425-
// TODO: vitest-migration: jasmine.addMatchers is not supported. Please manually migrate to expect.extend().
425+
// TODO: vitest-migration: jasmine.addMatchers is not supported. Please manually migrate to expect.extend(). See: https://vitest.dev/api/expect.html#expect-extend
426426
jasmine.addMatchers({
427427
toBeAwesome: () => ({
428428
compare: (actual) => ({ pass: actual === 'awesome' })
@@ -437,7 +437,7 @@ describe('Jasmine to Vitest Transformer - Integration Tests', () => {
437437
438438
it('should handle spyOnAllFunctions', () => {
439439
const myObj = { func1: () => {}, func2: () => {} };
440-
// TODO: vitest-migration: Vitest does not have a direct equivalent for jasmine.spyOnAllFunctions(). Please spy on individual methods manually using vi.spyOn().
440+
// TODO: vitest-migration: Vitest does not have a direct equivalent for jasmine.spyOnAllFunctions(). Please spy on individual methods manually using vi.spyOn(). See: https://vitest.dev/api/vi.html#vi-spyon
441441
jasmine.spyOnAllFunctions(myObj);
442442
myObj.func1();
443443
expect(myObj.func1).toHaveBeenCalled();

packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-lifecycle.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,9 @@ export function transformPending(
8484
bodyNode,
8585
'Converted `pending()` to a skipped test (`it.skip`).',
8686
);
87-
reporter.recordTodo('pending');
88-
addTodoComment(
89-
replacement,
90-
'The pending() function was converted to a skipped test (`it.skip`).',
91-
);
87+
const category = 'pending';
88+
reporter.recordTodo(category);
89+
addTodoComment(replacement, category);
9290
ts.addSyntheticLeadingComment(
9391
replacement,
9492
ts.SyntaxKind.SingleLineCommentTrivia,

packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-lifecycle_spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,8 @@ describe('Jasmine to Vitest Transformer', () => {
190190
`,
191191
expected: `
192192
it.skip('is a work in progress', () => {
193-
// TODO: vitest-migration: The pending() function was converted to a skipped test (\`it.skip\`).
194-
// pending('Not yet implemented');
193+
// TODO: vitest-migration: The pending() function was converted to a skipped test (\`it.skip\`). See: https://vitest.dev/api/vi.html#it-skip
194+
// pending('Not yet implemented');
195195
});
196196
`,
197197
},
@@ -204,8 +204,8 @@ describe('Jasmine to Vitest Transformer', () => {
204204
`,
205205
expected: `
206206
it.skip('is a work in progress', function() {
207-
// TODO: vitest-migration: The pending() function was converted to a skipped test (\`it.skip\`).
208-
// pending('Not yet implemented');
207+
// TODO: vitest-migration: The pending() function was converted to a skipped test (\`it.skip\`). See: https://vitest.dev/api/vi.html#it-skip
208+
// pending('Not yet implemented');
209209
});
210210
`,
211211
},

packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher.ts

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -51,22 +51,17 @@ export function transformSyntacticSugarMatchers(
5151
const matcherName = pae.name.text;
5252

5353
if (matcherName === 'toHaveSpyInteractions') {
54-
reporter.recordTodo('toHaveSpyInteractions');
55-
addTodoComment(
56-
node,
57-
'Unsupported matcher ".toHaveSpyInteractions()" found. ' +
58-
'Please migrate this manually by checking the `mock.calls.length` of the individual spies.',
59-
);
54+
const category = 'toHaveSpyInteractions';
55+
reporter.recordTodo(category);
56+
addTodoComment(node, category);
6057

6158
return node;
6259
}
6360

6461
if (matcherName === 'toThrowMatching') {
65-
reporter.recordTodo('toThrowMatching');
66-
addTodoComment(
67-
node,
68-
'Unsupported matcher ".toThrowMatching()" found. Please migrate this manually.',
69-
);
62+
const category = 'toThrowMatching';
63+
reporter.recordTodo(category);
64+
addTodoComment(node, category, { name: matcherName });
7065

7166
return node;
7267
}
@@ -303,18 +298,13 @@ export function transformExpectAsync(
303298

304299
if (matcherName) {
305300
if (matcherName === 'toBePending') {
306-
reporter.recordTodo('toBePending');
307-
addTodoComment(
308-
node,
309-
'Unsupported matcher ".toBePending()" found. Vitest does not have a direct equivalent. ' +
310-
'Please migrate this manually, for example by using `Promise.race` to check if the promise settles within a short timeout.',
311-
);
301+
const category = 'toBePending';
302+
reporter.recordTodo(category);
303+
addTodoComment(node, category);
312304
} else {
313-
reporter.recordTodo('unsupported-expect-async-matcher');
314-
addTodoComment(
315-
node,
316-
`Unsupported expectAsync matcher ".${matcherName}()" found. Please migrate this manually.`,
317-
);
305+
const category = 'unsupported-expect-async-matcher';
306+
reporter.recordTodo(category);
307+
addTodoComment(node, category, { name: matcherName });
318308
}
319309
}
320310

@@ -422,11 +412,9 @@ export function transformArrayWithExactContents(
422412
}
423413

424414
if (!ts.isArrayLiteralExpression(argument.arguments[0])) {
425-
reporter.recordTodo('arrayWithExactContents-dynamic-variable');
426-
addTodoComment(
427-
node,
428-
'Cannot transform jasmine.arrayWithExactContents with a dynamic variable. Please migrate this manually.',
429-
);
415+
const category = 'arrayWithExactContents-dynamic-variable';
416+
reporter.recordTodo(category);
417+
addTodoComment(node, category);
430418

431419
return node;
432420
}
@@ -617,11 +605,9 @@ export function transformExpectNothing(
617605
const originalText = node.getFullText().trim();
618606

619607
reporter.reportTransformation(sourceFile, node, 'Removed `expect().nothing()` statement.');
620-
reporter.recordTodo('expect-nothing');
621-
addTodoComment(
622-
replacement,
623-
'expect().nothing() has been removed because it is redundant in Vitest. Tests without assertions pass by default.',
624-
);
608+
const category = 'expect-nothing';
609+
reporter.recordTodo(category);
610+
addTodoComment(replacement, category);
625611
ts.addSyntheticLeadingComment(
626612
replacement,
627613
ts.SyntaxKind.SingleLineCommentTrivia,

packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher_spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ describe('Jasmine to Vitest Transformer', () => {
168168
{
169169
description: 'should add a TODO for toThrowMatching',
170170
input: `expect(() => {}).toThrowMatching((e) => e.message === 'foo');`,
171-
expected: `// TODO: vitest-migration: Unsupported matcher ".toThrowMatching()" found. Please migrate this manually.
171+
expected: `// TODO: vitest-migration: Unsupported matcher ".toThrowMatching()" found. Please migrate this manually. See: https://vitest.dev/api/expect.html#tothrowerror
172172
expect(() => {}).toThrowMatching((e) => e.message === 'foo');`,
173173
},
174174
{

packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-misc.ts

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { createViCallExpression } from '../utils/ast-helpers';
1818
import { getJasmineMethodName, isJasmineCallExpression } from '../utils/ast-validation';
1919
import { addTodoComment } from '../utils/comment-helpers';
2020
import { RefactorContext } from '../utils/refactor-context';
21+
import { TodoCategory } from '../utils/todo-notes';
2122

2223
export function transformTimerMocks(
2324
node: ts.Node,
@@ -140,56 +141,42 @@ export function transformGlobalFunctions(
140141
node,
141142
`Found unsupported global function \`${functionName}\`.`,
142143
);
143-
reporter.recordTodo(functionName);
144-
addTodoComment(
145-
node,
146-
`Unsupported global function \`${functionName}\` found. This function is used for custom reporters in Jasmine ` +
147-
'and has no direct equivalent in Vitest.',
148-
);
144+
const category = 'unsupported-global-function';
145+
reporter.recordTodo(category);
146+
addTodoComment(node, category, { name: functionName });
149147
}
150148

151149
return node;
152150
}
153151

154-
const JASMINE_UNSUPPORTED_CALLS = new Map<string, string>([
155-
[
156-
'addMatchers',
157-
'jasmine.addMatchers is not supported. Please manually migrate to expect.extend().',
158-
],
159-
[
160-
'addCustomEqualityTester',
161-
'jasmine.addCustomEqualityTester is not supported. Please manually migrate to expect.addEqualityTesters().',
162-
],
163-
[
164-
'mapContaining',
165-
'jasmine.mapContaining is not supported. Vitest does not have a built-in matcher for Maps.' +
166-
' Please manually assert the contents of the Map.',
167-
],
168-
[
169-
'setContaining',
170-
'jasmine.setContaining is not supported. Vitest does not have a built-in matcher for Sets.' +
171-
' Please manually assert the contents of the Set.',
172-
],
152+
const UNSUPPORTED_JASMINE_CALLS_CATEGORIES = new Set<TodoCategory>([
153+
'addMatchers',
154+
'addCustomEqualityTester',
155+
'mapContaining',
156+
'setContaining',
173157
]);
174158

159+
// A type guard to ensure that the methodName is one of the categories handled by this transformer.
160+
function isUnsupportedJasmineCall(
161+
methodName: string,
162+
): methodName is 'addMatchers' | 'addCustomEqualityTester' | 'mapContaining' | 'setContaining' {
163+
return UNSUPPORTED_JASMINE_CALLS_CATEGORIES.has(methodName as TodoCategory);
164+
}
165+
175166
export function transformUnsupportedJasmineCalls(
176167
node: ts.Node,
177168
{ sourceFile, reporter }: RefactorContext,
178169
): ts.Node {
179170
const methodName = getJasmineMethodName(node);
180-
if (!methodName) {
181-
return node;
182-
}
183171

184-
const message = JASMINE_UNSUPPORTED_CALLS.get(methodName);
185-
if (message) {
172+
if (methodName && isUnsupportedJasmineCall(methodName)) {
186173
reporter.reportTransformation(
187174
sourceFile,
188175
node,
189176
`Found unsupported call \`jasmine.${methodName}\`.`,
190177
);
191178
reporter.recordTodo(methodName);
192-
addTodoComment(node, message);
179+
addTodoComment(node, methodName);
193180
}
194181

195182
return node;
@@ -238,11 +225,9 @@ export function transformUnknownJasmineProperties(
238225
node,
239226
`Found unknown jasmine property \`jasmine.${propName}\`.`,
240227
);
241-
reporter.recordTodo(`unknown-jasmine-property: ${propName}`);
242-
addTodoComment(
243-
node,
244-
`Unsupported jasmine property "${propName}" found. Please migrate this manually.`,
245-
);
228+
const category = 'unknown-jasmine-property';
229+
reporter.recordTodo(category);
230+
addTodoComment(node, category, { name: propName });
246231
}
247232
}
248233

packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-misc_spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ describe('Jasmine to Vitest Transformer', () => {
108108
});
109109
`,
110110
expected: `
111-
// TODO: vitest-migration: jasmine.addMatchers is not supported. Please manually migrate to expect.extend().
111+
// TODO: vitest-migration: jasmine.addMatchers is not supported. Please manually migrate to expect.extend(). See: https://vitest.dev/api/expect.html#expect-extend
112112
jasmine.addMatchers({
113113
toBeDivisibleByTwo: function () {
114114
return {
@@ -141,7 +141,7 @@ describe('Jasmine to Vitest Transformer', () => {
141141
});
142142
`,
143143
// eslint-disable-next-line max-len
144-
expected: `// TODO: vitest-migration: jasmine.addCustomEqualityTester is not supported. Please manually migrate to expect.addEqualityTesters().
144+
expected: `// TODO: vitest-migration: jasmine.addCustomEqualityTester is not supported. Please manually migrate to expect.addEqualityTesters(). See: https://vitest.dev/api/expect.html#expect-addequalitytesters
145145
jasmine.addCustomEqualityTester((a, b) => {
146146
return a.toString() === b.toString();
147147
});

packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy.ts

Lines changed: 23 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,12 @@ export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.
146146

147147
return ts.factory.createCallExpression(newExpression, undefined, [arrowFunction]);
148148
}
149-
default:
150-
reporter.recordTodo('unsupported-spy-strategy');
151-
addTodoComment(
152-
node,
153-
`Unsupported spy strategy ".and.${strategyName}()" found. Please migrate this manually.`,
154-
);
149+
default: {
150+
const category = 'unsupported-spy-strategy';
151+
reporter.recordTodo(category);
152+
addTodoComment(node, category, { name: strategyName });
153+
break;
154+
}
155155
}
156156

157157
if (newMethodName) {
@@ -184,20 +184,18 @@ export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.
184184

185185
// jasmine.createSpy(name, originalFn) -> vi.fn(originalFn)
186186
return createViCallExpression('fn', node.arguments.length > 1 ? [node.arguments[1]] : []);
187-
case 'spyOnAllFunctions':
187+
case 'spyOnAllFunctions': {
188188
reporter.reportTransformation(
189189
sourceFile,
190190
node,
191191
'Found unsupported `jasmine.spyOnAllFunctions()`.',
192192
);
193-
reporter.recordTodo('spyOnAllFunctions');
194-
addTodoComment(
195-
node,
196-
'Vitest does not have a direct equivalent for jasmine.spyOnAllFunctions().' +
197-
' Please spy on individual methods manually using vi.spyOn().',
198-
);
193+
const category = 'spyOnAllFunctions';
194+
reporter.recordTodo(category);
195+
addTodoComment(node, category);
199196

200197
return node;
198+
}
201199
}
202200

203201
return node;
@@ -218,11 +216,9 @@ export function transformCreateSpyObj(
218216
);
219217

220218
if (node.arguments.length < 2) {
221-
reporter.recordTodo('createSpyObj-single-argument');
222-
addTodoComment(
223-
node,
224-
'jasmine.createSpyObj called with a single argument is not supported for transformation.',
225-
);
219+
const category = 'createSpyObj-single-argument';
220+
reporter.recordTodo(category);
221+
addTodoComment(node, category);
226222

227223
return node;
228224
}
@@ -236,11 +232,9 @@ export function transformCreateSpyObj(
236232
} else if (ts.isObjectLiteralExpression(methods)) {
237233
properties = createSpyObjWithObject(methods);
238234
} else {
239-
reporter.recordTodo('createSpyObj-dynamic-variable');
240-
addTodoComment(
241-
node,
242-
'Cannot transform jasmine.createSpyObj with a dynamic variable. Please migrate this manually.',
243-
);
235+
const category = 'createSpyObj-dynamic-variable';
236+
reporter.recordTodo(category);
237+
addTodoComment(node, category);
244238

245239
return node;
246240
}
@@ -249,11 +243,9 @@ export function transformCreateSpyObj(
249243
if (ts.isObjectLiteralExpression(propertiesArg)) {
250244
properties.push(...(propertiesArg.properties as unknown as ts.PropertyAssignment[]));
251245
} else {
252-
reporter.recordTodo('createSpyObj-dynamic-property-map');
253-
addTodoComment(
254-
node,
255-
'Cannot transform jasmine.createSpyObj with a dynamic property map. Please migrate this manually.',
256-
);
246+
const category = 'createSpyObj-dynamic-property-map';
247+
reporter.recordTodo(category);
248+
addTodoComment(node, category);
257249
}
258250
}
259251

@@ -426,12 +418,9 @@ export function transformSpyCallInspection(
426418
!ts.isIdentifier(node.parent.name) ||
427419
node.parent.name.text !== 'args'
428420
) {
429-
reporter.recordTodo('mostRecent-without-args');
430-
addTodoComment(
431-
node,
432-
'Direct usage of mostRecent() is not supported.' +
433-
' Please refactor to access .args directly or use vi.mocked(spy).mock.lastCall.',
434-
);
421+
const category = 'mostRecent-without-args';
422+
reporter.recordTodo(category);
423+
addTodoComment(node, category);
435424
}
436425

437426
return node;

0 commit comments

Comments
 (0)