Skip to content
Independent guides for QA & test automationRSSEditorial policy
QA Vibes

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

6 high4 medium6 low2 tests · 3 assertions
  • High

    Focused test (.only)

    Line 3

    `.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)
  • High

    Hardcoded credential

    Line 7

    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)
  • High

    Fixed wait (waitForTimeout)

    Line 9

    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)
  • High

    Missing await

    Line 11

    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)
  • High

    Assertion that can never fail

    Line 12

    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)
  • High

    Test without an assertion

    Line 15

    “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)
  • Medium

    Waiting for networkidle

    Line 5

    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)
  • Medium

    Forced action (force: true)

    Line 8

    `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)
  • Medium

    XPath locator

    Line 8

    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)
  • Medium

    Assertion that doesn't retry

    Line 10

    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)
  • Low

    Hardcoded environment URL

    Line 4

    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)
  • Low

    CSS or XPath locator

    Line 6

    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)
  • Low

    CSS or XPath locator

    Line 7

    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)
  • Low

    CSS or XPath locator

    Line 10

    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)
  • Low

    CSS or XPath locator

    Line 12

    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)
  • Low

    Hardcoded environment URL

    Line 16

    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, and fixme left in the code
  • force: 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.