From 30c581947b5952dde9992b7b6c1fcf87fb708449 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Wed, 30 Aug 2023 22:39:29 -0500 Subject: [PATCH] Add custom options to expect-expect (#235) * Exclude .idea files * Update expect-expect to support multiple expressions * Create docs for expect-expect with options * Remove yarn.lock from PR --------- Co-authored-by: Sam Mayer --- .gitignore | 3 +- docs/rules/expect-expect.md | 19 ++++- package.json | 3 +- src/rules/expect-expect.ts | 58 +++++++++----- tests/expect-expect.test.ts | 155 ++++++++++++++++++++---------------- 5 files changed, 147 insertions(+), 91 deletions(-) diff --git a/.gitignore b/.gitignore index b7ecb47..059e4e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ dist/ eslint-remote-tester-results/ -fixtures/node_modules \ No newline at end of file +fixtures/node_modules +.idea/ diff --git a/docs/rules/expect-expect.md b/docs/rules/expect-expect.md index cabb3b6..f281baf 100644 --- a/docs/rules/expect-expect.md +++ b/docs/rules/expect-expect.md @@ -18,7 +18,22 @@ Examples of **correct** code for this rule: ```js test('myLogic', () => { - const actual = myLogic() - expect(actual).toBe(true) + const actual = myLogic() + expect(actual).toBe(true) }) ``` + +## Options + +> Default: `expect` + +Array of custom expression strings that are converted into a regular expression. + +```json +{ + "custom-expressions": [ + "expectValue", + "mySecondExpression" + ] +} +``` diff --git a/package.json b/package.json index d184986..d9e6411 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "eslint-doc-generator": "^1.4.3", "eslint-plugin-eslint-plugin": "^5.1.1", "eslint-plugin-node": "^11.1.0", + "eslint-plugin-vitest": "^0.2.8", "eslint-remote-tester": "^3.0.0", "eslint-remote-tester-repositories": "^1.0.1", "jiti": "^1.19.3", @@ -78,4 +79,4 @@ "dependencies": { "@typescript-eslint/utils": "^6.4.0" } -} \ No newline at end of file +} diff --git a/src/rules/expect-expect.ts b/src/rules/expect-expect.ts index 4f6411a..267122c 100644 --- a/src/rules/expect-expect.ts +++ b/src/rules/expect-expect.ts @@ -4,6 +4,7 @@ import { getTestCallExpressionsFromDeclaredVariables, isTypeOfVitestFnCall } fro export const RULE_NAME = 'expect-expect' export type MESSAGE_ID = 'expectedExpect'; +type Options = [{'custom-expressions': string[]}] /** * Checks if node names returned by getNodeName matches any of the given star patterns @@ -12,26 +13,29 @@ export type MESSAGE_ID = 'expectedExpect'; * request.**.expect * request.**.expect* */ +function buildRegularExpression(pattern: string) { + return new RegExp( + `^${pattern + .split('.') + .map(x => { + if (x === '**') return '[a-z\\d\\.]*' + + return x.replace(/\*/gu, '[a-z\\d]*') + }) + .join('\\.')}(\\.|$)`, + 'ui') +} + function matchesAssertFunctionName( nodeName: string, patterns: readonly string[] ): boolean { - return patterns.some(p => - new RegExp( - `^${p - .split('.') - .map(x => { - if (x === '**') return '[a-z\\d\\.]*' - - return x.replace(/\*/gu, '[a-z\\d]*') - }) - .join('\\.')}(\\.|$)`, - 'ui' - ).test(nodeName) + return patterns.some(pattern => + buildRegularExpression(pattern).test(nodeName) ) } -export default createEslintRule<[], MESSAGE_ID>({ +export default createEslintRule({ name: RULE_NAME, meta: { type: 'suggestion', @@ -39,16 +43,27 @@ export default createEslintRule<[], MESSAGE_ID>({ description: 'Enforce having expectation in test body', recommended: 'strict' }, - schema: [], + schema: [ + { + type: 'object', + properties: { + 'custom-expressions': { + type: 'array' + } + }, + additionalProperties: false + } + ], messages: { expectedExpect: 'Use \'expect\' in test body' } }, - defaultOptions: [], + defaultOptions: [{ 'custom-expressions': ['expect'] }], create: (context) => { const unchecked: TSESTree.CallExpression[] = [] + const validExpressions = context.options.map(option => option['custom-expressions']).flat() - function checkCallExpressionUsed(nodes: TSESTree.Node[]) { + function checkCallExpressionUsed(nodes: TSESTree.Node[], unchecked: TSESTree.CallExpression[]) { for (const node of nodes) { const index = node.type === AST_NODE_TYPES.CallExpression ? unchecked.indexOf(node) @@ -57,7 +72,7 @@ export default createEslintRule<[], MESSAGE_ID>({ if (node.type === AST_NODE_TYPES.FunctionDeclaration) { const declaredVariables = context.getDeclaredVariables(node) const textCallExpression = getTestCallExpressionsFromDeclaredVariables(declaredVariables, context) - checkCallExpressionUsed(textCallExpression) + checkCallExpressionUsed(textCallExpression, unchecked) } if (index !== -1) { unchecked.splice(index, 1) @@ -72,10 +87,13 @@ export default createEslintRule<[], MESSAGE_ID>({ if (isTypeOfVitestFnCall(node, context, ['test'])) { if (node.callee.type === AST_NODE_TYPES.MemberExpression && - isSupportedAccessor(node.callee.property, 'todo')) return + isSupportedAccessor(node.callee.property, 'todo')) { + return + } + unchecked.push(node) - } else if (matchesAssertFunctionName(name, ['expect'])) { - checkCallExpressionUsed(context.getAncestors()) + } else if (matchesAssertFunctionName(name, validExpressions)) { + checkCallExpressionUsed(context, unchecked) } }, 'Program:exit'() { diff --git a/tests/expect-expect.test.ts b/tests/expect-expect.test.ts index b5b55c7..57ba4f9 100644 --- a/tests/expect-expect.test.ts +++ b/tests/expect-expect.test.ts @@ -3,71 +3,92 @@ import rule, { RULE_NAME } from '../src/rules/expect-expect' import { ruleTester } from './ruleTester' describe(RULE_NAME, () => { - it(RULE_NAME, () => { - ruleTester.run(RULE_NAME, rule, { - valid: [ - `test("shows error", () => { - expect(true).toBe(false); - });`, - `it("foo", function () { - expect(true).toBe(false); - })`, - `it('foo', () => { - expect(true).toBe(false); - }); - function myTest() { if ('bar') {} }`, - `function myTest(param) {} - describe('my test', () => { - it('should do something', () => { - myTest("num"); - expect(1).toEqual(1); - }); - });`, - `const myFunc = () => {}; - it("works", () => expect(myFunc()).toBe(undefined));`, - `describe('title', () => { - it('test is not ok', () => { - [1, 2, 3, 4, 5, 6].forEach((n) => { - expect(n).toBe(1); - }); - }); - });`, - `desctibe('title', () => { - test('some test', () => { - expect(obj1).not.toEqual(obj2); - }) - })`, - 'it("should pass", () => expect(true).toBeDefined())', - `const myFunc = () => {}; - it("works", () => expect(myFunc()).toBe(undefined));`, - `const myFunc = () => {}; - it("works", () => expect(myFunc()).toBe(undefined));` - ], - invalid: [ - { - code: 'test("shows error", () => {});', - errors: [{ messageId: 'expectedExpect' }] - }, - { - code: `it("foo", function () { - if (1 === 2) {} - })`, - errors: [{ messageId: 'expectedExpect' }] - }, - { - code: `import { it } from 'vitest'; - describe('Button with increment', () => { - it('should show name props', () => { - console.log('test with missing expect'); - }); - });`, - errors: [{ messageId: 'expectedExpect' }] - }, - { - code: 'it("should also fail",() => expectSaga(mySaga).returns());', - errors: [{ messageId: 'expectedExpect' }] - } - ] - }) - }) + it(`${RULE_NAME} with custom expressions`, () => { + ruleTester.run(RULE_NAME, rule, { + valid: [{ + code: 'test("shows success", () => {expectValue(true).toBe(false);});', + options: [{ 'custom-expressions': ['expectValue'] }] + }, + { + code: 'test("shows success", () => {' + + 'mySecondExpression(true).toBe(true);});', + options: [ + { 'custom-expressions': ['expectValue', 'mySecondExpression'] } + ] + }], + invalid: [ + { + code: 'test("shows error", () => {});', + errors: [{ messageId: 'expectedExpect' }] + } + ] + }) + }) + it(`${RULE_NAME} without custom expressions`, () => { + ruleTester.run(RULE_NAME, rule, { + valid: [ + `test("shows error", () => { + expect(true).toBe(false); + });`, + `it("foo", function () { + expect(true).toBe(false); + })`, + `it('foo', () => { + expect(true).toBe(false); + }); + function myTest() { if ('bar') {} }`, + `function myTest(param) {} + describe('my test', () => { + it('should do something', () => { + myTest("num"); + expect(1).toEqual(1); + }); + });`, + `const myFunc = () => {}; + it("works", () => expect(myFunc()).toBe(undefined));`, + `describe('title', () => { + it('test is not ok', () => { + [1, 2, 3, 4, 5, 6].forEach((n) => { + expect(n).toBe(1); + }); + }); + });`, + `describe('title', () => { + test('some test', () => { + expect(obj1).not.toEqual(obj2); + }) + })`, + 'it("should pass", () => expect(true).toBeDefined())', + `const myFunc = () => {}; + it("works", () => expect(myFunc()).toBe(undefined));`, + `const myFunc = () => {}; + it("works", () => expect(myFunc()).toBe(undefined));` + ], + invalid: [ + { + code: 'test("shows error", () => {});', + errors: [{ messageId: 'expectedExpect' }] + }, + { + code: `it("foo", function () { + if (1 === 2) {} + })`, + errors: [{ messageId: 'expectedExpect' }] + }, + { + code: `import { it } from 'vitest'; + describe('Button with increment', () => { + it('should show name props', () => { + console.log('test with missing expect'); + }); + });`, + errors: [{ messageId: 'expectedExpect' }] + }, + { + code: 'it("should also fail",() => expectSaga(mySaga).returns());', + errors: [{ messageId: 'expectedExpect' }] + } + ] + }) + }) })