Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions content/docs/playwright-debugging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
title: Debugging with Playwright
description: How to use the Playwright UI and debug modes to inspect and step through end-to-end tests.
group: Testing
order: 1
---

## Overview

The project ships two Playwright modes beyond a plain `test:e2e` run that make it much easier to develop and debug end-to-end tests: **UI mode** and **Debug mode**.

---

## UI Mode — `npm run test:e2e:ui`

UI mode opens a visual browser interface where you can browse all test files, run individual tests, and watch each step execute in real time.

```bash
npm run test:e2e:ui
```

### What you get

- **Test explorer** — sidebar lists every spec file and test; click any to run just that one.
- **Timeline scrubber** — replay every action step-by-step after a run, with before/after DOM snapshots.
- **Live browser** — watch the actual browser execute the test in the right-hand panel.
- **Trace viewer built in** — no need to open a separate trace file; it's all inline.
- **Watch mode** — tests re-run automatically when you save a spec file.

### Typical workflow

1. Start UI mode:
```bash
npm run test:e2e:ui
```
2. Click a test in the left panel to run it.
3. If it fails, click the failing step in the timeline to see the DOM snapshot at that exact moment.
4. Edit your spec or source code — the test re-runs automatically.

---

## Debug Mode — `npm run test:e2e:debug`

Debug mode runs tests headed (visible browser) and pauses execution at the start so you can step through actions one at a time using the **Playwright Inspector**.

```bash
npm run test:e2e:debug
```

### What you get

- **Playwright Inspector** — a floating control panel that shows the current action, lets you step forward, and highlights the targeted element in the browser.
- **`page.pause()` breakpoints** — add `await page.pause()` anywhere in a spec to halt execution at that exact line.
- **Live locator picker** — click the crosshair icon in the Inspector to point at any element and get its recommended locator string.
- **Console output** — browser console logs stream in real time alongside the Inspector.

### Typical workflow

1. Add a `page.pause()` where you want to break:
```ts
await page.click('button[type="submit"]');
await page.pause(); // execution stops here
await page.waitForURL('/dashboard');
```
2. Start debug mode:
```bash
npm run test:e2e:debug
```
3. The browser opens and the Inspector appears. Click **Resume** to run until the next `page.pause()`, or **Step over** to advance one action at a time.
4. Remove `page.pause()` calls before committing.

### Run a single test in debug mode

```bash
npx playwright test e2e/tests/01-admin-approve-nonprofit.spec.ts --debug
```

---

## Running a specific test file

Both modes accept a file path or test title filter:

```bash
# UI mode, scoped to one file
npx playwright test e2e/tests/03-nonprofit-claim-product.spec.ts --ui

# Debug mode, scoped by test name
npx playwright test --debug -g "admin approves nonprofit"
```

---

## Viewing traces after a failed CI run

When tests run in CI (`test:e2e`), traces are saved on failure. Download the artifact and open it locally:

```bash
npx playwright show-trace path/to/trace.zip
```

---

## Quick reference

| Command | What it does |
| ------------------------------------ | ---------------------------------------------- |
| `npm run test:e2e` | Headless run, all tests |
| `npm run test:e2e:ui` | Visual UI mode with timeline scrubber |
| `npm run test:e2e:debug` | Headed + Playwright Inspector, pauses at start |
| `npx playwright test <file> --debug` | Debug a single spec file |
| `npx playwright show-trace <file>` | Open a saved trace zip |
107 changes: 107 additions & 0 deletions e2e/tests/06-announcement-emails.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* Test 06 — Admin creates an announcement and email notification is triggered
*
* Steps:
* 1. Admin navigates to /announcements
* 2. Clicks "New Announcement"
* 3. Fills title, content, and group target
* 4. Submits — POST /api/admin-announcements is called
* 5. POST /api/announcement-emails is triggered with the new announcement ID
* 6. New announcement row appears in the grid
*/

import { test, expect } from '@playwright/test';
import '../load-env';
import { PrismaClient } from '../../src/generated/prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { E2E_PREFIX } from '../shared-state';

const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL,
});

test.use({ storageState: 'e2e/.auth/admin.json' });
test.describe.configure({ mode: 'serial' });

const TEST_ANNOUNCEMENT_TITLE = `${E2E_PREFIX} Test Announcement`;
const TEST_ANNOUNCEMENT_CONTENT =
'This is an automated E2E test announcement. Please ignore.';

test.afterAll(async () => {
const prisma = new PrismaClient({ adapter });
try {
await prisma.announcement.deleteMany({
where: { title: { startsWith: E2E_PREFIX } },
});
} finally {
await prisma.$disconnect();
}
});

