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 68a76b378194..7506cea81e0c 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy.ts @@ -72,20 +72,36 @@ function getUnwrappedParent(node: ts.Node): ts.Node | undefined { } function isChainedWithAnd(node: ts.Node): boolean { + let previous: ts.Node = node; let parent = node.parent; while (parent) { - if (ts.isPropertyAccessExpression(parent)) { - if (ts.isIdentifier(parent.name) && parent.name.text === 'and') { + if (ts.isPropertyAccessExpression(parent) && ts.isIdentifier(parent.name)) { + if (parent.name.text === 'and') { return true; } - } else if (ts.isElementAccessExpression(parent)) { - if ( - ts.isStringLiteralLike(parent.argumentExpression) && - parent.argumentExpression.text === 'and' - ) { + // `withArgs(...)` returns the spy, so a strategy may still follow. + if (parent.name.text === 'withArgs') { + previous = parent; + parent = parent.parent; + continue; + } + } else if ( + ts.isElementAccessExpression(parent) && + ts.isStringLiteralLike(parent.argumentExpression) + ) { + if (parent.argumentExpression.text === 'and') { return true; } - } else if (isTransparentWrapper(parent)) { + if (parent.argumentExpression.text === 'withArgs') { + previous = parent; + parent = parent.parent; + continue; + } + } else if ( + isTransparentWrapper(parent) || + (ts.isCallExpression(parent) && parent.expression === previous) + ) { + previous = parent; parent = parent.parent; continue; } @@ -95,6 +111,33 @@ function isChainedWithAnd(node: ts.Node): boolean { return false; } +/** + * Checks whether a spy expression chain contains a `withArgs(...)` link. + */ +function chainContainsWithArgs(expression: ts.Expression): boolean { + let current: ts.Expression = unwrapExpression(expression); + while (true) { + if (ts.isCallExpression(current)) { + current = unwrapExpression(current.expression); + } else if (ts.isPropertyAccessExpression(current)) { + if (ts.isIdentifier(current.name) && current.name.text === 'withArgs') { + return true; + } + current = unwrapExpression(current.expression); + } else if (ts.isElementAccessExpression(current)) { + if ( + ts.isStringLiteralLike(current.argumentExpression) && + current.argumentExpression.text === 'withArgs' + ) { + return true; + } + current = unwrapExpression(current.expression); + } else { + return false; + } + } +} + function isSpyCreation(node: ts.Expression): boolean { const expression = unwrapExpression(node); if (!ts.isCallExpression(expression)) { @@ -181,6 +224,19 @@ function transformSpyStrategy(node: ts.CallExpression, refactorCtx: RefactorCont } if (spyCall) { + // Vitest mocks have no `withArgs`, so a strategy for specific arguments cannot be converted. + // The whole statement is left untouched, with a single TODO on the outermost strategy call; + // inner `.and` chains reached while the visitor descends are covered by that TODO. + if (chainContainsWithArgs(spyCall)) { + if (!isChainedWithAnd(node)) { + const category = 'withArgs'; + reporter.recordTodo(category, sourceFile, node); + addTodoComment(node, category); + } + + return node; + } + let newMethodName: string | undefined; let args = node.arguments; 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 e14de03c3c17..5c82afd10c53 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 @@ -145,6 +145,36 @@ getSpy().and.callThrough();`, input: `spyOn(service, 'myMethod').and.unknownStrategy();`, expected: `// TODO: vitest-migration: Unsupported spy strategy ".and.unknownStrategy()" found. Please migrate this manually. See: https://vitest.dev/api/mocked.html#mock vi.spyOn(service, 'myMethod').and.unknownStrategy();`, + }, + { + description: 'should add a TODO for a spy strategy configured with .withArgs()', + input: `spyOn(service, 'myMethod').withArgs(1).and.returnValue(2);`, + expected: `// TODO: vitest-migration: Vitest does not have a direct equivalent for spy.withArgs(). Please migrate this manually, for example with mockImplementation() that checks the arguments. See: https://vitest.dev/api/mock.html#mockimplementation +vi.spyOn(service, 'myMethod').withArgs(1).and.returnValue(2);`, + }, + { + description: 'should add a single TODO for multiple chained .withArgs() strategies', + input: `mySpy.withArgs(1).and.returnValue(10).withArgs(2).and.returnValue(20);`, + expected: `// TODO: vitest-migration: Vitest does not have a direct equivalent for spy.withArgs(). Please migrate this manually, for example with mockImplementation() that checks the arguments. See: https://vitest.dev/api/mock.html#mockimplementation +mySpy.withArgs(1).and.returnValue(10).withArgs(2).and.returnValue(20);`, + }, + { + description: 'should not transform a default strategy chained after .withArgs()', + input: `mySpy.withArgs(1).and.returnValue(2).and.returnValue(0);`, + expected: `// TODO: vitest-migration: Vitest does not have a direct equivalent for spy.withArgs(). Please migrate this manually, for example with mockImplementation() that checks the arguments. See: https://vitest.dev/api/mock.html#mockimplementation +mySpy.withArgs(1).and.returnValue(2).and.returnValue(0);`, + }, + { + description: 'should add a TODO for .withArgs() on an existing spy', + input: `mySpy.withArgs(1).and.returnValue(2);`, + expected: `// TODO: vitest-migration: Vitest does not have a direct equivalent for spy.withArgs(). Please migrate this manually, for example with mockImplementation() that checks the arguments. See: https://vitest.dev/api/mock.html#mockimplementation +mySpy.withArgs(1).and.returnValue(2);`, + }, + { + description: 'should add a TODO for a wrapped .withArgs() call', + input: `mySpy.withArgs(1)!.and.returnValue(2);`, + expected: `// TODO: vitest-migration: Vitest does not have a direct equivalent for spy.withArgs(). Please migrate this manually, for example with mockImplementation() that checks the arguments. See: https://vitest.dev/api/mock.html#mockimplementation +mySpy.withArgs(1)!.and.returnValue(2);`, }, { description: 'should correctly identify chained spies with element access (bracket notation)', 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 0aa2a820698d..8b0db3cd45dc 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/utils/todo-notes.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/utils/todo-notes.ts @@ -165,6 +165,12 @@ export const TODO_NOTES = { ' the original implementation but also clears the recorded calls.', url: 'https://vitest.dev/api/mock.html#mockreset', }, + 'withArgs': { + message: + 'Vitest does not have a direct equivalent for spy.withArgs().' + + ' Please migrate this manually, for example with mockImplementation() that checks the arguments.', + url: 'https://vitest.dev/api/mock.html#mockimplementation', + }, 'mostRecent-without-args': { message: 'Direct usage of mostRecent() is not supported.' +