Skip to content

Commit

Permalink
feat(rules): add prefer-todo rule (#218)
Browse files Browse the repository at this point in the history
Fixes #217
  • Loading branch information
doniyor2109 authored and SimenB committed Jan 29, 2019
1 parent 8dd5a80 commit 0933d82
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -118,6 +118,7 @@ for more information about extending configuration files.
| [valid-describe][] | Enforce valid `describe()` callback | ![recommended][] | |
| [valid-expect-in-promise][] | Enforce having return statement when testing with promises | ![recommended][] | |
| [valid-expect][] | Enforce valid `expect()` usage | ![recommended][] | |
| [prefer-todo][] | Suggest using `test.todo()` | | ![fixable-green][] |

## Credit

Expand Down Expand Up @@ -151,6 +152,7 @@ for more information about extending configuration files.
[valid-describe]: docs/rules/valid-describe.md
[valid-expect-in-promise]: docs/rules/valid-expect-in-promise.md
[valid-expect]: docs/rules/valid-expect.md
[prefer-todo]: docs/rules/prefer-todo.md
[fixable-green]: https://img.shields.io/badge/-fixable-green.svg
[fixable-yellow]: https://img.shields.io/badge/-fixable-yellow.svg
[recommended]: https://img.shields.io/badge/-recommended-lightgrey.svg
28 changes: 28 additions & 0 deletions docs/rules/prefer-todo.md
@@ -0,0 +1,28 @@
# Suggest using `test.todo` (prefer-todo)

When test cases are empty then it is better to mark them as `test.todo` as it
will be highlighted in the summary output.

## Rule details

This rule triggers a warning if empty test case is used without 'test.todo'.

```js
test('i need to write this test');
```

### Default configuration

The following pattern is considered warning:

```js
test('i need to write this test'); // Unimplemented test case
test('i need to write this test', () => {}); // Empty test case body
test.skip('i need to write this test', () => {}); // Empty test case body
```

The following pattern is not warning:

```js
test.todo('i need to write this test');
```
2 changes: 2 additions & 0 deletions index.js
Expand Up @@ -27,6 +27,7 @@ const requireTothrowMessage = require('./rules/require-tothrow-message');
const noAliasMethods = require('./rules/no-alias-methods');
const noTestCallback = require('./rules/no-test-callback');
const noTruthyFalsy = require('./rules/no-truthy-falsy');
const preferTodo = require('./rules/prefer-todo');

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

Expand Down Expand Up @@ -114,5 +115,6 @@ module.exports = {
'no-alias-methods': noAliasMethods,
'no-test-callback': noTestCallback,
'no-truthy-falsy': noTruthyFalsy,
'prefer-todo': preferTodo,
},
};
59 changes: 59 additions & 0 deletions rules/__tests__/prefer-todo.test.js
@@ -0,0 +1,59 @@
'use strict';

const { RuleTester } = require('eslint');
const rule = require('../prefer-todo');

const ruleTester = new RuleTester({
parserOptions: { ecmaVersion: 2015 },
});

ruleTester.run('prefer-todo', rule, {
valid: [
'test.todo("i need to write this test");',
'test(obj)',
'fit("foo")',
'xit("foo")',
'test("stub", () => expect(1).toBe(1));',
`
supportsDone && params.length < test.length
? done => test(...params, done)
: () => test(...params);
`,
],
invalid: [
{
code: `test("i need to write this test");`,
errors: [
{ message: 'Prefer todo test case over unimplemented test case' },
],
output: 'test.todo("i need to write this test");',
},
{
code: 'test(`i need to write this test`);',
errors: [
{ message: 'Prefer todo test case over unimplemented test case' },
],
output: 'test.todo(`i need to write this test`);',
},
{
code: 'it("foo", function () {})',
errors: ['Prefer todo test case over empty test case'],
output: 'it.todo("foo")',
},
{
code: 'it("foo", () => {})',
errors: ['Prefer todo test case over empty test case'],
output: 'it.todo("foo")',
},
{
code: `test.skip("i need to write this test", () => {});`,
errors: ['Prefer todo test case over empty test case'],
output: 'test.todo("i need to write this test");',
},
{
code: `test.skip("i need to write this test", function() {});`,
errors: ['Prefer todo test case over empty test case'],
output: 'test.todo("i need to write this test");',
},
],
});
78 changes: 78 additions & 0 deletions rules/prefer-todo.js
@@ -0,0 +1,78 @@
'use strict';

const {
getDocsUrl,
isFunction,
composeFixers,
getNodeName,
isString,
} = require('./util');

function isOnlyTestTitle(node) {
return node.arguments.length === 1;
}

function isFunctionBodyEmpty(node) {
return node.body.body && !node.body.body.length;
}

function isTestBodyEmpty(node) {
const fn = node.arguments[1]; // eslint-disable-line prefer-destructuring
return fn && isFunction(fn) && isFunctionBodyEmpty(fn);
}

function addTodo(node, fixer) {
const testName = getNodeName(node.callee)
.split('.')
.shift();
return fixer.replaceText(node.callee, `${testName}.todo`);
}

function removeSecondArg({ arguments: [first, second] }, fixer) {
return fixer.removeRange([first.range[1], second.range[1]]);
}

function isFirstArgString({ arguments: [firstArg] }) {
return firstArg && isString(firstArg);
}

const isTestCase = node =>
node &&
node.type === 'CallExpression' &&
['it', 'test', 'it.skip', 'test.skip'].includes(getNodeName(node.callee));

function create(context) {
return {
CallExpression(node) {
if (isTestCase(node) && isFirstArgString(node)) {
const combineFixers = composeFixers(node);

if (isTestBodyEmpty(node)) {
context.report({
message: 'Prefer todo test case over empty test case',
node,
fix: combineFixers(removeSecondArg, addTodo),
});
}

if (isOnlyTestTitle(node)) {
context.report({
message: 'Prefer todo test case over unimplemented test case',
node,
fix: combineFixers(addTodo),
});
}
}
},
};
}

module.exports = {
create,
meta: {
docs: {
url: getDocsUrl(__filename),
},
fixable: 'code',
},
};
14 changes: 14 additions & 0 deletions rules/util.js
Expand Up @@ -130,6 +130,10 @@ const isDescribe = node =>
const isFunction = node =>
node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';

const isString = node =>
(node.type === 'Literal' && typeof node.value === 'string') ||
node.type === 'TemplateLiteral';

/**
* Generates the URL to documentation for the given rule name. It uses the
* package version to build the link to a tagged version of the
Expand Down Expand Up @@ -182,6 +186,14 @@ const scopeHasLocalReference = (scope, referenceName) => {
);
};

function composeFixers(node) {
return (...fixers) => {
return fixerApi => {
return fixers.reduce((all, fixer) => [...all, fixer(node, fixerApi)], []);
};
};
}

module.exports = {
method,
method2,
Expand All @@ -199,6 +211,8 @@ module.exports = {
isDescribe,
isFunction,
isTestCase,
isString,
getDocsUrl,
scopeHasLocalReference,
composeFixers,
};

0 comments on commit 0933d82

Please sign in to comment.