@schematics/angular
Version:
Schematics specific to Angular
401 lines • 21 kB
JavaScript
"use strict";
/**
* @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
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.transformSyntacticSugarMatchers = transformSyntacticSugarMatchers;
exports.transformAsymmetricMatchers = transformAsymmetricMatchers;
exports.transformtoHaveBeenCalledBefore = transformtoHaveBeenCalledBefore;
exports.transformToHaveClass = transformToHaveClass;
exports.transformExpectAsync = transformExpectAsync;
exports.transformComplexMatchers = transformComplexMatchers;
exports.transformArrayWithExactContents = transformArrayWithExactContents;
exports.transformCalledOnceWith = transformCalledOnceWith;
exports.transformWithContext = transformWithContext;
exports.transformExpectNothing = transformExpectNothing;
/**
* @fileoverview This file contains transformers that migrate Jasmine matchers to their
* Vitest counterparts. It handles a wide range of matchers, including syntactic sugar
* (e.g., `toBeTrue`), asymmetric matchers (e.g., `jasmine.any`), async promise matchers
* (`expectAsync`), and complex matchers that require restructuring, such as
* `toHaveBeenCalledOnceWith` and `arrayWithExactContents`.
*/
const typescript_1 = __importDefault(require("../../../third_party/github.com/Microsoft/TypeScript/lib/typescript"));
const ast_helpers_1 = require("../utils/ast-helpers");
const ast_validation_1 = require("../utils/ast-validation");
const comment_helpers_1 = require("../utils/comment-helpers");
const SUGAR_MATCHER_CHANGES = new Map([
['toBeTrue', { newName: 'toBe', newArgs: [typescript_1.default.factory.createTrue()] }],
['toBeFalse', { newName: 'toBe', newArgs: [typescript_1.default.factory.createFalse()] }],
['toBePositiveInfinity', { newName: 'toBe', newArgs: [typescript_1.default.factory.createIdentifier('Infinity')] }],
[
'toBeNegativeInfinity',
{
newName: 'toBe',
newArgs: [
typescript_1.default.factory.createPrefixUnaryExpression(typescript_1.default.SyntaxKind.MinusToken, typescript_1.default.factory.createIdentifier('Infinity')),
],
},
],
['toHaveSize', { newName: 'toHaveLength' }],
]);
function transformSyntacticSugarMatchers(node, { sourceFile, reporter }) {
if (!typescript_1.default.isCallExpression(node) || !typescript_1.default.isPropertyAccessExpression(node.expression)) {
return node;
}
const pae = node.expression;
const matcherName = pae.name.text;
if (matcherName === 'toHaveSpyInteractions') {
const category = 'toHaveSpyInteractions';
reporter.recordTodo(category);
(0, comment_helpers_1.addTodoComment)(node, category);
return node;
}
if (matcherName === 'toThrowMatching') {
const category = 'toThrowMatching';
reporter.recordTodo(category);
(0, comment_helpers_1.addTodoComment)(node, category, { name: matcherName });
return node;
}
const mapping = SUGAR_MATCHER_CHANGES.get(matcherName);
if (mapping) {
reporter.reportTransformation(sourceFile, node, `Transformed matcher ".${matcherName}()" to ".${mapping.newName}()".`);
const newExpression = (0, ast_helpers_1.createPropertyAccess)(pae.expression, mapping.newName);
const newArgs = mapping.newArgs ?? [...node.arguments];
return typescript_1.default.factory.updateCallExpression(node, newExpression, node.typeArguments, newArgs);
}
return node;
}
const ASYMMETRIC_MATCHER_NAMES = [
'anything',
'any',
'stringMatching',
'objectContaining',
'arrayContaining',
'stringContaining',
];
function transformAsymmetricMatchers(node, { sourceFile, reporter, pendingVitestValueImports }) {
if (typescript_1.default.isPropertyAccessExpression(node) &&
typescript_1.default.isIdentifier(node.expression) &&
node.expression.text === 'jasmine') {
const matcherName = node.name.text;
if (ASYMMETRIC_MATCHER_NAMES.includes(matcherName)) {
(0, ast_helpers_1.addVitestValueImport)(pendingVitestValueImports, 'expect');
reporter.reportTransformation(sourceFile, node, `Transformed asymmetric matcher \`jasmine.${matcherName}\` to \`expect.${matcherName}\`.`);
return (0, ast_helpers_1.createPropertyAccess)('expect', node.name);
}
}
return node;
}
function transformtoHaveBeenCalledBefore(node, { sourceFile, reporter }) {
if (!typescript_1.default.isCallExpression(node) ||
!typescript_1.default.isPropertyAccessExpression(node.expression) ||
node.arguments.length !== 1) {
return node;
}
const pae = node.expression;
const matcherName = pae.name.text;
let isNegated = false;
let expectExpression = pae.expression;
if (typescript_1.default.isPropertyAccessExpression(expectExpression) && expectExpression.name.text === 'not') {
isNegated = true;
expectExpression = expectExpression.expression;
}
if (!typescript_1.default.isCallExpression(expectExpression) || matcherName !== 'toHaveBeenCalledBefore') {
return node;
}
reporter.reportTransformation(sourceFile, node, 'Transformed `toHaveBeenCalledBefore` to a Vitest-compatible spy invocation order comparison.');
const [spyB] = node.arguments;
const [spyA] = expectExpression.arguments;
const createInvocationOrderAccess = (spyIdentifier) => {
const mockedSpy = typescript_1.default.factory.createCallExpression((0, ast_helpers_1.createPropertyAccess)('vi', 'mocked'), undefined, [spyIdentifier]);
const mockProperty = (0, ast_helpers_1.createPropertyAccess)(mockedSpy, 'mock');
return (0, ast_helpers_1.createPropertyAccess)(mockProperty, 'invocationCallOrder');
};
const createMinCall = (spyIdentifier) => {
return typescript_1.default.factory.createCallExpression((0, ast_helpers_1.createPropertyAccess)('Math', 'min'), undefined, [
typescript_1.default.factory.createSpreadElement(createInvocationOrderAccess(spyIdentifier)),
]);
};
const newExpect = (0, ast_helpers_1.createExpectCallExpression)([createMinCall(spyA)]);
const newMatcherName = isNegated ? 'toBeGreaterThanOrEqual' : 'toBeLessThan';
return typescript_1.default.factory.createCallExpression((0, ast_helpers_1.createPropertyAccess)(newExpect, newMatcherName), undefined, [createMinCall(spyB)]);
}
function transformToHaveClass(node, { sourceFile, reporter }) {
if (!typescript_1.default.isCallExpression(node) ||
!typescript_1.default.isPropertyAccessExpression(node.expression) ||
node.arguments.length !== 1) {
return node;
}
const pae = node.expression;
const matcherName = pae.name.text;
let isNegated = false;
let expectExpression = pae.expression;
if (typescript_1.default.isPropertyAccessExpression(expectExpression) && expectExpression.name.text === 'not') {
isNegated = true;
expectExpression = expectExpression.expression;
}
if (matcherName !== 'toHaveClass') {
return node;
}
reporter.reportTransformation(sourceFile, node, 'Transformed `.toHaveClass()` to a `classList.contains()` check.');
const [className] = node.arguments;
const newExpectArgs = [];
if (typescript_1.default.isCallExpression(expectExpression)) {
const [element] = expectExpression.arguments;
const classListContains = typescript_1.default.factory.createCallExpression((0, ast_helpers_1.createPropertyAccess)((0, ast_helpers_1.createPropertyAccess)(element, 'classList'), 'contains'), undefined, [className]);
newExpectArgs.push(classListContains);
// Pass the context message from withContext to the new expect call
if (expectExpression.arguments.length > 1) {
newExpectArgs.push(expectExpression.arguments[1]);
}
}
else {
return node;
}
const newExpect = (0, ast_helpers_1.createExpectCallExpression)(newExpectArgs);
const newMatcher = isNegated ? typescript_1.default.factory.createFalse() : typescript_1.default.factory.createTrue();
return typescript_1.default.factory.createCallExpression((0, ast_helpers_1.createPropertyAccess)(newExpect, 'toBe'), undefined, [
newMatcher,
]);
}
const ASYNC_MATCHER_CHANGES = new Map([
['toBeResolved', { base: 'resolves', matcher: 'toThrow', not: true, keepArgs: false }],
['toBeResolvedTo', { base: 'resolves', matcher: 'toEqual', keepArgs: true }],
['toBeRejected', { base: 'rejects', matcher: 'toThrow', keepArgs: false }],
['toBeRejectedWith', { base: 'rejects', matcher: 'toEqual', keepArgs: true }],
['toBeRejectedWithError', { base: 'rejects', matcher: 'toThrowError', keepArgs: true }],
]);
function transformExpectAsync(node, { sourceFile, reporter }) {
if (!typescript_1.default.isCallExpression(node) ||
!typescript_1.default.isPropertyAccessExpression(node.expression) ||
!typescript_1.default.isCallExpression(node.expression.expression)) {
return node;
}
const matcherCall = node;
const matcherPae = node.expression;
const expectCall = node.expression.expression;
if (!typescript_1.default.isIdentifier(expectCall.expression) || expectCall.expression.text !== 'expectAsync') {
return node;
}
const matcherName = typescript_1.default.isIdentifier(matcherPae.name) ? matcherPae.name.text : undefined;
const mapping = matcherName ? ASYNC_MATCHER_CHANGES.get(matcherName) : undefined;
if (mapping) {
reporter.reportTransformation(sourceFile, node, `Transformed \`expectAsync(...).${matcherName}\` to \`expect(...).${mapping.base}.${mapping.matcher}\`.`);
const newExpectCall = (0, ast_helpers_1.createExpectCallExpression)([expectCall.arguments[0]]);
let newMatcherChain = (0, ast_helpers_1.createPropertyAccess)(newExpectCall, mapping.base);
if (mapping.not) {
newMatcherChain = (0, ast_helpers_1.createPropertyAccess)(newMatcherChain, 'not');
}
newMatcherChain = (0, ast_helpers_1.createPropertyAccess)(newMatcherChain, mapping.matcher);
const newMatcherArgs = mapping.keepArgs ? [...matcherCall.arguments] : [];
return typescript_1.default.factory.createCallExpression(newMatcherChain, undefined, newMatcherArgs);
}
if (matcherName) {
if (matcherName === 'toBePending') {
const category = 'toBePending';
reporter.recordTodo(category);
(0, comment_helpers_1.addTodoComment)(node, category);
}
else {
const category = 'unsupported-expect-async-matcher';
reporter.recordTodo(category);
(0, comment_helpers_1.addTodoComment)(node, category, { name: matcherName });
}
}
return node;
}
function transformComplexMatchers(node, { sourceFile, reporter }) {
if (!typescript_1.default.isCallExpression(node) ||
!typescript_1.default.isPropertyAccessExpression(node.expression) ||
node.expression.name.text !== 'toEqual' ||
node.arguments.length !== 1) {
return node;
}
const argument = node.arguments[0];
const jasmineMatcherName = (0, ast_validation_1.getJasmineMethodName)(argument);
if (!jasmineMatcherName) {
return node;
}
const expectCall = node.expression.expression;
let newMatcherName;
let newArgs;
let negate = false;
switch (jasmineMatcherName) {
case 'truthy':
newMatcherName = 'toBeTruthy';
break;
case 'falsy':
newMatcherName = 'toBeFalsy';
break;
case 'empty':
newMatcherName = 'toHaveLength';
newArgs = [typescript_1.default.factory.createNumericLiteral(0)];
break;
case 'notEmpty':
newMatcherName = 'toHaveLength';
newArgs = [typescript_1.default.factory.createNumericLiteral(0)];
negate = true;
break;
case 'is':
newMatcherName = 'toBe';
if (typescript_1.default.isCallExpression(argument)) {
newArgs = [...argument.arguments];
}
break;
}
if (newMatcherName) {
reporter.reportTransformation(sourceFile, node, `Transformed \`.toEqual(jasmine.${jasmineMatcherName}())\` to \`.${newMatcherName}()\`.`);
let expectExpression = expectCall;
// Handle cases like `expect(...).not.toEqual(jasmine.notEmpty())`
if (typescript_1.default.isPropertyAccessExpression(expectCall) && expectCall.name.text === 'not') {
// The original expression was negated, so flip the negate flag
negate = !negate;
// Use the expression before the `.not`
expectExpression = expectCall.expression;
}
if (negate) {
expectExpression = (0, ast_helpers_1.createPropertyAccess)(expectExpression, 'not');
}
const newExpression = (0, ast_helpers_1.createPropertyAccess)(expectExpression, newMatcherName);
return typescript_1.default.factory.createCallExpression(newExpression, undefined, newArgs ?? []);
}
return node;
}
function transformArrayWithExactContents(node, { sourceFile, reporter }) {
if (!typescript_1.default.isExpressionStatement(node) ||
!typescript_1.default.isCallExpression(node.expression) ||
!typescript_1.default.isPropertyAccessExpression(node.expression.expression) ||
node.expression.expression.name.text !== 'toEqual' ||
node.expression.arguments.length !== 1) {
return node;
}
const argument = node.expression.arguments[0];
if (!(0, ast_validation_1.isJasmineCallExpression)(argument, 'arrayWithExactContents') ||
argument.arguments.length !== 1) {
return node;
}
if (!typescript_1.default.isArrayLiteralExpression(argument.arguments[0])) {
const category = 'arrayWithExactContents-dynamic-variable';
reporter.recordTodo(category);
(0, comment_helpers_1.addTodoComment)(node, category);
return node;
}
reporter.reportTransformation(sourceFile, node, 'Transformed `jasmine.arrayWithExactContents()` to `.toHaveLength()` and `.toEqual(expect.arrayContaining())`.');
const expectCall = node.expression.expression.expression;
const arrayLiteral = argument.arguments[0];
const lengthCall = typescript_1.default.factory.createCallExpression((0, ast_helpers_1.createPropertyAccess)(expectCall, 'toHaveLength'), undefined, [typescript_1.default.factory.createNumericLiteral(arrayLiteral.elements.length)]);
const containingCall = typescript_1.default.factory.createCallExpression((0, ast_helpers_1.createPropertyAccess)(expectCall, 'toEqual'), undefined, [
typescript_1.default.factory.createCallExpression((0, ast_helpers_1.createPropertyAccess)('expect', 'arrayContaining'), undefined, [arrayLiteral]),
]);
const lengthStmt = typescript_1.default.factory.createExpressionStatement(lengthCall);
const containingStmt = typescript_1.default.factory.createExpressionStatement(containingCall);
const category = 'arrayWithExactContents-check';
reporter.recordTodo(category);
(0, comment_helpers_1.addTodoComment)(lengthStmt, category);
return [lengthStmt, containingStmt];
}
function transformCalledOnceWith(node, { sourceFile, reporter }) {
if (!typescript_1.default.isExpressionStatement(node)) {
return node;
}
const call = node.expression;
if (!typescript_1.default.isCallExpression(call) ||
!typescript_1.default.isPropertyAccessExpression(call.expression) ||
call.expression.name.text !== 'toHaveBeenCalledOnceWith') {
return node;
}
reporter.reportTransformation(sourceFile, node, 'Transformed `.toHaveBeenCalledOnceWith()` to `.toHaveBeenCalledTimes(1)` and `.toHaveBeenCalledWith()`.');
const expectCall = call.expression.expression;
const args = call.arguments;
const timesCall = typescript_1.default.factory.createCallExpression((0, ast_helpers_1.createPropertyAccess)(expectCall, 'toHaveBeenCalledTimes'), undefined, [typescript_1.default.factory.createNumericLiteral(1)]);
const withCall = typescript_1.default.factory.createCallExpression((0, ast_helpers_1.createPropertyAccess)(expectCall, 'toHaveBeenCalledWith'), undefined, args);
return [
typescript_1.default.factory.createExpressionStatement(timesCall),
typescript_1.default.factory.createExpressionStatement(withCall),
];
}
function transformWithContext(node, { sourceFile, reporter }) {
if (!typescript_1.default.isCallExpression(node) || !typescript_1.default.isPropertyAccessExpression(node.expression)) {
return node;
}
// Traverse the chain of property access expressions to find the .withContext() call
let currentExpression = node.expression;
const propertyChain = [];
while (typescript_1.default.isPropertyAccessExpression(currentExpression)) {
if (!typescript_1.default.isIdentifier(currentExpression.name)) {
// Break if we encounter a private identifier or something else unexpected
return node;
}
propertyChain.push(currentExpression.name);
currentExpression = currentExpression.expression;
}
const withContextCall = currentExpression;
// Check if we found a .withContext() call
if (!typescript_1.default.isCallExpression(withContextCall) ||
!typescript_1.default.isPropertyAccessExpression(withContextCall.expression) ||
!typescript_1.default.isIdentifier(withContextCall.expression.name) ||
withContextCall.expression.name.text !== 'withContext') {
return node;
}
reporter.reportTransformation(sourceFile, withContextCall, 'Transformed `.withContext()` to the `expect(..., message)` syntax.');
const expectCall = withContextCall.expression.expression;
if (!typescript_1.default.isCallExpression(expectCall) ||
!typescript_1.default.isIdentifier(expectCall.expression) ||
expectCall.expression.text !== 'expect') {
return node;
}
const contextMessage = withContextCall.arguments[0];
if (!contextMessage) {
// No message provided, so unwrap the .withContext() call.
let newChain = expectCall;
for (let i = propertyChain.length - 1; i >= 0; i--) {
newChain = typescript_1.default.factory.createPropertyAccessExpression(newChain, propertyChain[i]);
}
return typescript_1.default.factory.updateCallExpression(node, newChain, node.typeArguments, node.arguments);
}
const newExpectArgs = [...expectCall.arguments, contextMessage];
const newExpectCall = typescript_1.default.factory.updateCallExpression(expectCall, expectCall.expression, expectCall.typeArguments, newExpectArgs);
// Rebuild the property access chain
let newExpression = newExpectCall;
for (let i = propertyChain.length - 1; i >= 0; i--) {
newExpression = typescript_1.default.factory.createPropertyAccessExpression(newExpression, propertyChain[i]);
}
return typescript_1.default.factory.updateCallExpression(node, newExpression, node.typeArguments, node.arguments);
}
function transformExpectNothing(node, { sourceFile, reporter }) {
if (!typescript_1.default.isExpressionStatement(node)) {
return node;
}
const call = node.expression;
if (!typescript_1.default.isCallExpression(call) ||
!typescript_1.default.isPropertyAccessExpression(call.expression) ||
!typescript_1.default.isIdentifier(call.expression.name) ||
call.expression.name.text !== 'nothing') {
return node;
}
const expectCall = call.expression.expression;
if (!typescript_1.default.isCallExpression(expectCall) ||
!typescript_1.default.isIdentifier(expectCall.expression) ||
expectCall.expression.text !== 'expect' ||
expectCall.arguments.length > 0) {
return node;
}
// The statement is `expect().nothing()`, which can be removed.
const replacement = typescript_1.default.factory.createEmptyStatement();
const originalText = node.getFullText().trim();
reporter.reportTransformation(sourceFile, node, 'Removed `expect().nothing()` statement.');
const category = 'expect-nothing';
reporter.recordTodo(category);
(0, comment_helpers_1.addTodoComment)(replacement, category);
typescript_1.default.addSyntheticLeadingComment(replacement, typescript_1.default.SyntaxKind.SingleLineCommentTrivia, ` ${originalText}`, true);
return replacement;
}
//# sourceMappingURL=jasmine-matcher.js.map