Playwright Next.js Testing Tutorial | Coding Capybaras
A Playwright Next.js testing tutorial for founders: the five flows worth testing, the CI setup, how to read a failure, and the prompt that writes the tests.
· Justin Boggs

Photo by Peter Herrmann on Unsplash
End-to-end testing your Next.js SaaS with Playwright means writing a handful of scripts that drive a real browser through your app the way a customer would — sign up, pay, use the thing, change a setting — and fail loudly when any of it breaks. You do not need to write them by hand. Your AI assistant writes them well, because Playwright's API reads like a description of user behavior. What you need is the judgment to pick which five flows deserve tests, the CI wiring to run them on every push, and enough literacy to read a failure. That's what this tutorial covers.
TL;DR
- End-to-end tests drive a real browser. They catch the breakages that unit tests miss, like a broken redirect after checkout.
- Test five flows, not fifty: signup, checkout, the core product action, billing/account changes, and one failure path.
- Let your AI assistant write the specs, but give it the Playwright best-practices rules — role-based locators, web-first assertions, no CSS selectors.
- Run them in GitHub Actions on every pull request, against a production build, with traces on retry.
- A red CI run is the point. Read the trace, paste the failure back to your AI, ship the fix.
What end-to-end testing actually means for a SaaS
An end-to-end test is a script that opens a real browser, performs a sequence of user actions against your running app, and asserts that the app responded correctly. No mocks of your own code. No poking at internal functions. The test clicks the button a human would click and checks that the page a human would see appeared.
That's a different job from the unit tests I wrote about in AI-generated tests as a safety net for non-coders. A unit test proves that your discount math returns $80. An end-to-end test proves that a customer can actually reach the page where that discount gets applied, click through Stripe, and land back on a dashboard that says "Pro."
Both matter. But for a small SaaS run by one person, end-to-end tests earn their keep faster, because the bugs that cost you money are almost never wrong arithmetic. They're a redirect that stopped working after a route rename. A sign-in button that renders but no longer submits. A webhook that fires while the success page 404s. Those all pass every unit test in your repo and still lose you the customer.
Playwright is Microsoft's browser automation framework, and it's the sane default here. Next.js documents it as a first-class option with a create-next-app template, it drives Chromium, Firefox and WebKit through one API, and TypeScript works out of the box without you configuring anything.
The part that makes it viable for non-engineers is the API's vocabulary. A Playwright test says page.getByRole('button', { name: 'Sign in' }).click(). You can read that. You can tell whether it matches what your app does. That readability is what turns an AI-written test suite from a black box into something you can actually supervise — and supervision is the whole game when you're shipping code you didn't write yourself.
Here's how the three test types divide the work:
| | Unit test | Integration test | End-to-end test | | --- | --- | --- | --- | | What it runs | One function | A few pieces together | The whole app in a browser | | Speed | Milliseconds | Fast | Seconds to a minute | | Catches | Bad logic | Bad wiring between modules | Broken user journeys | | Misses | Everything above it | Browser-level breakage | Subtle math errors | | How many you need | Dozens | A handful | Five to ten | | Cost when it breaks | Cheap to fix | Moderate | You hear about it from a customer first |
The ratio matters. Ten end-to-end tests that cover real journeys beat two hundred that click every button on every page. Every test you add is a test you maintain, and a slow, flaky suite gets ignored — which is worse than no suite at all, because you stop reading the red.
The five flows worth testing
Pick the flows where breakage costs you money or trust. For nearly every SaaS, that's the same five.
1. Signup and sign-in. If nobody can create an account, nothing else matters. Test the full path: land on the marketing page, click through to signup, complete it, land on the dashboard authenticated. If you use magic links or OAuth, test the callback specifically — that's where it breaks. A magic-link flow has more moving parts than it looks like, and the failure mode is silent.
2. Checkout and subscription activation. The money path. A test should take a signed-in free user through your pricing page, into Stripe Checkout, and back to a dashboard that reflects the new plan. Use Stripe's test mode and the 4242 4242 4242 4242 test card so this runs safely in CI forever. The assertion that matters isn't "Stripe accepted the card" — Stripe is fine, that's their job. It's "my app noticed." Which means the test needs to check that the plan badge, the feature gate, or the entitlement actually changed after the redirect. This is precisely the class of bug I lost a weekend to in Stripe webhook hell.
3. The core product action. Whatever your app is actually for. Create the record, run the report, generate the thing. One test, the happy path, start to finish. If a customer would email you about it being broken within the hour, it belongs here.
4. Account and billing changes. Update email, change plan, open the billing portal, cancel. These are lower traffic than signup but they're where people are already annoyed, so a bug here converts directly into churn. If you've wired up the Stripe customer portal, test that the button opens a portal session for the right customer — not that the portal itself works, which is Stripe's problem.
5. One failure path. Pick the most likely wrong turn and prove it's handled gracefully. A signed-out user hitting a protected route should be redirected to sign-in, not shown a stack trace. A free-tier user hitting a Pro feature should see an upgrade prompt, not a 500. This is the test founders skip and the one that makes your app feel finished.
That's it. Five tests, maybe eight once you split a couple. If you're tempted to add a sixth, ask whether a failure in that flow would actually reach a customer before you noticed. Usually the answer is no.
One rule from Playwright's own guidance is worth internalizing here: don't test third-party services. Don't assert that Stripe's checkout page renders correctly or that your email provider's dashboard loads. You don't control those, they change without telling you, and every one you test is a future 3 a.m. red build that isn't your fault and isn't your problem.
Setting it up in a Next.js app
Installation is one command from your project root:
pnpm create playwright
It prompts for a test directory (take the default, tests/ or e2e/), whether to add a GitHub Actions workflow (yes), and whether to install browsers (yes). When it finishes you have a playwright.config.ts and a sample spec.
Three config changes matter for a Next.js SaaS. First, set a baseURL so tests can say page.goto('/pricing') instead of repeating localhost everywhere. Second, use the webServer option so Playwright starts your app itself and waits for it to be ready, instead of you remembering to start it. Third — and this is the one people skip — run against a production build, not the dev server.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
webServer: {
command: 'pnpm build && pnpm start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
})
Next.js recommends testing against production code because it more closely resembles how the app actually behaves. It also dodges a whole category of false failures where a test times out waiting on a dev-server recompile. The tradeoff is that the build has to finish before tests start, which adds a minute or two. Worth it.
A warning specific to this boilerplate, and to any repo where you run a dev server while an agent works: pnpm build and pnpm dev both write to .next/, and running them at once corrupts each other's compiled chunks. If your dev server is up, don't kick off a Playwright run that builds. Stop the server first, or point the test run at a separate port. I lost an afternoon to a "Tailwind is broken" bug that was two processes fighting over one directory.
One browser is enough to start. Playwright makes it trivial to add Firefox and WebKit later, and you should before you have real customers on Safari, but three browsers triples your CI time on day one for very little signal.
Start the local loop in UI mode:
npx playwright test --ui
That opens a watch-mode panel where you can run one test, step through it, and see exactly what the browser saw at each action. It's the single best tool here for a non-engineer, because it turns "the test failed" into a filmstrip you can scrub.
The prompt that writes the tests
You are not typing these tests out. You're specifying them and reviewing the result. But a bare "write me some Playwright tests" produces exactly what you'd expect: brittle CSS selectors, waitForTimeout(3000) sprinkled everywhere, and tests that pass locally and fail in CI at random.
The fix is to hand your assistant the rules up front. Here's the prompt I use, and the version worth putting in your CLAUDE.md file so you never retype it:
Write a Playwright end-to-end test for the signup flow in this Next.js app. Follow Playwright's official best practices:
- Use role-based locators (
getByRole,getByLabel,getByText). Never CSS classes or XPath.- Use web-first assertions (
await expect(locator).toBeVisible()). Neverexpect(await locator.isVisible()).toBe(true).- Never use
waitForTimeout. Rely on auto-waiting.- Each test must be fully isolated — no shared state between tests.
- Do not test third-party services. Stub external API calls with
page.route.- Read the actual route and component files before writing, and use the real labels and button text from the JSX rather than guessing.
Then tell me which assertions you chose and why, and which parts of the flow you did not cover.
The last line is the one that earns its keep. Asking for the gaps forces the model to be explicit about what it skipped, and that list is usually where the real bugs are hiding. This is the same review discipline that makes AI code review work at all — you're not checking the syntax, you're checking the judgment.
Two failure modes to watch for in what comes back. The first is invented labels: the test asserts on a button that says "Get started" when your app says "Start free." That's the same file-path and API hallucination problem in a new costume, and the fix is the same — make the model read the source first, which the prompt above does explicitly.
The second is fake passing. A test that navigates to a page and asserts the page loaded technically passes and proves nothing. If every assertion in a generated test is toBeVisible() on something that was already on screen before the action, the test is theater. Ask: what changed as a result of what the user did, and does the test check that?
Playwright also ships a recorder — npx playwright codegen localhost:3000 — that watches you click through your app and writes the test for you, picking resilient locators automatically. For a founder, the best workflow is often a hybrid: record the raw clicks yourself, then hand the recording to your AI and ask it to clean it up, add meaningful assertions, and name the test properly.
Wiring it into CI and reading the failures
Tests you have to remember to run are tests you don't run. The whole value arrives when they run automatically on every push — the same principle behind the Next.js deployment recipes that put a preview build behind every pull request.
pnpm create playwright already offered to generate .github/workflows/playwright.yml. The generated file works. Three tweaks make it better for a solo SaaS:
- run: npx playwright install chromium --with-deps
Install only the browsers you actually test with — Playwright calls this out specifically, and it saves real download time and disk on every run. Then make sure failures are inspectable:
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 7
And keep trace: 'on-first-retry' in your config rather than 'on'. Traces are heavy; recording one for every passing test wastes minutes for no benefit. On retry, you get a trace exactly when something went wrong.
Here's the shape of the loop:
flowchart LR
A[Push to branch] --> B[GitHub Actions]
B --> C[pnpm build]
C --> D[Playwright: 5 flows]
D -->|all green| E[Merge + deploy]
D -->|red| F[Download trace artifact]
F --> G[Open trace viewer]
G --> H[Paste failure to AI]
H --> A
Then the part nobody teaches: reading a failure. A Playwright failure is unusually informative if you know where to look. It names the test, the line, and prints an expected-versus-received block. Expected: visible / Received: hidden means the element never appeared. Timeout 30000ms exceeded on a locator means the thing you asked for was never found — usually a changed label, not a broken feature.
For CI failures specifically, use the trace, not the screenshot. Playwright's trace viewer gives you a timeline of every action with a DOM snapshot at each step, plus the network requests. You download the artifact, open it, and scrub to the moment it went wrong. It tells you whether your app returned a 500, whether the button rendered with different text, or whether a redirect went somewhere unexpected.
Then paste the whole failure — test name, error block, and what you saw in the trace — back to your assistant. A failing test is the highest-quality bug report you can hand an AI, because it's a precise, reproducible description of a gap between expected and actual. That's a much better starting point than "the signup is broken," and it's the same technique that makes debugging with Claude Code work for non-technical founders.
One caution: when a test fails, the first question is "did my app break?" not "is the test wrong?" The instinct to delete or weaken a failing test is strong and it's almost always the wrong call. If you genuinely changed the behavior on purpose, update the test to match the new intent. If you didn't, you just caught a regression before a customer did, which is the entire reason the test exists.
Frequently asked questions
Do I need end-to-end tests before I launch?
No. Ship first, test the flows that hurt. The honest sequence is: launch, get your first few customers, then add tests for signup and checkout because those are the two things you can't afford to break silently. Adding a full suite pre-launch, before you know which flows matter, is a way to feel productive without being productive.
How long should a Playwright suite take to run?
Under three minutes for five flows on a single browser, including the production build. If it creeps past five, look for waitForTimeout calls or tests that sign up a new user when they could reuse a signed-in session. Playwright supports reusing authenticated state via a setup project, so you log in once and skip it for every subsequent test.
What's a flaky test and why does everyone complain about them?
A flaky test passes and fails on the same code depending on timing. They're corrosive because they train you to ignore red builds. Nearly all flakiness in Playwright comes from manual waits or from asserting on something before it has rendered — using web-first assertions and auto-waiting locators eliminates most of it by design.
Can I test Stripe checkout without real charges?
Yes. Stripe test mode gives you card numbers that always succeed, always decline, or always require authentication. Run your CI against test-mode keys and the whole checkout flow exercises safely, forever. Just don't assert on the contents of Stripe's own hosted page — assert on what your app does after the redirect.
Should I use Playwright or Cypress?
Either works, and if you already have Cypress, keep it. For a new Next.js project in 2026 I'd pick Playwright: it's in the official Next.js testing docs, it covers WebKit (so real Safari behavior), the trace viewer is better than anything comparable, and the locator API reads more like plain English — which matters a lot when an AI is writing the tests and you're reviewing them.
Where do the test files live in this boilerplate?
Alongside the code they cover, same as the unit tests. End-to-end specs go in a top-level e2e/ directory since they test the app as a whole rather than one region. Keep them out of /platform/ — those flows belong to your product, not the shared plumbing.
Ship the five, not the fifty
End-to-end testing your Next.js SaaS with Playwright is a small, bounded job that most founders either skip entirely or turn into a months-long project. Neither is right. Five tests covering signup, checkout, your core action, account changes, and one failure path will catch the overwhelming majority of breakage that would otherwise reach a customer — and your AI assistant can write all five in an afternoon if you give it the right constraints.
The skill worth building isn't writing tests. It's deciding what deserves one, and reading a failure well enough to hand it back. Both of those are founder judgment, not engineering, and neither gets easier by waiting.
If you're wiring this into a Next.js app that also needs auth, billing, and email, Coding Capybaras is the free boilerplate I built for exactly this workflow — and the marketplace has copy-paste prompts for the integrations mentioned above.