eslint-plugin-jest 24.5.0 → 24.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +2 -3
- package/docs/rules/prefer-to-be-null.md +4 -0
- package/docs/rules/prefer-to-be-undefined.md +4 -0
- package/docs/rules/require-hook.md +149 -0
- package/docs/rules/valid-expect-in-promise.md +55 -14
- package/lib/rules/lowercase-name.js +20 -1
- package/lib/rules/no-large-snapshots.js +1 -3
- package/lib/rules/prefer-to-be-null.js +2 -0
- package/lib/rules/prefer-to-be-undefined.js +2 -0
- package/lib/rules/prefer-to-be.js +4 -1
- package/lib/rules/require-hook.js +87 -0
- package/lib/rules/utils.js +3 -3
- package/lib/rules/valid-expect-in-promise.js +252 -68
- package/lib/rules/valid-expect.js +10 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
# [24.7.0](https://github.com/jest-community/eslint-plugin-jest/compare/v24.6.0...v24.7.0) (2021-10-10)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* create `require-hook` rule ([#929](https://github.com/jest-community/eslint-plugin-jest/issues/929)) ([6204b31](https://github.com/jest-community/eslint-plugin-jest/commit/6204b311e849b51a0e4705015575139f590ae7a4))
|
|
7
|
+
* deprecate `prefer-to-be-null` rule ([4db9161](https://github.com/jest-community/eslint-plugin-jest/commit/4db91612e988e84ac2facbfe466331b22eeccec9))
|
|
8
|
+
* deprecate `prefer-to-be-undefined` rule ([fa08f09](https://github.com/jest-community/eslint-plugin-jest/commit/fa08f0944e89915fb215bbeff970f12459121cb8))
|
|
9
|
+
|
|
10
|
+
# [24.6.0](https://github.com/jest-community/eslint-plugin-jest/compare/v24.5.2...v24.6.0) (2021-10-09)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **valid-expect-in-promise:** re-implement rule ([#916](https://github.com/jest-community/eslint-plugin-jest/issues/916)) ([7a49c58](https://github.com/jest-community/eslint-plugin-jest/commit/7a49c5831e3d85a60c11e385203b8f83d98ad580))
|
|
16
|
+
|
|
17
|
+
## [24.5.2](https://github.com/jest-community/eslint-plugin-jest/compare/v24.5.1...v24.5.2) (2021-10-04)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **lowercase-name:** consider skip and only prefixes for ignores ([#923](https://github.com/jest-community/eslint-plugin-jest/issues/923)) ([8716c24](https://github.com/jest-community/eslint-plugin-jest/commit/8716c24678ea7dc7c9f692b573d1ea19a67efd84))
|
|
23
|
+
|
|
24
|
+
## [24.5.1](https://github.com/jest-community/eslint-plugin-jest/compare/v24.5.0...v24.5.1) (2021-10-04)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* **prefer-to-be:** don't consider RegExp literals as `toBe`-able ([#922](https://github.com/jest-community/eslint-plugin-jest/issues/922)) ([99b6d42](https://github.com/jest-community/eslint-plugin-jest/commit/99b6d429e697d60645b4bc64ee4ae34d7016118c))
|
|
30
|
+
|
|
1
31
|
# [24.5.0](https://github.com/jest-community/eslint-plugin-jest/compare/v24.4.3...v24.5.0) (2021-09-29)
|
|
2
32
|
|
|
3
33
|
|
package/README.md
CHANGED
|
@@ -184,16 +184,15 @@ installations requiring long-term consistency.
|
|
|
184
184
|
| [prefer-spy-on](docs/rules/prefer-spy-on.md) | Suggest using `jest.spyOn()` | | ![fixable][] |
|
|
185
185
|
| [prefer-strict-equal](docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | ![suggest][] |
|
|
186
186
|
| [prefer-to-be](docs/rules/prefer-to-be.md) | Suggest using `toBe()` for primitive literals | | ![fixable][] |
|
|
187
|
-
| [prefer-to-be-null](docs/rules/prefer-to-be-null.md) | Suggest using `toBeNull()` | ![style][] | ![fixable][] |
|
|
188
|
-
| [prefer-to-be-undefined](docs/rules/prefer-to-be-undefined.md) | Suggest using `toBeUndefined()` | ![style][] | ![fixable][] |
|
|
189
187
|
| [prefer-to-contain](docs/rules/prefer-to-contain.md) | Suggest using `toContain()` | ![style][] | ![fixable][] |
|
|
190
188
|
| [prefer-to-have-length](docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` | ![style][] | ![fixable][] |
|
|
191
189
|
| [prefer-todo](docs/rules/prefer-todo.md) | Suggest using `test.todo` | | ![fixable][] |
|
|
190
|
+
| [require-hook](docs/rules/require-hook.md) | Require setup and teardown code to be within a hook | | |
|
|
192
191
|
| [require-to-throw-message](docs/rules/require-to-throw-message.md) | Require a message for `toThrow()` | | |
|
|
193
192
|
| [require-top-level-describe](docs/rules/require-top-level-describe.md) | Require test cases and hooks to be inside a `describe` block | | |
|
|
194
193
|
| [valid-describe](docs/rules/valid-describe.md) | Enforce valid `describe()` callback | ![recommended][] | |
|
|
195
194
|
| [valid-expect](docs/rules/valid-expect.md) | Enforce valid `expect()` usage | ![recommended][] | |
|
|
196
|
-
| [valid-expect-in-promise](docs/rules/valid-expect-in-promise.md) |
|
|
195
|
+
| [valid-expect-in-promise](docs/rules/valid-expect-in-promise.md) | Ensure promises that have expectations in their chain are valid | ![recommended][] | |
|
|
197
196
|
| [valid-title](docs/rules/valid-title.md) | Enforce valid titles | ![recommended][] | ![fixable][] |
|
|
198
197
|
|
|
199
198
|
<!-- end base rules list -->
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Suggest using `toBeNull()` (`prefer-to-be-null`)
|
|
2
2
|
|
|
3
|
+
## Deprecated
|
|
4
|
+
|
|
5
|
+
This rule has been deprecated in favor of [`prefer-to-be`](prefer-to-be.md).
|
|
6
|
+
|
|
3
7
|
In order to have a better failure message, `toBeNull()` should be used upon
|
|
4
8
|
asserting expectations on null value.
|
|
5
9
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Suggest using `toBeUndefined()` (`prefer-to-be-undefined`)
|
|
2
2
|
|
|
3
|
+
## Deprecated
|
|
4
|
+
|
|
5
|
+
This rule has been deprecated in favor of [`prefer-to-be`](prefer-to-be.md).
|
|
6
|
+
|
|
3
7
|
In order to have a better failure message, `toBeUndefined()` should be used upon
|
|
4
8
|
asserting expectations on undefined value.
|
|
5
9
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Require setup and teardown code to be within a hook (`require-hook`)
|
|
2
|
+
|
|
3
|
+
Often while writing tests you have some setup work that needs to happen before
|
|
4
|
+
tests run, and you have some finishing work that needs to happen after tests
|
|
5
|
+
run. Jest provides helper functions to handle this.
|
|
6
|
+
|
|
7
|
+
It's common when writing tests to need to perform setup work that needs to
|
|
8
|
+
happen before tests run, and finishing work after tests run.
|
|
9
|
+
|
|
10
|
+
Because Jest executes all `describe` handlers in a test file _before_ it
|
|
11
|
+
executes any of the actual tests, it's important to ensure setup and teardown
|
|
12
|
+
work is done inside `before*` and `after*` handlers respectively, rather than
|
|
13
|
+
inside the `describe` blocks.
|
|
14
|
+
|
|
15
|
+
## Rule details
|
|
16
|
+
|
|
17
|
+
This rule flags any expression that is either at the toplevel of a test file or
|
|
18
|
+
directly within the body of a `describe`, _except_ for the following:
|
|
19
|
+
|
|
20
|
+
- `import` statements
|
|
21
|
+
- `const` variables
|
|
22
|
+
- `let` _declarations_
|
|
23
|
+
- Types
|
|
24
|
+
- Calls to the standard Jest globals
|
|
25
|
+
|
|
26
|
+
This rule flags any function calls within test files that are directly within
|
|
27
|
+
the body of a `describe`, and suggests wrapping them in one of the four
|
|
28
|
+
lifecycle hooks.
|
|
29
|
+
|
|
30
|
+
Here is a slightly contrived test file showcasing some common cases that would
|
|
31
|
+
be flagged:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { database, isCity } from '../database';
|
|
35
|
+
import { Logger } from '../../../src/Logger';
|
|
36
|
+
import { loadCities } from '../api';
|
|
37
|
+
|
|
38
|
+
jest.mock('../api');
|
|
39
|
+
|
|
40
|
+
const initializeCityDatabase = () => {
|
|
41
|
+
database.addCity('Vienna');
|
|
42
|
+
database.addCity('San Juan');
|
|
43
|
+
database.addCity('Wellington');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const clearCityDatabase = () => {
|
|
47
|
+
database.clear();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
initializeCityDatabase();
|
|
51
|
+
|
|
52
|
+
test('that persists cities', () => {
|
|
53
|
+
expect(database.cities.length).toHaveLength(3);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('city database has Vienna', () => {
|
|
57
|
+
expect(isCity('Vienna')).toBeTruthy();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('city database has San Juan', () => {
|
|
61
|
+
expect(isCity('San Juan')).toBeTruthy();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('when loading cities from the api', () => {
|
|
65
|
+
let consoleWarnSpy = jest.spyOn(console, 'warn');
|
|
66
|
+
|
|
67
|
+
loadCities.mockResolvedValue(['Wellington', 'London']);
|
|
68
|
+
|
|
69
|
+
it('does not duplicate cities', async () => {
|
|
70
|
+
await database.loadCities();
|
|
71
|
+
|
|
72
|
+
expect(database.cities).toHaveLength(4);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('logs any duplicates', async () => {
|
|
76
|
+
await database.loadCities();
|
|
77
|
+
|
|
78
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
79
|
+
'Ignored duplicate cities: Wellington',
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
clearCityDatabase();
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Here is the same slightly contrived test file showcasing the same common cases
|
|
88
|
+
but in ways that would be **not** flagged:
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
import { database, isCity } from '../database';
|
|
92
|
+
import { Logger } from '../../../src/Logger';
|
|
93
|
+
import { loadCities } from '../api';
|
|
94
|
+
|
|
95
|
+
jest.mock('../api');
|
|
96
|
+
|
|
97
|
+
const initializeCityDatabase = () => {
|
|
98
|
+
database.addCity('Vienna');
|
|
99
|
+
database.addCity('San Juan');
|
|
100
|
+
database.addCity('Wellington');
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const clearCityDatabase = () => {
|
|
104
|
+
database.clear();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
initializeCityDatabase();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('that persists cities', () => {
|
|
112
|
+
expect(database.cities.length).toHaveLength(3);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('city database has Vienna', () => {
|
|
116
|
+
expect(isCity('Vienna')).toBeTruthy();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('city database has San Juan', () => {
|
|
120
|
+
expect(isCity('San Juan')).toBeTruthy();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('when loading cities from the api', () => {
|
|
124
|
+
let consoleWarnSpy;
|
|
125
|
+
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
consoleWarnSpy = jest.spyOn(console, 'warn');
|
|
128
|
+
loadCities.mockResolvedValue(['Wellington', 'London']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('does not duplicate cities', async () => {
|
|
132
|
+
await database.loadCities();
|
|
133
|
+
|
|
134
|
+
expect(database.cities).toHaveLength(4);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('logs any duplicates', async () => {
|
|
138
|
+
await database.loadCities();
|
|
139
|
+
|
|
140
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
141
|
+
'Ignored duplicate cities: Wellington',
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
afterEach(() => {
|
|
147
|
+
clearCityDatabase();
|
|
148
|
+
});
|
|
149
|
+
```
|
|
@@ -1,31 +1,72 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Ensure promises that have expectations in their chain are valid (`valid-expect-in-promise`)
|
|
2
2
|
|
|
3
|
-
Ensure
|
|
4
|
-
promise
|
|
3
|
+
Ensure promises that include expectations are returned or awaited.
|
|
5
4
|
|
|
6
5
|
## Rule details
|
|
7
6
|
|
|
8
|
-
This rule
|
|
9
|
-
|
|
7
|
+
This rule flags any promises within the body of a test that include expectations
|
|
8
|
+
that have either not been returned or awaited.
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
The following pattern is considered warning:
|
|
10
|
+
The following patterns is considered warning:
|
|
14
11
|
|
|
15
12
|
```js
|
|
16
|
-
it('
|
|
17
|
-
|
|
18
|
-
expect(
|
|
13
|
+
it('promises a person', () => {
|
|
14
|
+
api.getPersonByName('bob').then(person => {
|
|
15
|
+
expect(person).toHaveProperty('name', 'Bob');
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('promises a counted person', () => {
|
|
20
|
+
const promise = api.getPersonByName('bob').then(person => {
|
|
21
|
+
expect(person).toHaveProperty('name', 'Bob');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
promise.then(() => {
|
|
25
|
+
expect(analytics.gottenPeopleCount).toBe(1);
|
|
19
26
|
});
|
|
20
27
|
});
|
|
28
|
+
|
|
29
|
+
it('promises multiple people', () => {
|
|
30
|
+
const firstPromise = api.getPersonByName('bob').then(person => {
|
|
31
|
+
expect(person).toHaveProperty('name', 'Bob');
|
|
32
|
+
});
|
|
33
|
+
const secondPromise = api.getPersonByName('alice').then(person => {
|
|
34
|
+
expect(person).toHaveProperty('name', 'Alice');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return Promise.any([firstPromise, secondPromise]);
|
|
38
|
+
});
|
|
21
39
|
```
|
|
22
40
|
|
|
23
41
|
The following pattern is not warning:
|
|
24
42
|
|
|
25
43
|
```js
|
|
26
|
-
it('
|
|
27
|
-
|
|
28
|
-
expect(
|
|
44
|
+
it('promises a person', async () => {
|
|
45
|
+
await api.getPersonByName('bob').then(person => {
|
|
46
|
+
expect(person).toHaveProperty('name', 'Bob');
|
|
29
47
|
});
|
|
30
48
|
});
|
|
49
|
+
|
|
50
|
+
it('promises a counted person', () => {
|
|
51
|
+
let promise = api.getPersonByName('bob').then(person => {
|
|
52
|
+
expect(person).toHaveProperty('name', 'Bob');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
promise = promise.then(() => {
|
|
56
|
+
expect(analytics.gottenPeopleCount).toBe(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return promise;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('promises multiple people', () => {
|
|
63
|
+
const firstPromise = api.getPersonByName('bob').then(person => {
|
|
64
|
+
expect(person).toHaveProperty('name', 'Bob');
|
|
65
|
+
});
|
|
66
|
+
const secondPromise = api.getPersonByName('alice').then(person => {
|
|
67
|
+
expect(person).toHaveProperty('name', 'Alice');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return Promise.allSettled([firstPromise, secondPromise]);
|
|
71
|
+
});
|
|
31
72
|
```
|
|
@@ -21,6 +21,24 @@ const findNodeNameAndArgument = node => {
|
|
|
21
21
|
return [(0, _utils.getNodeName)(node).split('.')[0], node.arguments[0]];
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
const populateIgnores = ignore => {
|
|
25
|
+
const ignores = [];
|
|
26
|
+
|
|
27
|
+
if (ignore.includes(_utils.DescribeAlias.describe)) {
|
|
28
|
+
ignores.push(...Object.keys(_utils.DescribeAlias));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (ignore.includes(_utils.TestCaseName.test)) {
|
|
32
|
+
ignores.push(...Object.keys(_utils.TestCaseName));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (ignore.includes(_utils.TestCaseName.it)) {
|
|
36
|
+
ignores.push(...Object.keys(_utils.TestCaseName));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return ignores;
|
|
40
|
+
};
|
|
41
|
+
|
|
24
42
|
var _default = (0, _utils.createRule)({
|
|
25
43
|
name: __filename,
|
|
26
44
|
meta: {
|
|
@@ -70,6 +88,7 @@ var _default = (0, _utils.createRule)({
|
|
|
70
88
|
allowedPrefixes = [],
|
|
71
89
|
ignoreTopLevelDescribe
|
|
72
90
|
}]) {
|
|
91
|
+
const ignores = populateIgnores(ignore);
|
|
73
92
|
let numberOfDescribeBlocks = 0;
|
|
74
93
|
return {
|
|
75
94
|
CallExpression(node) {
|
|
@@ -96,7 +115,7 @@ var _default = (0, _utils.createRule)({
|
|
|
96
115
|
|
|
97
116
|
const firstCharacter = description.charAt(0);
|
|
98
117
|
|
|
99
|
-
if (!firstCharacter || firstCharacter === firstCharacter.toLowerCase() ||
|
|
118
|
+
if (!firstCharacter || firstCharacter === firstCharacter.toLowerCase() || ignores.includes(name)) {
|
|
100
119
|
return;
|
|
101
120
|
}
|
|
102
121
|
|
|
@@ -100,8 +100,6 @@ var _default = (0, _utils.createRule)({
|
|
|
100
100
|
|
|
101
101
|
return {
|
|
102
102
|
CallExpression(node) {
|
|
103
|
-
var _matcher$node$parent;
|
|
104
|
-
|
|
105
103
|
if (!(0, _utils.isExpectCall)(node)) {
|
|
106
104
|
return;
|
|
107
105
|
}
|
|
@@ -110,7 +108,7 @@ var _default = (0, _utils.createRule)({
|
|
|
110
108
|
matcher
|
|
111
109
|
} = (0, _utils.parseExpectCall)(node);
|
|
112
110
|
|
|
113
|
-
if ((matcher === null || matcher === void 0 ? void 0 :
|
|
111
|
+
if ((matcher === null || matcher === void 0 ? void 0 : matcher.node.parent.type) !== _experimentalUtils.AST_NODE_TYPES.CallExpression) {
|
|
114
112
|
return;
|
|
115
113
|
}
|
|
116
114
|
|
|
@@ -20,7 +20,10 @@ const isNullEqualityMatcher = matcher => isNullLiteral(getFirstArgument(matcher)
|
|
|
20
20
|
|
|
21
21
|
const isFirstArgumentIdentifier = (matcher, name) => (0, _utils.isIdentifier)(getFirstArgument(matcher), name);
|
|
22
22
|
|
|
23
|
-
const isPrimitiveLiteral = matcher =>
|
|
23
|
+
const isPrimitiveLiteral = matcher => {
|
|
24
|
+
const firstArg = getFirstArgument(matcher);
|
|
25
|
+
return firstArg.type === _experimentalUtils.AST_NODE_TYPES.Literal && !('regex' in firstArg);
|
|
26
|
+
};
|
|
24
27
|
|
|
25
28
|
const getFirstArgument = matcher => {
|
|
26
29
|
return (0, _utils.followTypeAssertionChain)(matcher.arguments[0]);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
|
|
8
|
+
var _experimentalUtils = require("@typescript-eslint/experimental-utils");
|
|
9
|
+
|
|
10
|
+
var _utils = require("./utils");
|
|
11
|
+
|
|
12
|
+
const isJestFnCall = node => {
|
|
13
|
+
var _getNodeName;
|
|
14
|
+
|
|
15
|
+
if ((0, _utils.isDescribeCall)(node) || (0, _utils.isTestCaseCall)(node) || (0, _utils.isHook)(node)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return !!((_getNodeName = (0, _utils.getNodeName)(node)) !== null && _getNodeName !== void 0 && _getNodeName.startsWith('jest.'));
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const shouldBeInHook = node => {
|
|
23
|
+
switch (node.type) {
|
|
24
|
+
case _experimentalUtils.AST_NODE_TYPES.ExpressionStatement:
|
|
25
|
+
return shouldBeInHook(node.expression);
|
|
26
|
+
|
|
27
|
+
case _experimentalUtils.AST_NODE_TYPES.CallExpression:
|
|
28
|
+
return !isJestFnCall(node);
|
|
29
|
+
|
|
30
|
+
default:
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
var _default = (0, _utils.createRule)({
|
|
36
|
+
name: __filename,
|
|
37
|
+
meta: {
|
|
38
|
+
docs: {
|
|
39
|
+
category: 'Best Practices',
|
|
40
|
+
description: 'Require setup and teardown code to be within a hook',
|
|
41
|
+
recommended: false
|
|
42
|
+
},
|
|
43
|
+
messages: {
|
|
44
|
+
useHook: 'This should be done within a hook'
|
|
45
|
+
},
|
|
46
|
+
type: 'suggestion',
|
|
47
|
+
schema: []
|
|
48
|
+
},
|
|
49
|
+
defaultOptions: [],
|
|
50
|
+
|
|
51
|
+
create(context) {
|
|
52
|
+
const checkBlockBody = body => {
|
|
53
|
+
for (const statement of body) {
|
|
54
|
+
if (shouldBeInHook(statement)) {
|
|
55
|
+
context.report({
|
|
56
|
+
node: statement,
|
|
57
|
+
messageId: 'useHook'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
Program(program) {
|
|
65
|
+
checkBlockBody(program.body);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
CallExpression(node) {
|
|
69
|
+
if (!(0, _utils.isDescribeCall)(node) || node.arguments.length < 2) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const [, testFn] = node.arguments;
|
|
74
|
+
|
|
75
|
+
if (!(0, _utils.isFunction)(testFn) || testFn.body.type !== _experimentalUtils.AST_NODE_TYPES.BlockStatement) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
checkBlockBody(testFn.body.body);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
exports.default = _default;
|
package/lib/rules/utils.js
CHANGED
|
@@ -233,7 +233,7 @@ const reparseAsMatcher = parsedMember => ({ ...parsedMember,
|
|
|
233
233
|
*
|
|
234
234
|
* If this matcher isn't called, this will be `null`.
|
|
235
235
|
*/
|
|
236
|
-
arguments: parsedMember.node.parent
|
|
236
|
+
arguments: parsedMember.node.parent.type === _experimentalUtils.AST_NODE_TYPES.CallExpression ? parsedMember.node.parent.arguments : null
|
|
237
237
|
});
|
|
238
238
|
/**
|
|
239
239
|
* Re-parses the given `parsedMember` as a `ParsedExpectModifier`.
|
|
@@ -260,7 +260,7 @@ const reparseMemberAsModifier = parsedMember => {
|
|
|
260
260
|
throw new Error(`modifier name must be either "${ModifierName.resolves}" or "${ModifierName.rejects}" (got "${parsedMember.name}")`);
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
const negation =
|
|
263
|
+
const negation = isExpectMember(parsedMember.node.parent, ModifierName.not) ? parsedMember.node.parent : undefined;
|
|
264
264
|
return { ...parsedMember,
|
|
265
265
|
negation
|
|
266
266
|
};
|
|
@@ -297,7 +297,7 @@ const parseExpectCall = expect => {
|
|
|
297
297
|
const modifier = expectation.modifier = reparseMemberAsModifier(parsedMember);
|
|
298
298
|
const memberNode = modifier.negation || modifier.node;
|
|
299
299
|
|
|
300
|
-
if (!
|
|
300
|
+
if (!isExpectMember(memberNode.parent)) {
|
|
301
301
|
return expectation;
|
|
302
302
|
}
|
|
303
303
|
|
|
@@ -9,96 +9,204 @@ var _experimentalUtils = require("@typescript-eslint/experimental-utils");
|
|
|
9
9
|
|
|
10
10
|
var _utils = require("./utils");
|
|
11
11
|
|
|
12
|
-
const isPromiseChainCall = node =>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
return isFullExpectCall(line.expression);
|
|
19
|
-
}
|
|
12
|
+
const isPromiseChainCall = node => {
|
|
13
|
+
if (node.type === _experimentalUtils.AST_NODE_TYPES.CallExpression && node.callee.type === _experimentalUtils.AST_NODE_TYPES.MemberExpression && (0, _utils.isSupportedAccessor)(node.callee.property)) {
|
|
14
|
+
// promise methods should have at least 1 argument
|
|
15
|
+
if (node.arguments.length === 0) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
switch ((0, _utils.getAccessorValue)(node.callee.property)) {
|
|
20
|
+
case 'then':
|
|
21
|
+
return node.arguments.length < 3;
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
case 'catch':
|
|
24
|
+
case 'finally':
|
|
25
|
+
return node.arguments.length < 2;
|
|
26
|
+
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
return
|
|
29
|
+
return false;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
const
|
|
32
|
+
const findTopMostCallExpression = node => {
|
|
33
|
+
let topMostCallExpression = node;
|
|
34
|
+
let {
|
|
35
|
+
parent
|
|
36
|
+
} = node;
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
while (parent) {
|
|
39
|
+
if (parent.type === _experimentalUtils.AST_NODE_TYPES.CallExpression) {
|
|
40
|
+
topMostCallExpression = parent;
|
|
41
|
+
parent = parent.parent;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (parent.type !== _experimentalUtils.AST_NODE_TYPES.MemberExpression) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
parent = parent.parent;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return topMostCallExpression;
|
|
46
53
|
};
|
|
47
54
|
|
|
48
|
-
const
|
|
49
|
-
|
|
55
|
+
const isTestCaseCallWithCallbackArg = node => {
|
|
56
|
+
if (!(0, _utils.isTestCaseCall)(node)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isJestEach = (0, _utils.getNodeName)(node).endsWith('.each');
|
|
50
61
|
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
if (isJestEach && node.callee.type !== _experimentalUtils.AST_NODE_TYPES.TaggedTemplateExpression) {
|
|
63
|
+
// isJestEach but not a TaggedTemplateExpression, so this must be
|
|
64
|
+
// the `jest.each([])()` syntax which this rule doesn't support due
|
|
65
|
+
// to its complexity (see jest-community/eslint-plugin-jest#710)
|
|
66
|
+
// so we return true to trigger bailout
|
|
67
|
+
return true;
|
|
55
68
|
}
|
|
56
69
|
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
if (isJestEach || node.arguments.length >= 2) {
|
|
71
|
+
const [, callback] = node.arguments;
|
|
72
|
+
const callbackArgIndex = Number(isJestEach);
|
|
73
|
+
return callback && (0, _utils.isFunction)(callback) && callback.params.length === 1 + callbackArgIndex;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return false;
|
|
59
77
|
};
|
|
60
78
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
79
|
+
const isPromiseMethodThatUsesValue = (node, identifier) => {
|
|
80
|
+
const {
|
|
81
|
+
name
|
|
82
|
+
} = identifier;
|
|
83
|
+
|
|
84
|
+
if (node.argument === null) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (node.argument.type === _experimentalUtils.AST_NODE_TYPES.CallExpression && node.argument.arguments.length > 0) {
|
|
89
|
+
const nodeName = (0, _utils.getNodeName)(node.argument);
|
|
90
|
+
|
|
91
|
+
if (['Promise.all', 'Promise.allSettled'].includes(nodeName)) {
|
|
92
|
+
const [firstArg] = node.argument.arguments;
|
|
64
93
|
|
|
65
|
-
|
|
66
|
-
|
|
94
|
+
if (firstArg.type === _experimentalUtils.AST_NODE_TYPES.ArrayExpression && firstArg.elements.some(nod => (0, _utils.isIdentifier)(nod, name))) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
67
97
|
}
|
|
68
98
|
|
|
69
|
-
|
|
99
|
+
if (['Promise.resolve', 'Promise.reject'].includes(nodeName) && node.argument.arguments.length === 1) {
|
|
100
|
+
return (0, _utils.isIdentifier)(node.argument.arguments[0], name);
|
|
101
|
+
}
|
|
70
102
|
}
|
|
71
103
|
|
|
72
|
-
return
|
|
104
|
+
return node.argument.type === _experimentalUtils.AST_NODE_TYPES.Identifier && (0, _utils.isIdentifier)(node.argument, name);
|
|
73
105
|
};
|
|
106
|
+
/**
|
|
107
|
+
* Attempts to determine if the runtime value represented by the given `identifier`
|
|
108
|
+
* is `await`ed or `return`ed within the given `body` of statements
|
|
109
|
+
*/
|
|
74
110
|
|
|
75
|
-
const isParentThenOrPromiseReturned = (node, testFunctionBody) => node.type === _experimentalUtils.AST_NODE_TYPES.ReturnStatement || isPromiseReturnedLater(node, testFunctionBody);
|
|
76
111
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
112
|
+
const isValueAwaitedOrReturned = (identifier, body) => {
|
|
113
|
+
const {
|
|
114
|
+
name
|
|
115
|
+
} = identifier;
|
|
116
|
+
|
|
117
|
+
for (const node of body) {
|
|
118
|
+
// skip all nodes that are before this identifier, because they'd probably
|
|
119
|
+
// be affecting a different runtime value (e.g. due to reassignment)
|
|
120
|
+
if (node.range[0] <= identifier.range[0]) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (node.type === _experimentalUtils.AST_NODE_TYPES.ReturnStatement) {
|
|
125
|
+
return isPromiseMethodThatUsesValue(node, identifier);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (node.type === _experimentalUtils.AST_NODE_TYPES.ExpressionStatement) {
|
|
129
|
+
if (node.expression.type === _experimentalUtils.AST_NODE_TYPES.AwaitExpression) {
|
|
130
|
+
return isPromiseMethodThatUsesValue(node.expression, identifier);
|
|
131
|
+
} // (re)assignment changes the runtime value, so if we've not found an
|
|
132
|
+
// await or return already we act as if we've reached the end of the body
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if (node.expression.type === _experimentalUtils.AST_NODE_TYPES.AssignmentExpression) {
|
|
136
|
+
var _getNodeName;
|
|
137
|
+
|
|
138
|
+
// unless we're assigning to the same identifier, in which case
|
|
139
|
+
// we might be chaining off the existing promise value
|
|
140
|
+
if ((0, _utils.isIdentifier)(node.expression.left, name) && (_getNodeName = (0, _utils.getNodeName)(node.expression.right)) !== null && _getNodeName !== void 0 && _getNodeName.startsWith(`${name}.`) && isPromiseChainCall(node.expression.right)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
break;
|
|
83
145
|
}
|
|
84
146
|
}
|
|
85
147
|
|
|
86
|
-
|
|
87
|
-
|
|
148
|
+
if (node.type === _experimentalUtils.AST_NODE_TYPES.BlockStatement && isValueAwaitedOrReturned(identifier, node.body)) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return false;
|
|
88
154
|
};
|
|
89
155
|
|
|
90
|
-
const
|
|
156
|
+
const findFirstBlockBodyUp = node => {
|
|
157
|
+
let parent = node;
|
|
158
|
+
|
|
159
|
+
while (parent) {
|
|
160
|
+
if (parent.type === _experimentalUtils.AST_NODE_TYPES.BlockStatement) {
|
|
161
|
+
return parent.body;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
parent = parent.parent;
|
|
165
|
+
}
|
|
166
|
+
/* istanbul ignore next */
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
throw new Error(`Could not find BlockStatement - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const isDirectlyWithinTestCaseCall = node => {
|
|
173
|
+
let parent = node;
|
|
174
|
+
|
|
175
|
+
while (parent) {
|
|
176
|
+
if ((0, _utils.isFunction)(parent)) {
|
|
177
|
+
var _parent;
|
|
178
|
+
|
|
179
|
+
parent = parent.parent;
|
|
180
|
+
return !!(((_parent = parent) === null || _parent === void 0 ? void 0 : _parent.type) === _experimentalUtils.AST_NODE_TYPES.CallExpression && (0, _utils.isTestCaseCall)(parent));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
parent = parent.parent;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return false;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const isVariableAwaitedOrReturned = variable => {
|
|
190
|
+
const body = findFirstBlockBodyUp(variable); // it's pretty much impossible for us to track destructuring assignments,
|
|
191
|
+
// so we return true to bailout gracefully
|
|
192
|
+
|
|
193
|
+
if (!(0, _utils.isIdentifier)(variable.id)) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return isValueAwaitedOrReturned(variable.id, body);
|
|
198
|
+
};
|
|
91
199
|
|
|
92
200
|
var _default = (0, _utils.createRule)({
|
|
93
201
|
name: __filename,
|
|
94
202
|
meta: {
|
|
95
203
|
docs: {
|
|
96
204
|
category: 'Best Practices',
|
|
97
|
-
description: '
|
|
205
|
+
description: 'Ensure promises that have expectations in their chain are valid',
|
|
98
206
|
recommended: 'error'
|
|
99
207
|
},
|
|
100
208
|
messages: {
|
|
101
|
-
|
|
209
|
+
expectInFloatingPromise: "This promise should either be returned or awaited to ensure the expects in it's chain are called"
|
|
102
210
|
},
|
|
103
211
|
type: 'suggestion',
|
|
104
212
|
schema: []
|
|
@@ -106,30 +214,106 @@ var _default = (0, _utils.createRule)({
|
|
|
106
214
|
defaultOptions: [],
|
|
107
215
|
|
|
108
216
|
create(context) {
|
|
217
|
+
let inTestCaseWithDoneCallback = false; // an array of booleans representing each promise chain we enter, with the
|
|
218
|
+
// boolean value representing if we think a given chain contains an expect
|
|
219
|
+
// in it's body.
|
|
220
|
+
//
|
|
221
|
+
// since we only care about the inner-most chain, we represent the state in
|
|
222
|
+
// reverse with the inner-most being the first item, as that makes it
|
|
223
|
+
// slightly less code to assign to by not needing to know the length
|
|
224
|
+
|
|
225
|
+
const chains = [];
|
|
109
226
|
return {
|
|
110
227
|
CallExpression(node) {
|
|
111
|
-
|
|
228
|
+
// there are too many ways that the done argument could be used with
|
|
229
|
+
// promises that contain expect that would make the promise safe for us
|
|
230
|
+
if (isTestCaseCallWithCallbackArg(node)) {
|
|
231
|
+
inTestCaseWithDoneCallback = true;
|
|
112
232
|
return;
|
|
113
|
-
}
|
|
233
|
+
} // if this call expression is a promise chain, add it to the stack with
|
|
234
|
+
// value of "false", as we assume there are no expect calls initially
|
|
114
235
|
|
|
115
|
-
const testFunction = findTestFunction(node);
|
|
116
236
|
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
237
|
+
if (isPromiseChainCall(node)) {
|
|
238
|
+
chains.unshift(false);
|
|
239
|
+
return;
|
|
240
|
+
} // if we're within a promise chain, and this call expression looks like
|
|
241
|
+
// an expect call, mark the deepest chain as having an expect call
|
|
121
242
|
|
|
122
|
-
|
|
123
|
-
|
|
243
|
+
|
|
244
|
+
if (chains.length > 0 && (0, _utils.isExpectCall)(node)) {
|
|
245
|
+
chains[0] = true;
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
'CallExpression:exit'(node) {
|
|
250
|
+
// there are too many ways that the "done" argument could be used to
|
|
251
|
+
// make promises containing expects safe in a test for us to be able to
|
|
252
|
+
// accurately check, so we just bail out completely if it's present
|
|
253
|
+
if (inTestCaseWithDoneCallback) {
|
|
254
|
+
if ((0, _utils.isTestCaseCall)(node)) {
|
|
255
|
+
inTestCaseWithDoneCallback = false;
|
|
124
256
|
}
|
|
125
257
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!isPromiseChainCall(node)) {
|
|
262
|
+
return;
|
|
263
|
+
} // since we're exiting this call expression (which is a promise chain)
|
|
264
|
+
// we remove it from the stack of chains, since we're unwinding
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
const hasExpectCall = chains.shift(); // if the promise chain we're exiting doesn't contain an expect,
|
|
268
|
+
// then we don't need to check it for anything
|
|
269
|
+
|
|
270
|
+
if (!hasExpectCall) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const {
|
|
275
|
+
parent
|
|
276
|
+
} = findTopMostCallExpression(node); // if we don't have a parent (which is technically impossible at runtime)
|
|
277
|
+
// or our parent is not directly within the test case, we stop checking
|
|
278
|
+
// because we're most likely in the body of a function being defined
|
|
279
|
+
// within the test, which we can't track
|
|
130
280
|
|
|
131
|
-
|
|
281
|
+
if (!parent || !isDirectlyWithinTestCaseCall(parent)) {
|
|
282
|
+
return;
|
|
132
283
|
}
|
|
284
|
+
|
|
285
|
+
switch (parent.type) {
|
|
286
|
+
case _experimentalUtils.AST_NODE_TYPES.VariableDeclarator:
|
|
287
|
+
{
|
|
288
|
+
if (isVariableAwaitedOrReturned(parent)) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
case _experimentalUtils.AST_NODE_TYPES.AssignmentExpression:
|
|
296
|
+
{
|
|
297
|
+
if (parent.left.type === _experimentalUtils.AST_NODE_TYPES.Identifier && isValueAwaitedOrReturned(parent.left, findFirstBlockBodyUp(parent))) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
case _experimentalUtils.AST_NODE_TYPES.ExpressionStatement:
|
|
305
|
+
break;
|
|
306
|
+
|
|
307
|
+
case _experimentalUtils.AST_NODE_TYPES.ReturnStatement:
|
|
308
|
+
case _experimentalUtils.AST_NODE_TYPES.AwaitExpression:
|
|
309
|
+
default:
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
context.report({
|
|
314
|
+
messageId: 'expectInFloatingPromise',
|
|
315
|
+
node: parent
|
|
316
|
+
});
|
|
133
317
|
}
|
|
134
318
|
|
|
135
319
|
};
|
|
@@ -26,19 +26,25 @@ const getPromiseCallExpressionNode = node => {
|
|
|
26
26
|
node = node.parent;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
if (node.type === _experimentalUtils.AST_NODE_TYPES.CallExpression && node.callee
|
|
29
|
+
if (node.type === _experimentalUtils.AST_NODE_TYPES.CallExpression && node.callee.type === _experimentalUtils.AST_NODE_TYPES.MemberExpression && (0, _utils.isSupportedAccessor)(node.callee.object) && (0, _utils.getAccessorValue)(node.callee.object) === 'Promise' && node.parent) {
|
|
30
30
|
return node;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
return null;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
const findPromiseCallExpressionNode = node =>
|
|
36
|
+
const findPromiseCallExpressionNode = node => {
|
|
37
|
+
var _node$parent;
|
|
38
|
+
|
|
39
|
+
return (_node$parent = node.parent) !== null && _node$parent !== void 0 && _node$parent.parent && [_experimentalUtils.AST_NODE_TYPES.CallExpression, _experimentalUtils.AST_NODE_TYPES.ArrayExpression].includes(node.parent.type) ? getPromiseCallExpressionNode(node.parent) : null;
|
|
40
|
+
};
|
|
37
41
|
|
|
38
42
|
const getParentIfThenified = node => {
|
|
39
|
-
|
|
43
|
+
var _node$parent2;
|
|
44
|
+
|
|
45
|
+
const grandParentNode = (_node$parent2 = node.parent) === null || _node$parent2 === void 0 ? void 0 : _node$parent2.parent;
|
|
40
46
|
|
|
41
|
-
if (grandParentNode && grandParentNode.type === _experimentalUtils.AST_NODE_TYPES.CallExpression &&
|
|
47
|
+
if (grandParentNode && grandParentNode.type === _experimentalUtils.AST_NODE_TYPES.CallExpression && (0, _utils.isExpectMember)(grandParentNode.callee) && ['then', 'catch'].includes((0, _utils.getAccessorValue)(grandParentNode.callee.property)) && grandParentNode.parent) {
|
|
42
48
|
// Just in case `then`s are chained look one above.
|
|
43
49
|
return getParentIfThenified(grandParentNode);
|
|
44
50
|
}
|