@schematics/angular
Version:
Schematics specific to Angular
316 lines • 18 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.transformSpies = transformSpies;
exports.transformCreateSpyObj = transformCreateSpyObj;
exports.transformSpyReset = transformSpyReset;
exports.transformSpyCallInspection = transformSpyCallInspection;
/**
* @fileoverview This file contains transformers dedicated to converting Jasmine's spying
* functionality to Vitest's mocking APIs. It handles the creation of spies (`spyOn`,
* `createSpy`, `createSpyObj`), spy strategies (`and.returnValue`, `and.callFake`),
* and the inspection of spy calls (`spy.calls.reset`, `spy.calls.mostRecent`).
*/
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");
function transformSpies(node, refactorCtx) {
const { sourceFile, reporter, pendingVitestValueImports } = refactorCtx;
if (!typescript_1.default.isCallExpression(node)) {
return node;
}
if (typescript_1.default.isIdentifier(node.expression) &&
(node.expression.text === 'spyOn' || node.expression.text === 'spyOnProperty')) {
(0, ast_helpers_1.addVitestValueImport)(pendingVitestValueImports, 'vi');
reporter.reportTransformation(sourceFile, node, `Transformed \`${node.expression.text}\` to \`vi.spyOn\`.`);
return typescript_1.default.factory.updateCallExpression(node, (0, ast_helpers_1.createPropertyAccess)('vi', 'spyOn'), node.typeArguments, node.arguments);
}
if (typescript_1.default.isPropertyAccessExpression(node.expression)) {
const pae = node.expression;
if (typescript_1.default.isPropertyAccessExpression(pae.expression) &&
typescript_1.default.isIdentifier(pae.expression.name) &&
pae.expression.name.text === 'and') {
const spyCall = pae.expression.expression;
let newMethodName;
if (typescript_1.default.isIdentifier(pae.name)) {
const strategyName = pae.name.text;
switch (strategyName) {
case 'returnValue':
newMethodName = 'mockReturnValue';
break;
case 'resolveTo':
newMethodName = 'mockResolvedValue';
break;
case 'rejectWith':
newMethodName = 'mockRejectedValue';
break;
case 'returnValues': {
reporter.reportTransformation(sourceFile, node, 'Transformed `.and.returnValues()` to chained `.mockReturnValueOnce()` calls.');
const returnValues = node.arguments;
if (returnValues.length === 0) {
// No values, so it's a no-op. Just transform the spyOn call.
return transformSpies(spyCall, refactorCtx);
}
// spy.and.returnValues(a, b) -> spy.mockReturnValueOnce(a).mockReturnValueOnce(b)
let chainedCall = spyCall;
for (const value of returnValues) {
const mockCall = typescript_1.default.factory.createCallExpression((0, ast_helpers_1.createPropertyAccess)(chainedCall, 'mockReturnValueOnce'), undefined, [value]);
chainedCall = mockCall;
}
return chainedCall;
}
case 'callFake':
newMethodName = 'mockImplementation';
break;
case 'callThrough':
reporter.reportTransformation(sourceFile, node, 'Removed redundant `.and.callThrough()` call.');
return transformSpies(spyCall, refactorCtx); // .and.callThrough() is redundant, just transform spyOn.
case 'stub': {
reporter.reportTransformation(sourceFile, node, 'Transformed `.and.stub()` to `.mockImplementation()`.');
const newExpression = (0, ast_helpers_1.createPropertyAccess)(spyCall, 'mockImplementation');
const arrowFn = typescript_1.default.factory.createArrowFunction(undefined, undefined, [], undefined, typescript_1.default.factory.createToken(typescript_1.default.SyntaxKind.EqualsGreaterThanToken), typescript_1.default.factory.createBlock([], /* multiline */ true));
return typescript_1.default.factory.createCallExpression(newExpression, undefined, [arrowFn]);
}
case 'throwError': {
reporter.reportTransformation(sourceFile, node, 'Transformed `.and.throwError()` to `.mockImplementation()`.');
const errorArg = node.arguments[0];
const throwStatement = typescript_1.default.factory.createThrowStatement(typescript_1.default.isNewExpression(errorArg)
? errorArg
: typescript_1.default.factory.createNewExpression(typescript_1.default.factory.createIdentifier('Error'), undefined, node.arguments));
const arrowFunction = typescript_1.default.factory.createArrowFunction(undefined, undefined, [], undefined, typescript_1.default.factory.createToken(typescript_1.default.SyntaxKind.EqualsGreaterThanToken), typescript_1.default.factory.createBlock([throwStatement], true));
const newExpression = (0, ast_helpers_1.createPropertyAccess)(spyCall, 'mockImplementation');
return typescript_1.default.factory.createCallExpression(newExpression, undefined, [arrowFunction]);
}
default: {
const category = 'unsupported-spy-strategy';
reporter.recordTodo(category);
(0, comment_helpers_1.addTodoComment)(node, category, { name: strategyName });
break;
}
}
if (newMethodName) {
reporter.reportTransformation(sourceFile, node, `Transformed spy strategy \`.and.${strategyName}()\` to \`.${newMethodName}()\`.`);
const newExpression = typescript_1.default.factory.updatePropertyAccessExpression(pae, spyCall, typescript_1.default.factory.createIdentifier(newMethodName));
return typescript_1.default.factory.updateCallExpression(node, newExpression, node.typeArguments, node.arguments);
}
}
}
}
const jasmineMethodName = (0, ast_validation_1.getJasmineMethodName)(node);
switch (jasmineMethodName) {
case 'createSpy':
(0, ast_helpers_1.addVitestValueImport)(pendingVitestValueImports, 'vi');
reporter.reportTransformation(sourceFile, node, 'Transformed `jasmine.createSpy()` to `vi.fn()`.');
// jasmine.createSpy(name, originalFn) -> vi.fn(originalFn)
return (0, ast_helpers_1.createViCallExpression)('fn', node.arguments.length > 1 ? [node.arguments[1]] : []);
case 'spyOnAllFunctions': {
reporter.reportTransformation(sourceFile, node, 'Found unsupported `jasmine.spyOnAllFunctions()`.');
const category = 'spyOnAllFunctions';
reporter.recordTodo(category);
(0, comment_helpers_1.addTodoComment)(node, category);
return node;
}
}
return node;
}
function transformCreateSpyObj(node, { sourceFile, reporter, pendingVitestValueImports }) {
if (!(0, ast_validation_1.isJasmineCallExpression)(node, 'createSpyObj')) {
return node;
}
(0, ast_helpers_1.addVitestValueImport)(pendingVitestValueImports, 'vi');
reporter.reportTransformation(sourceFile, node, 'Transformed `jasmine.createSpyObj()` to an object literal with `vi.fn()`.');
const firstArg = node.arguments[0];
const hasBaseName = typescript_1.default.isStringLiteral(firstArg);
const baseName = hasBaseName ? firstArg.text : undefined;
const methods = hasBaseName ? node.arguments[1] : firstArg;
const propertiesArg = hasBaseName ? node.arguments[2] : node.arguments[1];
let properties = [];
if (node.arguments.length < 2 && hasBaseName) {
const category = 'createSpyObj-single-argument';
reporter.recordTodo(category);
(0, comment_helpers_1.addTodoComment)(node, category);
return node;
}
if (typescript_1.default.isArrayLiteralExpression(methods)) {
properties = createSpyObjWithArray(methods, baseName);
}
else if (typescript_1.default.isObjectLiteralExpression(methods)) {
properties = createSpyObjWithObject(methods, baseName);
}
else {
const category = 'createSpyObj-dynamic-variable';
reporter.recordTodo(category);
(0, comment_helpers_1.addTodoComment)(node, category);
return node;
}
if (propertiesArg) {
if (typescript_1.default.isObjectLiteralExpression(propertiesArg)) {
properties.push(...propertiesArg.properties);
}
else {
const category = 'createSpyObj-dynamic-property-map';
reporter.recordTodo(category);
(0, comment_helpers_1.addTodoComment)(node, category);
}
}
return typescript_1.default.factory.createObjectLiteralExpression(properties, true);
}
function createSpyObjWithArray(methods, baseName) {
return methods.elements
.map((element) => {
if (typescript_1.default.isStringLiteral(element)) {
const mockFn = (0, ast_helpers_1.createViCallExpression)('fn');
const methodName = element.text;
let finalExpression = mockFn;
if (baseName) {
finalExpression = typescript_1.default.factory.createCallExpression((0, ast_helpers_1.createPropertyAccess)(finalExpression, 'mockName'), undefined, [typescript_1.default.factory.createStringLiteral(`${baseName}.${methodName}`)]);
}
return typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createIdentifier(methodName), finalExpression);
}
return undefined;
})
.filter((p) => !!p);
}
function createSpyObjWithObject(methods, baseName) {
return methods.properties
.map((prop) => {
if (typescript_1.default.isPropertyAssignment(prop) && typescript_1.default.isIdentifier(prop.name)) {
const methodName = prop.name.text;
const returnValue = prop.initializer;
let mockFn = (0, ast_helpers_1.createViCallExpression)('fn');
if (baseName) {
mockFn = typescript_1.default.factory.createCallExpression((0, ast_helpers_1.createPropertyAccess)(mockFn, 'mockName'), undefined, [typescript_1.default.factory.createStringLiteral(`${baseName}.${methodName}`)]);
}
const mockReturnValue = (0, ast_helpers_1.createPropertyAccess)(mockFn, 'mockReturnValue');
return typescript_1.default.factory.createPropertyAssignment(typescript_1.default.factory.createIdentifier(methodName), typescript_1.default.factory.createCallExpression(mockReturnValue, undefined, [returnValue]));
}
return undefined;
})
.filter((p) => !!p);
}
function transformSpyReset(node, { sourceFile, reporter }) {
if (typescript_1.default.isCallExpression(node) &&
typescript_1.default.isPropertyAccessExpression(node.expression) &&
typescript_1.default.isIdentifier(node.expression.name) &&
node.expression.name.text === 'reset' &&
typescript_1.default.isPropertyAccessExpression(node.expression.expression)) {
const callsPae = node.expression.expression;
if (typescript_1.default.isIdentifier(callsPae.name) && callsPae.name.text === 'calls') {
reporter.reportTransformation(sourceFile, node, 'Transformed `spy.calls.reset()` to `.mockClear()`.');
const spyIdentifier = callsPae.expression;
const newExpression = (0, ast_helpers_1.createPropertyAccess)(spyIdentifier, 'mockClear');
return typescript_1.default.factory.updateCallExpression(node, newExpression, node.typeArguments, []);
}
}
return node;
}
function getSpyIdentifierFromCalls(node) {
if (typescript_1.default.isIdentifier(node.name) && node.name.text === 'calls') {
return node.expression;
}
return undefined;
}
function createMockedSpyMockProperty(spyIdentifier, pendingVitestValueImports) {
(0, ast_helpers_1.addVitestValueImport)(pendingVitestValueImports, 'vi');
const mockedSpy = typescript_1.default.factory.createCallExpression((0, ast_helpers_1.createPropertyAccess)('vi', 'mocked'), undefined, [spyIdentifier]);
return (0, ast_helpers_1.createPropertyAccess)(mockedSpy, 'mock');
}
function transformMostRecentArgs(node, { sourceFile, reporter, pendingVitestValueImports }) {
// Check 1: Is it a property access for `.args`?
if (!typescript_1.default.isPropertyAccessExpression(node) ||
!typescript_1.default.isIdentifier(node.name) ||
node.name.text !== 'args') {
return node;
}
// Check 2: Is the preceding expression a call expression?
const mostRecentCall = node.expression;
if (!typescript_1.default.isCallExpression(mostRecentCall) ||
!typescript_1.default.isPropertyAccessExpression(mostRecentCall.expression)) {
return node;
}
// Check 3: Is it a call to `.mostRecent`?
const mostRecentPae = mostRecentCall.expression;
if (!typescript_1.default.isIdentifier(mostRecentPae.name) ||
mostRecentPae.name.text !== 'mostRecent' ||
!typescript_1.default.isPropertyAccessExpression(mostRecentPae.expression)) {
return node;
}
// Check 4: Can we get the spy identifier from `spy.calls`?
const spyIdentifier = getSpyIdentifierFromCalls(mostRecentPae.expression);
if (!spyIdentifier) {
return node;
}
// If all checks pass, perform the transformation.
reporter.reportTransformation(sourceFile, node, 'Transformed `spy.calls.mostRecent().args` to `vi.mocked(spy).mock.lastCall`.');
const mockProperty = createMockedSpyMockProperty(spyIdentifier, pendingVitestValueImports);
return (0, ast_helpers_1.createPropertyAccess)(mockProperty, 'lastCall');
}
function transformSpyCallInspection(node, refactorCtx) {
const mostRecentArgsTransformed = transformMostRecentArgs(node, refactorCtx);
if (mostRecentArgsTransformed !== node) {
return mostRecentArgsTransformed;
}
if (!typescript_1.default.isCallExpression(node) || !typescript_1.default.isPropertyAccessExpression(node.expression)) {
return node;
}
const { sourceFile, reporter, pendingVitestValueImports } = refactorCtx;
const pae = node.expression; // e.g., mySpy.calls.count
const spyIdentifier = typescript_1.default.isPropertyAccessExpression(pae.expression)
? getSpyIdentifierFromCalls(pae.expression)
: undefined;
if (spyIdentifier) {
const mockProperty = createMockedSpyMockProperty(spyIdentifier, pendingVitestValueImports);
const callsProperty = (0, ast_helpers_1.createPropertyAccess)(mockProperty, 'calls');
const callName = pae.name.text;
let newExpression;
let message;
switch (callName) {
case 'any':
message = 'Transformed `spy.calls.any()` to a check on `mock.calls.length`.';
newExpression = typescript_1.default.factory.createBinaryExpression((0, ast_helpers_1.createPropertyAccess)(callsProperty, 'length'), typescript_1.default.SyntaxKind.GreaterThanToken, typescript_1.default.factory.createNumericLiteral(0));
break;
case 'count':
message = 'Transformed `spy.calls.count()` to `mock.calls.length`.';
newExpression = (0, ast_helpers_1.createPropertyAccess)(callsProperty, 'length');
break;
case 'first':
message = 'Transformed `spy.calls.first()` to `mock.calls[0]`.';
newExpression = typescript_1.default.factory.createElementAccessExpression(callsProperty, 0);
break;
case 'all':
case 'allArgs':
message = `Transformed \`spy.calls.${callName}()\` to \`mock.calls\`.`;
newExpression = callsProperty;
break;
case 'argsFor':
message = 'Transformed `spy.calls.argsFor()` to `mock.calls[i]`.';
newExpression = typescript_1.default.factory.createElementAccessExpression(callsProperty, node.arguments[0]);
break;
case 'mostRecent':
if (!typescript_1.default.isPropertyAccessExpression(node.parent) ||
!typescript_1.default.isIdentifier(node.parent.name) ||
node.parent.name.text !== 'args') {
const category = 'mostRecent-without-args';
reporter.recordTodo(category);
(0, comment_helpers_1.addTodoComment)(node, category);
}
return node;
}
if (newExpression && message) {
reporter.reportTransformation(sourceFile, node, message);
return newExpression;
}
}
return node;
}
//# sourceMappingURL=jasmine-spy.js.map