Skip to content

Commit eb0a2a2

Browse files
ranyitzSimenB
authored andcommittedFeb 11, 2018
feat(rules): add consistent-test-it rule
Fixes #12
1 parent 8518811 commit eb0a2a2

File tree

7 files changed

+652
-24
lines changed

7 files changed

+652
-24
lines changed
 

Diff for: ‎README.md

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ for more information about extending configuration files.
8080

8181
| Rule | Description | Recommended | Fixable |
8282
| ------------------------------------------------------------------ | --------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------- |
83+
| [consistent-test-it](docs/rules/consistent-test-it.md) | Enforce consistent test or it keyword | | ![fixable](https://img.shields.io/badge/-fixable-green.svg) |
8384
| [no-disabled-tests](docs/rules/no-disabled-tests.md) | Disallow disabled tests | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | |
8485
| [no-focused-tests](docs/rules/no-focused-tests.md) | Disallow focused tests | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | |
8586
| [no-identical-title](docs/rules/no-identical-title.md) | Disallow identical titles | ![recommended](https://img.shields.io/badge/-recommended-lightgrey.svg) | |

Diff for: ‎docs/rules/consistent-test-it.md

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Have control over `test` and `it` usages (consistent-test-it)
2+
3+
Jest allows you to choose how you want to define your tests, using the `it` or
4+
the `test` keywords, with multiple permutations for each:
5+
6+
* **it:** `it`, `xit`, `fit`, `it.only`, `it.skip`.
7+
* **test:** `test`, `xtest`, `test.only`, `test.skip`.
8+
9+
This rule gives you control over the usage of these keywords in your codebase.
10+
11+
## Rule Details
12+
13+
This rule can be configured as follows
14+
15+
```js
16+
{
17+
type: 'object',
18+
properties: {
19+
fn: {
20+
enum: ['it', 'test'],
21+
},
22+
withinDescribe: {
23+
enum: ['it', 'test'],
24+
},
25+
},
26+
additionalProperties: false,
27+
}
28+
```
29+
30+
#### fn
31+
32+
Decides whether to use `test` or `it`.
33+
34+
#### withinDescribe
35+
36+
Decides whether to use `test` or `it` within a describe scope.
37+
38+
```js
39+
/*eslint jest/consistent-test-it: ["error", {"fn": "test"}]*/
40+
41+
test('foo'); // valid
42+
test.only('foo'); // valid
43+
44+
it('foo'); // invalid
45+
it.only('foo'); // invalid
46+
```
47+
48+
```js
49+
/*eslint jest/consistent-test-it: ["error", {"fn": "it"}]*/
50+
51+
it('foo'); // valid
52+
it.only('foo'); // valid
53+
54+
test('foo'); // invalid
55+
test.only('foo'); // invalid
56+
```
57+
58+
```js
59+
/*eslint jest/consistent-test-it: ["error", {"fn": "it", "withinDescribe": "test"}]*/
60+
61+
it('foo'); // valid
62+
describe('foo', function() {
63+
test('bar'); // valid
64+
});
65+
66+
test('foo'); // invalid
67+
describe('foo', function() {
68+
it('bar'); // invalid
69+
});
70+
```
71+
72+
### Default configuration
73+
74+
The default configuration forces top level test to use `test` and all tests
75+
nested within describe to use `it`.
76+
77+
```js
78+
/*eslint jest/consistent-test-it: ["error"]*/
79+
80+
test('foo'); // valid
81+
describe('foo', function() {
82+
it('bar'); // valid
83+
});
84+
85+
it('foo'); // invalid
86+
describe('foo', function() {
87+
test('bar'); // invalid
88+
});
89+
```

Diff for: ‎index.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22

3+
const consistentTestIt = require('./rules/consistent-test-it');
34
const noDisabledTests = require('./rules/no-disabled-tests');
45
const noFocusedTests = require('./rules/no-focused-tests');
56
const noIdenticalTitle = require('./rules/no-identical-title');
@@ -57,6 +58,7 @@ module.exports = {
5758
'.snap': snapshotProcessor,
5859
},
5960
rules: {
61+
'consistent-test-it': consistentTestIt,
6062
'no-disabled-tests': noDisabledTests,
6163
'no-focused-tests': noFocusedTests,
6264
'no-identical-title': noIdenticalTitle,

Diff for: ‎rules/__tests__/consistent-test-it.test.js

+416
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,416 @@
1+
'use strict';
2+
3+
const RuleTester = require('eslint').RuleTester;
4+
const rules = require('../..').rules;
5+
const ruleTester = new RuleTester({
6+
parserOptions: {
7+
ecmaVersion: 6,
8+
},
9+
});
10+
11+
ruleTester.run('consistent-test-it with fn=test', rules['consistent-test-it'], {
12+
valid: [
13+
{
14+
code: 'test("foo")',
15+
options: [{ fn: 'test' }],
16+
},
17+
{
18+
code: 'test.only("foo")',
19+
options: [{ fn: 'test' }],
20+
},
21+
{
22+
code: 'test.skip("foo")',
23+
options: [{ fn: 'test' }],
24+
},
25+
{
26+
code: 'xtest("foo")',
27+
options: [{ fn: 'test' }],
28+
},
29+
{
30+
code: 'describe("suite", () => { test("foo") })',
31+
options: [{ fn: 'test' }],
32+
},
33+
],
34+
invalid: [
35+
{
36+
code: 'it("foo")',
37+
options: [{ fn: 'test' }],
38+
errors: [{ message: "Prefer using 'test' instead of 'it'" }],
39+
output: 'test("foo")',
40+
},
41+
{
42+
code: 'xit("foo")',
43+
options: [{ fn: 'test' }],
44+
errors: [{ message: "Prefer using 'test' instead of 'it'" }],
45+
output: 'xtest("foo")',
46+
},
47+
{
48+
code: 'fit("foo")',
49+
options: [{ fn: 'test' }],
50+
errors: [{ message: "Prefer using 'test' instead of 'it'" }],
51+
output: 'test.only("foo")',
52+
},
53+
{
54+
code: 'it.skip("foo")',
55+
options: [{ fn: 'test' }],
56+
errors: [{ message: "Prefer using 'test' instead of 'it'" }],
57+
output: 'test.skip("foo")',
58+
},
59+
{
60+
code: 'it.only("foo")',
61+
options: [{ fn: 'test' }],
62+
errors: [{ message: "Prefer using 'test' instead of 'it'" }],
63+
output: 'test.only("foo")',
64+
},
65+
{
66+
code: 'describe("suite", () => { it("foo") })',
67+
options: [{ fn: 'test' }],
68+
errors: [
69+
{ message: "Prefer using 'test' instead of 'it' within describe" },
70+
],
71+
output: 'describe("suite", () => { test("foo") })',
72+
},
73+
],
74+
});
75+
76+
ruleTester.run('consistent-test-it with fn=it', rules['consistent-test-it'], {
77+
valid: [
78+
{
79+
code: 'it("foo")',
80+
options: [{ fn: 'it' }],
81+
},
82+
{
83+
code: 'fit("foo")',
84+
options: [{ fn: 'it' }],
85+
},
86+
{
87+
code: 'xit("foo")',
88+
options: [{ fn: 'it' }],
89+
},
90+
{
91+
code: 'it.only("foo")',
92+
options: [{ fn: 'it' }],
93+
},
94+
{
95+
code: 'it.skip("foo")',
96+
options: [{ fn: 'it' }],
97+
},
98+
{
99+
code: 'describe("suite", () => { it("foo") })',
100+
options: [{ fn: 'it' }],
101+
},
102+
],
103+
invalid: [
104+
{
105+
code: 'test("foo")',
106+
options: [{ fn: 'it' }],
107+
errors: [{ message: "Prefer using 'it' instead of 'test'" }],
108+
output: 'it("foo")',
109+
},
110+
{
111+
code: 'xtest("foo")',
112+
options: [{ fn: 'it' }],
113+
errors: [{ message: "Prefer using 'it' instead of 'test'" }],
114+
output: 'xit("foo")',
115+
},
116+
{
117+
code: 'test.skip("foo")',
118+
options: [{ fn: 'it' }],
119+
errors: [{ message: "Prefer using 'it' instead of 'test'" }],
120+
output: 'it.skip("foo")',
121+
},
122+
{
123+
code: 'test.only("foo")',
124+
options: [{ fn: 'it' }],
125+
errors: [{ message: "Prefer using 'it' instead of 'test'" }],
126+
output: 'it.only("foo")',
127+
},
128+
{
129+
code: 'describe("suite", () => { test("foo") })',
130+
options: [{ fn: 'it' }],
131+
errors: [
132+
{ message: "Prefer using 'it' instead of 'test' within describe" },
133+
],
134+
output: 'describe("suite", () => { it("foo") })',
135+
},
136+
],
137+
});
138+
139+
ruleTester.run(
140+
'consistent-test-it with fn=test and withinDescribe=it ',
141+
rules['consistent-test-it'],
142+
{
143+
valid: [
144+
{
145+
code: 'test("foo")',
146+
options: [{ fn: 'test', withinDescribe: 'it' }],
147+
},
148+
{
149+
code: 'test.only("foo")',
150+
options: [{ fn: 'test', withinDescribe: 'it' }],
151+
},
152+
{
153+
code: 'test.skip("foo")',
154+
options: [{ fn: 'test', withinDescribe: 'it' }],
155+
},
156+
{
157+
code: 'xtest("foo")',
158+
options: [{ fn: 'test', withinDescribe: 'it' }],
159+
},
160+
{
161+
code: '[1,2,3].forEach(() => { test("foo") })',
162+
options: [{ fn: 'test', withinDescribe: 'it' }],
163+
},
164+
],
165+
invalid: [
166+
{
167+
code: 'describe("suite", () => { test("foo") })',
168+
options: [{ fn: 'test', withinDescribe: 'it' }],
169+
errors: [
170+
{ message: "Prefer using 'it' instead of 'test' within describe" },
171+
],
172+
output: 'describe("suite", () => { it("foo") })',
173+
},
174+
{
175+
code: 'describe("suite", () => { test.only("foo") })',
176+
options: [{ fn: 'test', withinDescribe: 'it' }],
177+
errors: [
178+
{ message: "Prefer using 'it' instead of 'test' within describe" },
179+
],
180+
output: 'describe("suite", () => { it.only("foo") })',
181+
},
182+
{
183+
code: 'describe("suite", () => { xtest("foo") })',
184+
options: [{ fn: 'test', withinDescribe: 'it' }],
185+
errors: [
186+
{ message: "Prefer using 'it' instead of 'test' within describe" },
187+
],
188+
output: 'describe("suite", () => { xit("foo") })',
189+
},
190+
{
191+
code: 'describe("suite", () => { test.skip("foo") })',
192+
options: [{ fn: 'test', withinDescribe: 'it' }],
193+
errors: [
194+
{ message: "Prefer using 'it' instead of 'test' within describe" },
195+
],
196+
output: 'describe("suite", () => { it.skip("foo") })',
197+
},
198+
],
199+
}
200+
);
201+
202+
ruleTester.run(
203+
'consistent-test-it with fn=it and withinDescribe=test ',
204+
rules['consistent-test-it'],
205+
{
206+
valid: [
207+
{
208+
code: 'it("foo")',
209+
options: [{ fn: 'it', withinDescribe: 'test' }],
210+
},
211+
{
212+
code: 'it.only("foo")',
213+
options: [{ fn: 'it', withinDescribe: 'test' }],
214+
},
215+
{
216+
code: 'it.skip("foo")',
217+
options: [{ fn: 'it', withinDescribe: 'test' }],
218+
},
219+
{
220+
code: 'xit("foo")',
221+
options: [{ fn: 'it', withinDescribe: 'test' }],
222+
},
223+
{
224+
code: '[1,2,3].forEach(() => { it("foo") })',
225+
options: [{ fn: 'it', withinDescribe: 'test' }],
226+
},
227+
],
228+
invalid: [
229+
{
230+
code: 'describe("suite", () => { it("foo") })',
231+
options: [{ fn: 'it', withinDescribe: 'test' }],
232+
errors: [
233+
{ message: "Prefer using 'test' instead of 'it' within describe" },
234+
],
235+
output: 'describe("suite", () => { test("foo") })',
236+
},
237+
{
238+
code: 'describe("suite", () => { it.only("foo") })',
239+
options: [{ fn: 'it', withinDescribe: 'test' }],
240+
errors: [
241+
{ message: "Prefer using 'test' instead of 'it' within describe" },
242+
],
243+
output: 'describe("suite", () => { test.only("foo") })',
244+
},
245+
{
246+
code: 'describe("suite", () => { xit("foo") })',
247+
options: [{ fn: 'it', withinDescribe: 'test' }],
248+
errors: [
249+
{ message: "Prefer using 'test' instead of 'it' within describe" },
250+
],
251+
output: 'describe("suite", () => { xtest("foo") })',
252+
},
253+
{
254+
code: 'describe("suite", () => { it.skip("foo") })',
255+
options: [{ fn: 'it', withinDescribe: 'test' }],
256+
errors: [
257+
{ message: "Prefer using 'test' instead of 'it' within describe" },
258+
],
259+
output: 'describe("suite", () => { test.skip("foo") })',
260+
},
261+
],
262+
}
263+
);
264+
265+
ruleTester.run(
266+
'consistent-test-it with fn=test and withinDescribe=test ',
267+
rules['consistent-test-it'],
268+
{
269+
valid: [
270+
{
271+
code: 'describe("suite", () => { test("foo") })',
272+
options: [{ fn: 'test', withinDescribe: 'test' }],
273+
},
274+
{
275+
code: 'test("foo");',
276+
options: [{ fn: 'test', withinDescribe: 'test' }],
277+
},
278+
],
279+
invalid: [
280+
{
281+
code: 'describe("suite", () => { it("foo") })',
282+
options: [{ fn: 'test', withinDescribe: 'test' }],
283+
errors: [
284+
{ message: "Prefer using 'test' instead of 'it' within describe" },
285+
],
286+
output: 'describe("suite", () => { test("foo") })',
287+
},
288+
{
289+
code: 'it("foo")',
290+
options: [{ fn: 'test', withinDescribe: 'test' }],
291+
errors: [{ message: "Prefer using 'test' instead of 'it'" }],
292+
output: 'test("foo")',
293+
},
294+
],
295+
}
296+
);
297+
298+
ruleTester.run(
299+
'consistent-test-it with fn=it and withinDescribe=it ',
300+
rules['consistent-test-it'],
301+
{
302+
valid: [
303+
{
304+
code: 'describe("suite", () => { it("foo") })',
305+
options: [{ fn: 'it', withinDescribe: 'it' }],
306+
},
307+
{
308+
code: 'it("foo")',
309+
options: [{ fn: 'it', withinDescribe: 'it' }],
310+
},
311+
],
312+
invalid: [
313+
{
314+
code: 'describe("suite", () => { test("foo") })',
315+
options: [{ fn: 'it', withinDescribe: 'it' }],
316+
errors: [
317+
{ message: "Prefer using 'it' instead of 'test' within describe" },
318+
],
319+
output: 'describe("suite", () => { it("foo") })',
320+
},
321+
{
322+
code: 'test("foo")',
323+
options: [{ fn: 'it', withinDescribe: 'it' }],
324+
errors: [{ message: "Prefer using 'it' instead of 'test'" }],
325+
output: 'it("foo")',
326+
},
327+
],
328+
}
329+
);
330+
331+
ruleTester.run(
332+
'consistent-test-it defaults without config object',
333+
rules['consistent-test-it'],
334+
{
335+
valid: [
336+
{
337+
code: 'test("foo")',
338+
},
339+
],
340+
invalid: [
341+
{
342+
code: 'describe("suite", () => { test("foo") })',
343+
errors: [
344+
{ message: "Prefer using 'it' instead of 'test' within describe" },
345+
],
346+
output: 'describe("suite", () => { it("foo") })',
347+
},
348+
],
349+
}
350+
);
351+
352+
ruleTester.run(
353+
'consistent-test-it with withinDescribe=it',
354+
rules['consistent-test-it'],
355+
{
356+
valid: [
357+
{
358+
code: 'test("foo")',
359+
options: [{ withinDescribe: 'it' }],
360+
},
361+
{
362+
code: 'describe("suite", () => { it("foo") })',
363+
options: [{ withinDescribe: 'it' }],
364+
},
365+
],
366+
invalid: [
367+
{
368+
code: 'it("foo")',
369+
options: [{ withinDescribe: 'it' }],
370+
errors: [{ message: "Prefer using 'test' instead of 'it'" }],
371+
output: 'test("foo")',
372+
},
373+
{
374+
code: 'describe("suite", () => { test("foo") })',
375+
options: [{ withinDescribe: 'it' }],
376+
errors: [
377+
{ message: "Prefer using 'it' instead of 'test' within describe" },
378+
],
379+
output: 'describe("suite", () => { it("foo") })',
380+
},
381+
],
382+
}
383+
);
384+
385+
ruleTester.run(
386+
'consistent-test-it with withinDescribe=test',
387+
rules['consistent-test-it'],
388+
{
389+
valid: [
390+
{
391+
code: 'test("foo")',
392+
options: [{ withinDescribe: 'test' }],
393+
},
394+
{
395+
code: 'describe("suite", () => { test("foo") })',
396+
options: [{ withinDescribe: 'test' }],
397+
},
398+
],
399+
invalid: [
400+
{
401+
code: 'it("foo")',
402+
options: [{ withinDescribe: 'test' }],
403+
errors: [{ message: "Prefer using 'test' instead of 'it'" }],
404+
output: 'test("foo")',
405+
},
406+
{
407+
code: 'describe("suite", () => { it("foo") })',
408+
options: [{ withinDescribe: 'test' }],
409+
errors: [
410+
{ message: "Prefer using 'test' instead of 'it' within describe" },
411+
],
412+
output: 'describe("suite", () => { test("foo") })',
413+
},
414+
],
415+
}
416+
);

Diff for: ‎rules/consistent-test-it.js

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
'use strict';
2+
3+
const getNodeName = require('./util').getNodeName;
4+
const isTestCase = require('./util').isTestCase;
5+
const isDescribe = require('./util').isDescribe;
6+
7+
module.exports = {
8+
meta: {
9+
docs: {
10+
url:
11+
'https://github.com/jest-community/eslint-plugin-jest/blob/master/docs/rules/consistent-test-it.md',
12+
},
13+
fixable: 'code',
14+
schema: [
15+
{
16+
type: 'object',
17+
properties: {
18+
fn: {
19+
enum: ['it', 'test'],
20+
},
21+
withinDescribe: {
22+
enum: ['it', 'test'],
23+
},
24+
},
25+
additionalProperties: false,
26+
},
27+
],
28+
},
29+
create(context) {
30+
const configObj = context.options[0] || {};
31+
const testKeyword = configObj.fn || 'test';
32+
const testKeywordWithinDescribe =
33+
configObj.withinDescribe || configObj.fn || 'it';
34+
35+
let describeNestingLevel = 0;
36+
37+
return {
38+
CallExpression(node) {
39+
const nodeName = getNodeName(node.callee);
40+
41+
if (isDescribe(node)) {
42+
describeNestingLevel++;
43+
}
44+
45+
if (
46+
isTestCase(node) &&
47+
describeNestingLevel === 0 &&
48+
nodeName.indexOf(testKeyword) === -1
49+
) {
50+
const oppositeTestKeyword = getOppositeTestKeyword(testKeyword);
51+
52+
context.report({
53+
message:
54+
"Prefer using '{{ testKeyword }}' instead of '{{ oppositeTestKeyword }}'",
55+
node: node.callee,
56+
data: { testKeyword, oppositeTestKeyword },
57+
fix(fixer) {
58+
const nodeToReplace =
59+
node.callee.type === 'MemberExpression'
60+
? node.callee.object
61+
: node.callee;
62+
63+
const fixedNodeName = getPreferredNodeName(nodeName, testKeyword);
64+
return [fixer.replaceText(nodeToReplace, fixedNodeName)];
65+
},
66+
});
67+
}
68+
69+
if (
70+
isTestCase(node) &&
71+
describeNestingLevel > 0 &&
72+
nodeName.indexOf(testKeywordWithinDescribe) === -1
73+
) {
74+
const oppositeTestKeyword = getOppositeTestKeyword(
75+
testKeywordWithinDescribe
76+
);
77+
78+
context.report({
79+
message:
80+
"Prefer using '{{ testKeywordWithinDescribe }}' instead of '{{ oppositeTestKeyword }}' within describe",
81+
node: node.callee,
82+
data: { testKeywordWithinDescribe, oppositeTestKeyword },
83+
fix(fixer) {
84+
const nodeToReplace =
85+
node.callee.type === 'MemberExpression'
86+
? node.callee.object
87+
: node.callee;
88+
89+
const fixedNodeName = getPreferredNodeName(
90+
nodeName,
91+
testKeywordWithinDescribe
92+
);
93+
return [fixer.replaceText(nodeToReplace, fixedNodeName)];
94+
},
95+
});
96+
}
97+
},
98+
'CallExpression:exit'(node) {
99+
if (isDescribe(node)) {
100+
describeNestingLevel--;
101+
}
102+
},
103+
};
104+
},
105+
};
106+
107+
function getPreferredNodeName(nodeName, preferredTestKeyword) {
108+
switch (nodeName) {
109+
case 'fit':
110+
return 'test.only';
111+
default:
112+
return nodeName.startsWith('f') || nodeName.startsWith('x')
113+
? nodeName.charAt(0) + preferredTestKeyword
114+
: preferredTestKeyword;
115+
}
116+
}
117+
118+
function getOppositeTestKeyword(test) {
119+
if (test === 'test') {
120+
return 'it';
121+
}
122+
123+
return 'test';
124+
}

Diff for: ‎rules/no-identical-title.js

+1-24
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,7 @@
11
'use strict';
22

33
const isDescribe = require('./util').isDescribe;
4-
5-
const testCaseNames = Object.assign(Object.create(null), {
6-
fit: true,
7-
it: true,
8-
'it.only': true,
9-
'it.skip': true,
10-
test: true,
11-
'test.only': true,
12-
'test.skip': true,
13-
xit: true,
14-
xtest: true,
15-
});
16-
17-
const getNodeName = node => {
18-
if (node.type === 'MemberExpression') {
19-
return node.object.name + '.' + node.property.name;
20-
}
21-
return node.name;
22-
};
23-
24-
const isTestCase = node =>
25-
node &&
26-
node.type === 'CallExpression' &&
27-
testCaseNames[getNodeName(node.callee)];
4+
const isTestCase = require('./util').isTestCase;
285

296
const newDescribeContext = () => ({
307
describeTitles: [],

Diff for: ‎rules/util.js

+19
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,30 @@ const describeAliases = Object.assign(Object.create(null), {
8888
xdescribe: true,
8989
});
9090

91+
const testCaseNames = Object.assign(Object.create(null), {
92+
fit: true,
93+
it: true,
94+
'it.only': true,
95+
'it.skip': true,
96+
test: true,
97+
'test.only': true,
98+
'test.skip': true,
99+
xit: true,
100+
xtest: true,
101+
});
102+
91103
const getNodeName = node => {
92104
if (node.type === 'MemberExpression') {
93105
return node.object.name + '.' + node.property.name;
94106
}
95107
return node.name;
96108
};
97109

110+
const isTestCase = node =>
111+
node &&
112+
node.type === 'CallExpression' &&
113+
testCaseNames[getNodeName(node.callee)];
114+
98115
const isDescribe = node =>
99116
node.type === 'CallExpression' && describeAliases[getNodeName(node.callee)];
100117

@@ -116,6 +133,8 @@ module.exports = {
116133
expectNotToEqualCase: expectNotToEqualCase,
117134
expectToBeUndefinedCase: expectToBeUndefinedCase,
118135
expectNotToBeUndefinedCase: expectNotToBeUndefinedCase,
136+
getNodeName: getNodeName,
119137
isDescribe: isDescribe,
120138
isFunction: isFunction,
139+
isTestCase: isTestCase,
121140
};

0 commit comments

Comments
 (0)
Please sign in to comment.