Repository: skovhus/jest-codemods
Pull request #244 (2022-04-21 20:33:38)

Add sinon + enzyme chai matchers
Adds additional chai matchers to support common sinon + enzyme cases:
- chai-enzyme:
    - `.to.be.present()`
    - `.to.contain.keys()`
    - `.to.have.type()`
    - `.to.have.state()`
    - `.to.have.descendants()`
- chai-sinon:
    - `.to.be.called` + `calledOnce`, `calledTwice`, etc
    - `.to.have.called()`
    - `.to.have.been.calledWithMatch()`
    - `.to.be.calledWithExactly()`
- `.to.have.prop()` + `.to.have.prop()s`

Also includes:
- handles `.not.to` -> `.to.not` cases
- refines `equals` cases


cc @lencioni @danbeam


Patch:
diff --git a/src/transformers/chai-should.ts b/src/transformers/chai-should.ts
index 9d29c8a..5b46c87 100644
--- a/src/transformers/chai-should.ts
+++ b/src/transformers/chai-should.ts
@@ -4,6 +4,7 @@ import {
   createCallUtil,
   getExpectNodeUtil,
   getNodeBeforeMemberExpressionUtil,
+  isExpectCallUtil,
   updateExpectUtil,
 } from '../utils/chai-chain-utils'
 import finale from '../utils/finale'
