# E2E Testing with Playwright

> Run and write end-to-end tests with Playwright. Covers test execution, Page Objects, bootstrap helpers, and debugging flaky tests.

*Canonical: https://makerkit.dev/docs/nextjs-prisma/testing/e2e*

---

End-to-end tests live in `apps/e2e` and use Playwright to test complete user flows against a real browser and database.

Playwright E2E tests verify that authentication, navigation, forms, and business logic work correctly together. Tests run against seeded test data in a real PostgreSQL database, catching issues that mocks would miss. For testing isolated business logic, use [unit tests with Vitest](./unit) instead.

## Running Tests

### Production Build (Recommended)

For reliable results, run tests against a production-like build:

```bash
# Terminal 1: Build and start the app
pnpm --filter web build:test
pnpm --filter web start:test

# Terminal 2: Run tests with 2 workers
pnpm --filter web-e2e test:slow
```

The `test:slow` command runs with 2 workers and stops on first failure. This reduces flakiness from race conditions and resource contention.

### Fast Iteration (Single Test)

When developing or debugging a specific test:

```bash
pnpm --filter web-e2e exec playwright test auth.spec.ts --workers=1
```

You can also run tests by partial name:

```bash
# Runs any spec matching "billing"
pnpm --filter web-e2e exec playwright test billing --workers=1
```

### Full Suite

Run all tests (rare, usually for CI):

```bash
pnpm --filter web-e2e test
```

### Interactive UI Mode

Debug tests with Playwright's visual interface:

```bash
pnpm --filter web-e2e test:ui
```

This opens a browser where you can step through tests, inspect elements, and see traces.

## Test Structure

Tests are organized by feature in `apps/e2e/tests/`:

```
tests/
├── auth/
│   ├── auth.spec.ts      # Sign up, sign in, password reset, MFA
│   └── auth.po.ts        # Page Object for auth
├── account/
│   └── account.spec.ts   # Account management
├── members/
│   └── members.spec.ts   # Team member management
├── invitations/
│   └── invitations.spec.ts
├── roles/
│   └── roles.spec.ts     # Custom roles
├── settings/
│   ├── settings.spec.ts
│   ├── profile-picture.spec.ts
│   └── organization-settings.spec.ts
├── admin/
│   ├── admin-users.spec.ts
│   ├── admin-organizations.spec.ts
│   └── impersonation.spec.ts
├── utils/
│   ├── bootstrap-helpers.ts
│   └── test-data.po.ts
├── mailbox.po.ts         # Email testing helper
└── global.setup.ts       # Seeds database before tests
```

## Page Object Pattern

Every feature uses a Page Object that encapsulates selectors and actions. This keeps tests readable and maintainable.

```typescript title="apps/e2e/tests/auth/auth.po.ts"
export class AuthPageObject {
  constructor(private readonly page: Page) {}

  async goToSignIn() {
    await this.page.goto('/auth/sign-in');
  }

  async signIn(params: { email: string; password: string }) {
    await this.page.getByTestId('email-input').fill(params.email);
    await this.page.locator('input[name="password"]').fill(params.password);
    await this.page.getByTestId('auth-submit-button').click();
  }

  async expectToBeSignedIn() {
    await expect(
      this.page.getByTestId('account-switcher-trigger')
    ).toBeVisible();
  }
}
```

Tests use the Page Object:

```typescript title="apps/e2e/tests/auth/auth.spec.ts"
test('should sign in with valid credentials', async ({ page }) => {
  const auth = new AuthPageObject(page);

  await auth.goToSignIn();
  await auth.signIn({ email: 'test@example.com', password: 'Password123!' });
  await auth.expectToBeSignedIn();
});
```

## Bootstrap Helpers

Creating users through the UI is slow. Bootstrap helpers create users directly in the database and log in via API:

```typescript title="apps/e2e/tests/settings/settings.spec.ts"
import { bootstrapAuthenticatedUser } from '../utils/bootstrap-helpers';

test('should update profile settings', async ({ page }) => {
  // Creates user in DB and logs in via API (no UI)
  const { user, auth } = await bootstrapAuthenticatedUser(page);

  // User is already logged in and on /dashboard
  await page.goto('/settings');
  // ... rest of test
});
```

Available bootstrap helpers:

| Helper | Description |
|--------|-------------|
| `bootstrapAuthenticatedUser` | Creates and logs in a user |
| `bootstrapUserWithOrg` | Creates user + organization, switches to org context |
| `bootstrapOrgWithMembers` | Creates org with multiple members, logged in as owner |
| `bootstrapOrgMember` | Creates org, logged in as a regular member (not owner) |
| `bootstrapSuperAdminUser` | Creates and logs in a super admin |

Example with organization:

