8.2 KiB
E2E Test Helpers & Documentation
This directory contains end-to-end tests using Playwright with deterministic test fixtures.
Overview
The E2E test infrastructure provides:
- Deterministic Testing: Consistent time, random values, and no animations
- E2E User Authentication: Bypass normal auth via
x-e2e-userheader - Fixture Data: Pre-populated test data in
services/e2eFixtures.ts - Reusable Helpers: Common test utilities in
helpers/fixtures.ts
Available E2E Users
The following test users are available (defined in services/e2eFixtures.ts):
| User Type | User ID | Name | Description |
|---|---|---|---|
landlord |
e2e-landlord-1 | Alex Landlord | Fully onboarded landlord with properties and leases |
landlord-2 |
e2e-landlord-2 | Jordan Landlord | Second landlord for multi-user testing |
landlord-onboarding |
e2e-landlord-setup-1 | (empty) | New landlord not yet onboarded |
tenant |
e2e-tenant-1 | Taylor Tenant | Tenant with active lease |
admin |
e2e-admin-1 | Admin User | Admin with full system access |
Test Data Available
Each E2E user has pre-populated data:
Landlord (Alex)
- Properties: Skyline Lofts (available), Maple Row (occupied)
- Leases: 1 active lease for Maple Row
- Messages: 11 messages (mix of read/unread)
- Maintenance Requests: 2 requests (open, in-progress)
- Payment Transactions: 3 completed payments
Tenant (Taylor)
- Leases: 1 active lease at Maple Row
- Messages: Conversations with landlord
- Maintenance Requests: 2 submitted requests
- Payment Methods: 1 default payment method
- Payment Transactions: Payment history
Landlord-2 (Jordan)
- Properties: Riverside Condo
- Leases: None (testing landlord with no leases)
How to Write Tests
Basic UI Test
import { expect, test } from "@playwright/test";
import {
applyDeterministicPage,
ROUTES,
useE2EUser,
} from "../helpers/fixtures.ts";
test("landlord views dashboard", async ({ page }) => {
// 1. Apply deterministic behavior
await applyDeterministicPage(page);
// 2. Set E2E user context
await useE2EUser(page, "landlord");
// 3. Navigate and test
await page.goto(ROUTES.landlordDashboard);
await expect(page.locator("h1")).toContainText("Dashboard");
});
API Test
import { expect, test } from "@playwright/test";
import { e2eHeaders } from "../helpers/fixtures.ts";
test("API returns landlord properties", async ({ request }) => {
const response = await request.get("/api/retrieve/properties", {
headers: e2eHeaders("landlord"),
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
expect(data.properties).toHaveLength(2);
});
Test with Generated Data
import { generateTestProperty } from "../helpers/fixtures.ts";
test("create new property", async ({ page, request }) => {
const newProperty = generateTestProperty({
title: "My Test Property",
bedrooms: 3,
});
const response = await request.post("/api/create/property", {
headers: e2eHeaders("landlord"),
data: newProperty,
});
expect(response.ok()).toBeTruthy();
});
Helper Functions Reference
useE2EUser(page, userType)
Sets the E2E user context for browser tests.
- Parameters:
page: Playwright Page objectuserType: E2E user type ("landlord", "tenant", etc.)
- What it does:
- Injects
x-e2e-userheader in all requests - Sets
localStorageflag to skip encryption
- Injects
applyDeterministicPage(page)
Makes page behavior deterministic for consistent tests.
- Parameters:
page: Playwright Page object
- What it does:
- Stubs
Date.now()to return fixed base time - Stubs
Math.random()for consistent sequences - Disables CSS animations/transitions
- Sets timezone to UTC
- Stubs
e2eHeaders(userType)
Creates headers for API requests with E2E authentication.
- Parameters:
userType: E2E user type
- Returns: Headers object with
x-e2e-userandContent-Type
generateTestProperty(overrides?)
Generates a unique test property object.
- Parameters:
overrides: Optional partial property data
- Returns: Complete property object with unique values
generateTestLease(overrides?)
Generates a unique test lease object.
generateTestMessage(overrides?)
Generates a unique test message object.
generateTestMaintenanceRequest(overrides?)
Generates a unique test maintenance request object.
expectVisible(page, selector)
Asserts an element is visible.
expectText(page, selector, text)
Asserts an element contains specific text.
expectNoConsoleErrors(page)
Asserts no console errors occurred.
waitForPageLoad(page)
Waits for page to fully load (networkidle + domcontentloaded).
Routes Reference
Common routes are available in ROUTES constant:
ROUTES.home; // "/"
ROUTES.landlordDashboard; // "/landlord/dashboard"
ROUTES.tenantDashboard; // "/tenant/dashboard"
ROUTES.landlordProperties; // "/landlord/properties"
ROUTES.landlordLeases; // "/landlord/leases"
ROUTES.tenantLeases; // "/tenant/leases"
// ... and more
Running Tests
Run all E2E tests
deno task e2e
Run specific test file
deno run -A npm:playwright test tests/e2e/example.test.ts
Run with UI mode
deno task e2e:ui
View test report
deno task e2e:report
Best Practices
1. Always Apply Deterministic Behavior
test.beforeEach(async ({ page }) => {
await applyDeterministicPage(page);
});
2. Set User Context Early
test("my test", async ({ page }) => {
await useE2EUser(page, "landlord"); // Do this first
await page.goto("/some/page"); // Then navigate
});
3. Use Generated Data for Uniqueness
// Good - unique every time
const property = generateTestProperty();
// Bad - may conflict
const property = { title: "Test Property" };
4. Wait for Page Load
await page.goto("/some/page");
await waitForPageLoad(page); // Always wait after navigation
5. Check for Console Errors
await expectNoConsoleErrors(page); // Catch JS errors
6. Use Resilient Assertions
// Good - checks for section header
await expectText(page, "h1", "Dashboard");
// Bad - assumes specific data that may change
await expectText(page, "text=Skyline Lofts", "Skyline Lofts");
Data Isolation Testing
The E2E infrastructure supports testing data isolation:
test("users cannot see each other's data", async ({ page }) => {
// Landlord view
await useE2EUser(page, "landlord");
await page.goto(ROUTES.landlordProperties);
await expect(page.locator("text=Skyline Lofts")).toBeVisible();
// Tenant view (should not see landlord properties)
await useE2EUser(page, "tenant");
await page.goto(ROUTES.tenantLeases);
await expect(page.locator("text=Skyline Lofts")).not.toBeVisible();
});
Troubleshooting
Tests Failing with "Not Found"
- Ensure the dev server is running with
E2E_TEST=true - Check that routes exist in the codebase
Tests Flaky / Inconsistent
- Ensure
applyDeterministicPage()is called - Check that you're using
waitForPageLoad()after navigation - Verify no real-time timers or intervals in your code
Data Not Persisting
- Remember: E2E tests use in-memory fixtures
- Data created in one test persists for that test run
- Use unique generated data to avoid conflicts
Authentication Issues
- Verify
x-e2e-userheader is being set - Check
services/stateMiddleware.tsfor E2E logic - Ensure
E2E_TEST=trueis set in environment
Additional Resources
- Playwright Documentation
services/e2eFixtures.ts- Fixture data definitionsservices/stateMiddleware.ts- E2E authentication bypassplaywright.config.ts- Test configuration