@@ -23,70 +24,84 @@ function addLeadingComment(node, comment) {
 }
 
 const fns = [
-  'keys',
+  'Uint16Array',
+  'Uint32Array',
+  'Uint8Array',
+  'Uint8ClampedArray',
   'a',
+  'above',
   'an',
-  'instanceof',
-  'lengthof',
-  'length',
-  'equal',
-  'equals',
-  'throw',
-  'include',
-  'includes',
+  'array',
+  'below',
+  'callCount', // sinon-chai
+  'calledWith', // sinon-chai
+  'calledWithMatch', // sinon-chai
+  'calledWithExactly', // sinon-chai
+  'descendants', // chai-enzyme
   'contain',
+  'containing',
+  'containingAllOf',
   'contains',
-  'eql',
   'eq',
-  'above',
-  'gt',
+  'eql',
+  'eqls',
+  'equal',
+  'equalTo',
+  'equals',
+  'exactly', // sinon-chai
   'greaterthan',
+  'gt',
+  'gte',
+  'include',
+  'includes',
+  'instanceof',
+  'key',
+  'keys',
   'least',
-  'below',
+  'length',
+  'lengthof',
   'lessthan',
   'lt',
-  'most',
+  'lte',
   'match',
-  'string',
   'members',
-  'property',
+  'most',
+  'ofSize',
   'ownproperty',
   'ownpropertydescriptor',
-  'gte',
-  'lte',
+  'present', // chai-enzyme
+  'prop', // chai-enzyme
+  'property',
+  'props', // chai-enzyme
+  'state', // chai-enzyme
+  'string',
+  'throw',
+  'type', // chai-enzyme
   'within',
-
   // https://www.chaijs.com/plugins/chai-arrays/ plugin:
-  'array',
-  'Uint8Array',
-  'Uint16Array',
-  'Uint32Array',
-  'Uint8ClampedArray',
-
-  'ofSize',
-  'equalTo',
-  'containing',
-  'containingAllOf',
   // TODO: containingAnyOf
-  // TODO: sorted
 ].map((name) => name.toLowerCase())
 
 const members = [
-  'ok',
-  'true',
-  'false',
+  'called', // sinon-chai
+  'calledOnce', // sinon-chai
+  'calledThrice', // sinon-chai
+  'calledTwice', // sinon-chai
+  'defined',
+  'empty',
+  'exist',
   'extensible',
+  'false',
   'finite',
-  'function',
   'frozen',
-  'sealed',
+  'function',
+  'nan',
   'null',
+  'ok',
+  'sealed',
+  'true',
   'undefined',
-  'exist',
-  'empty',
-  'nan',
-  'defined',
-]
+].map((name) => name.toLowerCase())
 
 const unsupportedProperties = new Set([
   'arguments',
@@ -180,10 +195,7 @@ export default function transformer(fileInfo, api, options) {
     mutations += 1
   }
 
-  const isExpectCall = (node) =>
-    node.name === 'expect' ||
-    (node.type === j.MemberExpression.name && isExpectCall(node.object)) ||
-    (node.type === j.CallExpression.name && isExpectCall(node.callee))
+  const isExpectCall = (node) => isExpectCallUtil(j, node)
 
   const typeOf = (path, value, args, containsNot) => {
     switch (args[0].value) {
@@ -191,13 +203,15 @@ export default function transformer(fileInfo, api, options) {
         return createCall(
           'toBeNull',
           [],
-          updateExpect(value, (node) => node)
+          updateExpect(value, (node) => node),
+          containsNot
         )
       case 'undefined':
         return createCall(
           'toBeUndefined',
           [],
-          updateExpect(value, (node) => node)
+          updateExpect(value, (node) => node),
+          containsNot
         )
       case 'array': {
         const parentExpressionStatement = findParentOfType(path, 'ExpressionStatement')
@@ -388,7 +402,7 @@ export default function transformer(fileInfo, api, options) {
           const { value } = p
           const propertyName = value.property.name.toLowerCase()
 
-          // Reject "ok" when it isn't  proceeded by "to"
+          // Reject "ok" when it isn't proceeded by "to"
           return !(propertyName === 'ok' && !chainContains('to', value, 'to'))
         })
 
@@ -488,12 +502,24 @@ export default function transformer(fileInfo, api, options) {
 
           case 'exist':
           case 'defined': {
+            if (propertyName === 'defined' && containsNot) {
+              return createCall('toBeDefined', [], rest, true)
+            }
+
             return containsNot
               ? createCall('toBeFalsy', [], rest)
               : createCall('toBeDefined', [], rest)
           }
           case 'function':
             return typeOf(p, value, [j.literal('function')], containsNot)
+          case 'called':
+            return createCall('toBeCalled', [], rest, containsNot)
+          case 'calledonce':
+            return createCall('toBeCalledTimes', [j.literal(1)], rest, containsNot)
+          case 'calledtwice':
+            return createCall('toBeCalledTimes', [j.literal(2)], rest, containsNot)
+          case 'calledthrice':
+            return createCall('toBeCalledTimes', [j.literal(3)], rest, containsNot)
           default:
             return value
         }
@@ -501,6 +527,27 @@ export default function transformer(fileInfo, api, options) {
       .size()
   }
 
+  /* 
+    reverses `expect().not.to` -> `expect().to.not` to be
+    handled correctly by subsequent expression updates
+  */
+  const reverseNotToExpressions = () => {
+    root
+      .find(j.MemberExpression, {
+        object: {
+          object: {
+            callee: { name: 'expect' },
+          },
+          property: { name: 'not' },
+        },
+        property: { name: 'to' },
+      })
+      .forEach((np) => {
+        np.node.property.name = 'not'
+        np.node.object.property.name = 'to'
+      })
+  }
+
   const updateCallExpressions = () =>
     root
       .find(j.CallExpression, {
@@ -518,7 +565,6 @@ export default function transformer(fileInfo, api, options) {
         const restRaw = getAllBefore(isPrefix, value.callee, 'should')
         const rest = getRestWithLengthHandled(p, restRaw)
         const containsNot = chainContains('not', value.callee, isPrefix)
-        const containsDeep = chainContains('deep', value.callee, isPrefix)
         const containsAny = chainContains('any', value.callee, isPrefix)
         const args = value.arguments
         const numberOfArgs = args.length
@@ -527,13 +573,69 @@ export default function transformer(fileInfo, api, options) {
         const propertyName = value.callee.property.name.toLowerCase()
 
         switch (propertyName) {
+          case 'type':
+          case 'descendants': {
+            /* 
+              if `.not`: expect(wrapper.find(Foo)).toHaveLength(0)
+              else if `.exactly`: expect(wrapper.find(Foo)).toHaveLength(exactly.arg[0])
+              else: expect(wrapper.find(Foo).length).toBeGreaterThan(0)
+            */
+
+            let lengthArg
+            if (containsNot) {
+              // for .not, it should always be 0
+              lengthArg = 0
+            } else {
+              // if its an `.exactly`, use as length
+              let path = value.callee
+              // find next call expression
+              while (!path.callee) {
+                path = path.object
+              }
+              if (path.callee?.property?.name === 'exactly') {
+                lengthArg = path.arguments?.[0]?.value
+              }
+            }
+
+            const wrapNodeWithFind = (node) =>
+              j.callExpression(j.memberExpression(node, j.identifier('find')), args)
+
+            if (lengthArg != null) {
+              return createCall(
+                'toHaveLength',
+                [j.literal(lengthArg)],
+                updateExpect(value, wrapNodeWithFind)
+              )
+            }
+
+            return createCall(
+              'toBeGreaterThan',
+              [j.literal(0)],
+              updateExpect(value, (node) =>
+                j.memberExpression(wrapNodeWithFind(node), j.identifier('length'))
+              )
+            )
+          }
+          case 'callcount':
+            return createCall('toBeCalledTimes', args, rest, containsNot)
+          case 'calledwith':
+            return createCall('toBeCalledWith', args, rest, containsNot)
+          case 'calledwithmatch':
+            return createCall('toBeCalledWith', args.map(containing), rest, containsNot)
+          case 'calledwithexactly':
+            return createCall('toBeCalledWith', args, rest, containsNot)
+          case 'exactly':
+            // handle `expect(sinonSpy).to.have.called.exactly(3)`
+            if (chainContains('called', value.callee, isPrefix)) {
+              return createCall('toBeCalledTimes', [firstArg], rest, containsNot)
+            }
+            return value
+          case 'equalto':
+          case 'eqls':
+          case 'eql':
           case 'eq':
           case 'equal':
           case 'equals':
-            if (containsDeep) {
-              return createCall('toEqual', args, rest, containsNot)
-            }
-
             if (numberOfArgs === 1) {
               const { type } = firstArg
 
@@ -550,26 +652,58 @@ export default function transformer(fileInfo, api, options) {
               }
             }
 
-            return createCall('toBe', args, rest, containsNot)
+            return createCall('toEqual', args, rest, containsNot)
           case 'throw':
             return createCall('toThrowError', args, rest, containsNot)
           case 'string':
             return createCall('toContain', args, rest, containsNot)
+          case 'state':
+            return createCall(
+              'toHaveProperty',
+              args,
+              updateExpect(value, (node) =>
+                j.callExpression(j.memberExpression(node, j.identifier('state')), [])
+              ),
+              containsNot
+            )
           case 'include':
           case 'includes':
           case 'contain':
           case 'contains': {
-            if (args.length === 1 && args[0].type === j.ObjectExpression.name) {
+            const argType = args[0]?.type
+            if (args.length === 1 && argType === j.ObjectExpression.name) {
               return createCall('toMatchObject', args, rest, containsNot)
             }
 
             const expectNode = getExpectNode(value)
             if (expectNode != null) {
-              const isExpectParamStringLiteral =
-                expectNode.arguments[0].type === j.Literal.name
-              if (isExpectParamStringLiteral) {
+              // handle `expect([1, 2]).toContain(1)
+              if (expectNode.arguments[0].type === j.Literal.name) {
                 return createCall('toContain', args, rest, containsNot)
               }
+              // handle `expect(someArr).toContain(1)`
+              if (args.length === 1 && argType === j.Literal.name) {
+                return createCall(
+                  'toContain',
+                  args,
+                  updateExpect(value, (node) => node),
+                  containsNot
+                )
+              }
+            }
+
+            // 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(
@@ -602,14 +736,6 @@ export default function transformer(fileInfo, api, options) {
               updateExpect(value, (node) => node),
               false
             )
-          case 'eql':
-            if (numberOfArgs === 1 && args[0].type === 'Literal') {
-              return createCall('toBe', args, rest, containsNot)
-            } else {
-              return createCall('toEqual', args, rest, containsNot)
-            }
-          case 'equalto':
-            return createCall('toEqual', args, rest, containsNot)
           case 'above':
           case 'greaterthan':
           case 'gt':
@@ -634,40 +760,102 @@ export default function transformer(fileInfo, api, options) {
             }
 
             return createCall('toEqual', args.map(containing), rest, containsNot)
-          case 'keys':
+          case 'key':
+            return createCall('toHaveProperty', args, rest, containsNot)
+          case 'keys': {
             if (containsAny) {
               logWarning('Unsupported Chai Assertion "any.keys"', p)
               return value
             }
 
+            const updateExpectFn = updateExpect(value, (node) => {
+              if (node.type === j.ArrayExpression.name) {
+                return node
+              }
+              return createCallChain(['Object', 'keys'], [node])
+            })
+
+            // handle single arguments, eg: `expect(serverConfig).to.have.all.keys('middleware')`
+            if (
+              args.length === 1 &&
+              [j.Literal.name, j.StringLiteral.name, j.Identifier.name].includes(
+                args[0].type
+              )
+            ) {
+              return createCall('toContain', args, updateExpectFn, containsNot)
+            }
+
             return createCall(
               'toEqual',
               [createCallChain(['expect', 'arrayContaining'], parseArgs(args))],
-              updateExpect(value, (node) => {
-                if (node.type === j.ObjectExpression.name) {
-                  return createCallChain(['Object', 'keys'], [node])
-                }
-                return node
-              }),
+              updateExpectFn,
               containsNot
             )
+          }
           case 'a':
-          case 'an':
+          case 'an': {
             if (!args.length) {
               return value
             }
 
-            if (args[0].type === 'Literal') {
+            const t = args[0].type
+            if (t === 'Literal' || t === 'StringLiteral') {
               return typeOf(p, value, args, containsNot)
             }
 
             return createCall('toBeInstanceOf', args, rest, containsNot)
+          }
           case 'instanceof':
             return createCall('toBeInstanceOf', args, rest, containsNot)
           case 'length':
           case 'lengthof':
           case 'ofsize':
             return createCall('toHaveLength', args, restRaw, containsNot)
+          case 'prop':
+            return createCall(
+              'toHaveProperty',
+              args,
+              updateExpect(value, (node) =>
+                j.callExpression(j.memberExpression(node, j.identifier('props')), [])
+              ),
+              containsNot
+            )
+          case 'props':
+            if (args[0].type === 'ArrayExpression') {
+              return createCall(
+                'toEqual',
+                [createCallChain(['expect', 'arrayContaining'], args)],
+                updateExpect(value, (node) =>
+                  createCallChain(
+                    ['Object', 'keys'],
+                    [
+                      j.callExpression(
+                        j.memberExpression(node, j.identifier('props')),
+                        []
+                      ),
+                    ]
+                  )
+                )
+              )
+            } else if (args[0].type === 'ObjectExpression') {
+              return createCall(
+                'toMatchObject',
+                args,
+                updateExpect(value, (node) =>
+                  j.callExpression(j.memberExpression(node, j.identifier('props')), [])
+                )
+              )
+            }
+            break
+          case 'present':
+            return createCall(
+              'toBeGreaterThan',
+              [j.literal(0)],
+              updateExpect(value, (node) =>
+                j.memberExpression(node, j.identifier('length'))
+              ),
+              containsNot
+            )
           case 'property':
             return createCall('toHaveProperty', args, rest, containsNot)
           case 'ownproperty':
@@ -717,6 +905,7 @@ export default function transformer(fileInfo, api, options) {
           case 'uint32array':
           case 'uint8clampedarray':
             return typeOf(p, value, [{ value: propertyName }], containsNot)
+
           default:
             return value
         }
@@ -740,6 +929,7 @@ export default function transformer(fileInfo, api, options) {
       })
       .size()
 
+  reverseNotToExpressions()
   mutations += shouldChainedToExpect()
   mutations += shouldIdentifierToExpect()
   mutations += updateCallExpressions()
diff --git a/src/utils/consts.ts b/src/utils/consts.ts
index f034a68..a970711 100644
--- a/src/utils/consts.ts
+++ b/src/utils/consts.ts
@@ -9,6 +9,10 @@ export const PROP_WITH_SECONDS_ARGS = [
 
 export const JEST_MATCHER_TO_MAX_ARGS = {
   toBe: 1,
+  toBeCalled: 0,
+  toBeCalledTimes: 1,
+  toBeCalledWith: Infinity,
+  toBeLastCalledWith: Infinity,
   toBeCloseTo: 2,
   toBeDefined: 0,
   toBeFalsy: 0,


Test patch:
diff --git a/src/transformers/chai-should.test.ts b/src/transformers/chai-should.test.ts
index 992cccc..4e329ec 100644
--- a/src/transformers/chai-should.test.ts
+++ b/src/transformers/chai-should.test.ts
@@ -19,6 +19,92 @@ function expectTransformation(source, expectedOutput) {
   expect(consoleWarnings).toEqual([])
 }
 
+test('chai-enzyme: handle .to.contain(JSXElement)', () => {
+  expectTransformation(
+    `
+    expect(wrapper).to.contain(<ImpressionLogger />)
+  `,
+    `
+    expect(wrapper.contains(<ImpressionLogger />)).toEqual(true)
+  `
+  )
+})
+
+test('chai-enzyme: handle .to.be.present', () => {
+  expectTransformation(
+    `
+        expect(wrapper).to.be.present()
+    `,
+    `
+        expect(wrapper.length).toBeGreaterThan(0)
+    `
+  )
+})
+
+test('chai-enzyme: handle .to.contain.keys', () => {
+  expectTransformation(
+    `
+        expect(wrapper.props()).to.contain.keys([
+          'verificationCodeInput',
+          'errorMessage',
+          'verifyEmail',
+          'sendVerificationCode',
+          'updateVerificationCodeInput',
+        ])
+    `,
+    `
+        expect(Object.keys(wrapper.props())).toEqual(expect.arrayContaining([
+          'verificationCodeInput',
+          'errorMessage',
+          'verifyEmail',
+          'sendVerificationCode',
+          'updateVerificationCodeInput',
+        ]))
+    `
+  )
+})
+
+test('chai-enzyme: handles .to.have.type', () => {
+  expectTransformation(
+    `
+        expect(wrapper).to.have.type(MockInnerComponent);
+    `,
+    `
+        expect(wrapper.find(MockInnerComponent).length).toBeGreaterThan(0);
+    `
+  )
+})
+
+test('chai-enzyme: handles .to.have.state', () => {
+  expectTransformation(
+    `
+        expect(wrapper).to.have.state('loading', false);
+        expect(wrapper).not.to.have.state('loading', false);
+    `,
+    `
+        expect(wrapper.state()).toHaveProperty('loading', false);
+        expect(wrapper.state()).not.toHaveProperty('loading', false);
+    `
+  )
+})
+
+test('chai-enzyme: handles descendants', () => {
+  expectTransformation(
+    `
+        expect(wrapper).to.have.exactly(2).descendants(foo);
+        expect(wrapper).to.have.descendants(foo);
+        expect(wrapper).to.have.descendants('div');
+        expect(wrapper).to.not.have.descendants(foo);
+    `,
+    `
+        expect(wrapper.find(foo)).toHaveLength(2);
+        expect(wrapper.find(foo).length).toBeGreaterThan(0);
+        expect(wrapper.find('div').length).toBeGreaterThan(0);
+        expect(wrapper.find(foo)).toHaveLength(0);
+    `
+  )
+})
+
 test('removes imports and does basic conversions of should and expect', () => {
   expectTransformation(
     `
@@ -34,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).toBe(PLACEHOLDER);
-                expect(textField.props.placeholder).not.toBe(PLACEHOLDER);
+                expect(textField.props.placeholder).toEqual(PLACEHOLDER);
+                expect(textField.props.placeholder).not.toEqual(PLACEHOLDER);
             });
         });
     `
@@ -70,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).toBe(PLACEHOLDER);
-                expect(textField.props.placeholder).not.toBe(PLACEHOLDER);
+                expect(textField.props.placeholder).toEqual(PLACEHOLDER);
+                expect(textField.props.placeholder).not.toEqual(PLACEHOLDER);
             });
 
             it('should inherit id prop', () => {
-                expect(dropdown.props.id).toBe(STANDARD_PROPS.id);
-                expect(dropdown.props.id).not.toBe(STANDARD_PROPS.id);
+                expect(dropdown.props.id).toEqual(STANDARD_PROPS.id);
+                expect(dropdown.props.id).not.toEqual(STANDARD_PROPS.id);
             });
 
             it('should map open prop to visible prop', () => {
@@ -104,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).toBe(PLACEHOLDER);
-              expect(textField.props.placeholder).not.toBe(PLACEHOLDER);
+              expect(textField.props.placeholder).toEqual(PLACEHOLDER);
+              expect(textField.props.placeholder).not.toEqual(PLACEHOLDER);
           });
         });`
   )
@@ -133,7 +219,9 @@ test('converts "a-an"', () => {
         expect({ foo: 'bar' }).to.be.an(Object);
         expect('xyz').to.be.a(String);
         expect(null).to.be.a('null');
+        expect(null).to.not.be.a('null');
         expect(undefined).to.be.an('undefined');
+        expect(undefined).not.to.be.an('undefined');
         expect(new Error()).to.be.an('error');
         expect(new Promise()).to.be.a('promise');
         expect(new Float32Array()).to.be.a('float32array');
@@ -145,6 +233,7 @@ test('converts "a-an"', () => {
         expect(baz).to.not.be.an('array');
 
         'test'.should.be.a('string');
+        expect(foo).to.be.a('function');
     `,
     `
         expect(typeof 'test').toBe('string');
@@ -152,7 +241,9 @@ test('converts "a-an"', () => {
         expect({ foo: 'bar' }).toBeInstanceOf(Object);
         expect('xyz').toBeInstanceOf(String);
         expect(null).toBeNull();
+        expect(null).not.toBeNull();
         expect(undefined).toBeUndefined();
+        expect(undefined).not.toBeUndefined();
         expect(new Error()).toBeInstanceOf(Error);
         expect(new Promise()).toBeInstanceOf(Promise);
         expect(new Float32Array()).toBeInstanceOf(Float32Array);
@@ -164,6 +255,7 @@ test('converts "a-an"', () => {
         expect(Array.isArray(baz)).toBe(false);
 
         expect(typeof 'test').toBe('string');
+        expect(foo).toBeInstanceOf(Function);
     `
   )
 })
@@ -206,6 +298,135 @@ test('converts "below"', () => {
   )
 })
 
+test('converts "called"', () => {
+  expectTransformation(
+    `
+        expect(sinonSpy).to.be.called;
+        expect(sinonSpy).not.to.be.called;
+        expect(sinonSpy).to.not.be.called;
+        expect(sinonSpy).to.be.not.called;
+    `,
+    `
+        expect(sinonSpy).toBeCalled();
+        expect(sinonSpy).not.toBeCalled();
+        expect(sinonSpy).not.toBeCalled();
+        expect(sinonSpy).not.toBeCalled();
+    `
+  )
+})
+
+test('converts "called.exactly(n)"', () => {
+  expectTransformation(
+    `
+        expect(sinonSpy).to.have.called.exactly(3)
+        expect(sinonSpy).to.not.have.called.exactly(3)
+        expect(sinonSpy).to.have.been.called.exactly(3)
+        expect(sinonSpy).to.not.have.been.called.exactly(3)
+    `,
+    `
+        expect(sinonSpy).toBeCalledTimes(3)
+        expect(sinonSpy).not.toBeCalledTimes(3)
+        expect(sinonSpy).toBeCalledTimes(3)
+        expect(sinonSpy).not.toBeCalledTimes(3)
+    `
+  )
+})
+
+test('converts "callCount"', () => {
+  expectTransformation(
+    `
+        expect(sinonSpy).to.have.callCount(1);
+        expect(sinonSpy).not.to.have.callCount(2);
+    `,
+    `
+        expect(sinonSpy).toBeCalledTimes(1);
+        expect(sinonSpy).not.toBeCalledTimes(2);
+    `
+  )
+})
+
+test('converts "calledOnce"', () => {
+  expectTransformation(
+    `
+        expect(sinonSpy).to.be.calledOnce;
+        expect(sinonSpy).not.to.be.calledOnce;
+    `,
+    `
+        expect(sinonSpy).toBeCalledTimes(1);
+        expect(sinonSpy).not.toBeCalledTimes(1);
+    `
+  )
+})
+
+test('converts "calledTwice"', () => {
+  expectTransformation(
+    `
+        expect(sinonSpy).to.be.calledTwice;
+        expect(sinonSpy).not.to.be.calledTwice;
+    `,
+    `
+        expect(sinonSpy).toBeCalledTimes(2);
+        expect(sinonSpy).not.toBeCalledTimes(2);
+    `
+  )
+})
+
+test('converts "calledThrice"', () => {
+  expectTransformation(
+    `
+        expect(sinonSpy).to.be.calledThrice;
+        expect(sinonSpy).not.to.be.calledThrice;
+    `,
+    `
+        expect(sinonSpy).toBeCalledTimes(3);
+        expect(sinonSpy).not.toBeCalledTimes(3);
+    `
+  )
+})
+
+test('converts "calledWith"', () => {
+  expectTransformation(
+    `
+        expect(sinonSpy).to.be.calledWith(1, 2, 3);
+        expect(sinonSpy).not.to.be.calledWith('a', 'b');
+    `,
+    `
+        expect(sinonSpy).toBeCalledWith(1, 2, 3);
+        expect(sinonSpy).not.toBeCalledWith('a', 'b');
+    `
+  )
+})
+
+it('converts "calledWithMatch"', () => {
+  expectTransformation(
+    `
+        expect(stub).to.have.been.calledWithMatch({
+          foo: 'foo',
+          bar: 1
+        });
+    `,
+    `
+        expect(stub).toBeCalledWith(expect.objectContaining({
+          foo: 'foo',
+          bar: 1
+        }));
+    `
+  )
+})
+
+test('converts "calledWithExactly"', () => {
+  expectTransformation(
+    `
+        expect(sinonSpy).to.be.calledWithExactly(1, 2, 3);
+        expect(sinonSpy).not.to.be.calledWithExactly('a', 'b');
+    `,
+    `
+        expect(sinonSpy).toBeCalledWith(1, 2, 3);
+        expect(sinonSpy).not.toBeCalledWith('a', 'b');
+    `
+  )
+})
+
 test('converts "eql"', () => {
   expectTransformation(
     `
@@ -221,8 +442,8 @@ test('converts "eql"', () => {
         expect([1, 2, 3]).toEqual([1, 2, 3]);
         expect(a).toEqual(a);
 
-        expect("123").toBe("123");
-        expect("123").not.toBe("123");
+        expect("123").toEqual("123");
+        expect("123").not.toEqual("123");
     `
   )
 })
@@ -241,16 +462,16 @@ test('converts "equal"', () => {
         should.not.equal('foo', 'bar');
     `,
     `
-        expect('hello').toBe('hello');
+        expect('hello').toEqual('hello');
         // some message here explaining hello
-        expect('hello').toBe('hello');
-        expect(42).toBe(42);
-        expect(1).not.toBe(true);
-        expect({ foo: 'bar' }).not.toBe({ foo: 'bar' });
+        expect('hello').toEqual('hello');
+        expect(42).toEqual(42);
+        expect(1).not.toEqual(true);
+        expect({ foo: 'bar' }).not.toEqual({ foo: 'bar' });
         expect({ foo: 'bar' }).toEqual({ foo: 'bar' });
 
-        expect('foo').toBe('foo');
-        expect('foo').not.toBe('bar');
+        expect('foo').toEqual('foo');
+        expect('foo').not.toEqual('bar');
     `
   )
 })
@@ -277,7 +498,7 @@ test('converts "exist-defined"', () => {
 
         expect(foo).toBeDefined();
         expect(foo).not.toBeDefined();
-        expect(foo).toBeFalsy();
+        expect(foo).not.toBeDefined();
 
         expect('').toBeDefined();
     `
@@ -387,12 +608,14 @@ test('converts "includes-contains"', () => {
         expect([1, 2, 3]).to.include(2);
         expect('foobar').which.contains('foo');
         expect({ foo: 1, bar: 2 }).to.contain({ bar: 2 });
+        expect(a).to.contain(b);
     `,
     `
         expect('foobar').toContain('foo');
-        expect([1, 2, 3]).toEqual(expect.arrayContaining([2]));
+        expect([1, 2, 3]).toContain(2);
         expect('foobar').toContain('foo');
         expect({ foo: 1, bar: 2 }).toMatchObject({ bar: 2 });
+        expect(a).toEqual(expect.arrayContaining([b]));
     `
   )
 })
@@ -404,8 +627,8 @@ test('converts chained "includes-contains"', () => {
         expect(arr).to.be.an('array').that.does.not.include(3);
     `,
     `
-        expect([1, 2, 3]).toEqual(expect.arrayContaining([2]));
-        expect(arr).toEqual(expect.not.arrayContaining([3]));
+        expect([1, 2, 3]).toContain(2);
+        expect(arr).not.toContain(3);
     `
   )
 })
@@ -443,12 +666,35 @@ test('converts "keys"', () => {
         expect({ foo: 1, bar: 2 }).to.have.all.keys({ bar: 6, foo: 7 });
         expect({ foo: 1, bar: 2, baz: 3 }).to.contain.all.keys(['bar', 'foo']);
         expect({ foo: 1, bar: 2, baz: 3 }).to.contain.all.keys({ bar: 6 });
+        expect(serverConfig).to.have.all.keys('middleware');
+        expect(actualDispatch).to.include.all.keys('type', 'payload', 'meta');
+        expect(serverConfig).to.include.all.keys('middleware');
+        expect(serverConfig).to.include.all.keys(foo);
     `,
     `
         expect([1, 2, 3]).toEqual(expect.arrayContaining([1, 2]));
         expect(Object.keys({ foo: 1, bar: 2 })).toEqual(expect.arrayContaining(Object.keys({ bar: 6, foo: 7 })));
         expect(Object.keys({ foo: 1, bar: 2, baz: 3 })).toEqual(expect.arrayContaining(['bar', 'foo']));
         expect(Object.keys({ foo: 1, bar: 2, baz: 3 })).toEqual(expect.arrayContaining(Object.keys({ bar: 6 })));
+        expect(Object.keys(serverConfig)).toContain('middleware');
+        expect(Object.keys(actualDispatch)).toEqual(expect.arrayContaining(['type', 'payload', 'meta']));
+        expect(Object.keys(serverConfig)).toContain('middleware');
+        expect(Object.keys(serverConfig)).toContain(foo);
+    `
+  )
+})
+
+test('converts "key"', () => {
+  expectTransformation(
+    `
+        expect(obj).to.have.key('keyName');
+        expect(obj).to.not.have.key('keyName');
+        expect(obj).not.to.have.key('keyName');
+    `,
+    `
+        expect(obj).toHaveProperty('keyName');
+        expect(obj).not.toHaveProperty('keyName');
+        expect(obj).not.toHaveProperty('keyName');
     `
   )
 })
@@ -552,10 +798,14 @@ test('converts "null"', () => {
     `
         expect(null).to.be.null;
         expect(undefined).to.not.be.null;
+        expect(undefined).not.to.be.null;
+        expect(undefined).to.be.not.null;
     `,
     `
         expect(null).toBeNull();
         expect(undefined).not.toBeNull();
+        expect(undefined).not.toBeNull();
+        expect(undefined).not.toBeNull();
     `
   )
 })
@@ -613,6 +863,38 @@ test('converts "ownpropertydescriptor"', () => {
   )
 })
 
+test('converts "prop"', () => {
+  expectTransformation(
+    `
+        expect(enzymeWrapper).to.have.prop('a');
+        expect(enzymeWrapper).to.have.prop('a', 'b');
+        expect(enzymeWrapper).to.have.prop('a', 2);
+        expect(enzymeWrapper).to.have.prop('a', [1, 2]);
+        expect(enzymeWrapper).to.not.have.prop('a');
+    `,
+    `
+        expect(enzymeWrapper.props()).toHaveProperty('a');
+        expect(enzymeWrapper.props()).toHaveProperty('a', 'b');
+        expect(enzymeWrapper.props()).toHaveProperty('a', 2);
+        expect(enzymeWrapper.props()).toHaveProperty('a', [1, 2]);
+        expect(enzymeWrapper.props()).not.toHaveProperty('a');
+    `
+  )
+})
+
+test('converts "props"', () => {
+  expectTransformation(
+    `
+        expect(enzymeWrapper).to.have.props(['a', 'b']);
+        expect(enzymeWrapper).to.have.props({ a: 1, b: 2 });
+    `,
+    `
+        expect(Object.keys(enzymeWrapper.props())).toEqual(expect.arrayContaining(['a', 'b']));
+        expect(enzymeWrapper.props()).toMatchObject({ a: 1, b: 2 });
+    `
+  )
+})
+
 test('converts "property"', () => {
   expectTransformation(
     `
@@ -716,7 +998,7 @@ it('warns about using chai extensions', () => {
         const chai = require('chai');
         const sinonChai = require('sinon-chai');
         chai.use(sinonChai);
-        `)
+`)
 
   expect(consoleWarnings).toEqual([
     'jest-codemods warning: (test.js line 4) Unsupported Chai Extension "chai.use()"',
@@ -748,11 +1030,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).toBe(true);
+        expect(foo).toEqual(true);
         // Expected foo to be defined for \${id}
         expect(foo).toBeDefined();
         // 'Expected ' + foo + ' to be defined'
-        expect(foo).toBe(true);
+        expect(foo).toEqual(true);
     `
   )
 })
@@ -761,9 +1043,77 @@ test('converts equal(null) to toBeNull()', () => {
   expectTransformation(
     `
         expect(actual).to.equal(null);
+        expect(actual).not.to.equal(null);
+        expect(actual).to.not.equal(null);
+    `,
+    `
+        expect(actual).toBeNull();
+        expect(actual).not.toBeNull();
+        expect(actual).not.toBeNull();
+    `
+  )
+})
+
+test('converts eq(null) to toBeNull()', () => {
+  expectTransformation(
+    `
+        expect(actual).to.eq(null);
+        expect(actual).not.to.eq(null);
+        expect(actual).to.not.eq(null);
+    `,
+    `
+        expect(actual).toBeNull();
+        expect(actual).not.toBeNull();
+        expect(actual).not.toBeNull();
+    `
+  )
+})
+
+test('converts eql(null) to toBeNull()', () => {
+  expectTransformation(
+    `
+        expect(actual).to.eql(null);
+        expect(actual).not.to.eql(null);
+        expect(actual).to.not.eql(null);
+    `,
+    `
+        expect(actual).toBeNull();
+        expect(actual).not.toBeNull();
+        expect(actual).not.toBeNull();
+    `
+  )
+})
+
+test('converts eqls(null) to toBeNull()', () => {
+  expectTransformation(
+    `
+        expect(actual).to.be.eqls(null);
+        expect(actual).not.to.eqls(null);
+        expect(actual).to.be.not.eqls(null);
+        expect(actual).to.not.be.eqls(null);
+    `,
+    `
+        expect(actual).toBeNull();
+        expect(actual).not.toBeNull();
+        expect(actual).not.toBeNull();
+        expect(actual).not.toBeNull();
+    `
+  )
+})
+
+test('converts equalto(null) to toBeNull()', () => {
+  expectTransformation(
+    `
+        expect(actual).to.be.equalTo(null);
+        expect(actual).not.to.be.equalTo(null);
+        expect(actual).to.not.be.equalTo(null);
+        expect(actual).to.be.not.equalTo(null);
     `,
     `
         expect(actual).toBeNull();
+        expect(actual).not.toBeNull();
+        expect(actual).not.toBeNull();
+        expect(actual).not.toBeNull();
     `
   )
 })