From 7b9ec83a9ddabbc6a8049c69940f04edce748d88 Mon Sep 17 00:00:00 2001 From: Nils Haberkamp Date: Fri, 11 Aug 2023 21:53:08 +0200 Subject: [PATCH] feat: add prefer-user-facing-locators rule --- docs/rules/prefer-user-facing-locators.md | 26 +++++++++ src/index.ts | 2 + src/rules/no-raw-selector.ts | 30 +++++++++++ test/spec/no-raw-selector.spec.ts | 65 +++++++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 docs/rules/prefer-user-facing-locators.md create mode 100644 src/rules/no-raw-selector.ts create mode 100644 test/spec/no-raw-selector.spec.ts diff --git a/docs/rules/prefer-user-facing-locators.md b/docs/rules/prefer-user-facing-locators.md new file mode 100644 index 0000000..b7267f5 --- /dev/null +++ b/docs/rules/prefer-user-facing-locators.md @@ -0,0 +1,26 @@ +## Disallow using `page.locator` (`no-raw-selector`) + +Prefer using user-facing locators over `page.locator` to make tests more robust. + +Check out the [Playwright documentation](https://playwright.dev/docs/locators) +for more information. + +## Rule Details + +Example of **incorrect** code for this rule: + +```javascript +await page.locator('button').click(); +``` + +Example of **correct** code for this rule: + +```javascript +await page.getByRole('button').click(); +``` + +```javascript +await page.getByRole('button', { + name: 'Submit', +}); +``` diff --git a/src/index.ts b/src/index.ts index 5597f46..870662b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import noNestedStep from './rules/no-nested-step'; import noNetworkidle from './rules/no-networkidle'; import noNthMethods from './rules/no-nth-methods'; import noPagePause from './rules/no-page-pause'; +import noRawSelector from './rules/no-raw-selector'; import noRestrictedMatchers from './rules/no-restricted-matchers'; import noSkippedTest from './rules/no-skipped-test'; import noUselessAwait from './rules/no-useless-await'; @@ -102,6 +103,7 @@ export = { 'no-networkidle': noNetworkidle, 'no-nth-methods': noNthMethods, 'no-page-pause': noPagePause, + 'no-raw-selector': noRawSelector, 'no-restricted-matchers': noRestrictedMatchers, 'no-skipped-test': noSkippedTest, 'no-useless-await': noUselessAwait, diff --git a/src/rules/no-raw-selector.ts b/src/rules/no-raw-selector.ts new file mode 100644 index 0000000..1e318cb --- /dev/null +++ b/src/rules/no-raw-selector.ts @@ -0,0 +1,30 @@ +import { Rule } from 'eslint'; +import { getStringValue, isPageMethod } from '../utils/ast'; + +export default { + create(context) { + return { + CallExpression(node) { + if (node.callee.type !== 'MemberExpression') return; + const method = getStringValue(node.callee.property); + + if (isPageMethod(node, 'locator') || method === 'locator') { + context.report({ messageId: 'noRawSelector', node }); + } + }, + }; + }, + meta: { + docs: { + category: 'Best Practices', + description: 'Disallows the usage of raw selectors', + recommended: false, + url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-raw-selector.md', + }, + messages: { + noRawSelector: + 'Usage of raw selector detected. Use methods like .getByRole() or .getByText() instead of raw selectors.', + }, + type: 'suggestion', + }, +} as Rule.RuleModule; diff --git a/test/spec/no-raw-selector.spec.ts b/test/spec/no-raw-selector.spec.ts new file mode 100644 index 0000000..82c0f63 --- /dev/null +++ b/test/spec/no-raw-selector.spec.ts @@ -0,0 +1,65 @@ +import rule from '../../src/rules/no-raw-selector'; +import { runRuleTester, test } from '../utils/rule-tester'; + +const messageId = 'noRawSelector'; + +runRuleTester('no-raw-selector', rule, { + invalid: [ + { + code: test('await page.locator()'), + errors: [{ column: 34, endColumn: 48, line: 1, messageId }], + }, + { + code: test('await this.page.locator()'), + errors: [{ column: 34, endColumn: 53, line: 1, messageId }], + }, + { + code: test("await page.locator('.btn')"), + errors: [{ column: 34, endColumn: 54, line: 1, messageId }], + }, + { + code: test('await page["locator"](".btn")'), + errors: [{ column: 34, endColumn: 57, line: 1, messageId }], + }, + { + code: test('await page[`locator`](".btn")'), + errors: [{ column: 34, endColumn: 57, line: 1, messageId }], + }, + + { + code: test('await frame.locator()'), + errors: [{ column: 34, endColumn: 49, line: 1, messageId }], + }, + + { + code: test( + 'const section = await page.getByRole("section"); section.locator(".btn")' + ), + errors: [{ column: 77, endColumn: 100, line: 1, messageId }], + }, + ], + valid: [ + test('await page.click()'), + test('await this.page.click()'), + test('await page["hover"]()'), + test('await page[`check`]()'), + + // Preferred user facing locators + test('await page.getByText("lorem ipsum")'), + test('await page.getByLabel(/Email/)'), + test('await page.getByRole("button", { name: /submit/i })'), + test('await page.getByTestId("my-test-button").click()'), + test( + 'await page.getByRole("button").filter({ hasText: "Add to cart" }).click()' + ), + + test('await frame.getByRole("button")'), + + test( + 'const section = page.getByRole("section"); section.getByRole("button")' + ), + + // bare calls + test('() => page.locator'), + ], +});