Skip to content

Commit 2724099

Browse files
committed
fix(@schematics/angular): transform fail() to expect.fail() in refactor-jasmine-vitest
Previously, fail() calls in Jasmine specs were transformed into throw new Error(...). In Vitest, expect.fail(...) is the idiomatic assertion method to explicitly fail a test with an AssertionError, properly formatting test failures in test runner output and avoiding generic unhandled exception throws. This update converts fail(...) call expressions to expect.fail(...), registers expect in the pending Vitest value imports, and moves the transformer into the call expression transformers pipeline.
1 parent d827ba9 commit 2724099

6 files changed

Lines changed: 100 additions & 44 deletions

File tree

packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.integration_spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,14 +392,14 @@ describe('Jasmine to Vitest Transformer - Integration Tests', () => {
392392
393393
it('should handle fail()', () => {
394394
if (true) {
395-
throw new Error('This should not have happened');
395+
expect.fail('This should not have happened');
396396
}
397397
});
398398
399399
it('should handle fail() with a specific error', () => {
400400
try {
401401
expect(1).toBe(2);
402-
throw new Error('Expected test to fail');
402+
expect.fail('Expected test to fail');
403403
} catch (err) {
404404
expect(err.message).toBe('1 !== 2');
405405
}

packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ const callExpressionTransformers = [
144144

145145
// **Stage 3: Global Functions & Cleanup**
146146
// These handle global Jasmine functions and catch-alls for unsupported APIs.
147+
transformFail,
147148
transformTimerMocks,
148149
transformUnsupportedGlobalFunctions,
149150
transformUnsupportedJasmineCalls,
@@ -168,7 +169,6 @@ const expressionStatementTransformers = [
168169
transformCalledOnceWith,
169170
transformArrayWithExactContents,
170171
transformExpectNothing,
171-
transformFail,
172172
transformJasmineMembers,
173173
];
174174

@@ -227,18 +227,16 @@ export function transformJasmineToVitest(
227227
}
228228

229229
for (const transformer of callExpressionTransformers) {
230-
if (
231-
!(
232-
(options.browserMode && transformer === transformToHaveClass) ||
233-
(options.fakeAsync === false &&
234-
[
235-
transformFakeAsyncFlush,
236-
transformFakeAsyncFlushMicrotasks,
237-
transformFakeAsyncTick,
238-
transformFakeAsyncTest,
239-
].includes(transformer))
240-
)
241-
) {
230+
if (!(
231+
(options.browserMode && transformer === transformToHaveClass) ||
232+
(options.fakeAsync === false &&
233+
[
234+
transformFakeAsyncFlush,
235+
transformFakeAsyncFlushMicrotasks,
236+
transformFakeAsyncTick,
237+
transformFakeAsyncTest,
238+
].includes(transformer))
239+
)) {
242240
transformedNode = transformer(transformedNode, refactorCtx);
243241
}
244242
}

packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer_add-imports_spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,24 @@ describe('Jasmine to Vitest Transformer - addImports option', () => {
178178
`;
179179
await expectTransformation(input, expected, true);
180180
});
181+
182+
it('should add import for `expect` when `fail()` is used and addImports is true', async () => {
183+
const input = `
184+
describe('My Suite', () => {
185+
it('fails', () => {
186+
fail('Something went wrong');
187+
});
188+
});
189+
`;
190+
const expected = `
191+
import { describe, expect, it } from 'vitest';
192+
193+
describe('My Suite', () => {
194+
it('fails', () => {
195+
expect.fail('Something went wrong');
196+
});
197+
});
198+
`;
199+
await expectTransformation(input, expected, true);
200+
});
181201
});

packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-misc.ts

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -90,29 +90,53 @@ export function transformTimerMocks(node: ts.Node, ctx: RefactorContext): ts.Nod
9090
return node;
9191
}
9292

93-
export function transformFail(node: ts.Node, { sourceFile, reporter }: RefactorContext): ts.Node {
93+
export function transformFail(
94+
node: ts.Node,
95+
{ sourceFile, reporter, pendingVitestValueImports }: RefactorContext,
96+
): ts.Node {
9497
if (
95-
ts.isExpressionStatement(node) &&
96-
ts.isCallExpression(node.expression) &&
97-
ts.isIdentifier(node.expression.expression) &&
98-
node.expression.expression.text === 'fail'
98+
ts.isCallExpression(node) &&
99+
ts.isIdentifier(node.expression) &&
100+
node.expression.text === 'fail'
99101
) {
100-
reporter.reportTransformation(sourceFile, node, 'Transformed `fail()` to `throw new Error()`.');
101-
102-
const arg = node.expression.arguments[0];
103-
let throwExpression: ts.Expression;
104-
105-
if (arg && ts.isNewExpression(arg)) {
106-
throwExpression = arg;
107-
} else {
108-
throwExpression = ts.factory.createNewExpression(
109-
ts.factory.createIdentifier('Error'),
110-
undefined,
111-
arg ? [arg] : [],
112-
);
102+
addVitestValueImport(pendingVitestValueImports, 'expect');
103+
reporter.reportTransformation(sourceFile, node, 'Transformed `fail()` to `expect.fail()`.');
104+
105+
const arg = node.arguments[0];
106+
let replacementArg: ts.Expression | undefined = arg;
107+
let hasNonStringArg = false;
108+
109+
if (arg) {
110+
if (ts.isNewExpression(arg)) {
111+
replacementArg = arg.arguments && arg.arguments.length > 0 ? arg.arguments[0] : undefined;
112+
} else if (
113+
!ts.isStringLiteral(arg) &&
114+
!ts.isNoSubstitutionTemplateLiteral(arg) &&
115+
!ts.isTemplateExpression(arg)
116+
) {
117+
replacementArg = ts.factory.createCallExpression(
118+
ts.factory.createIdentifier('String'),
119+
undefined,
120+
[arg],
121+
);
122+
hasNonStringArg = true;
123+
}
113124
}
114125

115-
const replacement = ts.factory.createThrowStatement(throwExpression);
126+
const replacement = ts.factory.createCallExpression(
127+
ts.factory.createPropertyAccessExpression(
128+
ts.factory.createIdentifier('expect'),
129+
ts.factory.createIdentifier('fail'),
130+
),
131+
undefined,
132+
replacementArg ? [replacementArg] : [],
133+
);
134+
135+
if (hasNonStringArg) {
136+
const category = 'fail-non-string-argument';
137+
reporter.recordTodo(category, sourceFile, node);
138+
addTodoComment(replacement, category);
139+
}
116140

117141
return ts.setOriginalNode(ts.setTextRange(replacement, node), node);
118142
}
@@ -197,11 +221,7 @@ const UNSUPPORTED_GLOBAL_FUNCTION_CATEGORIES = new Set<TodoCategory>([
197221
function isUnsupportedGlobalFunction(
198222
methodName: string,
199223
): methodName is
200-
| 'setSpecProperty'
201-
| 'setSuiteProperty'
202-
| 'throwUnless'
203-
| 'throwUnlessAsync'
204-
| 'getSpecProperty' {
224+
'setSpecProperty' | 'setSuiteProperty' | 'throwUnless' | 'throwUnlessAsync' | 'getSpecProperty' {
205225
return UNSUPPORTED_GLOBAL_FUNCTION_CATEGORIES.has(methodName as TodoCategory);
206226
}
207227

packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-misc_spec.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,31 @@ jasmine.clock().withMock(noop);`,
5959
describe('transformFail', () => {
6060
const testCases = [
6161
{
62-
description: 'should transform fail() to throw new Error()',
62+
description: 'should transform fail() to expect.fail()',
6363
input: `fail('This should not happen');`,
64-
expected: `throw new Error('This should not happen');`,
64+
expected: `expect.fail('This should not happen');`,
6565
},
6666
{
67-
description: 'should transform fail() without a message to throw new Error()',
67+
description: 'should transform fail() without a message to expect.fail()',
6868
input: `fail();`,
69-
expected: `throw new Error();`,
69+
expected: `expect.fail();`,
7070
},
7171
{
7272
description: 'should transform fail() with an Error object',
7373
input: `fail(new TypeError('Invalid input'));`,
74-
expected: `throw new TypeError('Invalid input');`,
74+
expected: `expect.fail('Invalid input');`,
75+
},
76+
{
77+
description: 'should transform fail() with an empty Error object',
78+
input: `fail(new Error());`,
79+
expected: `expect.fail();`,
80+
},
81+
{
82+
description: 'should transform fail() with a non-string argument and add a TODO note',
83+
input: `fail(err);`,
84+
// eslint-disable-next-line max-len
85+
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
86+
expect.fail(String(err));`,
7587
},
7688
];
7789

packages/schematics/angular/refactor/jasmine-vitest/utils/todo-notes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ export const TODO_NOTES = {
6464
message:
6565
'expect().nothing() has been removed because it is redundant in Vitest. Tests without assertions pass by default.',
6666
},
67+
'fail-non-string-argument': {
68+
message:
69+
'expect.fail() only accepts a string message. ' +
70+
'Verify that converting this argument with String() produces the expected failure output.',
71+
url: 'https://vitest.dev/api/expect.html#expect-fail',
72+
},
6773
'unsupported-jasmine-member': {
6874
message: (context: { name: string }): string => `jasmine.${context.name} is not supported.`,
6975
},

0 commit comments

Comments
 (0)