Repository: skovhus/jest-codemods (ts)

Issue:
TypeScript Jasmine: Types should be migrated
```ts
let loggerSpy: jasmine.SpyObj<LoggerService>;
let errorServiceSpy: jasmine.Spy;
const loggerSpy = jasmine.createSpyObj<Logger>(['log']);
```

should be transformed to
```ts
let loggerSpy: jest.Mocked<LoggerService>;
let errorServiceSpy: jest.Mock;
const loggerSpy: jest.Mocked<Logger> = { log: jest.fn() };
```

Pull request description:
TypeScript: Jasmine types like jasmine.SpyObj and jasmine.Spy should be migrated
fixes #609 

Patch:
diff --git a/src/transformers/jasmine-globals.ts b/src/transformers/jasmine-globals.ts
index 97f5fbb..5196390 100644
--- a/src/transformers/jasmine-globals.ts
+++ b/src/transformers/jasmine-globals.ts
@@ -632,6 +632,7 @@ export default function jasmineGlobals(fileInfo, api, options) {
       )
     })
     .forEach((path) => {
+      const { typeParameters } = path.node
       const [, spyObjMethods, spyObjProperties] = path.node.arguments
 
       const properties: ObjectProperty[] =
@@ -672,8 +673,59 @@ export default function jasmineGlobals(fileInfo, api, options) {
         )
       }
 
+      if (typeParameters) {
+        path.parentPath.node.id.typeAnnotation = j.tsTypeAnnotation(
+          j.tsTypeReference(
+            j.tsQualifiedName(j.identifier('jest'), j.identifier('Mocked')),
+            typeParameters
+          )
+        )
+      }
+
       j(path).replaceWith(j.objectExpression(properties))
     })
 
+  root
+    .find(j.VariableDeclarator)
+    .filter((path) => {
+      const { typeAnnotation } = path.value.id
+      if (!typeAnnotation) return false
+
+      const { typeName } = typeAnnotation.typeAnnotation
+
+      return (
+        typeName &&
+        typeName.type === 'TSQualifiedName' &&
+        typeName.left.name === 'jasmine' &&
+        (typeName.right.name === 'Spy' || typeName.right.name === 'SpyObj')
+      )
+    })
+    .forEach((path) => {
+      const { typeAnnotation } = path.value.id
+      const { typeName } = typeAnnotation.typeAnnotation
+      const typeArgument = typeAnnotation.typeAnnotation.typeParameters?.params[0]
+
+      switch (typeName.right.name) {
+        case 'Spy': {
+          path.value.id.typeAnnotation = j.tsTypeAnnotation(
+            j.tsTypeReference(
+              j.tsQualifiedName(j.identifier('jest'), j.identifier('Mock')),
+              typeArgument ? j.tsTypeParameterInstantiation([typeArgument]) : null
+            )
+          )
+          break
+        }
+        case 'SpyObj': {
+          path.value.id.typeAnnotation = j.tsTypeAnnotation(
+            j.tsTypeReference(
+              j.tsQualifiedName(j.identifier('jest'), j.identifier('Mocked')),
+              j.tsTypeParameterInstantiation([typeArgument])
+            )
+          )
+          break
+        }
+      }
+    })
+
   return finale(fileInfo, j, root, options)
 }


Test patch:
diff --git a/src/transformers/jasmine-globals.test.ts b/src/transformers/jasmine-globals.test.ts
index d84e692..3f76eb4 100644
--- a/src/transformers/jasmine-globals.test.ts
+++ b/src/transformers/jasmine-globals.test.ts
@@ -94,6 +94,24 @@ test('jasmine.createSpy', () => {
   expect(consoleWarnings).toEqual([])
 })
 
+test('jasmine.createSpy with generic types', () => {
+  expectTransformation(
+    `
+    import type log from './log';
+    
+    jasmine.createSpy<typeof log>();
+    jasmine.createSpy<() => void>();
+    `,
+    `
+    import type log from './log';
+    
+    jest.fn<typeof log>();
+    jest.fn<() => void>();
+    `,
+    { parser: 'ts' }
+  )
+})
+
 test('not supported jasmine.createSpy().and.*', () => {
   wrappedPlugin(`
         jasmine.createSpy().and.unknownUtil();
@@ -382,6 +400,58 @@ describe('createSpyObj', () => {
     `
     )
   })
+
+  test('with types', () => {
+    expectTransformation(
+      `
+    import { LoggerService } from './logger-service.ts';
+
+    const loggerSpy = jasmine.createSpyObj<LoggerService>('LoggerService', ['log']);
+    `,
+      `
+    import { LoggerService } from './logger-service.ts';
+
+    const loggerSpy: jest.Mocked<LoggerService> = {
+        'log': jest.fn()
+    };
+    `,
+      { parser: 'ts' }
+    )
+  })
+})
+
+describe('types', () => {
+  test('jasmine.SpyObj', () =>
+    expectTransformation(
+      `
+      let loggerSpy: jasmine.SpyObj<LoggerService>;
+      let unknownSpy: jasmine.SpyObj<unknown>;
+      const errorHandlerSpy: jasmine.SpyObj<ErrorHandler> = jasmine.createSpyObj('ErrorHandler', ['handleError']);
+      `,
+      `
+      let loggerSpy: jest.Mocked<LoggerService>;
+      let unknownSpy: jest.Mocked<unknown>;
+      const errorHandlerSpy: jest.Mocked<ErrorHandler> = {
+            'handleError': jest.fn()
+      };
+      `,
+      { parser: 'ts' }
+    ))
+
+  test('jasmine.Spy', () =>
+    expectTransformation(
+      `
+        let setLanguageSpy: jasmine.Spy;
+        let logSpy: jasmine.Spy<(message: string) => void>;
+        const handleErrorSpy: jasmine.Spy<ErrorHandler['handleError']> = jasmine.createSpy();
+        `,
+      `
+        let setLanguageSpy: jest.Mock;
+        let logSpy: jest.Mock<(message: string) => void>;
+        const handleErrorSpy: jest.Mock<ErrorHandler['handleError']> = jest.fn();
+        `,
+      { parser: 'ts' }
+    ))
 })
 
 test('return value', () => {