Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/schematics/angular/refactor/jasmine-vitest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ export default function (options: Schema): Rule {
for (const file of files) {
reporter.incrementScannedFiles();
const content = tree.readText(file);
const newContent = transformJasmineToVitest(file, content, reporter);
const newContent = transformJasmineToVitest(file, content, reporter, {
addImports: !!options.addImports,
});

if (content !== newContent) {
tree.overwrite(file, newContent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
"type": "boolean",
"description": "Enable verbose logging to see detailed information about the transformations being applied.",
"default": false
},
"addImports": {
"type": "boolean",
"description": "Whether to add imports for the Vitest API. The Angular `unit-test` system automatically uses the Vitest globals option, which means explicit imports for global APIs like `describe`, `it`, `expect`, and `vi` are often not strictly necessary unless Vitest has been configured not to use globals.",
"default": false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { RefactorReporter } from './utils/refactor-reporter';
async function expectTransformation(input: string, expected: string): Promise<void> {
const logger = new logging.NullLogger();
const reporter = new RefactorReporter(logger);
const transformed = transformJasmineToVitest('spec.ts', input, reporter);
const transformed = transformJasmineToVitest('spec.ts', input, reporter, { addImports: false });
const formattedTransformed = await format(transformed, { parser: 'typescript' });
const formattedExpected = await format(expected, { parser: 'typescript' });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
transformSpyReset,
} from './transformers/jasmine-spy';
import { transformJasmineTypes } from './transformers/jasmine-type';
import { getVitestAutoImports } from './utils/ast-helpers';
import { addVitestValueImport, getVitestAutoImports } from './utils/ast-helpers';
import { RefactorContext } from './utils/refactor-context';
import { RefactorReporter } from './utils/refactor-reporter';

Expand All @@ -61,14 +61,17 @@ function restoreBlankLines(content: string): string {
/**
* Transforms a string of Jasmine test code to Vitest test code.
* This is the main entry point for the transformation.
* @param filePath The path to the file being transformed.
* @param content The source code to transform.
* @param reporter The reporter to track TODOs.
* @param options Transformation options.
* @returns The transformed code.
*/
export function transformJasmineToVitest(
filePath: string,
content: string,
reporter: RefactorReporter,
options: { addImports: boolean },
): string {
const contentWithPlaceholders = preserveBlankLines(content);

Expand All @@ -80,20 +83,29 @@ export function transformJasmineToVitest(
ts.ScriptKind.TS,
);

const pendingVitestImports = new Set<string>();
const pendingVitestValueImports = new Set<string>();
const pendingVitestTypeImports = new Set<string>();
const transformer: ts.TransformerFactory<ts.SourceFile> = (context) => {
const refactorCtx: RefactorContext = {
sourceFile,
reporter,
tsContext: context,
pendingVitestImports,
pendingVitestValueImports,
pendingVitestTypeImports,
};

const visitor: ts.Visitor = (node) => {
let transformedNode: ts.Node | readonly ts.Node[] = node;

// Transform the node itself based on its type
if (ts.isCallExpression(transformedNode)) {
if (options.addImports && ts.isIdentifier(transformedNode.expression)) {
const name = transformedNode.expression.text;
if (name === 'describe' || name === 'it' || name === 'expect') {
addVitestValueImport(pendingVitestValueImports, name);
}
}

const transformations = [
// **Stage 1: High-Level & Context-Sensitive Transformations**
// These transformers often wrap or fundamentally change the nature of the call,
Expand Down Expand Up @@ -171,16 +183,29 @@ export function transformJasmineToVitest(
const result = ts.transform(sourceFile, [transformer]);
let transformedSourceFile = result.transformed[0];

if (transformedSourceFile === sourceFile && !reporter.hasTodos && !pendingVitestImports.size) {
const hasPendingValueImports = pendingVitestValueImports.size > 0;
const hasPendingTypeImports = pendingVitestTypeImports.size > 0;

if (
transformedSourceFile === sourceFile &&
!reporter.hasTodos &&
!hasPendingValueImports &&
!hasPendingTypeImports
) {
return content;
}

const vitestImport = getVitestAutoImports(pendingVitestImports);
if (vitestImport) {
transformedSourceFile = ts.factory.updateSourceFile(transformedSourceFile, [
vitestImport,
...transformedSourceFile.statements,
]);
if (hasPendingTypeImports || (options.addImports && hasPendingValueImports)) {
const vitestImport = getVitestAutoImports(
options.addImports ? pendingVitestValueImports : new Set(),
pendingVitestTypeImports,
);
if (vitestImport) {
transformedSourceFile = ts.factory.updateSourceFile(transformedSourceFile, [
vitestImport,
...transformedSourceFile.statements,
]);
}
}

const printer = ts.createPrinter();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @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
*/

import { expectTransformation } from './test-helpers';

describe('Jasmine to Vitest Transformer', () => {
describe('addImports option', () => {
it('should add value imports when addImports is true', async () => {
const input = `spyOn(foo, 'bar');`;
const expected = `
import { vi } from 'vitest';
vi.spyOn(foo, 'bar');
`;
await expectTransformation(input, expected, true);
});

it('should generate a single, combined import for value and type imports when addImports is true', async () => {
const input = `
let mySpy: jasmine.Spy;
spyOn(foo, 'bar');
`;
const expected = `
import { type Mock, vi } from 'vitest';

let mySpy: Mock;
vi.spyOn(foo, 'bar');
`;
await expectTransformation(input, expected, true);
});

it('should only add type imports when addImports is false', async () => {
const input = `
let mySpy: jasmine.Spy;
spyOn(foo, 'bar');
`;
const expected = `
import type { Mock } from 'vitest';

let mySpy: Mock;
vi.spyOn(foo, 'bar');
`;
await expectTransformation(input, expected, false);
});

it('should not add an import if no Vitest APIs are used, even when addImports is true', async () => {
const input = `const a = 1;`;
const expected = `const a = 1;`;
await expectTransformation(input, expected, true);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ import { RefactorReporter } from './utils/refactor-reporter';
* @param input The Jasmine code snippet to be transformed.
* @param expected The expected Vitest code snippet after transformation.
*/
export async function expectTransformation(input: string, expected: string): Promise<void> {
export async function expectTransformation(
input: string,
expected: string,
addImports = false,
): Promise<void> {
const logger = new logging.NullLogger();
const reporter = new RefactorReporter(logger);
const transformed = transformJasmineToVitest('spec.ts', input, reporter);
const transformed = transformJasmineToVitest('spec.ts', input, reporter, { addImports });
const formattedTransformed = await format(transformed, { parser: 'typescript' });
const formattedExpected = await format(expected, { parser: 'typescript' });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
*/

import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { createExpectCallExpression, createPropertyAccess } from '../utils/ast-helpers';
import {
addVitestValueImport,
createExpectCallExpression,
createPropertyAccess,
} from '../utils/ast-helpers';
import { getJasmineMethodName, isJasmineCallExpression } from '../utils/ast-validation';
import { addTodoComment } from '../utils/comment-helpers';
import { RefactorContext } from '../utils/refactor-context';
Expand Down Expand Up @@ -94,7 +98,7 @@ const ASYMMETRIC_MATCHER_NAMES: ReadonlyArray<string> = [

export function transformAsymmetricMatchers(
node: ts.Node,
{ sourceFile, reporter }: RefactorContext,
{ sourceFile, reporter, pendingVitestValueImports }: RefactorContext,
): ts.Node {
if (
ts.isPropertyAccessExpression(node) &&
Expand All @@ -103,6 +107,7 @@ export function transformAsymmetricMatchers(
) {
const matcherName = node.name.text;
if (ASYMMETRIC_MATCHER_NAMES.includes(matcherName)) {
addVitestValueImport(pendingVitestValueImports, 'expect');
reporter.reportTransformation(
sourceFile,
node,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
*/

import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { createViCallExpression } from '../utils/ast-helpers';
import { addVitestValueImport, createViCallExpression } from '../utils/ast-helpers';
import { getJasmineMethodName, isJasmineCallExpression } from '../utils/ast-validation';
import { addTodoComment } from '../utils/comment-helpers';
import { RefactorContext } from '../utils/refactor-context';
import { TodoCategory } from '../utils/todo-notes';

export function transformTimerMocks(
node: ts.Node,
{ sourceFile, reporter }: RefactorContext,
{ sourceFile, reporter, pendingVitestValueImports }: RefactorContext,
): ts.Node {
if (
!ts.isCallExpression(node) ||
Expand Down Expand Up @@ -55,6 +55,7 @@ export function transformTimerMocks(
}

if (newMethodName) {
addVitestValueImport(pendingVitestValueImports, 'vi');
reporter.reportTransformation(
sourceFile,
node,
Expand Down Expand Up @@ -94,7 +95,7 @@ export function transformFail(node: ts.Node, { sourceFile, reporter }: RefactorC

export function transformDefaultTimeoutInterval(
node: ts.Node,
{ sourceFile, reporter }: RefactorContext,
{ sourceFile, reporter, pendingVitestValueImports }: RefactorContext,
): ts.Node {
if (
ts.isExpressionStatement(node) &&
Expand All @@ -108,6 +109,7 @@ export function transformDefaultTimeoutInterval(
assignment.left.expression.text === 'jasmine' &&
assignment.left.name.text === 'DEFAULT_TIMEOUT_INTERVAL'
) {
addVitestValueImport(pendingVitestValueImports, 'vi');
reporter.reportTransformation(
sourceFile,
node,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@
*/

import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { createPropertyAccess, createViCallExpression } from '../utils/ast-helpers';
import {
addVitestValueImport,
createPropertyAccess,
createViCallExpression,
} from '../utils/ast-helpers';
import { getJasmineMethodName, isJasmineCallExpression } from '../utils/ast-validation';
import { addTodoComment } from '../utils/comment-helpers';
import { RefactorContext } from '../utils/refactor-context';

export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.Node {
const { sourceFile, reporter } = refactorCtx;
const { sourceFile, reporter, pendingVitestValueImports } = refactorCtx;
if (!ts.isCallExpression(node)) {
return node;
}
Expand All @@ -29,6 +33,7 @@ export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.
ts.isIdentifier(node.expression) &&
(node.expression.text === 'spyOn' || node.expression.text === 'spyOnProperty')
) {
addVitestValueImport(pendingVitestValueImports, 'vi');
reporter.reportTransformation(
sourceFile,
node,
Expand Down Expand Up @@ -181,6 +186,7 @@ export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.
const jasmineMethodName = getJasmineMethodName(node);
switch (jasmineMethodName) {
case 'createSpy':
addVitestValueImport(pendingVitestValueImports, 'vi');
reporter.reportTransformation(
sourceFile,
node,
Expand Down Expand Up @@ -208,12 +214,13 @@ export function transformSpies(node: ts.Node, refactorCtx: RefactorContext): ts.

export function transformCreateSpyObj(
node: ts.Node,
{ sourceFile, reporter }: RefactorContext,
{ sourceFile, reporter, pendingVitestValueImports }: RefactorContext,
): ts.Node {
if (!isJasmineCallExpression(node, 'createSpyObj')) {
return node;
}

addVitestValueImport(pendingVitestValueImports, 'vi');
reporter.reportTransformation(
sourceFile,
node,
Expand Down Expand Up @@ -328,7 +335,11 @@ function getSpyIdentifierFromCalls(node: ts.PropertyAccessExpression): ts.Expres
return undefined;
}

function createMockedSpyMockProperty(spyIdentifier: ts.Expression): ts.PropertyAccessExpression {
function createMockedSpyMockProperty(
spyIdentifier: ts.Expression,
pendingVitestValueImports: Set<string>,
): ts.PropertyAccessExpression {
addVitestValueImport(pendingVitestValueImports, 'vi');
const mockedSpy = ts.factory.createCallExpression(
createPropertyAccess('vi', 'mocked'),
undefined,
Expand All @@ -340,7 +351,7 @@ function createMockedSpyMockProperty(spyIdentifier: ts.Expression): ts.PropertyA

function transformMostRecentArgs(
node: ts.Node,
{ sourceFile, reporter }: RefactorContext,
{ sourceFile, reporter, pendingVitestValueImports }: RefactorContext,
): ts.Node {
// Check 1: Is it a property access for `.args`?
if (
Expand Down Expand Up @@ -382,7 +393,7 @@ function transformMostRecentArgs(
node,
'Transformed `spy.calls.mostRecent().args` to `vi.mocked(spy).mock.lastCall`.',
);
const mockProperty = createMockedSpyMockProperty(spyIdentifier);
const mockProperty = createMockedSpyMockProperty(spyIdentifier, pendingVitestValueImports);

return createPropertyAccess(mockProperty, 'lastCall');
}
Expand All @@ -397,15 +408,15 @@ export function transformSpyCallInspection(node: ts.Node, refactorCtx: RefactorC
return node;
}

const { sourceFile, reporter } = refactorCtx;
const { sourceFile, reporter, pendingVitestValueImports } = refactorCtx;

const pae = node.expression; // e.g., mySpy.calls.count
const spyIdentifier = ts.isPropertyAccessExpression(pae.expression)
? getSpyIdentifierFromCalls(pae.expression)
: undefined;

if (spyIdentifier) {
const mockProperty = createMockedSpyMockProperty(spyIdentifier);
const mockProperty = createMockedSpyMockProperty(spyIdentifier, pendingVitestValueImports);
const callsProperty = createPropertyAccess(mockProperty, 'calls');

const callName = pae.name.text;
Expand Down
Loading