Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export default function (options: Schema): Rule {
const newContent = transformJasmineToVitest(file, content, reporter, {
addImports: !!options.addImports,
browserMode: !!options.browerMode,
fakeAsync: !!options.fakeAsync,
});

if (content !== newContent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
"description": "Whether the tests are intended to run in browser mode. If true, the `toHaveClass` assertions are left as is because Vitest browser mode has such an assertion. Otherwise they're migrated to an equivalent assertion.",
"default": false
},
"fakeAsync": {
Comment thread
yjaaidi marked this conversation as resolved.
"type": "boolean",
"description": "Whether to transform `fakeAsync` tests to Vitest fake timers.",
"default": false
},
"report": {
"type": "boolean",
"description": "Whether to generate a summary report file (jasmine-vitest-<date>.md) in the project root.",
Expand Down
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,
options: { addImports: boolean; browserMode: boolean } = {
options: { addImports: boolean; browserMode: boolean; fakeAsync?: boolean } = {
addImports: false,
browserMode: false,
},
Expand Down Expand Up @@ -534,4 +534,66 @@ describe('Jasmine to Vitest Transformer - Integration Tests', () => {

await expectTransformation(jasmineCode, vitestCode);
});

it('should not transform `fakeAsync`', async () => {
const jasmineCode = `
import { fakeAsync, flush, flushMicrotasks, tick } from '@angular/core/testing';

describe('My fakeAsync suite', () => {
it('works', fakeAsync(() => {
flush();
flushMicrotasks();
tick(100);
}));
});
`;
const vitestCode = `
import { fakeAsync, flush, flushMicrotasks, tick } from '@angular/core/testing';

describe('My fakeAsync suite', () => {
it('works', fakeAsync(() => {
flush();
flushMicrotasks();
tick(100);
}));
});
`;

await expectTransformation(jasmineCode, vitestCode);
});

it('should transform `fakeAsync` if `fakeAsync` option is true', async () => {
const jasmineCode = `
import { fakeAsync, flush, flushMicrotasks, tick } from '@angular/core/testing';

describe('My fakeAsync suite', () => {
it('works', fakeAsync(() => {
flush();
flushMicrotasks();
tick(100);
}));
});
`;
const vitestCode = `
describe('My fakeAsync suite', () => {
beforeAll(() => {
vi.useFakeTimers({ advanceTimeDelta: 1, shouldAdvanceTime: true });
});
afterAll(() => {
vi.useRealTimers();
});
it('works', async () => {
await vi.runAllTimersAsync();
await vi.advanceTimersByTimeAsync(0);
await vi.advanceTimersByTimeAsync(100);
});
});
`;

await expectTransformation(jasmineCode, vitestCode, {
addImports: false,
browserMode: false,
fakeAsync: true,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
*/

import ts from 'typescript';
import { transformFakeAsyncFlush } from './transformers/fake-async-flush';
import { transformFakeAsyncFlushMicrotasks } from './transformers/fake-async-flush-microtasks';
import { transformFakeAsyncTest } from './transformers/fake-async-test';
import { transformFakeAsyncTick } from './transformers/fake-async-tick';
import {
transformDoneCallback,
transformFocusedAndSkippedTests,
Expand Down Expand Up @@ -48,7 +52,11 @@ import {
transformSpyReset,
} from './transformers/jasmine-spy';
import { transformJasmineTypes } from './transformers/jasmine-type';
import { addVitestValueImport, getVitestAutoImports } from './utils/ast-helpers';
import {
addVitestValueImport,
getVitestAutoImports,
removeImportSpecifiers,
} from './utils/ast-helpers';
import { RefactorContext } from './utils/refactor-context';
import { RefactorReporter } from './utils/refactor-reporter';

Expand Down Expand Up @@ -129,6 +137,10 @@ const callExpressionTransformers = [
transformToHaveBeenCalledBefore,
transformToHaveClass,
transformToBeNullish,
transformFakeAsyncTest,
transformFakeAsyncTick,
transformFakeAsyncFlush,
transformFakeAsyncFlushMicrotasks,

// **Stage 3: Global Functions & Cleanup**
// These handle global Jasmine functions and catch-alls for unsupported APIs.
Expand Down Expand Up @@ -173,8 +185,10 @@ export function transformJasmineToVitest(
filePath: string,
content: string,
reporter: RefactorReporter,
options: { addImports: boolean; browserMode: boolean },
options: { addImports: boolean; browserMode: boolean; fakeAsync?: boolean },
): string {
options.fakeAsync ??= false;

const contentWithPlaceholders = preserveBlankLines(content);

const sourceFile = ts.createSourceFile(
Expand All @@ -187,6 +201,7 @@ export function transformJasmineToVitest(

const pendingVitestValueImports = new Set<string>();
const pendingVitestTypeImports = new Set<string>();
const pendingImportSpecifierRemovals = new Map<string, Set<string>>();

const transformer: ts.TransformerFactory<ts.SourceFile> = (context) => {
const refactorCtx: RefactorContext = {
Expand All @@ -195,6 +210,7 @@ export function transformJasmineToVitest(
tsContext: context,
pendingVitestValueImports,
pendingVitestTypeImports,
pendingImportSpecifierRemovals,
};

const visitor: ts.Visitor = (node) => {
Expand All @@ -211,7 +227,18 @@ export function transformJasmineToVitest(
}

for (const transformer of callExpressionTransformers) {
if (!(options.browserMode && transformer === transformToHaveClass)) {
if (
!(
(options.browserMode && transformer === transformToHaveClass) ||
(options.fakeAsync === false &&
[
transformFakeAsyncFlush,
transformFakeAsyncFlushMicrotasks,
transformFakeAsyncTick,
transformFakeAsyncTest,
].includes(transformer))
)
) {
transformedNode = transformer(transformedNode, refactorCtx);
}
}
Expand Down Expand Up @@ -249,16 +276,25 @@ export function transformJasmineToVitest(

const hasPendingValueImports = pendingVitestValueImports.size > 0;
const hasPendingTypeImports = pendingVitestTypeImports.size > 0;
const hasPendingImportSpecifierRemovals = pendingImportSpecifierRemovals.size > 0;

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

if (hasPendingImportSpecifierRemovals) {
transformedSourceFile = removeImportSpecifiers(
transformedSourceFile,
pendingImportSpecifierRemovals,
);
}

if (hasPendingTypeImports || (options.addImports && hasPendingValueImports)) {
const vitestImport = getVitestAutoImports(
options.addImports ? pendingVitestValueImports : new Set(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,32 @@ describe('Jasmine to Vitest Transformer - addImports option', () => {
true,
);
});

it('should add imports for `vi` when addImports is true', async () => {
const input = `
import { fakeAsync } from '@angular/core/testing';

describe('My fakeAsync suite', () => {
it('works', fakeAsync(() => {
expect(1).toBe(1);
}));
});
`;
const expected = `
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';

describe('My fakeAsync suite', () => {
beforeAll(() => {
vi.useFakeTimers({ advanceTimeDelta: 1, shouldAdvanceTime: true });
});
afterAll(() => {
vi.useRealTimers();
});
it('works', async () => {
expect(1).toBe(1);
});
});
`;
await expectTransformation(input, expected, true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export async function expectTransformation(
const transformed = transformJasmineToVitest('spec.ts', input, reporter, {
addImports,
browserMode: false,
fakeAsync: true,
});
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
@@ -0,0 +1,38 @@
/**
* @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 ts from 'typescript';
import { isNamedImportFrom } from '../utils/ast-helpers';
import { ANGULAR_CORE_TESTING } from '../utils/constants';
import { RefactorContext } from '../utils/refactor-context';
import { addImportSpecifierRemoval, createViCallExpression } from '../utils/refactor-helpers';

export function transformFakeAsyncFlushMicrotasks(node: ts.Node, ctx: RefactorContext): ts.Node {
if (
!(
ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === 'flushMicrotasks' &&
isNamedImportFrom(ctx.sourceFile, 'flushMicrotasks', ANGULAR_CORE_TESTING)
)
) {
return node;
}

ctx.reporter.reportTransformation(
ctx.sourceFile,
node,
`Transformed \`flushMicrotasks\` to \`await vi.advanceTimersByTimeAsync(0)\`.`,
);

addImportSpecifierRemoval(ctx, 'flushMicrotasks', ANGULAR_CORE_TESTING);

return ts.factory.createAwaitExpression(
createViCallExpression(ctx, 'advanceTimersByTimeAsync', [ts.factory.createNumericLiteral(0)]),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* @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('transformFakeAsyncFlushMicrotasks', () => {
const testCases = [
{
description: 'should replace `flushMicrotasks` with `await vi.advanceTimersByTimeAsync(0)`',
input: `
import { flushMicrotasks } from '@angular/core/testing';

flushMicrotasks();
`,
expected: `await vi.advanceTimersByTimeAsync(0);`,
},
{
description:
'should not replace `flushMicrotasks` if not imported from `@angular/core/testing`',
input: `
import { flushMicrotasks } from './my-flush-microtasks';

flushMicrotasks();
`,
expected: `
import { flushMicrotasks } from './my-flush-microtasks';

flushMicrotasks();
`,
},
];

testCases.forEach(({ description, input, expected }) => {
it(description, async () => {
await expectTransformation(input, expected);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @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 ts from 'typescript';
import { isNamedImportFrom } from '../utils/ast-helpers';
import { addTodoComment } from '../utils/comment-helpers';
import { ANGULAR_CORE_TESTING } from '../utils/constants';
import { RefactorContext } from '../utils/refactor-context';
import { addImportSpecifierRemoval, createViCallExpression } from '../utils/refactor-helpers';

export function transformFakeAsyncFlush(node: ts.Node, ctx: RefactorContext): ts.Node {
if (
!(
ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === 'flush' &&
isNamedImportFrom(ctx.sourceFile, 'flush', ANGULAR_CORE_TESTING)
)
) {
return node;
}

ctx.reporter.reportTransformation(
ctx.sourceFile,
node,
`Transformed \`flush\` to \`await vi.runAllTimersAsync()\`.`,
);

addImportSpecifierRemoval(ctx, 'flush', ANGULAR_CORE_TESTING);

if (node.arguments.length > 0) {
ctx.reporter.recordTodo('flush-max-turns', ctx.sourceFile, node);
addTodoComment(node, 'flush-max-turns');
}

const awaitRunAllTimersAsync = ts.factory.createAwaitExpression(
createViCallExpression(ctx, 'runAllTimersAsync'),
);

if (ts.isExpressionStatement(node.parent)) {
return awaitRunAllTimersAsync;
} else {
Comment thread
yjaaidi marked this conversation as resolved.
// If `flush` is not used as its own statement, then the return value is probably used.
// Therefore, we replace it with nullish coalescing that returns 0:
// > await vi.runAllTimersAsync() ?? 0;
ctx.reporter.recordTodo('flush-return-value', ctx.sourceFile, node);
addTodoComment(node, 'flush-return-value');

return ts.factory.createBinaryExpression(
awaitRunAllTimersAsync,
ts.SyntaxKind.QuestionQuestionToken,
ts.factory.createNumericLiteral(0),
);
}
}
Loading
Loading