How to test a multi-step form: state, branches, and the back button

A multi-step form is a state machine wearing a progress bar. Here's how to test one end to end — per-step validation, conditional branches, back-button state, and the final payload — in plain English, no scripts.

playbookformsno-codeqa
monito

How to test a multi-step form: state, branches, and the back button

playbookformsno-codeqa
July 28, 2026

A single form is easy to test: fill the fields, submit, check what happened. A multi-step form looks like the same thing spread over three screens, and that framing is exactly why it ships broken. It isn't one form in three parts. It's a small state machine with a progress bar bolted on top — a pile of accumulated answers that has to survive forward navigation, backward navigation, conditional branches that hide and show whole steps, a browser refresh, and a final submit that has to gather every field the user touched, including the ones that scrolled off screen two steps ago.

The happy path — fill step one, next, fill step two, next, fill step three, submit — is the part that works, because it's the part everyone clicks by hand before shipping. The bugs live in the transitions: the value that resets when you go back to fix a typo, the step that should have been skipped but wasn't, the validation you can walk straight past, and the submit payload that's missing the field from step one because that step unmounted and took its state with it. This is the playbook for testing the whole state machine without writing a line of Playwright.

What "testing a multi-step form" actually has to cover

The fields are trivial. The machine around them is where the surface is:

  1. Per-step validation that actually gates. Each step should refuse to advance while it's invalid — and crucially, the "Next" button is not the same guarantee as the final "Submit." A wizard that validates on submit but lets you skip through empty steps is a wizard that collects garbage and 500s at the end.
  2. State that survives navigation. Go forward, go back, go forward again. Every answer you already gave should still be there. The classic bug is a step that re-mounts on "Back" and clears itself, so returning to fix one typo silently wipes the other four fields.
  3. Conditional branches. "Are you a business or an individual?" on step one changes which steps exist after it. Pick business, you get a tax-ID step; pick individual, you skip it. Both paths need coverage, and so does the nasty case: choose business, fill the tax-ID step, go back, switch to individual — does the now-irrelevant tax ID get cleared, or does it ride along in the final payload?
  4. The progress indicator's honesty. "Step 2 of 4" should match reality, including when a branch makes it 3 steps instead of 4. A progress bar that lies about how many steps remain is a small bug that reliably increases abandonment.
  5. Refresh and resume. Reload mid-wizard. Does the user land back where they were with answers intact, or on step one with an empty form and a bad mood? Whichever you intend, test that it's what actually happens.
  6. The final payload. This is the one that matters. When the user hits submit on the last step, does the request contain every field from every step — including hidden branch fields and step-one values the UI stopped showing long ago? Multi-step forms love to submit a subset of themselves.
  7. The double-submit. One click on the final button makes one record. Two fast clicks make one record, not two, and not one record plus one unhandled error.

Most teams test items 1 and the happy path, ship, and meet items 2, 3, and 6 as production support tickets over the following months — usually reported as "it lost my information," which is the hardest bug to reproduce because it depends on the exact order the user clicked. The prompts below drive that order deliberately.

Nothing below references a field ID, a step selector, or a DOM path. A multi-step form gets redesigned constantly — steps get split, merged, reordered — and a test pinned to today's step indices rots on the first redesign. Describing intent instead of steps is why this approach scales where scripted form tests don't.

Prompt 1: the happy path, then break the navigation

Test the multi-step signup wizard on https://staging.yourapp.com/onboarding.

1. Walk the entire flow with valid data, step by step, to completion.
   Note how many steps there are and what each one asks for.
2. Now start over. On the LAST step before submitting, use the wizard's
   own "Back" navigation to return to the FIRST step. Confirm every
   value you entered is still there, unchanged.
3. From step one, move forward again through every step. Confirm none
   of your earlier answers were lost or reset on the way back through.
