Skip to content

Commit 620ba09

Browse files
committed
refactor(@schematics/angular): decompose transformSpies into modular helper functions
1 parent 8556db8 commit 620ba09

1 file changed

Lines changed: 172 additions & 145 deletions

File tree

  • packages/schematics/angular/refactor/jasmine-vitest/transformers

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

Lines changed: 172 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,8 @@ function isChainedWithAnd(node: ts.Node): boolean {
5454
return false;
5555
}
5656

57-
/* eslint-disable max-lines-per-function */
58-
export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.Node {
57+
function transformPrimarySpy(node: ts.CallExpression, refactorCtx: RefactorContext): ts.Node {
5958
const { sourceFile, reporter, pendingVitestValueImports } = refactorCtx;
60-
if (!ts.isCallExpression(node)) {
61-
return node;
62-
}
63-
6459
if (
6560
ts.isIdentifier(node.expression) &&
6661
(node.expression.text === 'spyOn' || node.expression.text === 'spyOnProperty')
@@ -98,166 +93,180 @@ export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.
9893
);
9994
}
10095

101-
if (ts.isPropertyAccessExpression(node.expression)) {
102-
const pae = node.expression;
103-
104-
const isChainedWithAndProperty =
105-
(ts.isPropertyAccessExpression(pae.expression) &&
106-
ts.isIdentifier(pae.expression.name) &&
107-
pae.expression.name.text === 'and') ||
108-
(ts.isElementAccessExpression(pae.expression) &&
109-
ts.isStringLiteral(pae.expression.argumentExpression) &&
110-
pae.expression.argumentExpression.text === 'and');
111-
112-
if (isChainedWithAndProperty) {
113-
const spyCall = ts.isPropertyAccessExpression(pae.expression)
114-
? pae.expression.expression
115-
: (pae.expression as ts.ElementAccessExpression).expression;
116-
let newMethodName: string | undefined;
117-
let args = node.arguments;
118-
119-
if (ts.isIdentifier(pae.name)) {
120-
const strategyName = pae.name.text;
121-
switch (strategyName) {
122-
case 'returnValue':
123-
{
124-
const result = getPromiseResolveRejectMethod(args[0]);
125-
if (result) {
126-
const methodMapping = {
127-
'resolve': 'mockResolvedValue',
128-
'reject': 'mockRejectedValue',
129-
};
130-
newMethodName = methodMapping[result.methodName];
131-
args = result.arguments;
132-
} else {
133-
newMethodName = 'mockReturnValue';
134-
}
135-
}
136-
break;
137-
case 'resolveTo':
138-
newMethodName = 'mockResolvedValue';
139-
break;
140-
case 'rejectWith':
141-
newMethodName = 'mockRejectedValue';
142-
break;
143-
case 'returnValues': {
144-
reporter.reportTransformation(
145-
sourceFile,
146-
node,
147-
'Transformed `.and.returnValues()` to chained `.mockReturnValueOnce()` calls.',
148-
);
149-
const returnValues = node.arguments;
150-
if (returnValues.length === 0) {
151-
// No values, so it's a no-op. Just transform the spyOn call.
152-
return transformSpies(spyCall, refactorCtx);
153-
}
154-
// spy.and.returnValues(a, b) -> spy.mockReturnValueOnce(a).mockReturnValueOnce(b)
155-
let chainedCall: ts.Expression = spyCall;
156-
for (const value of returnValues) {
157-
const mockCall = ts.factory.createCallExpression(
158-
createPropertyAccess(chainedCall, 'mockReturnValueOnce'),
159-
undefined,
160-
[value],
161-
);
162-
chainedCall = mockCall;
163-
}
164-
165-
return chainedCall;
166-
}
167-
case 'callFake':
168-
newMethodName = 'mockImplementation';
169-
break;
170-
case 'callThrough':
171-
reporter.reportTransformation(
172-
sourceFile,
173-
node,
174-
'Removed redundant `.and.callThrough()` call.',
175-
);
96+
return node;
97+
}
17698

177-
return transformSpies(spyCall, refactorCtx); // .and.callThrough() is redundant, just transform spyOn.
178-
case 'stub': {
179-
reporter.reportTransformation(
180-
sourceFile,
181-
node,
182-
'Transformed `.and.stub()` to `.mockImplementation()`.',
183-
);
184-
const newExpression = createPropertyAccess(spyCall, 'mockImplementation');
185-
const arrowFn = ts.factory.createArrowFunction(
186-
undefined,
187-
undefined,
188-
[],
189-
undefined,
190-
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
191-
ts.factory.createBlock([], /* multiline */ true),
192-
);
99+
function transformSpyStrategy(node: ts.CallExpression, refactorCtx: RefactorContext): ts.Node {
100+
const { sourceFile, reporter } = refactorCtx;
101+
if (!ts.isPropertyAccessExpression(node.expression)) {
102+
return node;
103+
}
193104

194-
return ts.factory.createCallExpression(newExpression, undefined, [arrowFn]);
105+
const pae = node.expression;
106+
const isChainedWithAndProperty =
107+
(ts.isPropertyAccessExpression(pae.expression) &&
108+
ts.isIdentifier(pae.expression.name) &&
109+
pae.expression.name.text === 'and') ||
110+
(ts.isElementAccessExpression(pae.expression) &&
111+
ts.isStringLiteral(pae.expression.argumentExpression) &&
112+
pae.expression.argumentExpression.text === 'and');
113+
114+
if (isChainedWithAndProperty) {
115+
const spyCall = ts.isPropertyAccessExpression(pae.expression)
116+
? pae.expression.expression
117+
: (pae.expression as ts.ElementAccessExpression).expression;
118+
let newMethodName: string | undefined;
119+
let args = node.arguments;
120+
121+
if (ts.isIdentifier(pae.name)) {
122+
const strategyName = pae.name.text;
123+
switch (strategyName) {
124+
case 'returnValue':
125+
{
126+
const result = getPromiseResolveRejectMethod(args[0]);
127+
if (result) {
128+
const methodMapping = {
129+
'resolve': 'mockResolvedValue',
130+
'reject': 'mockRejectedValue',
131+
};
132+
newMethodName = methodMapping[result.methodName];
133+
args = result.arguments;
134+
} else {
135+
newMethodName = 'mockReturnValue';
136+
}
195137
}
196-
case 'throwError': {
197-
reporter.reportTransformation(
198-
sourceFile,
199-
node,
200-
'Transformed `.and.throwError()` to `.mockImplementation()`.',
201-
);
202-
const errorArg = node.arguments[0];
203-
const throwStatement = ts.factory.createThrowStatement(
204-
ts.isNewExpression(errorArg)
205-
? errorArg
206-
: ts.factory.createNewExpression(
207-
ts.factory.createIdentifier('Error'),
208-
undefined,
209-
node.arguments,
210-
),
211-
);
212-
const arrowFunction = ts.factory.createArrowFunction(
213-
undefined,
214-
undefined,
215-
[],
138+
break;
139+
case 'resolveTo':
140+
newMethodName = 'mockResolvedValue';
141+
break;
142+
case 'rejectWith':
143+
newMethodName = 'mockRejectedValue';
144+
break;
145+
case 'returnValues': {
146+
reporter.reportTransformation(
147+
sourceFile,
148+
node,
149+
'Transformed `.and.returnValues()` to chained `.mockReturnValueOnce()` calls.',
150+
);
151+
const returnValues = node.arguments;
152+
if (returnValues.length === 0) {
153+
// No values, so it's a no-op. Just transform the spyOn call.
154+
return transformSpies(spyCall, refactorCtx);
155+
}
156+
// spy.and.returnValues(a, b) -> spy.mockReturnValueOnce(a).mockReturnValueOnce(b)
157+
let chainedCall: ts.Expression = spyCall;
158+
for (const value of returnValues) {
159+
const mockCall = ts.factory.createCallExpression(
160+
createPropertyAccess(chainedCall, 'mockReturnValueOnce'),
216161
undefined,
217-
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
218-
ts.factory.createBlock([throwStatement], true),
162+
[value],
219163
);
220-
const newExpression = createPropertyAccess(spyCall, 'mockImplementation');
221-
222-
return ts.factory.createCallExpression(newExpression, undefined, [arrowFunction]);
164+
chainedCall = mockCall;
223165
}
224-
case 'identity': {
225-
reporter.reportTransformation(
226-
sourceFile,
227-
node,
228-
'Transformed `.and.identity()` to `.getMockName()`.',
229-
);
230-
const newExpression = createPropertyAccess(spyCall, 'getMockName');
231166

232-
return ts.factory.createCallExpression(newExpression, undefined, undefined);
233-
}
234-
default: {
235-
const category = 'unsupported-spy-strategy';
236-
reporter.recordTodo(category, sourceFile, node);
237-
addTodoComment(node, category, { name: strategyName });
238-
break;
239-
}
167+
return chainedCall;
240168
}
169+
case 'callFake':
170+
newMethodName = 'mockImplementation';
171+
break;
172+
case 'callThrough':
173+
reporter.reportTransformation(
174+
sourceFile,
175+
node,
176+
'Removed redundant `.and.callThrough()` call.',
177+
);
241178

242-
if (newMethodName) {
179+
return transformSpies(spyCall, refactorCtx); // .and.callThrough() is redundant, just transform spyOn.
180+
case 'stub': {
243181
reporter.reportTransformation(
244182
sourceFile,
245183
node,
246-
`Transformed spy strategy \`.and.${strategyName}()\` to \`.${newMethodName}()\`.`,
184+
'Transformed `.and.stub()` to `.mockImplementation()`.',
247185
);
186+
const newExpression = createPropertyAccess(spyCall, 'mockImplementation');
187+
const arrowFn = ts.factory.createArrowFunction(
188+
undefined,
189+
undefined,
190+
[],
191+
undefined,
192+
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
193+
ts.factory.createBlock([], /* multiline */ true),
194+
);
195+
196+
return ts.factory.createCallExpression(newExpression, undefined, [arrowFn]);
197+
}
198+
case 'throwError': {
199+
reporter.reportTransformation(
200+
sourceFile,
201+
node,
202+
'Transformed `.and.throwError()` to `.mockImplementation()`.',
203+
);
204+
const errorArg = node.arguments[0];
205+
const throwStatement = ts.factory.createThrowStatement(
206+
ts.isNewExpression(errorArg)
207+
? errorArg
208+
: ts.factory.createNewExpression(
209+
ts.factory.createIdentifier('Error'),
210+
undefined,
211+
node.arguments,
212+
),
213+
);
214+
const arrowFunction = ts.factory.createArrowFunction(
215+
undefined,
216+
undefined,
217+
[],
218+
undefined,
219+
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
220+
ts.factory.createBlock([throwStatement], true),
221+
);
222+
const newExpression = createPropertyAccess(spyCall, 'mockImplementation');
248223

249-
const newExpression = ts.factory.updatePropertyAccessExpression(
250-
pae,
251-
spyCall,
252-
ts.factory.createIdentifier(newMethodName),
224+
return ts.factory.createCallExpression(newExpression, undefined, [arrowFunction]);
225+
}
226+
case 'identity': {
227+
reporter.reportTransformation(
228+
sourceFile,
229+
node,
230+
'Transformed `.and.identity()` to `.getMockName()`.',
253231
);
232+
const newExpression = createPropertyAccess(spyCall, 'getMockName');
254233

255-
return ts.factory.updateCallExpression(node, newExpression, node.typeArguments, args);
234+
return ts.factory.createCallExpression(newExpression, undefined, undefined);
256235
}
236+
default: {
237+
const category = 'unsupported-spy-strategy';
238+
reporter.recordTodo(category, sourceFile, node);
239+
addTodoComment(node, category, { name: strategyName });
240+
break;
241+
}
242+
}
243+
244+
if (newMethodName) {
245+
reporter.reportTransformation(
246+
sourceFile,
247+
node,
248+
`Transformed spy strategy \`.and.${strategyName}()\` to \`.${newMethodName}()\`.`,
249+
);
250+
251+
const newExpression = ts.factory.updatePropertyAccessExpression(
252+
pae,
253+
spyCall,
254+
ts.factory.createIdentifier(newMethodName),
255+
);
256+
257+
return ts.factory.updateCallExpression(node, newExpression, node.typeArguments, args);
257258
}
258259
}
259260
}
260261

262+
return node;
263+
}
264+
265+
function transformSpyOnAllFunctions(
266+
node: ts.CallExpression,
267+
refactorCtx: RefactorContext,
268+
): ts.Node {
269+
const { sourceFile, reporter } = refactorCtx;
261270
if (getJasmineMethodName(node) === 'spyOnAllFunctions') {
262271
reporter.reportTransformation(
263272
sourceFile,
@@ -274,6 +283,24 @@ export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.
274283
return node;
275284
}
276285

286+
export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.Node {
287+
if (!ts.isCallExpression(node)) {
288+
return node;
289+
}
290+
291+
const primaryResult = transformPrimarySpy(node, refactorCtx);
292+
if (primaryResult !== node) {
293+
return primaryResult;
294+
}
295+
296+
const strategyResult = transformSpyStrategy(node, refactorCtx);
297+
if (strategyResult !== node) {
298+
return strategyResult;
299+
}
300+
301+
return transformSpyOnAllFunctions(node, refactorCtx);
302+
}
303+
277304
export function transformCreateSpy(node: ts.Node, ctx: RefactorContext): ts.Node {
278305
const { reporter, sourceFile, pendingVitestValueImports } = ctx;
279306
if (!isJasmineCallExpression(node, 'createSpy')) {

0 commit comments

Comments
 (0)