Repository: skovhus/jest-codemods
Pull request #39 (2017-03-16 09:44:47)

Expect to jest transformer
@mjackson https://www.npmjs.com/package/expect is being integrated into Jest. This codemod automates the upgrade path. It is extending the work started in https://github.com/cpojer/js-codemod/pull/66

Background: https://github.com/facebook/jest/issues/1679

/cc @cpojer

Step 1
- [x] add initial codemod from https://github.com/cpojer/js-codemod/blob/master/transforms/expect.js by @vjeux 
- [x] `messages` from all matchers should be removed as some of them actually break Jest (e.g. `expect(object).toExist('remove me')`)
- [x] warning on usage of chained expect calls
- [x] remap non standard named expect import/require name
- [x] add more test cases
- [x] warn about comparator optional argument of `toInclude` and `toIncludeKeys` that is left but ignored by Jest
- [x] spyOn calls https://github.com/facebook/jest/issues/1679#issuecomment-282801070 and https://github.com/facebook/jest/issues/1679#issuecomment-282576745
 - - [x] andCall andReturn andThrow (createSpy and spyOn)
 - - [x] andCallThrough not supported in Jest?
 - - [x] `spy.calls[i].arguments[j] -> spy.mock.calls[i][j]`
 - - [x] `spy.calls.length -> spy.mock.calls.length`
 - - [x] `restoreSpies` and `resetSpies`
 - - [x] warn about `isSpy` is missing from Jest
 - - [x] `import expect, { createSpy, spyOn, isSpy } from 'expect'` warning
- [x] when running in standalone mode, running the transformer twice on the output shouldn't break or change anything (as `expect` will be used for both `expect@1` and the new Jest version, we cannot import detect).
- [x] toMatch can also be on objects, so cannot be translated 1:1 with Jest toMatch... See https://github.com/mjackson/expect#tomatch (maybe toMatchObject?) and https://github.com/ReactTraining/react-media/pull/61/files/886826394d672a721b7decaab3831d4af9d9177a#r124528600
- [x] expect allow expect argument to be undefined, Jest doesn't (example: `          expect(node.firstChild.innerHTML).not.toMatch(/hello/)` from `react-media`) 
- [x] ensure all matchers from `expect` are covered (or a warning is logged for unsupported features)

Step 2
- [x] try the codemod out on mjackson's or ReactTraining projects 
- - [x] react-router https://github.com/ReactTraining/react-router/pull/5027
- - [x] history https://github.com/ReactTraining/history/pull/468
- - [x] react-media https://github.com/ReactTraining/react-media/pull/61
- - [x] fix failures due to https://github.com/facebook/jest/issues/3360
- [x] consider if other transformers in jest-codemods would benefit form the standalone mode option (being able to run in node)
- [x] document standalone mode 
- [x] document steps to do after codemod has run
- [x] figure out if package is called `expect` or `jest-matchers`

### Bugs
- [x] `console.error` does't have `calls` property when using `jest` spies:
```
  it('warns when passed a history prop', () => {
    const history = {}
    const node = document.createElement('div')
    expect.spyOn(console, 'error')

    ReactDOM.render((
      <MemoryRouter history={history} />
    ), node)

    expect(console.error.calls.length).toBe(1)
    expect(console.error.calls[0].arguments[0]).toMatch(
      /<MemoryRouter.*import \{ Router }/)
    expect.restoreSpies()
  })
```

mjackson: Thank you, @skovhus :) How can I try this out? I'm not familiar with how to run a code mod. Is there a doc somewhere?
skovhus: Hi @mjackson. 

I'll update you when the codemod is ready. But this repo has a little CLI that should make it fairly easy to run on an existing codebase. It wraps jscodeshift. 

But I might try the codemod out on some of your repos with expect. ; )
skovhus: @mjackson for projects like React History and React Router it seems that you are using Expect+Karma+Mocha to execute your tests in Chrome or other browsers.

As Jest is executed in node only, these projects might not be good candidates for testing this codemod. Unless if you are interested in switching to a node only test environment? Let me know what you think : )
cpojer: This is fantastic.

@skovhus The new version of `expect` must definitely also be made to work on the web. So this codemod should enable people to switch from the previous version of expect to the new version. One way to try that out would be to run the codemod on react history/react router and then add `jest-matchers` and change `const expect = require('jest-matchers')` and make sure that works in the browser.

We may have to provide a single-file build for jest-matchers, that's still to be decided.

The idea is to ship this breaking change sometime in mid April.
skovhus: Thanks for the input! So essentially two uses cases: for upgrading expect and for upgrading to use Jest as test runner. 

The output of these two cases can be made compatible if we leave the jest-matchers input in there. 

So if we import expect from jest-matchers even for users just running on node... that might make sense, as we don't need two codemods or configuration. 
cpojer: Hmm, here would be my recommendation:

* Transform with Jest:
 * `expect.spyOn` => `jest.spyOn`, `expect.spy` => `jest.fn`
 * `require('expect')` gets removed.
* Transform without Jest:
 * `expect.spyOn` => `mock.spyOn`, `expect.spy` => `mock.fn` by including `jest-mock`.

The module `jest-matches` will be renamed to `expect`, so the require statement does not need to changed. Inside of Jest, we don't need to require it because it is globally available.
skovhus: Two codemods (or configuration) it is. 👍🏻
cpojer: btw. this plugin should help insert requires for the second one: https://github.com/cpojer/js-codemod/tree/master/extensions/imports
brantphoto: hi @skovhus I'm excited to use this codemod. We'll be using this exact scenario to move to Jest at my work. Thanks for making it!
skovhus: @brantphoto so happy to hear that! : )

I'll let you know when you can try it out. 👍 

Patch:
diff --git a/README.md b/README.md
index 09a4eae..277160c 100644
--- a/README.md
+++ b/README.md
@@ -65,7 +65,7 @@ $ jest-codemods --help
 
 To transform all test files in a directory run `jest-codemods test-folder` in your terminal.
 
-Notice the console output for errors, manual intervention and tweaks are to be expected.
+Notice the console output for errors, manual intervention and tweaks might be required.
 
 
 ## Usage (jscodeshift)
diff --git a/package.json b/package.json
index 85da979..6bf75a9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "jest-codemods",
-  "version": "0.10.1",
+  "version": "0.10.0",
   "description": "Codemods for migrating test files to Jest",
   "license": "MIT",
   "repository": "skovhus/jest-codemods",
@@ -63,7 +63,7 @@
     "eslint-config-prettier": "^1.6.0",
     "eslint-plugin-import": "^2.2.0",
     "eslint-plugin-prettier": "^2.0.1",
-    "jest": "^19.0.2",
+    "jest": "^20.0.4",
     "prettier": "^1.0.2"
   },
   "jest": {
diff --git a/src/cli/index.js b/src/cli/index.js
index 11b8e1d..298a820 100644
--- a/src/cli/index.js
+++ b/src/cli/index.js
@@ -1,4 +1,5 @@
 #!/usr/bin/env node
+import chalk from 'chalk';
 import globby from 'globby';
 import inquirer from 'inquirer';
 import meow from 'meow';
@@ -46,6 +47,7 @@ const ALL_TRANSFORMERS = [
     TRANSFORMER_AVA,
     TRANSFORMER_CHAI_ASSERT,
     // TRANSFORMER_CHAI_SHOULD & TRANSFORMER_SHOULD doesn't have import detection
+    // TODO: waiting for expect@20+ release: TRANSFORMER_EXPECT,
     TRANSFORMER_MOCHA,
     TRANSFORMER_TAPE,
 ];
@@ -76,6 +78,13 @@ inquirer
                     name: 'Chai: Should/Expect BDD Syntax',
                     value: TRANSFORMER_CHAI_SHOULD,
                 },
+                /*
+                // TODO: waiting for expect@20+ release
+                {
+                    name: 'Expect@1.x',
+                    value: TRANSFORMER_EXPECT,
+                },
+                */
                 {
                     name: 'Mocha',
                     value: TRANSFORMER_MOCHA,
@@ -98,6 +107,24 @@ inquirer
                 },
             ],
         },
+        /*
+        // TODO: waiting for expect@20+ release
+        {
+            name: 'standaloneMode',
+            type: 'list',
+            message: 'Should the tests be able to run in a browser?',
+            choices: [
+                {
+                    name: 'The tests should run on node.js (recommended)',
+                    value: false,
+                },
+                {
+                    name: 'The tests should run in a browser',
+                    value: true,
+                },
+            ],
+        },
+        */
         {
             type: 'list',
             name: 'mochaAssertion',
@@ -132,7 +159,7 @@ inquirer
         },
     ])
     .then(answers => {
-        const { files, transformer, mochaAssertion } = answers;
+        const { files, transformer, mochaAssertion, standaloneMode } = answers;
 
         if (transformer === 'other') {
             return supportFailure('AVA, Tape, Chai and Mocha');
@@ -154,5 +181,20 @@ inquirer
             checkGitStatus(cli.flags.force);
         }
 
-        return executeTransformations(filesExpanded, cli.flags, transformers);
+        const transformerArgs = [];
+        if (standaloneMode) {
+            transformerArgs.push('--standaloneMode=true');
+            console.log(
+                chalk.yellow(
+                    '\nNOTICE: You need to manually install jest-matchers and jest-mock'
+                )
+            );
+        }
+
+        return executeTransformations(
+            filesExpanded,
+            cli.flags,
+            transformers,
+            transformerArgs
+        );
     });
diff --git a/src/cli/transformers.js b/src/cli/transformers.js
index ef26cac..3449d94 100644
--- a/src/cli/transformers.js
+++ b/src/cli/transformers.js
@@ -4,10 +4,10 @@ import execa from 'execa';
 export const transformerDirectory = path.join(__dirname, '../', 'transformers');
 export const jscodeshiftExecutable = require.resolve('.bin/jscodeshift');
 
