0Pricing
React Academy · Lesson

Testing Forms & User Flows End-to-End

Automate multi-step form submissions and navigation flows with Playwright page objects.

Testing Forms & User Flows End-to-End is a free React Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the React Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

End-to-End Form Testing Strategy

E2E form tests simulate the full user journey: land on the page, fill fields, submit, handle validation errors, and verify the success outcome.

Page Object Model

The Page Object Model (POM) encapsulates selectors and interactions for a page into a class, making tests readable and reducing duplication.

class LoginPage {
  constructor(private page: Page) {}

  async goto() { await this.page.goto('/login'); }
  async fillEmail(email: string) { await this.page.getByLabel('Email').fill(email); }
  async fillPassword(pw: string) { await this.page.getByLabel('Password').fill(pw); }
  async submit() { await this.page.getByRole('button', { name: /log in/i }).click(); }
  async getError() { return this.page.getByRole('alert').textContent(); }
}

Using the Page Object in Tests

Tests become intent-level descriptions of user actions when using page objects — easy to read and maintain.

test('successful login redirects to dashboard', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.fillEmail('alice@example.com');
  await loginPage.fillPassword('password123');
  await loginPage.submit();
  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

Testing Validation Errors

Submit with invalid or empty data and assert on the error messages the UI displays.

test('shows validation errors on empty submit', async ({ page }) => {
  await page.goto('/register');
  await page.getByRole('button', { name: 'Create account' }).click();
  await expect(page.getByText('Email is required')).toBeVisible();
  await expect(page.getByText('Password must be at least 8 characters')).toBeVisible();
});

Multi-Step Form Testing

For multi-step wizards, progress through each step, asserting the form advances and retains previous input.

test('completes 3-step signup', async ({ page }) => {
  await page.goto('/signup');
  // Step 1
  await page.getByLabel('First Name').fill('Alice');
  await page.getByRole('button', { name: 'Next' }).click();
  // Step 2
  await page.getByLabel('Plan').selectOption('pro');
  await page.getByRole('button', { name: 'Next' }).click();
  // Step 3
  await page.getByLabel('Card Number').fill('4242424242424242');
  await page.getByRole('button', { name: 'Complete' }).click();
  await expect(page.getByText('Welcome, Alice!')).toBeVisible();
});

Mocking API in Playwright

Use page.route() to intercept API calls and return mock responses — useful for testing edge cases without a real backend.

await page.route('/api/register', async route => {
  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify({ token: 'fake-token' }),
  });
});

await page.goto('/register');
// ...fill and submit form
await expect(page).toHaveURL('/dashboard');

Testing File Uploads

Use setInputFiles() to simulate a file upload in Playwright.

test('uploads a profile photo', async ({ page }) => {
  await page.goto('/profile');
  const fileInput = page.getByLabel('Profile photo');
  await fileInput.setInputFiles('./fixtures/avatar.png');
  await page.getByRole('button', { name: 'Upload' }).click();
  await expect(page.getByAltText('Profile photo')).toBeVisible();
});

Auth State Reuse

Log in once, save browser storage state, and reuse it across tests to skip repeated login flows.

// auth.setup.ts
test('authenticate', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('alice@example.com');
  await page.getByLabel('Password').fill('password');
  await page.getByRole('button', { name: 'Log in' }).click();
  await page.context().storageState({ path: 'e2e/.auth/user.json' });
});

// playwright.config.ts — use storageState in other tests
use: { storageState: 'e2e/.auth/user.json' }

Testing Error Recovery

Simulate network failures and verify the user can retry or sees a helpful error message.

await page.route('/api/submit', route => route.abort());
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Network error. Please try again.')).toBeVisible();
await page.unroute('/api/submit');
// Now the real request goes through
await page.getByRole('button', { name: 'Retry' }).click();

Keyboard Navigation Testing

Test that forms are fully keyboard accessible by tabbing through fields and submitting with Enter.

test('submits form with keyboard only', async ({ page }) => {
  await page.goto('/contact');
  await page.keyboard.press('Tab'); // focus name
  await page.keyboard.type('Alice');
  await page.keyboard.press('Tab'); // focus email
  await page.keyboard.type('alice@example.com');
  await page.keyboard.press('Tab'); // focus submit
  await page.keyboard.press('Enter');
  await expect(page.getByText('Message sent!')).toBeVisible();
});

Fixtures for Test Data

Use Playwright fixtures to share setup logic (like creating a test user) across multiple tests cleanly.

// fixtures.ts
const test = base.extend<{ testUser: User }>({ 
  testUser: async ({}, use) => {
    const user = await createTestUser();
    await use(user);
    await deleteTestUser(user.id);
  },
});

// In a test file:
test('edits profile', async ({ page, testUser }) => {
  await page.goto(`/users/${testUser.id}/edit`);
});

Parallel Test Execution

Playwright runs tests in parallel by default across workers. Use test.describe.serial only for tests that must run in order.

Quick Check

What is the main benefit of the Page Object Model (POM) in E2E testing?

Recap

E2E form tests use Page Objects for maintainability, page.route() for API mocking, setInputFiles() for uploads, and storage state to reuse auth sessions. Test validation, multi-step flows, and keyboard navigation to cover real user journeys.

Frequently asked questions

Is the “Testing Forms & User Flows End-to-End” lesson free?

Yes — the full text of “Testing Forms & User Flows End-to-End” is free to read here on the web, and the React Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the React Academy course, upgrade to CoddyKit PRO.

What will I learn in “Testing Forms & User Flows End-to-End”?

Automate multi-step form submissions and navigation flows with Playwright page objects. You practise React Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start React Academy?

No prior experience is required. React Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Testing Forms & User Flows End-to-End” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this React Academy lesson?

Yes. Every React Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Integration Testing with React Testing Library
  2. Testing Async UI & API Calls with MSW
  3. Getting Started with Playwright for React
  4. Testing Forms & User Flows End-to-End
← Back to React Academy