browser-qa — verify with the browser, not by guessing
When to use this skill
Trigger when the user wants confirmation that a user-visible flow actually works. Strong signals:
- "test the signup flow", "verify checkout", "qa this page"
- After implementing a UI change, before declaring it done
- When a unit test passes but the user says "it's still broken"
- "screenshot what the page looks like at "
Do not trigger for: pure logic tests (use test-architect), API contract tests (use api-architect), or for code that has no UI surface.
The output contract
A Playwright run that produces:
- A test file (or files) that another engineer can read in 60 seconds and understand the flow.
- A pass/fail result with the specific failing step and a screenshot of that step.
- No flaky waits — every wait is anchored to an observable condition (a network response, a visible element, a URL change).
- A trace file the user can open in
npx playwright show-tracefor any failure.
Workflow
1 — Reconnaissance
- Is Playwright already installed? Check
package.jsonandplaywright.config.{ts,js}. - If not:
npm i -D @playwright/test && npx playwright install --with-deps chromium. - Read the existing test directory (
tests/,e2e/,playwright/) to learn the project's conventions before writing new tests.
2 — Map the flow
Before writing the test, write the steps in plain English. Example:
signup flow:
1. visit /signup
2. fill email + password
3. click "Create account"
4. expect navigation to /verify-email
5. open the verification link from the test mailbox
6. expect /onboarding
Show this to the user. Confirm the flow matches before coding. Half of bad e2e tests fail because they tested the wrong sequence.
3 — Write the test
Use Playwright's built-in locators in this priority order:
getByRole('button', { name: 'Sign in' })— accessible, resilient to copy changesgetByLabel('Email')— for form inputsgetByTestId('checkout-submit')— when nothing else is stable- CSS selectors — last resort, only when the above don't fit
Never use XPath. Never use page.locator('div > div > div:nth-child(3)'). Those are landmines.
4 — Eliminate flake at write time
For every interaction that triggers async work:
- After a click that submits a form →
await page.waitForResponse(r => r.url().includes('/api/auth/signup') && r.ok()) - After a navigation →
await expect(page).toHaveURL('/onboarding') - After a state change →
await expect(page.getByText('Welcome')).toBeVisible()
Never await page.waitForTimeout(2000). If you find yourself reaching for it, the test is wrong.
5 — Capture on failure
Configure playwright.config.ts:
use: {
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
When a test fails, point the user at the trace: npx playwright show-trace test-results/<name>/trace.zip.
6 — Run and report
Run with --reporter=list for human-readable output. After the run:
- If green: report which flows passed, and which assertions actually fired.
- If red: report the specific step that failed, the expected vs actual, and the path to the screenshot.
Patterns and anti-patterns
✅ Do:
- One flow per test file.
signup.spec.ts,checkout.spec.ts,password-reset.spec.ts. - Use
test.describe.serial(...)only when state is genuinely shared. Default to parallel. - Pin Playwright + browser versions in
package.json. A floating browser breaks tests in CI. - Mock or stub external services (Stripe webhooks, email providers) — your test owns the failure, not their flakiness.
❌ Don't:
- Don't share auth state via global variables. Use
storageStateper test or per worker. - Don't assert on the absence of an element with
toBeHidden()without atimeout— the negative path is the flakiest in Playwright. - Don't run against prod. Have the user point at staging or local; refuse to run destructive flows against prod URLs.
- Don't catch the error and
console.logit. Let the test framework fail.
Example invocation
User: "Verify the signup → first-login flow works on localhost:3000."
- Check the project for an existing Playwright config — none found.
- Install Playwright and Chromium with deps.
- Map the flow with the user: visit /signup → fill form → submit → land on /onboarding → see welcome message.
- Write
tests/signup.spec.tsusinggetByRoleandgetByLabel. - Add
waitForResponseon the signup POST andtoHaveURL('/onboarding')after redirect. - Run:
npx playwright test tests/signup.spec.ts --reporter=list. - Report: 1/1 passed, trace at test-results/signup/trace.zip if you want to scrub through it.
See also
test-architect— for unit and integration tests that don't need a browsercode-auditor— to find logic bugs the e2e didn't reachui-polish— when the test passes but the page still looks broken