Skip to content

Commit 629f5cb

Browse files
committed
feat(@schematics/angular): add misc transformations to jasmine-to-vitest schematic
This commit adds transformers for miscellaneous Jasmine APIs and handles unsupported features. Coverage includes: - Timer mocks (jasmine.clock) - The fail() function - jasmine.DEFAULT_TIMEOUT_INTERVAL It also includes the logic to identify and add TODO comments for any unsupported Jasmine APIs, ensuring a safe migration.
1 parent d6830d2 commit 629f5cb

File tree

3 files changed

+501
-1
lines changed

3 files changed

+501
-1
lines changed

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ import {
2424
transformWithContext,
2525
transformtoHaveBeenCalledBefore,
2626
} from './transformers/jasmine-matcher';
27+
import {
28+
transformDefaultTimeoutInterval,
29+
transformFail,
30+
transformGlobalFunctions,
31+
transformTimerMocks,
32+
transformUnknownJasmineProperties,
33+
transformUnsupportedJasmineCalls,
34+
} from './transformers/jasmine-misc';
2735
import {
2836
transformCreateSpyObj,
2937
transformSpies,
@@ -80,13 +88,23 @@ export function transformJasmineToVitest(
8088
transformDoneCallback,
8189
transformtoHaveBeenCalledBefore,
8290
transformToHaveClass,
91+
transformTimerMocks,
92+
transformFocusedAndSkippedTests,
93+
transformPending,
94+
transformDoneCallback,
95+
transformGlobalFunctions,
96+
transformUnsupportedJasmineCalls,
8397
];
8498

8599
for (const transformer of transformations) {
86100
transformedNode = transformer(transformedNode, refactorCtx);
87101
}
88102
} else if (ts.isPropertyAccessExpression(transformedNode)) {
89-
const transformations = [transformAsymmetricMatchers, transformSpyCallInspection];
103+
const transformations = [
104+
transformAsymmetricMatchers,
105+
transformSpyCallInspection,
106+
transformUnknownJasmineProperties,
107+
];
90108
for (const transformer of transformations) {
91109
transformedNode = transformer(transformedNode, refactorCtx);
92110
}
@@ -95,6 +113,8 @@ export function transformJasmineToVitest(
95113
transformCalledOnceWith,
96114
transformArrayWithExactContents,
97115
transformExpectNothing,
116+
transformFail,
117+
transformDefaultTimeoutInterval,
98118
];
99119

100120
for (const transformer of statementTransformers) {
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
/**
10+
* @fileoverview This file contains transformers for miscellaneous Jasmine APIs that don't
11+
* fit into other categories. This includes timer mocks (`jasmine.clock`), the `fail()`
12+
* function, and configuration settings like `jasmine.DEFAULT_TIMEOUT_INTERVAL`. It also
13+
* includes logic to identify and add TODO comments for unsupported Jasmine features.
14+
*/
15+
16+
import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
17+
import { createViCallExpression } from '../utils/ast-helpers';
18+
import { getJasmineMethodName, isJasmineCallExpression } from '../utils/ast-validation';
19+
import { addTodoComment } from '../utils/comment-helpers';
20+
import { RefactorContext } from '../utils/refactor-context';
21+
22+
export function transformTimerMocks(
23+
node: ts.Node,
24+
{ sourceFile, reporter }: RefactorContext,
25+
): ts.Node {
26+
if (
27+
!ts.isCallExpression(node) ||
28+
!ts.isPropertyAccessExpression(node.expression) ||
29+
!ts.isIdentifier(node.expression.name)
30+
) {
31+
return node;
32+
}
33+
34+
const pae = node.expression;
35+
const clockCall = pae.expression;
36+
if (!isJasmineCallExpression(clockCall, 'clock')) {
37+
return node;
38+
}
39+
40+
let newMethodName: string | undefined;
41+
switch (pae.name.text) {
42+
case 'install':
43+
newMethodName = 'useFakeTimers';
44+
break;
45+
case 'tick':
46+
newMethodName = 'advanceTimersByTime';
47+
break;
48+
case 'uninstall':
49+
newMethodName = 'useRealTimers';
50+
break;
51+
case 'mockDate':
52+
newMethodName = 'setSystemTime';
53+
break;
54+
}
55+
56+
if (newMethodName) {
57+
reporter.reportTransformation(
58+
sourceFile,
59+
node,
60+
`Transformed \`jasmine.clock().${pae.name.text}\` to \`vi.${newMethodName}\`.`,
61+
);
62+
const newArgs = newMethodName === 'useFakeTimers' ? [] : node.arguments;
63+
64+
return createViCallExpression(newMethodName, newArgs);
65+
}
66+
67+
return node;
68+
}
69+
70+
export function transformFail(node: ts.Node, { sourceFile, reporter }: RefactorContext): ts.Node {
71+
if (
72+
ts.isExpressionStatement(node) &&
73+
ts.isCallExpression(node.expression) &&
74+
ts.isIdentifier(node.expression.expression) &&
75+
node.expression.expression.text === 'fail'
76+
) {
77+
reporter.reportTransformation(sourceFile, node, 'Transformed `fail()` to `throw new Error()`.');
78+
const reason = node.expression.arguments[0];
79+
80+
return ts.factory.createThrowStatement(
81+
ts.factory.createNewExpression(
82+
ts.factory.createIdentifier('Error'),
83+
undefined,
84+
reason ? [reason] : [],
85+
),
86+
);
87+
}
88+
89+
return node;
90+
}
91+
92+
export function transformDefaultTimeoutInterval(
93+
node: ts.Node,
94+
{ sourceFile, reporter }: RefactorContext,
95+
): ts.Node {
96+
if (
97+
ts.isExpressionStatement(node) &&
98+
ts.isBinaryExpression(node.expression) &&
99+
node.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken
100+
) {
101+
const assignment = node.expression;
102+
if (
103+
ts.isPropertyAccessExpression(assignment.left) &&
104+
ts.isIdentifier(assignment.left.expression) &&
105+
assignment.left.expression.text === 'jasmine' &&
106+
assignment.left.name.text === 'DEFAULT_TIMEOUT_INTERVAL'
107+
) {
108+
reporter.reportTransformation(
109+
sourceFile,
110+
node,
111+
'Transformed `jasmine.DEFAULT_TIMEOUT_INTERVAL` to `vi.setConfig()`.',
112+
);
113+
const timeoutValue = assignment.right;
114+
const setConfigCall = createViCallExpression('setConfig', [
115+
ts.factory.createObjectLiteralExpression(
116+
[ts.factory.createPropertyAssignment('testTimeout', timeoutValue)],
117+
false,
118+
),
119+
]);
120+
121+
return ts.factory.createExpressionStatement(setConfigCall);
122+
}
123+
}
124+
125+
return node;
126+
}
127+
128+
export function transformGlobalFunctions(
129+
node: ts.Node,
130+
{ sourceFile, reporter }: RefactorContext,
131+
): ts.Node {
132+
if (
133+
ts.isCallExpression(node) &&
134+
ts.isIdentifier(node.expression) &&
135+
(node.expression.text === 'setSpecProperty' || node.expression.text === 'setSuiteProperty')
136+
) {
137+
const functionName = node.expression.text;
138+
reporter.reportTransformation(
139+
sourceFile,
140+
node,
141+
`Found unsupported global function \`${functionName}\`.`,
142+
);
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+
);
149+
}
150+
151+
return node;
152+
}
153+
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+
],
173+
]);
174+
175+
export function transformUnsupportedJasmineCalls(
176+
node: ts.Node,
177+
{ sourceFile, reporter }: RefactorContext,
178+
): ts.Node {
179+
const methodName = getJasmineMethodName(node);
180+
if (!methodName) {
181+
return node;
182+
}
183+
184+
const message = JASMINE_UNSUPPORTED_CALLS.get(methodName);
185+
if (message) {
186+
reporter.reportTransformation(
187+
sourceFile,
188+
node,
189+
`Found unsupported call \`jasmine.${methodName}\`.`,
190+
);
191+
reporter.recordTodo(methodName);
192+
addTodoComment(node, message);
193+
}
194+
195+
return node;
196+
}
197+
198+
// If any additional properties are added to transforms, they should also be added to this list.
199+
const HANDLED_JASMINE_PROPERTIES = new Set([
200+
// Spies
201+
'createSpy',
202+
'createSpyObj',
203+
'spyOnAllFunctions',
204+
// Clock
205+
'clock',
206+
// Matchers
207+
'any',
208+
'anything',
209+
'stringMatching',
210+
'objectContaining',
211+
'arrayContaining',
212+
'arrayWithExactContents',
213+
'truthy',
214+
'falsy',
215+
'empty',
216+
'notEmpty',
217+
'mapContaining',
218+
'setContaining',
219+
// Other
220+
'DEFAULT_TIMEOUT_INTERVAL',
221+
'addMatchers',
222+
'addCustomEqualityTester',
223+
]);
224+
225+
export function transformUnknownJasmineProperties(
226+
node: ts.Node,
227+
{ sourceFile, reporter }: RefactorContext,
228+
): ts.Node {
229+
if (
230+
ts.isPropertyAccessExpression(node) &&
231+
ts.isIdentifier(node.expression) &&
232+
node.expression.text === 'jasmine'
233+
) {
234+
const propName = node.name.text;
235+
if (!HANDLED_JASMINE_PROPERTIES.has(propName)) {
236+
reporter.reportTransformation(
237+
sourceFile,
238+
node,
239+
`Found unknown jasmine property \`jasmine.${propName}\`.`,
240+
);
241+
reporter.recordTodo(`unknown-jasmine-property: ${propName}`);
242+
addTodoComment(
243+
node,
244+
`Unsupported jasmine property "${propName}" found. Please migrate this manually.`,
245+
);
246+
}
247+
}
248+
249+
return node;
250+
}

0 commit comments

Comments
 (0)