test('admin navigates to /announcements page', async ({ page }) => {
await page.goto('/announcements');
await expect(
page.getByRole('heading', { name: /announcement system/i })
).toBeVisible();
});

test('admin creates an announcement and email endpoint is called', async ({
page,
}) => {
let announcementEmailCalled = false;
let capturedAnnouncementId: string | undefined;

// Capture the announcement ID from the create API response
await page.route('**/api/admin-announcements', async (route) => {
if (route.request().method() !== 'POST') {
return route.continue();
}
const response = await route.fetch();
const body = await response.json();
if (body?.id) {
capturedAnnouncementId = body.id as string;
}
await route.fulfill({ response });
});

// Mock the email endpoint so no real emails are sent
await page.route('**/api/announcement-emails', async (route) => {
announcementEmailCalled = true;
const requestBody = route.request().postDataJSON();
expect(requestBody.announcementId).toBeTruthy();
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true, sent: 2 }),
});
});

await page.goto('/announcements');

// Open the create dialog
await page.getByRole('button', { name: /new announcement/i }).click();

await expect(
page.getByRole('heading', { name: /create new announcement/i })
).toBeVisible();

// Fill the form
await page
.getByPlaceholder('Enter announcement title')
.fill(TEST_ANNOUNCEMENT_TITLE);
await page
.getByPlaceholder('Enter announcement content')
.fill(TEST_ANNOUNCEMENT_CONTENT);

// Submit
await page.getByRole('button', { name: 'Create' }).click();

// Wait for the row to appear in the grid
await expect(page.getByText(TEST_ANNOUNCEMENT_TITLE)).toBeVisible({
timeout: 8_000,
});

// Verify the email endpoint was triggered
expect(announcementEmailCalled).toBe(true);
expect(capturedAnnouncementId).toBeTruthy();
});
105 changes: 105 additions & 0 deletions e2e/tests/07-discussion-emails.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Test 07 — User creates a discussion thread and email notification is triggered
*
* Steps:
* 1. Admin navigates to /discussion
* 2. Clicks "New Thread"
* 3. Fills title, content, and group target
* 4. Submits — POST /api/threads is called
* 5. POST /api/discussion-emails is triggered with the new thread ID
* 6. New thread row appears in the grid
*/

import { test, expect } from '@playwright/test';
import '../load-env';
import { PrismaClient } from '../../src/generated/prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { E2E_PREFIX } from '../shared-state';

const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL,
});

test.use({ storageState: 'e2e/.auth/admin.json' });
test.describe.configure({ mode: 'serial' });

const TEST_THREAD_TITLE = `${E2E_PREFIX} Test Discussion Thread`;
const TEST_THREAD_CONTENT =
'This is an automated E2E test discussion thread. Please ignore.';

test.afterAll(async () => {
const prisma = new PrismaClient({ adapter });
try {
await prisma.thread.deleteMany({
where: { title: { startsWith: E2E_PREFIX } },
});
} finally {
await prisma.$disconnect();
}
});

test('admin navigates to /discussion page', async ({ page }) => {
await page.goto('/discussion');
await expect(
page.getByRole('heading', { name: /discussion threads/i })
).toBeVisible();
});

test('user creates a discussion thread and email endpoint is called', async ({
page,
}) => {
let discussionEmailCalled = false;
let capturedThreadId: string | undefined;

// Capture the thread ID from the create API response
await page.route('**/api/threads', async (route) => {
if (route.request().method() !== 'POST') {
return route.continue();
}
const response = await route.fetch();
const body = await response.json();
if (body?.id) {
capturedThreadId = body.id as string;
}
await route.fulfill({ response });
});

// Mock the email endpoint so no real emails are sent
await page.route('**/api/discussion-emails', async (route) => {
discussionEmailCalled = true;
const requestBody = route.request().postDataJSON();
expect(requestBody.threadId).toBeTruthy();
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true, sent: 3 }),
});
});

await page.goto('/discussion');

// Open the create dialog
await page.getByRole('button', { name: /new thread/i }).click();

await expect(
page.getByRole('heading', { name: /create new thread/i })
).toBeVisible();

// Fill the form
await page.getByPlaceholder('Thread Title').fill(TEST_THREAD_TITLE);
await page
.getByPlaceholder('Share your thoughts, questions, or ideas...')
.fill(TEST_THREAD_CONTENT);

// Submit
await page.getByRole('button', { name: 'Create Thread' }).click();

// Wait for the new thread row to appear in the grid
await expect(page.getByText(TEST_THREAD_TITLE)).toBeVisible({
timeout: 8_000,
});

// Verify the email endpoint was triggered
expect(discussionEmailCalled).toBe(true);
expect(capturedThreadId).toBeTruthy();
});
Loading
Loading