From 2e755dd6c8b3fab0d6379f79fe9d3517db452b08 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 21 Jun 2020 18:57:10 +1200 Subject: [PATCH] feat(valid-title): support `mustMatch` option Closes #233 --- docs/rules/valid-title.md | 39 ++++++++++- src/rules/__tests__/valid-title.test.ts | 91 +++++++++++++++++++++++++ src/rules/valid-title.ts | 63 +++++++++++++++-- 3 files changed, 185 insertions(+), 8 deletions(-) diff --git a/docs/rules/valid-title.md b/docs/rules/valid-title.md index 6ab5cfbb0..a109504f8 100644 --- a/docs/rules/valid-title.md +++ b/docs/rules/valid-title.md @@ -152,9 +152,10 @@ describe('foo', () => { ## Options ```ts -interface { +interface Options { ignoreTypeOfDescribeName?: boolean; disallowedWords?: string[]; + mustMatch?: Partial> | string; } ``` @@ -172,7 +173,7 @@ Default: `[]` A string array of words that are not allowed to be used in test titles. Matching is not case-sensitive, and looks for complete words: -Examples of **incorrect** code using `disallowedWords`: +Examples of **incorrect** code when using `disallowedWords`: ```js // with disallowedWords: ['correct', 'all', 'every', 'properly'] @@ -190,3 +191,37 @@ it('correctly sets the value', () => {}); test('that everything is as it should be', () => {}); describe('the proper way to handle things', () => {}); ``` + +#### `mustMatch` + +Default: `{}` + +Allows enforcing that titles must match a given Regular Expression. An object +can be provided to apply different Regular Expressions to specific Jest test +function groups (`describe`, `test`, and `it`). + +Examples of **incorrect** code when using `mustMatch`: + +```js +// with mustMatch: '$that' +describe('the correct way to do things', () => {}); +fit('this there!', () => {}); + +// with mustMatch: { test: '$that' } +describe('the tests that will be run', () => {}); +test('the stuff works', () => {}); +xtest('errors that are thrown have messages', () => {}); +``` + +Examples of **correct** code when using `mustMatch`: + +```js +// with mustMatch: '$that' +describe('that thing that needs to be done', () => {}); +fit('that this there!', () => {}); + +// with mustMatch: { test: '$that' } +describe('the tests that will be run', () => {}); +test('that the stuff works', () => {}); +xtest('that errors that thrown have messages', () => {}); +``` diff --git a/src/rules/__tests__/valid-title.test.ts b/src/rules/__tests__/valid-title.test.ts index 714dc805b..7b4187ef6 100644 --- a/src/rules/__tests__/valid-title.test.ts +++ b/src/rules/__tests__/valid-title.test.ts @@ -100,6 +100,97 @@ ruleTester.run('disallowedWords option', rule, { ], }); +ruleTester.run('mustMatch option', rule, { + valid: [ + 'describe("the correct way to properly handle all the things", () => {});', + 'test("that all is as it should be", () => {});', + { + code: 'it("correctly sets the value", () => {});', + options: [{ mustMatch: undefined }], + }, + { + code: 'it("correctly sets the value", () => {});', + options: [{ mustMatch: / /u.source }], + }, + { + code: 'it("correctly sets the value #unit", () => {});', + options: [{ mustMatch: /#(?:unit|integration|e2e)/u.source }], + }, + { + code: 'it("correctly sets the value", () => {});', + options: [{ mustMatch: { test: /#(?:unit|integration|e2e)/u.source } }], + }, + ], + invalid: [ + { + code: 'test("the correct way to properly handle all things", () => {});', + options: [{ mustMatch: /#(?:unit|integration|e2e)/u.source }], + errors: [ + { + messageId: 'mustMatch', + data: { + jestFunctionName: 'test', + pattern: /#(?:unit|integration|e2e)/u.source, + }, + column: 6, + line: 1, + }, + ], + }, + { + code: 'describe("the test", () => {});', + options: [ + { mustMatch: { describe: /#(?:unit|integration|e2e)/u.source } }, + ], + errors: [ + { + messageId: 'mustMatch', + data: { + jestFunctionName: 'describe', + pattern: /#(?:unit|integration|e2e)/u.source, + }, + column: 10, + line: 1, + }, + ], + }, + { + code: 'xdescribe("the test", () => {});', + options: [ + { mustMatch: { describe: /#(?:unit|integration|e2e)/u.source } }, + ], + errors: [ + { + messageId: 'mustMatch', + data: { + jestFunctionName: 'describe', + pattern: /#(?:unit|integration|e2e)/u.source, + }, + column: 11, + line: 1, + }, + ], + }, + { + code: 'describe.skip("the test", () => {});', + options: [ + { mustMatch: { describe: /#(?:unit|integration|e2e)/u.source } }, + ], + errors: [ + { + messageId: 'mustMatch', + data: { + jestFunctionName: 'describe', + pattern: /#(?:unit|integration|e2e)/u.source, + }, + column: 15, + line: 1, + }, + ], + }, + ], +}); + ruleTester.run('title-must-be-string', rule, { valid: [ 'it("is a string", () => {});', diff --git a/src/rules/valid-title.ts b/src/rules/valid-title.ts index e05f5aedf..3e67f45ea 100644 --- a/src/rules/valid-title.ts +++ b/src/rules/valid-title.ts @@ -37,17 +37,33 @@ const quoteStringValue = (node: StringNode): string => ? `\`${node.quasis[0].value.raw}\`` : node.raw; +const findMustMatchPattern = ( + mustMatch: Partial> | string, + nodeName: string, +): string | null => { + const mustMatchRecord = + typeof mustMatch === 'string' + ? { describe: mustMatch, test: mustMatch, it: mustMatch } + : mustMatch; + + return mustMatchRecord[nodeName] || null; +}; + +interface Options { + ignoreTypeOfDescribeName?: boolean; + disallowedWords?: string[]; + mustMatch?: Partial> | string; +} + type MessageIds = | 'titleMustBeString' | 'emptyTitle' | 'duplicatePrefix' | 'accidentalSpace' - | 'disallowedWord'; + | 'disallowedWord' + | 'mustMatch'; -export default createRule< - [{ ignoreTypeOfDescribeName?: boolean; disallowedWords?: string[] }], - MessageIds ->({ +export default createRule<[Options], MessageIds>({ name: __filename, meta: { docs: { @@ -61,6 +77,7 @@ export default createRule< duplicatePrefix: 'should not have duplicate prefix', accidentalSpace: 'should not have leading or trailing spaces', disallowedWord: '"{{ word }}" is not allowed in test titles.', + mustMatch: '{{ jestFunctionName }} should match {{ pattern }}', }, type: 'suggestion', schema: [ @@ -75,6 +92,20 @@ export default createRule< type: 'array', items: { type: 'string' }, }, + mustMatch: { + oneOf: [ + { type: 'string' }, + { + type: 'object', + properties: { + describe: { type: 'string' }, + test: { type: 'string' }, + it: { type: 'string' }, + }, + additionalProperties: false, + }, + ], + }, }, additionalProperties: false, }, @@ -82,7 +113,10 @@ export default createRule< fixable: 'code', }, defaultOptions: [{ ignoreTypeOfDescribeName: false, disallowedWords: [] }], - create(context, [{ ignoreTypeOfDescribeName, disallowedWords = [] }]) { + create( + context, + [{ ignoreTypeOfDescribeName, disallowedWords = [], mustMatch }], + ) { const disallowedWordsRegexp = new RegExp( `\\b(${disallowedWords.join('|')})\\b`, 'iu', @@ -181,6 +215,23 @@ export default createRule< ], }); } + + if (!mustMatch) { + return; + } + + const [jestFunctionName] = nodeName.split('.'); + const pattern = findMustMatchPattern(mustMatch, jestFunctionName); + + if (pattern) { + if (!new RegExp(pattern, 'u').test(title)) { + context.report({ + messageId: 'mustMatch', + node: argument, + data: { jestFunctionName, pattern }, + }); + } + } }, }; },