Repository: skovhus/jest-codemods
Pull request #118 (2018-05-29 20:17:52)

jasmine globals
we're migrating from using `jasmine` to using `jest-circus` and i'm writing some codemods that'll simplify the migration.
this is not going to be perfect, because there's a lot of state that's impossible to track through AST, but hopefully it can get us closer to the full migration

codecov-io: # [Codecov](https://codecov.io/gh/skovhus/jest-codemods/pull/118?src=pr&el=h1) Report
> Merging [#118](https://codecov.io/gh/skovhus/jest-codemods/pull/118?src=pr&el=desc) into [master](https://codecov.io/gh/skovhus/jest-codemods/commit/60d646f9c5904bb68034983bfcfacd9522e8da5e?src=pr&el=desc) will **increase** coverage by `0.1%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/skovhus/jest-codemods/pull/118/graphs/tree.svg?src=pr&token=rt2Tk3o31c&height=150&width=650)](https://codecov.io/gh/skovhus/jest-codemods/pull/118?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##           master     #118     +/-   ##
=========================================
+ Coverage   94.98%   95.09%   +0.1%     
=========================================
  Files          23       24      +1     
  Lines        1057     1080     +23     
=========================================
+ Hits         1004     1027     +23     
  Misses         53       53
```


| [Impacted Files](https://codecov.io/gh/skovhus/jest-codemods/pull/118?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/transformers/jasmine-globals.js](https://codecov.io/gh/skovhus/jest-codemods/pull/118/diff?src=pr&el=tree#diff-c3JjL3RyYW5zZm9ybWVycy9qYXNtaW5lLWdsb2JhbHMuanM=) | `100% <100%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/skovhus/jest-codemods/pull/118?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/skovhus/jest-codemods/pull/118?src=pr&el=footer). Last update [60d646f...2f0b0b1](https://codecov.io/gh/skovhus/jest-codemods/pull/118?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

SimenB: I don't know how you wanna go, but e.g. `expect(mock.calls.count()).toHaveLength(1)` or `expect(mock.calls.count().length).toBe(1)` can both be converted to `expect(mock).toHaveBeenCalledTimes(1)`. That last one is compatible with both Jest spies and jasmine spies, so then it doesn't matter if the spy was created by a `jasmine.createSpy` or `jest.fn`
skovhus: @aaronabramov let me know if you want to add the CLI support and documentation, else I'll do it. :)

Pointers to where we need to change stuff:
- https://github.com/skovhus/jest-codemods/blob/master/src/cli/index.js#L50
- we don't want this converter in https://github.com/skovhus/jest-codemods/blob/master/src/cli/index.js#L55 (as we don't have input detection)
- https://github.com/skovhus/jest-codemods/blob/master/src/cli/index.js#L87
aaronabramov: added to CLI, just two lines, right?
there's no mention of `jasmine` in `README.md` so i didn't touch it :)
aaronabramov: @SimenB i don't think this codemod will cover a lot of cases honestly. my goal is to just make it not crash on `jasmine is undefined` :P most of the use cases i found so far are way more complicated.. like
`stuff.calls.mostRecent().args()` which needs to be replaced with `stuff.mock.calls[stuff.mock.calls.length - 1][0]`
so yeah.. it's pretty complex :)
aaronabramov: added `jasmine.createSpy()` -> `jest.fn()` transform as well
aaronabramov: been migrating a lot of tests this week. added some more globals + better spyOn support
skovhus: Thanks! 🙌

Patch:
diff --git a/src/cli/index.js b/src/cli/index.js
index 787b2c5..3e3d9e7 100644
--- a/src/cli/index.js
+++ b/src/cli/index.js
@@ -47,6 +47,7 @@ const TRANSFORMER_CHAI_ASSERT = 'chai-assert';
 const TRANSFORMER_CHAI_SHOULD = 'chai-should';
 const TRANSFORMER_EXPECT_JS = 'expect-js';
 const TRANSFORMER_EXPECT_1 = 'expect';