-function executeTransformation(files, flags, transformer) {
+function executeTransformation(files, flags, transformer, transformerArgs) {
     const transformerPath = path.join(transformerDirectory, `${transformer}.js`);
 
-    const args = ['-t', transformerPath].concat(files);
+    let args = ['-t', transformerPath].concat(files);
     if (flags.dry) {
         args.push('--dry');
     }
@@ -15,6 +15,10 @@ function executeTransformation(files, flags, transformer) {
         args.push('--parser', flags.parser);
     }
 
+    if (transformerArgs && transformerArgs.length > 0) {
+        args = args.concat(transformerArgs);
+    }
+
     console.log(`Executing command: jscodeshift ${args.join(' ')}`);
 
     const result = execa.sync(jscodeshiftExecutable, args, {
@@ -27,8 +31,13 @@ function executeTransformation(files, flags, transformer) {
     }
 }
 
-export function executeTransformations(files, flags, transformers) {
+export function executeTransformations(
+    files,
+    flags,
+    transformers,
+    transformerOptions = []
+) {
     transformers.forEach(t => {
-        executeTransformation(files, flags, t);
+        executeTransformation(files, flags, t, transformerOptions);
     });
 }
diff --git a/src/transformers/ava.js b/src/transformers/ava.js
index 5d1c66b..2ceaf06 100644
--- a/src/transformers/ava.js
+++ b/src/transformers/ava.js
@@ -2,9 +2,7 @@
  * Codemod for transforming AVA tests into Jest.
  */
 
-import detectQuoteStyle from '../utils/quote-style';
 import { removeRequireAndImport } from '../utils/imports';
-import detectIncompatiblePackages from '../utils/incompatible-packages';
 import { PROP_WITH_SECONDS_ARGS } from '../utils/consts';
 import {
     detectUnsupportedNaming,
@@ -16,7 +14,7 @@ import {
     getMemberExpressionElements,
 } from '../utils/recast-helpers';
 import logger from '../utils/logger';
-import proxyquireTransformer from '../utils/proxyquire';
+import finale from '../utils/finale';
 
 const SPECIAL_THROWS_CASE = '(special throws case)';
 const SPECIAL_BOOL = '(special bool case)';
@@ -42,7 +40,6 @@ const tPropertiesMap = {
     ifError: 'toBeFalsy',
     error: 'toBeFalsy',
     plan: SPECIAL_PLAN_CASE,
-    snapshot: 'toMatchSnapshot',
 };
 
 const tPropertiesNotMapped = new Set(['end', 'fail', 'pass']);
@@ -56,7 +53,7 @@ const avaToJestMethods = {
     only: 'test.only',
 };
 
-export default function avaToJest(fileInfo, api) {
+export default function avaToJest(fileInfo, api, options) {
     const j = api.jscodeshift;
     const ast = j(fileInfo.source);
 
@@ -209,15 +206,9 @@ export default function avaToJest(fileInfo, api) {
                     )
                 );
         },
-
-        () => detectIncompatiblePackages(fileInfo, j, ast),
-        () => proxyquireTransformer(fileInfo, j, ast),
     ];
 
     transforms.forEach(t => t());
 
-    // As Recast is not preserving original quoting, we try to detect it,
-    // and default to something sane.
-    const quote = detectQuoteStyle(j, ast) || 'single';
-    return ast.toSource({ quote });
+    return finale(fileInfo, j, ast, options);
 }
diff --git a/src/transformers/chai-assert.js b/src/transformers/chai-assert.js
index b06ac70..236f8ec 100644
--- a/src/transformers/chai-assert.js
+++ b/src/transformers/chai-assert.js
@@ -1,6 +1,6 @@
-import detectQuoteStyle from '../utils/quote-style';
 import logger from '../utils/logger';
 import { removeRequireAndImport } from '../utils/imports';
+import finale from '../utils/finale';
 
 const getAssertionExpression = identifier => ({
     type: 'CallExpression',
@@ -176,7 +176,7 @@ const unsupportedAssertions = [
     'ifError',
 ];
 
-export default function transformer(fileInfo, api) {
+export default function transformer(fileInfo, api, options) {
     const j = api.jscodeshift;
     const ast = j(fileInfo.source);
 
@@ -418,8 +418,5 @@ export default function transformer(fileInfo, api) {
         })
         .replaceWith(path => makeExpectation('toBeTruthy', path.value.arguments[0]));
 
-    // As Recast is not preserving original quoting, we try to detect it,
-    // and default to something sane.
-    const quote = detectQuoteStyle(j, ast) || 'single';
-    return ast.toSource({ quote });
+    return finale(fileInfo, j, ast, options);
 }
diff --git a/src/transformers/chai-should.js b/src/transformers/chai-should.js
index 3a44017..cd8ef35 100644
--- a/src/transformers/chai-should.js
+++ b/src/transformers/chai-should.js
@@ -8,7 +8,7 @@ import {
 import logger from '../utils/logger';
 import { removeRequireAndImport } from '../utils/imports';
 import { traverseMemberExpressionUtil } from '../utils/recast-helpers';
-import detectQuoteStyle from '../utils/quote-style';
+import finale from '../utils/finale';
 
 const fns = [
     'keys',
@@ -73,7 +73,7 @@ const mapValueNameToObjectMethod = {
 
 export const assertPrefixes = new Set(['to', 'with', 'that']);
 
-module.exports = function transformer(fileInfo, api) {
+module.exports = function transformer(fileInfo, api, options) {
     const j = api.jscodeshift;
     const root = j(fileInfo.source);
     let mutations = 0;
@@ -496,6 +496,5 @@ module.exports = function transformer(fileInfo, api) {
         return null;
     }
 
-    const quote = detectQuoteStyle(j, root) || 'single';
-    return root.toSource({ quote });
+    return finale(fileInfo, j, root, options);
 };
diff --git a/src/transformers/expect.js b/src/transformers/expect.js
new file mode 100644
index 0000000..2696b7b
--- /dev/null
+++ b/src/transformers/expect.js
@@ -0,0 +1,357 @@
+import { JEST_MATCHER_TO_MAX_ARGS, JEST_MOCK_PROPERTIES } from '../utils/consts';
+import { getRequireOrImportName, removeRequireAndImport } from '../utils/imports';
+import {
+    findParentCallExpression,
+    findParentOfType,
+    findParentVariableDeclaration,
+} from '../utils/recast-helpers';
+import logger from '../utils/logger';
+import finale from '../utils/finale';
+
+const matcherRenaming = {
+    toExist: 'toBeTruthy',
+    toNotExist: 'toBeFalsy',
+    toNotBe: 'not.toBe',
+    toNotEqual: 'not.toEqual',
+    toNotThrow: 'not.toThrow',
+    toBeA: 'toBeInstanceOf',
+    toBeAn: 'toBeInstanceOf',
+    toNotBeA: 'not.toBeInstanceOf',
+    toNotBeAn: 'not.toBeInstanceOf',
+    toNotMatch: 'not.toMatch',
+    toBeFewerThan: 'toBeLessThan',
+    toBeLessThanOrEqualTo: 'toBeLessThanOrEqual',
+    toBeMoreThan: 'toBeGreaterThan',
+    toBeGreaterThanOrEqualTo: 'toBeGreaterThanOrEqual',
+    toInclude: 'toContain',
+    toExclude: 'not.toContain',
+    toNotContain: 'not.toContain',
+    toNotInclude: 'not.toContain',
+    toNotHaveBeenCalled: 'not.toHaveBeenCalled',
+};
+
+const matchersToBe = new Set(['toBeA', 'toBeAn', 'toNotBeA', 'toNotBeAn']);
+
+const matchersWithKey = new Set([
+    'toContainKey',
+    'toExcludeKey',
+    'toIncludeKey',
+    'toNotContainKey',
+    'toNotIncludeKey',
+]);
+
+const matchersWithKeys = new Set([
+    'toContainKeys',
+    'toExcludeKeys',
+    'toIncludeKeys',
+    'toNotContainKeys',
+    'toNotIncludeKeys',
+]);
+
+const expectSpyFunctions = new Set(['createSpy', 'spyOn', 'isSpy', 'restoreSpies']);
+const unsupportedSpyFunctions = new Set(['isSpy', 'restoreSpies']);
+
+export default function expectTransformer(fileInfo, api, options) {
+    const j = api.jscodeshift;
+    const ast = j(fileInfo.source);
+    const { standaloneMode } = options;
+
+    const expectFunctionName = getRequireOrImportName(j, ast, 'expect');
+
+    if (!expectFunctionName) {
+        // No expect require/import were found
+        return fileInfo.source;
+    }
+
+    if (!standaloneMode) {
+        removeRequireAndImport(j, ast, 'expect');
+    }
+
+    const logWarning = (msg, node) => logger(fileInfo, msg, node);
+
+    function balanceMatcherNodeArguments(matcherNode, matcher, path) {
+        const newJestMatcherName = matcher.name.replace('not.', '');
+        const maxArgs = JEST_MATCHER_TO_MAX_ARGS[newJestMatcherName];
+        if (typeof maxArgs === 'undefined') {
+            throw new Error(
+                `Unknown matcher "${newJestMatcherName}" (JEST_MATCHER_TO_MAX_ARGS)`
+            );
+        }
+
+        if (matcherNode.arguments.length > maxArgs) {
+            // Try to remove assertion message
+            const lastArg = matcherNode.arguments[matcherNode.arguments.length - 1];
+            if (lastArg.type === 'Literal') {
+                matcherNode.arguments.pop();
+            }
+        }
+
+        if (matcherNode.arguments.length <= maxArgs) {
+            return;
+        }
+
+        logWarning(
+            `Too many arguments given to "${newJestMatcherName}". Expected max ${maxArgs} but got ${matcherNode.arguments.length}`,
+            path
+        );
+    }
+
+    const updateMatchers = () =>
+        ast
+            .find(j.MemberExpression, {
+                object: {
+                    type: 'CallExpression',
+                    callee: { type: 'Identifier', name: expectFunctionName },
+                },
+                property: { type: 'Identifier' },
+            })
+            .forEach(path => {
+                if (path.parentPath.parentPath.node.type === 'MemberExpression') {
+                    logWarning(
+                        'Chaining expect matchers is currently not supported',
+                        path
+                    );
+                    return;
+                }
+
+                if (!standaloneMode) {
+                    path.parentPath.node.callee.object.callee.name = 'expect';
+                }
+
+                const matcherNode = path.parentPath.node;
+                const matcher = path.node.property;
+                const matcherName = matcher.name;
+
+                const matcherArgs = matcherNode.arguments;
+                const expectArgs = path.node.object.arguments;
+
+                const isNot =
+                    matcherName.indexOf('Not') !== -1 ||
+                    matcherName.indexOf('Exclude') !== -1;
+
+                if (matcherRenaming[matcherName]) {
+                    matcher.name = matcherRenaming[matcherName];
+                }
+
+                if (matchersToBe.has(matcherName)) {
+                    if (matcherArgs[0].type === 'Literal') {
+                        expectArgs[0] = j.unaryExpression('typeof', expectArgs[0]);
+                        matcher.name = isNot ? 'not.toBe' : 'toBe';
+                    }
+                }
+
+                if (matchersWithKey.has(matcherName)) {
+                    expectArgs[0] = j.template.expression`Object.keys(${expectArgs[0]})`;
+                    matcher.name = isNot ? 'not.toContain' : 'toContain';
+                }
+
+                if (matchersWithKeys.has(matcherName)) {
+                    const keys = matcherArgs[0];
+                    matcherArgs[0] = j.identifier('e');
+                    matcher.name = isNot ? 'not.toContain' : 'toContain';
+                    j(path.parentPath).replaceWith(
+                        j.template.expression`\
+${keys}.forEach(e => {
+  ${matcherNode}
+})`
+                    );
+                }
+
+                if (matcherName === 'toMatch' || matcherName === 'toNotMatch') {
+                    // expect toMatch handles string, reg exp and object.
+                    const { name, type } = matcherArgs[0];
+                    if (type === 'ObjectExpression' || type === 'Identifier') {
+                        matcher.name = isNot ? 'not.toMatchObject' : 'toMatchObject';
+
+                        if (type === 'Identifier') {
+                            logWarning(
+                                `Use "toMatch" if "${name}" is not an object`,
+                                path
+                            );
+                        }
+                    }
+                }
+
+                balanceMatcherNodeArguments(matcherNode, matcher, path);
+            });
+
+    const updateSpies = () => {
+        ast
+            .find(j.CallExpression, {
+                callee: {
+                    type: 'Identifier',
+                    name: name => expectSpyFunctions.has(name),
+                },
+            })
+            .forEach(path => {
+                logWarning(
+                    `"${path.value.callee.name}" is currently not supported ` +
+                        `(use "expect.${path.value.callee.name}" instead for transformation to work)`,
+                    path
+                );
+            });
+
+        // Update expect.createSpy calls and warn about restoreSpies
+        ast
+            .find(j.MemberExpression, {
+                object: {
+                    type: 'Identifier',
+                    name: expectFunctionName,
+                },
+                property: { type: 'Identifier' },
+            })
+            .forEach(path => {
+                const { name } = path.value.property;
+                if (name === 'createSpy') {
+                    path.value.property.name = 'fn';
+                }
+
+                if (unsupportedSpyFunctions.has(name)) {
+                    logWarning(
+                        `"${path.value.property.name}" is currently not supported`,
+                        path
+                    );
+                }
+            });
+
+        // Warn about expect.spyOn calls with variable assignment
+        ast
+            .find(j.MemberExpression, {
+                object: {
+                    type: 'Identifier',
+                    name: expectFunctionName,
+                },
+                property: { type: 'Identifier', name: 'spyOn' },
+            })
+            .forEach(path => {
+                const parentAssignment =
+                    findParentOfType(path, 'VariableDeclarator') ||
+                    findParentOfType(path, 'AssignmentExpression');
+                if (!parentAssignment) {
+                    logWarning(
+                        `"${path.value.property.name}" without variable assignment might not work as expected (see https://facebook.github.io/jest/docs/jest-object.html#jestspyonobject-methodname)`,
+                        path
+                    );
+                }
+            });
+
+        // Update mock chain calls
+        const updateSpyProperty = (path, property) => {
+            if (!property) {
+                return;
+            }
+
+            if (property.name === 'andReturn') {
+                const callExpression = findParentCallExpression(path, property.name)
+                    .value;
+                callExpression.arguments = [
+                    j.arrowFunctionExpression(
+                        [j.identifier('()')],
+                        callExpression.arguments[0]
+                    ),
+                ];
+            }
+
+            if (property.name === 'andThrow') {
+                const callExpression = findParentCallExpression(path, property.name)
+                    .value;
+                const throughExpression = callExpression.arguments[0];
+                callExpression.arguments = [
+                    j.arrowFunctionExpression(
+                        [j.identifier('()')],
+                        j.blockStatement([j.throwStatement(throughExpression)])
+                    ),
+                ];
+            }
+
+            if (property.name === 'andCallThrough') {
+                logWarning(`"${property.name}" is currently not supported`, path);
+            }
+
+            const propertyNameMap = {
+                andCall: 'mockImplementation',
+                andReturn: 'mockImplementation',
+                andThrow: 'mockImplementation',
+                calls: 'mock.calls',
+                reset: 'mockClear',
+                restore: 'mockReset',
+            };
+
+            const newPropertyName = propertyNameMap[property.name];
+            if (newPropertyName) {
+                property.name = newPropertyName;
+            }
+
+            // Remap mock.calls[x].arguments[y] to mock.calls[x][y]
+            const potentialArgumentsNode = path.parentPath.parentPath.value;
+            if (
+                property.name === 'mock.calls' &&
+                potentialArgumentsNode.property &&
+                potentialArgumentsNode.property.name === 'arguments'
+            ) {
+                const outherNode = path.parentPath.parentPath.parentPath;
+
+                const variableName = path.value.object.name;
+                const callsArg = path.parentPath.value.property.name;
+                const argumentsArg = outherNode.value.property.name;
+
+                outherNode.replace(
+                    j.memberExpression(
+                        j.memberExpression(
+                            j.memberExpression(
+                                j.identifier(variableName),
+                                j.identifier('mock.calls')
+                            ),
+                            j.identifier(callsArg),
+                            true
+                        ),
+                        j.identifier(argumentsArg),
+                        true
+                    )
+                );
+            }
+        };
+
+        const spyVariables = [];
+        ast
+            .find(j.MemberExpression, {
+                object: {
+                    type: 'Identifier',
+                    name: expectFunctionName,
+                },
+                property: {
+                    type: 'Identifier',
+                    name: name => JEST_MOCK_PROPERTIES.has(name),
+                },
+            })
+            .forEach(path => {
+                const spyVariable = findParentVariableDeclaration(path);
+                if (spyVariable) {
+                    spyVariables.push(spyVariable.value.id.name);
+                }
+
+                const { property } = path.parentPath.parentPath.value;
+
+                updateSpyProperty(path, property);
+            });
+
+        // Update spy variable methods
+        ast
+            .find(j.MemberExpression, {
+                object: {
+                    type: 'Identifier',
+                    name: name => spyVariables.indexOf(name) >= 0,
+                },
+                property: { type: 'Identifier' },
+            })
+            .forEach(path => {
+                const { property } = path.value;
+                updateSpyProperty(path, property);
+            });
+    };
+
+    updateMatchers();
+    updateSpies();
+
+    return finale(fileInfo, j, ast, options, expectFunctionName);
+}
diff --git a/src/transformers/mocha.js b/src/transformers/mocha.js
index b3ccf1c..624d8a1 100644
--- a/src/transformers/mocha.js
+++ b/src/transformers/mocha.js
@@ -1,4 +1,4 @@
-import detectQuoteStyle from '../utils/quote-style';
+import finale from '../utils/finale';
 
 const methodMap = {
     suite: 'describe',
@@ -32,9 +32,9 @@ function hasBinding(name, scope) {
     return scope.isGlobal ? false : hasBinding(name, scope.parent);
 }
 
-export default function mochaToJest(file, api) {
+export default function mochaToJest(fileInfo, api, options) {
     const j = api.jscodeshift;
-    const ast = j(file.source);
+    const ast = j(fileInfo.source);
 
     Object.keys(methodMap).forEach(mochaMethod => {
         const jestMethod = methodMap[mochaMethod];
@@ -75,8 +75,5 @@ export default function mochaToJest(file, api) {
         });
     });
 
-    // As Recast is not preserving original quoting, we try to detect it,
-    // and default to something sane.
-    const quote = detectQuoteStyle(j, ast) || 'single';
-    return ast.toSource({ quote });
+    return finale(fileInfo, j, ast, options);
 }
diff --git a/src/transformers/should.js b/src/transformers/should.js
index 587878c..85daad6 100644
--- a/src/transformers/should.js
+++ b/src/transformers/should.js
@@ -8,7 +8,7 @@ const assertionRemappings = {
     throws: 'throw',
 };
 
-module.exports = function transformer(fileInfo, api) {
+module.exports = function transformer(fileInfo, api, options) {
     const j = api.jscodeshift;
     const root = j(fileInfo.source);
 
@@ -86,5 +86,5 @@ module.exports = function transformer(fileInfo, api) {
     const quote = detectQuoteStyle(j, root) || 'single';
     fileInfo.source = root.toSource({ quote });
 
-    return chaiShouldTransformer(fileInfo, api);
+    return chaiShouldTransformer(fileInfo, api, options);
 };
diff --git a/src/transformers/tape.js b/src/transformers/tape.js
index 3ab358b..2783da6 100644
--- a/src/transformers/tape.js
+++ b/src/transformers/tape.js
@@ -1,9 +1,7 @@
 /**
  * Codemod for transforming Tape tests into Jest.
  */
-import detectQuoteStyle from '../utils/quote-style';
 import { removeRequireAndImport } from '../utils/imports';
-import detectIncompatiblePackages from '../utils/incompatible-packages';
 import { PROP_WITH_SECONDS_ARGS } from '../utils/consts';
 import {
     detectUnsupportedNaming,
@@ -11,7 +9,7 @@ import {
     rewriteDestructuredTArgument,
 } from '../utils/tape-ava-helpers';
 import logger from '../utils/logger';
-import proxyquireTransformer from '../utils/proxyquire';
+import finale from '../utils/finale';
 
 const SPECIAL_THROWS_CASE = '(special throws case)';
 const SPECIAL_PLAN_CASE = '(special plan case)';
@@ -80,7 +78,7 @@ const tPropertiesUnsupported = new Set([
 
 const unsupportedTestFunctionProperties = new Set(['createStream', 'onFinish']);
 
-export default function tapeToJest(fileInfo, api) {
+export default function tapeToJest(fileInfo, api, options) {
     const j = api.jscodeshift;
     const ast = j(fileInfo.source);
 
@@ -256,15 +254,9 @@ export default function tapeToJest(fileInfo, api) {
                     rewriteAssertionsAndTestArgument(j, p);
                 });
         },
-
-        () => detectIncompatiblePackages(fileInfo, j, ast),
-        () => proxyquireTransformer(fileInfo, j, ast),
     ];
 
     transforms.forEach(t => t());
 
-    // As Recast is not preserving original quoting, we try to detect it,
-    // and default to something sane.
-    const quote = detectQuoteStyle(j, ast) || 'single';
-    return ast.toSource({ quote });
+    return finale(fileInfo, j, ast, options);
 }
diff --git a/src/utils/chai-chain-utils.js b/src/utils/chai-chain-utils.js
index a7179f2..16e78f0 100644
--- a/src/utils/chai-chain-utils.js
+++ b/src/utils/chai-chain-utils.js
@@ -1,11 +1,11 @@
-import { MATCHER_TO_MAX_ARGS } from './consts';
+import { JEST_MATCHER_TO_MAX_ARGS } from './consts';
 
 export const createCallUtil = j => (fnName, args, rest, containsNot) => {
     const expression = containsNot ? j.memberExpression(rest, j.identifier('not')) : rest;
 
-    const numberOfArgs = MATCHER_TO_MAX_ARGS[fnName];
+    const numberOfArgs = JEST_MATCHER_TO_MAX_ARGS[fnName];
     if (typeof numberOfArgs === 'undefined') {
-        throw new Error(`Unknown matcher "${fnName}" (MATCHER_TO_MAX_ARGS)`);
+        throw new Error(`Unknown matcher "${fnName}" (JEST_MATCHER_TO_MAX_ARGS)`);
     }
 
     return j.memberExpression(
diff --git a/src/utils/consts.js b/src/utils/consts.js
index b0f0277..1f12e71 100644
--- a/src/utils/consts.js
+++ b/src/utils/consts.js
@@ -7,7 +7,7 @@ export const PROP_WITH_SECONDS_ARGS = [
     'not.toMatch',
 ];
 
-export const MATCHER_TO_MAX_ARGS = {
+export const JEST_MATCHER_TO_MAX_ARGS = {
     toBe: 1,
     toBeCloseTo: 2,
     toBeDefined: 0,
@@ -37,3 +37,5 @@ export const MATCHER_TO_MAX_ARGS = {
     toThrowError: 1,
     toThrowErrorMatchingSnapshot: 0,
 };
+
+export const JEST_MOCK_PROPERTIES = new Set(['spyOn', 'fn', 'createSpy']);
diff --git a/src/utils/finale.js b/src/utils/finale.js
new file mode 100644
index 0000000..f5169ad
--- /dev/null
+++ b/src/utils/finale.js
@@ -0,0 +1,57 @@
+import { JEST_MOCK_PROPERTIES } from './consts';
+import { addRequireOrImportOnceFactory, hasRequireOrImport } from './imports';
+import logger from './logger';
+import proxyquireTransformer from './proxyquire';
+import detectQuoteStyle from './quote-style';
+
+function detectIncompatiblePackages(fileInfo, j, ast) {
+    ['sinon', 'testdouble'].forEach(pkg => {
+        if (hasRequireOrImport(j, ast, pkg)) {
+            logger(fileInfo, `Usage of package "${pkg}" might be incompatible with Jest`);
+        }
+    });
+}
+
+function updateJestImports(j, ast, isStandaloneMode, testFunctionName = 'jest') {
+    const addRequireOrImportOnce = addRequireOrImportOnceFactory(j, ast);
+
+    if (isStandaloneMode && !hasRequireOrImport(j, ast, 'expect')) {
+        addRequireOrImportOnce('expect', 'expect');
+    }
+
+    ast
+        .find(j.CallExpression, {
+            callee: {
+                type: 'MemberExpression',
+                object: { type: 'Identifier', name: testFunctionName },
+                property: { name: p => JEST_MOCK_PROPERTIES.has(p) },
+            },
+        })
+        .forEach(path => {
+            const { callee } = path.node;
+            if (isStandaloneMode) {
+                const mockLocalName = 'mock';
+                addRequireOrImportOnce(mockLocalName, 'jest-mock');
+                callee.object = mockLocalName;
+            } else {
+                callee.object = 'jest';
+            }
+        });
+}
+
+/**
+ * Exposes the finale shared by all transformers.
+ * @return the ast.toSource that should be returned to jscodeshift.
+ */
+export default function finale(fileInfo, j, ast, transformerOptions, testFunctionName) {
+    const { standaloneMode } = transformerOptions;
+
+    detectIncompatiblePackages(fileInfo, j, ast);
+    updateJestImports(j, ast, standaloneMode, testFunctionName);
+    proxyquireTransformer(fileInfo, j, ast);
+
+    // As Recast is not preserving original quoting, we try to detect it,
+    // and default to something sane.
+    const quote = detectQuoteStyle(j, ast) || 'single';
+    return ast.toSource({ quote });
+}
diff --git a/src/utils/imports.js b/src/utils/imports.js
index 4b4bd92..159d7ca 100644
--- a/src/utils/imports.js
+++ b/src/utils/imports.js
@@ -1,3 +1,35 @@
+import { findParentVariableDeclaration } from './recast-helpers';
+
+export function addRequireOrImport(j, ast, localName, pkg) {
+    const { statement } = j.template;
+
+    const requires = ast.find(j.CallExpression, {
+        callee: { name: 'require' },
+    });
+
+    let requireStatement;
+    if (requires.size()) {
+        requireStatement = statement`const ${localName} = require(${j.literal(pkg)});`;
+    } else {
+        requireStatement = j.importDeclaration(
+            [j.importDefaultSpecifier(j.identifier(localName))],
+            j.literal(pkg)
+        );
+    }
+
+    ast.find(j.Program).get('body', 0).insertBefore(requireStatement);
+}
+
+export function addRequireOrImportOnceFactory(j, ast) {
+    const pkgs = new Set([]);
+    return (localName, pkg) => {
+        if (!pkgs.has(pkg)) {
+            addRequireOrImport(j, ast, localName, pkg);
+            pkgs.add(pkg);
+        }
+    };
+}
+
 export function findRequires(j, ast, pkg) {
     return ast
         .find(j.CallExpression, {
@@ -25,16 +57,6 @@ export function hasRequireOrImport(j, ast, pkg) {
     return requires + imports > 0;
 }
 
-function findParentVariableDeclaration(path) {
-    if (!path) {
-        return null;
-    }
-    if (path.value.type === 'VariableDeclarator') {
-        return path;
-    }
-    return findParentVariableDeclaration(path.parentPath);
-}
-
 function findParentPathMemberRequire(path) {
     if (path.parentPath && path.parentPath.value.type === 'MemberExpression') {
         return path.parentPath.value.property;
@@ -42,6 +64,29 @@ function findParentPathMemberRequire(path) {
     return null;
 }
 
+/**
+ * Returns localName for any CommonJS or import statements for the given package.
+ * @return string if import were found, else undefined
+ */
+export function getRequireOrImportName(j, ast, pkg) {
+    let localName = null;
+    findRequires(j, ast, pkg).forEach(p => {
+        const variableDeclarationPath = findParentVariableDeclaration(p);
+        if (variableDeclarationPath) {
+            localName = variableDeclarationPath.value.id.name;
+        }
+    });
+
+    findImports(j, ast, pkg).forEach(p => {
+        const pathSpecifier = p.value.specifiers[0];
+        if (pathSpecifier) {
+            localName = pathSpecifier.local.name;
+        }
+    });
+
+    return localName;
+}
+
 /**
  * Detects and removes CommonJS and import statements for given package.
  * @return the import variable name or null if no import were found.
diff --git a/src/utils/incompatible-packages.js b/src/utils/incompatible-packages.js
deleted file mode 100644
index 2e144c1..0000000
--- a/src/utils/incompatible-packages.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { hasRequireOrImport } from './imports';
-import logger from './logger';
-
-export default function detectIncompatiblePackages(fileInfo, j, ast) {
-    ['sinon', 'testdouble'].forEach(pkg => {
-        if (hasRequireOrImport(j, ast, pkg)) {
-            logger(fileInfo, `Usage of package "${pkg}" might be incompatible with Jest`);
-        }
-    });
-}
diff --git a/src/utils/quote-style.js b/src/utils/quote-style.js
index d854e70..324fc7f 100644
--- a/src/utils/quote-style.js
+++ b/src/utils/quote-style.js
@@ -5,7 +5,8 @@
  * @return 'double', 'single' or null
  */
 export default function detectQuoteStyle(j, ast) {
-    let detectedQuoting = null;
+    let doubles = 0;
+    let singles = 0;
 
     ast
         .find(j.Literal, {
@@ -14,14 +15,17 @@ export default function detectQuoteStyle(j, ast) {
         })
         .forEach(p => {
             // The raw value is from the original babel source
-            if (p.value.raw[0] === "'") {
-                detectedQuoting = 'single';
+            const quote = p.value.raw[0];
+            if (quote === '"') {
+                doubles += 1;
             }
-
-            if (p.value.raw[0] === '"') {
-                detectedQuoting = 'double';
+            if (quote === "'") {
+                singles += 1;
             }
         });
 
-    return detectedQuoting;
+    if (doubles === singles) {
+        return null;
+    }
+    return doubles > singles ? 'double' : 'single';
 }
diff --git a/src/utils/recast-helpers.js b/src/utils/recast-helpers.js
index 7b075e3..7cd930c 100644
--- a/src/utils/recast-helpers.js
+++ b/src/utils/recast-helpers.js
@@ -31,6 +31,34 @@ export function getMemberExpressionElements(node, _rest = []) {
     return getMemberExpressionElements(node.object, [node.property.name].concat(_rest));
 }
 
+export function findParentCallExpression(path, name) {
+    if (!path) {
+        return null;
+    }
+    if (
+        path.value.type === 'CallExpression' &&
+        path.value.callee.property &&
+        path.value.callee.property.name === name
+    ) {
+        return path;
+    }
+    return findParentCallExpression(path.parentPath, name);
+}
+
+export function findParentVariableDeclaration(path) {
+    return findParentOfType(path, 'VariableDeclarator');
+}
+
+export function findParentOfType(path, type) {
+    if (!path || !path.value) {
+        return null;
+    }
+    if (path.value.type === type) {
+        return path;
+    }
+    return findParentOfType(path.parentPath, type);
+}
+
 export function traverseMemberExpressionUtil(j, nodeValidator) {
     const traverseMemberExpression = node => {
         if (!node) {
diff --git a/yarn.lock b/yarn.lock
index 99aa1f7..3fdc407 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -69,7 +69,7 @@ ansi-escapes@^1.1.0, ansi-escapes@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
 
-ansi-regex@^2.0.0:
+ansi-regex@^2.0.0, ansi-regex@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
 
@@ -467,13 +467,13 @@ babel-jest@^16.0.0:
     babel-plugin-istanbul "^2.0.0"
     babel-preset-jest "^16.0.0"
 
-babel-jest@^19.0.0:
-  version "19.0.0"
-  resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-19.0.0.tgz#59323ced99a3a84d359da219ca881074ffc6ce3f"
+babel-jest@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-20.0.3.tgz#e4a03b13dc10389e140fc645d09ffc4ced301671"
   dependencies:
     babel-core "^6.0.0"
     babel-plugin-istanbul "^4.0.0"
-    babel-preset-jest "^19.0.0"
+    babel-preset-jest "^20.0.3"
 
 babel-messages@^6.23.0:
   version "6.23.0"
@@ -524,9 +524,9 @@ babel-plugin-jest-hoist@^16.0.0:
   version "16.0.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-16.0.0.tgz#b58ca3f770982a7e7c25b5614b2e57e9dafc6e76"
 
-babel-plugin-jest-hoist@^19.0.0:
-  version "19.0.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-19.0.0.tgz#4ae2a04ea612a6e73651f3fde52c178991304bea"
+babel-plugin-jest-hoist@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-20.0.3.tgz#afedc853bd3f8dc3548ea671fbe69d03cc2c1767"
 
 babel-plugin-jscript@^1.0.4:
   version "1.0.4"
@@ -929,11 +929,11 @@ babel-preset-jest@^16.0.0:
   dependencies:
     babel-plugin-jest-hoist "^16.0.0"
 
-babel-preset-jest@^19.0.0:
-  version "19.0.0"
-  resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-19.0.0.tgz#22d67201d02324a195811288eb38294bb3cac396"
+babel-preset-jest@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-20.0.3.tgz#cbacaadecb5d689ca1e1de1360ebfc66862c178a"
   dependencies:
-    babel-plugin-jest-hoist "^19.0.0"
+    babel-plugin-jest-hoist "^20.0.3"
 
 babel-preset-stage-1@^6.5.0:
   version "6.22.0"
@@ -1026,6 +1026,10 @@ babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0, babylon@^6.16.1, babylon@^6.8
   version "6.16.1"
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3"
 
+babylon@^6.17.4:
+  version "6.17.4"
+  resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.17.4.tgz#3e8b7402b88d22c3423e137a1577883b15ff869a"
+
 balanced-match@^0.4.1:
   version "0.4.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
@@ -1320,7 +1324,7 @@ content-type-parser@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.1.tgz#c3e56988c53c65127fb46d4032a3a900246fdc94"
 
-convert-source-map@^1.1.0:
+convert-source-map@^1.1.0, convert-source-map@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.4.0.tgz#e3dad195bf61bfe13a7a3c73e9876ec14a0268f3"
 
@@ -1395,7 +1399,7 @@ debug@2.2.0, debug@~2.2.0:
   dependencies:
     ms "0.7.1"
 
-debug@^2.1.1, debug@^2.2.0:
+debug@^2.1.1, debug@^2.2.0, debug@^2.6.3:
   version "2.6.3"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.3.tgz#0f7eb8c30965ec08c72accfa0130c8b79984141d"
   dependencies:
@@ -1479,9 +1483,9 @@ detective@^4.3.1:
     acorn "^4.0.3"
     defined "^1.0.0"
 
-diff@^3.0.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9"
+diff@^3.2.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.0.tgz#056695150d7aa93237ca7e378ac3b1682b7963b9"
 
 doctrine@1.5.0:
   version "1.5.0"
@@ -2004,7 +2008,7 @@ glob-parent@^2.0.0:
   dependencies:
     is-glob "^2.0.0"
 
-glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5:
+glob@7.1.1, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1:
   version "7.1.1"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
   dependencies:
@@ -2074,7 +2078,7 @@ got@^5.0.0:
     unzip-response "^1.0.2"
     url-parse-lax "^1.0.0"
 
-graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6:
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4:
   version "4.1.11"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
 
@@ -2284,7 +2288,7 @@ is-builtin-module@^1.0.0:
   dependencies:
     builtin-modules "^1.0.0"
 
-is-ci@^1.0.9:
+is-ci@^1.0.10:
   version "1.0.10"
   resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e"
   dependencies:
@@ -2443,37 +2447,37 @@ isstream@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
 
-istanbul-api@^1.1.0-alpha.1:
-  version "1.1.7"
-  resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.7.tgz#f6f37f09f8002b130f891c646b70ee4a8e7345ae"
+istanbul-api@^1.1.1:
+  version "1.1.10"
+  resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.1.10.tgz#f27e5e7125c8de13f6a80661af78f512e5439b2b"
   dependencies:
     async "^2.1.4"
     fileset "^2.0.2"
-    istanbul-lib-coverage "^1.0.2"
-    istanbul-lib-hook "^1.0.5"
-    istanbul-lib-instrument "^1.7.0"
-    istanbul-lib-report "^1.0.0"
-    istanbul-lib-source-maps "^1.1.1"
-    istanbul-reports "^1.0.2"
+    istanbul-lib-coverage "^1.1.1"
+    istanbul-lib-hook "^1.0.7"
+    istanbul-lib-instrument "^1.7.3"
+    istanbul-lib-report "^1.1.1"
+    istanbul-lib-source-maps "^1.2.1"
+    istanbul-reports "^1.1.1"
     js-yaml "^3.7.0"
     mkdirp "^0.5.1"
     once "^1.4.0"
 
-istanbul-lib-coverage@^1.0.0:
+istanbul-lib-coverage@^1.0.0, istanbul-lib-coverage@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.1.tgz#f263efb519c051c5f1f3343034fc40e7b43ff212"
 
-istanbul-lib-coverage@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.0.2.tgz#87a0c015b6910651cb3b184814dfb339337e25e1"
+istanbul-lib-coverage@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da"
 
-istanbul-lib-hook@^1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.5.tgz#6ca3d16d60c5f4082da39f7c5cd38ea8a772b88e"
+istanbul-lib-hook@^1.0.7:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.0.7.tgz#dd6607f03076578fe7d6f2a630cf143b49bacddc"
   dependencies:
     append-transform "^0.4.0"
 
-istanbul-lib-instrument@^1.1.1, istanbul-lib-instrument@^1.1.4:
+istanbul-lib-instrument@^1.1.4, istanbul-lib-instrument@^1.4.2:
   version "1.4.2"
   resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.4.2.tgz#0e2fdfac93c1dabf2e31578637dc78a19089f43e"
   dependencies:
@@ -2485,138 +2489,149 @@ istanbul-lib-instrument@^1.1.1, istanbul-lib-instrument@^1.1.4:
     istanbul-lib-coverage "^1.0.0"
     semver "^5.3.0"
 
-istanbul-lib-instrument@^1.6.2, istanbul-lib-instrument@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.0.tgz#b8e0dc25709bb44e17336ab47b7bb5c97c23f659"
+istanbul-lib-instrument@^1.6.2, istanbul-lib-instrument@^1.7.3:
+  version "1.7.3"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.7.3.tgz#925b239163eabdd68cc4048f52c2fa4f899ecfa7"
   dependencies:
     babel-generator "^6.18.0"
     babel-template "^6.16.0"
     babel-traverse "^6.18.0"
     babel-types "^6.18.0"
-    babylon "^6.13.0"
-    istanbul-lib-coverage "^1.0.2"
+    babylon "^6.17.4"
+    istanbul-lib-coverage "^1.1.1"
     semver "^5.3.0"
 
-istanbul-lib-report@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.0.0.tgz#d83dac7f26566b521585569367fe84ccfc7aaecb"
+istanbul-lib-report@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz#f0e55f56655ffa34222080b7a0cd4760e1405fc9"
   dependencies:
-    istanbul-lib-coverage "^1.0.2"
+    istanbul-lib-coverage "^1.1.1"
     mkdirp "^0.5.1"
     path-parse "^1.0.5"
     supports-color "^3.1.2"
 
-istanbul-lib-source-maps@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.1.1.tgz#f8c8c2e8f2160d1d91526d97e5bd63b2079af71c"
+istanbul-lib-source-maps@^1.1.0, istanbul-lib-source-maps@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.1.tgz#a6fe1acba8ce08eebc638e572e294d267008aa0c"
   dependencies:
-    istanbul-lib-coverage "^1.0.2"
+    debug "^2.6.3"
+    istanbul-lib-coverage "^1.1.1"
     mkdirp "^0.5.1"
-    rimraf "^2.4.4"
+    rimraf "^2.6.1"
     source-map "^0.5.3"
 
-istanbul-reports@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.0.2.tgz#4e8366abe6fa746cc1cd6633f108de12cc6ac6fa"
+istanbul-reports@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.1.tgz#042be5c89e175bc3f86523caab29c014e77fee4e"
   dependencies:
     handlebars "^4.0.3"
 
-jest-changed-files@^19.0.2:
-  version "19.0.2"
-  resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-19.0.2.tgz#16c54c84c3270be408e06d2e8af3f3e37a885824"
+jest-changed-files@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-20.0.3.tgz#9394d5cc65c438406149bef1bf4d52b68e03e3f8"
 
-jest-cli@^19.0.2:
-  version "19.0.2"
-  resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-19.0.2.tgz#cc3620b62acac5f2d93a548cb6ef697d4ec85443"
+jest-cli@^20.0.4:
+  version "20.0.4"
+  resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-20.0.4.tgz#e532b19d88ae5bc6c417e8b0593a6fe954b1dc93"
   dependencies:
     ansi-escapes "^1.4.0"
     callsites "^2.0.0"
-    chalk "^1.1.1"
-    graceful-fs "^4.1.6"
-    is-ci "^1.0.9"
-    istanbul-api "^1.1.0-alpha.1"
-    istanbul-lib-coverage "^1.0.0"
-    istanbul-lib-instrument "^1.1.1"
-    jest-changed-files "^19.0.2"
-    jest-config "^19.0.2"
-    jest-environment-jsdom "^19.0.2"
-    jest-haste-map "^19.0.0"
-    jest-jasmine2 "^19.0.2"
-    jest-message-util "^19.0.0"
-    jest-regex-util "^19.0.0"
-    jest-resolve-dependencies "^19.0.0"
-    jest-runtime "^19.0.2"
-    jest-snapshot "^19.0.2"
-    jest-util "^19.0.2"
+    chalk "^1.1.3"
+    graceful-fs "^4.1.11"
+    is-ci "^1.0.10"
+    istanbul-api "^1.1.1"
+    istanbul-lib-coverage "^1.0.1"
+    istanbul-lib-instrument "^1.4.2"
+    istanbul-lib-source-maps "^1.1.0"
+    jest-changed-files "^20.0.3"
+    jest-config "^20.0.4"
+    jest-docblock "^20.0.3"
+    jest-environment-jsdom "^20.0.3"
+    jest-haste-map "^20.0.4"
+    jest-jasmine2 "^20.0.4"
+    jest-message-util "^20.0.3"
+    jest-regex-util "^20.0.3"
+    jest-resolve-dependencies "^20.0.3"
+    jest-runtime "^20.0.4"
+    jest-snapshot "^20.0.3"
+    jest-util "^20.0.3"
     micromatch "^2.3.11"
-    node-notifier "^5.0.1"
+    node-notifier "^5.0.2"
+    pify "^2.3.0"
     slash "^1.0.0"
     string-length "^1.0.1"
     throat "^3.0.0"
-    which "^1.1.1"
+    which "^1.2.12"
     worker-farm "^1.3.1"
-    yargs "^6.3.0"
+    yargs "^7.0.2"
 
-jest-config@^19.0.2:
-  version "19.0.2"
-  resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-19.0.2.tgz#1b9bd2db0ddd16df61c2b10a54009e1768da6411"
+jest-config@^20.0.4:
+  version "20.0.4"
+  resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-20.0.4.tgz#e37930ab2217c913605eff13e7bd763ec48faeea"
   dependencies:
-    chalk "^1.1.1"
-    jest-environment-jsdom "^19.0.2"
-    jest-environment-node "^19.0.2"
-    jest-jasmine2 "^19.0.2"
-    jest-regex-util "^19.0.0"
-    jest-resolve "^19.0.2"
-    jest-validate "^19.0.2"
-    pretty-format "^19.0.0"
-
-jest-diff@^19.0.0:
-  version "19.0.0"
-  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-19.0.0.tgz#d1563cfc56c8b60232988fbc05d4d16ed90f063c"
+    chalk "^1.1.3"
+    glob "^7.1.1"
+    jest-environment-jsdom "^20.0.3"
+    jest-environment-node "^20.0.3"
+    jest-jasmine2 "^20.0.4"
+    jest-matcher-utils "^20.0.3"
+    jest-regex-util "^20.0.3"
+    jest-resolve "^20.0.4"
+    jest-validate "^20.0.3"
+    pretty-format "^20.0.3"
+
+jest-diff@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-20.0.3.tgz#81f288fd9e675f0fb23c75f1c2b19445fe586617"
   dependencies:
     chalk "^1.1.3"
-    diff "^3.0.0"
-    jest-matcher-utils "^19.0.0"
-    pretty-format "^19.0.0"
+    diff "^3.2.0"
+    jest-matcher-utils "^20.0.3"
+    pretty-format "^20.0.3"
 
-jest-environment-jsdom@^19.0.2:
-  version "19.0.2"
-  resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-19.0.2.tgz#ceda859c4a4b94ab35e4de7dab54b926f293e4a3"
-  dependencies:
-    jest-mock "^19.0.0"
-    jest-util "^19.0.2"
-    jsdom "^9.11.0"
+jest-docblock@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-20.0.3.tgz#17bea984342cc33d83c50fbe1545ea0efaa44712"
 
-jest-environment-node@^19.0.2:
-  version "19.0.2"
-  resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-19.0.2.tgz#6e84079db87ed21d0c05e1f9669f207b116fe99b"
+jest-environment-jsdom@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-20.0.3.tgz#048a8ac12ee225f7190417713834bb999787de99"
   dependencies:
-    jest-mock "^19.0.0"
-    jest-util "^19.0.2"
+    jest-mock "^20.0.3"
+    jest-util "^20.0.3"
+    jsdom "^9.12.0"
 
-jest-file-exists@^19.0.0:
-  version "19.0.0"
-  resolved "https://registry.yarnpkg.com/jest-file-exists/-/jest-file-exists-19.0.0.tgz#cca2e587a11ec92e24cfeab3f8a94d657f3fceb8"
+jest-environment-node@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-20.0.3.tgz#d488bc4612af2c246e986e8ae7671a099163d403"
+  dependencies:
+    jest-mock "^20.0.3"
+    jest-util "^20.0.3"
 
-jest-haste-map@^19.0.0:
-  version "19.0.0"
-  resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-19.0.0.tgz#adde00b62b1fe04432a104b3254fc5004514b55e"
+jest-haste-map@^20.0.4:
+  version "20.0.4"
+  resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-20.0.4.tgz#653eb55c889ce3c021f7b94693f20a4159badf03"
   dependencies:
     fb-watchman "^2.0.0"
-    graceful-fs "^4.1.6"
+    graceful-fs "^4.1.11"
+    jest-docblock "^20.0.3"
     micromatch "^2.3.11"
-    sane "~1.5.0"
+    sane "~1.6.0"
     worker-farm "^1.3.1"
 
-jest-jasmine2@^19.0.2:
-  version "19.0.2"
-  resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-19.0.2.tgz#167991ac825981fb1a800af126e83afcca832c73"
+jest-jasmine2@^20.0.4:
+  version "20.0.4"
+  resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-20.0.4.tgz#fcc5b1411780d911d042902ef1859e852e60d5e1"
   dependencies:
-    graceful-fs "^4.1.6"
-    jest-matcher-utils "^19.0.0"
-    jest-matchers "^19.0.0"
-    jest-message-util "^19.0.0"
-    jest-snapshot "^19.0.2"
+    chalk "^1.1.3"
+    graceful-fs "^4.1.11"
+    jest-diff "^20.0.3"
+    jest-matcher-utils "^20.0.3"
+    jest-matchers "^20.0.3"
+    jest-message-util "^20.0.3"
+    jest-snapshot "^20.0.3"
+    once "^1.4.0"
+    p-map "^1.1.1"
 
 jest-matcher-utils@^19.0.0:
   version "19.0.0"
@@ -2625,87 +2640,93 @@ jest-matcher-utils@^19.0.0:
     chalk "^1.1.3"
     pretty-format "^19.0.0"
 
-jest-matchers@^19.0.0:
-  version "19.0.0"
-  resolved "https://registry.yarnpkg.com/jest-matchers/-/jest-matchers-19.0.0.tgz#c74ecc6ebfec06f384767ba4d6fa4a42d6755754"
+jest-matcher-utils@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-20.0.3.tgz#b3a6b8e37ca577803b0832a98b164f44b7815612"
   dependencies:
-    jest-diff "^19.0.0"
-    jest-matcher-utils "^19.0.0"
-    jest-message-util "^19.0.0"
-    jest-regex-util "^19.0.0"
+    chalk "^1.1.3"
+    pretty-format "^20.0.3"
 
-jest-message-util@^19.0.0:
-  version "19.0.0"
-  resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-19.0.0.tgz#721796b89c0e4d761606f9ba8cb828a3b6246416"
+jest-matchers@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/jest-matchers/-/jest-matchers-20.0.3.tgz#ca69db1c32db5a6f707fa5e0401abb55700dfd60"
   dependencies:
-    chalk "^1.1.1"
+    jest-diff "^20.0.3"
+    jest-matcher-utils "^20.0.3"
+    jest-message-util "^20.0.3"
+    jest-regex-util "^20.0.3"
+
+jest-message-util@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-20.0.3.tgz#6aec2844306fcb0e6e74d5796c1006d96fdd831c"
+  dependencies:
+    chalk "^1.1.3"
     micromatch "^2.3.11"
+    slash "^1.0.0"
 
-jest-mock@^19.0.0:
-  version "19.0.0"
-  resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-19.0.0.tgz#67038641e9607ab2ce08ec4a8cb83aabbc899d01"
+jest-mock@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-20.0.3.tgz#8bc070e90414aa155c11a8d64c869a0d5c71da59"
 
-jest-regex-util@^19.0.0:
-  version "19.0.0"
-  resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-19.0.0.tgz#b7754587112aede1456510bb1f6afe74ef598691"
+jest-regex-util@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-20.0.3.tgz#85bbab5d133e44625b19faf8c6aa5122d085d762"
 
-jest-resolve-dependencies@^19.0.0:
-  version "19.0.0"
-  resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-19.0.0.tgz#a741ad1fa094140e64ecf2642a504f834ece22ee"
+jest-resolve-dependencies@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-20.0.3.tgz#6e14a7b717af0f2cb3667c549de40af017b1723a"
   dependencies:
-    jest-file-exists "^19.0.0"
+    jest-regex-util "^20.0.3"
 
-jest-resolve@^19.0.2:
-  version "19.0.2"
-  resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-19.0.2.tgz#5793575de4f07aec32f7d7ff0c6c181963eefb3c"
+jest-resolve@^20.0.4:
+  version "20.0.4"
+  resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-20.0.4.tgz#9448b3e8b6bafc15479444c6499045b7ffe597a5"
   dependencies:
     browser-resolve "^1.11.2"
-    jest-haste-map "^19.0.0"
-    resolve "^1.2.0"
+    is-builtin-module "^1.0.0"
+    resolve "^1.3.2"
 
-jest-runtime@^19.0.2:
-  version "19.0.2"
-  resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-19.0.2.tgz#d9a43e72de416d27d196fd9c7940d98fe6685407"
+jest-runtime@^20.0.4:
+  version "20.0.4"
+  resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-20.0.4.tgz#a2c802219c4203f754df1404e490186169d124d8"
   dependencies:
     babel-core "^6.0.0"
-    babel-jest "^19.0.0"
+    babel-jest "^20.0.3"
     babel-plugin-istanbul "^4.0.0"
     chalk "^1.1.3"
-    graceful-fs "^4.1.6"
-    jest-config "^19.0.2"
-    jest-file-exists "^19.0.0"
-    jest-haste-map "^19.0.0"
-    jest-regex-util "^19.0.0"
-    jest-resolve "^19.0.2"
-    jest-util "^19.0.2"
+    convert-source-map "^1.4.0"
+    graceful-fs "^4.1.11"
+    jest-config "^20.0.4"
+    jest-haste-map "^20.0.4"
+    jest-regex-util "^20.0.3"
+    jest-resolve "^20.0.4"
+    jest-util "^20.0.3"
     json-stable-stringify "^1.0.1"
     micromatch "^2.3.11"
     strip-bom "3.0.0"
-    yargs "^6.3.0"
+    yargs "^7.0.2"
 
-jest-snapshot@^19.0.2:
-  version "19.0.2"
-  resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-19.0.2.tgz#9c1b216214f7187c38bfd5c70b1efab16b0ff50b"
+jest-snapshot@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-20.0.3.tgz#5b847e1adb1a4d90852a7f9f125086e187c76566"
   dependencies:
     chalk "^1.1.3"
-    jest-diff "^19.0.0"
-    jest-file-exists "^19.0.0"
-    jest-matcher-utils "^19.0.0"
-    jest-util "^19.0.2"
+    jest-diff "^20.0.3"
+    jest-matcher-utils "^20.0.3"
+    jest-util "^20.0.3"
     natural-compare "^1.4.0"
-    pretty-format "^19.0.0"
+    pretty-format "^20.0.3"
 
-jest-util@^19.0.2:
-  version "19.0.2"
-  resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-19.0.2.tgz#e0a0232a2ab9e6b2b53668bdb3534c2b5977ed41"
+jest-util@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-20.0.3.tgz#0c07f7d80d82f4e5a67c6f8b9c3fe7f65cfd32ad"
   dependencies:
-    chalk "^1.1.1"
-    graceful-fs "^4.1.6"
-    jest-file-exists "^19.0.0"
-    jest-message-util "^19.0.0"
-    jest-mock "^19.0.0"
-    jest-validate "^19.0.2"
-    leven "^2.0.0"
+    chalk "^1.1.3"
+    graceful-fs "^4.1.11"
+    jest-message-util "^20.0.3"
+    jest-mock "^20.0.3"
+    jest-validate "^20.0.3"
+    leven "^2.1.0"
     mkdirp "^0.5.1"
 
 jest-validate@19.0.0:
@@ -2717,20 +2738,20 @@ jest-validate@19.0.0:
     leven "^2.0.0"
     pretty-format "^19.0.0"
 
-jest-validate@^19.0.2:
-  version "19.0.2"
-  resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-19.0.2.tgz#dc534df5f1278d5b63df32b14241d4dbf7244c0c"
+jest-validate@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-20.0.3.tgz#d0cfd1de4f579f298484925c280f8f1d94ec3cab"
   dependencies:
-    chalk "^1.1.1"
-    jest-matcher-utils "^19.0.0"
-    leven "^2.0.0"
-    pretty-format "^19.0.0"
+    chalk "^1.1.3"
+    jest-matcher-utils "^20.0.3"
+    leven "^2.1.0"
+    pretty-format "^20.0.3"
 
-jest@^19.0.2:
-  version "19.0.2"
-  resolved "https://registry.yarnpkg.com/jest/-/jest-19.0.2.tgz#b794faaf8ff461e7388f28beef559a54f20b2c10"
+jest@^20.0.4:
+  version "20.0.4"
+  resolved "https://registry.yarnpkg.com/jest/-/jest-20.0.4.tgz#3dd260c2989d6dad678b1e9cc4d91944f6d602ac"
   dependencies:
-    jest-cli "^19.0.2"
+    jest-cli "^20.0.4"
 
 jodid25519@^1.0.0:
   version "1.0.2"
@@ -2779,7 +2800,7 @@ jscodeshift@~0.3.28:
     temp "^0.8.1"
     write-file-atomic "^1.2.0"
 
-jsdom@^9.11.0:
+jsdom@^9.12.0:
   version "9.12.0"
   resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-9.12.0.tgz#e8c546fffcb06c00d4833ca84410fed7f8a097d4"
   dependencies:
@@ -2880,7 +2901,7 @@ leven@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3"
 
-leven@^2.0.0:
+leven@^2.0.0, leven@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
 
@@ -3072,7 +3093,7 @@ node-int64@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
 
-node-notifier@^5.0.1:
+node-notifier@^5.0.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.1.2.tgz#2fa9e12605fa10009d44549d6fcd8a63dde0e4ff"
   dependencies:
@@ -3240,6 +3261,10 @@ p-locate@^2.0.0:
   dependencies:
     p-limit "^1.1.0"
 
+p-map@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a"
+
 package-json@^2.0.0:
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/package-json/-/package-json-2.4.0.tgz#0d15bd67d1cbbddbb2ca222ff2edb86bcb31a8bb"
@@ -3310,7 +3335,7 @@ performance-now@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
 
-pify@^2.0.0:
+pify@^2.0.0, pify@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
 
@@ -3373,6 +3398,13 @@ pretty-format@^19.0.0:
   dependencies:
     ansi-styles "^3.0.0"
 
+pretty-format@^20.0.3:
+  version "20.0.3"
+  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-20.0.3.tgz#020e350a560a1fe1a98dc3beb6ccffb386de8b14"
+  dependencies:
+    ansi-regex "^2.1.1"
+    ansi-styles "^3.0.0"
+
 private@^0.1.6, private@~0.1.5:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"
@@ -3663,7 +3695,7 @@ resolve@1.1.7:
   version "1.1.7"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
 
-resolve@^1.1.6, resolve@^1.2.0:
+resolve@^1.1.6, resolve@^1.3.2:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.2.tgz#1f0442c9e0cbb8136e87b9305f932f46c7f28235"
   dependencies:
@@ -3682,12 +3714,18 @@ right-align@^0.1.1:
   dependencies:
     align-text "^0.1.1"
 
-rimraf@2, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@~2.5.1, rimraf@~2.5.4:
+rimraf@2, rimraf@^2.2.8, rimraf@~2.5.1, rimraf@~2.5.4:
   version "2.5.4"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04"
   dependencies:
     glob "^7.0.5"
 
+rimraf@^2.6.1:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d"
+  dependencies:
+    glob "^7.0.5"
+
 rimraf@~2.2.6:
   version "2.2.8"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582"
@@ -3716,9 +3754,9 @@ safe-buffer@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
 
-sane@~1.5.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/sane/-/sane-1.5.0.tgz#a4adeae764d048621ecb27d5f9ecf513101939f3"
+sane@~1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/sane/-/sane-1.6.0.tgz#9610c452307a135d29c1fdfe2547034180c46775"
   dependencies:
     anymatch "^1.3.0"
     exec-sh "^0.2.0"
@@ -4217,7 +4255,7 @@ which-module@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
 
-which@^1.1.1, which@^1.2.12, which@^1.2.8:
+which@^1.2.12, which@^1.2.8:
   version "1.2.12"
   resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192"
   dependencies:
@@ -4283,7 +4321,6 @@ write@^0.2.1:
   dependencies:
     mkdirp "^0.5.1"
 
-
 xdg-basedir@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2"
@@ -4306,15 +4343,15 @@ yallist@^2.0.0:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
 
-yargs-parser@^4.2.0:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c"
+yargs-parser@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"
   dependencies:
     camelcase "^3.0.0"
 
-yargs@^6.3.0:
-  version "6.6.0"
-  resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208"
+yargs@^7.0.2:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
   dependencies:
     camelcase "^3.0.0"
     cliui "^3.2.0"
@@ -4328,7 +4365,7 @@ yargs@^6.3.0:
     string-width "^1.0.2"
     which-module "^1.0.0"
     y18n "^3.2.1"
-    yargs-parser "^4.2.0"
+    yargs-parser "^5.0.0"
 
 yargs@~3.10.0:
   version "3.10.0"


Test patch:
diff --git a/src/cli/transformers.test.js b/src/cli/transformers.test.js
index db9dcdc..ae06c4a 100644
--- a/src/cli/transformers.test.js
+++ b/src/cli/transformers.test.js
@@ -39,6 +39,20 @@ it('supports jscodeshift flags', () => {
     );
 });
 
+it('supports jscodeshift custom arguments', () => {
+    execaReturnValue = { error: null };
+    console.log = jest.fn();
+    executeTransformations(
+        'folder',
+        { dry: true, parser: 'flow' },
+        ['ava'],
+        ['--standaloneMode']
+    );
+    expect(console.log).toBeCalledWith(
+        `Executing command: jscodeshift -t ${path.join(transformerDirectory, 'ava.js')} folder --dry --parser flow --standaloneMode`
+    );
+});
+
 it('rethrows jscodeshift errors', () => {
     const transformerError = new Error('bum');
     execaReturnValue = { error: transformerError };
diff --git a/src/transformers/ava.test.js b/src/transformers/ava.test.js
index cd8e429..4b80086 100644
--- a/src/transformers/ava.test.js
+++ b/src/transformers/ava.test.js
@@ -69,7 +69,6 @@ test('mapping', (t) => {
   t.ifError(abc)
   t.error(abc)
   t.plan(3)
-  t.snapshot(abc)
 })
 `,
     `
@@ -98,7 +97,6 @@ test('mapping', () => {
   expect(abc).toBeFalsy()
   expect(abc).toBeFalsy()
   expect.assertions(3)
-  expect(abc).toMatchSnapshot()
 });
 `
 );
diff --git a/src/transformers/expect.test.js b/src/transformers/expect.test.js
new file mode 100644
index 0000000..04be848
--- /dev/null
+++ b/src/transformers/expect.test.js
@@ -0,0 +1,557 @@
+/* eslint-env jest */
+import chalk from 'chalk';
+import { wrapPlugin } from '../utils/test-helpers';
+import plugin from './expect';
+
+chalk.enabled = false;
+const wrappedPlugin = wrapPlugin(plugin);
+
+let consoleWarnings = [];
+beforeEach(() => {
+    consoleWarnings = [];
+    console.warn = v => consoleWarnings.push(v);
+});
+
+function testChanged(msg, source, expectedOutput, options = {}) {
+    test(msg, () => {
+        const result = wrappedPlugin(source, options);
+        expect(result).toBe(expectedOutput);
+        expect(consoleWarnings).toEqual([]);
+
+        // Running it twice should yield same result
+        expect(wrappedPlugin(result, options)).toBe(result);
+    });
+}
+
+testChanged(
+    'does not touch code without expect require/import',
+    `
+    const test = require("testlib");
+    test(t => {
+      expect(stuff).toExist();
+    })
+    `,
+    `
+    const test = require("testlib");
+    test(t => {
+      expect(stuff).toExist();
+    })
+    `
+);
+
+testChanged(
+    'maps expect matchers',
+    `
+    import expect from 'expect';
+
+    test(() => {
+      expect(stuff).toExist();
+      expect(stuff).toExist('message');
+      expect(stuff).toBeTruthy();
+      expect(stuff).toBeTruthy('message');
+
+      expect(stuff).toNotExist();
+      expect(stuff).toNotExist('message');
+      expect(stuff).toBeFalsy();
+      expect(stuff).toBeFalsy('message');
+
+      expect(stuff).toBe(null);
+      expect(stuff).toBe(null, 'message');
+
+      expect(stuff).toNotBe(42);
+      expect(stuff).toNotBe(42, 'message');
+
+      expect(stuff).toNotEqual({}, 'message');
+      expect(stuff).toNotEqual(42);
+
+      expect(stuff).toThrow();
+      expect(stuff).toThrow(/bum/);
+      expect(stuff).toThrow(/bum/, 'yes sir');
+
+      expect(stuff).toNotThrow();
+      expect(stuff).toNotThrow(/bum/);
+      expect(stuff).toNotThrow(/bum/, 'yes sir');
+
+      expect(stuff).toBeA('number');
+      expect(stuff).toBeA('number', 'message');
+
+      expect(stuff).toBeAn(Array);
+      expect(new Stuff).toBeAn(Stuff, 'Message');
+
+      expect(stuff).toNotBeA(Number);
+      expect(stuff).toNotBeAn(Array);
+
+      expect(stuff).toMatch({foo: 'bar'});
+      expect(stuff).toMatch({foo: 'bar'}, 'message');
+      expect(stuff).toMatch('a string');
+      expect(stuff).toMatch('a string', 'message');
+
+      expect(stuff).toNotMatch({foo: 'bar'});
+      expect(stuff).toNotMatch({foo: 'bar'}, 'message');
+      expect(stuff).toNotMatch('a string');
+      expect(stuff).toNotMatch('a string', 'message');
+
+      expect(stuff).toHaveBeenCalled();
+      expect(stuff).toNotHaveBeenCalled();
+      expect(stuff).toNotHaveBeenCalled('msg');
+      expect(stuff).toHaveBeenCalledWith('foo', 'bar');
+    });
+    `,
+    `
+    test(() => {
+      expect(stuff).toBeTruthy();
+      expect(stuff).toBeTruthy();
+      expect(stuff).toBeTruthy();
+      expect(stuff).toBeTruthy();
+
+      expect(stuff).toBeFalsy();
+      expect(stuff).toBeFalsy();
+      expect(stuff).toBeFalsy();
+      expect(stuff).toBeFalsy();
+
+      expect(stuff).toBe(null);
+      expect(stuff).toBe(null);
+
+      expect(stuff).not.toBe(42);
+      expect(stuff).not.toBe(42);
+
+      expect(stuff).not.toEqual({});
+      expect(stuff).not.toEqual(42);
+
+      expect(stuff).toThrow();
+      expect(stuff).toThrow(/bum/);
+      expect(stuff).toThrow(/bum/);
+
+      expect(stuff).not.toThrow();
+      expect(stuff).not.toThrow(/bum/);
+      expect(stuff).not.toThrow(/bum/);
+
+      expect(typeof stuff).toBe('number');
+      expect(typeof stuff).toBe('number');
+
+      expect(stuff).toBeInstanceOf(Array);
+      expect(new Stuff).toBeInstanceOf(Stuff);
+
+      expect(stuff).not.toBeInstanceOf(Number);
+      expect(stuff).not.toBeInstanceOf(Array);
+
+      expect(stuff).toMatchObject({foo: 'bar'});
+      expect(stuff).toMatchObject({foo: 'bar'});
+      expect(stuff).toMatch('a string');
+      expect(stuff).toMatch('a string');
+
+      expect(stuff).not.toMatchObject({foo: 'bar'});
+      expect(stuff).not.toMatchObject({foo: 'bar'});
+      expect(stuff).not.toMatch('a string');
+      expect(stuff).not.toMatch('a string');
+
+      expect(stuff).toHaveBeenCalled();
+      expect(stuff).not.toHaveBeenCalled();
+      expect(stuff).not.toHaveBeenCalled();
+      expect(stuff).toHaveBeenCalledWith('foo', 'bar');
+    });
+    `
+);
+
+testChanged(
+    'maps expect number matchers',
+    `
+    import expect from 'expect';
+
+    test(() => {
+      expect(stuff).toBeLessThan(42);
+      expect(stuff).toBeFewerThan(42);
+      expect(stuff).toBeLessThanOrEqualTo(42);
+      expect(stuff).toBeMoreThan(42);
+      expect(stuff).toBeGreaterThanOrEqualTo(42);
+    });
+    `,
+    `
+    test(() => {
+      expect(stuff).toBeLessThan(42);
+      expect(stuff).toBeLessThan(42);
+      expect(stuff).toBeLessThanOrEqual(42);
+      expect(stuff).toBeGreaterThan(42);
+      expect(stuff).toBeGreaterThanOrEqual(42);
+    });
+    `
+);
+
+testChanged(
+    'maps expect contain matchers',
+    `
+    import expect from 'expect';
+
+    test(() => {
+      expect(stuff).toInclude(1);
+      expect(stuff).toInclude(1, 'msg');
+      expect(stuff).toExclude('world');
+      expect(stuff).toNotContain({b: 2});
+      expect(stuff).toNotInclude('world');
+
+      expect({ a: 1 }).toIncludeKey('a');
+      expect({ a: 1 }).toExcludeKey('b');
+
+      expect({ a: 1, b: 2 }).toIncludeKeys([ 'a', 'b' ]);
+      expect({ a: 1, b: 2 }).toExcludeKeys([ 'c', 'd' ]);
+    });
+    `,
+    `
+    test(() => {
+      expect(stuff).toContain(1);
+      expect(stuff).toContain(1);
+      expect(stuff).not.toContain('world');
+      expect(stuff).not.toContain({b: 2});
+      expect(stuff).not.toContain('world');
+
+      expect(Object.keys({ a: 1 })).toContain('a');
+      expect(Object.keys({ a: 1 })).not.toContain('b');
+
+      [ 'a', 'b' ].forEach(e => {
+        expect({ a: 1, b: 2 }).toContain(e);
+      });
+      [ 'c', 'd' ].forEach(e => {
+        expect({ a: 1, b: 2 }).not.toContain(e);
+      });
+    });
+    `
+);
+
+testChanged(
+    'maps expect spy matchers',
+    `
+    import expect from 'expect';
+
+    test(() => {
+      expect(stuff).toHaveBeenCalled();
+      expect(stuff).toNotHaveBeenCalled();
+      expect(stuff).toHaveBeenCalledWith('foo', 'bar');
+    });
+    `,
+    `
+    test(() => {
+      expect(stuff).toHaveBeenCalled();
+      expect(stuff).not.toHaveBeenCalled();
+      expect(stuff).toHaveBeenCalledWith('foo', 'bar');
+    });
+    `
+);
+
+testChanged(
+    'maps spy creation calls',
+    `
+    import expect from 'expect';
+
+    test(() => {
+      var spy1 = expect.createSpy();
+      var spy2 = expect.spyOn(video, 'play');
+      var spyOn;
+
+      spyOn = expect.spyOn(video, 'play');
+      spyOn = expect.spyOn(video, 'play').andCall(fn);
+      spyOn = expect.spyOn(video, 'play').andReturn(42);
+      spyOn = expect.spyOn(video, 'play').andThrow(new Error('bum'));
+
+      expect.createSpy().andCall(fn);
+      expect.createSpy().andReturn(value);
+
+      not.a.spy.andCall(fn);
+    });
+    `,
+    `
+    test(() => {
+      var spy1 = jest.fn();
+      var spy2 = jest.spyOn(video, 'play');
+      var spyOn;
+
+      spyOn = jest.spyOn(video, 'play');
+      spyOn = jest.spyOn(video, 'play').mockImplementation(fn);
+      spyOn = jest.spyOn(video, 'play').mockImplementation(() => 42);
+      spyOn = jest.spyOn(video, 'play').mockImplementation(() => {
+        throw new Error('bum');
+      });
+
+      jest.fn().mockImplementation(fn);
+      jest.fn().mockImplementation(() => value);
+
+      not.a.spy.andCall(fn);
+    });
+    `
+);
+
+testChanged(
+    'maps spy methods on intitialized spies',
+    `
+    import expect from 'expect';
+
+    test(() => {
+      var spy1 = expect.createSpy();
+      var spy2 = expect.spyOn(video, 'play');
+      var spy3 = expect.spyOn(video, 'play');
+
+      spy1.andCall(fn);
+      spy2.andReturn(42);
+      spy3.andThrow(new Error('bum'));
+
+      not.a.spy.andCall(fn);
+      not.a.spy.andReturn(fn);
+      not.a.spy.andThrow(fn);
+
+      expect(spy1.calls.length).toBe(2);
+      expect(spy2.calls[i].arguments[j]).toBe('yes');
+
+      spy1.restore();
+      spy2.reset();
+    });
+    `,
+    `
+    test(() => {
+      var spy1 = jest.fn();
+      var spy2 = jest.spyOn(video, 'play');
+      var spy3 = jest.spyOn(video, 'play');
+
+      spy1.mockImplementation(fn);
+      spy2.mockImplementation(() => 42);
+      spy3.mockImplementation(() => {
+        throw new Error('bum');
+      });
+
+      not.a.spy.andCall(fn);
+      not.a.spy.andReturn(fn);
+      not.a.spy.andThrow(fn);
+
+      expect(spy1.mock.calls.length).toBe(2);
+      expect(spy2.mock.calls[i][j]).toBe('yes');
+
+      spy1.mockReset();
+      spy2.mockClear();
+    });
+    `
+);
+
+testChanged(
+    'renames non standard expect import name',
+    `
+    import exp from 'expect';
+
+    test(() => {
+      exp(stuff).toHaveBeenCalled();
+      exp(stuff).toNotHaveBeenCalled();
+      exp(stuff).toHaveBeenCalledWith(1);
+    });
+    `,
+    `
+    test(() => {
+      expect(stuff).toHaveBeenCalled();
+      expect(stuff).not.toHaveBeenCalled();
+      expect(stuff).toHaveBeenCalledWith(1);
+    });
+    `
+);
+
+testChanged(
+    'standaloneMode: keeps expect import',
+    `
+    import exp from 'expect';
+
+    test(() => {
+      exp(stuff).toHaveBeenCalled();
+    });
+    `,
+    `
+    import exp from 'expect';
+
+    test(() => {
+      exp(stuff).toHaveBeenCalled();
+    });
+    `,
+    {
+        standaloneMode: true,
+    }
+);
+
+testChanged(
+    'standaloneMode: rewrites expect.spyOn (import)',
+    `
+    import expect from 'expect';
+
+    test(() => {
+        var spy1 = expect.createSpy();
+        var spy2 = expect.spyOn(video, 'play');
+
+        spy1.restore();
+        spy1.reset();
+        expect(spy1.calls.length).toEqual(3);
+    });
+    `,
+    `
+    import mock from 'jest-mock';
+    import expect from 'expect';
+
+    test(() => {
+        var spy1 = mock.fn();
+        var spy2 = mock.spyOn(video, 'play');
+
+        spy1.mockReset();
+        spy1.mockClear();
+        expect(spy1.mock.calls.length).toEqual(3);
+    });
+    `,
+    {
+        standaloneMode: true,
+    }
+);
+
+testChanged(
+    'standaloneMode: rewrites expect.spyOn (require)',
+    `
+    const expect = require('expect');
+
+    test(() => {
+        var spy1 = expect.createSpy();
+        var spy2 = expect.spyOn(video, 'play');
+        expect(spy1.calls.length).toEqual(3);
+    });
+    `,
+    `
+    const mock = require('jest-mock');
+    const expect = require('expect');
+
+    test(() => {
+        var spy1 = mock.fn();
+        var spy2 = mock.spyOn(video, 'play');
+        expect(spy1.mock.calls.length).toEqual(3);
+    });
+    `,
+    {
+        standaloneMode: true,
+    }
+);
+
+test('warns about chaining', () => {
+    wrappedPlugin(
+        `
+        import expect from 'expect';
+
+        test(() => {
+          expect(3.14)
+            .toExist()
+            .toBeLessThan(4)
+            .toBeGreaterThan(3);
+        });
+    `
+    );
+    expect(consoleWarnings).toEqual([
+        'jest-codemods warning: (test.js line 5) Chaining expect matchers is currently not supported',
+    ]);
+});
+
+test('warns about unsupported spy features', () => {
+    wrappedPlugin(
+        `
+        import expect from 'expect';
+
+        test(() => {
+          expect.restoreSpies();
+          const spy = expect.spyOn(video, 'play').andCallThrough(fn);
+          expect(expect.isSpy(spy)).toBe(true);
+        });
+    `
+    );
+    expect(consoleWarnings).toEqual([
+        'jest-codemods warning: (test.js line 5) "restoreSpies" is currently not supported',
+        'jest-codemods warning: (test.js line 7) "isSpy" is currently not supported',
+        'jest-codemods warning: (test.js line 6) "andCallThrough" is currently not supported',
+    ]);
+});
+
+test('warns about spy features without expect', () => {
+    wrappedPlugin(
+        `
+        import expect, { createSpy, spyOn } from 'expect'
+
+        test(() => {
+          var spy1 = createSpy();
+          var spy2 = spyOn(video, 'play');
+        });
+    `
+    );
+    expect(consoleWarnings).toEqual([
+        'jest-codemods warning: (test.js line 5) "createSpy" is currently not supported ' +
+            '(use "expect.createSpy" instead for transformation to work)',
+        'jest-codemods warning: (test.js line 6) "spyOn" is currently not supported ' +
+            '(use "expect.spyOn" instead for transformation to work)',
+    ]);
+});
+
+test('warns about creating spies without assigning it to a variable', () => {
+    wrappedPlugin(
+        `
+        import expect from 'expect'
+
+        test(() => {
+            expect.spyOn(console, 'error');
+            expect(console.error.calls.length).toEqual(0);
+        });
+    `
+    );
+    expect(consoleWarnings).toEqual([
+        'jest-codemods warning: (test.js line 5) "spyOn" without variable assignment might not work as expected (see https://facebook.github.io/jest/docs/jest-object.html#jestspyonobject-methodname)',
+    ]);
+});
+
+test('warns about toMatch usage on variables', () => {
+    const result = wrappedPlugin(
+        `
+        import expect from 'expect'
+
+        test(() => {
+          expect(stuff).toMatch(variable);
+          expect(stuff).toNotMatch(variable);
+        });
+        `
+    );
+
+    expect(result).toEqual(
+        `
+        test(() => {
+          expect(stuff).toMatchObject(variable);
+          expect(stuff).not.toMatchObject(variable);
+        });
+        `
+    );
+
+    expect(consoleWarnings).toEqual([
+        'jest-codemods warning: (test.js line 5) Use "toMatch" if "variable" is not an object',
+        'jest-codemods warning: (test.js line 6) Use "toMatch" if "variable" is not an object',
+    ]);
+});
+
+test('warns about unsupported number of arguments (comparator)', () => {
+    wrappedPlugin(
+        `
+        import expect from 'expect';
+
+        test(() => {
+            expect({ a: 1, b: 2 }).toContainKey([ 'a', 'b' ], myFunc, 'msg');
+            expect({ a: 1, b: 2 }).toContain({ b: 2 }, myComparator);
+            expect({ a: 1, b: 2 }).toExclude([ 'a', 'b' ], myFunc);
+            expect({ a: 1, b: 2 }).toExcludeKey([ 'a', 'b' ], myFunc);
+            expect({ a: 1, b: 2 }).toInclude({ b: 2 }, myComparator);
+            expect({ a: 1, b: 2 }).toIncludeKeys([ 'a', 'b' ], () => {});
+            expect({ a: 1, b: 2 }).toNotContain([ 'a', 'b' ], myFunc, 'msg');
+            expect({ a: 1, b: 2 }).toNotContainKey([ 'a', 'b' ], myFunc);
+            expect({ a: 1, b: 2 }).toNotInclude([ 'a', 'b' ], myFunc, 'msg');
+            expect({ a: 1, b: 2 }).toNotIncludeKey([ 'a', 'b' ], myFunc);
+        });
+    `
+    );
+    const firstErrorLine = 5;
+    const numberOfErrors = 10;
+    expect(consoleWarnings).toEqual(
+        [...Array(numberOfErrors).keys()].map(
+            e =>
+                `jest-codemods warning: (test.js line ${e + firstErrorLine}) Too many arguments given to "toContain". Expected max 1 but got 2`
+        )
+    );
+});
diff --git a/src/transformers/tape.test.js b/src/transformers/tape.test.js
index 4a39329..de319b6 100644
--- a/src/transformers/tape.test.js
+++ b/src/transformers/tape.test.js
@@ -164,21 +164,6 @@ test(done => {
 `
 );
 
-testChanged(
-    'preserves quote style',
-    `
-import test from "tape";
-test("mytest", t => {
-    t.ok("msg");
-});
-`,
-    `
-test("mytest", () => {
-    expect("msg").toBeTruthy();
-});
-`
-);
-
 testChanged(
     'rewriting non standard naming of test function and t argument',
     `
@@ -477,20 +462,6 @@ test('not supported warnings: non standard argument for test.skip', () => {
     ]);
 });
 
-test('warns about some conflicting packages', () => {
-    wrappedPlugin(
-        `
-        import test from 'tape';
-        import proxyquire from 'proxyquire';
-        import testdouble from 'testdouble';
-        test(t => {});
-    `
-    );
-    expect(consoleWarnings).toEqual([
-        'jest-codemods warning: (test.js) Usage of package "testdouble" might be incompatible with Jest',
-    ]);
-});
-
 test('graciously warns about unknown destructured assertions', () => {
     wrappedPlugin(
         `
diff --git a/src/utils/finale.test.js b/src/utils/finale.test.js
new file mode 100644
index 0000000..510c68b
--- /dev/null
+++ b/src/utils/finale.test.js
@@ -0,0 +1,139 @@
+/* eslint-env jest */
+import chalk from 'chalk';
+import jscodeshift from 'jscodeshift';
+
+import finale from './finale';
+
+chalk.enabled = false;
+
+let consoleWarnings = [];
+beforeEach(() => {
+    consoleWarnings = [];
+    console.warn = v => consoleWarnings.push(v);
+});
+
+function testChanged(msg, source, expectedOutput, options = {}, expectedErrors = []) {
+    test(msg, () => {
+        const j = jscodeshift;
+        const ast = j(source);
+        const result = finale({ path: 'test.js' }, j, ast, options);
+        expect(result).toBe(expectedOutput);
+        expect(consoleWarnings).toEqual(expectedErrors);
+    });
+}
+
+testChanged(
+    'preserves quote style (double)',
+    `
+    import test from "tape";
+    test("mytest", t => {
+        t.ok("msg");
+    });
+    `,
+    `
+    import test from "tape";
+    test("mytest", t => {
+        t.ok("msg");
+    });
+    `
+);
+
+testChanged(
+    'preserves quote style (single)',
+    `
+    import test from 'tape';
+    test('mytest', t => {
+        t.ok('msg');
+    });
+    `,
+    `
+    import test from 'tape';
+    test('mytest', t => {
+        t.ok('msg');
+    });
+    `
+);
+
+testChanged(
+    'standaloneMode: rewrites spy calls (import)',
+    `
+    // @flow
+    import expect from 'expect';
+
+    test(() => {
+        var spy1 = jest.fn();
+        var spy2 = jest.spyOn(video, 'play');
+        jest.spyOn(video, 'play');
+        expect(1).toBe(1);
+    });
+    `,
+    `
+    import mock from 'jest-mock';
+    // @flow
+    import expect from 'expect';
+
+    test(() => {
+        var spy1 = mock.fn();
+        var spy2 = mock.spyOn(video, 'play');
+        mock.spyOn(video, 'play');
+        expect(1).toBe(1);
+    });
+    `,
+    {
+        standaloneMode: true,
+    }
+);
+
+testChanged(
+    'standaloneMode: add expect import when standaloneMode',
+    `
+    test(() => {
+        expect(1).toBe(1);
+    });
+    `,
+    `
+    import expect from 'expect';
+    test(() => {
+        expect(1).toBe(1);
+    });
+    `,
+    {
+        standaloneMode: true,
+    }
+);
+
+testChanged(
+    'standaloneMode: adds expect and jest-mock imports when standaloneMode',
+    `
+    test(() => {
+        var spy1 = jest.fn();
+        expect(1).toBe(1);
+    });
+    `,
+    `
+    import mock from 'jest-mock';
+    import expect from 'expect';
+    test(() => {
+        var spy1 = mock.fn();
+        expect(1).toBe(1);
+    });
+    `,
+    {
+        standaloneMode: true,
+    }
+);
+
+const unsupportedExample = `
+    import sinon from 'sinon';
+    import testdouble from 'testdouble';
+`;
+testChanged(
+    'warns about incompatible packages',
+    unsupportedExample,
+    unsupportedExample,
+    {},
+    [
+        'jest-codemods warning: (test.js) Usage of package "sinon" might be incompatible with Jest',
+        'jest-codemods warning: (test.js) Usage of package "testdouble" might be incompatible with Jest',
+    ]
+);
diff --git a/src/utils/test-helpers.js b/src/utils/test-helpers.js
index 046960b..8ef7ac5 100644
--- a/src/utils/test-helpers.js
+++ b/src/utils/test-helpers.js
@@ -8,10 +8,10 @@ export function api() {
     };
 }
 
-export function runPlugin(plugin, source) {
-    return plugin({ source, path: 'test.js' }, api());
+export function runPlugin(plugin, source, options = {}) {
+    return plugin({ source, path: 'test.js' }, api(), options);
 }
 
 export function wrapPlugin(plugin) {
-    return source => runPlugin(plugin, source);
+    return (source, options) => runPlugin(plugin, source, options);
 }