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:
User opens the app
Clicks "Sign in"
Enters credentials
Submits
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.