+const TRANSFORMER_JASMINE_GLOBALS = 'jasmine-globals';
 const TRANSFORMER_JASMINE_THIS = 'jasmine-this';
 const TRANSFORMER_MOCHA = 'mocha';
 const TRANSFORMER_SHOULD = 'should';
@@ -84,6 +85,10 @@ const TRANSFORMER_INQUIRER_CHOICES = [
         name: 'Expect@1.x (by mjackson)',
         value: TRANSFORMER_EXPECT_1,
     },
+    {
+        name: 'Jasmine: globals',
+        value: TRANSFORMER_JASMINE_GLOBALS,
+    },
     {
         name: 'Jasmine: this usage',
         value: TRANSFORMER_JASMINE_THIS,
diff --git a/src/transformers/jasmine-globals.js b/src/transformers/jasmine-globals.js
new file mode 100644
index 0000000..5ba03dc
--- /dev/null
+++ b/src/transformers/jasmine-globals.js
@@ -0,0 +1,271 @@
+/**
+ * Codemod for transforming Jasmine `this` context into Jest v20+ compatible syntax.
+ */
+
+import finale from '../utils/finale';
+
+export default function jasmineGlobals(fileInfo, api, options) {
+    const j = api.jscodeshift;
+    const root = j(fileInfo.source);
+
+    const emptyArrowFn = j('() => {}').__paths[0].value.program.body[0].expression;
+
+    root
+        // find all `jasmine.createSpy('stuff')
+        .find(j.CallExpression, {
+            callee: {
+                type: 'MemberExpression',
+                property: {
+                    type: 'Identifier',
+                    name: 'createSpy',
+                },
+                object: {
+                    type: 'Identifier',
+                    name: 'jasmine',
+                },
+            },
+        })
+        .forEach(path => {
+            // make it `jest.fn()`
+            path.node.callee = j.memberExpression(
+                j.identifier('jest'),
+                j.identifier('fn')
+            );
+            path.node.arguments = [];
+        });
+
+    root
+        // find all global `spyOn` calls that are standalone expressions.
+        // e.g.
+        // spyOn(stuff)
+        // but not
+        // spyOn(stuff).and.callThrough();
+        .find(j.ExpressionStatement, {
+            expression: {
+                type: 'CallExpression',
+                callee: {
+                    type: 'Identifier',
+                    name: 'spyOn',
+                },
+            },
+        })
+        .forEach(path => {
+            path.node.expression = j.callExpression(
+                j.memberExpression(
+                    path.node.expression,
+                    // add .mockImplementation(() => {}); call
+                    // because jasmine spy's default is to mock the return value,
+                    // whereas jest calls through by default.
+                    j.identifier('mockImplementation')
+                ),
+                [emptyArrowFn]
+            );
+        });
+
+    root
+        // find spyOn().and.*() expressions
+        .find(j.CallExpression, {
+            callee: {
+                type: 'MemberExpression',
+                object: {
+                    type: 'MemberExpression',
+                    property: { name: 'and' },
+                    object: {
+                        type: 'CallExpression',
+                        callee: {
+                            type: 'Identifier',
+                            name: 'spyOn',
+                        },
+                    },
+                },
+            },
+        })
+        .forEach(path => {
+            const spyType = path.node.callee.property.name;
+            switch (spyType) {
+                // if it's `spyOn().and.callThrough()`
+                // we should remove it and make just `spyOn()`
+                // because jest calls through by default
+                case 'callThrough': {
+                    const callee = path.node.callee.object.object.callee;
+                    const arg = path.node.callee.object.object.arguments;
+                    path.node.callee = callee;
+                    path.node.arguments = arg;
+                    break;
+                }
+                // if it's `spyOn().and.callFake()` replace with jest's
+                // equivalent `spyOn().mockImplementation();
+                case 'callFake': {
+                    path.node.callee.object = path.node.callee.object.object;
+                    path.node.callee.property.name = 'mockImplementation';
+                    break;
+                }
+                // `spyOn().and.returnValue()` is equivalent of
+                // `jest.spyOn().mockReturnValue()`
+                case 'returnValue': {
+                    path.node.callee.object = path.node.callee.object.object;
+                    path.node.callee.property.name = 'mockReturnValue';
+                    break;
+                }
+            }
+        });
+
+    root
+        //   find all `SpyOn` calls
+        .find(j.CallExpression, {
+            callee: { type: 'Identifier', name: 'spyOn' },
+        })
+        .forEach(path => {
+            // and make them `jest.spyOn()`
+            path.node.callee = j.memberExpression(
+                j.identifier('jest'),
+                j.identifier('spyOn')
+            );
+        });
+
+    root
+        // find all `*.calls.count()`
+        .find(j.CallExpression, {
+            callee: {
+                type: 'MemberExpression',
+                property: { type: 'Identifier', name: 'count' },
+                object: {
+                    type: 'MemberExpression',
+                    property: { type: 'Identifier', name: 'calls' },
+                },
+            },
+        })
+        .forEach(path => {
+            // replace `.count()` with `.length`
+            path.node.callee.property.name = 'length';
+            // add extra `.mock` property that jest uses:
+            //   stuff.calls.count() -> stuff.mock.calls.length
+            path.node.callee.object.object = j.memberExpression(
+                path.node.callee.object.object,
+                j.identifier('mock')
+            );
+            j(path).replaceWith(path.node.callee);
+        });
+
+    root
+        // find all `stuff.callCount`
+        .find(j.MemberExpression, {
+            property: {
+                type: 'Identifier',
+                name: 'callCount',
+            },
+        })
+        .forEach(path => {
+            // and make them `stuff.mock.calls.length`
+            path.node.property.name = 'length';
+            path.node.object = j.memberExpression(path.node.object, j.identifier('mock'));
+            path.node.object = j.memberExpression(
+                path.node.object,
+                j.identifier('calls')
+            );
+        });
+
+    root
+        // find `stuff.mostRecentCall`
+        .find(j.MemberExpression, {
+            property: {
+                type: 'Identifier',
+                name: 'mostRecentCall',
+            },
+        })
+        .forEach(path => {
+            // turn it into `stuff.mock.calls[stuff.mock.calls.length - 1]
+            path.node.object = j.memberExpression(path.node.object, j.identifier('mock'));
+            path.node.object = j.memberExpression(
+                path.node.object,
+                j.identifier('calls')
+            );
+            path.node.property = j.binaryExpression(
+                '-',
+                j.memberExpression(path.node.object, j.identifier('length')),
+                j.literal(1)
+            );
+            path.node.computed = true;
+        });
+
+    root
+        // find anything that accesses property on `args`
+        // like `stuff.mostRecentCall.args[0]`
+        .find(j.MemberExpression, {
+            object: {
+                type: 'MemberExpression',
+                property: {
+                    type: 'Identifier',
+                    name: 'args',
+                },
+            },
+        })
+        .forEach(path => {
+            // remove args, since jest calls are array of arrays
+            // `stuff.mostRecentCall[0]`
+            path.node.object.object && (path.node.object = path.node.object.object);
+        });
+
+    root
+        // find `stuff.argsForCall[*]`
+        .find(j.MemberExpression, {
+            object: {
+                type: 'MemberExpression',
+                property: {
+                    type: 'Identifier',
+                    name: 'argsForCall',
+                },
+            },
+        })
+        .forEach(path => {
+            // make them `stuff.mock.calls[*]
+            path.node.object.object && (path.node.object = path.node.object.object);
+            path.node.object = j.memberExpression(path.node.object, j.identifier('mock'));
+            path.node.object = j.memberExpression(
+                path.node.object,
+                j.identifier('calls')
+            );
+        });
+
+    root
+        // replace `andCallFake` with `mockImplementation`
+        .find(j.MemberExpression, {
+            property: {
+                type: 'Identifier',
+                name: 'andCallFake',
+            },
+        })
+        .forEach(path => {
+            path.node.property.name = 'mockImplementation';
+        });
+
+    root
+        // replace `andReturn` with `mockReturnValue`
+        .find(j.MemberExpression, {
+            property: {
+                type: 'Identifier',
+                name: 'andReturn',
+            },
+        })
+        .forEach(path => {
+            path.node.property.name = 'mockReturnValue';
+        });
+
+    root
+        // fin all `andCallThrough` and delete them since
+        // jest mocks call through by default
+        .find(j.CallExpression, {
+            callee: {
+                type: 'MemberExpression',
+                property: {
+                    type: 'Identifier',
+                    name: 'andCallThrough',
+                },
+            },
+        })
+        .forEach(path => {
+            j(path).replaceWith(path.node.callee.object);
+        });
+
+    return finale(fileInfo, j, root, options);
+}


Test patch:
diff --git a/src/transformers/jasmine-globals.test.js b/src/transformers/jasmine-globals.test.js
new file mode 100644
index 0000000..8de172e
--- /dev/null
+++ b/src/transformers/jasmine-globals.test.js
@@ -0,0 +1,78 @@
+/* eslint-env jest */
+import chalk from 'chalk';
+import { wrapPlugin } from '../utils/test-helpers';
+import plugin from './jasmine-globals';
+
+chalk.enabled = false;
+const wrappedPlugin = wrapPlugin(plugin);
+
+function testChanged(msg, source, expectedOutput) {
+    test(msg, () => {
+        const result = wrappedPlugin(source);
+        expect(result).toBe(expectedOutput);
+    });
+}
+
+testChanged(
+    'spyOn',
+    `
+    jest.spyOn().mockReturnValue();
+    spyOn(stuff).and.callThrough();
+    spyOn(stuff).and.callFake(() => 'lol');
+    spyOn(stuff).and.returnValue('lmao');
+    jest.spyOn();
+    spyOn(stuff);
+    jest.spyOn().mockImplementation();
+    jasmine.createSpy();
+    jasmine.createSpy('lmao');
+    `,
+    `
+    jest.spyOn().mockReturnValue();
+    jest.spyOn(stuff);
+    jest.spyOn(stuff).mockImplementation(() => 'lol');
+    jest.spyOn(stuff).mockReturnValue('lmao');
+    jest.spyOn();
+    jest.spyOn(stuff).mockImplementation(() => {});
+    jest.spyOn().mockImplementation();
+    jest.fn();
+    jest.fn();
+    `
+);
+
+testChanged(
+    'mock.calls.count()',
+    `
+    someMock.calls.count();
+    stuff.someMock.calls.count();
+    getMock(stuff).calls.count();
+    getMock(stuff).calls.count() > 1;
+    wyoming.cheyenne.callCount
+    wyoming.cheyenne.stuff.mostRecentCall.args[0]
+    georgia.atlanta.mostRecentCall.args.map(fn)
+    args.map()
+    oklahoma.argsForCall[0]
+    idaho.argsForCall[0][1]
+    spyOn('stuff').andCallFake(fn);
+    hmm.andCallFake(fn);
+    spyOn('stuff').andReturn('lol');
+    spyOn('stuff').andCallThrough();
+    stuff.andCallThrough();
+    `,
+    `
+    someMock.mock.calls.length;
+    stuff.someMock.mock.calls.length;
+    getMock(stuff).mock.calls.length;
+    getMock(stuff).mock.calls.length > 1;
+    wyoming.cheyenne.mock.calls.length
+    wyoming.cheyenne.stuff.mock.calls[wyoming.cheyenne.stuff.mock.calls.length - 1][0]
+    georgia.atlanta.mock.calls[georgia.atlanta.mock.calls.length - 1].map(fn)
+    args.map()
+    oklahoma.mock.calls[0]
+    idaho.mock.calls[0][1]
+    jest.spyOn('stuff').mockImplementation(fn);
+    hmm.mockImplementation(fn);
+    jest.spyOn('stuff').mockReturnValue('lol');
+    jest.spyOn('stuff');
+    stuff;
+    `
+);