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
Original file line number Diff line number Diff line change
Expand Up @@ -255,14 +255,15 @@ describe('Jasmine to Vitest Transformer - Integration Tests', () => {
});
`;

/* eslint-disable max-len */
const vitestCode = `
describe('Complex Scenarios', () => {
let serviceMock;

beforeEach(() => {
serviceMock = {
getData: vi.fn().mockReturnValue(expect.any(String)),
process: vi.fn().mockReturnValue(undefined),
getData: vi.fn().mockName("MyService.getData").mockReturnValue(expect.any(String)),
process: vi.fn().mockName("MyService.process").mockReturnValue(undefined),
};
});

Expand All @@ -274,6 +275,7 @@ describe('Jasmine to Vitest Transformer - Integration Tests', () => {

it('should handle array contents checking', () => {
const arr = [1, 2, 3];
// TODO: vitest-migration: Verify this matches strict array content (multiset equality). Vitest's arrayContaining is a subset check.
expect(arr).toHaveLength(3);
expect(arr).toEqual(expect.arrayContaining([3, 2, 1]));
});
Expand All @@ -299,6 +301,7 @@ describe('Jasmine to Vitest Transformer - Integration Tests', () => {
});
});
`;
/* eslint-enable max-len */

await expectTransformation(jasmineCode, vitestCode);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,20 @@ describe('Jasmine to Vitest Transformer', () => {
input: `const spy = jasmine.createSpyObj('MyService', { getPromise: Promise.resolve(jasmine.any(String)) });`,
expected: `
const spy = {
getPromise: vi.fn().mockReturnValue(Promise.resolve(expect.any(String))),
getPromise: vi.fn().mockName("MyService.getPromise").mockReturnValue(Promise.resolve(expect.any(String))),
};
`,
},
{
description: 'should handle arrayWithExactContents containing nested asymmetric matchers',
input: `expect(myArray).toEqual(jasmine.arrayWithExactContents([jasmine.objectContaining({ id: 1 })]));`,
/* eslint-disable max-len */
expected: `
// TODO: vitest-migration: Verify this matches strict array content (multiset equality). Vitest's arrayContaining is a subset check.
expect(myArray).toHaveLength(1);
expect(myArray).toEqual(expect.arrayContaining([expect.objectContaining({ id: 1 })]));
`,
/* eslint-enable max-len */
},
{
description: 'should handle a spy rejecting with an asymmetric matcher',
Expand All @@ -105,8 +108,8 @@ describe('Jasmine to Vitest Transformer', () => {
`,
expected: `
const myService = {
methodA: vi.fn(),
propA: 'valueA'
methodA: vi.fn().mockName("MyService.methodA"),
propA: 'valueA',
};
vi.spyOn(myService, 'methodA').mockReturnValue('mocked value');
myService.methodA('test');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,19 @@ function transformPromiseBasedDone(
return undefined;
}

function countDoneUsages(node: ts.Node, doneIdentifier: ts.Identifier): number {
let count = 0;
const visitor = (n: ts.Node) => {
if (ts.isIdentifier(n) && n.text === doneIdentifier.text) {
count++;
}
ts.forEachChild(n, visitor);
};
ts.forEachChild(node, visitor);

return count;
}

export function transformDoneCallback(node: ts.Node, refactorCtx: RefactorContext): ts.Node {
const { sourceFile, reporter, tsContext } = refactorCtx;
if (
Expand Down Expand Up @@ -309,12 +322,17 @@ export function transformDoneCallback(node: ts.Node, refactorCtx: RefactorContex
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: ts.Node): ts.Node | ts.Node[] | undefined => {
const complexTransformed = transformComplexDoneCallback(bodyNode, doneIdentifier, refactorCtx);
if (complexTransformed !== bodyNode) {
doneWasUsed = true;
handledUsages++; // complex transform handles one usage

return complexTransformed;
}
Expand All @@ -330,6 +348,7 @@ export function transformDoneCallback(node: ts.Node, refactorCtx: RefactorContex
callExpr.expression.name.text === 'fail'
) {
doneWasUsed = true;
handledUsages++;
reporter.reportTransformation(
sourceFile,
bodyNode,
Expand All @@ -350,6 +369,7 @@ export function transformDoneCallback(node: ts.Node, refactorCtx: RefactorContex
const promiseTransformed = transformPromiseBasedDone(callExpr, doneIdentifier, refactorCtx);
if (promiseTransformed) {
doneWasUsed = true;
handledUsages++;

return promiseTransformed;
}
Expand All @@ -360,6 +380,7 @@ export function transformDoneCallback(node: ts.Node, refactorCtx: RefactorContex
callExpr.expression.text === doneIdentifier.text
) {
doneWasUsed = true;
handledUsages++;

return ts.setTextRange(ts.factory.createEmptyStatement(), callExpr.expression);
}
Expand All @@ -383,6 +404,20 @@ export function transformDoneCallback(node: ts.Node, refactorCtx: RefactorContex
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);
addTodoComment(node, category);

return node;
}

if (!doneWasUsed) {
return node;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ describe('Jasmine to Vitest Transformer', () => {
});
`,
expected: `
// TODO: vitest-migration: The 'done' callback was used in an unhandled way. Please migrate manually.
it('should not transform a function with a parameter that is not a done callback', (value) => {
expect(value).toBe(true);
});
Expand All @@ -157,6 +158,20 @@ describe('Jasmine to Vitest Transformer', () => {
});
`,
},
{
description: 'should add a TODO for unhandled done usage',
input: `
it('should do something with helper', (done) => {
someHelper(done);
});
`,
expected: `
// TODO: vitest-migration: The 'done' callback was used in an unhandled way. Please migrate manually.
it('should do something with helper', (done) => {
someHelper(done);
});
`,
},
];

