diff --git a/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.integration_spec.ts b/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.integration_spec.ts index ebc7e1631907..8944e34dfa14 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.integration_spec.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.integration_spec.ts @@ -255,14 +255,15 @@ describe('Jasmine to Vitest Transformer - Integration Tests', () => { }); `; + /* eslint-disable max-len */ const vitestCode = ` describe('Complex Scenarios', () => { let serviceMock; beforeEach(() => { serviceMock = { - getData: vi.fn().mockReturnValue(expect.any(String)), - process: vi.fn().mockReturnValue(undefined), + getData: vi.fn().mockName("MyService.getData").mockReturnValue(expect.any(String)), + process: vi.fn().mockName("MyService.process").mockReturnValue(undefined), }; }); @@ -274,6 +275,7 @@ describe('Jasmine to Vitest Transformer - Integration Tests', () => { it('should handle array contents checking', () => { const arr = [1, 2, 3]; + // TODO: vitest-migration: Verify this matches strict array content (multiset equality). Vitest's arrayContaining is a subset check. expect(arr).toHaveLength(3); expect(arr).toEqual(expect.arrayContaining([3, 2, 1])); }); @@ -299,6 +301,7 @@ describe('Jasmine to Vitest Transformer - Integration Tests', () => { }); }); `; + /* eslint-enable max-len */ await expectTransformation(jasmineCode, vitestCode); }); diff --git a/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer_spec.ts b/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer_spec.ts index bc55811306b3..abe1f3655cdd 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer_spec.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer_spec.ts @@ -78,17 +78,20 @@ describe('Jasmine to Vitest Transformer', () => { input: `const spy = jasmine.createSpyObj('MyService', { getPromise: Promise.resolve(jasmine.any(String)) });`, expected: ` const spy = { - getPromise: vi.fn().mockReturnValue(Promise.resolve(expect.any(String))), + getPromise: vi.fn().mockName("MyService.getPromise").mockReturnValue(Promise.resolve(expect.any(String))), }; `, }, { description: 'should handle arrayWithExactContents containing nested asymmetric matchers', input: `expect(myArray).toEqual(jasmine.arrayWithExactContents([jasmine.objectContaining({ id: 1 })]));`, + /* eslint-disable max-len */ expected: ` + // TODO: vitest-migration: Verify this matches strict array content (multiset equality). Vitest's arrayContaining is a subset check. expect(myArray).toHaveLength(1); expect(myArray).toEqual(expect.arrayContaining([expect.objectContaining({ id: 1 })])); `, + /* eslint-enable max-len */ }, { description: 'should handle a spy rejecting with an asymmetric matcher', @@ -105,8 +108,8 @@ describe('Jasmine to Vitest Transformer', () => { `, expected: ` const myService = { - methodA: vi.fn(), - propA: 'valueA' + methodA: vi.fn().mockName("MyService.methodA"), + propA: 'valueA', }; vi.spyOn(myService, 'methodA').mockReturnValue('mocked value'); myService.methodA('test'); diff --git a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-lifecycle.ts b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-lifecycle.ts index 235eef129d11..f2b60c35f327 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-lifecycle.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-lifecycle.ts @@ -282,6 +282,19 @@ function transformPromiseBasedDone( return undefined; } +function countDoneUsages(node: ts.Node, doneIdentifier: ts.Identifier): number { + let count = 0; + const visitor = (n: ts.Node) => { + if (ts.isIdentifier(n) && n.text === doneIdentifier.text) { + count++; + } + ts.forEachChild(n, visitor); + }; + ts.forEachChild(node, visitor); + + return count; +} + export function transformDoneCallback(node: ts.Node, refactorCtx: RefactorContext): ts.Node { const { sourceFile, reporter, tsContext } = refactorCtx; if ( @@ -309,12 +322,17 @@ export function transformDoneCallback(node: ts.Node, refactorCtx: RefactorContex return node; } const doneIdentifier = doneParam.name; + + // Count total usages of 'done' in the body + const totalUsages = countDoneUsages(functionArg.body, doneIdentifier); + let handledUsages = 0; let doneWasUsed = false; const bodyVisitor = (bodyNode: ts.Node): ts.Node | ts.Node[] | undefined => { const complexTransformed = transformComplexDoneCallback(bodyNode, doneIdentifier, refactorCtx); if (complexTransformed !== bodyNode) { doneWasUsed = true; + handledUsages++; // complex transform handles one usage return complexTransformed; } @@ -330,6 +348,7 @@ export function transformDoneCallback(node: ts.Node, refactorCtx: RefactorContex callExpr.expression.name.text === 'fail' ) { doneWasUsed = true; + handledUsages++; reporter.reportTransformation( sourceFile, bodyNode, @@ -350,6 +369,7 @@ export function transformDoneCallback(node: ts.Node, refactorCtx: RefactorContex const promiseTransformed = transformPromiseBasedDone(callExpr, doneIdentifier, refactorCtx); if (promiseTransformed) { doneWasUsed = true; + handledUsages++; return promiseTransformed; } @@ -360,6 +380,7 @@ export function transformDoneCallback(node: ts.Node, refactorCtx: RefactorContex callExpr.expression.text === doneIdentifier.text ) { doneWasUsed = true; + handledUsages++; return ts.setTextRange(ts.factory.createEmptyStatement(), callExpr.expression); } @@ -383,6 +404,20 @@ export function transformDoneCallback(node: ts.Node, refactorCtx: RefactorContex return bodyVisitor(node); }); + // Safety check: if we found usages but didn't handle all of them, abort. + if (handledUsages < totalUsages) { + reporter.reportTransformation( + sourceFile, + node, + `Found unhandled usage of \`${doneIdentifier.text}\` callback. Skipping transformation.`, + ); + const category = 'unhandled-done-usage'; + reporter.recordTodo(category); + addTodoComment(node, category); + + return node; + } + if (!doneWasUsed) { return node; } diff --git a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-lifecycle_spec.ts b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-lifecycle_spec.ts index d5f9e3231180..f41f5fa7ae32 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-lifecycle_spec.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-lifecycle_spec.ts @@ -132,6 +132,7 @@ describe('Jasmine to Vitest Transformer', () => { }); `, expected: ` + // TODO: vitest-migration: The 'done' callback was used in an unhandled way. Please migrate manually. it('should not transform a function with a parameter that is not a done callback', (value) => { expect(value).toBe(true); }); @@ -157,6 +158,20 @@ describe('Jasmine to Vitest Transformer', () => { }); `, }, + { + description: 'should add a TODO for unhandled done usage', + input: ` + it('should do something with helper', (done) => { + someHelper(done); + }); + `, + expected: ` + // TODO: vitest-migration: The 'done' callback was used in an unhandled way. Please migrate manually. + it('should do something with helper', (done) => { + someHelper(done); + }); + `, + }, ]; testCases.forEach(({ description, input, expected }) => { diff --git a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher.ts b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher.ts index b1b746aad504..5de173bcd52b 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher.ts @@ -451,10 +451,14 @@ export function transformArrayWithExactContents( ], ); - return [ - ts.factory.createExpressionStatement(lengthCall), - ts.factory.createExpressionStatement(containingCall), - ]; + const lengthStmt = ts.factory.createExpressionStatement(lengthCall); + const containingStmt = ts.factory.createExpressionStatement(containingCall); + + const category = 'arrayWithExactContents-check'; + reporter.recordTodo(category); + addTodoComment(lengthStmt, category); + + return [lengthStmt, containingStmt]; } export function transformCalledOnceWith( diff --git a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher_spec.ts b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher_spec.ts index 69b0637254aa..a46fc63d2a01 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher_spec.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher_spec.ts @@ -225,19 +225,25 @@ expect(mySpyObj).toHaveSpyInteractions();`, { description: 'should transform toEqual(jasmine.arrayWithExactContents()) into two calls', input: `expect(myArray).toEqual(jasmine.arrayWithExactContents(['a', 'b']));`, + /* eslint-disable max-len */ expected: ` + // TODO: vitest-migration: Verify this matches strict array content (multiset equality). Vitest's arrayContaining is a subset check. expect(myArray).toHaveLength(2); expect(myArray).toEqual(expect.arrayContaining(['a', 'b'])); `, + /* eslint-enable max-len */ }, { description: 'should transform toEqual(jasmine.arrayWithExactContents()) with asymmetric matchers', input: `expect(myArray).toEqual(jasmine.arrayWithExactContents([jasmine.any(Number), 'a']));`, + /* eslint-disable max-len */ expected: ` + // TODO: vitest-migration: Verify this matches strict array content (multiset equality). Vitest's arrayContaining is a subset check. expect(myArray).toHaveLength(2); expect(myArray).toEqual(expect.arrayContaining([expect.any(Number), 'a'])); `, + /* eslint-enable max-len */ }, { description: 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 d2d9af5f01a6..c10fc739f9af 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-spy.ts @@ -227,6 +227,12 @@ export function transformCreateSpyObj( 'Transformed `jasmine.createSpyObj()` to an object literal with `vi.fn()`.', ); + const baseNameArg = node.arguments[0]; + const baseName = ts.isStringLiteral(baseNameArg) ? baseNameArg.text : undefined; + const methods = node.arguments[1]; + const propertiesArg = node.arguments[2]; + let properties: ts.PropertyAssignment[] = []; + if (node.arguments.length < 2) { const category = 'createSpyObj-single-argument'; reporter.recordTodo(category); @@ -235,14 +241,10 @@ export function transformCreateSpyObj( return node; } - const methods = node.arguments[1]; - const propertiesArg = node.arguments[2]; - let properties: ts.PropertyAssignment[] = []; - if (ts.isArrayLiteralExpression(methods)) { - properties = createSpyObjWithArray(methods); + properties = createSpyObjWithArray(methods, baseName); } else if (ts.isObjectLiteralExpression(methods)) { - properties = createSpyObjWithObject(methods); + properties = createSpyObjWithObject(methods, baseName); } else { const category = 'createSpyObj-dynamic-variable'; reporter.recordTodo(category); @@ -264,13 +266,28 @@ export function transformCreateSpyObj( return ts.factory.createObjectLiteralExpression(properties, true); } -function createSpyObjWithArray(methods: ts.ArrayLiteralExpression): ts.PropertyAssignment[] { +function createSpyObjWithArray( + methods: ts.ArrayLiteralExpression, + baseName: string | undefined, +): ts.PropertyAssignment[] { return methods.elements .map((element) => { if (ts.isStringLiteral(element)) { + const mockFn = createViCallExpression('fn'); + const methodName = element.text; + let finalExpression: ts.Expression = mockFn; + + if (baseName) { + finalExpression = ts.factory.createCallExpression( + createPropertyAccess(finalExpression, 'mockName'), + undefined, + [ts.factory.createStringLiteral(`${baseName}.${methodName}`)], + ); + } + return ts.factory.createPropertyAssignment( - ts.factory.createIdentifier(element.text), - createViCallExpression('fn'), + ts.factory.createIdentifier(methodName), + finalExpression, ); } @@ -279,13 +296,25 @@ function createSpyObjWithArray(methods: ts.ArrayLiteralExpression): ts.PropertyA .filter((p): p is ts.PropertyAssignment => !!p); } -function createSpyObjWithObject(methods: ts.ObjectLiteralExpression): ts.PropertyAssignment[] { +function createSpyObjWithObject( + methods: ts.ObjectLiteralExpression, + baseName: string | undefined, +): 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'); + let mockFn = createViCallExpression('fn'); + + if (baseName) { + mockFn = ts.factory.createCallExpression( + createPropertyAccess(mockFn, 'mockName'), + undefined, + [ts.factory.createStringLiteral(`${baseName}.${methodName}`)], + ); + } + const mockReturnValue = createPropertyAccess(mockFn, 'mockReturnValue'); return ts.factory.createPropertyAssignment( 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 c84b35c422ac..3fc98aa02601 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 @@ -118,8 +118,8 @@ vi.spyOn(service, 'myMethod').and.unknownStrategy();`, 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() + methodA: vi.fn().mockName("MyService.methodA"), + methodB: vi.fn().mockName("MyService.methodB"), };`, }, { @@ -134,8 +134,8 @@ vi.spyOn(service, 'myMethod').and.unknownStrategy();`, 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) + methodA: vi.fn().mockName("MyService.methodA").mockReturnValue('foo'), + methodB: vi.fn().mockName("MyService.methodB").mockReturnValue(42), };`, }, { @@ -143,7 +143,7 @@ vi.spyOn(service, 'myMethod').and.unknownStrategy();`, '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)) + methodA: vi.fn().mockName("MyService.methodA").mockReturnValue(expect.any(String)), };`, }, { @@ -158,23 +158,23 @@ vi.spyOn(service, 'myMethod').and.unknownStrategy();`, 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' + methodA: vi.fn().mockName("MyService.methodA"), + 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' + methodA: vi.fn().mockName("MyService.methodA").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() + methodA: vi.fn().mockName("MyService.methodA"), };`, }, ]; 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 c0a5eb406e6a..a4964b1e456b 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/utils/todo-notes.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/utils/todo-notes.ts @@ -56,6 +56,10 @@ export const TODO_NOTES = { message: 'Cannot transform jasmine.arrayWithExactContents with a dynamic variable. Please migrate this manually.', }, + 'arrayWithExactContents-check': { + message: + "Verify this matches strict array content (multiset equality). Vitest's arrayContaining is a subset check.", + }, 'expect-nothing': { message: 'expect().nothing() has been removed because it is redundant in Vitest. Tests without assertions pass by default.', @@ -120,6 +124,9 @@ export const TODO_NOTES = { ' Please refactor to access .args directly or use vi.mocked(spy).mock.lastCall.', url: 'https://vitest.dev/api/mocked.html#mock-lastcall', }, + 'unhandled-done-usage': { + message: "The 'done' callback was used in an unhandled way. Please migrate manually.", + }, } as const; /**