From acf80a030ea4f90bc887828ac1f7edf7240ac731 Mon Sep 17 00:00:00 2001 From: Mark Skelton Date: Sun, 18 Feb 2024 00:14:21 -0600 Subject: [PATCH] feat: Add `max-expects` rule --- README.md | 3 +- docs/rules/max-expects.md | 65 +++++++ src/index.ts | 2 + src/rules/max-expects.ts | 72 ++++++++ test/spec/max-expects.spec.ts | 339 ++++++++++++++++++++++++++++++++++ 5 files changed, 480 insertions(+), 1 deletion(-) create mode 100644 docs/rules/max-expects.md create mode 100644 src/rules/max-expects.ts create mode 100644 test/spec/max-expects.spec.ts diff --git a/README.md b/README.md index e4ad1b6..0618307 100644 --- a/README.md +++ b/README.md @@ -118,8 +118,9 @@ command line option.\ [suggestions](https://eslint.org/docs/latest/developer-guide/working-with-rules#providing-suggestions). | ✔ | 🔧 | 💡 | Rule | Description | -| :-: | :-: | :-: | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | +| :-: | :-: | :-: | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | --- | | ✔ | | | [expect-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/expect-expect.md) | Enforce assertion to be made in a test body | +| ✔ | | | [max-expects](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-expects.md) | Enforces a maximum number assertion calls in a test body | | | ✔ | | | [max-nested-describe](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-nested-describe.md) | Enforces a maximum depth to nested describe calls | | ✔ | 🔧 | | [missing-playwright-await](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/missing-playwright-await.md) | Enforce Playwright APIs to be awaited | | | | | [no-commented-out-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-commented-out-test.md) | Disallow commented out tests | diff --git a/docs/rules/max-expects.md b/docs/rules/max-expects.md new file mode 100644 index 0000000..29d5fab --- /dev/null +++ b/docs/rules/max-expects.md @@ -0,0 +1,65 @@ +# Enforces a maximum number assertion calls in a test body (`max-expects`) + +As more assertions are made, there is a possible tendency for the test to be +more likely to mix multiple objectives. To avoid this, this rule reports when +the maximum number of assertions is exceeded. + +## Rule details + +This rule enforces a maximum number of `expect()` calls. + +The following patterns are considered warnings (with the default option of +`{ "max": 5 } `): + +```js +test('should not pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); +}); +``` + +The following patterns are **not** considered warnings (with the default option +of `{ "max": 5 } `): + +```js +test('shout pass'); + +test('shout pass', () => {}); + +test.skip('shout pass', () => {}); + +test('should pass', function () { + expect(true).toBeDefined(); +}); + +test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); +}); +``` + +## Options + +```json +{ + "playwright/max-expects": [ + "error", + { + "max": 5 + } + ] +} +``` + +### `max` + +Enforces a maximum number of `expect()`. + +This has a default value of `5`. diff --git a/src/index.ts b/src/index.ts index ba0f69b..ed34208 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import globals from 'globals'; import expectExpect from './rules/expect-expect'; +import maxExpects from './rules/max-expects'; import maxNestedDescribe from './rules/max-nested-describe'; import missingPlaywrightAwait from './rules/missing-playwright-await'; import noCommentedOutTests from './rules/no-commented-out-tests'; @@ -42,6 +43,7 @@ const index = { configs: {}, rules: { 'expect-expect': expectExpect, + 'max-expects': maxExpects, 'max-nested-describe': maxNestedDescribe, 'missing-playwright-await': missingPlaywrightAwait, 'no-commented-out-tests': noCommentedOutTests, diff --git a/src/rules/max-expects.ts b/src/rules/max-expects.ts new file mode 100644 index 0000000..7132dd8 --- /dev/null +++ b/src/rules/max-expects.ts @@ -0,0 +1,72 @@ +import { Rule } from 'eslint'; +import * as ESTree from 'estree'; +import { getExpectType, getParent, isTestCall } from '../utils/ast'; + +export default { + create(context) { + const options = { + max: 5, + ...((context.options?.[0] as Record) ?? {}), + }; + + let count = 0; + + const maybeResetCount = (node: ESTree.Node) => { + const parent = getParent(node); + const isTestFn = + parent?.type !== 'CallExpression' || isTestCall(context, parent); + + if (isTestFn) { + count = 0; + } + }; + + return { + ArrowFunctionExpression: maybeResetCount, + 'ArrowFunctionExpression:exit': maybeResetCount, + CallExpression(node) { + if (!getExpectType(context, node)) return; + + count += 1; + + if (count > options.max) { + context.report({ + data: { + count: count.toString(), + max: options.max.toString(), + }, + messageId: 'exceededMaxAssertion', + node, + }); + } + }, + FunctionExpression: maybeResetCount, + 'FunctionExpression:exit': maybeResetCount, + }; + }, + meta: { + docs: { + category: 'Best Practices', + description: 'Enforces a maximum number assertion calls in a test body', + recommended: false, + url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-expects.md', + }, + messages: { + exceededMaxAssertion: + 'Too many assertion calls ({{ count }}) - maximum allowed is {{ max }}', + }, + schema: [ + { + additionalProperties: false, + properties: { + max: { + minimum: 1, + type: 'integer', + }, + }, + type: 'object', + }, + ], + type: 'suggestion', + }, +} as Rule.RuleModule; diff --git a/test/spec/max-expects.spec.ts b/test/spec/max-expects.spec.ts new file mode 100644 index 0000000..494ab07 --- /dev/null +++ b/test/spec/max-expects.spec.ts @@ -0,0 +1,339 @@ +import dedent from 'dedent'; +import rule from '../../src/rules/max-expects'; +import { runRuleTester } from '../utils/rule-tester'; + +runRuleTester('max-expects', rule, { + invalid: [ + { + code: dedent` + test('should not pass', function () { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + `, + errors: [ + { + column: 3, + line: 7, + messageId: 'exceededMaxAssertion', + }, + ], + }, + { + code: dedent` + test('should not pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + `, + errors: [ + { + column: 3, + line: 7, + messageId: 'exceededMaxAssertion', + }, + ], + }, + { + code: dedent` + test('should not pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + test('should not pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + `, + errors: [ + { + column: 3, + line: 7, + messageId: 'exceededMaxAssertion', + }, + { + column: 3, + line: 15, + messageId: 'exceededMaxAssertion', + }, + ], + }, + { + code: dedent` + describe('test', () => { + test('should not pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + }); + `, + errors: [ + { + column: 5, + line: 8, + messageId: 'exceededMaxAssertion', + }, + ], + }, + { + code: dedent` + test('should not pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + `, + errors: [ + { + column: 3, + line: 3, + messageId: 'exceededMaxAssertion', + }, + ], + options: [ + { + max: 1, + }, + ], + }, + // Global aliases + { + code: dedent` + it('should not pass', function () { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + `, + errors: [ + { + column: 3, + line: 7, + messageId: 'exceededMaxAssertion', + }, + ], + settings: { + playwright: { + globalAliases: { test: ['it'] }, + }, + }, + }, + ], + valid: [ + `test('should pass')`, + `test('should pass', () => {})`, + `test.skip('should pass', () => {})`, + dedent` + test('should pass', function () { + expect(true).toBeDefined(); + }); + `, + dedent` + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + `, + dedent` + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + // expect(true).toBeDefined(); + }); + `, + dedent` + test('should pass', async () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + `, + dedent` + test('should pass', async () => { + expect.hasAssertions(); + + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + `, + dedent` + test('should pass', async () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toEqual(expect.any(Boolean)); + }); + `, + dedent` + test('should pass', async () => { + expect.hasAssertions(); + + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toEqual(expect.any(Boolean)); + }); + `, + dedent` + describe('test', () => { + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + }); + `, + dedent` + test.each(['should', 'pass'], () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + `, + dedent` + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + `, + dedent` + function myHelper() { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }; + + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + `, + dedent` + function myHelper1() { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }; + + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + + function myHelper2() { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }; + `, + dedent` + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + + function myHelper() { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }; + `, + dedent` + const myHelper1 = () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }; + + test('should pass', function() { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + + const myHelper2 = function() { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }; + `, + { + code: dedent` + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + `, + options: [ + { + max: 10, + }, + ], + }, + // Global aliases + { + code: `it('should pass')`, + settings: { + playwright: { + globalAliases: { test: ['it'] }, + }, + }, + }, + ], +});