Skip to content

Commit 0933d82

Browse files
doniyor2109SimenB
authored andcommitted
feat(rules): add prefer-todo rule (#218)
Fixes #217
1 parent 8dd5a80 commit 0933d82

File tree

6 files changed

+183
-0
lines changed

6 files changed

+183
-0
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ for more information about extending configuration files.
118118
| [valid-describe][] | Enforce valid `describe()` callback | ![recommended][] | |
119119
| [valid-expect-in-promise][] | Enforce having return statement when testing with promises | ![recommended][] | |
120120
| [valid-expect][] | Enforce valid `expect()` usage | ![recommended][] | |
121+
| [prefer-todo][] | Suggest using `test.todo()` | | ![fixable-green][] |
121122

122123
## Credit
123124

@@ -151,6 +152,7 @@ for more information about extending configuration files.
151152
[valid-describe]: docs/rules/valid-describe.md
152153
[valid-expect-in-promise]: docs/rules/valid-expect-in-promise.md
153154
[valid-expect]: docs/rules/valid-expect.md
155+
[prefer-todo]: docs/rules/prefer-todo.md
154156
[fixable-green]: https://img.shields.io/badge/-fixable-green.svg
155157
[fixable-yellow]: https://img.shields.io/badge/-fixable-yellow.svg
156158
[recommended]: https://img.shields.io/badge/-recommended-lightgrey.svg

docs/rules/prefer-todo.md

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Suggest using `test.todo` (prefer-todo)
2+
3+
When test cases are empty then it is better to mark them as `test.todo` as it
4+
will be highlighted in the summary output.
5+
6+
## Rule details
7+
8+
This rule triggers a warning if empty test case is used without 'test.todo'.
9+
10+
```js
11+
test('i need to write this test');
12+
```
13+
14+
### Default configuration
15+
16+
The following pattern is considered warning:
17+
18+
```js
19+
test('i need to write this test'); // Unimplemented test case
20+
test('i need to write this test', () => {}); // Empty test case body
21+
test.skip('i need to write this test', () => {}); // Empty test case body
22+
```
23+
24+
The following pattern is not warning:
25+
26+
```js
27+
test.todo('i need to write this test');
28+
```

index.js

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const requireTothrowMessage = require('./rules/require-tothrow-message');
2727
const noAliasMethods = require('./rules/no-alias-methods');
2828
const noTestCallback = require('./rules/no-test-callback');
2929
const noTruthyFalsy = require('./rules/no-truthy-falsy');
30+
const preferTodo = require('./rules/prefer-todo');
3031

3132
const snapshotProcessor = require('./processors/snapshot-processor');
3233

@@ -114,5 +115,6 @@ module.exports = {
114115
'no-alias-methods': noAliasMethods,
115116
'no-test-callback': noTestCallback,
116117
'no-truthy-falsy': noTruthyFalsy,
118+
'prefer-todo': preferTodo,
117119
},
118120
};

rules/__tests__/prefer-todo.test.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
'use strict';
2+
3+
const { RuleTester } = require('eslint');
4+
const rule = require('../prefer-todo');
5+
6+
const ruleTester = new RuleTester({
7+
parserOptions: { ecmaVersion: 2015 },
8+
});
9+
10+
ruleTester.run('prefer-todo', rule, {
11+
valid: [
12+
'test.todo("i need to write this test");',
13+
'test(obj)',
14+
'fit("foo")',
15+
'xit("foo")',
16+
'test("stub", () => expect(1).toBe(1));',
17+
`
18+
supportsDone && params.length < test.length
19+
? done => test(...params, done)
20+
: () => test(...params);
21+
`,
22+
],
23+
invalid: [
24+
{
25+
code: `test("i need to write this test");`,
26+
errors: [
27+
{ message: 'Prefer todo test case over unimplemented test case' },
28+
],
29+
output: 'test.todo("i need to write this test");',
30+
},
31+
{
32+
code: 'test(`i need to write this test`);',
33+
errors: [
34+
{ message: 'Prefer todo test case over unimplemented test case' },
35+
],
36+
output: 'test.todo(`i need to write this test`);',
37+
},
38+
{
39+
code: 'it("foo", function () {})',
40+
errors: ['Prefer todo test case over empty test case'],
41+
output: 'it.todo("foo")',
42+
},
43+
{
44+
code: 'it("foo", () => {})',
45+
errors: ['Prefer todo test case over empty test case'],
46+
output: 'it.todo("foo")',
47+
},
48+
{
49+
code: `test.skip("i need to write this test", () => {});`,
50+
errors: ['Prefer todo test case over empty test case'],
51+
output: 'test.todo("i need to write this test");',
52+
},
53+
{
54+
code: `test.skip("i need to write this test", function() {});`,
55+
errors: ['Prefer todo test case over empty test case'],
56+
output: 'test.todo("i need to write this test");',
57+
},
58+
],
59+
});

