Repository: skovhus/jest-codemods (ts)

Issue:
Not all cases of jasmine.createSpyObj transformed
`jasmine.createSpyObj()` has the following parameters:

| Name  | Type | Attributes | Description |
| ------------- | ------------- |------------- |----------- |
| baseName  | String  | optional | Base name for the spies in the object.
| methodNames | Array.<String\> \| Object  | - | Array of method names to create spies for, or Object whose keys will be method names and values the returnValue.
| propertyNames | Array.<String\> \| Object | optional | Array of property names to create spies for, or Object whose keys will be propertynames and values the returnValue.

Source: https://jasmine.github.io/api/5.2/jasmine.html

A leading optional parameter (`baseName`) followed by mandatory parameters makes no sense in my view, but this is what the documentation says.

## Current behavior
- Only the scenario `jasmine.createSpyObj(string, string[])` is supported

## Desired behaviour
- `jasmine.createSpyObj(string, Object)` is supported
- The optional third parameter `propertyNames` is also supported with both types

Pull request description:
fix(jasmine-globals): enhance createSpyObj handling up to three parameters with all type variations
Fixes #590 

Patch:
diff --git a/src/transformers/jasmine-globals.ts b/src/transformers/jasmine-globals.ts
index 528f29e..d3e4936 100644
--- a/src/transformers/jasmine-globals.ts
+++ b/src/transformers/jasmine-globals.ts
@@ -1,6 +1,8 @@
 /**
  * Codemod for transforming Jasmine globals into Jest.
  */
+import { ObjectProperty } from 'jscodeshift'
+
 import finale from '../utils/finale'
 import logger from '../utils/logger'
 
@@ -532,21 +534,57 @@ export default function jasmineGlobals(fileInfo, api, options) {
         },
       },
     })
-    .filter(
-      (path) =>
-        path.node.arguments.length === 2 &&
-        path.node.arguments[1].type === 'ArrayExpression'
-    )
+    .filter((path) => {
+      const args = path.node.arguments
+
+      return (
+        (args.length === 2 || args.length === 3) &&
+        (args[1].type === 'ArrayExpression' || args[1].type === 'ObjectExpression') &&
+        (args[2] === undefined ||
+          args[2].type === 'ArrayExpression' ||
+          args[2].type === 'ObjectExpression')
+      )
+    })
     .forEach((path) => {
-      const properties = path.node.arguments[1].elements.map((arg) =>
-        j.objectProperty(
-          j.literal(arg.value),
-          j.callExpression(
-            j.memberExpression(j.identifier('jest'), j.identifier('fn')),
-            []
-          )
+      const [, spyObjMethods, spyObjProperties] = path.node.arguments
+
+      const properties: ObjectProperty[] =
+        spyObjMethods.type === 'ArrayExpression'
+          ? spyObjMethods.elements.map((arg) =>
+              j.objectProperty(
+                j.literal(arg.value),
+                j.callExpression(
+                  j.memberExpression(j.identifier('jest'), j.identifier('fn')),
+                  []
+                )
+              )
+            )
+          : spyObjMethods.properties.map((arg) =>
+              j.objectProperty(
+                j.literal(arg.key.name),
+                j.callExpression(
+                  j.memberExpression(j.identifier('jest'), j.identifier('fn')),
+                  [
+                    j.arrowFunctionExpression(
+                      [],
+                      j.blockStatement([j.returnStatement(arg.value)])
+                    ),
+                  ]
+                )
+              )
+            )
+
+      if (spyObjProperties !== undefined) {
+        properties.push(
+          ...(spyObjProperties.type === 'ArrayExpression'
+            ? spyObjProperties.elements.map((arg) =>
+                j.objectProperty(j.literal(arg.value), j.literal(null))
+              )
+            : spyObjProperties.properties.map((arg) =>
+                j.objectProperty(j.literal(arg.key.name), arg.value)
+              ))
         )
-      )
+      }
 
       j(path).replaceWith(j.objectExpression(properties))
     })


Test patch:
diff --git a/src/transformers/jasmine-globals.test.ts b/src/transformers/jasmine-globals.test.ts
index e757b7b..ec91b35 100644
--- a/src/transformers/jasmine-globals.test.ts
+++ b/src/transformers/jasmine-globals.test.ts
@@ -1,4 +1,5 @@
 /* eslint-env jest */
+import { describe } from '@jest/globals'
 import chalk from 'chalk'
 
 import { wrapPlugin } from '../utils/test-helpers'
@@ -226,19 +227,87 @@ test('jasmine.<jasmineToExpectFunctionName>(*)', () => {
   )
 })
 
-test('createSpyObj', () => {
-  expectTransformation(
-    `
+describe('createSpyObj', () => {
+  test('with methodNames array and no properties', () => {
+    expectTransformation(
+      `
     const spyObj = jasmine.createSpyObj('label', ['a', 'b', 'hyphen-ated']);
     `,
-    `
+      `
     const spyObj = {
         'a': jest.fn(),
         'b': jest.fn(),
         'hyphen-ated': jest.fn()
     };
     `
-  )
+    )
+  })
+
+  test('with methodNames array and properties object', () => {
+    expectTransformation(
+      `
+    const spyObj = jasmine.createSpyObj('', ['a', 'b'], { c: 5 });
+    `,
+      `
+    const spyObj = {
+        'a': jest.fn(),
+        'b': jest.fn(),
+        'c': 5
+    };
+    `
+    )
+  })
+
+  test('with methods object and no properties', () => {
+    expectTransformation(
+      `
+    const spyObj = jasmine.createSpyObj('label', {
+        a: 42,
+        b: true,
+        c: of(undefined)
+    });
+    `,
+      `
+    const spyObj = {
+        'a': jest.fn(() => {
+            return 42;
+        }),
+
+        'b': jest.fn(() => {
+            return true;
+        }),
+
+        'c': jest.fn(() => {
+            return of(undefined);
+        })
+    };
+    `
+    )
+  })
+
+  test('with methods object and properties array', () => {
+    expectTransformation(
+      `
+    const spyObj = jasmine.createSpyObj('label', {
+        a: 42,
+        b: true
+    }, ['c']);
+    `,
+      `
+    const spyObj = {
+        'a': jest.fn(() => {
+            return 42;
+        }),
+
+        'b': jest.fn(() => {
+            return true;
+        }),
+
+        'c': null
+    };
+    `
+    )
+  })
 })
 
 test('return value', () => {