Your Playwright test is green. Your checkout is broken

2026-11-04来源 dev.toEnglish
Your Playwright test is green. Your checkout is broken

Here is a Playwright test I've seen in almost every codebase I've worked on:

test("places an order", async ({ page }) => { await page.goto("/"); await page.getByLabel("Name").fill("Ada"); await page.getByRole("button", { name: "Place order" }).click(); await expect(page.getByRole("status")).toContainText("Order placed"); }); 

Good locators, a web-first assertion, no sleeps. It passes.

It also passes when the order API returns 500 and nothing is saved.

Why the test can't see it

The assertion checks the page, and the page is lying. Three very common frontend patterns produce a success message whether or not the server agreed:

// 1. Optimistic UI: show success first, send the request after showToast("Order placed"); await fetch("/api/orders", { method: "POST", body }); // 2. A catch that swallows the error await fetch("/api/orders", { method: "POST", body }).catch(() => {}); showToast("Order placed"); // 3. Checking that a response arrived, not that it succeeded const res = await fetch("/api/orders", { method: "POST", body }); if (res) showToast("Order placed"); // res.ok is never checked 

None of these throw. None of them log anything a test would notice. The user sees "Order placed", the test sees "Order placed", and the order never exists.

Here are three ways to catch it, from most precise to most general.

Fix 1: assert on the response

Wait for the request the click triggers, and check its status:

const [res] = await Promise.all([ page.waitForResponse((r) => r.url().endsWith("/api/orders") && r.request().method() === "POST"), page.getByRole("button", { name: "Place order" }).click(), ]); expect(res.status()).toBe(201); await expect(page.getByRole("status")).toContainText("Order placed"); 

This is the best check for a single test: exact endpoint, exact status. The catch is that you have to remember it on every test that writes data, and most suites don't. Mine didn't.

Fix 2: read the data back

A 201 still doesn't prove the order was stored. If your API has a list endpoint, ask it:

await expect .poll(async () => { const orders = await (await page.request.get("/api/orders")).json(); return orders.some((o) => o.name === "Ada"); }) .toBe(true); 

This catches the nastiest version of the bug: the server answers 200 and silently drops the write. expect.poll retries, so eventual consistency doesn't make it flaky.

Fix 3: a safety net for the whole suite

Fixes 1 and 2 only protect the tests you add them to. For everything else, you can make any test fail when the app answers with a server error, using an automatic fixture:

// fixtures.ts import { test as base, expect } from "@playwright/test"; export const test = base.extend<{ failOnServerErrors: void }>({ failOnServerErrors: [ async ({ page }, use) => { const problems: string[] = []; page.on("response", (res) => { if (res.status() >= 500) problems.push(`${res.request().method()} ${new URL(res.url()).pathname} returned ${res.status()}`); }); page.on("requestfailed", (req) => { if (req.method() !== "GET") problems.push(`${req.method()} ${new URL(req.url()).pathname} failed: ${req.failure()?.errorText}`); }); await use(); expect(problems, "the app reported errors during this test").toEqual([]); }, { auto: true }, ], }); export { expect }; 

Then import test from ./fixtures instead of @playwright/test. The test from the top of this post now fails:

Error: the app reported errors during this test - Array [] + Array [ + "POST /api/orders returned 500", + ] 

Twenty lines, no dependencies. I'd encourage you to try it on your suite today. You might be surprised what's been hiding.

Where the 20-line version falls short

I ran a version of this for a while and hit these problems:

  • Third-party noise. An analytics or ads endpoint returns a 500 and your checkout test fails. You need to watch only your own site.
  • Aborted requests. Navigating away cancels in-flight requests (net::ERR_ABORTED). Those aren't bugs.
  • Popups and new tabs. Listening on page misses requests from other pages. Listen on the browser context instead.
  • Tests that expect an error. A test for "show a friendly message when the server is down" needs a way to opt out, per test or per endpoint.
  • 4xx on writes. A rejected POST is often a real bug, but validation tests expect a 422. It should warn, not fail, unless you ask for strict.
  • The error lands as one assertion message. It's more useful as an attachment in the report, next to the trace.

I ended up packaging this as an open-source drop-in, @vedhajs/playwright-test. It handles the cases above. You change one import:

import { test, expect } from "@vedhajs/playwright-test"; // instead of "@playwright/test" 

The default is to fail on any 5xx or failed write from your own site, and warn on 4xx writes and page errors. It ignores third parties and attaches vedha-guard.json to each test. Opt out with test.use({ vedhaGuard: false }) or { ignore: ["/api/flaky"] }. It's still Playwright's runner, so retries, sharding and traces work as before.

The same Playwright test with two imports: @playwright/test passes, @vedhajs/playwright-test fails with "POST /api/orders returned 500"

It does not replace Fix 2. A write that returns 200 and stores nothing needs a read-back, and only you know which endpoint to read.

This matters more now that agents write tests

If Copilot, Claude Code or Cursor writes your tests, you get the test at the top of this post, every time: it checks what's on screen and almost never adds a response assertion. Put Fix 3 (either version) in the fixture they import from, and add a line to your AGENTS.md / CLAUDE.md telling them to use it. Agents copy the patterns they find.

Summary

Check Catches Cost
toContainText("Order placed") UI regressions Free, but trusts the page
waitForResponse + status 5xx/4xx on that request One block per write, easy to forget
Read-back with expect.poll Writes that weren't stored Needs a read endpoint
Suite-wide fixture 5xx and failed writes in every test One import

Have you shipped a bug that a green test was hiding? I'd like to hear what it was.

Code, demo and benchmark data for the package: https://github.com/HMAKT99/VEDHA (Apache-2.0)

原文:https://www.a8q9.cn/en/article/your-playwright-test-is-green-your-checkout-is-broken-46dc66.html

相关文章