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 @@ -72,20 +72,36 @@ function getUnwrappedParent(node: ts.Node): ts.Node | undefined {
}

function isChainedWithAnd(node: ts.Node): boolean {
let previous: ts.Node = node;
let parent = node.parent;
while (parent) {
if (ts.isPropertyAccessExpression(parent)) {
if (ts.isIdentifier(parent.name) && parent.name.text === 'and') {
if (ts.isPropertyAccessExpression(parent) && ts.isIdentifier(parent.name)) {
if (parent.name.text === 'and') {
return true;
}
} else if (ts.isElementAccessExpression(parent)) {
if (
ts.isStringLiteralLike(parent.argumentExpression) &&
parent.argumentExpression.text === 'and'
) {
// `withArgs(...)` returns the spy, so a strategy may still follow.
if (parent.name.text === 'withArgs') {
previous = parent;
parent = parent.parent;
continue;
}
} else if (
ts.isElementAccessExpression(parent) &&
ts.isStringLiteralLike(parent.argumentExpression)
) {
if (parent.argumentExpression.text === 'and') {
return true;
}
} else if (isTransparentWrapper(parent)) {
if (parent.argumentExpression.text === 'withArgs') {
previous = parent;
parent = parent.parent;
continue;
}
} else if (
isTransparentWrapper(parent) ||
(ts.isCallExpression(parent) && parent.expression === previous)
) {
previous = parent;
parent = parent.parent;
continue;
}
Expand All @@ -95,6 +111,33 @@ function isChainedWithAnd(node: ts.Node): boolean {
return false;
}

/**
* Checks whether a spy expression chain contains a `withArgs(...)` link.
*/
function chainContainsWithArgs(expression: ts.Expression): boolean {
let current: ts.Expression = unwrapExpression(expression);
while (true) {
if (ts.isCallExpression(current)) {
current = unwrapExpression(current.expression);
} else if (ts.isPropertyAccessExpression(current)) {
if (ts.isIdentifier(current.name) && current.name.text === 'withArgs') {
return true;
}
current = unwrapExpression(current.expression);
} else if (ts.isElementAccessExpression(current)) {
if (
ts.isStringLiteralLike(current.argumentExpression) &&
current.argumentExpression.text === 'withArgs'
) {
return true;
}
current = unwrapExpression(current.expression);
} else {
return false;
}
}
}

function isSpyCreation(node: ts.Expression): boolean {
const expression = unwrapExpression(node);
if (!ts.isCallExpression(expression)) {
Expand Down Expand Up @@ -181,6 +224,19 @@ function transformSpyStrategy(node: ts.CallExpression, refactorCtx: RefactorCont
}

if (spyCall) {
// Vitest mocks have no `withArgs`, so a strategy for specific arguments cannot be converted.
// The whole statement is left untouched, with a single TODO on the outermost strategy call;
// inner `.and` chains reached while the visitor descends are covered by that TODO.
if (chainContainsWithArgs(spyCall)) {
if (!isChainedWithAnd(node)) {
const category = 'withArgs';
reporter.recordTodo(category, sourceFile, node);
addTodoComment(node, category);
}

return node;
}

let newMethodName: string | undefined;
let args = node.arguments;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,36 @@ getSpy().and.callThrough();`,
input: `spyOn(service, 'myMethod').and.unknownStrategy();`,
expected: `// TODO: vitest-migration: Unsupported spy strategy ".and.unknownStrategy()" found. Please migrate this manually. See: https://vitest.dev/api/mocked.html#mock
vi.spyOn(service, 'myMethod').and.unknownStrategy();`,
},
{
description: 'should add a TODO for a spy strategy configured with .withArgs()',
input: `spyOn(service, 'myMethod').withArgs(1).and.returnValue(2);`,
expected: `// TODO: vitest-migration: Vitest does not have a direct equivalent for spy.withArgs(). Please migrate this manually, for example with mockImplementation() that checks the arguments. See: https://vitest.dev/api/mock.html#mockimplementation
vi.spyOn(service, 'myMethod').withArgs(1).and.returnValue(2);`,
},
{
description: 'should add a single TODO for multiple chained .withArgs() strategies',
input: `mySpy.withArgs(1).and.returnValue(10).withArgs(2).and.returnValue(20);`,
expected: `// TODO: vitest-migration: Vitest does not have a direct equivalent for spy.withArgs(). Please migrate this manually, for example with mockImplementation() that checks the arguments. See: https://vitest.dev/api/mock.html#mockimplementation
mySpy.withArgs(1).and.returnValue(10).withArgs(2).and.returnValue(20);`,
},
{
description: 'should not transform a default strategy chained after .withArgs()',
input: `mySpy.withArgs(1).and.returnValue(2).and.returnValue(0);`,
expected: `// TODO: vitest-migration: Vitest does not have a direct equivalent for spy.withArgs(). Please migrate this manually, for example with mockImplementation() that checks the arguments. See: https://vitest.dev/api/mock.html#mockimplementation
mySpy.withArgs(1).and.returnValue(2).and.returnValue(0);`,
},
{
description: 'should add a TODO for .withArgs() on an existing spy',
input: `mySpy.withArgs(1).and.returnValue(2);`,
expected: `// TODO: vitest-migration: Vitest does not have a direct equivalent for spy.withArgs(). Please migrate this manually, for example with mockImplementation() that checks the arguments. See: https://vitest.dev/api/mock.html#mockimplementation
mySpy.withArgs(1).and.returnValue(2);`,
},
{
description: 'should add a TODO for a wrapped .withArgs() call',
input: `mySpy.withArgs(1)!.and.returnValue(2);`,
expected: `// TODO: vitest-migration: Vitest does not have a direct equivalent for spy.withArgs(). Please migrate this manually, for example with mockImplementation() that checks the arguments. See: https://vitest.dev/api/mock.html#mockimplementation
mySpy.withArgs(1)!.and.returnValue(2);`,
},
{
description: 'should correctly identify chained spies with element access (bracket notation)',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ export const TODO_NOTES = {
' the original implementation but also clears the recorded calls.',
url: 'https://vitest.dev/api/mock.html#mockreset',
},
'withArgs': {
message:
'Vitest does not have a direct equivalent for spy.withArgs().' +
' Please migrate this manually, for example with mockImplementation() that checks the arguments.',
url: 'https://vitest.dev/api/mock.html#mockimplementation',
},
'mostRecent-without-args': {
message:
'Direct usage of mostRecent() is not supported.' +
Expand Down
Loading