Quiz - E2E on money paths only
Your invoice app already has an integration test proving the Stripe webhook flips the plan in Postgres, and a component test proving the billing page renders the “Pro” badge when passed a Pro plan. A teammate wants to also add a Playwright test for the full upgrade. Does this one earn its weight?
Yes — only the browser can prove the session survives the redirect out to Stripe and back, and that the UI flips after the out-of-band webhook lands; that composition is what neither cheaper test sees.
No — the integration test plus the component test already compose to cover this, so the Playwright test only adds cost and flake.
No — checkout touches a third party Playwright can’t drive deterministically, so it belongs at the seam, not in the browser.
A small team on this stack has a disciplined integration suite and Sentry-led production observability, and ships its entire first year with zero Playwright tests. How should you read that?
It’s the correct default — the seam suite catches the clustered bugs, observability catches the unknowns, and manual smoke covers the rest until the runtime cost is worth paying.
It’s a coverage gap they’re behind on — the four money paths should have been in place from day one and the team is carrying hidden risk.
pnpm create playwright scaffolds a config with retries: 2, but the course overrides it to retries: process.env.CI ? 1 : 0. Why prefer one CI retry over the scaffold’s two?
One re-run is enough to tell a flake apart from a real failure; more retries start hiding real bugs behind a green check.
Two retries triple the CI runtime, and one retry is the cap that keeps the suite under the two-minute budget.
trace: 'on-first-retry' only captures a trace on the first retry, so any retry past the first records nothing useful.
retries: 3 is the anti-pattern from lesson 1 — it mutes the alarm on a real race instead of fixing it. The runtime and trace-capture framings are plausible but not the reason; the reason is keeping the flake signal honest.Why does the E2E suite need a separate saas_e2e Postgres with a full reset between runs, instead of reusing the integration suite’s per-worker DB with a transaction rollback per test?
Playwright drives the server in a different process, so the test has no handle on the server’s transaction and can’t open-then-roll-back around a request — isolation has to move to a full reset between runs.
Playwright runs tests fully in parallel, and transaction rollback isn’t safe under parallel workers, so a reset is the only parallel-safe option.
A production build can’t connect to a per-worker test database, so E2E needs a single long-lived database that survives the whole run.
This assertion is in a merged spec and the test is passing, but a reviewer flags it as dangerous:
expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();What’s wrong?
The missing await — the web-first matcher returns an unawaited promise, so the test moves past it without ever checking the condition and passes silently.
toBeVisible doesn’t auto-wait, so it needs a preceding await page.waitForSelector(...) to avoid racing the redirect.
getByRole with a regex name can match more than one heading, so the assertion is ambiguous and should use getByText.
await it. Drop the await and it returns a promise nobody waits on — the assertion never executes and the test passes while checking nothing, worse than a crash. The fix is one keyword. toBeVisible already auto-waits, so no waitForSelector is needed.The Stripe Checkout spec fills the card fields with page.frameLocator('iframe[name^="__privateStripeFrame"]').getByLabel(/card number/i). Why reach inside a frameLocator here when every other path uses plain page locators?
Stripe nests its card fields in iframes the page doesn’t own, and a normal locator can’t see across that boundary — frameLocator scopes into the frame, where you still pin to role and label, never CSS.
frameLocator is how Playwright drives a third-party origin; any locator on checkout.stripe.com must go through it because the page is cross-origin.
Stripe’s fields load asynchronously, and frameLocator is the only locator that auto-waits for late-rendered inputs.
page locator can’t cross — frameLocator is the handle that reaches inside it, and within the frame you keep using role-and-label locators. Crossing origins doesn’t require it (the redirect assertion uses plain page), and auto-waiting is a property of all locators, not unique to frameLocator.In the invoice value-loop spec, each test names its record invoice-${test.info().title}-${Date.now()} and asserts on that exact row, with no afterEach cleanup. Why this shape on a shared seeded org?
Parallel workers all write to the one saas_e2e org, so a unique id per test prevents row collisions, and the next db:e2e:reset is the canonical clean — deferred cleanup, not per-test deletes.
A timestamped id forces Playwright to create a fresh database per test, which is what guarantees isolation under fullyParallel.
Skipping afterEach keeps the test faster; the unique id is just a readable label so failures are easier to find in the report.
Your app’s primary sign-in is “Continue with Google.” For the per-PR Playwright suite, what’s the recommended way to cover it?
Drive the flow up to the redirect and assert the redirect URL your app built is correct — right client id, scopes, callback — and leave the full provider round-trip to an occasional manual pass.
Automate a real Google test account through the consent screen so the per-PR suite exercises the genuine round-trip.
Skip OAuth entirely in E2E and rely only on the integration test for the token exchange.
Quiz complete
Score by topic