Development
E2E Testing
End-to-end testing with Playwright, shared fixtures, and CI integration
Quick Start
Run All E2E Tests
# Using turbo (recommended)
bun test:e2e
# Using Playwright CLI directly
bunx playwright test
Run Tests for Specific App
# Using turbo
turbo test:e2e --filter=calnexus
turbo test:e2e --filter=lexilink
# Using app-level scripts
cd apps/calnexus && bun test:e2e
cd apps/lexilink && bun test:e2e
Run Tests in UI Mode (Development)
cd apps/calnexus
bun test:e2e:ui
Run Tests in Headed Mode (See Browser)
cd apps/calnexus
bun test:e2e:headed
Project Structure
hn-monorepo/
├── playwright.config.ts # Root config (runs all apps)
├── apps/
│ ├── calnexus/
│ │ ├── playwright.config.ts # CalNexus config
│ │ └── tests/e2e/ # CalNexus E2E tests
│ ├── lexilink/
│ │ ├── playwright.config.ts # Lexilink config
│ │ └── e2e/ # Lexilink E2E tests
│ ├── planex/
│ │ ├── playwright.config.ts # Planex config
│ │ └── e2e/ # Planex E2E tests
│ ├── nexus-lms/
│ │ ├── playwright.config.ts # Nexus LMS config
│ │ └── e2e/ # Nexus LMS E2E tests
│ ├── qript/
│ │ ├── playwright.config.ts # Qript config
│ │ └── e2e/ # Qript E2E tests
│ └── portfolio/
│ ├── playwright.config.ts # Portfolio config
│ └── e2e/ # Portfolio E2E tests
└── packages/
└── testing/
├── playwright-config.ts # Shared config factory
└── fixtures/
└── playwright.ts # Shared E2E helpers
Writing Tests
Basic Test Structure
import { expect, test } from "@playwright/test";
import { waitForHydration } from "@hn-monorepo/testing/fixtures";
test.describe("Feature Name", () => {
test("should do something", async ({ page }) => {
await page.goto("/");
await waitForHydration(page);
await expect(page.locator("h1")).toBeVisible();
});
});
Using Shared Helpers
The @hn-monorepo/testing package provides many helpful utilities:
import {
waitForHydration,
checkBasicAccessibility,
mockApiRoute,
fillAndSubmitForm,
testData,
} from "@hn-monorepo/testing/fixtures";
// Wait for Next.js hydration
await waitForHydration(page);
// Check basic accessibility
const a11y = await checkBasicAccessibility(page);
expect(a11y.hasMain).toBe(true);
// Mock API responses
await mockApiRoute(page, "/api/users", { users: [] });
// Fill and submit forms
await fillAndSubmitForm(page, {
email: "test@example.com",
password: "Password123!",
}, "Sign In");
// Generate test data
const user = testData.user({ email: "custom@example.com" });
Test Naming Conventions
- Use descriptive test names that explain what’s being tested
- Group related tests using
test.describe() - Use
.spec.tssuffix for test files - Use
.setup.tssuffix for setup/teardown files - Use
.auth-test.tssuffix for authenticated tests (planex pattern)
Test Categories
Smoke Tests (smoke.spec.ts)
- Basic page loads
- Critical user flows
- No console errors
- Quick checks that core functionality works
Authentication Tests (auth.spec.ts)
- Login/logout flows
- Registration
- Password reset
- Protected routes
Feature Tests (named after feature)
- Detailed testing of specific features
- User interactions
- Edge cases
Responsive Tests (responsive.spec.ts or within other tests)
- Mobile viewport (375x667)
- Tablet viewport (768x1024)
- Desktop viewport (1280x720)
Configuration
Shared Configuration
All apps use the shared config factory from @hn-monorepo/testing/playwright-config:
import { createPlaywrightConfig } from "@hn-monorepo/testing/playwright-config";
export default createPlaywrightConfig({
baseURL: "http://localhost:3000",
testDir: "./e2e",
devServerCommand: "bun run dev",
port: 3000,
workers: 1,
fullyParallel: false,
includeMobile: true,
includeTablet: true,
});
Custom Configuration
You can extend the base config for app-specific needs:
import { createPlaywrightConfig } from "@hn-monorepo/testing/playwright-config";
import { defineConfig } from "@playwright/test";
const baseConfig = createPlaywrightConfig({
// ... options
});
export default defineConfig({
...baseConfig,
projects: [
// Custom projects for auth workflows
{
name: "setup",
testMatch: /.*\.setup\.ts/,
},
{
name: "authenticated",
testMatch: /.*\.auth-test\.ts/,
use: {
storageState: "playwright/.auth/user.json",
},
dependencies: ["setup"],
},
],
});
Best Practices
1. Wait for Hydration
Always wait for Next.js hydration to complete before interacting with the page:
await page.goto("/");
await waitForHydration(page);
2. Use Accessible Selectors
Prefer accessible selectors over CSS classes:
// Preferred
await page.getByRole("button", { name: "Submit" }).click();
await page.getByLabel("Email").fill("test@example.com");
// Avoid
await page.locator(".submit-button").click();
3. Handle Loading States
Wait for content to load before assertions:
await page.waitForLoadState("networkidle");
await expect(page.getByText("Loading...")).not.toBeVisible();
4. Avoid Flaky Tests
- Use proper waiting mechanisms
- Don’t use arbitrary
waitForTimeout()unless absolutely necessary - Use
waitForSelector()with visible state - Check for element visibility before interaction
5. Clean Up After Tests
test.afterEach(async ({ page }) => {
await clearBrowserStorage(page);
});
6. Test in Isolation
Each test should be independent and not rely on previous test state.
CI/CD Integration
Tests automatically run in CI with optimized settings:
- Single worker to avoid overwhelming the server
- 2 retries for transient failures
- HTML report generated
- Screenshots on failure
- Traces on retry
Environment variables in CI:
CI=trueenables CI mode- Tests run sequentially
- Server doesn’t reuse existing instance
E2E Test Status in CI
| App | Status | Mock Provider | Notes |
|---|---|---|---|
| calnexus | Stable | NEXT_PUBLIC_MOCK_CONVEX=true | Non-experimental, failures block CI |
| lexilink | Stable | NEXT_PUBLIC_MOCK_CONVEX=true | Comprehensive mock coverage for all tested Convex functions |
E2E tests use NEXT_PUBLIC_MOCK_CONVEX=true to avoid real Convex calls in CI. Mock responses are defined in apps/<app>/e2e/fixtures/mocks.ts.
Debugging
Debug Tests Locally
# Run in headed mode
bun test:e2e:headed
# Open Playwright UI
bun test:e2e:ui
# Run with debug mode
PWDEBUG=1 bun test:e2e
View Test Reports
bunx playwright show-report
Inspect Test Artifacts
After test failures:
- Screenshots:
test-results/*/screenshot.png - Videos:
test-results/*/video.webm - Traces:
test-results/*/trace.zip
View traces:
bunx playwright show-trace test-results/*/trace.zip
Common Patterns
Testing Forms
test("submits contact form", async ({ page }) => {
await page.goto("/contact");
await fillAndSubmitForm(page, {
name: "John Doe",
email: "john@example.com",
message: "Hello!",
}, "Send");
await expect(page.getByText("Message sent")).toBeVisible();
});
Testing Authentication
test("user can log in", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("Email").fill("test@example.com");
await page.getByLabel("Password").fill("password123");
await page.getByRole("button", { name: "Sign In" }).click();
await expect(page).toHaveURL(/dashboard/);
});
Testing Responsive Design
test("mobile menu works", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto("/");
const mobileMenu = page.getByRole("button", { name: "Menu" });
await expect(mobileMenu).toBeVisible();
await mobileMenu.click();
await expect(page.getByRole("navigation")).toBeVisible();
});
Mocking API Responses
test("handles API errors", async ({ page }) => {
await mockApiRoute(page, "/api/users",
{ error: "Server error" },
500
);
await page.goto("/users");
await expect(page.getByText("Error loading users")).toBeVisible();
});
Performance Testing
test("page loads quickly", async ({ page }) => {
const start = Date.now();
await page.goto("/");
await waitForHydration(page);
const loadTime = Date.now() - start;
expect(loadTime).toBeLessThan(3000); // 3 seconds
});
Accessibility Testing
test("page is accessible", async ({ page }) => {
await page.goto("/");
const a11y = await checkBasicAccessibility(page);
expect(a11y.hasMain).toBe(true);
expect(a11y.hasNav).toBe(true);
});
Troubleshooting
Tests Fail Locally But Pass in CI
- Check Node.js/Bun version matches CI
- Clear
.nextbuild cache - Install latest browsers:
bunx playwright install
Port Already in Use
- Change port in app’s
playwright.config.ts - Kill process using the port:
lsof -ti:3000 | xargs kill
Slow Test Execution
- Reduce number of workers
- Use
fullyParallel: false - Run specific test files instead of entire suite
Browser Not Launching
bunx playwright install
bunx playwright install-deps
Updating Dependencies
# Update Playwright
bun update @playwright/test
# Install new browsers
bunx playwright install