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 5b30e9f24f4b..ce667dce1afc 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 @@ -392,14 +392,14 @@ describe('Jasmine to Vitest Transformer - Integration Tests', () => { it('should handle fail()', () => { if (true) { - throw new Error('This should not have happened'); + expect.fail('This should not have happened'); } }); it('should handle fail() with a specific error', () => { try { expect(1).toBe(2); - throw new Error('Expected test to fail'); + expect.fail('Expected test to fail'); } catch (err) { expect(err.message).toBe('1 !== 2'); } 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 f652368b03f7..e436434f134a 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.ts @@ -144,6 +144,7 @@ const callExpressionTransformers = [ // **Stage 3: Global Functions & Cleanup** // These handle global Jasmine functions and catch-alls for unsupported APIs. + transformFail, transformTimerMocks, transformUnsupportedGlobalFunctions, transformUnsupportedJasmineCalls, @@ -168,7 +169,6 @@ const expressionStatementTransformers = [ transformCalledOnceWith, transformArrayWithExactContents, transformExpectNothing, - transformFail, transformJasmineMembers, ]; @@ -227,18 +227,16 @@ export function transformJasmineToVitest( } for (const transformer of callExpressionTransformers) { - if ( - !( - (options.browserMode && transformer === transformToHaveClass) || - (options.fakeAsync === false && - [ - transformFakeAsyncFlush, - transformFakeAsyncFlushMicrotasks, - transformFakeAsyncTick, - transformFakeAsyncTest, - ].includes(transformer)) - ) - ) { + if (!( + (options.browserMode && transformer === transformToHaveClass) || + (options.fakeAsync === false && + [ + transformFakeAsyncFlush, + transformFakeAsyncFlushMicrotasks, + transformFakeAsyncTick, + transformFakeAsyncTest, + ].includes(transformer)) + )) { transformedNode = transformer(transformedNode, refactorCtx); } } diff --git a/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer_add-imports_spec.ts b/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer_add-imports_spec.ts index f4b10d485920..cbe05226ef7b 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer_add-imports_spec.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer_add-imports_spec.ts @@ -178,4 +178,24 @@ describe('Jasmine to Vitest Transformer - addImports option', () => { `; await expectTransformation(input, expected, true); }); + + it('should add import for `expect` when `fail()` is used and addImports is true', async () => { + const input = ` + describe('My Suite', () => { + it('fails', () => { + fail('Something went wrong'); + }); + }); + `; + const expected = ` + import { describe, expect, it } from 'vitest'; + + describe('My Suite', () => { + it('fails', () => { + expect.fail('Something went wrong'); + }); + }); + `; + await expectTransformation(input, expected, true); + }); }); diff --git a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-misc.ts b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-misc.ts index f71353cc9783..fe7e944e53e1 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-misc.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-misc.ts @@ -90,29 +90,53 @@ export function transformTimerMocks(node: ts.Node, ctx: RefactorContext): ts.Nod return node; } -export function transformFail(node: ts.Node, { sourceFile, reporter }: RefactorContext): ts.Node { +export function transformFail( + node: ts.Node, + { sourceFile, reporter, pendingVitestValueImports }: RefactorContext, +): ts.Node { if ( - ts.isExpressionStatement(node) && - ts.isCallExpression(node.expression) && - ts.isIdentifier(node.expression.expression) && - node.expression.expression.text === 'fail' + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === 'fail' ) { - reporter.reportTransformation(sourceFile, node, 'Transformed `fail()` to `throw new Error()`.'); - - const arg = node.expression.arguments[0]; - let throwExpression: ts.Expression; - - if (arg && ts.isNewExpression(arg)) { - throwExpression = arg; - } else { - throwExpression = ts.factory.createNewExpression( - ts.factory.createIdentifier('Error'), - undefined, - arg ? [arg] : [], - ); + addVitestValueImport(pendingVitestValueImports, 'expect'); + reporter.reportTransformation(sourceFile, node, 'Transformed `fail()` to `expect.fail()`.'); + + const arg = node.arguments[0]; + let replacementArg: ts.Expression | undefined = arg; + let hasNonStringArg = false; + + if (arg) { + if (ts.isNewExpression(arg)) { + replacementArg = arg.arguments && arg.arguments.length > 0 ? arg.arguments[0] : undefined; + } else if ( + !ts.isStringLiteral(arg) && + !ts.isNoSubstitutionTemplateLiteral(arg) && + !ts.isTemplateExpression(arg) + ) { + replacementArg = ts.factory.createCallExpression( + ts.factory.createIdentifier('String'), + undefined, + [arg], + ); + hasNonStringArg = true; + } } - const replacement = ts.factory.createThrowStatement(throwExpression); + const replacement = ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier('expect'), + ts.factory.createIdentifier('fail'), + ), + undefined, + replacementArg ? [replacementArg] : [], + ); + + if (hasNonStringArg) { + const category = 'fail-non-string-argument'; + reporter.recordTodo(category, sourceFile, node); + addTodoComment(replacement, category); + } return ts.setOriginalNode(ts.setTextRange(replacement, node), node); } @@ -197,11 +221,7 @@ const UNSUPPORTED_GLOBAL_FUNCTION_CATEGORIES = new Set([ function isUnsupportedGlobalFunction( methodName: string, ): methodName is - | 'setSpecProperty' - | 'setSuiteProperty' - | 'throwUnless' - | 'throwUnlessAsync' - | 'getSpecProperty' { + 'setSpecProperty' | 'setSuiteProperty' | 'throwUnless' | 'throwUnlessAsync' | 'getSpecProperty' { return UNSUPPORTED_GLOBAL_FUNCTION_CATEGORIES.has(methodName as TodoCategory); } diff --git a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-misc_spec.ts b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-misc_spec.ts index a5b29f2d2b6a..49cb2d54475a 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-misc_spec.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-misc_spec.ts @@ -59,19 +59,31 @@ jasmine.clock().withMock(noop);`, describe('transformFail', () => { const testCases = [ { - description: 'should transform fail() to throw new Error()', + description: 'should transform fail() to expect.fail()', input: `fail('This should not happen');`, - expected: `throw new Error('This should not happen');`, + expected: `expect.fail('This should not happen');`, }, { - description: 'should transform fail() without a message to throw new Error()', + description: 'should transform fail() without a message to expect.fail()', input: `fail();`, - expected: `throw new Error();`, + expected: `expect.fail();`, }, { description: 'should transform fail() with an Error object', input: `fail(new TypeError('Invalid input'));`, - expected: `throw new TypeError('Invalid input');`, + expected: `expect.fail('Invalid input');`, + }, + { + description: 'should transform fail() with an empty Error object', + input: `fail(new Error());`, + expected: `expect.fail();`, + }, + { + description: 'should transform fail() with a non-string argument and add a TODO note', + input: `fail(err);`, + // eslint-disable-next-line max-len + expected: `// TODO: vitest-migration: expect.fail() only accepts a string message. Verify that converting this argument with String() produces the expected failure output. See: https://vitest.dev/api/expect.html#expect-fail +expect.fail(String(err));`, }, ]; 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 598606d7bde6..0179a0314277 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/utils/todo-notes.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/utils/todo-notes.ts @@ -64,6 +64,12 @@ export const TODO_NOTES = { message: 'expect().nothing() has been removed because it is redundant in Vitest. Tests without assertions pass by default.', }, + 'fail-non-string-argument': { + message: + 'expect.fail() only accepts a string message. ' + + 'Verify that converting this argument with String() produces the expected failure output.', + url: 'https://vitest.dev/api/expect.html#expect-fail', + }, 'unsupported-jasmine-member': { message: (context: { name: string }): string => `jasmine.${context.name} is not supported.`, },