From 807116e930aea15830d0ff90c982c95d2c2ca733 Mon Sep 17 00:00:00 2001 From: Han Yeong-woo Date: Fri, 29 Sep 2023 02:31:30 +0900 Subject: [PATCH 1/2] feat: Add `prefer-to-have-count` rule --- docs/rules/prefer-to-have-count.md | 23 +++++++++ src/index.ts | 2 + src/rules/prefer-to-have-count.ts | 65 +++++++++++++++++++++++++ test/spec/prefer-to-have-count.spec.ts | 66 ++++++++++++++++++++++++++ 4 files changed, 156 insertions(+) create mode 100644 docs/rules/prefer-to-have-count.md create mode 100644 src/rules/prefer-to-have-count.ts create mode 100644 test/spec/prefer-to-have-count.spec.ts diff --git a/docs/rules/prefer-to-have-count.md b/docs/rules/prefer-to-have-count.md new file mode 100644 index 0000000..a91696b --- /dev/null +++ b/docs/rules/prefer-to-have-count.md @@ -0,0 +1,23 @@ +# Suggest using `toHaveCount()` (`prefer-to-have-count`) + +In order to have a better failure message, `toHaveCount()` should be used upon +asserting expectations on locators `count()` method. + +## Rule details + +This rule triggers a warning if `toBe()`, `toEqual()` or `toStrictEqual()` is +used to assert locators `count()` method. + +The following patterns are considered warnings: + +```javascript +expect(await files.count()).toBe(1); +expect(await files.count()).toEqual(1); +expect(await files.count()).toStrictEqual(1); +``` + +The following pattern is **not** a warning: + +```javascript +await expect(files).toHaveCount(1); +``` diff --git a/src/index.ts b/src/index.ts index 5f4981f..a1d8f11 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import preferLowercaseTitle from './rules/prefer-lowercase-title'; import preferStrictEqual from './rules/prefer-strict-equal'; import preferToBe from './rules/prefer-to-be'; import preferToContain from './rules/prefer-to-contain'; +import preferToHaveCount from './rules/prefer-to-have-count'; import preferToHaveLength from './rules/prefer-to-have-length'; import preferWebFirstAssertions from './rules/prefer-web-first-assertions'; import requireSoftAssertions from './rules/require-soft-assertions'; @@ -113,6 +114,7 @@ export = { 'prefer-strict-equal': preferStrictEqual, 'prefer-to-be': preferToBe, 'prefer-to-contain': preferToContain, + 'prefer-to-have-count': preferToHaveCount, 'prefer-to-have-length': preferToHaveLength, 'prefer-web-first-assertions': preferWebFirstAssertions, 'require-soft-assertions': requireSoftAssertions, diff --git a/src/rules/prefer-to-have-count.ts b/src/rules/prefer-to-have-count.ts new file mode 100644 index 0000000..0fe34e2 --- /dev/null +++ b/src/rules/prefer-to-have-count.ts @@ -0,0 +1,65 @@ +import { Rule } from 'eslint'; +import { replaceAccessorFixer } from '../utils/fixer'; +import { parseExpectCall } from '../utils/parseExpectCall'; + +const matchers = new Set(['toBe', 'toEqual', 'toStrictEqual']); + +export default { + create(context) { + return { + CallExpression(node) { + const expectCall = parseExpectCall(node); + if (!expectCall || !matchers.has(expectCall.matcherName)) { + return; + } + + const [argument] = node.arguments; + if ( + argument.type !== 'AwaitExpression' || + argument.argument.type !== 'CallExpression' || + argument.argument.callee.type !== 'MemberExpression' + ) { + return; + } + + const callee = argument.argument.callee; + context.report({ + fix(fixer) { + return [ + // remove the "await" expression + fixer.removeRange([ + argument.range![0], + argument.range![0] + 'await'.length + 1, + ]), + // remove the "count()" method accessor + fixer.removeRange([ + callee.property.range![0] - 1, + argument.argument.range![1], + ]), + // replace the current matcher with "toHaveCount" + replaceAccessorFixer(fixer, expectCall.matcher, 'toHaveCount'), + // insert "await" to before "expect()" + fixer.insertTextBefore(node, 'await '), + ]; + }, + messageId: 'useToHaveCount', + node: expectCall.matcher, + }); + }, + }; + }, + meta: { + docs: { + category: 'Best Practices', + description: 'Suggest using `toHaveCount()`', + recommended: false, + url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-count.md', + }, + fixable: 'code', + messages: { + useToHaveCount: 'Use toHaveCount() instead', + }, + schema: [], + type: 'suggestion', + }, +} as Rule.RuleModule; diff --git a/test/spec/prefer-to-have-count.spec.ts b/test/spec/prefer-to-have-count.spec.ts new file mode 100644 index 0000000..d66b10e --- /dev/null +++ b/test/spec/prefer-to-have-count.spec.ts @@ -0,0 +1,66 @@ +import rule from '../../src/rules/prefer-to-have-count'; +import { runRuleTester } from '../utils/rule-tester'; + +runRuleTester('prefer-to-have-count', rule, { + invalid: [ + { + code: 'expect(await files.count()).toBe(1)', + errors: [ + { column: 29, endColumn: 33, line: 1, messageId: 'useToHaveCount' }, + ], + output: 'await expect(files).toHaveCount(1)', + }, + { + code: 'expect(await files.count()).not.toBe(1)', + errors: [ + { column: 33, endColumn: 37, line: 1, messageId: 'useToHaveCount' }, + ], + output: 'await expect(files).not.toHaveCount(1)', + }, + { + code: 'expect.soft(await files["count"]()).not.toBe(1)', + errors: [ + { column: 41, endColumn: 45, line: 1, messageId: 'useToHaveCount' }, + ], + output: 'await expect.soft(files).not.toHaveCount(1)', + }, + { + code: 'expect(await files["count"]()).not["toBe"](1)', + errors: [ + { column: 36, endColumn: 42, line: 1, messageId: 'useToHaveCount' }, + ], + output: 'await expect(files).not["toHaveCount"](1)', + }, + { + code: 'expect(await files.count())[`toEqual`](1)', + errors: [ + { column: 29, endColumn: 38, line: 1, messageId: 'useToHaveCount' }, + ], + output: 'await expect(files)[`toHaveCount`](1)', + }, + { + code: 'expect(await files.count()).toStrictEqual(1)', + errors: [ + { column: 29, endColumn: 42, line: 1, messageId: 'useToHaveCount' }, + ], + output: 'await expect(files).toHaveCount(1)', + }, + { + code: 'expect(await files.count()).not.toStrictEqual(1)', + errors: [ + { column: 33, endColumn: 46, line: 1, messageId: 'useToHaveCount' }, + ], + output: 'await expect(files).not.toHaveCount(1)', + }, + ], + valid: [ + 'await expect(files).toHaveCount(1)', + "expect(files.name).toBe('file')", + "expect(files['name']).toBe('file')", + "expect(files[`name`]).toBe('file')", + 'expect(result).toBe(true)', + `expect(user.getUserName(5)).not.toEqual('Paul')`, + `expect(user.getUserName(5)).not.toEqual('Paul')`, + 'expect(a)', + ], +}); From c9a51bb0ab70c79dddaa3abde694eb09344ed393 Mon Sep 17 00:00:00 2001 From: Han Yeong-woo Date: Fri, 29 Sep 2023 02:36:10 +0900 Subject: [PATCH 2/2] docs: add `prefer-to-have-count` to the README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 298ec40..9b1593d 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ command line option.\ | | 🔧 | | [prefer-lowercase-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names | | | 🔧 | | [prefer-to-be](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-be.md) | Suggest using `toBe()` | | | 🔧 | | [prefer-to-contain](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-contain.md) | Suggest using `toContain()` | +| | 🔧 | | [prefer-to-have-count](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-count.md) | Suggest using `toHaveCount()` | | | 🔧 | | [prefer-to-have-length](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` | | ✔ | 🔧 | | [prefer-web-first-assertions](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-web-first-assertions.md) | Suggest using web first assertions | | | | | [require-top-level-describe](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-top-level-describe.md) | Require test cases and hooks to be inside a `test.describe` block |