testCases.forEach(({ description, input, expected }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -451,10 +451,14 @@ export function transformArrayWithExactContents(
],
);

return [
ts.factory.createExpressionStatement(lengthCall),
ts.factory.createExpressionStatement(containingCall),
];
const lengthStmt = ts.factory.createExpressionStatement(lengthCall);
const containingStmt = ts.factory.createExpressionStatement(containingCall);

const category = 'arrayWithExactContents-check';
reporter.recordTodo(category);
addTodoComment(lengthStmt, category);

return [lengthStmt, containingStmt];
}

export function transformCalledOnceWith(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,19 +225,25 @@ expect(mySpyObj).toHaveSpyInteractions();`,
{
description: 'should transform toEqual(jasmine.arrayWithExactContents()) into two calls',
input: `expect(myArray).toEqual(jasmine.arrayWithExactContents(['a', 'b']));`,
/* eslint-disable max-len */
expected: `
// TODO: vitest-migration: Verify this matches strict array content (multiset equality). Vitest's arrayContaining is a subset check.
expect(myArray).toHaveLength(2);
expect(myArray).toEqual(expect.arrayContaining(['a', 'b']));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how you do the same in Jest/Vitest:

Suggested change
expect(myArray).toEqual(expect.arrayContaining(['a', 'b']));
expect(myArray).toEqual(['a', 'b']);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not equivalent. The following passes but would fail with that transformation:
expect([1, 2]).toEqual(jasmine.arrayWithExactContents([2, 1]))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, yeah that makes sense

`,
/* eslint-enable max-len */
},
{
description:
'should transform toEqual(jasmine.arrayWithExactContents()) with asymmetric matchers',
input: `expect(myArray).toEqual(jasmine.arrayWithExactContents([jasmine.any(Number), 'a']));`,
/* eslint-disable max-len */
expected: `
// TODO: vitest-migration: Verify this matches strict array content (multiset equality). Vitest's arrayContaining is a subset check.
expect(myArray).toHaveLength(2);
expect(myArray).toEqual(expect.arrayContaining([expect.any(Number), 'a']));
`,
/* eslint-enable max-len */
},
{
description:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ export function transformCreateSpyObj(
'Transformed `jasmine.createSpyObj()` to an object literal with `vi.fn()`.',
);

const baseNameArg = node.arguments[0];
const baseName = ts.isStringLiteral(baseNameArg) ? baseNameArg.text : undefined;
const methods = node.arguments[1];
const propertiesArg = node.arguments[2];
let properties: ts.PropertyAssignment[] = [];

if (node.arguments.length < 2) {
const category = 'createSpyObj-single-argument';
reporter.recordTodo(category);
Expand All @@ -235,14 +241,10 @@ export function transformCreateSpyObj(
return node;
}

const methods = node.arguments[1];
const propertiesArg = node.arguments[2];
let properties: ts.PropertyAssignment[] = [];

if (ts.isArrayLiteralExpression(methods)) {
properties = createSpyObjWithArray(methods);
properties = createSpyObjWithArray(methods, baseName);
} else if (ts.isObjectLiteralExpression(methods)) {
properties = createSpyObjWithObject(methods);
properties = createSpyObjWithObject(methods, baseName);
} else {
const category = 'createSpyObj-dynamic-variable';
reporter.recordTodo(category);
Expand All @@ -264,13 +266,28 @@ export function transformCreateSpyObj(
return ts.factory.createObjectLiteralExpression(properties, true);
}

function createSpyObjWithArray(methods: ts.ArrayLiteralExpression): ts.PropertyAssignment[] {
function createSpyObjWithArray(
methods: ts.ArrayLiteralExpression,
baseName: string | undefined,
): ts.PropertyAssignment[] {
return methods.elements
.map((element) => {
if (ts.isStringLiteral(element)) {
const mockFn = createViCallExpression('fn');
const methodName = element.text;
let finalExpression: ts.Expression = mockFn;

if (baseName) {
finalExpression = ts.factory.createCallExpression(
createPropertyAccess(finalExpression, 'mockName'),
undefined,
[ts.factory.createStringLiteral(`${baseName}.${methodName}`)],
);
}

return ts.factory.createPropertyAssignment(
ts.factory.createIdentifier(element.text),
createViCallExpression('fn'),
ts.factory.createIdentifier(methodName),
finalExpression,
);
}

Expand All @@ -279,13 +296,25 @@ function createSpyObjWithArray(methods: ts.ArrayLiteralExpression): ts.PropertyA
.filter((p): p is ts.PropertyAssignment => !!p);
}

function createSpyObjWithObject(methods: ts.ObjectLiteralExpression): ts.PropertyAssignment[] {
function createSpyObjWithObject(
methods: ts.ObjectLiteralExpression,
baseName: string | undefined,
): ts.PropertyAssignment[] {
return methods.properties
.map((prop) => {
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
const methodName = prop.name.text;
const returnValue = prop.initializer;
const mockFn = createViCallExpression('fn');
let mockFn = createViCallExpression('fn');

if (baseName) {
mockFn = ts.factory.createCallExpression(
createPropertyAccess(mockFn, 'mockName'),
undefined,
[ts.factory.createStringLiteral(`${baseName}.${methodName}`)],
);
}

const mockReturnValue = createPropertyAccess(mockFn, 'mockReturnValue');

return ts.factory.createPropertyAssignment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ vi.spyOn(service, 'myMethod').and.unknownStrategy();`,
description: 'should transform jasmine.createSpyObj with an array of methods',
input: `const myService = jasmine.createSpyObj('MyService', ['methodA', 'methodB']);`,
expected: `const myService = {
methodA: vi.fn(),
methodB: vi.fn()
methodA: vi.fn().mockName("MyService.methodA"),
methodB: vi.fn().mockName("MyService.methodB"),
};`,
},
{
Expand All @@ -134,16 +134,16 @@ vi.spyOn(service, 'myMethod').and.unknownStrategy();`,
description: 'should transform jasmine.createSpyObj with an object of return values',
input: `const myService = jasmine.createSpyObj('MyService', { methodA: 'foo', methodB: 42 });`,
expected: `const myService = {
methodA: vi.fn().mockReturnValue('foo'),
methodB: vi.fn().mockReturnValue(42)
methodA: vi.fn().mockName("MyService.methodA").mockReturnValue('foo'),
methodB: vi.fn().mockName("MyService.methodB").mockReturnValue(42),
};`,
},
{
description:
'should transform jasmine.createSpyObj with an object of return values containing an asymmetric matcher',
input: `const myService = jasmine.createSpyObj('MyService', { methodA: jasmine.any(String) });`,
expected: `const myService = {
methodA: vi.fn().mockReturnValue(expect.any(String))
methodA: vi.fn().mockName("MyService.methodA").mockReturnValue(expect.any(String)),
};`,
},
{
Expand All @@ -158,23 +158,23 @@ vi.spyOn(service, 'myMethod').and.unknownStrategy();`,
description: 'should transform jasmine.createSpyObj with a property map',
input: `const myService = jasmine.createSpyObj('MyService', ['methodA'], { propA: 'valueA' });`,
expected: `const myService = {
methodA: vi.fn(),
propA: 'valueA'
methodA: vi.fn().mockName("MyService.methodA"),
propA: 'valueA',
};`,
},
{
description: 'should transform jasmine.createSpyObj with a method map and a property map',
input: `const myService = jasmine.createSpyObj('MyService', { methodA: 'foo' }, { propA: 'valueA' });`,
expected: `const myService = {
methodA: vi.fn().mockReturnValue('foo'),
propA: 'valueA'
methodA: vi.fn().mockName("MyService.methodA").mockReturnValue('foo'),
propA: 'valueA',
};`,
},
{
description: 'should ignore non-string literals in the method array',
input: `const myService = jasmine.createSpyObj('MyService', ['methodA', 123, someVar]);`,
expected: `const myService = {
methodA: vi.fn()
methodA: vi.fn().mockName("MyService.methodA"),
};`,
},
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ export const TODO_NOTES = {
message:
'Cannot transform jasmine.arrayWithExactContents with a dynamic variable. Please migrate this manually.',
},
'arrayWithExactContents-check': {
message:
"Verify this matches strict array content (multiset equality). Vitest's arrayContaining is a subset check.",
},
'expect-nothing': {
message:
'expect().nothing() has been removed because it is redundant in Vitest. Tests without assertions pass by default.',
Expand Down Expand Up @@ -120,6 +124,9 @@ export const TODO_NOTES = {
' Please refactor to access .args directly or use vi.mocked(spy).mock.lastCall.',
url: 'https://vitest.dev/api/mocked.html#mock-lastcall',
},
'unhandled-done-usage': {
message: "The 'done' callback was used in an unhandled way. Please migrate manually.",
},
} as const;

/**
Expand Down