From 4c69f0c0763d7875b87c17d5f8ad93fda6f13592 Mon Sep 17 00:00:00 2001 From: MeAkib Date: Sat, 31 Oct 2026 07:29:22 +0600 Subject: [PATCH 1/3] fix(@schematics/angular): add a TODO for negated expectAsync matchers in refactor-jasmine-vitest Negated async matchers such as `expectAsync(promise).not.toBeResolved()` were left unchanged without a TODO comment, so the migrated test failed at runtime because `expectAsync` does not exist in Vitest. Negated async matchers have no exact Vitest equivalent, so add the existing unsupported `expectAsync` matcher TODO comment to them. --- .../transformers/jasmine-matcher.ts | 22 ++++++++++++++++--- .../transformers/jasmine-matcher_spec.ts | 8 +++++++ 2 files changed, 27 insertions(+), 3 deletions(-) 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 1e0a87dc80f2..d1c3159acb71 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher.ts @@ -259,11 +259,27 @@ export function transformExpectAsync( node: ts.Node, { sourceFile, reporter }: RefactorContext, ): ts.Node { + if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) { + return node; + } + + // Negated async matchers have no exact equivalent (e.g. `.not.toBeResolvedTo()` also passes on rejection). + const notPae = node.expression.expression; if ( - !ts.isCallExpression(node) || - !ts.isPropertyAccessExpression(node.expression) || - !ts.isCallExpression(node.expression.expression) + ts.isPropertyAccessExpression(notPae) && + notPae.name.text === 'not' && + ts.isCallExpression(notPae.expression) && + ts.isIdentifier(notPae.expression.expression) && + notPae.expression.expression.text === 'expectAsync' ) { + const category = 'unsupported-expect-async-matcher'; + reporter.recordTodo(category, sourceFile, node); + addTodoComment(node, category, { name: `not.${node.expression.name.text}` }); + + return node; + } + + if (!ts.isCallExpression(node.expression.expression)) { return node; } 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 274202ed1689..5a903165299f 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 @@ -84,6 +84,14 @@ describe('transformExpectAsync', () => { await expectAsync(myPromise).toBeSomethingElse(); `, }, + { + description: 'should add a TODO for a negated expectAsync matcher', + input: `await expectAsync(myPromise).not.toBeResolved();`, + expected: ` + // TODO: vitest-migration: Unsupported expectAsync matcher ".not.toBeResolved()" found. Please migrate this manually. + await expectAsync(myPromise).not.toBeResolved(); + `, + }, { description: 'should add a specific TODO for toBePending', input: `await expectAsync(myPromise).toBePending();`, From c40aefad20c7612385fbce1faac0c37872706b5c Mon Sep 17 00:00:00 2001 From: MeAkib Date: Tue, 3 Nov 2026 15:01:44 +0600 Subject: [PATCH 2/3] fixup! fix(@schematics/angular): add a TODO for negated expectAsync matchers in refactor-jasmine-vitest --- .../transformers/jasmine-matcher.ts | 49 ++++++++++++------- .../transformers/jasmine-matcher_spec.ts | 16 ++++++ 2 files changed, 46 insertions(+), 19 deletions(-) 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 d1c3159acb71..d2df0c78b57d 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher.ts @@ -263,36 +263,47 @@ export function transformExpectAsync( return node; } - // Negated async matchers have no exact equivalent (e.g. `.not.toBeResolvedTo()` also passes on rejection). - const notPae = node.expression.expression; + // When the chain continues past this call (e.g. the `withContext('msg')` link itself), + // the outermost matcher call handles the whole statement. if ( - ts.isPropertyAccessExpression(notPae) && - notPae.name.text === 'not' && - ts.isCallExpression(notPae.expression) && - ts.isIdentifier(notPae.expression.expression) && - notPae.expression.expression.text === 'expectAsync' + node.parent && + ts.isPropertyAccessExpression(node.parent) && + node.parent.expression === node ) { - const category = 'unsupported-expect-async-matcher'; - reporter.recordTodo(category, sourceFile, node); - addTodoComment(node, category, { name: `not.${node.expression.name.text}` }); - - return node; - } - - if (!ts.isCallExpression(node.expression.expression)) { return node; } const matcherCall = node; const matcherPae = node.expression; - const expectCall = node.expression.expression; + + // Walk down any modifier chain (`.not`, `.already`, `.withContext(...)`) to the root call. + // Modifiers have no direct vitest equivalent, so a modified matcher falls through to the + // TODO handling below. + const modifierNames: string[] = []; + let expectCall: ts.CallExpression | undefined; + let current: ts.Expression = matcherPae.expression; + while (!expectCall) { + if (ts.isCallExpression(current)) { + if (ts.isIdentifier(current.expression)) { + expectCall = current; + } else { + current = current.expression; + } + } else if (ts.isPropertyAccessExpression(current) && ts.isIdentifier(current.name)) { + modifierNames.unshift(current.name.text); + current = current.expression; + } else { + return node; + } + } if (!ts.isIdentifier(expectCall.expression) || expectCall.expression.text !== 'expectAsync') { return node; } const matcherName = ts.isIdentifier(matcherPae.name) ? matcherPae.name.text : undefined; - const mapping = matcherName ? ASYNC_MATCHER_CHANGES.get(matcherName) : undefined; + const mapping = + matcherName && modifierNames.length === 0 ? ASYNC_MATCHER_CHANGES.get(matcherName) : undefined; if (mapping) { reporter.reportTransformation( @@ -314,14 +325,14 @@ export function transformExpectAsync( } if (matcherName) { - if (matcherName === 'toBePending') { + if (matcherName === 'toBePending' && modifierNames.length === 0) { const category = 'toBePending'; reporter.recordTodo(category, sourceFile, node); addTodoComment(node, category); } else { const category = 'unsupported-expect-async-matcher'; reporter.recordTodo(category, sourceFile, node); - addTodoComment(node, category, { name: matcherName }); + addTodoComment(node, category, { name: [...modifierNames, matcherName].join('.') }); } } 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 5a903165299f..f4e4da32e20d 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 @@ -92,6 +92,22 @@ describe('transformExpectAsync', () => { await expectAsync(myPromise).not.toBeResolved(); `, }, + { + description: 'should add a TODO for an expectAsync matcher modified with already', + input: `await expectAsync(myPromise).already.toBeResolved();`, + expected: ` + // TODO: vitest-migration: Unsupported expectAsync matcher ".already.toBeResolved()" found. Please migrate this manually. + await expectAsync(myPromise).already.toBeResolved(); + `, + }, + { + description: 'should add a TODO for an expectAsync matcher modified with withContext', + input: `await expectAsync(myPromise).withContext('msg').toBeResolved();`, + expected: ` + // TODO: vitest-migration: Unsupported expectAsync matcher ".withContext.toBeResolved()" found. Please migrate this manually. + await expectAsync(myPromise).withContext('msg').toBeResolved(); + `, + }, { description: 'should add a specific TODO for toBePending', input: `await expectAsync(myPromise).toBePending();`, From 96f96b96ea8b8c41355bd07a814e53ec63b322bd Mon Sep 17 00:00:00 2001 From: MeAkib Date: Wed, 4 Nov 2026 17:13:27 +0600 Subject: [PATCH 3/3] fixup! fix(@schematics/angular): add a TODO for negated expectAsync matchers in refactor-jasmine-vitest --- .../transformers/jasmine-matcher.ts | 54 +++++++++---------- .../transformers/jasmine-matcher_spec.ts | 13 +++++ 2 files changed, 37 insertions(+), 30 deletions(-) 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 d2df0c78b57d..454fdf8c1ce1 100644 --- a/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher.ts +++ b/packages/schematics/angular/refactor/jasmine-vitest/transformers/jasmine-matcher.ts @@ -263,31 +263,33 @@ export function transformExpectAsync( return node; } - // When the chain continues past this call (e.g. the `withContext('msg')` link itself), - // the outermost matcher call handles the whole statement. - if ( - node.parent && - ts.isPropertyAccessExpression(node.parent) && - node.parent.expression === node - ) { - return node; - } - const matcherCall = node; const matcherPae = node.expression; + const matcherName = ts.isIdentifier(matcherPae.name) ? matcherPae.name.text : undefined; + if (!matcherName || matcherName === 'withContext') { + return node; + } // Walk down any modifier chain (`.not`, `.already`, `.withContext(...)`) to the root call. // Modifiers have no direct vitest equivalent, so a modified matcher falls through to the - // TODO handling below. + // TODO handling below. `withContext` is the only chainable modifier method, so any other + // intermediate call (e.g. a `.then(...)` chained on a matcher) is not an `expectAsync` chain. const modifierNames: string[] = []; let expectCall: ts.CallExpression | undefined; let current: ts.Expression = matcherPae.expression; while (!expectCall) { if (ts.isCallExpression(current)) { - if (ts.isIdentifier(current.expression)) { + if (ts.isIdentifier(current.expression) && current.expression.text === 'expectAsync') { expectCall = current; + } else if ( + ts.isPropertyAccessExpression(current.expression) && + ts.isIdentifier(current.expression.name) && + current.expression.name.text === 'withContext' + ) { + modifierNames.unshift('withContext'); + current = current.expression.expression; } else { - current = current.expression; + return node; } } else if (ts.isPropertyAccessExpression(current) && ts.isIdentifier(current.name)) { modifierNames.unshift(current.name.text); @@ -297,13 +299,7 @@ export function transformExpectAsync( } } - if (!ts.isIdentifier(expectCall.expression) || expectCall.expression.text !== 'expectAsync') { - return node; - } - - const matcherName = ts.isIdentifier(matcherPae.name) ? matcherPae.name.text : undefined; - const mapping = - matcherName && modifierNames.length === 0 ? ASYNC_MATCHER_CHANGES.get(matcherName) : undefined; + const mapping = modifierNames.length === 0 ? ASYNC_MATCHER_CHANGES.get(matcherName) : undefined; if (mapping) { reporter.reportTransformation( @@ -324,16 +320,14 @@ export function transformExpectAsync( return ts.factory.createCallExpression(newMatcherChain, undefined, newMatcherArgs); } - if (matcherName) { - if (matcherName === 'toBePending' && modifierNames.length === 0) { - const category = 'toBePending'; - reporter.recordTodo(category, sourceFile, node); - addTodoComment(node, category); - } else { - const category = 'unsupported-expect-async-matcher'; - reporter.recordTodo(category, sourceFile, node); - addTodoComment(node, category, { name: [...modifierNames, matcherName].join('.') }); - } + if (matcherName === 'toBePending' && modifierNames.length === 0) { + const category = 'toBePending'; + reporter.recordTodo(category, sourceFile, node); + addTodoComment(node, category); + } else { + const category = 'unsupported-expect-async-matcher'; + reporter.recordTodo(category, sourceFile, node); + addTodoComment(node, category, { name: [...modifierNames, matcherName].join('.') }); } return node; 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 f4e4da32e20d..10c6be262414 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 @@ -108,6 +108,19 @@ describe('transformExpectAsync', () => { await expectAsync(myPromise).withContext('msg').toBeResolved(); `, }, + { + description: 'should add a single TODO for an expectAsync matcher with combined modifiers', + input: `await expectAsync(myPromise).withContext('msg').not.toBeResolved();`, + expected: ` + // TODO: vitest-migration: Unsupported expectAsync matcher ".withContext.not.toBeResolved()" found. Please migrate this manually. + await expectAsync(myPromise).withContext('msg').not.toBeResolved(); + `, + }, + { + description: 'should transform an expectAsync matcher with a chained .then()', + input: `await expectAsync(myPromise).toBeResolved().then(() => {});`, + expected: `await expect(myPromise).resolves.not.toThrow().then(() => {});`, + }, { description: 'should add a specific TODO for toBePending', input: `await expectAsync(myPromise).toBePending();`,