Skip to content
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'),
],
});