From 953f921ce2ba4c1f4a3f30663e9bf36e5ff90d01 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 10 Oct 2025 11:41:49 -0400 Subject: [PATCH] feat(@schematics/angular): add Jasmine spy API transformations to jasmine-to-vitest schematic This commit adds transformers for Jasmine's spying functionality (https://jasmine.github.io/tutorials/spying_on_properties). Coverage includes: - spyOn, spyOnProperty, jasmine.createSpy, and jasmine.createSpyObj - Spy strategies (and.returnValue, and.callFake, etc.) are mapped to Vitest's 'mock*' equivalents. - Inspection of spy calls (spy.calls.reset, spy.calls.mostRecent). --- .../jasmine-vitest/test-file-transformer.ts | 14 +- .../transformers/jasmine-spy.ts | 449 ++++++++++++++++++ .../transformers/jasmine-spy_spec.ts | 263 ++++++++++ 3 files changed, 724 insertions(+), 2 deletions(-) create mode 100644 packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy.ts create mode 100644 packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy_spec.ts diff --git a/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.ts b/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.ts index 434e50b670bf..816c0f6326da 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.ts @@ -24,6 +24,12 @@ import { transformWithContext, transformtoHaveBeenCalledBefore, } from './transformers/jasmine-matcher'; +import { + transformCreateSpyObj, + transformSpies, + transformSpyCallInspection, + transformSpyReset, +} from './transformers/jasmine-spy'; import { RefactorContext } from './utils/refactor-context'; import { RefactorReporter } from './utils/refactor-reporter'; @@ -65,6 +71,11 @@ export function transformJasmineToVitest( transformSyntacticSugarMatchers, transformFocusedAndSkippedTests, transformComplexMatchers, + transformSpies, + transformCreateSpyObj, + transformSpyReset, + transformFocusedAndSkippedTests, + transformSpyCallInspection, transformPending, transformDoneCallback, transformtoHaveBeenCalledBefore, @@ -75,8 +86,7 @@ export function transformJasmineToVitest( transformedNode = transformer(transformedNode, refactorCtx); } } else if (ts.isPropertyAccessExpression(transformedNode)) { - const transformations = [transformAsymmetricMatchers]; - + const transformations = [transformAsymmetricMatchers, transformSpyCallInspection]; for (const transformer of transformations) { transformedNode = transformer(transformedNode, refactorCtx); } diff --git a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy.ts b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy.ts new file mode 100644 index 000000000000..2d13a7474104 --- /dev/null +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy.ts @@ -0,0 +1,449 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +/** + * @fileoverview This file contains transformers dedicated to converting Jasmine's spying + * functionality to Vitest's mocking APIs. It handles the creation of spies (`spyOn`, + * `createSpy`, `createSpyObj`), spy strategies (`and.returnValue`, `and.callFake`), + * and the inspection of spy calls (`spy.calls.reset`, `spy.calls.mostRecent`). + */ + +import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript'; +import { createPropertyAccess, createViCallExpression } from '../utils/ast-helpers'; +import { getJasmineMethodName, isJasmineCallExpression } from '../utils/ast-validation'; +import { addTodoComment } from '../utils/comment-helpers'; +import { RefactorContext } from '../utils/refactor-context'; + +export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.Node { + const { sourceFile, reporter } = refactorCtx; + if (!ts.isCallExpression(node)) { + return node; + } + + if ( + ts.isIdentifier(node.expression) && + (node.expression.text === 'spyOn' || node.expression.text === 'spyOnProperty') + ) { + reporter.reportTransformation( + sourceFile, + node, + `Transformed \`${node.expression.text}\` to \`vi.spyOn\`.`, + ); + + return ts.factory.updateCallExpression( + node, + createPropertyAccess('vi', 'spyOn'), + node.typeArguments, + node.arguments, + ); + } + + if (ts.isPropertyAccessExpression(node.expression)) { + const pae = node.expression; + + if ( + ts.isPropertyAccessExpression(pae.expression) && + ts.isIdentifier(pae.expression.name) && + pae.expression.name.text === 'and' + ) { + const spyCall = pae.expression.expression; + let newMethodName: string | undefined; + if (ts.isIdentifier(pae.name)) { + const strategyName = pae.name.text; + switch (strategyName) { + case 'returnValue': + newMethodName = 'mockReturnValue'; + break; + case 'resolveTo': + newMethodName = 'mockResolvedValue'; + break; + case 'rejectWith': + newMethodName = 'mockRejectedValue'; + break; + case 'returnValues': { + reporter.reportTransformation( + sourceFile, + node, + 'Transformed `.and.returnValues()` to chained `.mockReturnValueOnce()` calls.', + ); + const returnValues = node.arguments; + if (returnValues.length === 0) { + // No values, so it's a no-op. Just transform the spyOn call. + return transformSpies(spyCall, refactorCtx); + } + // spy.and.returnValues(a, b) -> spy.mockReturnValueOnce(a).mockReturnValueOnce(b) + let chainedCall: ts.Expression = spyCall; + for (const value of returnValues) { + const mockCall = ts.factory.createCallExpression( + createPropertyAccess(chainedCall, 'mockReturnValueOnce'), + undefined, + [value], + ); + chainedCall = mockCall; + } + + return chainedCall; + } + case 'callFake': + newMethodName = 'mockImplementation'; + break; + case 'callThrough': + reporter.reportTransformation( + sourceFile, + node, + 'Removed redundant `.and.callThrough()` call.', + ); + + return transformSpies(spyCall, refactorCtx); // .and.callThrough() is redundant, just transform spyOn. + case 'stub': { + reporter.reportTransformation( + sourceFile, + node, + 'Transformed `.and.stub()` to `.mockImplementation()`.', + ); + const newExpression = createPropertyAccess(spyCall, 'mockImplementation'); + const arrowFn = ts.factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createBlock([], /* multiline */ true), + ); + + return ts.factory.createCallExpression(newExpression, undefined, [arrowFn]); + } + case 'throwError': { + reporter.reportTransformation( + sourceFile, + node, + 'Transformed `.and.throwError()` to `.mockImplementation()`.', + ); + const errorArg = node.arguments[0]; + const throwStatement = ts.factory.createThrowStatement( + ts.isNewExpression(errorArg) + ? errorArg + : ts.factory.createNewExpression( + ts.factory.createIdentifier('Error'), + undefined, + node.arguments, + ), + ); + const arrowFunction = ts.factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createBlock([throwStatement], true), + ); + const newExpression = createPropertyAccess(spyCall, 'mockImplementation'); + + return ts.factory.createCallExpression(newExpression, undefined, [arrowFunction]); + } + default: + reporter.recordTodo('unsupported-spy-strategy'); + addTodoComment( + node, + `Unsupported spy strategy ".and.${strategyName}()" found. Please migrate this manually.`, + ); + } + + if (newMethodName) { + reporter.reportTransformation( + sourceFile, + node, + `Transformed spy strategy \`.and.${strategyName}()\` to \`.${newMethodName}()\`.`, + ); + const newExpression = createPropertyAccess(spyCall, newMethodName); + + return ts.factory.updateCallExpression( + node, + newExpression, + node.typeArguments, + node.arguments, + ); + } + } + } + } + + const jasmineMethodName = getJasmineMethodName(node); + switch (jasmineMethodName) { + case 'createSpy': + reporter.reportTransformation( + sourceFile, + node, + 'Transformed `jasmine.createSpy()` to `vi.fn()`.', + ); + + // jasmine.createSpy(name, originalFn) -> vi.fn(originalFn) + return createViCallExpression('fn', node.arguments.length > 1 ? [node.arguments[1]] : []); + case 'spyOnAllFunctions': + reporter.reportTransformation( + sourceFile, + node, + 'Found unsupported `jasmine.spyOnAllFunctions()`.', + ); + reporter.recordTodo('spyOnAllFunctions'); + addTodoComment( + node, + 'Vitest does not have a direct equivalent for jasmine.spyOnAllFunctions().' + + ' Please spy on individual methods manually using vi.spyOn().', + ); + + return node; + } + + return node; +} + +export function transformCreateSpyObj( + node: ts.Node, + { sourceFile, reporter }: RefactorContext, +): ts.Node { + if (!isJasmineCallExpression(node, 'createSpyObj')) { + return node; + } + + reporter.reportTransformation( + sourceFile, + node, + 'Transformed `jasmine.createSpyObj()` to an object literal with `vi.fn()`.', + ); + + if (node.arguments.length < 2) { + reporter.recordTodo('createSpyObj-single-argument'); + addTodoComment( + node, + 'jasmine.createSpyObj called with a single argument is not supported for transformation.', + ); + + return node; + } + + const methods = node.arguments[1]; + const propertiesArg = node.arguments[2]; + let properties: ts.PropertyAssignment[] = []; + + if (ts.isArrayLiteralExpression(methods)) { + properties = createSpyObjWithArray(methods); + } else if (ts.isObjectLiteralExpression(methods)) { + properties = createSpyObjWithObject(methods); + } else { + reporter.recordTodo('createSpyObj-dynamic-variable'); + addTodoComment( + node, + 'Cannot transform jasmine.createSpyObj with a dynamic variable. Please migrate this manually.', + ); + + return node; + } + + if (propertiesArg) { + if (ts.isObjectLiteralExpression(propertiesArg)) { + properties.push(...(propertiesArg.properties as unknown as ts.PropertyAssignment[])); + } else { + reporter.recordTodo('createSpyObj-dynamic-property-map'); + addTodoComment( + node, + 'Cannot transform jasmine.createSpyObj with a dynamic property map. Please migrate this manually.', + ); + } + } + + return ts.factory.createObjectLiteralExpression(properties, true); +} + +function createSpyObjWithArray(methods: ts.ArrayLiteralExpression): ts.PropertyAssignment[] { + return methods.elements + .map((element) => { + if (ts.isStringLiteral(element)) { + return ts.factory.createPropertyAssignment( + ts.factory.createIdentifier(element.text), + createViCallExpression('fn'), + ); + } + + return undefined; + }) + .filter((p): p is ts.PropertyAssignment => !!p); +} + +function createSpyObjWithObject(methods: ts.ObjectLiteralExpression): ts.PropertyAssignment[] { + return methods.properties + .map((prop) => { + if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) { + const methodName = prop.name.text; + const returnValue = prop.initializer; + const mockFn = createViCallExpression('fn'); + const mockReturnValue = createPropertyAccess(mockFn, 'mockReturnValue'); + + return ts.factory.createPropertyAssignment( + ts.factory.createIdentifier(methodName), + ts.factory.createCallExpression(mockReturnValue, undefined, [returnValue]), + ); + } + + return undefined; + }) + .filter((p): p is ts.PropertyAssignment => !!p); +} + +export function transformSpyReset( + node: ts.Node, + { sourceFile, reporter }: RefactorContext, +): ts.Node { + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + ts.isIdentifier(node.expression.name) && + node.expression.name.text === 'reset' && + ts.isPropertyAccessExpression(node.expression.expression) + ) { + const callsPae = node.expression.expression; + if (ts.isIdentifier(callsPae.name) && callsPae.name.text === 'calls') { + reporter.reportTransformation( + sourceFile, + node, + 'Transformed `spy.calls.reset()` to `.mockClear()`.', + ); + const spyIdentifier = callsPae.expression; + const newExpression = createPropertyAccess(spyIdentifier, 'mockClear'); + + return ts.factory.updateCallExpression(node, newExpression, node.typeArguments, []); + } + } + + return node; +} + +function getSpyIdentifierFromCalls(node: ts.PropertyAccessExpression): ts.Expression | undefined { + if (ts.isIdentifier(node.name) && node.name.text === 'calls') { + return node.expression; + } + + return undefined; +} + +function createMockedSpyMockProperty(spyIdentifier: ts.Expression): ts.PropertyAccessExpression { + const mockedSpy = ts.factory.createCallExpression( + createPropertyAccess('vi', 'mocked'), + undefined, + [spyIdentifier], + ); + + return createPropertyAccess(mockedSpy, 'mock'); +} + +export function transformSpyCallInspection( + node: ts.Node, + { sourceFile, reporter }: RefactorContext, +): ts.Node { + // mySpy.calls.mostRecent().args -> vi.mocked(mySpy).mock.lastCall + if ( + ts.isPropertyAccessExpression(node) && + ts.isIdentifier(node.name) && + node.name.text === 'args' + ) { + const mostRecentCall = node.expression; + if ( + ts.isCallExpression(mostRecentCall) && + ts.isPropertyAccessExpression(mostRecentCall.expression) + ) { + const mostRecentPae = mostRecentCall.expression; // mySpy.calls.mostRecent + if ( + ts.isIdentifier(mostRecentPae.name) && + mostRecentPae.name.text === 'mostRecent' && + ts.isPropertyAccessExpression(mostRecentPae.expression) + ) { + const spyIdentifier = getSpyIdentifierFromCalls(mostRecentPae.expression); + if (spyIdentifier) { + reporter.reportTransformation( + sourceFile, + node, + 'Transformed `spy.calls.mostRecent().args` to `vi.mocked(spy).mock.lastCall`.', + ); + const mockProperty = createMockedSpyMockProperty(spyIdentifier); + + return createPropertyAccess(mockProperty, 'lastCall'); + } + } + } + } + + if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) { + const pae = node.expression; // e.g., mySpy.calls.count + const spyIdentifier = ts.isPropertyAccessExpression(pae.expression) + ? getSpyIdentifierFromCalls(pae.expression) + : undefined; + + if (spyIdentifier) { + const mockProperty = createMockedSpyMockProperty(spyIdentifier); + const callsProperty = createPropertyAccess(mockProperty, 'calls'); + + const callName = pae.name.text; + let newExpression: ts.Node | undefined; + let message: string | undefined; + + switch (callName) { + case 'any': + message = 'Transformed `spy.calls.any()` to a check on `mock.calls.length`.'; + newExpression = ts.factory.createBinaryExpression( + createPropertyAccess(callsProperty, 'length'), + ts.SyntaxKind.GreaterThanToken, + ts.factory.createNumericLiteral(0), + ); + break; + case 'count': + message = 'Transformed `spy.calls.count()` to `mock.calls.length`.'; + newExpression = createPropertyAccess(callsProperty, 'length'); + break; + case 'first': + message = 'Transformed `spy.calls.first()` to `mock.calls[0]`.'; + newExpression = ts.factory.createElementAccessExpression(callsProperty, 0); + break; + case 'all': + case 'allArgs': + message = `Transformed \`spy.calls.${callName}()\` to \`mock.calls\`.`; + newExpression = callsProperty; + break; + case 'argsFor': + message = 'Transformed `spy.calls.argsFor()` to `mock.calls[i]`.'; + newExpression = ts.factory.createElementAccessExpression( + callsProperty, + node.arguments[0], + ); + break; + case 'mostRecent': + if ( + !ts.isPropertyAccessExpression(node.parent) || + !ts.isIdentifier(node.parent.name) || + node.parent.name.text !== 'args' + ) { + reporter.recordTodo('mostRecent-without-args'); + addTodoComment( + node, + 'Direct usage of mostRecent() is not supported.' + + ' Please refactor to access .args directly or use vi.mocked(spy).mock.lastCall.', + ); + } + + return node; + } + + if (newExpression && message) { + reporter.reportTransformation(sourceFile, node, message); + + return newExpression; + } + } + } + + return 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 new file mode 100644 index 000000000000..d54f2c271252 --- /dev/null +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy_spec.ts @@ -0,0 +1,263 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { logging } from '@angular-devkit/core'; +import { format } from 'prettier'; +import { transformJasmineToVitest } from '../test-file-transformer'; +import { RefactorReporter } from '../utils/refactor-reporter'; + +async function expectTransformation(input: string, expected: string): Promise { + const logger = new logging.NullLogger(); + const reporter = new RefactorReporter(logger); + const transformed = transformJasmineToVitest('spec.ts', input, reporter); + const formattedTransformed = await format(transformed, { parser: 'typescript' }); + const formattedExpected = await format(expected, { parser: 'typescript' }); + + expect(formattedTransformed).toBe(formattedExpected); +} + +describe('Jasmine to Vitest Transformer', () => { + describe('transformSpies', () => { + const testCases = [ + { + description: 'should transform spyOn(object, "method") to vi.spyOn(object, "method")', + input: `spyOn(service, 'myMethod');`, + expected: `vi.spyOn(service, 'myMethod');`, + }, + { + description: 'should transform .and.returnValue(...) to .mockReturnValue(...)', + input: `spyOn(service, 'myMethod').and.returnValue(42);`, + expected: `vi.spyOn(service, 'myMethod').mockReturnValue(42);`, + }, + { + description: 'should transform .and.returnValues() to chained .mockReturnValueOnce() calls', + input: `spyOn(service, 'myMethod').and.returnValues('a', 'b', 'c');`, + expected: `vi.spyOn(service, 'myMethod').mockReturnValueOnce('a').mockReturnValueOnce('b').mockReturnValueOnce('c');`, + }, + { + description: 'should transform .and.callFake(...) to .mockImplementation(...)', + input: `spyOn(service, 'myMethod').and.callFake(() => 'fake');`, + expected: `vi.spyOn(service, 'myMethod').mockImplementation(() => 'fake');`, + }, + { + description: 'should remove .and.callThrough()', + input: `spyOn(service, 'myMethod').and.callThrough();`, + expected: `vi.spyOn(service, 'myMethod');`, + }, + { + description: 'should transform jasmine.createSpy("name") to vi.fn()', + input: `const mySpy = jasmine.createSpy('mySpy');`, + expected: `const mySpy = vi.fn();`, + }, + { + description: 'should transform jasmine.createSpy("name", fn) to vi.fn(fn)', + input: `const mySpy = jasmine.createSpy('mySpy', () => 'foo');`, + expected: `const mySpy = vi.fn(() => 'foo');`, + }, + { + description: 'should transform spyOnProperty(object, "prop") to vi.spyOn(object, "prop")', + input: `spyOnProperty(service, 'myProp');`, + expected: `vi.spyOn(service, 'myProp');`, + }, + { + description: 'should transform .and.stub() to .mockImplementation(() => {})', + input: `spyOn(service, 'myMethod').and.stub();`, + expected: `vi.spyOn(service, 'myMethod').mockImplementation(() => {});`, + }, + { + description: 'should add a TODO for jasmine.spyOnAllFunctions(object)', + input: `jasmine.spyOnAllFunctions(myObject);`, + // eslint-disable-next-line max-len + expected: `// TODO: vitest-migration: Vitest does not have a direct equivalent for jasmine.spyOnAllFunctions(). Please spy on individual methods manually using vi.spyOn(). + jasmine.spyOnAllFunctions(myObject); + `, + }, + { + description: 'should handle chained calls on jasmine.createSpy()', + input: `const mySpy = jasmine.createSpy('mySpy').and.returnValue(true);`, + expected: `const mySpy = vi.fn().mockReturnValue(true);`, + }, + { + description: 'should handle .and.returnValues() with no arguments', + input: `spyOn(service, 'myMethod').and.returnValues();`, + expected: `vi.spyOn(service, 'myMethod');`, + }, + { + description: + 'should transform .and.throwError("message") to .mockImplementation(() => { throw new Error("message") })', + input: `spyOn(service, 'myMethod').and.throwError('Something went wrong');`, + expected: `vi.spyOn(service, 'myMethod').mockImplementation(() => { throw new Error('Something went wrong') });`, + }, + { + description: + 'should transform .and.throwError(new Error("message")) to .mockImplementation(() => { throw new Error("message") })', + input: `spyOn(service, 'myMethod').and.throwError(new Error('Custom Error'));`, + expected: `vi.spyOn(service, 'myMethod').mockImplementation(() => { throw new Error('Custom Error') });`, + }, + { + description: 'should transform .and.resolveTo(value) to .mockResolvedValue(value)', + input: `spyOn(service, 'myMethod').and.resolveTo('some value');`, + expected: `vi.spyOn(service, 'myMethod').mockResolvedValue('some value');`, + }, + { + description: 'should transform .and.rejectWith(error) to .mockRejectedValue(error)', + input: `spyOn(service, 'myMethod').and.rejectWith('some error');`, + expected: `vi.spyOn(service, 'myMethod').mockRejectedValue('some error');`, + }, + ]; + + testCases.forEach(({ description, input, expected }) => { + it(description, async () => { + await expectTransformation(input, expected); + }); + }); + }); + + describe('transformCreateSpyObj', () => { + const testCases = [ + { + description: 'should transform jasmine.createSpyObj with an array of methods', + input: `const myService = jasmine.createSpyObj('MyService', ['methodA', 'methodB']);`, + expected: `const myService = { + methodA: vi.fn(), + methodB: vi.fn() + };`, + }, + { + description: 'should add a TODO if the second argument is not a literal', + input: `const myService = jasmine.createSpyObj('MyService', methodNames);`, + expected: ` + // TODO: vitest-migration: Cannot transform jasmine.createSpyObj with a dynamic variable. Please migrate this manually. + const myService = jasmine.createSpyObj('MyService', methodNames); + `, + }, + { + description: 'should transform jasmine.createSpyObj with an object of return values', + input: `const myService = jasmine.createSpyObj('MyService', { methodA: 'foo', methodB: 42 });`, + expected: `const myService = { + methodA: vi.fn().mockReturnValue('foo'), + methodB: vi.fn().mockReturnValue(42) + };`, + }, + { + description: + 'should transform jasmine.createSpyObj with an object of return values containing an asymmetric matcher', + input: `const myService = jasmine.createSpyObj('MyService', { methodA: jasmine.any(String) });`, + expected: `const myService = { + methodA: vi.fn().mockReturnValue(expect.any(String)) + };`, + }, + { + description: 'should add a TODO for jasmine.createSpyObj with only one argument', + input: `const myService = jasmine.createSpyObj('MyService');`, + expected: ` + // TODO: vitest-migration: jasmine.createSpyObj called with a single argument is not supported for transformation. + const myService = jasmine.createSpyObj('MyService'); + `, + }, + { + description: 'should transform jasmine.createSpyObj with a property map', + input: `const myService = jasmine.createSpyObj('MyService', ['methodA'], { propA: 'valueA' });`, + expected: `const myService = { + methodA: vi.fn(), + propA: 'valueA' + };`, + }, + { + description: 'should transform jasmine.createSpyObj with a method map and a property map', + input: `const myService = jasmine.createSpyObj('MyService', { methodA: 'foo' }, { propA: 'valueA' });`, + expected: `const myService = { + methodA: vi.fn().mockReturnValue('foo'), + propA: 'valueA' + };`, + }, + { + description: 'should ignore non-string literals in the method array', + input: `const myService = jasmine.createSpyObj('MyService', ['methodA', 123, someVar]);`, + expected: `const myService = { + methodA: vi.fn() + };`, + }, + ]; + + testCases.forEach(({ description, input, expected }) => { + it(description, async () => { + await expectTransformation(input, expected); + }); + }); + }); + + describe('transformSpyReset', () => { + const testCases = [ + { + description: 'should transform spy.calls.reset() to spy.mockClear()', + input: `mySpy.calls.reset();`, + expected: `mySpy.mockClear();`, + }, + ]; + + testCases.forEach(({ description, input, expected }) => { + it(description, async () => { + await expectTransformation(input, expected); + }); + }); + }); + + describe('transformSpyCallInspection', () => { + const testCases = [ + { + description: 'should transform spy.calls.any()', + input: `expect(mySpy.calls.any()).toBe(true);`, + expected: `expect(vi.mocked(mySpy).mock.calls.length > 0).toBe(true);`, + }, + { + description: 'should transform spy.calls.count()', + input: `expect(mySpy.calls.count()).toBe(1);`, + expected: `expect(vi.mocked(mySpy).mock.calls.length).toBe(1);`, + }, + { + description: 'should transform spy.calls.argsFor(0)', + input: `const args = mySpy.calls.argsFor(0);`, + expected: `const args = vi.mocked(mySpy).mock.calls[0];`, + }, + { + description: 'should transform spy.calls.allArgs()', + input: `const allArgs = mySpy.calls.allArgs();`, + expected: `const allArgs = vi.mocked(mySpy).mock.calls;`, + }, + { + description: 'should transform spy.calls.all()', + input: `const allCalls = mySpy.calls.all();`, + expected: `const allCalls = vi.mocked(mySpy).mock.calls;`, + }, + { + description: 'should transform spy.calls.mostRecent().args', + input: `const recentArgs = mySpy.calls.mostRecent().args;`, + expected: `const recentArgs = vi.mocked(mySpy).mock.lastCall;`, + }, + { + description: 'should transform spy.calls.first()', + input: `const firstCall = mySpy.calls.first();`, + expected: `const firstCall = vi.mocked(mySpy).mock.calls[0];`, + }, + { + description: 'should add a TODO for spy.calls.mostRecent() without .args', + input: `const mostRecent = mySpy.calls.mostRecent();`, + // eslint-disable-next-line max-len + expected: `// TODO: vitest-migration: Direct usage of mostRecent() is not supported. Please refactor to access .args directly or use vi.mocked(spy).mock.lastCall. +const mostRecent = mySpy.calls.mostRecent();`, + }, + ]; + + testCases.forEach(({ description, input, expected }) => { + it(description, async () => { + await expectTransformation(input, expected); + }); + }); + }); +});