rules/prefer-todo.js

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
'use strict';
2+
3+
const {
4+
getDocsUrl,
5+
isFunction,
6+
composeFixers,
7+
getNodeName,
8+
isString,
9+
} = require('./util');
10+
11+
function isOnlyTestTitle(node) {
12+
return node.arguments.length === 1;
13+
}
14+
15+
function isFunctionBodyEmpty(node) {
16+
return node.body.body && !node.body.body.length;
17+
}
18+
19+
function isTestBodyEmpty(node) {
20+
const fn = node.arguments[1]; // eslint-disable-line prefer-destructuring
21+
return fn && isFunction(fn) && isFunctionBodyEmpty(fn);
22+
}
23+
24+
function addTodo(node, fixer) {
25+
const testName = getNodeName(node.callee)
26+
.split('.')
27+
.shift();
28+
return fixer.replaceText(node.callee, `${testName}.todo`);
29+
}
30+
31+
function removeSecondArg({ arguments: [first, second] }, fixer) {
32+
return fixer.removeRange([first.range[1], second.range[1]]);
33+
}
34+
35+
function isFirstArgString({ arguments: [firstArg] }) {
36+
return firstArg && isString(firstArg);
37+
}
38+
39+
const isTestCase = node =>
40+
node &&
41+
node.type === 'CallExpression' &&
42+
['it', 'test', 'it.skip', 'test.skip'].includes(getNodeName(node.callee));
43+
44+
function create(context) {
45+
return {
46+
CallExpression(node) {
47+
if (isTestCase(node) && isFirstArgString(node)) {
48+
const combineFixers = composeFixers(node);
49+
50+
if (isTestBodyEmpty(node)) {
51+
context.report({
52+
message: 'Prefer todo test case over empty test case',
53+
node,
54+
fix: combineFixers(removeSecondArg, addTodo),
55+
});
56+
}
57+
58+
if (isOnlyTestTitle(node)) {
59+
context.report({
60+
message: 'Prefer todo test case over unimplemented test case',
61+
node,
62+
fix: combineFixers(addTodo),
63+
});
64+
}
65+
}
66+
},
67+
};
68+
}
69+
70+
module.exports = {
71+
create,
72+
meta: {
73+
docs: {
74+
url: getDocsUrl(__filename),
75+
},
76+
fixable: 'code',
77+
},
78+
};

rules/util.js

+14
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ const isDescribe = node =>
130130
const isFunction = node =>
131131
node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';
132132

133+
const isString = node =>
134+
(node.type === 'Literal' && typeof node.value === 'string') ||
135+
node.type === 'TemplateLiteral';
136+
133137
/**
134138
* Generates the URL to documentation for the given rule name. It uses the
135139
* package version to build the link to a tagged version of the
@@ -182,6 +186,14 @@ const scopeHasLocalReference = (scope, referenceName) => {
182186
);
183187
};
184188

189+
function composeFixers(node) {
190+
return (...fixers) => {
191+
return fixerApi => {
192+
return fixers.reduce((all, fixer) => [...all, fixer(node, fixerApi)], []);
193+
};
194+
};
195+
}
196+
185197
module.exports = {
186198
method,
187199
method2,
@@ -199,6 +211,8 @@ module.exports = {
199211
isDescribe,
200212
isFunction,
201213
isTestCase,
214+
isString,
202215
getDocsUrl,
203216
scopeHasLocalReference,
217+
composeFixers,
204218
};

0 commit comments

Comments
 (0)