npm init playwright@latest
// playwright.config.ts
import { defineConfig } from
'@playwright/test';
export default defineConfig({
testDir: './e2e',
timeout: 30000,
retries: 2,
use: {
baseURL: 'http://localhost:3000',
screenshot: 'only-on-failure',
trace: 'on-first-retry',
},
webServer: {
command: 'npm run dev',
port: 3000,
reuseExistingServer:
!process.env.CI,
},
projects: [
{ name: 'chromium',
use: { browserName: 'chromium' } },
{ name: 'firefox',
use: { browserName: 'firefox' } },
{ name: 'webkit',
use: { browserName: 'webkit' } },
],
});
// e2e/fixtures.ts
import { test as base } from
'@playwright/test';
const TEST_USER = {
email: 'e2e@test.com',
password: 'TestPass123',
name: 'E2E User',
};
export const test = base.extend({
// Auto-register & login before test
authenticatedPage: async (
{ page }, use
) => {
// Register (ignore if exists)
await page.request.post(
'/api/v1/auth/register',
{ data: TEST_USER });
// Login
const res = await page.request.post(
'/api/v1/auth/login',
{ data: {
email: TEST_USER.email,
password: TEST_USER.password,
}});
const { token } = await res.json();
// Set auth cookie/header
await page.addInitScript(
(t) => {
localStorage.setItem('token', t);
}, token);
await page.goto('/');
await use(page);
},
});
// e2e/upload-flow.spec.ts
import { test } from './fixtures';
import { expect } from '@playwright/test';
import path from 'path';
test('upload → filter → export',
async ({ authenticatedPage: page }) => {
// Step 1: Upload image
const fileInput =
page.locator('input[type="file"]');
await fileInput.setInputFiles(
path.join(__dirname,
'fixtures/test-image.jpg'));
// Wait for upload to complete
await expect(
page.locator('.editor-canvas'))
.toBeVisible({ timeout: 10000 });
// Step 2: Apply sepia filter
await page.click(
'[data-filter="sepia"]');
await expect(
page.locator('[data-filter="sepia"]'))
.toHaveClass(/active/);
// Step 3: Export
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('button:has-text("Export")'),
]);
// Step 4: Verify download
const filename = download.suggestedFilename();
expect(filename).toMatch(/\.webp$/);
const filePath =
await download.path();
expect(filePath).toBeTruthy();
});
// e2e/auth.spec.ts
import { test, expect } from
'@playwright/test';
test('register → login → see dashboard',
async ({ page }) => {
const unique = Date.now();
// Register
await page.goto('/register');
await page.fill('[name="name"]',
'New User');
await page.fill('[name="email"]',
`user${unique}@test.com`);
await page.fill('[name="password"]',
'SecurePass1');
await page.click(
'button:has-text("Register")');
// Should redirect to dashboard
await expect(page)
.toHaveURL('/dashboard');
await expect(
page.locator('h1'))
.toContainText('Welcome');
});
test('shows validation errors',
async ({ page }) => {
await page.goto('/register');
await page.fill('[name="email"]',
'bad-email');
await page.fill('[name="password"]',
'123');
await page.click(
'button:has-text("Register")');
await expect(
page.locator('.error'))
.toContainText('Invalid email');
});
// Visual regression test
test('editor looks correct',
async ({ authenticatedPage: page }) => {
await page.goto('/editor');
await expect(page).toHaveScreenshot(
'editor-default.png',
{ maxDiffPixelRatio: 0.01 });
});
// .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx playwright install
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
git switch -c feature/PIXELCRAFT-074-e2e-tests
git add e2e/ playwright.config.ts .github/
git commit -m "Add Playwright E2E tests + CI (PIXELCRAFT-074)"
git push origin feature/PIXELCRAFT-074-e2e-tests
# PR → Review → Merge → Close ticket ✅
E2E tests answer the only question that matters: can the user do their job?
Unit tests verify code. Integration tests verify systems. E2E tests verify the product.