4. Change one value on an early step, go forward to the end, and confirm
   the change stuck (and didn't revert to the original).

Report any step where a field was blank, reset, or reverted after
navigating away and back. Report all console errors, even ones that
didn't visibly break anything.

Step 2 is the test almost no scripted suite runs, because the scripted suite fills the form once, front to back, and submits — it never navigates backward, so it never sees the re-mount bug. The agent reads the actual rendered values on each step, so "the field is still there" and "the field looks there but reverted to the default" come back as different outcomes instead of one green tick.

Prompt 2: per-step validation that can't be skipped

On https://staging.yourapp.com/onboarding, test that each step
validates before it lets you advance.

For each step in the wizard:
1. Leave a required field empty (or enter something obviously invalid,
   like "abc" in an email or amount field) and try to advance with
   "Next".
2. Confirm the wizard REFUSES to advance and shows a clear, specific
   error on the offending field — not a generic banner, and not silent
   nothing.
3. Fix the field and confirm the error clears and "Next" now works.
4. After reaching the final step legitimately, check: is there any way
   to reach submit with an earlier step still invalid? Try using any
   step indicator, breadcrumb, or URL to jump ahead past an unfilled
   step.

Flag any step you can advance past while it's invalid, and any case
where the final submit is reachable with an earlier step incomplete.

Step 4 is the one that finds the expensive bug: a wizard where the "Next" button validates but a clickable step-number breadcrumb doesn't, so a user (or an impatient one) jumps to the end and submits a half-empty form. The server then either rejects it with a confusing error or, worse, accepts it and creates a broken record. An agent that tries the breadcrumb and the button finds the gap between them; a script that only clicks "Next" never does.

Prompt 3: the conditional branch, both directions

On https://staging.yourapp.com/onboarding, test the conditional
branching. The flow changes based on the "account type" choice on the
first step (business vs. individual, or whatever the equivalent is).

1. Choose the first option. Walk to the end and note exactly which
   steps appeared. Confirm the progress indicator ("step X of Y")
   matched the real number of steps.
2. Start over, choose the second option, and confirm a DIFFERENT set
   of steps appeared, correct for that choice, with the count again
   matching the indicator.
3. Now the tricky one: choose the option that reveals an extra step,
   fill that extra step in, then go BACK and switch to the option that
   should NOT have that step. Confirm the extra step disappears from
   the flow.
4. Complete the flow on that second choice and, if you can see the
   submission or a confirmation summary, report whether the data from
   the abandoned branch is gone or still hanging around.

Report any mismatch between the steps shown and the choice made, any
wrong "step X of Y" count, and any leftover data from a branch the
user backed out of.

Step 3 is the single highest-value check in this whole post. Conditional branches are usually tested in the forward direction only — pick A, see A's steps; pick B, see B's steps — and that passes. The bug hides in the switch: fields from the abandoned branch that never get cleared and ride along in the final submission, so a user who briefly considered the business plan ships a tax ID they don't have to your backend. Testing it by hand requires remembering to do exactly this dance every time; an agent does it the same way on every run, which is the reason agents catch bugs your scripts miss.

Prompt 4: refresh, resume, and the final payload

On https://staging.yourapp.com/onboarding, test persistence and the
final submission.

1. Fill the first two steps with distinctive, memorable values, then
   RELOAD the page. Report exactly what happens: does the wizard resume
   where you left off with data intact, restart empty, or something in
   between? State which, clearly.
2. Complete the whole flow to a successful submit.
3. Watch the network tab for the final submit request. Confirm its
   payload contains a value from EVERY step, including the very first
   one and any conditional step. Call out any field the user entered
   that is missing from the request.
4. Immediately after a successful submit, click the submit button again
   (or reload and resubmit if the UI allows). Confirm this does NOT
   create a second record — the button should disable, or the app
   should guard against the duplicate.

Report the final payload's completeness explicitly, any 4xx/5xx that
the UI didn't surface, and whether a double-submit was prevented.

Step 3 is the payoff. The most common serious multi-step-form bug is a submission missing early-step data, because those steps unmounted and their state wasn't lifted anywhere durable. It passes every visual check — the UI looked complete — and only shows up in the request body or in a database row that's missing a column the user definitely filled in. The agent reading the actual network request is the only witness to that gap; a screenshot shows a happy confirmation screen sitting on top of an incomplete record.

Reading the results

Each run comes back as a Monito Session, not a pass/fail bit: a screenshot timeline of every step and transition, the network log with the real final submit request and its exact payload, console output, and the agent's reasoning wherever it made a judgment call. For multi-step forms the two documents to read are the screenshot timeline — because state bugs are transitions, and you want to see the field the moment before and after it reset — and the final submit request, which is the only place the "looked complete, submitted incomplete" bug is visible.

The prompts survive your redesigns because none of them names a step index, a field, or a selector — only what the wizard is for. Split a step in two, reorder them, add a branch: the intent didn't change, so the prompt didn't either. That's the same property that makes an agent-driven signup test outlast the form it tests, and it's why a prompt is a better long-term artifact than a spec for anything with a UI that moves.

The one to run right now

If you run a single check today, run the branch-switch-and-payload test — it's the cheapest way to catch the two nastiest multi-step bugs at once (leftover branch data and an incomplete final submit):

On https://staging.yourapp.com/onboarding, choose the account-type
option that reveals an EXTRA step, fill that step in, then go back and
switch to the option that should NOT have it. Complete the flow to
submit. Watch the network tab: confirm the final request contains every
field from every step the user actually completed, and does NOT contain
data from the branch they backed out of. Quote the exact payload and
flag any missing or leftover field. Capture screenshots and any console
errors.

Save it as a Test Scenario, pin it to your staging Project, and wire it into CI against every preview deploy that touches onboarding, checkout, or any other wizard. A full run is typically 8–13 credits — roughly $0.08–$0.13 — and your first run is free. Point it at staging, paste the prompt, and read the payload. If every field the user typed is in the request and nothing from the abandoned branch is, you've earned the one flow you can't afford to lose users in.