Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rules): add prefer-todo rule #218

Merged
merged 7 commits into from
Jan 29, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
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
30 changes: 30 additions & 0 deletions docs/rules/prefer-todo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 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');
```

This rule is enabled by default.
doniyor2109 marked this conversation as resolved.
Show resolved Hide resolved

### 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
Original file line number Diff line number Diff line change
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,
},
};
57 changes: 57 additions & 0 deletions rules/__tests__/prefer-todo.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'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)',
'test("stub", () => expect(1).toBe(1));',
doniyor2109 marked this conversation as resolved.
Show resolved Hide resolved
`
supportsDone && params.length < test.length
doniyor2109 marked this conversation as resolved.
Show resolved Hide resolved
? 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");',
},
],
});
76 changes: 76 additions & 0 deletions rules/prefer-todo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use strict';

const {
getDocsUrl,
isTestCase,
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 {
arguments: [, fn],
doniyor2109 marked this conversation as resolved.
Show resolved Hide resolved
} = node;
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);
}

function create(context) {
return {
CallExpression(node) {
if (isTestCase(node) && isFirstArgString(node)) {
doniyor2109 marked this conversation as resolved.
Show resolved Hide resolved
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',
},
};
13 changes: 13 additions & 0 deletions rules/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ const isDescribe = node =>
const isFunction = node =>
node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';

const isString = node =>
node.type === 'Literal' || node.type === 'TemplateLiteral';
doniyor2109 marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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 +185,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 +210,8 @@ module.exports = {
isDescribe,
isFunction,
isTestCase,
isString,
getDocsUrl,
scopeHasLocalReference,
composeFixers,
};