This picks up exactly where How to Code a Web App leaves off — a service running locally (and on its early, unindexed preview URL), a login that works, screens that render. Running is not the same claim as correct. This lesson covers proving it: a functional test pass, a fix-up loop for whatever that pass turns up, and verification across the viewports and browsers a real visitor will actually use. It stops at "verified" — the next lesson in this series covers shipping it to the real, discoverable domain. If nothing runs yet, go get a local build first; there is nothing to test against here.
TL;DR
Testing a web app is not one phase, it's a rule applied over and over: if a check has a machine-readable oracle, the agent owns it — the human is only for what needs their eyes, their hands, their accounts, or their authority. Most of what "feels like" a human QA task on a web app — does the page load, did the record save, does the layout overflow at this width, did the right font apply — is actually fetchable, queryable, measurable, or drivable by the agent itself, because a browser is something an agent can automate directly. That split is the whole argument of this lesson. On top of it: a fix-up session is a loop, not a single edit — reproduce, diagnose, fix, re-verify, then check whether the same shape of bug exists anywhere else, because it usually does. And a screenshot only proves rough layout; it does not prove an exact value — colors, fonts, spacing, and viewport behavior need to be asserted programmatically, not eyeballed.
The three gates, not one "testing phase"
Collapsing testing into a single step loses the moments where different classes of bug actually surface. Split it into three named gates instead:
| Gate | What it catches | Who runs it |
|---|---|---|
| Dev-loop check | Build/type errors, obvious breakage, on every change | Agent, automatically |
| Pre-ship functional pass | Real user flows, negative paths, cross-viewport, cross-browser | Agent drives it, human confirms a short batched list |
| Post-deploy verification | Bugs that only exist in the deployed environment | Agent, read-only, against the real thing |
[Verified against an existing standard] A separate, older checklist this project maintains for building mobile apps structures testing the same way — as named gates with a stated pass criterion each, not one undifferentiated "test the app" step, and its pre-release gate is a fixed scenario list that starts with "the golden path, from zero, as a brand-new user." Keep that ordering here: a first-time signup-to-first-action walkthrough is scenario one, before anything else gets tested, because the worst defects live on the path every single visitor takes.
The one deliberate deviation from that mobile checklist: there, nearly every pre-release scenario is tagged [human], because on a phone build only a human can install and tap through it. On the web, the agent can drive a real browser directly — click, type, submit, resize, read computed styles, inspect the console. So the split shifts hard toward the agent. That shift is this lesson's whole point: don't default to "ask the human to check it" just because that's the native-app habit. Ask whether the check has a machine-readable answer first.
What the agent owns, and what it hands to the human
The agent does these itself, no human involved:
- Enumerates the surface — every route, every form, every outbound integration (email, notifications, third-party calls) — and publishes that list as the test plan. The human approves the plan, not each individual check.
- Runs the build/type gate.
- Sweeps every route: fetch it, assert the status code, assert key content is actually present in the response, not just "200."
- Drives every form: a valid submission, each required field left empty, each boundary value, a hostile input. Asserts the visible error message, not only the HTTP status — a form that returns 400 but shows the user nothing has not passed.
- Checks results against the datastore, never against the UI's own claim. "Saved ✓" on screen is not evidence; the row actually changing state is.
- Tests auth and ownership negatives: logged out, logged in as the wrong user, someone else's record ID, a garbage or expired token.
- Sweeps a declared set of viewports with numeric assertions (see below), not a glance at one screenshot.
- Reads the browser console and network tab on every page — some defects are invisible on screen and only visible there.
- Cleans up every account, listing, or record it created for testing, and reports having done so.
The agent asks the human — batched into one message, numbered, with an expected result stated per step, never "have a look and tell me":
- A real inbox. Did a given email arrive, in the inbox or spam, does it render, does its link work when clicked from their actual mail client. The agent cannot see someone's mailbox.
- A real device pass. Their phone, their browser, their network — a short, numbered script (4-6 steps) with an expected outcome per step.
- Anything gated behind credentials the agent doesn't hold — a payment sandbox, a third-party console.
- Perceptual or product judgment — is this copy right, is this the right number of items on the page. The human is the oracle for taste. Never for correctness.
- An explicit go/no-go on anything irreversible — sending a real broadcast, an account-affecting change, anything that can't be quietly undone.
The agent does not ask the human to check: whether pages load, whether redirects and canonical tags are correct (fetchable); whether a record actually changed state (queryable); whether a layout overflows at a given width (measurable); whether the intended font, size, or color actually applied (readable from computed style); whether an error message is correct (drivable); whether yesterday's fix broke something else (re-runnable). Every one of those has a machine-readable answer. Asking a human to eyeball something the agent could have measured wastes their time and adds a slower, less precise oracle where a faster, exact one was available.
The functional pass itself
A concrete checklist, in the order that catches the worst defects earliest:
- Golden path, from zero. A brand-new user's full first session — signup, first meaningful action, first result seen. This is scenario one, not an afterthought, because it's the one path every real visitor takes.
- Environment-divergent branches, explicitly enumerated. Some code only runs correctly in the deployed environment, never locally: a database only reachable from the platform's private network, a secure-cookie branch that never fires over local plain HTTP, an absolute URL that resolves to a container's internal hostname instead of the public one. List these per project. They are untestable where you built them — the functional pass has to run at least once against the deployed preview URL, not only against localhost, or an entire branch of the app goes unverified until a real user hits it.
- Every form, every boundary. Valid input, missing required fields, boundary values, hostile input (script tags, oversized payloads, wrong types). Assert the message shown, not just the status code.
- Auth and ownership negatives. Logged out, wrong user, someone else's ID, an expired or garbage token — each should fail the same clean way, not with a generic 500 or a silent no-op.
- Every outbound integration, hit for real. A real email to a real inbox, a real notification, a real webhook call — never mocked at this stage. A newsletter script that logs "sent" and writes a database row while silently running in a no-op dev mode because one environment variable was missing is a real category of bug, and it only shows up if something checks the actual outbound side, not the code path's own self-report.
- Cleanup, verified. Delete every test account and record created, and confirm the deletion — not just that a delete call was issued.