@schematics/angular
Version:
Schematics specific to Angular
276 lines • 15.7 kB
JavaScript
;
/**
* @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.transformFocusedAndSkippedTests = transformFocusedAndSkippedTests;
exports.transformPending = transformPending;
exports.transformDoneCallback = transformDoneCallback;
/**
* @fileoverview This file contains transformers that convert Jasmine lifecycle functions
* and test setup/teardown patterns to their Vitest equivalents. This includes handling
* focused/skipped tests (fdescribe, fit, xdescribe, xit), pending tests, and asynchronous
* operations that use the `done` callback.
*/
const typescript_1 = __importDefault(require("../../../third_party/github.com/Microsoft/TypeScript/lib/typescript"));
const ast_helpers_1 = require("../utils/ast-helpers");
const comment_helpers_1 = require("../utils/comment-helpers");
const FOCUSED_SKIPPED_RENAMES = new Map([
['fdescribe', { newBase: 'describe', newName: 'only' }],
['fit', { newBase: 'it', newName: 'only' }],
['xdescribe', { newBase: 'describe', newName: 'skip' }],
['xit', { newBase: 'it', newName: 'skip' }],
]);
function transformFocusedAndSkippedTests(node, { sourceFile, reporter }) {
if (!typescript_1.default.isCallExpression(node) || !typescript_1.default.isIdentifier(node.expression)) {
return node;
}
const oldName = node.expression.text;
const rename = FOCUSED_SKIPPED_RENAMES.get(oldName);
if (rename) {
reporter.reportTransformation(sourceFile, node, `Transformed \`${oldName}\` to \`${rename.newBase}.${rename.newName}\`.`);
const newPropAccess = (0, ast_helpers_1.createPropertyAccess)(rename.newBase, rename.newName);
return typescript_1.default.factory.updateCallExpression(node, newPropAccess, node.typeArguments, node.arguments);
}
return node;
}
function transformPending(node, { sourceFile, reporter, tsContext }) {
if (!typescript_1.default.isCallExpression(node) ||
!typescript_1.default.isIdentifier(node.expression) ||
node.expression.text !== 'it') {
return node;
}
const testFn = node.arguments[1];
if (!testFn || (!typescript_1.default.isArrowFunction(testFn) && !typescript_1.default.isFunctionExpression(testFn))) {
return node;
}
let hasPending = false;
const bodyTransformVisitor = (bodyNode) => {
if (typescript_1.default.isExpressionStatement(bodyNode) &&
typescript_1.default.isCallExpression(bodyNode.expression) &&
typescript_1.default.isIdentifier(bodyNode.expression.expression) &&
bodyNode.expression.expression.text === 'pending') {
hasPending = true;
const replacement = typescript_1.default.factory.createEmptyStatement();
const originalText = bodyNode.getFullText().trim();
reporter.reportTransformation(sourceFile, bodyNode, 'Converted `pending()` to a skipped test (`it.skip`).');
const category = 'pending';
reporter.recordTodo(category);
(0, comment_helpers_1.addTodoComment)(replacement, category);
typescript_1.default.addSyntheticLeadingComment(replacement, typescript_1.default.SyntaxKind.SingleLineCommentTrivia, ` ${originalText}`, true);
return replacement;
}
return typescript_1.default.visitEachChild(bodyNode, bodyTransformVisitor, tsContext);
};
const newBody = typescript_1.default.visitNode(testFn.body, bodyTransformVisitor);
if (!hasPending) {
return node;
}
const newExpression = (0, ast_helpers_1.createPropertyAccess)(node.expression, 'skip');
const newTestFn = typescript_1.default.isArrowFunction(testFn)
? typescript_1.default.factory.updateArrowFunction(testFn, testFn.modifiers, testFn.typeParameters, testFn.parameters, testFn.type, testFn.equalsGreaterThanToken, newBody ?? typescript_1.default.factory.createBlock([]))
: typescript_1.default.factory.updateFunctionExpression(testFn, testFn.modifiers, testFn.asteriskToken, testFn.name, testFn.typeParameters, testFn.parameters, testFn.type, newBody ?? typescript_1.default.factory.createBlock([]));
const newArgs = [node.arguments[0], newTestFn, ...node.arguments.slice(2)];
return typescript_1.default.factory.updateCallExpression(node, newExpression, node.typeArguments, newArgs);
}
function transformComplexDoneCallback(node, doneIdentifier, refactorCtx) {
const { sourceFile, reporter } = refactorCtx;
if (!typescript_1.default.isExpressionStatement(node) ||
!typescript_1.default.isCallExpression(node.expression) ||
!typescript_1.default.isPropertyAccessExpression(node.expression.expression)) {
return node;
}
const call = node.expression;
const pae = call.expression;
if (!typescript_1.default.isPropertyAccessExpression(pae)) {
return node;
}
if (pae.name.text !== 'then' || call.arguments.length !== 1) {
return node;
}
const thenCallback = call.arguments[0];
if (!typescript_1.default.isArrowFunction(thenCallback) && !typescript_1.default.isFunctionExpression(thenCallback)) {
return node;
}
// Re-create the .then() call but with a modified callback that has `done()` removed.
const thenCallbackBody = typescript_1.default.isBlock(thenCallback.body)
? thenCallback.body
: typescript_1.default.factory.createBlock([typescript_1.default.factory.createExpressionStatement(thenCallback.body)]);
const newStatements = thenCallbackBody.statements.filter((stmt) => {
return (!typescript_1.default.isExpressionStatement(stmt) ||
!typescript_1.default.isCallExpression(stmt.expression) ||
!typescript_1.default.isIdentifier(stmt.expression.expression) ||
stmt.expression.expression.text !== doneIdentifier.text);
});
if (newStatements.length === thenCallbackBody.statements.length) {
// No "done()" call was removed, so don't transform.
return node;
}
reporter.reportTransformation(sourceFile, node, 'Transformed promise `.then()` with `done()` to `await`.');
const newThenCallback = typescript_1.default.isArrowFunction(thenCallback)
? typescript_1.default.factory.updateArrowFunction(thenCallback, thenCallback.modifiers, thenCallback.typeParameters, thenCallback.parameters, thenCallback.type, thenCallback.equalsGreaterThanToken, typescript_1.default.factory.updateBlock(thenCallbackBody, newStatements))
: typescript_1.default.factory.updateFunctionExpression(thenCallback, thenCallback.modifiers, thenCallback.asteriskToken, thenCallback.name, thenCallback.typeParameters, thenCallback.parameters, thenCallback.type, typescript_1.default.factory.updateBlock(thenCallbackBody, newStatements));
const newCall = typescript_1.default.factory.updateCallExpression(call, call.expression, call.typeArguments, [
newThenCallback,
]);
return typescript_1.default.factory.createExpressionStatement(typescript_1.default.factory.createAwaitExpression(newCall));
}
function transformPromiseBasedDone(callExpr, doneIdentifier, refactorCtx) {
const { sourceFile, reporter } = refactorCtx;
if (typescript_1.default.isPropertyAccessExpression(callExpr.expression) &&
(callExpr.expression.name.text === 'then' || callExpr.expression.name.text === 'catch')) {
const promiseHandler = callExpr.arguments[0];
if (promiseHandler) {
let isDoneHandler = false;
// promise.then(done)
if (typescript_1.default.isIdentifier(promiseHandler) && promiseHandler.text === doneIdentifier.text) {
isDoneHandler = true;
}
// promise.catch(done.fail)
if (typescript_1.default.isPropertyAccessExpression(promiseHandler) &&
typescript_1.default.isIdentifier(promiseHandler.expression) &&
promiseHandler.expression.text === doneIdentifier.text &&
promiseHandler.name.text === 'fail') {
isDoneHandler = true;
}
// promise.then(() => done())
if (typescript_1.default.isArrowFunction(promiseHandler) && !promiseHandler.parameters.length) {
const body = promiseHandler.body;
if (typescript_1.default.isCallExpression(body) &&
typescript_1.default.isIdentifier(body.expression) &&
body.expression.text === doneIdentifier.text) {
isDoneHandler = true;
}
if (typescript_1.default.isBlock(body) && body.statements.length === 1) {
const stmt = body.statements[0];
if (typescript_1.default.isExpressionStatement(stmt) &&
typescript_1.default.isCallExpression(stmt.expression) &&
typescript_1.default.isIdentifier(stmt.expression.expression) &&
stmt.expression.expression.text === doneIdentifier.text) {
isDoneHandler = true;
}
}
}
if (isDoneHandler) {
reporter.reportTransformation(sourceFile, callExpr, 'Transformed promise `.then(done)` to `await`.');
return typescript_1.default.factory.createExpressionStatement(typescript_1.default.factory.createAwaitExpression(callExpr.expression.expression));
}
}
}
return undefined;
}
function countDoneUsages(node, doneIdentifier) {
let count = 0;
const visitor = (n) => {
if (typescript_1.default.isIdentifier(n) && n.text === doneIdentifier.text) {
count++;
}
typescript_1.default.forEachChild(n, visitor);
};
typescript_1.default.forEachChild(node, visitor);
return count;
}
function transformDoneCallback(node, refactorCtx) {
const { sourceFile, reporter, tsContext } = refactorCtx;
if (!typescript_1.default.isCallExpression(node) ||
!typescript_1.default.isIdentifier(node.expression) ||
!['it', 'beforeEach', 'afterEach', 'beforeAll', 'afterAll'].includes(node.expression.text)) {
return node;
}
const functionArg = node.arguments.find((arg) => typescript_1.default.isArrowFunction(arg) || typescript_1.default.isFunctionExpression(arg));
if (!functionArg || (!typescript_1.default.isArrowFunction(functionArg) && !typescript_1.default.isFunctionExpression(functionArg))) {
return node;
}
if (functionArg.parameters.length !== 1) {
return node;
}
const doneParam = functionArg.parameters[0];
if (!typescript_1.default.isIdentifier(doneParam.name)) {
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) => {
const complexTransformed = transformComplexDoneCallback(bodyNode, doneIdentifier, refactorCtx);
if (complexTransformed !== bodyNode) {
doneWasUsed = true;
handledUsages++; // complex transform handles one usage
return complexTransformed;
}
if (typescript_1.default.isExpressionStatement(bodyNode) && typescript_1.default.isCallExpression(bodyNode.expression)) {
const callExpr = bodyNode.expression;
// Transform `done.fail('message')` to `throw new Error('message')`
if (typescript_1.default.isPropertyAccessExpression(callExpr.expression) &&
typescript_1.default.isIdentifier(callExpr.expression.expression) &&
callExpr.expression.expression.text === doneIdentifier.text &&
callExpr.expression.name.text === 'fail') {
doneWasUsed = true;
handledUsages++;
reporter.reportTransformation(sourceFile, bodyNode, 'Transformed `done.fail()` to `throw new Error()`.');
const errorArgs = callExpr.arguments.length > 0 ? [callExpr.arguments[0]] : [];
return typescript_1.default.factory.createThrowStatement(typescript_1.default.factory.createNewExpression(typescript_1.default.factory.createIdentifier('Error'), undefined, errorArgs));
}
// Transform `promise.then(done)` or `promise.catch(done.fail)` to `await promise`
const promiseTransformed = transformPromiseBasedDone(callExpr, doneIdentifier, refactorCtx);
if (promiseTransformed) {
doneWasUsed = true;
handledUsages++;
return promiseTransformed;
}
// Remove `done()`
if (typescript_1.default.isIdentifier(callExpr.expression) &&
callExpr.expression.text === doneIdentifier.text) {
doneWasUsed = true;
handledUsages++;
return typescript_1.default.setTextRange(typescript_1.default.factory.createEmptyStatement(), callExpr.expression);
}
}
return typescript_1.default.visitEachChild(bodyNode, bodyVisitor, tsContext);
};
const newBody = typescript_1.default.visitNode(functionArg.body, (node) => {
if (typescript_1.default.isBlock(node)) {
const newStatements = node.statements.flatMap((stmt) => bodyVisitor(stmt));
return typescript_1.default.factory.updateBlock(node, newStatements.filter((s) => !!s));
}
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);
(0, comment_helpers_1.addTodoComment)(node, category);
return node;
}
if (!doneWasUsed) {
return node;
}
reporter.reportTransformation(sourceFile, node, `Converted test with \`done\` callback to an \`async\` test.`);
const newModifiers = [
typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.AsyncKeyword),
...(typescript_1.default.getModifiers(functionArg) ?? []).filter((mod) => mod.kind !== typescript_1.default.SyntaxKind.AsyncKeyword),
];
let newFunction;
if (typescript_1.default.isArrowFunction(functionArg)) {
newFunction = typescript_1.default.factory.updateArrowFunction(functionArg, newModifiers, functionArg.typeParameters, [], // remove parameters
functionArg.type, functionArg.equalsGreaterThanToken, newBody ?? typescript_1.default.factory.createBlock([]));
}
else {
// isFunctionExpression
newFunction = typescript_1.default.factory.updateFunctionExpression(functionArg, newModifiers, functionArg.asteriskToken, functionArg.name, functionArg.typeParameters, [], // remove parameters
functionArg.type, newBody ?? typescript_1.default.factory.createBlock([]));
}
const newArgs = node.arguments.map((arg) => (arg === functionArg ? newFunction : arg));
return typescript_1.default.factory.updateCallExpression(node, node.expression, node.typeArguments, newArgs);
}
//# sourceMappingURL=jasmine-lifecycle.js.map