```typescript
test('owner can remove members', async ({ page }) => {
  const { org, auth } = await bootstrapOrgWithMembers(page, {
    memberEmails: ['member1@test.com', 'member2@test.com']
  });

  // Logged in as owner, in org context
  await page.goto(`/settings/members`);
  // ... test member removal
});
```

## Environment Variables

Configure test behavior in `apps/e2e/.env.local`:

| Variable | Default | Description |
|----------|---------|-------------|
| `ENABLE_TEAM_ACCOUNT_TESTS` | `true` | Include team/organization specs |
| `PLAYWRIGHT_SERVER_COMMAND` | - | If set, Playwright starts the server automatically |

To disable team account tests (for personal-account-only mode):

```bash
ENABLE_TEAM_ACCOUNT_TESTS=false pnpm --filter web-e2e test:slow
```

## Configuration Reference

Key settings in `apps/e2e/playwright.config.ts`:

```typescript
export default defineConfig({
  testDir: './tests',
  globalSetup: './tests/global.setup.ts', // Seeds database
  fullyParallel: true,
  retries: 2,
  timeout: 120 * 1000, // 2 minutes per test
  use: {
    baseURL: 'http://localhost:3000',
    screenshot: 'only-on-failure',
    trace: 'on-first-retry',
    testIdAttribute: 'data-testid',
  },
});
```

## Global Setup

Before tests run, `global.setup.ts` seeds the database:

```typescript title="apps/e2e/tests/global.setup.ts"
async function globalSetup() {
  await execAsync('pnpm --filter scripts seed', {
    cwd: process.cwd(),
    env: process.env,
  });
}
```

This ensures consistent test data. The seed script creates base users and data needed for tests.

## Debugging Flaky Tests

### View Test Reports

After a test run, open the HTML report:

```bash
pnpm --filter web-e2e report
```

### Capture Traces

Traces are captured on first retry by default. View them in the report or:

```bash
pnpm --filter web-e2e exec playwright show-trace trace.zip
```

### Common Flakiness Causes

1. **Race conditions**: Use `await expect(...).toPass()` for polling assertions
2. **Animation timing**: Wait for elements with `waitForSelector`
3. **Database state**: Ensure tests don't depend on each other's data
4. **Resource contention**: Reduce workers with `--workers=1`

### Example: Polling Assertion

Instead of:

```typescript
// Might fail if email takes time to arrive
const result = await mailbox.getEmail(email);
expect(result).not.toBeNull();
```

Use:

```typescript
// Retries until passing or timeout
await expect(async () => {
  const result = await mailbox.getEmail(email);
  expect(result).not.toBeNull();
}).toPass({ timeout: 10_000 });
```

## CI Configuration

For GitHub Actions or similar CI:

```yaml
- name: Run E2E tests
  run: |
    pnpm --filter web build:test
    pnpm --filter web start:test &
    sleep 10
    pnpm --filter web-e2e test
  env:
    CI: true
```

The `CI` environment variable:
- Limits workers to 2 (configured in playwright.config.ts)
- Fails on `test.only` (prevents accidental commits)
- Enables stricter error handling

## Available Test Commands

```bash
# Standard run (stops on first failure)
pnpm --filter web-e2e test

# Slow run with 2 workers (most reliable)
pnpm --filter web-e2e test:slow

# Fast run with 8 workers (when tests are stable)
pnpm --filter web-e2e test:fast

# Interactive UI
pnpm --filter web-e2e test:ui

# View last report
pnpm --filter web-e2e report
```

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "How do I run E2E tests in headed mode to see the browser?", "answer": "Use pnpm --filter web-e2e exec playwright test --headed. You can also use test:ui for the interactive UI mode which provides step-through debugging."},
     {"question": "How do I debug a failing E2E test?", "answer": "Run pnpm --filter web-e2e report to view the HTML report with screenshots and traces. Traces are captured on first retry and show every action, network request, and DOM snapshot."},
     {"question": "What's the difference between test:slow and test:fast?", "answer": "test:slow runs with 2 workers for reliability, test:fast runs with 8 workers for speed. Use test:slow when tests are flaky or during CI, test:fast when your test suite is stable."},
     {"question": "How do bootstrap helpers speed up tests?", "answer": "Bootstrap helpers create users and organizations directly in the database and log in via API, skipping the UI entirely. This makes test setup nearly instant compared to clicking through sign-up forms."},
     {"question": "Can I run E2E tests against a production build?", "answer": "Yes, and it's recommended. Use pnpm --filter web build:test && pnpm --filter web start:test to build and serve a production-like build, then run your tests against it."},
     {"question": "How do I test email verification flows?", "answer": "MakerKit uses Mailpit to capture emails locally. The Mailbox helper in tests fetches verification links from Mailpit and navigates to them automatically."}
   ]
/%}

## Next Steps

- [Unit Testing with Vitest](./unit) for testing business logic in isolation
- [Writing Your Own Tests](./writing-tests) for patterns and best practices
