From e72ba01e91ca2c8523e5af0fa3c6a962e9f226a0 Mon Sep 17 00:00:00 2001 From: Suganya Date: Thu, 17 Oct 2019 06:26:58 +0530 Subject: [PATCH] feat(rules): prefer valid-title (#273) --- docs/rules/valid-title.md | 65 ++++++++++ src/rules/__tests__/valid-title.test.ts | 160 ++++++++++++++++++++++++ src/rules/utils.ts | 10 ++ src/rules/valid-title.ts | 56 +++++++++ 4 files changed, 291 insertions(+) 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/rules/__tests__/valid-title.test.ts b/src/rules/__tests__/valid-title.test.ts new file mode 100644 index 000000000..f3f4b7982 --- /dev/null +++ b/src/rules/__tests__/valid-title.test.ts @@ -0,0 +1,160 @@ +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 () {})', + '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/utils.ts b/src/rules/utils.ts index ebe7c4d4c..ab892ea73 100644 --- a/src/rules/utils.ts +++ b/src/rules/utils.ts @@ -652,6 +652,16 @@ export const isDescribe = ( ); }; +export const getNodeTitle = (node: TSESTree.CallExpression): string | null => { + const [argument] = node.arguments; + + if (!isStringNode(argument) || AST_NODE_TYPES.Literal !== argument.type) { + return null; + } + + return getAccessorValue(argument); +}; + const collectReferences = (scope: TSESLint.Scope.Scope) => { const locals = new Set(); const unresolved = new Set(); diff --git a/src/rules/valid-title.ts b/src/rules/valid-title.ts new file mode 100644 index 000000000..9ba5792cd --- /dev/null +++ b/src/rules/valid-title.ts @@ -0,0 +1,56 @@ +import { + createRule, + getNodeName, + getNodeTitle, + isDescribe, + isTestCase, +} from './utils'; + +function trimFXprefix(word: string) { + return ['f', 'x'].includes(word.charAt(0)) ? word.substr(1) : word; +} + +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.charAt(0) === ' ') { + context.report({ + messageId: 'accidentalSpace', + node, + }); + } + + const nodeName = trimFXprefix(getNodeName(node.callee)); + const [firstWord] = title.split(' '); + + if (firstWord.toLowerCase() === nodeName) { + context.report({ + messageId: 'duplicatePrefix', + node, + }); + } + }, + }; + }, +});