This directory contains Playwright end-to-end tests for the Svelte Society platform.
# Run all E2E tests
bun run test:integration
# Run tests in UI mode (interactive, great for development)
bun run test:integration:ui
# Run tests in headed mode (see browser)
bun run test:integration:headed
# Run tests in debug mode (step through tests)
bun run test:integration:debug
# Run specific test file
bun test:integration tests/e2e/public/search.spec.ts
# Run tests matching a pattern
bun test:integration --grep "search"Total E2E Tests: 103+ tests across 18 test files Total Unit Tests: 47 tests across 1 test file Execution Time: ~15-20 seconds E2E (with 4 parallel workers), <100ms unit tests Success Rate: 100% (0% flaky tests)
- Public Tests (13 tests) - Content browsing, search, detail pages
- Authentication Tests (13 tests) - Login flows, protected routes, role-based access
- Content Submission (9 tests) - Submit recipes, videos, libraries with validation
- Admin Content Management (5 tests) - Edit, archive, publish content
- Admin User Management (4 tests) - View users, edit profiles, manage roles
- Admin Sidebar Shortcuts (6 tests) - Create, toggle, delete sidebar shortcuts
- SEO Endpoints (29 tests) - robots.txt, sitemap.xml validation and resilience
- Sponsor Submission (10 tests) - Sponsor form, tier selection, billing cycles
- Admin Sponsor Management (14 tests) - List, filter, edit, activate/pause/cancel sponsors
- SEO Utils (47 tests) - Meta tag generation, OG/Twitter Cards, helper functions
Each test file gets its own isolated database copy to ensure complete test independence:
How it works:
test.dbis the base database (initialized and seeded once)- Each test file gets a unique copy (e.g.,
test-public-search.db) - Tests set a cookie to route requests to their isolated database
- Databases are automatically created in
globalSetupand cleaned up inglobalTeardown
Benefits:
- ✅ Perfect test isolation - no test can affect another
- ✅ Parallel execution safe - tests never conflict
- ✅ Fast - databases pre-created, not during test execution
- ✅ Clean - automatic cleanup after tests complete
# Initialize test database (creates schema, runs migrations)
bun run db:test:init
# Seed test database (adds test users, content, tags)
bun run db:test:seed
# These run automatically as part of test:integrationThree test users with different permission levels:
- Username:
test_admin - Email:
admin@test.local - Role: Admin (full access to all features)
- Session Token:
test_session_admin_token
- Username:
test_contributor - Email:
contributor@test.local - Role: Moderator (can submit and moderate content)
- Session Token:
test_session_contributor_token
- Username:
test_viewer - Email:
viewer@test.local - Role: Member (read-only access, can save content)
- Session Token:
test_session_viewer_token
Use the loginAs helper function:
import { loginAs } from '../../helpers/auth'
test.beforeEach(async ({ page }) => {
await setupDatabaseIsolation(page)
await loginAs(page, 'admin') // or 'contributor' or 'viewer'
})Seed data includes:
Content: 8 items (recipes, videos, libraries, announcements, collections)
- Mix of published, pending review, and draft content
- Variety of content types for testing different views
Tags: 10 tags (svelte, sveltekit, testing, components, etc.)
Saved Content: Pre-saved items for testing saved content features
See tests/fixtures/test-data.ts for complete test data definitions.
tests/
├── e2e/ # End-to-end test suites
│ ├── public/ # Public user flows (browsing, search, detail pages)
│ ├── auth/ # Authentication flows (login, protected routes, roles)
│ ├── content/ # Content submission (recipes, videos, libraries)
│ └── admin/ # Admin workflows (content management, users, shortcuts)
├── pages/ # Page Object Models (POMs)
│ ├── BasePage.ts # Base class with common functionality
│ ├── HomePage.ts # Homepage navigation and search
│ ├── ContentListPage.ts # Content browsing pages
│ ├── ContentDetailPage.ts # Individual content detail pages
│ ├── LoginPage.ts # Login and authentication
│ ├── SubmitPage.ts # Content submission forms
│ ├── AdminDashboardPage.ts # Admin dashboard
│ ├── ContentEditPage.ts # Content editing
│ ├── UserManagementPage.ts # User management
│ ├── ShortcutsPage.ts # Sidebar shortcuts management
│ └── index.ts # Exports all POMs
├── helpers/ # Utility functions
│ ├── auth.ts # Authentication helpers (loginAs, logout)
│ └── database-isolation.ts # Database isolation setup
├── fixtures/ # Test data and fixtures
│ └── test-data.ts # Centralized test data definitions
└── setup/ # Global setup/teardown
├── global-setup.ts # Pre-create isolated databases
└── global-teardown.ts # Cleanup isolated databases
All tests use POMs to encapsulate page interactions:
import { test, expect } from '@playwright/test'
import { HomePage, ContentListPage } from '../../pages'
import { setupDatabaseIsolation } from '../../helpers/database-isolation'
test.describe('Search Functionality', () => {
test.beforeEach(async ({ page }) => {
await setupDatabaseIsolation(page)
})
test('can search for content', async ({ page }) => {
const homePage = new HomePage(page)
await homePage.goto()
await homePage.search('Counter')
const contentList = new ContentListPage(page)
await contentList.expectContentDisplayed()
const titles = await contentList.getContentTitles()
expect(titles.some((title) => title.includes('Counter'))).toBeTruthy()
})
})Why POMs?
- ✅ Maintainable - changes to UI only affect POM, not all tests
- ✅ Reusable - same page interactions used across multiple tests
- ✅ Readable - tests read like plain English
- ✅ Type-safe - TypeScript ensures correct usage
Always use data-testid attributes for element selection:
<!-- Component.svelte -->
<button data-testid="submit-button">Submit</button>
<input data-testid="search-input" type="search" />
<article data-testid="content-card">...</article>// POM
get submitButton(): Locator {
return this.page.getByTestId('submit-button')
}Auto-generated test-ids:
Form components automatically generate test-ids from their name prop:
<Input name="username" />→data-testid="input-username"<Textarea name="description" />→data-testid="textarea-description"<Select name="role" />→data-testid="select-role"
Why test-ids?
- ✅ Stable - don't break when CSS/styling changes
- ✅ Explicit - clear intent for testing
- ✅ Fast - direct element lookup
- ❌ Avoid CSS selectors (
.class,#id) - ❌ Avoid text selectors (
text=Submit)
✅ Do:
- Use POMs for all page interactions
- Add test-ids to new components
- Use
setupDatabaseIsolation()inbeforeEach - Write focused tests (test one thing per test)
- Keep tests independent (no shared state)
- Use descriptive test names
- Rely on Playwright's auto-waiting
- Use helper functions (
loginAs, etc.)
❌ Don't:
- Use CSS selectors or XPath
- Add unnecessary waits (use
waitForLoadStateonly when needed) - Test implementation details
- Make tests depend on execution order
- Leave
test.only()ortest.skip()in committed code - Hardcode test data (use
test-data.ts)
Every test file must call setupDatabaseIsolation():
import { setupDatabaseIsolation } from '../../helpers/database-isolation'
test.beforeEach(async ({ page }) => {
await setupDatabaseIsolation(page) // Auto-detects test file name
})This automatically:
- Detects your test file name from the stack trace
- Sets a cookie to route requests to your isolated database
- Ensures complete test independence
Current Performance:
- Local: ~14 seconds for 65 tests (with 4 workers)
- CI: ~15-20 seconds for test execution
- Total CI time: ~3-4 minutes (including build, dependencies, database setup)
Parallel Execution:
- ✅ Enabled by default (
fullyParallel: true) - ✅ 4 workers run tests concurrently
- ✅ Database isolation ensures safety
Why it's fast:
- Pre-created isolated databases (no runtime overhead)
- Parallel execution (4 tests at once)
- Efficient POMs (no redundant waits)
- Optimized CI (browser caching saves ~1.5 minutes)
- Check that no other process is accessing test databases
- Ensure tests are using
setupDatabaseIsolation() - Try running with
--workers=1to isolate the issue
- Ensure you're using
loginAs()helper for authenticated tests - Check that database was seeded:
bun run db:test:seed - Verify session tokens in
tests/fixtures/test-data.ts
- Check for missing
awaitkeywords - Ensure elements have
data-testidattributes - Look for slow network requests in the test output
- Try running in headed mode to see what's happening:
bun run test:integration:headed
- Avoid
waitForTimeout()- usewaitForLoadState('networkidle')if needed - Ensure proper waiting with
expectContentDisplayed()or similar - Check for race conditions in asynchronous operations
- Use
test.fail()to mark known flaky tests
- Check that
.env.developmentexists with required variables - Ensure
bun installcompleted successfully - Try cleaning:
rm -rf node_modules && bun install - Clear test databases:
rm -f test*.db*
Tests run automatically on GitHub Actions for every PR to staging:
Workflow: .github/workflows/playwright.yml
Steps:
- Install dependencies
- Cache Playwright browsers (~1.5 min savings)
- Initialize and seed test database
- Build application
- Run Playwright tests
- Post results as PR comment
- Upload artifacts (HTML report, screenshots, videos)
Performance:
- First run: ~5-6 minutes (downloads browsers)
- Subsequent runs: ~3-4 minutes (uses cached browsers)
Artifacts:
- HTML report (always uploaded, 30 day retention)
- Test results with screenshots/videos (uploaded on failure, 30 day retention)
- Playwright Documentation: https://playwright.dev
- PRD:
docs/PRD_PLAYWRIGHT_E2E_TESTING.md - Testing Guide:
docs/TESTING_GUIDE.md(coming soon) - Test Data:
tests/fixtures/test-data.ts
If you encounter issues:
- Check this README's troubleshooting section
- Review test output and error messages
- Run in headed mode to see browser:
bun run test:integration:headed - Check CI logs for similar failures
- Ask in team chat or create an issue