How to Test Onboarding Flow on Web (Complete Guide)
Onboarding is the first sustained interaction a user has with a product after they click an ad, open an email, or type a URL. If that experience fails, the user abandons before seeing any core value,
Why Onboarding Matters
Onboarding is the first sustained interaction a user has with a product after they click an ad, open an email, or type a URL. If that experience fails, the user abandons before seeing any core value, which directly hurts conversion rates, increases acquisition cost, and damages brand perception. A broken onboarding flow can also generate false‑negative metrics in analytics because users who drop off early are often mis‑classified as “low‑interest” rather than “blocked by a bug”. In regulated sectors (finance, health, education) a missing consent step or an inaccessible form can expose the company to compliance fines. Therefore, testing onboarding is not a nice‑to‑have; it is a risk‑mitigation activity that protects revenue, legal standing, and user trust.
Impact on conversion
A one‑second delay in the first page load can reduce conversions by up to 7 % (Google, 2023). When the delay is caused by a malfunctioning form validation script or a mis‑routed redirect, the loss compounds because users rarely retry. Tracking funnel drop‑off at each step (landing → consent → email entry → OTP → profile) reveals exactly where friction occurs, allowing teams to prioritize fixes that yield the biggest uplift.
Cost of broken onboarding
Fixing a defect after release costs roughly 30 × the cost of catching it in design (IBM Systems Sciences Institute). For onboarding, the cost is even higher because the defect affects every new user, not just a user, multiplying the loss over time. In a SaaS product with 10 k sign‑ups per month, a single 2 % drop‑off translates to 200 lost potential customers each month, or 2.4 k per year. When the average customer lifetime value is $500, that is $1.2 m of unrealized revenue annually.
Real‑world examples
- A fintech startup discovered that its “Sign up with Google” button threw a CSP error only in Safari 14, causing a 12 % drop‑off among iOS users.
- An e‑learning platform’s age‑gate modal trapped keyboard‑only users because the focus trap did not return focus after closing, violating WCAG 2.1 1.3.2 and leading to a complaint filed with the DOJ.
- A news site’s cookie banner blocked the email field when the user had disabled third‑party cookies, resulting in a silent validation failure that was never caught by unit tests because the banner was injected by a third‑party script.
These cases illustrate that onboarding bugs are often environment‑specific, involve third‑party code, or surface only under particular user conditions—precisely the scenarios that scripted tests miss unless they are deliberately broadened.
Core Components of a Web Onboarding Flow
Understanding the building blocks lets you map test cases to specific behaviors and avoid gaps. While each product varies, most web onboarding flows share the following modules.
Entry point (landing, ad, email)
The first touchpoint is usually a URL that may contain UTM parameters, referral tags, or promotional codes. The page must render correctly regardless of query‑string variations, and any pre‑filled fields (e.g., email from a mailto link) must be honored.
Consent & privacy notices
GDPR, CCPA, and similar regulations require explicit consent before storing personal data or tracking. This often appears as a modal or banner with “Accept”, “Reject”, and “Manage options” links. The notice must be accessible, dismissible without losing state, and must not block essential form fields.
Account creation (email/phone, social)
Users can create an account via traditional email/password, phone + OTP, or third‑party providers (Google, Apple, Facebook). Each path has distinct validation rules, error states, and redirect logic. Password policies (minimum length, required characters, strength meter) and phone‑number formatting (E.164, local delimiters) must be tested.
Email verification / OTP
After submitting credentials, the system typically sends a verification code. The flow must handle: code entry, expiry, resend limits, incorrect code attempts, and fallback to voice or SMS. Timing is critical—if the OTP expires before the user can retrieve it, the flow should offer a clear “Resend” option without losing previously entered data.
Profile setup
Many apps ask for additional information (name, avatar, preferences) before granting access to core features. This step may involve file uploads, cropping, or multi‑select dropdowns. Validation should enforce required vs optional fields, file type/size limits, and provide helpful inline errors.
Tutorial / product tour
A guided walkthrough highlights key UI elements. Implementation varies: modal tooltips, overlay highlights, or a separate “help” page. The tour must be skippable, resumeable, and must not trap focus or interfere with screen‑reader navigation.
First‑value moment
The point at which the user perceives tangible benefit (e.g., seeing a personalized dashboard, completing a first transaction, or receiving a welcome gift). Testing should confirm that the user reaches this state after completing all preceding steps and that any promised incentives are correctly applied.
Test Matrix for Onboarding
A structured matrix ensures coverage across happy paths, error conditions, accessibility, security, and edge cases. The table below lists representative test cases, their description, expected outcome, priority (P0 = blocking, P1 = high, P2 = medium), and suitability for automation (A = automatable, M = manual‑heavy, H = hybrid).
| ID | Description | Expected Result | Priority | Automation |
|---|---|---|---|---|
| ONB‑01 | Landing page loads with correct UTM params and pre‑fills email field | Page renders, email field contains value from query string | P0 | A |
| ONB‑02 | Consent modal appears on first visit; “Accept” stores consent cookie | Cookie set, modal disappears, no page reload | P0 | A |
| ONB‑03 | Consent modal “Reject” disables analytics scripts | No analytics calls fired after rejection | P1 | A |
| ONB‑04 | Email‑sign‑up form accepts valid email, rejects invalid formats | Valid email enables submit; invalid shows inline error | P0 | A |
| ONB‑05 | Password field enforces policy (8+ chars, 1 upper, 1 number) | Submit disabled until policy met; error messages update on each keystroke | P0 | A |
| ONB‑06 | Social login button (Google) launches OAuth popup, returns token | User redirected back to app with session cookie set | P0 | A |
| ONB‑07 | OTP entry: correct code verifies and proceeds to profile step | Profile step loads, OTP field cleared | P0 | A |
| ONB‑08 | OTP entry: three incorrect attempts trigger temporary lockout | Lockout message shown, resend disabled for 30 s | P1 | A |
| ONB‑09 | OTP resend link works after expiry, does not reset previously entered email | New code sent, email field unchanged | P1 | A |
| ONB‑10 | Profile upload: avatar accepts JPG/PNG ≤2 MB, rejects others | Accepted file previewed; rejected shows toast with size/type error | P1 | A |
| ONB‑11 | Profile form: required fields highlighted when left blank on submit | Inline error, focus moves to first empty field | P0 | A |
| ONB‑12 | Keyboard‑only navigation: Tab moves through all focusable elements in logical order | No focus traps, visible focus indicator | P0 | M (axe) |
| ONB‑13 | Screen‑reader announces consent modal title and description on focus | Proper ARIA labels, live region updates | P0 | M (axe) |
| ONB‑14 | Color contrast: all text meets WCAG AA (≥4.5:1) | Contrast ratios verified | P1 | M (axe) |
| ONB‑15 | CSP violation: inline script blocked in strict mode | No console error, fallback behavior works | P1 | A |
| ONB‑16 | Third‑party script latency: ad blocker removes analytics script | Core flow still completes, no JS errors | P2 | A |
| ONB‑17 | Network throttling (Slow 3G): form submission shows spinner, does not submit twice | Single request, spinner visible, retry disabled until response | P1 | A |
| ONB‑18 | Offline mode: submit button disabled, shows toast “No internet connection” | No request sent, user can retry after reconnection | P1 | A |
| ONB‑19 | Double‑click protection: rapid double submit sends only one request | Server receives single payload, no duplicate account | P1 | A |
| ONB‑20 | Locale switch (ar‑SA): layout mirrors, date inputs accept Arabic numerals | UI mirrors, validation works with Arabic digits | P2 | M |
| ONB‑21 | Age‑gate: user under 13 blocked, sees appropriate message | Access denied, no personal data stored | P0 | A |
| ONB‑22 | Data‑minimization: optional fields omitted from payload when left blank | Request body excludes null/undefined optional fields | P1 | A |
| ONB‑23 | Post‑onboarding redirect: user lands on intended landing‑after‑signup URL | URL matches config, no redirect loops | P0 | A |
| ONB‑24 | Welcome email/SMS sent successfully with correct personalization | Delivery verified via mock SMTP/SMS gateway | P1 | H |
| ONB‑25 | First‑value metric: dashboard shows personalized content after onboarding | Expected widget visible, no empty state | P0 | A |
*Notes:*
- Automation suitability assumes a typical E2E framework (Cypress/Playwright) with access to DOM, network, and axe‑core.
- Manual‑heavy items (M) involve sensory validation (contrast, screen‑reader) that are best verified with assistive‑technology tools or manual inspection.
- Hybrid (H) items may be partially automated (e.g., mocking SMTP) but require a manual check for deliverability or visual correctness.
Manual Testing Approach
Even with strong automation, a disciplined manual session uncovers issues that scripts assume away—such as subtle visual glitches, unexpected focus behavior, or device‑specific quirks. Below is a step‑by‑step guide that a QA engineer can follow for each release candidate.
Preparation (environment, data, personas)
- Create a clean test tenant – provision a fresh sub‑domain or feature‑flag environment to avoid cross‑test pollution.
- Define data sets – generate a CSV with valid/invalid emails, phone numbers, passwords that violate each policy rule, and a set of disposable test credit cards (if payment is part of onboarding).
- Select personas – adopt at least four distinct behavior profiles:
- *Curious*: reads every tooltip, explores help links.
- *Impatient*: skips optional steps, uses keyboard shortcuts, tolerates minimal feedback.
- *Novice*: relies on default values, makes frequent errors, needs clear guidance.
- *Adversarial*: attempts SQL injection, XSS payloads, oversized file uploads.
- Tool preparation – install axe‑core browser extension, enable Chrome DevTools throttling (Slow 3G, offline), and have a network‑sniffing tool (e.g., Wireshark or mitmproxy) ready to capture API calls.
Step‑by‑step script (happy path)
- Navigate to the onboarding URL with a clean incognito window.
- Verify that any UTM parameters are reflected in pre‑filled fields (if applicable).
- Confirm consent modal appears; click “Accept” and observe the consent cookie (
consent=true) in Application → Cookies. - Fill email field with a valid address from the data set; observe real‑time validation (if any).
- Enter a compliant password; note that the submit button becomes active only after all constraints satisfied.
- Submit the form; wait for OTP delivery modal.
- Retrieve the OTP from the test mailbox (e.g., Mailinator) and enter it.
- Confirm progression to the profile step; ensure the email field is now read‑only or hidden.
- Upload a valid avatar (JPEG ≤2 MB); check preview and file‑size toast.
- Complete optional fields (leave some blank) and submit.
- Validate that the user is redirected to the intended post‑onboarding page and that a session token is present.
- Look for the first‑value element (e.g., a welcome badge) and assert its visibility.
- Log out and repeat steps 1‑12 using a social login path to ensure parity.
Error path exploration
For each field, deliberately enter invalid data and verify:
- Inline error appears without page reload.
- Error message follows WCAG 3.3.1 (identifies the error and suggests a fix).
- Focus moves to the erroneous field after submission attempt.
- Submitting the form with errors does not trigger a network request.
Repeat for OTP (wrong code, expired code, rapid resend), file upload (exceed size, wrong MIME type), and social login (revoked token, cancelled OAuth flow).
Edge cases (network, timing, locale)
- Network throttling: enable Slow 3G, observe spinner duration, ensure no duplicate submissions.
- Offline: disable network, attempt submit; confirm button disabled and toast appears. Re‑enable network, verify retry works.
- Race condition: simulate double click (two mousedown events within 50 ms) using DevTools console; confirm server receives a single request.
- Locale switch: change
Accept-Languageheader toar-SA, ar;q=0.9; verify layout mirrors, date picker accepts Arabic-Indic digits, and error messages translate. - Timezone: set system timezone to UTC‑12; ensure any date‑based validation (e.g., birth‑date ≥13 years) works correctly.
Accessibility checks (WCAG)
Run axe‑core on each step and record violations:
- Landmarks – ensure
,,are present. - Labels – every input has an associated
oraria-label. - Contrast – text/background ratio ≥4.5:1 for AA, ≥7:1 for AAA where applicable.
- Keyboard – Tab order follows visual order; no modality traps.
- ARIA live regions – validation errors and toasts are announced.
- Focus visible – outline or custom focus style present on interactive elements.
Document each violation with a screenshot, the violated rule, and a suggested fix.
Security/privacy checks
- CSP: verify that
script-srcdisallowsunsafe-inlineand that any required inline scripts are moved to external files with nonces or hashes. - Data leakage: ensure that email, password, or OTP never appear in URL fragments or query strings after submission.
- Rate limiting: attempt OTP resend >5 times in 10 seconds; confirm server returns 429 with retry‑after header.
- Authentication: after OTP verification, inspect the session cookie for
HttpOnly,Secure, andSameSite=Strictattributes. - Privacy consent: toggle consent off and verify that analytics endpoints (
/collect,/track) are not called (use DevTools → Network → filter by domain). - Input sanitization: submit payloads containing
or SQL quotes; confirm they are escaped or rejected, and no reflected XSS appears in subsequent pages.
Documentation and bug reporting
For each defect, capture:
- Test case ID (from matrix).
- Browser/version, OS, device emulator (if mobile web).
- Steps to reproduce (including any throttling or locale settings).
- Expected vs actual behavior, with screenshots or video (max 30 s).
- Severity (based on impact on conversion, compliance, or security).
- Suggested fix or workaround.
Upload to the issue tracker with the label onboarding and link to the relevant test‑case document for traceability.
Automated Testing Approaches
Automation provides repeatable regression safety nets. The key is to layer different test types so that each catches defects at the appropriate fidelity level.
Unit / component testing (Jest, React Testing Library)
Isolate individual onboarding widgets (email input, password strength meter, OTP timer). Example: testing the password policy component.
// PasswordPolicy.test.js
import { render, screen } from '@testing-library/react';
import PasswordPolicy from './PasswordPolicy';
test('disables submit when password lacks uppercase', () => {
render(<PasswordPolicy onChange={jest.fn()} />);
const input = screen.getByLabelText(/password/i);
const button = screen.getByRole('button', { name: /submit/i });
// enter lowercase only
userEvent.type(input, 'lowercase123');
expect(button).toBeDisabled();
// add uppercase
userEvent.type(input, 'A');
expect(button).toBeEnabled();
});
Run with npm test -- --testPathPattern=onboarding. Aim for ≥90 % line coverage on onboarding‑related components.
End‑to‑end testing (Cypress, Playwright)
Simulate the full flow across browsers. Below is a Cypress script that covers the happy path and a few error paths.
// cypress/integration/onboarding_spec.js
describe('Onboarding flow', () => {
beforeEach(() => {
cy.visit('/signup?utm_source=newsletter&email=test%40example.com');
// clear cookies to simulate first visit
cy.clearCookies();
});
it('loads with prefilled email from UTM', () => {
cy.get('input[name=email]')
.should('have.value', 'test@example.com');
});
it('accepts consent and proceeds', () => {
cy.get('[data-testid=consent-accept]').click();
cy.getCookie('consent').should('have.property', 'value', 'true');
cy.get('button[type=submit]').should('not.be.disabled');
});
it('shows inline error for invalid email', () => {
cy.get('input[name=email]').type('bad-email{enter}');
cy.get('[data-testid=email-error]')
.should('contain', 'Enter a valid email address');
});
it('blocks submit after three wrong OTP attempts', () => {
// fill valid email/password
cy.get('input[name=email]').type('user@example.com');
cy.get('input[name=password]').type('ValidPass1!{enter}');
// OTP modal appears
cy.get('[data-testid=otp-input]').type('00000{enter}');
cy.get('[data-testid=otp-error]').should('contain', 'Invalid code');
// repeat twice more
cy.get('[data-testid=otp-input]').type('11111{enter}');
cy.get('[data-testid=otp-error]').should('contain', 'Invalid code');
cy.get('[data-testid=otp-input]').type('22222{enter}');
cy.get('[data-testid=otp-error]').should('contain', 'Invalid code');
// third attempt should lock
cy.get('[data-testid=otp-input]').type('33333{enter}');
cy.get('[data-testid=otp-error]')
.should('contain', 'Too many attempts. Try again in 30 seconds');
cy.get('[data-testid=resend-otp]').should('be.disabled');
});
});
Execute with npx cypress run --spec "cypress/integration/onboarding_spec.js" and integrate into CI.
Playwright offers similar capability with built‑in tracing:
# tests/test_onboarding.py
from playwright.sync_api import expect
def test_social_login(page):
page.goto("/signup")
page.click("text=Sign up with Google")
# Popup handling
with page.expect_popup() as popup_info:
pass
popup = popup_info.value
popup.fill("input[type=email]", "user@example.com")
popup.click("text=Next")
popup.fill("input[type=password]", "SecurePass!23")
popup.click("text=Sign in")
# Wait for redirect back
page.wait_for_url("**/welcome")
expect(page.get_by_role("heading", name="Welcome").to_be_visible()
Run with pytest -k onboarding.
Visual regression (Applitools, Percy)
Capture screenshots of key onboarding screens (landing, consent, OTP, profile) across Chrome, Firefox, Safari, and mobile viewports. Configure a baseline on the main branch; any pixel drift beyond a threshold flags a UI regression.
// Applitools Eyes Cypress example
import { Eyes, Target } from '@applitools/eyes-cypress';
describe('Visual onboarding', () => {
const eyes = new Eyes();
before(() => {
eyes.open(
cy,
'MyApp',
'Onboarding UI',
{ width: 1200, height: 800 }
);
});
after(() => eyes.close());
it('checks consent modal', () => {
cy.visit('/signup');
eyes.check('consent-modal', Target.window().fully());
});
it('checks OTP screen', () => {
cy.get('[data-testid=consent-accept]').click();
cy.get('input[name=email]').type('test@example.com{enter}');
cy.get('input[name=password]').type('ValidPass1!{enter}');
eyes.check('otp-screen', Target.window().fully());
});
});
API contract testing (Postman, Pact)
Validate that the backend endpoints called during onboarding adhere to the agreed schema (request/response shape, status codes, error codes). Example Pact test for the /api/v1/auth/otp/verify endpoint.
// pact/consumer/onboarding.pact.js
const { Pact } = require('@pact-foundation/pact');
const fetch = require('node-fetch');
const provider = new Pact({
consumer: 'WebOnboarding',
provider: 'AuthService',
port: 1234,
log: path.resolve(process.cwd(), 'logs', 'pact.log'),
dir: path.resolve(process.cwd(), 'pacts'),
});
describe('OTP verification', () => {
describe('when given a valid code', () => {
before(() =>
provider.addInteraction({
state: 'user has pending OTP',
uponReceiving: 'a verification request',
withRequest: {
method: 'POST',
path: '/api/v1/auth/otp/verify',
headers: { 'Content-Type': 'application/json' },
body: { email: 'test@example.com', code: '123456' },
},
willRespondWith: {
status: 200,
body: { token: Pact.Matchers.term('[a-z0-9]{32}', 'abcdef1234567890abcdef123456') },
},
})
);
it('returns a token', async () => {
const res = await fetch('http://localhost:1234/api/v1/auth/otp/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'test@example.com', code: '123456' }),
});
const json = await res.json();
expect(json).toHaveProperty('token');
expect(json.token).toMatch(/^[a-z0-9]{32}$/);
});
});
});
Run with npm run test:pact and publish to a Pact broker for provider verification.
Performance / load (k6, Lighthouse)
Use k6 to simulate many concurrent users hitting the onboarding endpoints and measure latency, error rates, and throughput. Combine with Lighthouse CI to ensure that performance budgets (FCP < 2 s, TTI < 3.5 s) are not regressed.
// k6/script.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
stages: [
{ duration: '2m', target: 20 }, // ramp‑up to 20 VUs
{ duration: '5m', target: 20 }, // stay at 20
{ duration: '2m', target: 0 }, // ramp‑down
],
};
export default function () {
const payload = JSON.stringify({
email: `__VU@example.com`,
password: `Pass${__VU}!`,
});
const params = { headers: { 'Content-Type': 'application/json' } };
const res = http.post('https://app.example.com/api/v1/auth/signup', payload, params);
check(res, {
'status is 200': (r) => r.status === 200,
'token present': (r) => r.json().token !== '',
});
sleep(1);
}
Run with k6 run script.js. Lighthouse CI can be added to the same pipeline: lhci autorun --upload.target=temporary-public-storage.
Tooling Specific to Web
Choosing the right stack reduces flakiness and speeds up feedback. Below is a comparison of popular open‑source tools for onboarding testing, followed by concrete snippets that illustrate typical usage.
| Tool | Primary Use | Language | Strengths | Weaknesses |
|---|---|---|---|---|
| Cypress | E2E, component, network stubbing | JavaScript/TypeScript | Fast test runner, built‑in waiting, easy debugging | Limited cross‑browser (Chrome‑family only) |
| Playwright | E2E, API, visual | JavaScript/TypeScript, Python, .NET, Java | Multi‑browser (Chromium, Firefox, WebKit), auto‑wait, tracing | Slightly heavier setup |
| Jest + RTL | Unit/component | JavaScript/TypeScript | Fast, excellent mocking, snapshot testing | No real browser, limited to DOM emulation |
| axe‑core | Accessibility | JavaScript | Comprehensive WCAG rules, integrates with Cypress/Playwright | Requires manual triage of false positives |
| Lighthouse CI | Performance, SEO, PWA | JavaScript | Lab‑based metrics, CI‑friendly | Not a substitute for real‑user monitoring |
| Applitools/Percy | Visual regression | Multiple | AI‑based diff, cross‑browser baselines | Commercial tier needed for full features |
| k6 | Load/performance | Go‑based script (JS) | Scenario‑based, cloud‑optional | Less suited for functional assertions |
| Pact | Contract testing | Multiple | Consumer‑driven, detects breaking changes early | Requires broker infrastructure |
Cypress example: handling iframes (common for social login widgets)
Social providers often render their OAuth UI inside an . Cypress can’t directly interact with cross‑origin iframes, but you can stub the network call or use cy.origin() (available from Cypress 12).
// cypress/support/commands.js
Cypress.Commands.add('fillSocialLogin', (provider, email, password) => {
cy.origin(`https://accounts.${provider}.com`, () => {
cy.get('input[type=email]').type(email);
cy.get('input[id=password]').type(password);
cy.get('button#signIn').click();
});
});
// test
it('logs in with Google', () => {
cy.visit('/signup');
cy.get('[data-testid=google-signin]').click();
cy.fillSocialLogin('google', 'user@example.com', 'SecurePass!23');
cy.url().should('include', '/welcome');
});
Playwright example: network throttling and offline simulation
Playwright provides built‑in contexts to emulate network conditions and to go offline.
from playwright.sync_api import expect, Page
def test_offline_submission(page: Page):
# emulate slow 3G
page.context.set_offline(False)
page.context.set_network_conditions(
download=500/1000, # 0.5 Mbps
upload=250/1000,
latency=40 # ms
)
page.goto('/signup')
page.fill('input[name=email]', 'user@example.com')
page.fill('input[name=password]', 'Passw0rd!')
page.click('button[type=submit]')
# OTP step
page.fill('input[name=otp]', '123456')
page.click('button#verify')
# go offline before submit
page.context.set_offline(True)
page.click('button#submit-profile')
expect(page.locator('text=No internet connection')).to_be_visible()
# restore online and retry
page.context.set_offline(False)
page.click('button#retry')
expect(page.locator('text=Welcome')).to_be_visible()
Jest + RTL example: testing accessibility of a custom modal
Using jest-axe to assert that a rendered component passes a basic axe scan.
import { render, screen } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('Consent modal has no axe violations', async () => {
render(<ConsentModal onAccept={() => {}} onReject={() => {}} />);
const results = await axe(screen.container);
expect(results).toHaveNoViolations();
});
Run with npm test -- --testNamePattern="Consent modal" and ensure the CI step fails on any new violation.
Lighthouse CI configuration (.lighthouserc.json)
A minimal config that runs on each PR and enforces a performance budget.
{
"ci": {
"collect": {
"url": ["https://staging.example.com/signup?utm_source=test"],
"
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free