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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ CLI option\
| [no-slowed-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-slowed-test.md) | Disallow usage of the `.slow` annotation | | | 💡 |
| [no-standalone-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-standalone-expect.md) | Disallow using expect outside of `test` blocks | ✅ | | |
| [no-unsafe-references](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unsafe-references.md) | Prevent unsafe variable references in `page.evaluate()` | ✅ | 🔧 | |
| [no-unused-locators](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unused-locators.md) | Disallow usage of page locators that are not used | ✅ | | |
| [no-useless-await](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-await.md) | Disallow unnecessary `await`s for Playwright methods | ✅ | 🔧 | |
| [no-useless-not](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-not.md) | Disallow usage of `not` matchers when a specific matcher exists | ✅ | 🔧 | |
| [no-wait-for-navigation](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-navigation.md) | Disallow usage of `page.waitForNavigation()` | ✅ | | 💡 |
Expand Down
24 changes: 24 additions & 0 deletions docs/rules/no-unused-locators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Disallow usage of page locators that are not used (`no-unused-locators`)

Using locators without performing any actions or assertions on them can lead to
unexpected behavior/flakiness in tests. This rule helps ensure that locators are
used in some way by requiring that they are either acted upon or asserted
against.

## Rule Details

Examples of **incorrect** code for this rule:

```javascript
page.getByRole('button', { name: 'Sign in' })
```

Examples of **correct** code for this rule:

```javascript
const btn = page.getByRole('button', { name: 'Sign in' })

await page.getByRole('button', { name: 'Sign in' }).click()

await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible()
```
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import noSkippedTest from './rules/no-skipped-test.js'
import noSlowedTest from './rules/no-slowed-test.js'
import noStandaloneExpect from './rules/no-standalone-expect.js'
import noUnsafeReferences from './rules/no-unsafe-references.js'
import noUnusedLocators from './rules/no-unused-locators.js'
import noUselessAwait from './rules/no-useless-await.js'
import noUselessNot from './rules/no-useless-not.js'
import noWaitForNavigation from './rules/no-wait-for-navigation.js'
Expand Down Expand Up @@ -78,6 +79,7 @@ const index = {
'no-slowed-test': noSlowedTest,
'no-standalone-expect': noStandaloneExpect,
'no-unsafe-references': noUnsafeReferences,
'no-unused-locators': noUnusedLocators,
'no-useless-await': noUselessAwait,
'no-useless-not': noUselessNot,
'no-wait-for-navigation': noWaitForNavigation,
Expand Down Expand Up @@ -126,6 +128,7 @@ const sharedConfig = {
'playwright/no-skipped-test': 'warn',
'playwright/no-standalone-expect': 'error',
'playwright/no-unsafe-references': 'error',
'playwright/no-unused-locators': 'error',
'playwright/no-useless-await': 'warn',
'playwright/no-useless-not': 'warn',
'playwright/no-wait-for-navigation': 'error',
Expand Down
39 changes: 39 additions & 0 deletions src/rules/no-unused-locators.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { runRuleTester } from '../utils/rule-tester.js'
import rule from './no-unused-locators.js'

runRuleTester('no-unused-locators', rule, {
invalid: [
{
code: "page.getByRole('button', { name: 'Sign in' })",
errors: [
{
column: 1,
endColumn: 46,
endLine: 1,
line: 1,
messageId: 'noUnusedLocator',
},
],
},
{
code: "await page.getByTestId('my-test-id')",
errors: [
{
column: 7,
endColumn: 37,
endLine: 1,
line: 1,
messageId: 'noUnusedLocator',
},
],
},
],
valid: [
`await page.getByRole('button', { name: 'Sign in' }).all()`,
"const btn = page.getByLabel('Sign in')",
"const btn = page.getByPlaceholder('User Name').first()",
"await page.getByRole('button', { name: 'Sign in' }).click()",
"expect(page.getByTestId('User Name')).toBeVisible()",
"expect(page.getByRole('User Name').first()).toBeVisible()",
],
})
36 changes: 36 additions & 0 deletions src/rules/no-unused-locators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { isPageMethod } from '../utils/ast.js'
import { createRule } from '../utils/createRule.js'

const LOCATOR_REGEX =
/locator|getBy(Role|Text|Label|Placeholder|AltText|Title|TestId)/

export default createRule({
create(context) {
return {
CallExpression(node) {
if (!isPageMethod(node, LOCATOR_REGEX)) {
return
}

if (
node.parent.type === 'ExpressionStatement' ||
node.parent.type === 'AwaitExpression'
) {
context.report({ messageId: 'noUnusedLocator', node })
}
},
}
},
meta: {
docs: {
category: 'Possible Errors',
description: `Disallow usage of page locators that are not used`,
recommended: true,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-unused-locators.md',
},
messages: {
noUnusedLocator: 'Unused locator',
},
type: 'problem',
},
})
5 changes: 4 additions & 1 deletion src/utils/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ export function dig(node: ESTree.Node, identifier: string | RegExp): boolean {
: false
}

export function isPageMethod(node: ESTree.CallExpression, name: string) {
export function isPageMethod(
node: ESTree.CallExpression,
name: string | RegExp,
) {
return (
node.callee.type === 'MemberExpression' &&
dig(node.callee.object, /(^(page|frame)|(Page|Frame)$)/) &&
Expand Down