Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add prefer-user-facing-locators rule #160

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/rules/no-raw-locators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## Disallow using raw locators (`no-raw-locators`)

Prefer using user-facing locators over raw locators 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',
});
```
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 noRawLocators from './rules/no-raw-locators';
import noRestrictedMatchers from './rules/no-restricted-matchers';
import noSkippedTest from './rules/no-skipped-test';
import noUselessAwait from './rules/no-useless-await';
Expand Down Expand Up @@ -102,6 +103,7 @@ export = {
'no-networkidle': noNetworkidle,
'no-nth-methods': noNthMethods,
'no-page-pause': noPagePause,
'no-raw-locators': noRawLocators,
'no-restricted-matchers': noRestrictedMatchers,
'no-skipped-test': noSkippedTest,
'no-useless-await': noUselessAwait,
Expand Down
30 changes: 30 additions & 0 deletions src/rules/no-raw-locators.ts
Original file line number Diff line number Diff line change
@@ -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: 'noRawLocator', node });
}
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Disallows the usage of raw locators',
recommended: false,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-raw-locators.md',
},
messages: {
noRawLocator:
'Usage of raw locator detected. Use methods like .getByRole() or .getByText() instead of raw locators.',
},
type: 'suggestion',
},
} as Rule.RuleModule;
65 changes: 65 additions & 0 deletions test/spec/no-raw-locators.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import rule from '../../src/rules/no-raw-locators';
import { runRuleTester, test } from '../utils/rule-tester';

const messageId = 'noRawLocator';

runRuleTester('no-raw-locators', 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'),
],
});
Loading