Skip to content

Part 5.5 — End-to-End Testing with Playwright: Real User Flows

Site Console Site Console
4 min read Updated Jan 13, 2026 Frontend Design 0 comments
End-to-End Testing React Apps with Playwright

Component and integration tests tell you pieces work.
End-to-end tests tell you the system works.

Playwright runs a real browser, loads your React app, talks to your NestJS REST API, and exercises flows exactly as users do. When an E2E test passes, you know your frontend, backend, database, auth, and routing are aligned.

This post shows how to write high-value E2E tests that protect real business flows—without turning your test suite into a maintenance nightmare.


1. What E2E Tests Are (and Are Not)

E2E tests are for:

  • critical user journeys

  • auth and permissions

  • multi-page flows

  • regressions that escaped before

They are not for:

  • edge-case UI states

  • exhaustive form validation

  • pixel-perfect assertions

  • replacing unit tests

Keep E2E tests few, focused, and meaningful.


2. Install and Initialize Playwright

From your React project:

pnpm add -D @playwright/test
pnpm exec playwright install

Initialize config:

pnpm exec playwright init

This creates:

playwright.config.ts
tests/

3. Configure Playwright for Your Stack

Edit playwright.config.ts:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,
  use: {
    baseURL: 'http://localhost:5173',
    headless: true,
  },
  webServer: {
    command: 'pnpm dev',
    port: 5173,
    reuseExistingServer: !process.env.CI,
  },
});

This ensures:

  • React app is running

  • Playwright waits for it

  • CI behaves like local

Your backend should already be running (or started in CI).


4. Test the App Loads

Create tests/smoke.spec.ts:

import { test, expect } from '@playwright/test';

test('homepage loads', async ({ page }) => {
  await page.goto('/');
  await expect(page.getByText(/welcome/i)).toBeVisible();
});

This simple test catches broken builds instantly.


5. Testing a Real Auth Flow

Assume:

  • /login page

  • email + password

  • JWT stored in memory or localStorage

test('user can log in', async ({ page }) => {
  await page.goto('/login');

  await page.fill('input[name="email"]', 'alice@example.com');
  await page.fill('input[name="password"]', 'password');

  await page.click('button[type="submit"]');

  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByText(/welcome, alice/i)).toBeVisible();
});

This test validates:

  • routing

  • form handling

  • backend auth

  • token handling

  • protected route access

One test. Huge coverage.


6. Creating Data via the UI (CRUD Flow)

test('user creates a post', async ({ page }) => {
  await page.goto('/posts/new');

  await page.fill('input[name="title"]', 'My First Post');
  await page.fill('textarea[name="body"]', 'Hello world');

  await page.click('button[type="submit"]');

  await expect(page).toHaveURL('/posts');
  await expect(page.getByText('My First Post')).toBeVisible();
});

This covers:

  • form submission

  • API POST

  • DB write

  • redirect

  • list refresh

Exactly how users experience it.


7. Avoid UI Coupling with Roles & Labels

Prefer:

page.getByRole('button', { name: /submit/i });

Over:

page.locator('.btn-primary');

This makes tests:

  • resilient to CSS changes

  • aligned with accessibility

  • readable

Accessibility-friendly apps are easier to test.


8. Handling Auth Setup Faster (Optional)

Logging in via UI every test is slow.

Use a setup helper:

test.beforeEach(async ({ page }) => {
  await page.request.post('/api/test/login', {
    data: { email: 'alice@example.com' },
  });
});

This requires a test-only backend endpoint guarded by NODE_ENV === 'test'.

Trade-off:

  • ⚡ faster tests

  • ❌ less realistic

Use sparingly.


9. Isolate Test Data

Your E2E tests should use:

  • a dedicated test database

  • reset between runs

  • known seed data

Never run E2E tests against production or shared staging DBs.


10. Test Authorization Boundaries

Example:

test('non-owner cannot delete post', async ({ page }) => {
  await page.goto('/posts/1');

  await expect(
    page.getByRole('button', { name: /delete/i }),
  ).toBeHidden();
});

This locks in permission logic at the UI level.


11. Capture Screenshots on Failure

Playwright does this automatically.

In CI, this gives you:

  • screenshots

  • videos

  • traces

Which drastically reduces debugging time.


12. Keep E2E Tests Deterministic

Rules:

  • avoid random data unless seeded

  • avoid time-based assertions

  • avoid real emails / payments

  • avoid flaky selectors

If a test flakes once, fix it immediately.


13. How Many E2E Tests Is Enough?

A good baseline:

  • 1 smoke test

  • 1 auth flow

  • 1 CRUD flow per core entity

  • 1 permission boundary test

That’s usually under 15 tests—and extremely effective.


14. Summary

You now know how to:

  • set up Playwright for React apps

  • write real browser-based tests

  • validate auth, CRUD, and navigation

  • keep E2E tests focused and valuable

  • avoid common flakiness traps

In Part 5.6, you’ll wire frontend tests into CI, manage flakiness, and keep your test suite healthy as your app grows.

Related

Leave a comment

Sign in to leave a comment.

Comments