Skip to content

Commit 89e35f4

Browse files
fixup! fix(@schematics/angular): cast createSpyObj to MockedObject for jasmine.SpyObj variables in refactor-jasmine-vitest
Address review: use explicit type argument of createSpyObj, look into union types and keep an existing cast instead of adding a second one. Co-authored-by: Alan Agius <alanagius@google.com>
1 parent befdb9c commit 89e35f4

2 files changed

Lines changed: 73 additions & 4 deletions

File tree

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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -462,10 +462,20 @@ export function transformCreateSpyObj(node: ts.Node, ctx: RefactorContext): ts.N
462462

463463
const spyObjLiteral = ts.factory.createObjectLiteralExpression(properties, true);
464464

465+
let parent = node.parent;
466+
while (parent && ts.isParenthesizedExpression(parent)) {
467+
parent = parent.parent;
468+
}
469+
470+
// An existing cast on the call already gives the literal its type.
471+
if (parent && (ts.isAsExpression(parent) || ts.isTypeAssertionExpression(parent))) {
472+
return spyObjLiteral;
473+
}
474+
465475
// The literal only has the spied members, so it is not assignable to the `MockedObject<T>`
466476
// that a `jasmine.SpyObj<T>` annotation is transformed to. Jasmine's untyped `createSpyObj`
467477
// returns `any`, which is why the original assignment compiled.
468-
const spyObjTypeArguments = getAssignedSpyObjTypeArguments(node);
478+
const spyObjTypeArguments = node.typeArguments ?? getAssignedSpyObjTypeArguments(parent);
469479
if (!spyObjTypeArguments) {
470480
return spyObjLiteral;
471481
}
@@ -481,6 +491,18 @@ export function transformCreateSpyObj(node: ts.Node, ctx: RefactorContext): ts.N
481491
function getSpyObjTypeArguments(
482492
type: ts.TypeNode | undefined,
483493
): ts.NodeArray<ts.TypeNode> | undefined {
494+
// `jasmine.SpyObj<T> | undefined` under `strictNullChecks`.
495+
if (type && ts.isUnionTypeNode(type)) {
496+
for (const member of type.types) {
497+
const typeArguments = getSpyObjTypeArguments(member);
498+
if (typeArguments) {
499+
return typeArguments;
500+
}
501+
}
502+
503+
return undefined;
504+
}
505+
484506
if (
485507
type &&
486508
ts.isTypeReferenceNode(type) &&
@@ -527,12 +549,12 @@ function findEnclosingVariableDeclaration(
527549

528550
/**
529551
* Finds the `T` of a `jasmine.SpyObj<T>` annotation on the variable that a `createSpyObj` call
530-
* initializes (`const spy: jasmine.SpyObj<T> = ...`) or is assigned to (`spy = ...`).
552+
* initializes (`const spy: jasmine.SpyObj<T> = ...`) or is assigned to (`spy = ...`), given the
553+
* call's parent outside any parentheses.
531554
*/
532555
function getAssignedSpyObjTypeArguments(
533-
node: ts.CallExpression,
556+
parent: ts.Node | undefined,
534557
): ts.NodeArray<ts.TypeNode> | undefined {
535-
const parent = getUnwrappedParent(node);
536558
if (!parent) {
537559
return undefined;
538560
}

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,53 @@ describe('transformCreateSpyObj', () => {
214214
} as MockedObject<MyService>;
215215
`,
216216
},
217+
{
218+
description:
219+
'should cast jasmine.createSpyObj to MockedObject when assigned to a jasmine.SpyObj union variable',
220+
input: `
221+
let myService: jasmine.SpyObj<MyService> | undefined;
222+
myService = jasmine.createSpyObj('MyService', ['methodA']);
223+
`,
224+
expected: `
225+
import type { MockedObject } from 'vitest';
226+
227+
let myService: MockedObject<MyService> | undefined;
228+
myService = {
229+
methodA: vi.fn().mockName('MyService.methodA'),
230+
} as MockedObject<MyService>;
231+
`,
232+
},
233+
{
234+
description:
235+
'should cast jasmine.createSpyObj with an explicit type argument to MockedObject',
236+
input: `const myService = jasmine.createSpyObj<MyService>('MyService', ['methodA']);`,
237+
expected: `
238+
import type { MockedObject } from 'vitest';
239+
const myService = {
240+
methodA: vi.fn().mockName('MyService.methodA'),
241+
} as MockedObject<MyService>;
242+
`,
243+
},
244+
{
245+
description:
246+
'should not add a second cast to a jasmine.createSpyObj call that is already cast',
247+
input: `
248+
let myService: jasmine.SpyObj<MyService>;
249+
myService = jasmine.createSpyObj('MyService', ['methodA']) as jasmine.SpyObj<MyService>;
250+
const other = jasmine.createSpyObj<Other>('Other', ['methodB']) as any;
251+
`,
252+
expected: `
253+
import type { MockedObject } from 'vitest';
254+
255+
let myService: MockedObject<MyService>;
256+
myService = {
257+
methodA: vi.fn().mockName('MyService.methodA'),
258+
} as MockedObject<MyService>;
259+
const other = {
260+
methodB: vi.fn().mockName('Other.methodB'),
261+
} as any;
262+
`,
263+
},
217264
{
218265
description:
219266
'should cast jasmine.createSpyObj to the jasmine.SpyObj type of the variable in the enclosing describe block',

0 commit comments

Comments
 (0)