End-to-End Testing with Playwright

December 14, 2025 ยท View on GitHub

Hemmelig uses Playwright for end-to-end integration testing.

Setup

Playwright and browsers are installed as dev dependencies. If you need to install browser dependencies on your system:

sudo npx playwright install-deps

Test Database

The e2e tests automatically use a separate test database (database/hemmelig-test.db) that is:

  1. Created fresh before each test run
  2. Migrated with the latest schema
  3. Seeded with a test user
  4. Deleted after tests complete

This ensures tests don't affect your development database.

Test User Credentials (created automatically):

  • Email: e2e-test@hemmelig.local
  • Username: e2etestuser
  • Password: E2ETestPassword123!

Running Tests

# Run all e2e tests
npm run test:e2e

# Run tests with interactive UI
npm run test:e2e:ui

# Run tests in debug mode
npm run test:e2e:debug

# Run a specific test file
npx playwright test tests/e2e/secret.spec.ts

# Run tests in headed mode (see the browser)
npx playwright test --headed

Test Structure

Tests are located in tests/e2e/:

FileDescription
auth.spec.tsAuthentication tests (setup, login, registration)
home.spec.tsHomepage and secret form tests
secret.spec.tsSecret creation, viewing, and deletion flows
navigation.spec.tsNavigation and routing tests
fixtures.tsShared test fixtures (authenticatedPage)
global-setup.tsCreates test database and user before tests
global-teardown.tsCleans up test database after tests

Configuration

The Playwright configuration is in playwright.config.ts:

  • Test directory: tests/e2e/
  • Base URL: http://localhost:5173
  • Browser: Chromium (Desktop Chrome)
  • Web server: Automatically starts Vite with test database
  • Global setup: Creates fresh test database and test user
  • Global teardown: Deletes test database

Writing Tests

Basic Test Structure

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

test.describe('Feature Name', () => {
    test('should do something', async ({ page }) => {
        await page.goto('/');

        // Interact with elements
        await page.locator('.ProseMirror').fill('My secret');
        await page.getByRole('button', { name: /create/i }).click();

        // Assert results
        await expect(page.getByText(/success/i)).toBeVisible();
    });
});

Using Authenticated Page Fixture

For tests that require authentication:

import { expect, test } from './fixtures';

test('should create a secret when logged in', async ({ authenticatedPage }) => {
    await authenticatedPage.goto('/');
    // authenticatedPage is already logged in with test user
    await expect(authenticatedPage.locator('.ProseMirror')).toBeVisible();
});

Common Patterns

Interacting with the secret editor:

const editor = page.locator('.ProseMirror');
await editor.click();
await editor.fill('Secret content');

Creating and viewing a secret:

// Create
await page.goto('/');
await page.locator('.ProseMirror').fill('My secret');
await page
    .getByRole('button', { name: /create/i })
    .first()
    .click();

// Get the URL
const urlInput = page.locator('input[readonly]').first();
const secretUrl = await urlInput.inputValue();

// View
await page.goto(secretUrl);
await page.getByRole('button', { name: /unlock/i }).click();

CI Integration

Tests run in CI with these settings (from playwright.config.ts):

  • forbidOnly: true - Fails if .only is left in tests
  • retries: 2 - Retries failed tests twice
  • workers: 1 - Single worker to prevent conflicts

GitHub Actions Example

- name: Install Playwright Browsers
  run: npx playwright install --with-deps chromium

- name: Run E2E Tests
  run: npm run test:e2e

Viewing Test Reports

After running tests, view the HTML report:

npx playwright show-report

Debugging Failed Tests

  1. Run in debug mode: npm run test:e2e:debug
  2. Run with UI: npm run test:e2e:ui
  3. View traces: Failed tests generate traces in test-results/
  4. Screenshots: Failed tests save screenshots automatically

Best Practices

  1. Use data-testid for stable selectors when possible
  2. Prefer user-facing selectors like getByRole, getByText, getByPlaceholder
  3. Add appropriate timeouts for async operations
  4. Keep tests independent - each test should work in isolation
  5. Use .first() when multiple elements match to avoid strict mode violations