Repository: skovhus/jest-codemods
Pull request #322 (2022-07-05 19:24:53)

More chai/sinon patches (take 2)
@skovhus this is https://github.com/skovhus/jest-codemods/pull/315 without amending the old commit.

I wrote https://github.com/danbeam/jest-codemods/commit/171f96d46a9d1225abafbb3aa87995aeb9a16d46 chronologically before https://github.com/skovhus/jest-codemods/pull/291 but forgot to upstream it (whoops) so I'm doing that now.

@catc wrote https://github.com/danbeam/jest-codemods/commit/d2b3d76e3e14772b83bbc07dcd796ea5193f2c53 and I wrote https://github.com/danbeam/jest-codemods/commit/ddc2c66b16d4493573839f55d79223ecd3e6f1d6.  cc @lencioni

codecov[bot]: # [Codecov](https://codecov.io/gh/skovhus/jest-codemods/pull/322?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus) Report
> Merging [#322](https://codecov.io/gh/skovhus/jest-codemods/pull/322?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus) (ddc2c66) into [main](https://codecov.io/gh/skovhus/jest-codemods/commit/fbb9bdef37d826bf2d5594ddd44872f5d0f16286?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus) (fbb9bde) will **decrease** coverage by `0.13%`.
> The diff coverage is `84.00%`.

```diff
@@            Coverage Diff             @@
##             main     #322      +/-   ##
==========================================
- Coverage   92.22%   92.09%   -0.14%     
==========================================
  Files          26       26              
  Lines        1852     1872      +20     
  Branches      385      392       +7     
==========================================
+ Hits         1708     1724      +16     
  Misses        102      102              
- Partials       42       46       +4     
```


| [Impacted Files](https://codecov.io/gh/skovhus/jest-codemods/pull/322?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus) | Coverage Δ | |
|---|---|---|
| [src/utils/consts.ts](https://codecov.io/gh/skovhus/jest-codemods/pull/322/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus#diff-c3JjL3V0aWxzL2NvbnN0cy50cw==) | `100.00% <ø> (ø)` | |
| [src/transformers/chai-should.ts](https://codecov.io/gh/skovhus/jest-codemods/pull/322/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus#diff-c3JjL3RyYW5zZm9ybWVycy9jaGFpLXNob3VsZC50cw==) | `94.78% <75.00%> (-0.80%)` | :arrow_down: |
| [src/transformers/sinon.ts](https://codecov.io/gh/skovhus/jest-codemods/pull/322/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus#diff-c3JjL3RyYW5zZm9ybWVycy9zaW5vbi50cw==) | `87.50% <92.30%> (+0.18%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/skovhus/jest-codemods/pull/322?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/skovhus/jest-codemods/pull/322?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus). Last update [fbb9bde...ddc2c66](https://codecov.io/gh/skovhus/jest-codemods/pull/322?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Kenneth+Skovhus).


Patch:
diff --git a/src/transformers/chai-should.ts b/src/transformers/chai-should.ts
index 5b46c87..925e473 100644
--- a/src/transformers/chai-should.ts
+++ b/src/transformers/chai-should.ts
@@ -635,7 +635,7 @@ export default function transformer(fileInfo, api, options) {
           case 'eql':
           case 'eq':
           case 'equal':
-          case 'equals':
+          case 'equals': {
             if (numberOfArgs === 1) {
               const { type } = firstArg
 
@@ -652,7 +652,16 @@ export default function transformer(fileInfo, api, options) {
               }
             }
 
-            return createCall('toEqual', args, rest, containsNot)
+            const containsDeep = chainContains('deep', value.callee, isPrefix)
+            const isStrict = ['equal', 'equals', 'eq'].includes(propertyName)
+
+            return createCall(
+              isStrict && !containsDeep ? 'toBe' : 'toEqual',
+              args,
+              rest,
+              containsNot
+            )
+          }
           case 'throw':
             return createCall('toThrowError', args, rest, containsNot)
           case 'string':
@@ -675,6 +684,15 @@ export default function transformer(fileInfo, api, options) {
               return createCall('toMatchObject', args, rest, containsNot)
             }
 
+            // handle `expect(wrapper.text()).to.contain('string')`
+            const expectArg = rest.arguments[0]
+            if (
+              expectArg?.type === j.CallExpression.name &&
+              expectArg.callee?.property?.name === 'text'
+            ) {
+              return createCall('toContain', args, rest, containsNot)
+            }
+
             const expectNode = getExpectNode(value)
             if (expectNode != null) {
               // handle `expect([1, 2]).toContain(1)
@@ -694,16 +712,7 @@ export default function transformer(fileInfo, api, options) {
 
             // handle `expect(wrapper).to.contain(<div />)`
             if (args?.[0]?.type === j.JSXElement.name) {
-              return createCall(
-                'toEqual',
-                [j.identifier('true')],
-                updateExpect(value, (node) => {
-                  return j.callExpression(
-                    j.memberExpression(node, j.identifier('contains')),
-                    [args[0]]
-                  )
-                })
-              )
+              return createCall('containsMatchingElement', args, rest, containsNot)
             }
 
             return createCall(
@@ -922,6 +931,13 @@ export default function transformer(fileInfo, api, options) {
       })
       .replaceWith((p) => {
         const { value } = p
+        /* 
+          if call expression is spread, don't transform, eg:
+          const foo = { params: ...context(searchParams) }
+        */
+        if (p.parentPath?.value?.type === j.SpreadElement.name) {
+          return value
+        }
         return j.callExpression(
           j.identifier(chaiToJestGlobalMethods[value.callee.name]),
           value.arguments
diff --git a/src/transformers/sinon.ts b/src/transformers/sinon.ts
index 46b656f..b8a6248 100644
--- a/src/transformers/sinon.ts
+++ b/src/transformers/sinon.ts
@@ -58,6 +58,7 @@ const isPrefix = (name) => EXPECT_PREFIXES.has(name)
 /* 
   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)
 */
 function transformCallCountAssertions(j, ast) {
   const chainContains = chainContainsUtil(j)
@@ -128,6 +129,26 @@ function transformCallCountAssertions(j, ast) {
           )
       }
     })
+
+  // expect(stub).toHaveProperty('callCount', 1) -> expect(stub).toHaveBeenCalledTimes(1)
+  ast
+    .find(j.CallExpression, {
+      callee: {
+        type: j.MemberExpression.name,
+        property: {
+          name: 'toHaveProperty',
+        },
+        object: (node) => isExpectCallUtil(j, node),
+      },
+      arguments: (args) => args?.[0]?.value === 'callCount',
+    })
+    .replaceWith((np) => {
+      const { value } = np
+      const newArgs = value.arguments.slice(1)
+      value.callee.property.name = 'toHaveBeenCalledTimes'
+      value.arguments = newArgs
+      return value
+    })
 }
 
 /* 
@@ -183,7 +204,7 @@ function transformCalledWithAssertions(j, ast) {
 /* 
 sinon.stub(Api, 'get') -> jest.spyOn(Api, 'get')
 */
-function transformStub(j: core.JSCodeshift, ast, sinonExpression, logWarning) {
+function transformStub(j, ast, sinonExpression, logWarning) {
   ast
     .find(j.CallExpression, {
       callee: {
@@ -235,8 +256,8 @@ function transformStub(j: core.JSCodeshift, ast, sinonExpression, logWarning) {
           }
         } else if (propertyName === 'stub') {
           const parent =
-            findParentOfType(np, 'VariableDeclaration') ||
-            findParentOfType(np, 'ExpressionStatement')
+            findParentOfType(np, j.VariableDeclaration.name) ||
+            findParentOfType(np, j.ExpressionStatement.name)
 
           const hasReturn =
             j(parent)
@@ -363,7 +384,7 @@ function transformStubGetCalls(j: core.JSCodeshift, ast) {
     .returns
     .returnsArg
 */
-function transformMock(j: core.JSCodeshift, ast) {
+function transformMock(j: core.JSCodeshift, ast, parser: string) {
   // stub.withArgs(111).returns('foo') => stub.mockImplementation((...args) => { if (args[0] === '111') return 'foo' })
   ast
     .find(j.CallExpression, {
@@ -428,8 +449,18 @@ function transformMock(j: core.JSCodeshift, ast) {
           return j.logicalExpression('&&', logicalExp, binExp)
         })
 
+      const isTypescript = parser === 'tsx' || parser === 'ts'
+      const mockImplementationArg = j.spreadPropertyPattern(
+        j.identifier.from({
+          name: 'args',
+          typeAnnotation: isTypescript
+            ? j.typeAnnotation(j.arrayTypeAnnotation(j.anyTypeAnnotation()))
+            : null,
+        })
+      )
+
       const mockImplementationFn = j.arrowFunctionExpression(
-        [j.spreadPropertyPattern(j.identifier('args'))],
+        [mockImplementationArg],
         j.blockStatement([
           j.ifStatement(
             mockImplementationConditionalExpression,
@@ -685,7 +716,7 @@ export default function transformer(fileInfo: FileInfo, api: API, options) {
 
   transformStub(j, ast, sinonExpression, logWarning)
   transformMockTimers(j, ast)
-  transformMock(j, ast)
+  transformMock(j, ast, options.parser)
   transformMockResets(j, ast)
   transformCallCountAssertions(j, ast)
   transformCalledWithAssertions(j, ast)
diff --git a/src/utils/consts.ts b/src/utils/consts.ts
index a970711..6e90376 100644
--- a/src/utils/consts.ts
+++ b/src/utils/consts.ts
@@ -27,6 +27,7 @@ export const JEST_MATCHER_TO_MAX_ARGS = {
   toBeUndefined: 0,
   toContain: 1,
   toContainEqual: 1,
+  containsMatchingElement: 1,
   toEqual: 1,
   toHaveBeenCalled: 0,
   toHaveBeenCalledTimes: 1,


Test patch:
diff --git a/src/transformers/chai-should.test.ts b/src/transformers/chai-should.test.ts
index f15671d..e4f64b4 100644
--- a/src/transformers/chai-should.test.ts
+++ b/src/transformers/chai-should.test.ts
@@ -25,7 +25,7 @@ test('chai-enzyme: handle .to.contain(JSXElement)', () => {
     expect(wrapper).to.contain(<ImpressionLogger />)
   `,
     `
-    expect(wrapper.contains(<ImpressionLogger />)).toEqual(true)
+    expect(wrapper).containsMatchingElement(<ImpressionLogger />)
   `
   )
 })
@@ -120,8 +120,8 @@ test('removes imports and does basic conversions of should and expect', () => {
     `
         describe('Instantiating TextField', () => {
             it('should set the placeholder correctly', () => {
-                expect(textField.props.placeholder).toEqual(PLACEHOLDER);
-                expect(textField.props.placeholder).not.toEqual(PLACEHOLDER);
+                expect(textField.props.placeholder).toBe(PLACEHOLDER);
+                expect(textField.props.placeholder).not.toBe(PLACEHOLDER);
             });
         });
     `
@@ -156,13 +156,13 @@ test('removes imports and does basic conversions of should and expect (2)', () =
     `
         describe('Instantiating TextField', () => {
             it('should set the placeholder correctly', () => {
-                expect(textField.props.placeholder).toEqual(PLACEHOLDER);
-                expect(textField.props.placeholder).not.toEqual(PLACEHOLDER);
+                expect(textField.props.placeholder).toBe(PLACEHOLDER);
+                expect(textField.props.placeholder).not.toBe(PLACEHOLDER);
             });
 
             it('should inherit id prop', () => {
-                expect(dropdown.props.id).toEqual(STANDARD_PROPS.id);
-                expect(dropdown.props.id).not.toEqual(STANDARD_PROPS.id);
+                expect(dropdown.props.id).toBe(STANDARD_PROPS.id);
+                expect(dropdown.props.id).not.toBe(STANDARD_PROPS.id);
             });
 
             it('should map open prop to visible prop', () => {
@@ -190,8 +190,8 @@ test('removes imports (case where should is not assigned)', () => {
     `
         describe('Instantiating TextField', () => {
           it('should set the placeholder correctly', () => {
-              expect(textField.props.placeholder).toEqual(PLACEHOLDER);
-              expect(textField.props.placeholder).not.toEqual(PLACEHOLDER);
+              expect(textField.props.placeholder).toBe(PLACEHOLDER);
+              expect(textField.props.placeholder).not.toBe(PLACEHOLDER);
           });
         });`
   )
@@ -456,22 +456,28 @@ test('converts "equal"', () => {
         expect(42).to.equal(42);
         expect(1).to.not.equal(true);
         expect({ foo: 'bar' }).to.not.equal({ foo: 'bar' });
+
         expect({ foo: 'bar' }).to.deep.equal({ foo: 'bar' });
+        expect({ foo: 'bar' }).not.to.deep.equal({ foo: 'bar' });
+        expect({ foo: 'bar' }).to.not.deep.equal({ foo: 'bar' });
 
         should.equal('foo', 'foo');
         should.not.equal('foo', 'bar');
     `,
     `
-        expect('hello').toEqual('hello');
+        expect('hello').toBe('hello');
         // some message here explaining hello
-        expect('hello').toEqual('hello');
-        expect(42).toEqual(42);
-        expect(1).not.toEqual(true);
-        expect({ foo: 'bar' }).not.toEqual({ foo: 'bar' });
+        expect('hello').toBe('hello');
+        expect(42).toBe(42);
+        expect(1).not.toBe(true);
+        expect({ foo: 'bar' }).not.toBe({ foo: 'bar' });
+
         expect({ foo: 'bar' }).toEqual({ foo: 'bar' });
+        expect({ foo: 'bar' }).not.toEqual({ foo: 'bar' });
+        expect({ foo: 'bar' }).not.toEqual({ foo: 'bar' });
 
-        expect('foo').toEqual('foo');
-        expect('foo').not.toEqual('bar');
+        expect('foo').toBe('foo');
+        expect('foo').not.toBe('bar');
     `
   )
 })
@@ -633,6 +639,17 @@ test('converts chained "includes-contains"', () => {
   )
 })
 
+test('chai-enzyme: handle .text() with .contains()', () => {
+  expectTransformation(
+    `
+      expect(wrapper.text()).to.contain(priceString);
+    `,
+    `
+      expect(wrapper.text()).toContain(priceString);
+    `
+  )
+})
+
 test('converts empty array assertion', () => {
   expectTransformation(
     `
@@ -1030,11 +1047,11 @@ test('removes params to expect() except for the first', () => {
         // Expected foo to be defined
         expect(foo).toBeDefined();
         // Expected foo to be defined
-        expect(foo).toEqual(true);
+        expect(foo).toBe(true);
         // Expected foo to be defined for \${id}
         expect(foo).toBeDefined();
         // 'Expected ' + foo + ' to be defined'
-        expect(foo).toEqual(true);
+        expect(foo).toBe(true);
     `
   )
 })
@@ -1118,6 +1135,47 @@ test('converts equalto(null) to toBeNull()', () => {
   )
 })
 
+test('converts subtly different equality operators', () => {
+  expectTransformation(
+    `
+        // Strict equality (===)
+        expect(2 + 2).to.eq(5);
+        expect(2 + 2).to.equal(5);
+        expect(2 + 2).to.equals(5);
+        expect(2 + 2).to.not.equals(5);
+        expect(2 + 2).not.to.equals(5);
+
+        // Strict equality (===) + deep = deep equality
+        expect(2 + 2).to.deep.eq(5);
+
+        // Deep equality
+        expect(2 + 2).to.eql(5);
+        expect(2 + 2).to.eqls(5);
+        expect(2 + 2).to.not.eqls(5);
+        expect(2 + 2).not.to.eqls(5);
+        expect(2 + 2).to.deep.eql(5);
+    `,
+    `
+        // Strict equality (===)
+        expect(2 + 2).toBe(5);
+        expect(2 + 2).toBe(5);
+        expect(2 + 2).toBe(5);
+        expect(2 + 2).not.toBe(5);
+        expect(2 + 2).not.toBe(5);
+
+        // Strict equality (===) + deep = deep equality
+        expect(2 + 2).toEqual(5);
+
+        // Deep equality
+        expect(2 + 2).toEqual(5);
+        expect(2 + 2).toEqual(5);
+        expect(2 + 2).not.toEqual(5);
+        expect(2 + 2).not.toEqual(5);
+        expect(2 + 2).toEqual(5);
+    `
+  )
+})
+
 test('converts "within"', () => {
   expectTransformation(
     `
@@ -1219,9 +1277,10 @@ it('leaves code without should/expect', () => {
   expect(result).toBeNull()
 })
 
-it('converts before, after and context to beforeAll, afterAll and describe', () => {
-  expectTransformation(
-    `
+describe('converts before, after and context to beforeAll, afterAll and describe', () => {
+  it('converts cases correctly', () => {
+    expectTransformation(
+      `
         before(() => {
           doSetup();
         });
@@ -1238,7 +1297,7 @@ it('converts before, after and context to beforeAll, afterAll and describe', ()
           });
         });
     `,
-    `
+      `
         beforeAll(() => {
           doSetup();
         });
@@ -1255,7 +1314,25 @@ it('converts before, after and context to beforeAll, afterAll and describe', ()
           });
         });
     `
-  )
+    )
+  })
+
+  it('skips call expression spreads with the same identifier', () => {
+    expectTransformation(
+      `
+      const foo = {
+        a: 'aa',
+        ...context(searchParams),
+      }
+    `,
+      `
+      const foo = {
+        a: 'aa',
+        ...context(searchParams),
+      }
+    `
+    )
+  })
 })
 
 test('supports chai-arrays plugin', () => {
diff --git a/src/transformers/sinon.test.ts b/src/transformers/sinon.test.ts
index b987915..ba64f05 100644
--- a/src/transformers/sinon.test.ts
+++ b/src/transformers/sinon.test.ts
@@ -7,12 +7,24 @@ import plugin from './sinon'
 chalk.level = 0
 
 const wrappedPlugin = wrapPlugin(plugin)
+
 beforeEach(() => {
   jest.spyOn(console, 'warn').mockImplementation().mockClear()
 })
 
-function expectTransformation(source, expectedOutput, warnings = []) {
-  const result = wrappedPlugin(source)
+interface TransformationOptions {
+  warnings?: string[]
+  parser?: string
+}
+
+function expectTransformation(
+  source,
+  expectedOutput,
+  options: TransformationOptions = {}
+) {
+  const { warnings = [], parser } = options
+
+  const result = wrappedPlugin(source, { parser })
   expect(result).toBe(expectedOutput)
   expect(console.warn).toBeCalledTimes(warnings.length)
   warnings.forEach((warning, i) => {
@@ -63,11 +75,13 @@ describe('spies and stubs', () => {
         jest.fn(() => 'foo');
         jest.spyOn(a, b).mockClear().mockImplementation(() => c) // too many args
 `,
-      [
-        'jest-codemods warning: (test.js line 6) stubbing all methods in an object is not supported; stub each one you care about individually',
-        'jest-codemods warning: (test.js line 9) 4+ arguments found in sinon.stub call; did you mean to use this many?',
-        'jest-codemods warning: (test.js line 14) 4+ arguments found in sinon.spy call; did you mean to use this many?',
-      ]
+      {
+        warnings: [
+          'jest-codemods warning: (test.js line 6) stubbing all methods in an object is not supported; stub each one you care about individually',
+          'jest-codemods warning: (test.js line 9) 4+ arguments found in sinon.stub call; did you mean to use this many?',
+          'jest-codemods warning: (test.js line 14) 4+ arguments found in sinon.spy call; did you mean to use this many?',
+        ],
+      }
     )
   })
 
@@ -180,6 +194,23 @@ describe('spies and stubs', () => {
     )
   })
 
+  it('handles .withArgs for typescript files', () => {
+    expectTransformation(
+      `
+      import sinon from 'sinon-sandbox'
+
+      sinon.stub().withArgs('foo').returns('something')
+    `,
+      `
+      jest.fn().mockImplementation((...args: any[]) => {
+            if (args[0] === 'foo')
+                  return 'something';
+      })
+    `,
+      { parser: 'tsx' }
+    )
+  })
+
   /* 
     apiStub.getCall(0).args[1].data
     apistub.args[1][1]
@@ -406,6 +437,19 @@ describe('spy count and call assertions', () => {
     )
   })
 
+  it(`handles .toHaveProperty('callCount', n)`, () => {
+    expectTransformation(
+      `
+      import sinon from 'sinon-sandbox'
+
+      expect(stub).toHaveProperty('callCount', 1);
+    `,
+      `
+      expect(stub).toHaveBeenCalledTimes(1);
+    `
+    )
+  })
+
   it('handles call counts with args', () => {
     expectTransformation(
       `