Skip to content

Commit 5e3676a

Browse files
committed
feat(@schematics/angular): stabilize refactor-jasmine-vitest schematic
Stabilize `refactor-jasmine-vitest` schematic by covering the known remaining test patterns and cases.
1 parent 2678f5f commit 5e3676a

7 files changed

Lines changed: 191 additions & 37 deletions

File tree

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
@@ -288,8 +288,8 @@ describe('Jasmine to Vitest Transformer - Integration Tests', () => {
288288
});
289289
290290
it('should handle spy call order', () => {
291-
const spyA = vi.fn();
292-
const spyB = vi.fn();
291+
const spyA = vi.fn().mockName('spyA');
292+
const spyB = vi.fn().mockName('spyB');
293293
spyA();
294294
spyB();
295295
expect(Math.min(...vi.mocked(spyA).mock.invocationCallOrder)).toBeLessThan(Math.min(...vi.mocked(spyB).mock.invocationCallOrder));
@@ -387,7 +387,7 @@ describe('Jasmine to Vitest Transformer - Integration Tests', () => {
387387
});
388388
389389
it('should handle spies throwing errors', () => {
390-
const spy = vi.fn().mockImplementation(() => { throw new Error('Test Error') });
390+
const spy = vi.fn().mockName('mySpy').mockImplementation(() => { throw new Error('Test Error') });
391391
expect(() => spy()).toThrowError('Test Error');
392392
});
393393
});

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
transformUnsupportedJasmineCalls,
4141
} from './transformers/jasmine-misc';
4242
import {
43+
transformCreateSpy,
4344
transformCreateSpyObj,
4445
transformSpies,
4546
transformSpyCallInspection,
@@ -116,6 +117,7 @@ const callExpressionTransformers = [
116117
transformSyntacticSugarMatchers,
117118
transformComplexMatchers,
118119
transformSpies,
120+
transformCreateSpy,
119121
transformCreateSpyObj,
120122
transformSpyReset,
121123
transformSpyCallInspection,

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

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ export function transformTimerMocks(
5252
case 'mockDate':
5353
newMethodName = 'setSystemTime';
5454
break;
55+
case 'autoTick': {
56+
const category = 'clockAutoTick';
57+
reporter.recordTodo(category, sourceFile, node);
58+
addTodoComment(node, category);
59+
60+
return node;
61+
}
62+
case 'withMock': {
63+
const category = 'clockWithMock';
64+
reporter.recordTodo(category, sourceFile, node);
65+
addTodoComment(node, category);
66+
67+
return node;
68+
}
5569
}
5670

5771
if (newMethodName) {
@@ -85,15 +99,21 @@ export function transformFail(node: ts.Node, { sourceFile, reporter }: RefactorC
8599
node.expression.expression.text === 'fail'
86100
) {
87101
reporter.reportTransformation(sourceFile, node, 'Transformed `fail()` to `throw new Error()`.');
88-
const reason = node.expression.arguments[0];
89102

90-
const replacement = ts.factory.createThrowStatement(
91-
ts.factory.createNewExpression(
103+
const arg = node.expression.arguments[0];
104+
let throwExpression: ts.Expression;
105+
106+
if (arg && ts.isNewExpression(arg)) {
107+
throwExpression = arg;
108+
} else {
109+
throwExpression = ts.factory.createNewExpression(
92110
ts.factory.createIdentifier('Error'),
93111
undefined,
94-
reason ? [reason] : [],
95-
),
96-
);
112+
arg ? [arg] : [],
113+
);
114+
}
115+
116+
const replacement = ts.factory.createThrowStatement(throwExpression);
97117

98118
return ts.setOriginalNode(ts.setTextRange(replacement, node), node);
99119
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ describe('Jasmine to Vitest Transformer - transformTimerMocks', () => {
3535
input: `jasmine.clock().mockDate();`,
3636
expected: `vi.setSystemTime(new Date());`,
3737
},
38+
{
39+
description: 'should add a TODO for jasmine.clock().autoTick()',
40+
input: 'jasmine.clock().autoTick();',
41+
expected: `// TODO: vitest-migration: Vitest does not have a direct equivalent for jasmine.clock().autoTick(). Please migrate this manually. See: https://vitest.dev/api/vi.html#fake-timers
42+
jasmine.clock().autoTick();`,
43+
},
44+
{
45+
description: 'should add a TODO for jasmine.clock().withMock()',
46+
input: 'jasmine.clock().withMock(noop);',
47+
expected: `// TODO: vitest-migration: Vitest does not have a direct equivalent for jasmine.clock().withMock(). Please migrate this manually via vi.useFakeTimers() and vi.useRealTimers(). See: https://vitest.dev/api/vi.html#vi-usefaketimers
48+
jasmine.clock().withMock(noop);`,
49+
},
3850
];
3951

4052
testCases.forEach(({ description, input, expected }) => {
@@ -56,6 +68,11 @@ describe('transformFail', () => {
5668
input: `fail();`,
5769
expected: `throw new Error();`,
5870
},
71+
{
72+
description: 'should transform fail() with an Error object',
73+
input: `fail(new TypeError('Invalid input'));`,
74+
expected: `throw new TypeError('Invalid input');`,
75+
},
5976
];
6077

6178
testCases.forEach(({ description, input, expected }) => {

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

Lines changed: 103 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,16 @@ export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.
151151

152152
return ts.factory.createCallExpression(newExpression, undefined, [arrowFunction]);
153153
}
154+
case 'identity': {
155+
reporter.reportTransformation(
156+
sourceFile,
157+
node,
158+
'Transformed `.and.identity()` to `.getMockName()`.',
159+
);
160+
const newExpression = createPropertyAccess(spyCall, 'getMockName');
161+
162+
return ts.factory.createCallExpression(newExpression, undefined, undefined);
163+
}
154164
default: {
155165
const category = 'unsupported-spy-strategy';
156166
reporter.recordTodo(category, sourceFile, node);
@@ -183,35 +193,54 @@ export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.
183193
}
184194
}
185195

186-
const jasmineMethodName = getJasmineMethodName(node);
187-
switch (jasmineMethodName) {
188-
case 'createSpy':
189-
addVitestValueImport(pendingVitestValueImports, 'vi');
190-
reporter.reportTransformation(
191-
sourceFile,
192-
node,
193-
'Transformed `jasmine.createSpy()` to `vi.fn()`.',
194-
);
195-
196-
// jasmine.createSpy(name, originalFn) -> vi.fn(originalFn)
197-
return createViCallExpression('fn', node.arguments.length > 1 ? [node.arguments[1]] : []);
198-
case 'spyOnAllFunctions': {
199-
reporter.reportTransformation(
200-
sourceFile,
201-
node,
202-
'Found unsupported `jasmine.spyOnAllFunctions()`.',
203-
);
204-
const category = 'spyOnAllFunctions';
205-
reporter.recordTodo(category, sourceFile, node);
206-
addTodoComment(node, category);
196+
if (getJasmineMethodName(node) === 'spyOnAllFunctions') {
197+
reporter.reportTransformation(
198+
sourceFile,
199+
node,
200+
'Found unsupported `jasmine.spyOnAllFunctions()`.',
201+
);
202+
const category = 'spyOnAllFunctions';
203+
reporter.recordTodo(category, sourceFile, node);
204+
addTodoComment(node, category);
207205

208-
return node;
209-
}
206+
return node;
210207
}
211208

212209
return node;
213210
}
214211

212+
export function transformCreateSpy(
213+
node: ts.Node,
214+
{ reporter, sourceFile, pendingVitestValueImports }: RefactorContext,
215+
): ts.Node {
216+
if (!isJasmineCallExpression(node, 'createSpy')) {
217+
return node;
218+
}
219+
220+
addVitestValueImport(pendingVitestValueImports, 'vi');
221+
reporter.reportTransformation(
222+
sourceFile,
223+
node,
224+
'Transformed `jasmine.createSpy()` to `vi.fn()`.',
225+
);
226+
227+
const spyName = node.arguments[0];
228+
const viFnCallExpression = createViCallExpression(
229+
'fn',
230+
node.arguments.length > 1 ? [node.arguments[1]] : [],
231+
);
232+
233+
// jasmine.createSpy() -> vi.fn()
234+
// jasmine.createSpy(name, originalFn) -> vi.fn(originalFn).mockName(name)
235+
return !spyName
236+
? viFnCallExpression
237+
: ts.factory.createCallExpression(
238+
createPropertyAccess(viFnCallExpression, 'mockName'),
239+
undefined,
240+
[node.arguments[0]],
241+
);
242+
}
243+
215244
export function transformCreateSpyObj(
216245
node: ts.Node,
217246
{ sourceFile, reporter, pendingVitestValueImports }: RefactorContext,
@@ -428,12 +457,56 @@ function transformMostRecentArgs(
428457
return createPropertyAccess(mockProperty, 'lastCall');
429458
}
430459

460+
function transformThisFor(
461+
node: ts.Node,
462+
{ sourceFile, reporter, pendingVitestValueImports }: RefactorContext,
463+
): ts.Node {
464+
// Check 1: Is the node is a call expression?
465+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) {
466+
return node;
467+
}
468+
469+
// Check 2: Is it a call to `.thisFor`?
470+
const thisForPae = node.expression;
471+
if (
472+
!ts.isIdentifier(thisForPae.name) ||
473+
thisForPae.name.text !== 'thisFor' ||
474+
!ts.isPropertyAccessExpression(thisForPae.expression)
475+
) {
476+
return node;
477+
}
478+
479+
// Check 3: Can we get the spy identifier from `spy.calls`?
480+
const spyIdentifier = getSpyIdentifierFromCalls(thisForPae.expression);
481+
if (!spyIdentifier) {
482+
return node;
483+
}
484+
485+
// If all checks pass, perform the transformation.
486+
reporter.reportTransformation(
487+
sourceFile,
488+
node,
489+
'Transformed `spy.calls.thisFor(index)` to `vi.mocked(spy).mock.contexts[index]`.',
490+
);
491+
const mockProperty = createMockedSpyMockProperty(spyIdentifier, pendingVitestValueImports);
492+
493+
return ts.factory.createElementAccessExpression(
494+
createPropertyAccess(mockProperty, 'contexts'),
495+
node.arguments[0] ?? 0,
496+
);
497+
}
498+
431499
export function transformSpyCallInspection(node: ts.Node, refactorCtx: RefactorContext): ts.Node {
432500
const mostRecentArgsTransformed = transformMostRecentArgs(node, refactorCtx);
433501
if (mostRecentArgsTransformed !== node) {
434502
return mostRecentArgsTransformed;
435503
}
436504

505+
const thisForTransformed = transformThisFor(node, refactorCtx);
506+
if (thisForTransformed !== node) {
507+
return thisForTransformed;
508+
}
509+
437510
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) {
438511
return node;
439512
}
@@ -479,6 +552,13 @@ export function transformSpyCallInspection(node: ts.Node, refactorCtx: RefactorC
479552
message = 'Transformed `spy.calls.argsFor()` to `mock.calls[i]`.';
480553
newExpression = ts.factory.createElementAccessExpression(callsProperty, node.arguments[0]);
481554
break;
555+
case 'saveArgumentsByValue':
556+
{
557+
const category = 'saveArgumentsByValue';
558+
reporter.recordTodo(category, sourceFile, node);
559+
addTodoComment(node, category);
560+
}
561+
break;
482562
case 'mostRecent':
483563
if (
484564
!ts.isPropertyAccessExpression(node.parent) ||

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ describe('Jasmine to Vitest Transformer - transformSpies', () => {
3636
expected: `vi.spyOn(service, 'myMethod');`,
3737
},
3838
{
39-
description: 'should transform jasmine.createSpy("name") to vi.fn()',
39+
description: 'should transform jasmine.createSpy("name") to vi.fn().mockName("name")',
4040
input: `const mySpy = jasmine.createSpy('mySpy');`,
41-
expected: `const mySpy = vi.fn();`,
41+
expected: `const mySpy = vi.fn().mockName('mySpy');`,
4242
},
4343
{
44-
description: 'should transform jasmine.createSpy("name", fn) to vi.fn(fn)',
44+
description: 'should transform jasmine.createSpy("name", fn) to vi.fn(fn).mockName("name")',
4545
input: `const mySpy = jasmine.createSpy('mySpy', () => 'foo');`,
46-
expected: `const mySpy = vi.fn(() => 'foo');`,
46+
expected: `const mySpy = vi.fn(() => 'foo').mockName('mySpy');`,
4747
},
4848
{
4949
description: 'should transform spyOnProperty(object, "prop") to vi.spyOn(object, "prop")',
@@ -65,7 +65,7 @@ describe('Jasmine to Vitest Transformer - transformSpies', () => {
6565
{
6666
description: 'should handle chained calls on jasmine.createSpy()',
6767
input: `const mySpy = jasmine.createSpy('mySpy').and.returnValue(true);`,
68-
expected: `const mySpy = vi.fn().mockReturnValue(true);`,
68+
expected: `const mySpy = vi.fn().mockName('mySpy').mockReturnValue(true);`,
6969
},
7070
{
7171
description: 'should handle .and.returnValues() with no arguments',
@@ -94,6 +94,11 @@ describe('Jasmine to Vitest Transformer - transformSpies', () => {
9494
input: `spyOn(service, 'myMethod').and.rejectWith('some error');`,
9595
expected: `vi.spyOn(service, 'myMethod').mockRejectedValue('some error');`,
9696
},
97+
{
98+
description: 'should transform .and.identity() to .getMockName()',
99+
input: `spyOn(service, 'myMethod').and.identity();`,
100+
expected: `vi.spyOn(service, 'myMethod').getMockName();`,
101+
},
97102
{
98103
description: 'should add a TODO for an unsupported spy strategy',
99104
input: `spyOn(service, 'myMethod').and.unknownStrategy();`,
@@ -258,6 +263,11 @@ describe('transformSpyCallInspection', () => {
258263
input: `const recentArgs = mySpy.calls.mostRecent().args;`,
259264
expected: `const recentArgs = vi.mocked(mySpy).mock.lastCall;`,
260265
},
266+
{
267+
description: 'should transform spy.calls.thisFor(index)',
268+
input: `const context = mySpy.calls.thisFor(1337);`,
269+
expected: `const context = vi.mocked(mySpy).mock.contexts[1337];`,
270+
},
261271
{
262272
description: 'should transform spy.calls.first()',
263273
input: `const firstCall = mySpy.calls.first();`,
@@ -269,6 +279,14 @@ describe('transformSpyCallInspection', () => {
269279
expected: `// TODO: vitest-migration: Direct usage of mostRecent() is not supported. Please refactor to access .args directly or use vi.mocked(spy).mock.lastCall. See: https://vitest.dev/api/mocked.html#mock-lastcall
270280
const mostRecent = mySpy.calls.mostRecent();`,
271281
},
282+
{
283+
description: 'should add a TODO for spy.calls.saveArgumentsByValue()',
284+
input: `const saveArgs = mySpy.calls.saveArgumentsByValue();`,
285+
expected:
286+
'// TODO: vitest-migration: Vitest does not have a direct equivalent for spy.calls.saveArgumentsByValue().' +
287+
' Please migrate this manually by cloning and storing the arguments in a local variable.' +
288+
'\nconst saveArgs = mySpy.calls.saveArgumentsByValue();',
289+
},
272290
];
273291

274292
testCases.forEach(({ description, input, expected }) => {

packages/schematics/angular/refactor/jasmine-vitest/utils/todo-notes.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,23 @@ export const TODO_NOTES = {
124124
' Please refactor to access .args directly or use vi.mocked(spy).mock.lastCall.',
125125
url: 'https://vitest.dev/api/mocked.html#mock-lastcall',
126126
},
127+
'saveArgumentsByValue': {
128+
message:
129+
'Vitest does not have a direct equivalent for spy.calls.saveArgumentsByValue().' +
130+
' Please migrate this manually by cloning and storing the arguments in a local variable.',
131+
},
132+
'clockAutoTick': {
133+
message:
134+
'Vitest does not have a direct equivalent for jasmine.clock().autoTick(). Please migrate this manually.',
135+
url: 'https://vitest.dev/api/vi.html#fake-timers',
136+
},
137+
'clockWithMock': {
138+
message:
139+
'Vitest does not have a direct equivalent for jasmine.clock().withMock().' +
140+
' Please migrate this manually via vi.useFakeTimers() and vi.useRealTimers().',
141+
url: 'https://vitest.dev/api/vi.html#vi-usefaketimers',
142+
},
143+
127144
'unhandled-done-usage': {
128145
message: "The 'done' callback was used in an unhandled way. Please migrate manually.",
129146
},

0 commit comments

Comments
 (0)