Free tool
Playwright test reviewer
Paste a Playwright test, or a test before and after an AI agent changed it, and see what a careful reviewer would question. Same input, same findings, every time.
Runs entirely in your browser. Your code is never sent anywhere and never executed.
Verdict
Needs changes
- HighLine 3
Focused test (.only)
`.only` runs just this test or group and skips everything else in the file. Remove it, and set `forbidOnly` in CI so it can't be merged.
test.only('user can log in', async ({ page }) => {ESLint:playwright/no-focused-testPlaywright docs (opens in a new tab) - HighLine 7
Hardcoded credential
A credential is written into the test, so anyone who can read the repository can read it. Load it from an environment variable or your CI secret store.
await page.locator('input[type="password"]').fill('••••••');No ESLint rule for this checkPlaywright docs (opens in a new tab) - HighLine 9
Fixed wait (waitForTimeout)
A fixed wait either slows every run or isn't long enough on a slow one. Wait for the condition the test needs instead, e.g. `await expect(locator).toBeVisible()`.
await page.waitForTimeout(3000);
ESLint:playwright/no-wait-for-timeoutPlaywright docs (opens in a new tab) - HighLine 11
Missing await
This web-first assertion is not awaited, so the test can finish before it runs and pass without checking anything. Add `await`.
expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();ESLint:playwright/missing-playwright-awaitPlaywright docs (opens in a new tab) - HighLine 12
Assertion that can never fail
A Locator object is always truthy, so this assertion passes whether or not the element exists. Use a web-first assertion such as `await expect(locator).toBeVisible()`.
expect(page.locator('.welcome-banner')).toBeTruthy();ESLint:playwright/no-unnecessary-assertionsPlaywright docs (opens in a new tab) - HighLine 15
Test without an assertion
“profile page loads” has no expect() call, so it passes as long as nothing throws. If its assertions live in a helper function, you can ignore this.
test('profile page loads', async ({ page }) => {ESLint:playwright/expect-expectPlaywright docs (opens in a new tab) - MediumLine 5
Waiting for networkidle
Playwright discourages `networkidle`: it waits for the network to go quiet, which is slow and unreliable on pages with polling or analytics. Assert on the element or state the test needs.
await page.waitForLoadState('networkidle');ESLint:playwright/no-networkidlePlaywright docs (opens in a new tab) - MediumLine 8
Forced action (force: true)
`force: true` skips Playwright's actionability checks, so the action can succeed on an element a real user couldn't use: hidden, covered, or disabled.
await page.locator('//button[contains(text(), "Sign in")]').click({ force: true });ESLint:playwright/no-force-optionPlaywright docs (opens in a new tab) - MediumLine 8
XPath locator
XPath breaks when markup changes and is hard to review. Prefer `getByRole`, `getByLabel`, or `getByTestId`.
await page.locator('//button[contains(text(), "Sign in")]').click({ force: true });ESLint:playwright/no-raw-locatorsPlaywright docs (opens in a new tab) - MediumLine 10
Assertion that doesn't retry
Reading `isVisible()` once and asserting on the result doesn't wait or retry. Use `await expect(locator).toBeVisible(…)`, which waits for the condition.
expect(await page.locator('.dashboard-title').isVisible()).toBe(true);ESLint:playwright/prefer-web-first-assertionsPlaywright docs (opens in a new tab) - LowLine 4
Hardcoded environment URL
A full URL ties the test to one environment. Set `baseURL` in playwright.config and navigate with a path, e.g. `page.goto('/login')`.
await page.goto('https://staging.example.com/login');No ESLint rule for this checkPlaywright docs (opens in a new tab) - LowLine 6
CSS or XPath locator
Raw selectors describe page structure, not what users see. Prefer `getByRole`, `getByLabel`, or `getByTestId`.
await page.locator('#email').fill('[email protected]');ESLint:playwright/no-raw-locatorsPlaywright docs (opens in a new tab) - LowLine 7
CSS or XPath locator
Raw selectors describe page structure, not what users see. Prefer `getByRole`, `getByLabel`, or `getByTestId`.
await page.locator('input[type="password"]').fill('SuperSecret123!');ESLint:playwright/no-raw-locatorsPlaywright docs (opens in a new tab) - LowLine 10
CSS or XPath locator
Raw selectors describe page structure, not what users see. Prefer `getByRole`, `getByLabel`, or `getByTestId`.
expect(await page.locator('.dashboard-title').isVisible()).toBe(true);ESLint:playwright/no-raw-locatorsPlaywright docs (opens in a new tab) - LowLine 12
CSS or XPath locator
Raw selectors describe page structure, not what users see. Prefer `getByRole`, `getByLabel`, or `getByTestId`.
expect(page.locator('.welcome-banner')).toBeTruthy();ESLint:playwright/no-raw-locatorsPlaywright docs (opens in a new tab) - LowLine 16
Hardcoded environment URL
A full URL ties the test to one environment. Set `baseURL` in playwright.config and navigate with a path, e.g. `page.goto('/login')`.
await page.goto('https://staging.example.com/profile');No ESLint rule for this checkPlaywright docs (opens in a new tab)
Enforce these checks in CI
9 of these findings map to rules in eslint-plugin-playwright, so your pipeline can catch them on every pull request.
// npm install -D eslint-plugin-playwright
// eslint.config.mjs
import { defineConfig } from "eslint/config";
import playwright from "eslint-plugin-playwright";
export default defineConfig([
{
files: ["tests/**"],
extends: [playwright.configs["flat/recommended"]],
rules: {
"playwright/expect-expect": "error",
"playwright/missing-playwright-await": "error",
"playwright/no-focused-test": "error",
"playwright/no-force-option": "error",
"playwright/no-networkidle": "error",
"playwright/no-raw-locators": "error",
"playwright/no-unnecessary-assertions": "error",
"playwright/no-wait-for-timeout": "error",
"playwright/prefer-web-first-assertions": "error",
},
},
]);
What it checks
- Fixed waits,
networkidle, and deprecated wait APIs - Playwright calls and web-first assertions that aren't awaited
- Tests with no assertion, and assertions that can never fail
.only,skip, andfixmeleft in the codeforce: true, element handles, XPath and brittle CSS locators- Hardcoded passwords and tokens, and hardcoded environment URLs
Compare mode is built for reviewing tests an AI agent has “healed”. It flags removed assertions, changed expected text, new skips, and added fixed waits: the changes that can turn a real regression into a passing run. See what Playwright's healer agent is allowed to change.
What it doesn't do
It reads your code as text: comments and string contents are masked first, then pattern checks run. It doesn't execute or type-check the test, and it can't follow helper functions into other files. Treat each finding as a question for review, not a verdict.
Most checks match a rule in eslint-plugin-playwright. Use this page to review a snippet in a pull request or chat, and the ESLint plugin to enforce the same rules in CI. The reviewer generates that config for you.