diff --git a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy.ts b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy.ts index 6ac2a12c41f0..68a76b378194 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy.ts @@ -95,6 +95,19 @@ function isChainedWithAnd(node: ts.Node): boolean { return false; } +function isSpyCreation(node: ts.Expression): boolean { + const expression = unwrapExpression(node); + if (!ts.isCallExpression(expression)) { + return false; + } + + if (ts.isIdentifier(expression.expression)) { + return expression.expression.text === 'spyOn' || expression.expression.text === 'spyOnProperty'; + } + + return isJasmineCallExpression(expression, 'createSpy'); +} + function transformPrimarySpy(node: ts.CallExpression, refactorCtx: RefactorContext): ts.Node { const { sourceFile, reporter, pendingVitestValueImports } = refactorCtx; if ( @@ -224,6 +237,16 @@ function transformSpyStrategy(node: ts.CallExpression, refactorCtx: RefactorCont newMethodName = 'mockImplementation'; break; case 'callThrough': + if (!isSpyCreation(spyCall)) { + // On an existing spy, `.and.callThrough()` restores the original implementation, + // so it is not redundant and cannot be dropped. + const category = 'callThrough-on-existing-spy'; + reporter.recordTodo(category, sourceFile, node); + addTodoComment(node, category); + + return node; + } + reporter.reportTransformation( sourceFile, node, diff --git a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy_spec.ts b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy_spec.ts index 27b4655b5c4a..e14de03c3c17 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy_spec.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy_spec.ts @@ -48,6 +48,23 @@ describe('Jasmine to Vitest Transformer - transformSpies', () => { input: `spyOn(service, 'myMethod').and.callThrough();`, expected: `vi.spyOn(service, 'myMethod');`, }, + { + description: 'should remove .and.callThrough() on a wrapped spyOn call', + input: `(spyOn(service, 'myMethod')!).and.callThrough();`, + expected: `(vi.spyOn(service, 'myMethod')!);`, + }, + { + description: 'should add a TODO for .and.callThrough() on an existing spy', + input: `mySpy.and.callThrough();`, + expected: `// TODO: vitest-migration: Vitest does not have a direct equivalent for .and.callThrough() on an existing spy. Please migrate this manually, for example with spy.mockReset(), which restores the original implementation but also clears the recorded calls. See: https://vitest.dev/api/mock.html#mockreset +mySpy.and.callThrough();`, + }, + { + description: 'should add a TODO for .and.callThrough() on a spy returned by a call', + input: `getSpy().and.callThrough();`, + expected: `// TODO: vitest-migration: Vitest does not have a direct equivalent for .and.callThrough() on an existing spy. Please migrate this manually, for example with spy.mockReset(), which restores the original implementation but also clears the recorded calls. See: https://vitest.dev/api/mock.html#mockreset +getSpy().and.callThrough();`, + }, { description: 'should transform jasmine.createSpy("name") to vi.fn().mockName("name")', input: `const mySpy = jasmine.createSpy('mySpy');`, diff --git a/packages/schematics/angular/refactor/jasmine-vitest/utils/todo-notes.ts b/packages/schematics/angular/refactor/jasmine-vitest/utils/todo-notes.ts index c22e9affedc7..0aa2a820698d 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/utils/todo-notes.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/utils/todo-notes.ts @@ -158,6 +158,13 @@ export const TODO_NOTES = { `Unsupported spy strategy ".and.${context.name}()" found. Please migrate this manually.`, url: 'https://vitest.dev/api/mocked.html#mock', }, + 'callThrough-on-existing-spy': { + message: + 'Vitest does not have a direct equivalent for .and.callThrough() on an existing spy.' + + ' Please migrate this manually, for example with spy.mockReset(), which restores' + + ' the original implementation but also clears the recorded calls.', + url: 'https://vitest.dev/api/mock.html#mockreset', + }, 'mostRecent-without-args': { message: 'Direct usage of mostRecent() is not supported.' +