Skip to content
Open
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
2 changes: 2 additions & 0 deletions docs/rules/missing-playwright-await.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Example of **incorrect** code for this rule:
```javascript
expect(page).toMatchText('text')
expect.poll(() => foo).toBe(true)
page.goto('https://example.com')

test.step('clicks the button', async () => {
await page.click('button')
Expand All @@ -20,6 +21,7 @@ Example of **correct** code for this rule:
```javascript
await expect(page).toMatchText('text')
await expect.poll(() => foo).toBe(true)
await page.goto('https://example.com')

await test.step('clicks the button', async () => {
await page.click('button')
Expand Down
24 changes: 24 additions & 0 deletions src/rules/missing-playwright-await.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,21 @@ runRuleTester('missing-playwright-await', rule, {
},
},
},
// Page methods
{
code: `page.goto('https://example.com')`,
errors: [
{ column: 1, endColumn: 10, endLine: 1, line: 1, messageId: 'page' },
],
output: `await page.goto('https://example.com')`,
},
{
code: test(`page.goto('https://example.com')`),
errors: [
{ column: 28, endColumn: 37, endLine: 1, line: 1, messageId: 'page' },
],
output: test(`await page.goto('https://example.com')`),
},
],
valid: [
// Basic
Expand Down Expand Up @@ -368,5 +383,14 @@ runRuleTester('missing-playwright-await', rule, {
},
},
},
// Page methods
{ code: `await page.goto('https://example.com')` },
{ code: `await page.title()` },
// Other page methods are ignored
{ code: `page.frames()` },
// Other methods with the same name are ignored
{ code: `randomObject.title()` },
// Does not need to be awaited when returned
{ code: `() => { return page.content() }` },
],
})
85 changes: 84 additions & 1 deletion src/rules/missing-playwright-await.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Rule } from 'eslint'
import ESTree from 'estree'
import { getParent, getStringValue, isIdentifier } from '../utils/ast'
import { getParent, getStringValue, isIdentifier, isPage } from '../utils/ast'
import { createRule } from '../utils/createRule'
import { ParsedFnCall, parseFnCall } from '../utils/parseFnCall'

Expand Down Expand Up @@ -57,6 +57,73 @@ const playwrightTestMatchers = [
'toBeInViewport',
]

const pageMethods = new Set([
'$', // deprecated
'$$', // deprecated
'$eval', // deprecated
'$$eval', // deprecated
'addInitScript',
'addLocatorHandler',
'addScriptTag',
'addStyleTag',
'bringToFront',
'check', // deprecated
'click', // deprecated
'close',
'content',
'dblclick', // deprecated
'dispatchEvent', // deprecated
'dragAndDrop',
'emulateMedia',
'evaluate',
'evaluateHandle',
'exposeBinding',
'exposeFunction',
'fill', // deprecated
'focus', // deprecated
'getAttribute', // deprecated
'goBack',
'goForward',
'goto',
'hover', // deprecated
'innerHTML', // deprecated
'innerText', // deprecated
'inputValue', // deprecated
'isChecked', // deprecated
'isDisabled', // deprecated
'isEditable', // deprecated
'isEnabled', // deprecated
'isHidden', // deprecated
'isVisible', // deprecated
'opener',
'pause',
'pdf',
'press', // deprecated
'reload',
'removeLocatorHandler',
'route',
'routeFromHAR',
'screenshot',
'selectOption', // deprecated
'setChecked', // deprecated
'setContent',
'setExtraHTTPHeaders',
'setInputFiles', // deprecated
'setViewportSize',
'tap', // deprecated
'textContent', // deprecated
'title',
'type', // deprecated
'unroute',
'unrouteAll',
'waitForFunction',
'waitForLoadState',
'waitForNavigation', // deprecated
'waitForSelector', // deprecated
'waitForTimeout', // deprecated
'waitForURL',
])

function getReportNode(node: ESTree.Node) {
const parent = getParent(node)
return parent?.type === 'MemberExpression' ? parent : node
Expand Down Expand Up @@ -139,6 +206,21 @@ export default createRule({

return {
CallExpression(node) {
// Checking validity of calls to methods on the page object
if (isPage(node) && node.callee.type === 'MemberExpression') {
const method = getStringValue(node.callee.property)
const isValid = checkValidity(node)

if (!isValid && pageMethods.has(method)) {
context.report({
data: { method },
fix: (fixer) => fixer.insertTextBefore(node, 'await '),
messageId: 'page',
node: node.callee,
})
}
}

const call = parseFnCall(context, node)
if (call?.type !== 'step' && call?.type !== 'expect') return

Expand Down Expand Up @@ -167,6 +249,7 @@ export default createRule({
messages: {
expect: "'{{matcherName}}' must be awaited or returned.",
expectPoll: "'expect.poll' matchers must be awaited or returned.",
page: "'{{method}}' must be awaited or returned.",
testStep: "'test.step' must be awaited or returned.",
},
schema: [
Expand Down
9 changes: 8 additions & 1 deletion src/utils/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,17 @@ export function dig(node: ESTree.Node, identifier: string | RegExp): boolean {
: false
}

export function isPage(node: ESTree.CallExpression) {
return (
node.callee.type === 'MemberExpression' &&
dig(node.callee.object, /(^(page|frame)|(Page|Frame)$)/)
)
}

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