diff --git a/package.json b/package.json index bfcf211a..9f68ba06 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "@types/update-notifier": "5.1.0", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "5.62.0", - "ast-types": "0.16.1", + "ast-types": "0.14.2", "codecov": "3.8.3", "eslint": "8.57.0", "eslint-config-prettier": "8.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f14106e8..cb609fa9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,8 +52,8 @@ importers: specifier: 5.62.0 version: 5.62.0(eslint@8.57.0)(typescript@5.5.4) ast-types: - specifier: 0.16.1 - version: 0.16.1 + specifier: 0.14.2 + version: 0.14.2 codecov: specifier: 3.8.3 version: 3.8.3 diff --git a/src/transformers/sinon.test.ts b/src/transformers/sinon.test.ts index 13c44bf2..fb0573f7 100644 --- a/src/transformers/sinon.test.ts +++ b/src/transformers/sinon.test.ts @@ -187,6 +187,58 @@ describe.each([ ) }) + it('handles .resolves/.rejects', () => { + expectTransformation( + ` + ${sinonImport} + sinon.stub().resolves(); + sinon.stub().resolves(1); + sinon.stub().rejects(); + sinon.stub().rejects(new Error('error msg')); + `, + ` + jest.fn().mockResolvedValue(); + jest.fn().mockResolvedValue(1); + jest.fn().mockRejectedValue(new Error()); + jest.fn().mockRejectedValue(new Error('error msg')); + ` + ) + }) + + it('handles .callsFake', () => { + expectTransformation( + ` + ${sinonImport} + sinon.stub().callsFake(); + sinon.stub().callsFake((a, b) => a + b); + sinon.stub().callsFake(async () => ({ a: 1 })); + `, + ` + jest.fn().mockImplementation(); + jest.fn().mockImplementation((a, b) => a + b); + jest.fn().mockImplementation(async () => ({ a: 1 })); + ` + ) + }) + + it('handles .throws', () => { + expectTransformation( + ` + ${sinonImport} + sinon.stub().throws(); + sinon.stub().throws(new Error('error msg')); + `, + ` + jest.fn().mockImplementation(() => { + throw new Error(); + }); + jest.fn().mockImplementation(() => { + throw new Error('error msg'); + }); + ` + ) + }) + it('handles .withArgs returns', () => { expectTransformation( ` @@ -199,6 +251,8 @@ describe.each([ const stub = sinon.stub(foo, 'bar').withArgs('foo', 1).returns('something') sinon.stub(foo, 'bar').withArgs('foo', sinon.match.object).returns('something') sinon.stub().withArgs('foo', sinon.match.any).returns('something') + sinon.stub().withArgs('boo', sinon.match.any).returnsArg(1) + sinon.stub().withArgs('boo').returns() `, ` jest.fn().mockImplementation((...args) => { @@ -236,6 +290,63 @@ describe.each([ return 'something'; } }) + jest.fn().mockImplementation((...args) => { + if (args[0] === 'boo' && args.length >= 2) { + return args[1]; + } + }) + jest.fn().mockImplementation((...args) => { + if (args[0] === 'boo') { + return undefined; + } + }) +` + ) + }) + + it('handles .withArgs chained with .resolves/.rejects/.throws/.callsFake', () => { + expectTransformation( + ` + ${sinonImport} + + sinon.stub().withArgs('foo').resolves('something') + sinon.stub().withArgs('foo', 'bar').rejects() + sinon.stub().withArgs('foo', 'bar', 1).rejects(new Error('something')) + sinon.stub(Api, 'get').withArgs('foo', 'bar', 1).throws() + const stub = sinon.stub(foo, 'bar').withArgs('foo', 1).throws(new Error('something')) + sinon.stub(foo, 'bar').withArgs('foo', sinon.match.object).callsFake((_, obj) => obj) +`, + ` + jest.fn().mockImplementation((...args) => { + if (args[0] === 'foo') { + return Promise.resolve('something'); + } + }) + jest.fn().mockImplementation((...args) => { + if (args[0] === 'foo' && args[1] === 'bar') { + return Promise.reject(new Error()); + } + }) + jest.fn().mockImplementation((...args) => { + if (args[0] === 'foo' && args[1] === 'bar' && args[2] === 1) { + return Promise.reject(new Error('something')); + } + }) + jest.spyOn(Api, 'get').mockClear().mockImplementation((...args) => { + if (args[0] === 'foo' && args[1] === 'bar' && args[2] === 1) { + throw new Error(); + } + }) + const stub = jest.spyOn(foo, 'bar').mockClear().mockImplementation((...args) => { + if (args[0] === 'foo' && args[1] === 1) { + throw new Error('something'); + } + }) + jest.spyOn(foo, 'bar').mockClear().mockImplementation((...args) => { + if (args[0] === 'foo' && typeof args[1] === 'object') { + return ((_, obj) => obj)(...args); + } + }) ` ) }) @@ -258,7 +369,7 @@ describe.each([ ) }) - /* + /* apiStub.getCall(0).args[1].data apistub.args[1][1] */ @@ -386,6 +497,60 @@ describe.each([ { parser: 'ts' } ) }) + it('handles .callsArg* after .on*Call/.withArgs', () => { + expectTransformation( + ` + ${sinonImport} + + apiStub.onFirstCall().callsArg(0) + apiStub.onSecondCall().callsArgOn(1, thisArg) + apiStub.onThirdCall().callsArgWith(2, 'a', 'b') + apiStub.onCall(2).callsArgOnWith(3, thisArg, 'c', 'd') + + apiStub.withArgs('foo', 'bar').callsArg(0) + apiStub.withArgs(sinon.match.any, sinon.match.func).callsArgOn(1, thisArg) + apiStub.withArgs(sinon.match.any).callsArgWith(0, 'a', 'b') +`, + ` + apiStub.mockImplementation((...args) => { + if (apiStub.mock.calls.length === 0) { + return args[0](); + } + }) + apiStub.mockImplementation((...args) => { + if (apiStub.mock.calls.length === 1) { + return args[1].call(thisArg); + } + }) + apiStub.mockImplementation((...args) => { + if (apiStub.mock.calls.length === 2) { + return args[2]('a', 'b'); + } + }) + apiStub.mockImplementation((...args) => { + if (apiStub.mock.calls.length === 2) { + return args[3].call(thisArg, 'c', 'd'); + } + }) + + apiStub.mockImplementation((...args) => { + if (args[0] === 'foo' && args[1] === 'bar') { + return args[0](); + } + }) + apiStub.mockImplementation((...args) => { + if (args.length >= 2 && typeof args[1] === 'function') { + return args[1].call(thisArg); + } + }) + apiStub.mockImplementation((...args) => { + if (args.length >= 1) { + return args[0]('a', 'b'); + } + }) +` + ) + }) it('handles on*Call', () => { expectTransformation( @@ -466,6 +631,65 @@ describe.each([ { parser: 'tsx' } ) }) + + it('handles on*Call chained with .resolves/.rejects/.throws/.callsFake', () => { + expectTransformation( + ` + ${sinonSandboxImport} + + stub.onFirstCall().resolves() + stub.onSecondCall().resolves(1) + + stub.onFirstCall().rejects() + stub.onSecondCall().rejects(new Error('msg')) + + stub.onThirdCall().throws() + stub.onThirdCall().throws(new Error('msg')) + + stub.onCall(1).callsFake(() => 2) +`, + ` + stub.mockImplementation(() => { + if (stub.mock.calls.length === 0) { + return Promise.resolve(); + } + }) + stub.mockImplementation(() => { + if (stub.mock.calls.length === 1) { + return Promise.resolve(1); + } + }) + + stub.mockImplementation(() => { + if (stub.mock.calls.length === 0) { + return Promise.reject(new Error()); + } + }) + stub.mockImplementation(() => { + if (stub.mock.calls.length === 1) { + return Promise.reject(new Error('msg')); + } + }) + + stub.mockImplementation(() => { + if (stub.mock.calls.length === 2) { + throw new Error(); + } + }) + stub.mockImplementation(() => { + if (stub.mock.calls.length === 2) { + throw new Error('msg'); + } + }) + + stub.mockImplementation((...args) => { + if (stub.mock.calls.length === 1) { + return (() => 2)(...args); + } + }) +` + ) + }) }) describe('mocks', () => { @@ -489,6 +713,9 @@ describe.each([ Api.get.restore() Api.get.reset() sinon.restore() + sinon.reset() + sinon.resetBehavior() + sinon.resetHistory() stub.resetBehavior() stub.resetHistory() `, @@ -497,6 +724,9 @@ describe.each([ Api.get.mockRestore() Api.get.mockReset() jest.restoreAllMocks() + jest.resetAllMocks() + jest.resetAllMocks() + jest.resetAllMocks() stub.mockReset() stub.mockReset() ` @@ -598,6 +828,9 @@ describe.each([ expect(spy.calledThrice).to.equal(true) expect(spy.called).to.equal(true) + expect(spy.calledOnce).equals(true) + expect(spy.called).equals(true) + // .to.be expect(Api.get.callCount).to.be(1) expect(Api.get.called).to.be(true) @@ -629,6 +862,9 @@ describe.each([ expect(spy).toHaveBeenCalledTimes(3) expect(spy).toHaveBeenCalled() + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalled() + // .to.be expect(Api.get).toHaveBeenCalledTimes(1) expect(Api.get).toHaveBeenCalled() diff --git a/src/transformers/sinon.ts b/src/transformers/sinon.ts index 4c2885b8..2e7ec529 100644 --- a/src/transformers/sinon.ts +++ b/src/transformers/sinon.ts @@ -1,3 +1,4 @@ +import { ExpressionKind } from 'ast-types/gen/kinds' import core, { API, FileInfo } from 'jscodeshift' import { @@ -27,18 +28,56 @@ const SINON_CALL_COUNT_METHODS = [ 'notCalled', ] const CHAI_CHAIN_MATCHERS = new Set( - ['be', 'eq', 'eql', 'equal', 'toBe', 'toEqual', 'toBeTruthy', 'toBeFalsy'].map((a) => - a.toLowerCase() - ) + [ + 'be', + 'eq', + 'eql', + 'equal', + 'equals', + 'toBe', + 'toEqual', + 'toBeTruthy', + 'toBeFalsy', + ].map((a) => a.toLowerCase()) ) -const SINON_CALLED_WITH_METHODS = ['calledWith', 'notCalledWith', 'neverCalledWith'] -const SINON_SPY_METHODS = ['spy', 'stub'] +const SINON_CALLED_WITH_METHODS = [ + 'calledWith', + 'notCalledWith', + 'neverCalledWith', +] as const +const SINON_SPY_METHODS = ['spy', 'stub'] as const const SINON_MOCK_RESETS = { reset: 'mockReset', resetBehavior: 'mockReset', resetHistory: 'mockReset', restore: 'mockRestore', -} +} as const +const SINON_GLOBAL_MOCK_RESETS = { + reset: 'resetAllMocks', + resetBehavior: 'resetAllMocks', + resetHistory: 'resetAllMocks', + restore: 'restoreAllMocks', +} as const +const _sinonMockImpls = [ + 'returns', + 'returnsArg', + 'resolves', + 'rejects', + 'throws', + 'callsFake', + 'callsArg', + 'callsArgOn', + 'callsArgWith', + 'callsArgOnWith', +] as const +type SinonMockImplementer = (typeof _sinonMockImpls)[number] +const SINON_MOCK_IMPLEMENTERS: Set = new Set(_sinonMockImpls) +const SINON_MOCK_IMPLS_TO_JEST = { + returns: 'mockReturnValue', + resolves: 'mockResolvedValue', + rejects: 'mockRejectedValue', + callsFake: 'mockImplementation', +} as const satisfies Partial> const SINON_MATCHERS = { array: 'Array', func: 'Function', @@ -60,86 +99,7 @@ const isPrefix = (name) => EXPECT_PREFIXES.has(name) const isTypescript = (parser: string) => parser === 'tsx' || parser === 'ts' -const SINON_CALLS_ARG = new Set([ - 'callsArg', - 'callsArgOn', - 'callsArgWith', - 'callsArgOnWith', -]) - -/* - stub.callsArg(0) -> stub.mockImplementation((...args: any[]) => args[0]()) - stub.callsArgOn(1, thisArg) -> stub.mockImplementation((...args: any[]) => args[1].call(thisArg)) - stub.callsArgWith(2, arg1, arg2) -> stub.mockImplementation((...args: any[]) => args[2](arg1, arg2)) - stub.callsArgOnWith(3, thisArg, arg1, arg2) -> stub.mockImplementation((...args: any[]) => args[3].call(thisArg, arg1, arg2)) -*/ -function transformCallsArg(j, ast, parser) { - ast - .find(j.CallExpression, { - callee: { - type: j.MemberExpression.name, - property: { - name: (name) => SINON_CALLS_ARG.has(name), - }, - }, - }) - .replaceWith((np) => { - const { node } = np - - if (node.arguments.length < 1) return node - - const argName = j.memberExpression(j.identifier('args'), node.arguments[0], true) - - const mockImplementationArg = j.spreadPropertyPattern( - j.identifier.from({ - name: 'args', - typeAnnotation: isTypescript(parser) - ? j.typeAnnotation(j.arrayTypeAnnotation(j.anyTypeAnnotation())) - : null, - }) - ) - - let mockImplementationInvocation - - switch (node.callee.property.name) { - case 'callsArg': - mockImplementationInvocation = j.callExpression(argName, []) - break - case 'callsArgOn': - mockImplementationInvocation = j.callExpression( - j.memberExpression(argName, j.identifier('call')), - [node.arguments[1]] - ) - break - case 'callsArgWith': - mockImplementationInvocation = j.callExpression( - argName, - node.arguments.slice(1) - ) - break - case 'callsArgOnWith': - mockImplementationInvocation = j.callExpression( - j.memberExpression(argName, j.identifier('call')), - node.arguments.slice(1) - ) - break - } - - const mockImplementationFn = j.arrowFunctionExpression( - [mockImplementationArg], - mockImplementationInvocation - ) - - const mockFn = node.callee.object - - return j.callExpression( - j.memberExpression(mockFn, j.identifier('mockImplementation')), - [mockImplementationFn] - ) - }) -} - -/* +/* expect(spy.called).to.be(true) -> expect(spy).toHaveBeenCalled() expect(spy.callCount).to.equal(2) -> expect(spy).toHaveBeenCalledTimes(2) expect(stub).toHaveProperty('callCount', 1) -> expect(stub).toHaveBeenCalledTimes(1) @@ -175,11 +135,11 @@ function transformCallCountAssertions(j, ast) { np.node.arguments = [expectArg.object] }) - /* + /* handle `expect(spy.withArgs('foo').called).to.be(true)` -> `expect(spy.calledWith(1,2,3)).to.be(true)` and let subsequent transform fn take care of converting to - the final form (ie: see `transformCalledWithAssertions`) + the final form (ie: see `transformCalledWithAssertions`) */ if (expectArg.object.callee?.property?.name === 'withArgs') { // change .withArgs() -> .calledWith() @@ -236,7 +196,7 @@ function transformCallCountAssertions(j, ast) { }) } -/* +/* expect(spy.calledWith(1, 2, 3)).to.be(true) -> expect(spy).toHaveBeenCalledWith(1, 2, 3); https://github.com/jordalgo/jest-codemods/blob/7de97c1d0370c7915cf5e5cc2a860bc5dd96744b/src/transformers/sinon.js#L267 @@ -286,7 +246,7 @@ function transformCalledWithAssertions(j, ast) { }) } -/* +/* sinon.stub(Api, 'get') -> jest.spyOn(Api, 'get') */ function transformStub(j, ast, sinonExpression, logWarning) { @@ -344,35 +304,19 @@ function transformStub(j, ast, sinonExpression, logWarning) { findParentOfType(np, j.VariableDeclaration.name) || findParentOfType(np, j.ExpressionStatement.name) - const callsFake = j(parent).find(j.CallExpression, { - callee: { - type: 'MemberExpression', - property: { type: 'Identifier', name: 'callsFake' }, - }, - }) - const hasCallsFake = callsFake.size() > 0 - - if (hasCallsFake) { - callsFake.forEach((np) => { - np.node.callee.property.name = 'mockImplementation' - }) - return spyOn - } - - const hasReturn = + const hasMockImpls = j(parent) .find(j.CallExpression, { callee: { type: 'MemberExpression', property: { type: 'Identifier', - name: (name) => ['returns', 'returnsArg'].includes(name), + name: (name) => SINON_MOCK_IMPLEMENTERS.has(name), }, }, }) .size() > 0 - - if (!hasReturn) { + if (!hasMockImpls) { spyOn = j.callExpression( j.memberExpression(spyOn, j.identifier('mockImplementation')), [] @@ -388,6 +332,133 @@ function transformStub(j, ast, sinonExpression, logWarning) { }) } +function getMockImplReturn(j: core.JSCodeshift, sinonImpl) { + const sinonMethodName: SinonMockImplementer = sinonImpl.callee.property.name + + if (sinonMethodName.startsWith('callsArg')) { + /* + stub.callsArg(0) -> stub.mockImplementation((...args: any[]) => args[0]()) + stub.callsArgOn(1, thisArg) -> stub.mockImplementation((...args: any[]) => args[1].call(thisArg)) + stub.callsArgWith(2, arg1, arg2) -> stub.mockImplementation((...args: any[]) => args[2](arg1, arg2)) + stub.callsArgOnWith(3, thisArg, arg1, arg2) -> stub.mockImplementation((...args: any[]) => args[3].call(thisArg, arg1, arg2)) + */ + if (sinonImpl.arguments.length < 1) return undefined + + const argName = j.memberExpression(j.identifier('args'), sinonImpl.arguments[0], true) + switch (sinonMethodName) { + case 'callsArg': + return j.callExpression(argName, []) + case 'callsArgOn': + return j.callExpression(j.memberExpression(argName, j.identifier('call')), [ + sinonImpl.arguments[1], + ]) + case 'callsArgWith': + return j.callExpression(argName, sinonImpl.arguments.slice(1)) + case 'callsArgOnWith': + return j.callExpression( + j.memberExpression(argName, j.identifier('call')), + sinonImpl.arguments.slice(1) + ) + } + } + + let args = sinonImpl.arguments.slice(0, 1) + if ( + args.length === 0 && + (sinonMethodName === 'rejects' || sinonMethodName === 'throws') + ) { + args = [j.newExpression(j.identifier('Error'), [])] + } + + switch (sinonMethodName) { + case 'returns': + return args[0] ?? j.identifier('undefined') + case 'returnsArg': + return j.memberExpression(j.identifier('args'), args[0], true) + case 'resolves': + return j.callExpression( + j.memberExpression(j.identifier('Promise'), j.identifier('resolve')), + args + ) + case 'rejects': + return j.callExpression( + j.memberExpression(j.identifier('Promise'), j.identifier('reject')), + args + ) + case 'throws': + return j.throwStatement(args[0]) + case 'callsFake': + return j.callExpression(args[0], [j.spreadElement(j.identifier('args'))]) + } +} + +/** gets one of sinon mock implementers (returns/returnsArg/resolves/...) and returns jest equivalent */ +function getMockImplReplacement( + j: core.JSCodeshift, + sinonImpl, + parser, + conditionalExpr?: ExpressionKind +) { + const sinonMethodName: SinonMockImplementer = sinonImpl.callee.property.name + const isTypescript = parser === 'ts' || parser === 'tsx' + + if ( + conditionalExpr === undefined && + SINON_MOCK_IMPLS_TO_JEST[sinonMethodName] !== undefined + ) { + sinonImpl.callee.property.name = SINON_MOCK_IMPLS_TO_JEST[sinonMethodName] + if (sinonMethodName === 'rejects' && sinonImpl.arguments.length === 0) { + // mockRejectedValue without argument does not throw Error like sinon.rejects does, fix args + sinonImpl.arguments = [j.newExpression(j.identifier('Error'), [])] + } + return sinonImpl + } + + const implReturn = getMockImplReturn(j, sinonImpl) + if (implReturn === undefined) { + return sinonImpl + } + + const returnIsExpr = j(implReturn).isOfType(j.Expression) + let returnBody = + returnIsExpr && conditionalExpr === undefined + ? implReturn + : j.blockStatement([returnIsExpr ? j.returnStatement(implReturn) : implReturn]) + if (conditionalExpr !== undefined) { + returnBody = j.blockStatement([j.ifStatement(conditionalExpr, returnBody)]) + } + const mockImplementationArgs = + j(returnBody).find(j.Identifier, { name: 'args' }).size() === 0 + ? [] + : [ + j.spreadPropertyPattern( + j.identifier.from({ + name: 'args', + typeAnnotation: isTypescript + ? j.typeAnnotation(j.arrayTypeAnnotation(j.anyTypeAnnotation())) + : null, + }) + ), + ] + const mockImplementationFn = j.arrowFunctionExpression( + mockImplementationArgs, + returnBody + ) + + if (conditionalExpr === undefined) { + sinonImpl.callee.property.name = 'mockImplementation' + sinonImpl.arguments = [mockImplementationFn] + return sinonImpl + } + return j.callExpression( + j.memberExpression( + sinonImpl.callee.object.callee.object, + j.identifier('mockImplementation') + ), + [mockImplementationFn] + ) +} + /* transform .onCall(0), .on{First,Second,Third}Call() @@ -406,7 +477,7 @@ function transformStubOnCalls(j, ast, parser) { }, }, property: { - name: (n) => ['returns', 'returnsArg'].includes(n), + name: (n) => SINON_MOCK_IMPLEMENTERS.has(n), }, }, }) @@ -436,39 +507,7 @@ function transformStubOnCalls(j, ast, parser) { index ) - const isReturns = node.callee.property.name === 'returns' - const isTypescript = parser === 'ts' || parser === 'tsx' - - const mockImplementationArgs = isReturns - ? [] - : [ - j.spreadPropertyPattern( - j.identifier.from({ - name: 'args', - typeAnnotation: isTypescript - ? j.typeAnnotation(j.arrayTypeAnnotation(j.anyTypeAnnotation())) - : null, - }) - ), - ] - const mockImplementationReturn = isReturns - ? node.arguments[0] - : j.memberExpression(j.identifier('args'), node.arguments[0], true) - - const mockImplementationFn = j.arrowFunctionExpression( - mockImplementationArgs, - j.blockStatement([ - j.ifStatement( - callLengthConditionalExpression, - j.blockStatement([j.returnStatement(mockImplementationReturn)]) - ), - ]) - ) - - return j.callExpression( - j.memberExpression(mockFn, j.identifier('mockImplementation')), - [mockImplementationFn] - ) + return getMockImplReplacement(j, node, parser, callLengthConditionalExpression) }) } @@ -554,7 +593,7 @@ function transformStubGetCalls(j: core.JSCodeshift, ast) { return np.node.object } - /* + /* replace .args with mock.calls, handles: stub.args[0][0] -> stub.mock.calls[0][0] */ @@ -562,7 +601,7 @@ function transformStubGetCalls(j: core.JSCodeshift, ast) { }) } -/* +/* handles: .withArgs .returns @@ -580,7 +619,9 @@ function transformMock(j: core.JSCodeshift, ast, parser: string) { }, }, }, - property: { name: 'returns' }, + property: { + name: (n) => SINON_MOCK_IMPLEMENTERS.has(n), + }, }, }) .replaceWith((np) => { @@ -589,10 +630,9 @@ function transformMock(j: core.JSCodeshift, ast, parser: string) { // `jest.spyOn` or `jest.fn` const mockFn = node.callee.object.callee.object const mockImplementationArgs = node.callee.object.arguments - const mockImplementationReturn = node.arguments // unsupported/untransformable .withArgs, just remove .withArgs from chain - if (!mockImplementationArgs?.length || !mockImplementationReturn?.length) { + if (!mockImplementationArgs?.length) { node.callee = j.memberExpression(mockFn, node.callee.property) return node } @@ -605,7 +645,7 @@ function transformMock(j: core.JSCodeshift, ast, parser: string) { // generate conditional expression to match args used in .mockImplementation const mockImplementationConditionalExpression = (mockImplementationArgs as any[]) .map((arg, i) => { - const argName = j.identifier(`args[${i}]`) + const argName = j.memberExpression(j.identifier('args'), j.literal(i), true) // handle sinon matchers if (isSinonMatcherArg(arg)) { const matcherType = SINON_MATCHERS_WITH_ARGS[arg.property.name] @@ -633,73 +673,28 @@ function transformMock(j: core.JSCodeshift, ast, parser: string) { return j.logicalExpression('&&', logicalExp, binExp) }) - const mockImplementationArg = j.spreadPropertyPattern( - j.identifier.from({ - name: 'args', - typeAnnotation: isTypescript(parser) - ? j.typeAnnotation(j.arrayTypeAnnotation(j.anyTypeAnnotation())) - : null, - }) - ) - - const mockImplementationFn = j.arrowFunctionExpression( - [mockImplementationArg], - j.blockStatement([ - j.ifStatement( - mockImplementationConditionalExpression, - j.blockStatement([j.returnStatement(mockImplementationReturn[0])]) - ), - ]) - ) - - // `jest.fn` or `jest.spyOn` - return j.callExpression( - j.memberExpression(mockFn, j.identifier('mockImplementation')), - [mockImplementationFn] + return getMockImplReplacement( + j, + node, + parser, + mockImplementationConditionalExpression ) }) - // any remaining `.returns()` -> `.mockReturnValue()` - ast - .find(j.CallExpression, { - callee: { - type: 'MemberExpression', - property: { type: 'Identifier', name: 'returns' }, - }, - }) - .forEach((np) => { - np.node.callee.property.name = 'mockReturnValue' - }) - - // .returnsArg + // any remaining sinon mock impl (returns, returnsArg, etc.) ast .find(j.CallExpression, { callee: { type: 'MemberExpression', - property: { name: 'returnsArg' }, + property: { + name: (n) => SINON_MOCK_IMPLEMENTERS.has(n), + }, }, }) - .replaceWith((np) => { - const { node } = np - node.callee.property.name = 'mockImplementation' - const argToMock = j.literal(node.arguments[0].value) - - const argsVar = j.identifier.from({ - name: 'args', - typeAnnotation: isTypescript(parser) - ? j.typeAnnotation(j.arrayTypeAnnotation(j.anyTypeAnnotation())) - : null, - }) - const mockImplementationFn = j.arrowFunctionExpression( - [j.spreadPropertyPattern(argsVar)], - j.memberExpression(j.identifier('args'), argToMock) - ) - node.arguments = [mockImplementationFn] - return node - }) + .replaceWith(({ node }) => getMockImplReplacement(j, node, parser)) } -/* +/* handles mock resets/clears/etc: sinon.restore() -> jest.restoreAllMocks() stub.restore() -> stub.mockRestore() @@ -716,13 +711,13 @@ function transformMockResets(j, ast) { }, property: { type: 'Identifier', - name: 'restore', + name: (name) => Object.hasOwn(SINON_GLOBAL_MOCK_RESETS, name), }, }, }) - .forEach((np) => { - np.node.callee.object.name = 'jest' - np.node.callee.property.name = 'restoreAllMocks' + .forEach(({ node }) => { + node.callee.object.name = 'jest' + node.callee.property.name = SINON_GLOBAL_MOCK_RESETS[node.callee.property.name] }) ast @@ -741,7 +736,7 @@ function transformMockResets(j, ast) { }) } -/* +/* sinon.assert.called(spy) -> expect(spy).toHaveBeenCalled() sinon.assert.calledOnce(spy) -> expect(spy).toHaveBeenCalledTimes(1) sinon.assert.calledWith(spy, arg1, arg2) -> expect(spy).toHaveBeenCalledWith(arg1, arg2) @@ -822,7 +817,7 @@ function transformAssert(j, ast) { }) } -/* +/* sinon.match({ ... }) -> expect.objectContaining({ ... }) // .any. matches: sinon.match.[any|number|string|object|func|array] -> expect.any(type) @@ -943,7 +938,7 @@ function transformMockTimers(j, ast) { node.callee.property.name = 'advanceTimersByTime' }) - /* + /* `stub.restore` shares the same property name as `sinon.useFakeTimers().restore` so only transform those with `clock` object which seems to be the common name used for mock timers throughout our codebase @@ -1027,7 +1022,6 @@ export default function transformer(fileInfo: FileInfo, api: API, options) { transformMockTimers(j, ast) transformMock(j, ast, options.parser) transformMockResets(j, ast) - transformCallsArg(j, ast, options.parser) transformCallCountAssertions(j, ast) transformCalledWithAssertions(j, ast) transformAssert(j, ast)