top of page

Tutorial 6: Write Your First E2E Test

  • Contributor
  • Apr 18
  • 3 min read

E2E tests exercise your system from the user's perspective — clicking through a browser, hitting real services. This tutorial walks through writing one with Playwright.

What You'll Build

A Playwright test that signs in a user and verifies they reach the dashboard.

Step 1: Install Playwright (5 min)

npm init playwright@latest

Answer the prompts:

  • Test directory: tests/e2e

  • GitHub Actions: yes

  • Install browsers: yes

This sets up Playwright with sensible defaults.

Step 2: Understand the Pieces (5 min)

tests/e2e/
  example.spec.ts    # Sample test (delete after reading)
playwright.config.ts # Configuration

The config matters. Default settings work for most projects.

Step 3: Pick a User Journey (5 min)

The critical sign-in flow is a good first test:

  1. User opens the app

  2. Clicks "Sign in"

  3. Enters credentials

  4. Submits

  5. Sees the dashboard

Make sure you have a test user account that the test can use.

Step 4: Write the Test (15 min)

Create tests/e2e/sign-in.spec.ts:

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

test('user can sign in and see dashboard', async ({ page }) => {
  // Arrange — navigate to the sign-in page
  await page.goto('http://localhost:3000/signin');
  
  // Act — fill the form and submit
  await page.fill('[data-testid="email"]', 'test+e2e@example.com');
  await page.fill('[data-testid="password"]', 'testpassword');
  await page.click('[data-testid="signin-submit"]');
  
  // Assert — wait for redirect and verify dashboard
  await expect(page).toHaveURL(/.*\/dashboard/);
  await expect(page.locator('h1')).toContainText('Welcome');
});

Notice: data-testid selectors. Stable across UI changes.

Step 5: Add Test IDs to Your UI (15 min)

If your UI doesn't have data-testid attributes yet, add them now:

<input
  type="email"
  data-testid="email"
  onChange={handleEmail}
/>
<input
  type="password"
  data-testid="password"
  onChange={handlePassword}
/>
<button
  data-testid="signin-submit"
  type="submit"
>
  Sign in
</button>

Test IDs are stable. CSS classes change with refactors; test IDs are intentional.

Step 6: Set Up a Test User (10 min)

Tests need a user that exists. Options:

  • Hardcoded test user in dev/staging environments (works for first tests)

  • Created via API in test setup (cleaner, scales better)

  • Per-test creation and cleanup (best isolation)

For your first test, the hardcoded approach is fine:

// In setup
const TEST_USER = {
  email: 'test+e2e@example.com',
  password: 'TestPassword123!',
};

Make sure this user exists in your dev/test environment.

Step 7: Run It (5 min)

npx playwright test

First run: opens a browser, runs your test, reports pass/fail.

For visual debugging:

npx playwright test --headed

Watch the browser as the test runs.

Step 8: Use Playwright Inspector (10 min)

When tests fail or behave unexpectedly:

npx playwright test --debug

Steps through the test, lets you inspect at each point. The best debugging tool.

Step 9: Add One More Scenario (10 min)

Bad credentials:

test('shows error for wrong password', async ({ page }) => {
  await page.goto('http://localhost:3000/signin');
  await page.fill('[data-testid="email"]', 'test+e2e@example.com');
  await page.fill('[data-testid="password"]', 'wrong-password');
  await page.click('[data-testid="signin-submit"]');
  
  // Should see error, not redirect
  await expect(page.locator('[data-testid="error-message"]'))
    .toContainText('Invalid credentials');
  await expect(page).toHaveURL(/.*\/signin/);
});

Two scenarios per critical flow is a reasonable starting point.

Step 10: Wire to CI (10 min)

Playwright's GitHub Actions integration was set up by the installer. Verify it runs:

git add tests/e2e .github/workflows/playwright.yml
git commit -m "Add E2E tests"
git push

E2E runs on PRs. Failures block merge if configured.

What You Just Did

You have a working E2E test exercising the full stack. It catches integration issues no unit test would.

E2E tests are expensive. Keep the suite small (5-15 critical journeys) and focused on real user value.

Common Failure Modes

Flaky tests. Real-world timing variability. Use Playwright's built-in waits, not fixed sleeps.

Brittle selectors. Tests break on every UI change. Use test IDs.

Heavy test data. Hard to set up; flaky. Minimize.

Testing everything via E2E. Slow, hard to maintain. Reserve for critical journeys.

Test running against production. Pollutes real data. Always use dev/test environment.

Next Tutorial

Most of your service is the API. Test it: Tutorial 7: Test an API End-to-End.

Related reading

Keep learning. This article is part of the Test Automation path in the ShiftQuality Learning Center. Build test automation that lasts, with ROI you can defend.

bottom of page