Multi-Step Form Monitoring: Test Complex Form Flows
Monitor multi-step forms, wizards, and onboarding flows with Playwright. Catch validation errors, step navigation issues, and submission failures before users encounter them.
Multi-step forms—whether for user onboarding, applications, or surveys—are complex pieces of UI with many failure points. This guide shows you how to monitor them effectively with supaguard.
Why Multi-Step Forms Break
Multi-step forms have unique failure modes:
| Issue | Impact |
|---|---|
| Step navigation broken | Users stuck, can't proceed |
| Validation errors unclear | Users can't fix issues |
| Data not persisted between steps | Users lose progress |
| Back button breaks | Users can't correct mistakes |
| Final submission fails | Entire form effort wasted |
Basic Multi-Step Form Test
Here's a complete test for a typical multi-step form:
import { test, expect } from "@playwright/test";
test("complete onboarding wizard", async ({ page }) => {
await page.goto("https://app.example.com/onboarding");
// Step 1: Personal Information
await expect(page.getByText("Step 1 of 4")).toBeVisible();
await page.getByLabel("First Name").fill("Test");
await page.getByLabel("Last Name").fill("User");
await page.getByLabel("Email").fill("test@example.com");
await page.getByRole("button", { name: "Continue" }).click();
// Step 2: Company Information
await expect(page.getByText("Step 2 of 4")).toBeVisible();
await page.getByLabel("Company Name").fill("Test Company");
await page.getByLabel("Company Size").selectOption("10-50");
await page.getByLabel("Industry").selectOption("Technology");
await page.getByRole("button", { name: "Continue" }).click();
// Step 3: Preferences
await expect(page.getByText("Step 3 of 4")).toBeVisible();
await page.getByLabel("Notifications").check();
await page.getByLabel("Weekly digest").check();
await page.getByRole("button", { name: "Continue" }).click();
// Step 4: Review and Submit
await expect(page.getByText("Step 4 of 4")).toBeVisible();
await expect(page.getByText("Test User")).toBeVisible();
await expect(page.getByText("Test Company")).toBeVisible();
await page.getByRole("button", { name: "Complete Setup" }).click();
// Verify completion
await expect(page.getByText("Welcome to")).toBeVisible();
await expect(page).toHaveURL(/.*dashboard/);
});Testing Step Navigation
Forward Navigation
test("can navigate forward through steps", async ({ page }) => {
await page.goto("/onboarding");
// Step 1 → Step 2
await page.getByLabel("First Name").fill("Test");
await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByTestId("step-indicator")).toHaveText("2 / 4");
// Step 2 → Step 3
await page.getByLabel("Company").fill("Test Co");
await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByTestId("step-indicator")).toHaveText("3 / 4");
});Back Button
test("can navigate back without losing data", async ({ page }) => {
await page.goto("/onboarding");
// Complete step 1
await page.getByLabel("First Name").fill("Test");
await page.getByLabel("Last Name").fill("User");
await page.getByRole("button", { name: "Next" }).click();
// Go to step 2
await expect(page.getByText("Step 2")).toBeVisible();
await page.getByLabel("Company").fill("Test Company");
// Go back
await page.getByRole("button", { name: "Back" }).click();
// Verify step 1 data is preserved
await expect(page.getByLabel("First Name")).toHaveValue("Test");
await expect(page.getByLabel("Last Name")).toHaveValue("User");
});Browser Back Button
test("browser back button works correctly", async ({ page }) => {
await page.goto("/onboarding");
// Complete step 1
await page.getByLabel("First Name").fill("Test");
await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByText("Step 2")).toBeVisible();
// Use browser back
await page.goBack();
// Should be on step 1 with data preserved
await expect(page.getByText("Step 1")).toBeVisible();
await expect(page.getByLabel("First Name")).toHaveValue("Test");
});Testing Validation
Required Field Validation
test("shows validation errors for required fields", async ({ page }) => {
await page.goto("/onboarding");
// Try to proceed without filling required fields
await page.getByRole("button", { name: "Next" }).click();
// Should show validation errors
await expect(page.getByText("First name is required")).toBeVisible();
await expect(page.getByText("Email is required")).toBeVisible();
// Should still be on step 1
await expect(page.getByText("Step 1")).toBeVisible();
});Field Format Validation
test("validates email format", async ({ page }) => {
await page.goto("/onboarding");
await page.getByLabel("Email").fill("invalid-email");
await page.getByRole("button", { name: "Next" }).click();
await expect(page.getByText("Please enter a valid email")).toBeVisible();
// Fix the error
await page.getByLabel("Email").clear();
await page.getByLabel("Email").fill("valid@example.com");
await page.getByRole("button", { name: "Next" }).click();
// Should proceed to next step
await expect(page.getByText("Step 2")).toBeVisible();
});Cross-Field Validation
test("validates password confirmation matches", async ({ page }) => {
await page.goto("/signup");
await page.getByLabel("Password").fill("MyPassword123!");
await page.getByLabel("Confirm Password").fill("DifferentPassword!");
await page.getByRole("button", { name: "Continue" }).click();
await expect(page.getByText("Passwords do not match")).toBeVisible();
});Testing Data Persistence
Session Persistence
test("form data persists after page reload", async ({ page }) => {
await page.goto("/onboarding");
// Fill step 1
await page.getByLabel("First Name").fill("Test");
await page.getByLabel("Last Name").fill("User");
// Reload page
await page.reload();
// Data should still be there (if your form saves to session/localStorage)
await expect(page.getByLabel("First Name")).toHaveValue("Test");
await expect(page.getByLabel("Last Name")).toHaveValue("User");
});Progress Indicator
test("progress indicator reflects current step", async ({ page }) => {
await page.goto("/onboarding");
// Step 1: indicator shows step 1 active
await expect(page.getByTestId("step-1-indicator")).toHaveClass(/active/);
await expect(page.getByTestId("step-2-indicator")).not.toHaveClass(/active/);
// Proceed to step 2
await page.getByLabel("First Name").fill("Test");
await page.getByRole("button", { name: "Next" }).click();
// Indicator should update
await expect(page.getByTestId("step-1-indicator")).toHaveClass(/completed/);
await expect(page.getByTestId("step-2-indicator")).toHaveClass(/active/);
});Handling Dynamic Forms
Conditional Steps
Some forms show/hide steps based on previous answers:
test("shows correct steps based on selection", async ({ page }) => {
await page.goto("/application");
// Step 1: Select type
await page.getByLabel("I am a").selectOption("business");
await page.getByRole("button", { name: "Next" }).click();
// Business users should see company info step
await expect(page.getByText("Company Information")).toBeVisible();
// Go back and change selection
await page.getByRole("button", { name: "Back" }).click();
await page.getByLabel("I am a").selectOption("individual");
await page.getByRole("button", { name: "Next" }).click();
// Individuals should NOT see company info
await expect(page.getByText("Company Information")).not.toBeVisible();
await expect(page.getByText("Personal Details")).toBeVisible();
});Dynamic Field Addition
test("can add multiple items", async ({ page }) => {
await page.goto("/form");
// Add first item
await page.getByLabel("Item 1 Name").fill("First Item");
// Click to add another
await page.getByRole("button", { name: "Add another item" }).click();
// New field should appear
await expect(page.getByLabel("Item 2 Name")).toBeVisible();
await page.getByLabel("Item 2 Name").fill("Second Item");
// Verify both are submitted
await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByText("2 items submitted")).toBeVisible();
});Testing File Uploads
test("can upload documents in form", async ({ page }) => {
await page.goto("/application");
// Navigate to document upload step
// ... previous steps ...
// Upload file
const fileInput = page.getByLabel("Upload ID");
await fileInput.setInputFiles("./test-files/sample-id.pdf");
// Verify upload success
await expect(page.getByText("sample-id.pdf")).toBeVisible();
await expect(page.getByText("Upload complete")).toBeVisible();
// Proceed
await page.getByRole("button", { name: "Continue" }).click();
});Error Recovery
Network Error During Submission
test("handles submission error gracefully", async ({ page }) => {
await page.goto("/onboarding");
// Complete all steps...
// Intercept the final submission to simulate error
await page.route("**/api/onboarding", (route) =>
route.fulfill({ status: 500, body: "Server error" })
);
await page.getByRole("button", { name: "Complete Setup" }).click();
// Should show error message
await expect(page.getByText(/something went wrong|try again/i)).toBeVisible();
// Data should still be in form
await expect(page.getByRole("button", { name: "Complete Setup" })).toBeEnabled();
});Monitoring Configuration
Check Frequency
| Form Type | Frequency |
|---|---|
| User signup/onboarding | Every 5 minutes |
| Lead generation forms | Every 10 minutes |
| Application forms | Every 15 minutes |
| Surveys | Every 30 minutes |
Alert Priority
Form failures should generally be Critical if they:
- Block user registration
- Prevent purchases
- Stop lead capture
Set up appropriate alert policies based on business impact.
Common Issues
Animation Timing
// Wait for step transition animation to complete
await page.getByRole("button", { name: "Next" }).click();
await page.waitForTimeout(300); // If transition takes 300ms
await expect(page.getByText("Step 2")).toBeVisible();Loading States
// Wait for async validation to complete
await page.getByLabel("Email").fill("test@example.com");
await page.getByLabel("Email").blur(); // Trigger validation
await page.waitForSelector('[data-testid="email-valid"]');
await page.getByRole("button", { name: "Next" }).click();Autofill Conflicts
// Clear autofill before typing
await page.getByLabel("Email").clear();
await page.getByLabel("Email").fill("test@example.com");Related Resources
- E-Commerce Checkout — Payment form testing
- SaaS Login — Authentication flows
- Playwright Guide — Complete Playwright reference
Monitor SaaS Onboarding Flows to Improve Activation
Track onboarding reliability with synthetic monitoring to catch signup, verification, and first-project failures before they hurt activation metrics.
Monitor SaaS Login Flows: Protecting the Gateway to Your App
Authentication is the high-stakes foundation of any SaaS product. Learn to monitor email/password, OAuth (Google/GitHub), and MFA to ensure every user can access your platform 24/7.