From 1b2d24bd46815a6f40f2d7b7aba99df1e108abf9 Mon Sep 17 00:00:00 2001 From: Suganya Gnanasekar Date: Tue, 22 Oct 2019 06:06:23 +0530 Subject: [PATCH] feat(rules): prefer valid-title (#273) (#433) --- docs/rules/valid-title.md | 65 ++++++++++ src/__tests__/rules.test.ts | 2 +- src/rules/__tests__/valid-title.test.ts | 161 ++++++++++++++++++++++++ src/rules/valid-title.ts | 64 ++++++++++ 4 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 docs/rules/valid-title.md create mode 100644 src/rules/__tests__/valid-title.test.ts create mode 100644 src/rules/valid-title.ts diff --git a/docs/rules/valid-title.md b/docs/rules/valid-title.md new file mode 100644 index 000000000..933b827e5 --- /dev/null +++ b/docs/rules/valid-title.md @@ -0,0 +1,65 @@ +# Disallow duplicate setup and teardown hooks (no-duplicate-hooks) + +A describe/ test block should not contain accidentalSpace or duplicatePrefix. + +## Rule Details + +**duplicatePrefix** + +A describe/ test block should not start with duplicatePrefix + +Examples of **incorrect** code for this rule + +```js +test('test foo', () => {}); +it('it foo', () => {}); + +describe('foo', () => { + test('test bar', () => {}); +}); + +describe('describe foo', () => { + test('bar', () => {}); +}); +``` + +Examples of **correct** code for this rule + +```js +test('foo', () => {}); +it('foo', () => {}); + +describe('foo', () => { + test('bar', () => {}); +}); +``` + +**accidentalSpace** + +A describe/ test block should not contain accidentalSpace + +Examples of **incorrect** code for this rule + +```js +test(' foo', () => {}); +it(' foo', () => {}); + +describe('foo', () => { + test(' bar', () => {}); +}); + +describe(' foo', () => { + test('bar', () => {}); +}); +``` + +Examples of **correct** code for this rule + +```js +test('foo', () => {}); +it('foo', () => {}); + +describe('foo', () => { + test('bar', () => {}); +}); +``` diff --git a/src/__tests__/rules.test.ts b/src/__tests__/rules.test.ts index 6b6e12fcc..4fb7048be 100644 --- a/src/__tests__/rules.test.ts +++ b/src/__tests__/rules.test.ts @@ -3,7 +3,7 @@ import { resolve } from 'path'; import plugin from '../'; const ruleNames = Object.keys(plugin.rules); -const numberOfRules = 40; +const numberOfRules = 41; describe('rules', () => { it('should have a corresponding doc for each rule', () => { diff --git a/src/rules/__tests__/valid-title.test.ts b/src/rules/__tests__/valid-title.test.ts new file mode 100644 index 000000000..5f4d7edb6 --- /dev/null +++ b/src/rules/__tests__/valid-title.test.ts @@ -0,0 +1,161 @@ +import { TSESLint } from '@typescript-eslint/experimental-utils'; +import rule from '../valid-title'; + +const ruleTester = new TSESLint.RuleTester({ + parserOptions: { + ecmaVersion: 8, + }, +}); + +ruleTester.run('no-accidental-space', rule, { + valid: [ + 'describe("foo", function () {})', + 'describe(6, function () {})', + 'fdescribe("foo", function () {})', + 'xdescribe("foo", function () {})', + 'it("foo", function () {})', + 'fit("foo", function () {})', + 'xit("foo", function () {})', + 'test("foo", function () {})', + 'xtest("foo", function () {})', + 'someFn("foo", function () {})', + ` + describe('foo', () => { + it('bar', () => {}) + }) + `, + ], + invalid: [ + { + code: 'describe(" foo", function () {})', + errors: [{ messageId: 'accidentalSpace', column: 1, line: 1 }], + }, + { + code: 'fdescribe(" foo", function () {})', + errors: [{ messageId: 'accidentalSpace', column: 1, line: 1 }], + }, + { + code: 'xdescribe(" foo", function () {})', + errors: [{ messageId: 'accidentalSpace', column: 1, line: 1 }], + }, + { + code: 'it(" foo", function () {})', + errors: [{ messageId: 'accidentalSpace', column: 1, line: 1 }], + }, + { + code: 'fit(" foo", function () {})', + errors: [{ messageId: 'accidentalSpace', column: 1, line: 1 }], + }, + { + code: 'xit(" foo", function () {})', + errors: [{ messageId: 'accidentalSpace', column: 1, line: 1 }], + }, + { + code: 'test(" foo", function () {})', + errors: [{ messageId: 'accidentalSpace', column: 1, line: 1 }], + }, + { + code: 'xtest(" foo", function () {})', + errors: [{ messageId: 'accidentalSpace', column: 1, line: 1 }], + }, + { + code: ` + describe(' foo', () => { + it('bar', () => {}) + }) + `, + errors: [{ messageId: 'accidentalSpace', column: 7, line: 2 }], + }, + { + code: ` + describe('foo', () => { + it(' bar', () => {}) + }) + `, + errors: [{ messageId: 'accidentalSpace', column: 9, line: 3 }], + }, + ], +}); + +ruleTester.run('no-duplicate-prefix ft describe', rule, { + valid: [ + 'describe("foo", function () {})', + 'fdescribe("foo", function () {})', + 'xdescribe("foo", function () {})', + ], + invalid: [ + { + code: 'describe("describe foo", function () {})', + errors: [{ messageId: 'duplicatePrefix', column: 1, line: 1 }], + }, + { + code: 'fdescribe("describe foo", function () {})', + errors: [{ messageId: 'duplicatePrefix', column: 1, line: 1 }], + }, + { + code: 'xdescribe("describe foo", function () {})', + errors: [{ messageId: 'duplicatePrefix', column: 1, line: 1 }], + }, + ], +}); + +ruleTester.run('no-duplicate-prefix ft test', rule, { + valid: ['test("foo", function () {})', 'xtest("foo", function () {})'], + invalid: [ + { + code: 'test("test foo", function () {})', + errors: [{ messageId: 'duplicatePrefix', column: 1, line: 1 }], + }, + { + code: 'xtest("test foo", function () {})', + errors: [{ messageId: 'duplicatePrefix', column: 1, line: 1 }], + }, + ], +}); + +ruleTester.run('no-duplicate-prefix ft it', rule, { + valid: [ + 'it("foo", function () {})', + 'fit("foo", function () {})', + 'xit("foo", function () {})', + ], + invalid: [ + { + code: 'it("it foo", function () {})', + errors: [{ messageId: 'duplicatePrefix', column: 1, line: 1 }], + }, + { + code: 'fit("it foo", function () {})', + errors: [{ messageId: 'duplicatePrefix', column: 1, line: 1 }], + }, + { + code: 'xit("it foo", function () {})', + errors: [{ messageId: 'duplicatePrefix', column: 1, line: 1 }], + }, + ], +}); + +ruleTester.run('no-duplicate-prefix ft nested', rule, { + valid: [ + ` + describe('foo', () => { + it('bar', () => {}) + })`, + ], + invalid: [ + { + code: ` + describe('describe foo', () => { + it('bar', () => {}) + })`, + errors: [{ messageId: 'duplicatePrefix', column: 7, line: 2 }], + }, + { + code: ` + describe('foo', () => { + it('it bar', () => {}) + })`, + errors: [{ messageId: 'duplicatePrefix', column: 9, line: 3 }], + }, + ], +}); diff --git a/src/rules/valid-title.ts b/src/rules/valid-title.ts new file mode 100644 index 000000000..3ba3dd787 --- /dev/null +++ b/src/rules/valid-title.ts @@ -0,0 +1,64 @@ +import { + createRule, + getNodeName, + getStringValue, + isDescribe, + isStringNode, + isTestCase, +} from './utils'; + +import { TSESTree } from '@typescript-eslint/experimental-utils'; + +const trimFXprefix = (word: string) => + ['f', 'x'].includes(word.charAt(0)) ? word.substr(1) : word; + +const getNodeTitle = (node: TSESTree.CallExpression): string | null => { + const [argument] = node.arguments; + + return isStringNode(argument) ? getStringValue(argument) : null; +}; + +export default createRule({ + name: __filename, + meta: { + docs: { + category: 'Best Practices', + description: 'Enforce valid titles', + recommended: false, + }, + messages: { + duplicatePrefix: 'should not have duplicate prefix', + accidentalSpace: 'should not have space in the beginning', + }, + type: 'suggestion', + schema: [], + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node) { + if (!isDescribe(node) && !isTestCase(node)) return; + + const title = getNodeTitle(node); + if (!title) return; + + if (title.trimLeft().length !== title.length) { + context.report({ + messageId: 'accidentalSpace', + node, + }); + } + + const nodeName = trimFXprefix(getNodeName(node.callee)); + const [firstWord] = title.split(' '); + + if (firstWord.toLowerCase() === nodeName) { + context.report({ + messageId: 'duplicatePrefix', + node, + }); + } + }, + }; + }, +});