Skip to content

Commit 6a8c7fb

Browse files
pvolokFacebook Github Bot 0
authored andcommitted
Hoist jest.mock with 2 arguments.
Summary:Comes from jestjs#796 How about throwing if second argument references outside variables? So we will be able to allow referencing later. edit by cpojer: bypass-lint internal eslint invariant-rule is silly Closes jestjs#826 Differential Revision: D3096884 fb-gh-sync-id: 7fe7b24547f11046d7087e81f3c88d2f46097cba fbshipit-source-id: 7fe7b24547f11046d7087e81f3c88d2f46097cba
1 parent 64bed98 commit 6a8c7fb

File tree

7 files changed

+216
-41
lines changed

7 files changed

+216
-41
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
* Clear the terminal window when using `--watch`.
1313
* By default, `--watch` will now only runs tests related to changed files.
1414
`--watch=all` can be used to run all tests on file system changes.
15+
* Added the `jest.mock('moduleName', moduleFactory)` feature. `jest.mock` now
16+
gets hoisted by default. `jest.doMock` was added to explicitly mock a module
17+
without the hoisting feature of `babel-jest`.
1518

1619
## jest-cli 0.9.2, babel-jest 9.0.3
1720

docs/API.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ next: troubleshooting
1313
- [`jest.currentTestPath()`](#jest-currenttestpath)
1414
- [`jest.disableAutomock()`](#jest-disableautomock)
1515
- [`jest.enableAutomock()`](#jest-enableautomock)
16-
- [`jest.fn(implementation?)`](#jest-fn-implementation)
16+
- [`jest.fn(?implementation)`](#jest-fn-implementation)
1717
- [`jest.genMockFromModule(moduleName)`](#jest-genmockfrommodule-modulename)
18-
- [`jest.mock(moduleName)`](#jest-mock-modulename)
18+
- [`jest.mock(moduleName, ?factory)`](#jest-mock-modulename-factory)
1919
- [`jest.runAllTicks()`](#jest-runallticks)
2020
- [`jest.runAllTimers()`](#jest-runalltimers)
2121
- [`jest.runOnlyPendingTimers()`](#jest-runonlypendingtimers)
@@ -136,7 +136,7 @@ Re-enables automatic mocking in the module loader.
136136

137137
*Note: this method was previously called `autoMockOn`. When using `babel-jest`, calls to `enableAutomock` will automatically be hoisted to the top of the code block. Use `autoMockOn` if you want to explicitly avoid this behavior.*
138138

139-
### `jest.fn(implementation?)`
139+
### `jest.fn(?implementation)`
140140
Returns a new, unused [mock function](#mock-functions). Optionally takes a mock
141141
implementation.
142142

@@ -155,10 +155,27 @@ Given the name of a module, use the automatic mocking system to generate a mocke
155155

156156
This is useful when you want to create a [manual mock](/jest/docs/manual-mocks.html) that extends the automatic mock's behavior.
157157

158-
### `jest.mock(moduleName)`
158+
### `jest.mock(moduleName, ?factory)`
159159
Indicates that the module system should always return a mocked version of the specified module from `require()` (e.g. that it should never return the real module).
160160

161-
This is normally useful under the circumstances where you have called [`jest.autoMockOff()`](#jest-automockoff), but still wish to specify that certain particular modules should be mocked by the module system.
161+
```js
162+
jest.mock('moduleName');
163+
164+
const moduleName = require('moduleName'); // moduleName will be explicitly mocked
165+
```
166+
167+
The second argument can be used to specify an explicit module factory that is being run instead of using Jest's automocking feature:
168+
169+
```js
170+
jest.mock('moduleName', () => {
171+
return jest.fn(() => 42);
172+
});
173+
174+
const moduleName = require('moduleName'); // This runs the function specified as second argument to `jest.mock`.
175+
moduleName(); // Will return "42";
176+
```
177+
178+
*Note: When using `babel-jest`, calls to `mock` will automatically be hoisted to the top of the code block. Use `doMock` if you want to explicitly avoid this behavior.*
162179

163180
### `jest.runAllTicks()`
164181
Exhausts the **micro**-task queue (usually interfaced in node via `process.nextTick`).
@@ -186,6 +203,8 @@ On occasion there are times where the automatically generated mock the module sy
186203

187204
In these rare scenarios you can use this API to manually fill the slot in the module system's mock-module registry.
188205

206+
*Note It is recommended to use [`jest.mock()`](#jest-mock-modulename-factory) instead. The `jest.mock` API's second argument is a module factory instead of the expected exported module object.*
207+
189208
### `jest.unmock(moduleName)`
190209
Indicates that the module system should never return a mocked version of the specified module from `require()` (e.g. that it should always return the real module).
191210

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
3+
*
4+
* This source code is licensed under the BSD-style license found in the
5+
* LICENSE file in the root directory of this source tree. An additional grant
6+
* of patent rights can be found in the PATENTS file in the same directory.
7+
*/
8+
9+
'use strict';
10+
11+
export default () => 'unmocked';

packages/babel-plugin-jest-hoist/src/__tests__/integration-automock-off-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import b from '../__test_modules__/b';
1717
jest.disableAutomock();
1818
jest.mock('../__test_modules__/b');
1919

20-
describe('babel-plugin-jest-unmock', () => {
20+
describe('babel-plugin-jest-hoist', () => {
2121
it('hoists disableAutomock call before imports', () => {
2222
expect(a._isMockFunction).toBe(undefined);
2323
});

packages/babel-plugin-jest-hoist/src/__tests__/integration-test.js

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,38 @@ import a from '../__test_modules__/a';
1717
import b from '../__test_modules__/b';
1818
import c from '../__test_modules__/c';
1919
import d from '../__test_modules__/d';
20+
import e from '../__test_modules__/e';
2021

2122
// These will all be hoisted above imports
2223
jest.unmock('react');
2324
jest.unmock('../__test_modules__/Unmocked');
2425
jest
2526
.unmock('../__test_modules__/c')
2627
.unmock('../__test_modules__/d');
28+
jest.mock('../__test_modules__/e', () => {
29+
if (!global.CALLS) {
30+
global.CALLS = 0;
31+
}
32+
global.CALLS++;
33+
34+
return {
35+
_isMock: true,
36+
fn: () => {
37+
// The `jest.mock` transform will allow require, built-ins and globals.
38+
const path = require('path');
39+
const array = new Array(3);
40+
array[0] = path.sep;
41+
return jest.fn(() => array);
42+
},
43+
};
44+
});
2745

2846
// These will not be hoisted
2947
jest.unmock('../__test_modules__/a').dontMock('../__test_modules__/b');
3048
jest.unmock('../__test_modules__/' + 'c');
3149
jest.dontMock('../__test_modules__/Mocked');
3250

33-
34-
describe('babel-plugin-jest-unmock', () => {
51+
describe('babel-plugin-jest-hoist', () => {
3552
it('hoists react unmock call before imports', () => {
3653
expect(typeof React).toEqual('object');
3754
expect(React.isValidElement.mock).toBe(undefined);
@@ -48,6 +65,27 @@ describe('babel-plugin-jest-unmock', () => {
4865
expect(d()).toEqual('unmocked');
4966
});
5067

68+
it('hoists mock call with 2 arguments', () => {
69+
const path = require('path');
70+
71+
expect(e._isMock).toBe(true);
72+
73+
const mockFn = e.fn();
74+
expect(mockFn()).toEqual([path.sep, undefined, undefined]);
75+
});
76+
77+
it('only executes the module factories once', () => {
78+
global.CALLS = 0;
79+
80+
require('../__test_modules__/e');
81+
expect(global.CALLS).toEqual(1);
82+
83+
require('../__test_modules__/e');
84+
expect(global.CALLS).toEqual(1);
85+
86+
delete global.CALLS;
87+
});
88+
5189
it('does not hoist dontMock calls before imports', () => {
5290
expect(Mocked._isMockFunction).toBe(true);
5391
expect((new Mocked()).isMocked).toEqual(undefined);

packages/babel-plugin-jest-hoist/src/index.js

Lines changed: 109 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,118 @@
88

99
'use strict';
1010

11+
function invariant(condition, message) {
12+
if (!condition) {
13+
throw new Error('babel-plugin-jest-hoist: ' + message);
14+
}
15+
}
16+
17+
// We allow `jest`, `require`, all default Node.js globals and all ES2015
18+
// built-ins to be used inside of a `jest.mock` factory.
19+
const WHITELISTED_IDENTIFIERS = {
20+
jest: true,
21+
require: true,
22+
Infinity: true,
23+
NaN: true,
24+
undefined: true,
25+
Object: true,
26+
Function: true,
27+
Boolean: true,
28+
Symbol: true,
29+
Error: true,
30+
EvalError: true,
31+
InternalError: true,
32+
RangeError: true,
33+
ReferenceError: true,
34+
SyntaxError: true,
35+
TypeError: true,
36+
URIError: true,
37+
Number: true,
38+
Math: true,
39+
Date: true,
40+
String: true,
41+
RegExp: true,
42+
Array: true,
43+
Int8Array: true,
44+
Uint8Array: true,
45+
Uint8ClampedArray: true,
46+
Int16Array: true,
47+
Uint16Array: true,
48+
Int32Array: true,
49+
Uint32Array: true,
50+
Float32Array: true,
51+
Float64Array: true,
52+
Map: true,
53+
Set: true,
54+
WeakMap: true,
55+
WeakSet: true,
56+
ArrayBuffer: true,
57+
DataView: true,
58+
JSON: true,
59+
Promise: true,
60+
Generator: true,
61+
GeneratorFunction: true,
62+
Reflect: true,
63+
Proxy: true,
64+
Intl: true,
65+
arguments: true,
66+
};
67+
Object.keys(global).forEach(name => WHITELISTED_IDENTIFIERS[name] = true);
68+
1169
const JEST_GLOBAL = {name: 'jest'};
70+
const IDVisitor = {
71+
ReferencedIdentifier(path) {
72+
this.ids.add(path);
73+
},
74+
};
1275

1376
const FUNCTIONS = {
14-
mock: {
15-
checkArgs: args => args.length === 1 && args[0].isStringLiteral(),
16-
},
17-
unmock: {
18-
checkArgs: args => args.length === 1 && args[0].isStringLiteral(),
19-
},
20-
disableAutomock: {
21-
checkArgs: args => args.length === 0,
22-
},
23-
enableAutomock: {
24-
checkArgs: args => args.length === 0,
77+
mock: args => {
78+
if (args.length === 1) {
79+
return args[0].isStringLiteral();
80+
} else if (args.length === 2) {
81+
const moduleFactory = args[1];
82+
invariant(
83+
moduleFactory.isFunction(),
84+
'The second argument of `jest.mock` must be a function.'
85+
);
86+
87+
const ids = new Set();
88+
const parentScope = moduleFactory.parentPath.scope;
89+
moduleFactory.traverse(IDVisitor, {ids});
90+
for (const id of ids) {
91+
const name = id.node.name;
92+
let found = false;
93+
let scope = id.scope;
94+
95+
while (scope !== parentScope) {
96+
if (scope.bindings[name]) {
97+
found = true;
98+
break;
99+
}
100+
101+
scope = scope.parent;
102+
}
103+
104+
if (!found) {
105+
invariant(
106+
scope.hasGlobal(name) && WHITELISTED_IDENTIFIERS[name],
107+
'The second argument of `jest.mock()` is not allowed to ' +
108+
'reference any outside variables.\n' +
109+
'Invalid variable access: ' + name + '\n' +
110+
'Whitelisted objects: ' +
111+
Object.keys(WHITELISTED_IDENTIFIERS).join(', ') + '.'
112+
);
113+
}
114+
}
115+
116+
return true;
117+
}
118+
return false;
25119
},
120+
unmock: args => args.length === 1 && args[0].isStringLiteral(),
121+
disableAutomock: args => args.length === 0,
122+
enableAutomock: args => args.length === 0,
26123
};
27124

28125
module.exports = babel => {
@@ -37,7 +134,7 @@ module.exports = babel => {
37134
return (
38135
property.isIdentifier() &&
39136
FUNCTIONS[property.node.name] &&
40-
FUNCTIONS[property.node.name].checkArgs(expr.get('arguments')) &&
137+
FUNCTIONS[property.node.name](expr.get('arguments')) &&
41138
(
42139
object.isIdentifier(JEST_GLOBAL) ||
43140
(callee.isMemberExpression() && shouldHoistExpression(object))

0 commit comments

Comments
 (0)