Repository: skovhus/jest-codemods
Pull request #361 (2022-08-20 01:32:39)

[sinon] implement on{,First,Second,Third}Call([...])
Reviewers (other than @skovhus): @catc @lencioni @jayrobin

codecov[bot]: # [Codecov](https://codecov.io/gh/skovhus/jest-codemods/pull/361?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus) Report
> Merging [#361](https://codecov.io/gh/skovhus/jest-codemods/pull/361?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus) (899397d) into [main](https://codecov.io/gh/skovhus/jest-codemods/commit/1a26e298022e7956bc5954cc75816cd6ca58260a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus) (1a26e29) will **increase** coverage by `0.07%`.
> The diff coverage is `96.77%`.

```diff
@@            Coverage Diff             @@
##             main     #361      +/-   ##
==========================================
+ Coverage   92.09%   92.17%   +0.07%     
==========================================
  Files          26       26              
  Lines        1872     1903      +31     
  Branches      393      401       +8     
==========================================
+ Hits         1724     1754      +30     
  Misses        102      102              
- Partials       46       47       +1     
```


| [Impacted Files](https://codecov.io/gh/skovhus/jest-codemods/pull/361?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus) | Coverage Δ | |
|---|---|---|
| [src/transformers/sinon.ts](https://codecov.io/gh/skovhus/jest-codemods/pull/361/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus#diff-c3JjL3RyYW5zZm9ybWVycy9zaW5vbi50cw==) | `88.66% <96.77%> (+1.16%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus)


Patch:
diff --git a/src/transformers/sinon.ts b/src/transformers/sinon.ts
index fccb71e..370f1ba 100644
--- a/src/transformers/sinon.ts
+++ b/src/transformers/sinon.ts
@@ -54,6 +54,7 @@ const SINON_MATCHERS_WITH_ARGS = {
   string: 'string',
 }
 const SINON_NTH_CALLS = new Set(['firstCall', 'secondCall', 'thirdCall', 'lastCall'])
+const SINON_ON_NTH_CALLS = new Set(['onFirstCall', 'onSecondCall', 'onThirdCall'])
 const EXPECT_PREFIXES = new Set(['to'])
 const isPrefix = (name) => EXPECT_PREFIXES.has(name)
 
@@ -370,6 +371,90 @@ function transformStub(j, ast, sinonExpression, logWarning) {
     })
 }
 
+/*
+  transform .onCall(0), .on{First,Second,Third}Call()
+
+  stub.onCall(4).return(biscuits) -> stub.mockImplementation(() => { if (stub.mock.calls.length === 3) return biscuits; })
+  stub.onFirstCall().returnArg(2) -> stub.mockImplementation((...args: any[]) => { if (stub.mock.calls.length === 0) return args[2]; })
+*/
+function transformStubOnCalls(j, ast, parser) {
+  ast
+    .find(j.CallExpression, {
+      callee: {
+        object: {
+          callee: {
+            property: {
+              name: (n) => n === 'onCall' || SINON_ON_NTH_CALLS.has(n),
+            },
+          },
+        },
+        property: {
+          name: (n) => ['returns', 'returnsArg'].includes(n),
+        },
+      },
+    })
+    .replaceWith(({ node }) => {
+      let index
+      switch (node.callee.object.callee.property.name) {
+        case 'onCall':
+          index = node.callee.object.arguments[0]
+          break
+        case 'onFirstCall':
+          index = j.numericLiteral(0)
+          break
+        case 'onSecondCall':
+          index = j.numericLiteral(1)
+          break
+        case 'onThirdCall':
+          index = j.numericLiteral(2)
+          break
+      }
+      if (!index) return node
+
+      // `jest.spyOn` or `jest.fn`
+      const mockFn = node.callee.object.callee.object
+      const callLengthConditionalExpression = j.binaryExpression(
+        '===',
+        j.memberExpression(mockFn, j.identifier('mock.calls.length')),
+        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]
+      )
+    })
+}
+
 /*
   stub.getCall(0) -> stub.mock.calls[0]
   stub.getCall(0).args[1] -> stub.mock.calls[0][1]
@@ -547,7 +632,7 @@ function transformMock(j: core.JSCodeshift, ast, parser: string) {
         j.blockStatement([
           j.ifStatement(
             mockImplementationConditionalExpression,
-            j.returnStatement(mockImplementationReturn[0])
+            j.blockStatement([j.returnStatement(mockImplementationReturn[0])])
           ),
         ])
       )
@@ -804,6 +889,7 @@ export default function transformer(fileInfo: FileInfo, api: API, options) {
   const logWarning = (msg, node) => logger(fileInfo, msg, node)
 
   transformStub(j, ast, sinonExpression, logWarning)
+  transformStubOnCalls(j, ast, options.parser)
   transformMockTimers(j, ast)
   transformMock(j, ast, options.parser)
   transformMockResets(j, ast)


Test patch:
diff --git a/src/transformers/sinon.test.ts b/src/transformers/sinon.test.ts
index 5fddb52..297e73c 100644
--- a/src/transformers/sinon.test.ts
+++ b/src/transformers/sinon.test.ts
@@ -180,32 +180,39 @@ describe('spies and stubs', () => {
 `,
       `
         jest.fn().mockImplementation((...args) => {
-                if (args[0] === 'foo')
+                if (args[0] === 'foo') {
                         return 'something';
+                }
         })
         jest.fn().mockImplementation((...args) => {
-                if (args[0] === 'foo' && args[1] === 'bar')
+                if (args[0] === 'foo' && args[1] === 'bar') {
                         return 'something';
+                }
         })
         jest.fn().mockImplementation((...args) => {
-                if (args[0] === 'foo' && args[1] === 'bar' && args[2] === 1)
+                if (args[0] === 'foo' && args[1] === 'bar' && args[2] === 1) {
                         return 'something';
+                }
         })
         jest.spyOn(Api, 'get').mockClear().mockImplementation((...args) => {
-                if (args[0] === 'foo' && args[1] === 'bar' && args[2] === 1)
+                if (args[0] === 'foo' && args[1] === 'bar' && args[2] === 1) {
                         return 'something';
+                }
         })
         const stub = jest.spyOn(foo, 'bar').mockClear().mockImplementation((...args) => {
-                if (args[0] === 'foo' && args[1] === 1)
+                if (args[0] === 'foo' && args[1] === 1) {
                         return 'something';
+                }
         })
         jest.spyOn(foo, 'bar').mockClear().mockImplementation((...args) => {
-                if (args[0] === 'foo' && typeof args[1] === 'object')
+                if (args[0] === 'foo' && typeof args[1] === 'object') {
                         return 'something';
+                }
         })
         jest.fn().mockImplementation((...args) => {
-                if (args[0] === 'foo' && args.length >= 2)
+                if (args[0] === 'foo' && args.length >= 2) {
                         return 'something';
+                }
         })
 `
     )
@@ -220,8 +227,9 @@ describe('spies and stubs', () => {
     `,
       `
       jest.fn().mockImplementation((...args: any[]) => {
-            if (args[0] === 'foo')
+            if (args[0] === 'foo') {
                   return 'something';
+            }
       })
     `,
       { parser: 'tsx' }
@@ -356,6 +364,86 @@ describe('spies and stubs', () => {
       { parser: 'ts' }
     )
   })
+
+  it('handles on*Call', () => {
+    expectTransformation(
+      `
+      import sinon from 'sinon-sandbox'
+
+      stub.onFirstCall().returns(5)
+      stub.onSecondCall().returns(6, 7)
+
+      stub.onFirstCall().returnsArg(0)
+      stub.onSecondCall().returnsArg(1)
+
+      // invalid onCall() params
+      stub.onCall().returns('invalid')
+`,
+      `
+      stub.mockImplementation(() => {
+            if (stub.mock.calls.length === 0) {
+                  return 5;
+            }
+      })
+      stub.mockImplementation(() => {
+            if (stub.mock.calls.length === 1) {
+                  return 6;
+            }
+      })
+
+      stub.mockImplementation((...args) => {
+            if (stub.mock.calls.length === 0) {
+                  return args[0];
+            }
+      })
+      stub.mockImplementation((...args) => {
+            if (stub.mock.calls.length === 1) {
+                  return args[1];
+            }
+      })
+
+      // invalid onCall() params
+      stub.onCall().mockReturnValue('invalid')
+`
+    )
+  })
+  it('handles on*Call (parser: ts)', () => {
+    expectTransformation(
+      `
+      import sinon from 'sinon-sandbox'
+
+      stub.onThirdCall().returns([8, 9, 10])
+      stub.onCall(3).returns(biscuits)
+
+      stub.onThirdCall().returnsArg(2)
+      stub.onCall(3).returnsArg(biscuits)
+`,
+      `
+      stub.mockImplementation(() => {
+            if (stub.mock.calls.length === 2) {
+                  return [8, 9, 10];
+            }
+      })
+      stub.mockImplementation(() => {
+            if (stub.mock.calls.length === 3) {
+                  return biscuits;
+            }
+      })
+
+      stub.mockImplementation((...args: any[]) => {
+            if (stub.mock.calls.length === 2) {
+                  return args[2];
+            }
+      })
+      stub.mockImplementation((...args: any[]) => {
+            if (stub.mock.calls.length === 3) {
+                  return args[biscuits];
+            }
+      })
+`,
+      { parser: 'tsx' }
+    )
+  })
 })
 
 describe('mocks', () => {