Skip to content
Draft
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 @@ -14,6 +14,9 @@
*/

import ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { transformFakeAsyncFlush } from './transformers/fake-async-flush';
import { transformFakeAsyncTest } from './transformers/fake-async-test';
import { transformFakeAsyncTick } from './transformers/fake-async-tick';
import {
transformDoneCallback,
transformFocusedAndSkippedTests,
Expand Down Expand Up @@ -46,7 +49,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 @@ -121,6 +128,9 @@ const callExpressionTransformers = [
transformSpyCallInspection,
transformtoHaveBeenCalledBefore,
transformToHaveClass,
transformFakeAsyncTest,
transformFakeAsyncTick,
transformFakeAsyncFlush,

// **Stage 3: Global Functions & Cleanup**
// These handle global Jasmine functions and catch-alls for unsupported APIs.
Expand Down Expand Up @@ -179,6 +189,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 @@ -187,6 +198,7 @@ export function transformJasmineToVitest(
tsContext: context,
pendingVitestValueImports,
pendingVitestTypeImports,
pendingImportSpecifierRemovals,
};

const visitor: ts.Visitor = (node) => {
Expand Down Expand Up @@ -240,16 +252,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 @@ -116,4 +116,26 @@ describe('Jasmine to Vitest Transformer - addImports option', () => {
`;
await expectTransformation(input, expected, true);
});

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

it('works', fakeAsync(() => {
expect(1).toBe(1);
}));
`;
const expected = `
import { expect, it, onTestFinished, vi } from 'vitest';

it('works', async () => {
vi.useFakeTimers();
onTestFinished(() => {
vi.useRealTimers();
});
expect(1).toBe(1);
});
`;
await expectTransformation(input, expected, true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* @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 '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { isNamedImportFrom } from '../utils/ast-helpers';
import { ANGULAR_CORE_TESTING } from '../utils/constants';
import { RefactorContext, addImportSpecifierRemoval } from '../utils/refactor-context';

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)
) {
ctx.reporter.reportTransformation(
ctx.sourceFile,
node,
`Transformed \`flush\` to \`await vi.runAllTimersAsync()\`.`,
);

addImportSpecifierRemoval(ctx, 'flush', ANGULAR_CORE_TESTING);

return ts.factory.createAwaitExpression(
ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('vi'),
'runAllTimersAsync',
),
undefined,
[],
),
);
}

return node;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @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('transformFakeAsyncFlush', () => {
const testCases = [
{
description: 'should replace `flush` with `await vi.runAllTimersAsync()`',
input: `
import { flush } from '@angular/core/testing';

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

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

flush();
`,
},
{
description: 'should keep other imported symbols from `@angular/core/testing`',
input: `
import { TestBed, flush } from '@angular/core/testing';

flush();
`,
expected: `
import { TestBed } from '@angular/core/testing';

await vi.runAllTimersAsync();
`,
},
];

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,94 @@
/**
* @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 '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { isNamedImportFrom } from '../utils/ast-helpers';
import { ANGULAR_CORE_TESTING } from '../utils/constants';
import { RefactorContext, addImportSpecifierRemoval } from '../utils/refactor-context';

export function transformFakeAsyncTest(node: ts.Node, ctx: RefactorContext): ts.Node {
if (
!(
ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === 'fakeAsync' &&
node.arguments.length >= 1 &&
(ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0])) &&
isNamedImportFrom(ctx.sourceFile, 'fakeAsync', ANGULAR_CORE_TESTING)
)
) {
return node;
}

ctx.reporter.reportTransformation(
ctx.sourceFile,
node,
`Transformed \`fakeAsync\` to \`vi.useFakeTimers\`.`,
);

addImportSpecifierRemoval(ctx, 'fakeAsync', ANGULAR_CORE_TESTING);

ctx.pendingVitestValueImports.add('onTestFinished');
ctx.pendingVitestValueImports.add('vi');

const callback = node.arguments[0];
const callbackBody = ts.isBlock(callback.body)
? callback.body
: ts.factory.createBlock([ts.factory.createExpressionStatement(callback.body)]);

// Generate the following code:
// > vi.useFakeTimers();
// > onTestFinished(() => {
// > vi.useRealTimers();
// > });
const setupStatements: ts.Statement[] = [
ts.factory.createExpressionStatement(
ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('vi'),
'useFakeTimers',
),
undefined,
[],
),
),
ts.factory.createExpressionStatement(
ts.factory.createCallExpression(ts.factory.createIdentifier('onTestFinished'), undefined, [
ts.factory.createArrowFunction(
undefined,
undefined,
[],
undefined,
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
ts.factory.createBlock([
ts.factory.createExpressionStatement(
ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('vi'),
'useRealTimers',
),
undefined,
[],
),
),
]),
),
]),
),
...callbackBody.statements,
];

return ts.factory.createArrowFunction(
[ts.factory.createModifier(ts.SyntaxKind.AsyncKeyword)],
undefined,
[],
undefined,
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
ts.factory.createBlock(setupStatements),
);
}
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('transformFakeAsyncTest', () => {
const testCases = [
{
description: 'should transform fakeAsync test to `vi.useFakeTimers()`',
input: `
import { fakeAsync } from '@angular/core/testing';

it('works', fakeAsync(() => {
expect(1).toBe(1);
}));
`,
expected: `
it('works', async () => {
vi.useFakeTimers();
onTestFinished(() => {
vi.useRealTimers();
});
expect(1).toBe(1);
});
`,
},
{
description: 'should not replace `fakeAsync` if not imported from `@angular/core/testing`',
input: `
import { fakeAsync } from './my-fake-async';

it('works', fakeAsync(() => {
expect(1).toBe(1);
}));
`,
expected: `
import { fakeAsync } from './my-fake-async';

it('works', fakeAsync(() => {
expect(1).toBe(1);
}));
`,
},
];

testCases.forEach(({ description, input, expected }) => {
it(description, async () => {
await expectTransformation(input, expected);
});
});
});
Loading
Loading