Skip to content

Commit

Permalink
Add no-identical-title rule (fixes #33) (#97)
Browse files Browse the repository at this point in the history
* Add `no-identical-title` rule (fixes #33)

* Fix doc errors
  • Loading branch information
jfmengels authored and lo1tuma committed Aug 27, 2016
1 parent bbf05ea commit 42d1322
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
* [no-hooks](no-hooks.md) - disallow hooks
* [no-hooks-for-single-case](no-hooks-for-single-case.md) - disallow hooks for a single test or test suite
* [no-top-level-hooks](no-top-level-hooks.md) - disallow top-level hooks
* [no-identical-title](no-identical-title.md) - disallow identical titles
45 changes: 45 additions & 0 deletions docs/rules/no-identical-title.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Disallow identical titles (no-identical-title)

Having identical titles for two different tests or test suites may create confusion. For example, when a test with the same title as another test in the same test suite fails, it is harder to know which one failed and thus harder to fix.

## Rule Details

This rule looks at the title of every test and test suites. It will report when two test suites or two test cases at the same level of a test suite have the same title.

The following patterns are considered warnings:

```js
describe('foo', function () {
it('should do bar', function() {});
it('should do bar', function() {}); // Has the same title as the previous test

describe('baz', function() {
// ...
});

describe('baz', function() { // Has the same title as a previous test suite
// ...
});
});
```

These patterns would not be considered warnings:

```js
describe('foo', function () {
it('should do foo', function() {});
it('should do bar', function() {});

// Has the same name as a parent test suite, which is fine
describe('foo', function() {
// Has the same name as a test in a parent test suite, which is fine
it('should do foo', function() {});
it('should work', function() {});
});

describe('baz', function() { // Has the same title as a previous test suite
// Has the same name as a test in a sibling test suite, which is fine
it('should work', function() {});
});
});
```
3 changes: 2 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ module.exports = {
'no-hooks': require('./lib/rules/no-hooks'),
'no-hooks-for-single-case': require('./lib/rules/no-hooks-for-single-case'),
'no-sibling-hooks': require('./lib/rules/no-sibling-hooks'),
'no-top-level-hooks': require('./lib/rules/no-top-level-hooks')
'no-top-level-hooks': require('./lib/rules/no-top-level-hooks'),
'no-identical-title': require('./lib/rules/no-identical-title')
},
configs: {
recommended: {
Expand Down
60 changes: 60 additions & 0 deletions lib/rules/no-identical-title.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict';

var astUtil = require('../util/ast');

function newLayer() {
return {
describeTitles: [],
testTitles: []
};
}

function handlTestCaseTitles(context, titles, node, title) {
if (astUtil.isTestCase(node)) {
if (titles.indexOf(title) !== -1) {
context.report({
node: node,
message: 'Test title is used multiple times in the same test suite.'
});
}
titles.push(title);
}
}

function handlTestSuiteTitles(context, layers, node, title) {
var currentLayer = layers[layers.length - 1];
if (astUtil.isDescribe(node)) {
if (currentLayer.describeTitles.indexOf(title) !== -1) {
context.report({
node: node,
message: 'Test suite title is used multiple times.'
});
}
currentLayer.describeTitles.push(title);
layers.push(newLayer());
}
}

module.exports = function (context) {
var titleLayers = [
newLayer()
];
return {
CallExpression: function (node) {
var currentLayer = titleLayers[titleLayers.length - 1],
title;
if (!node.arguments || !node.arguments[0] || node.arguments[0].type !== 'Literal') {
return;
}

title = node.arguments[0].value;
handlTestCaseTitles(context, currentLayer.testTitles, node, title);
handlTestSuiteTitles(context, titleLayers, node, title);
},
'CallExpression:exit': function (node) {
if (astUtil.isDescribe(node)) {
titleLayers.pop();
}
}
};
};
139 changes: 139 additions & 0 deletions test/rules/no-identical-title.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
'use strict';

var RuleTester = require('eslint').RuleTester,
rules = require('../../').rules,
ruleTester = new RuleTester();

ruleTester.run('no-identical-title', rules['no-identical-title'], {

valid: [
[
'describe("describe", function() {',
' it("it", function() {});',
'});'
].join('\n'),
[
'describe("describe1", function() {',
' it("it1", function() {});',
' it("it2", function() {});',
'});'
].join('\n'),
[
'it("it1", function() {});',
'it("it2", function() {});'
].join('\n'),
[
'it.only("it1", function() {});',
'it("it2", function() {});'
].join('\n'),
[
'it.only("it1", function() {});',
'it.only("it2", function() {});'
].join('\n'),
[
'describe("title", function() {});',
'it("title", function() {});'
].join('\n'),
[
'describe("describe1", function() {',
' it("it1", function() {});',
' describe("describe2", function() {',
' it("it1", function() {});',
' });',
'});'
].join('\n'),
[
'describe("describe1", function() {',
' describe("describe2", function() {',
' it("it1", function() {});',
' });',
' it("it1", function() {});',
'});'
].join('\n'),
[
'describe("describe1", function() {',
' describe("describe2", function() {});',
'});'
].join('\n'),
[
'describe("describe1", function() {',
' describe("describe2", function() {});',
'});',
'describe("describe2", function() {});'
].join('\n'),
[
'describe("describe1", function() {});',
'describe("describe2", function() {});'
].join('\n'),
[
'it("it" + n, function() {});',
'it("it" + n, function() {});'
].join('\n'),
{
code: [
'it(`it${n}`, function() {});',
'it(`it${n}`, function() {});'
].join('\n'),
env: {
es6: true
}
}
],

invalid: [
{
code: [
'describe("describe1", function() {',
' it("it1", function() {});',
' it("it1", function() {});',
'});'
].join('\n'),
errors: [ { message: 'Test title is used multiple times in the same test suite.', column: 4, line: 3 } ]
},
{
code: [
'it("it1", function() {});',
'it("it1", function() {});'
].join('\n'),
errors: [ { message: 'Test title is used multiple times in the same test suite.', column: 1, line: 2 } ]
},
{
code: [
'it.only("it1", function() {});',
'it("it1", function() {});'
].join('\n'),
errors: [ { message: 'Test title is used multiple times in the same test suite.', column: 1, line: 2 } ]
},
{
code: [
'it.only("it1", function() {});',
'it.only("it1", function() {});'
].join('\n'),
errors: [ { message: 'Test title is used multiple times in the same test suite.', column: 1, line: 2 } ]
},
{
code: [
'it("it1", function() {});',
'specify("it1", function() {});'
].join('\n'),
errors: [ { message: 'Test title is used multiple times in the same test suite.', column: 1, line: 2 } ]
},
{
code: [
'describe("describe1", function() {});',
'describe("describe1", function() {});'
].join('\n'),
errors: [ { message: 'Test suite title is used multiple times.', column: 1, line: 2 } ]
},
{
code: [
'describe("describe1", function() {',
' describe("describe2", function() {});',
'});',
'describe("describe1", function() {});'
].join('\n'),
errors: [ { message: 'Test suite title is used multiple times.', column: 1, line: 4 } ]
}
]

});

0 comments on commit 42d1322

Please sign in to comment.