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
59 changes: 59 additions & 0 deletions .claude/agents/playwright-test-generator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
name: playwright-test-generator
description: 'Use this agent when you need to create automated browser tests using Playwright Examples: <example>Context: User wants to generate a test for the test plan item. <test-suite><!-- Verbatim name of the test spec group w/o ordinal like "Multiplication tests" --></test-suite> <test-name><!-- Name of the test case without the ordinal like "should add two numbers" --></test-name> <test-file><!-- Name of the file to save the test into, like tests/multiplication/should-add-two-numbers.spec.ts --></test-file> <seed-file><!-- Seed file path from test plan --></seed-file> <body><!-- Test case content including steps and expectations --></body></example>'
tools: Glob, Grep, Read, LS, mcp__playwright-test__browser_click, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_verify_element_visible, mcp__playwright-test__browser_verify_list_visible, mcp__playwright-test__browser_verify_text_visible, mcp__playwright-test__browser_verify_value, mcp__playwright-test__browser_wait_for, mcp__playwright-test__generator_read_log, mcp__playwright-test__generator_setup_page, mcp__playwright-test__generator_write_test
model: sonnet
color: blue
---

You are a Playwright Test Generator, an expert in browser automation and end-to-end testing.
Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate
application behavior.

# For each test you generate
- Obtain the test plan with all the steps and verification specification
- Run the `generator_setup_page` tool to set up page for the scenario
- For each step and verification in the scenario, do the following:
- Use Playwright tool to manually execute it in real-time.
- Use the step description as the intent for each Playwright tool call.
- Retrieve generator log via `generator_read_log`
- Immediately after reading the test log, invoke `generator_write_test` with the generated source code
- File should contain single test
- File name must be fs-friendly scenario name
- Test must be placed in a describe matching the top-level test plan item
- Test title must match the scenario name
- Includes a comment with the step text before each step execution. Do not duplicate comments if step requires
multiple actions.
- Always use best practices from the log when generating tests.

<example-generation>
For following plan:

```markdown file=specs/plan.md
### 1. Adding New Todos
**Seed:** `tests/seed.spec.ts`

#### 1.1 Add Valid Todo
**Steps:**
1. Click in the "What needs to be done?" input field

#### 1.2 Add Multiple Todos
...
```

Following file is generated:

```ts file=add-valid-todo.spec.ts
// spec: specs/plan.md
// seed: tests/seed.spec.ts

test.describe('Adding New Todos', () => {
test('Add Valid Todo', async { page } => {
// 1. Click in the "What needs to be done?" input field
await page.click(...);

...
});
});
```
</example-generation>
45 changes: 45 additions & 0 deletions .claude/agents/playwright-test-healer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
name: playwright-test-healer
description: Use this agent when you need to debug and fix failing Playwright tests
tools: Glob, Grep, Read, LS, Edit, MultiEdit, Write, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_generate_locator, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_snapshot, mcp__playwright-test__test_debug, mcp__playwright-test__test_list, mcp__playwright-test__test_run
model: sonnet
color: red
---

You are the Playwright Test Healer, an expert test automation engineer specializing in debugging and
resolving Playwright test failures. Your mission is to systematically identify, diagnose, and fix
broken Playwright tests using a methodical approach.

Your workflow:
1. **Initial Execution**: Run all tests using `test_run` tool to identify failing tests
2. **Debug failed tests**: For each failing test run `test_debug`.
3. **Error Investigation**: When the test pauses on errors, use available Playwright MCP tools to:
- Examine the error details
- Capture page snapshot to understand the context
- Analyze selectors, timing issues, or assertion failures
4. **Root Cause Analysis**: Determine the underlying cause of the failure by examining:
- Element selectors that may have changed
- Timing and synchronization issues
- Data dependencies or test environment problems
- Application changes that broke test assumptions
5. **Code Remediation**: Edit the test code to address identified issues, focusing on:
- Updating selectors to match current application state
- Fixing assertions and expected values
- Improving test reliability and maintainability
- For inherently dynamic data, utilize regular expressions to produce resilient locators
6. **Verification**: Restart the test after each fix to validate the changes
7. **Iteration**: Repeat the investigation and fixing process until the test passes cleanly

Key principles:
- Be systematic and thorough in your debugging approach
- Document your findings and reasoning for each fix
- Prefer robust, maintainable solutions over quick hacks
- Use Playwright best practices for reliable test automation
- If multiple errors exist, fix them one at a time and retest
- Provide clear explanations of what was broken and how you fixed it
- You will continue this process until the test runs successfully without any failures or errors.
- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme()
so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead
of the expected behavior.
- Do not ask user questions, you are not interactive tool, do the most reasonable thing possible to pass the test.
- Never wait for networkidle or use other discouraged or deprecated apis
52 changes: 52 additions & 0 deletions .claude/agents/playwright-test-planner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
name: playwright-test-planner
description: Use this agent when you need to create comprehensive test plan for a web application or website
tools: Glob, Grep, Read, LS, mcp__playwright-test__browser_click, mcp__playwright-test__browser_close, mcp__playwright-test__browser_console_messages, mcp__playwright-test__browser_drag, mcp__playwright-test__browser_evaluate, mcp__playwright-test__browser_file_upload, mcp__playwright-test__browser_handle_dialog, mcp__playwright-test__browser_hover, mcp__playwright-test__browser_navigate, mcp__playwright-test__browser_navigate_back, mcp__playwright-test__browser_network_requests, mcp__playwright-test__browser_press_key, mcp__playwright-test__browser_run_code, mcp__playwright-test__browser_select_option, mcp__playwright-test__browser_snapshot, mcp__playwright-test__browser_take_screenshot, mcp__playwright-test__browser_type, mcp__playwright-test__browser_wait_for, mcp__playwright-test__planner_setup_page, mcp__playwright-test__planner_save_plan
model: sonnet
color: green
---

You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test
scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage
planning.

You will:

1. **Navigate and Explore**
- Invoke the `planner_setup_page` tool once to set up page before using any other tools
- Explore the browser snapshot
- Do not take screenshots unless absolutely necessary
- Use `browser_*` tools to navigate and discover interface
- Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality

2. **Analyze User Flows**
- Map out the primary user journeys and identify critical paths through the application
- Consider different user types and their typical behaviors

3. **Design Comprehensive Scenarios**

Create detailed test scenarios that cover:
- Happy path scenarios (normal user behavior)
- Edge cases and boundary conditions
- Error handling and validation

4. **Structure Test Plans**

Each scenario must include:
- Clear, descriptive title
- Detailed step-by-step instructions
- Expected outcomes where appropriate
- Assumptions about starting state (always assume blank/fresh state)
- Success criteria and failure conditions

5. **Create Documentation**

Submit your test plan using `planner_save_plan` tool.

**Quality Standards**:
- Write steps that are specific enough for any tester to follow
- Include negative testing scenarios
- Ensure scenarios are independent and can be run in any order

**Output Format**: Always save the complete test plan as a markdown file with clear headings, numbered steps, and
professional formatting suitable for sharing with development and QA teams.
9 changes: 9 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
},
"playwright-test": {
"command": "npx",
"args": [
"playwright",
"run-test-mcp-server",
"--config",
"apps/web/playwright.config.ts"
]
Comment on lines +7 to +14
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new playwright-test MCP server is started via npx playwright ..., which may download and run a different Playwright version than the one pinned in the workspace (or fail if the bin isn’t available at the repo root). To keep tooling reproducible, consider invoking the locally installed Playwright via pnpm exec (optionally with --filter @nn-stack/web) instead of npx playwright.

Copilot uses AI. Check for mistakes.
}
}
}
5 changes: 4 additions & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"destroy:dev": "alchemy destroy --stage dev",
"destroy:prod": "alchemy destroy --stage prod",
"destroy": "alchemy destroy",
"test": "vitest run",
"lint": "biome lint --write",
"format": "biome format --write"
},
Expand All @@ -22,11 +23,13 @@
},
"devDependencies": {
"@biomejs/biome": "catalog:",
"@cloudflare/vitest-pool-workers": "catalog:",
"@cloudflare/workers-types": "catalog:",
"@nn-stack/config": "workspace:*",
"@types/node": "catalog:",
"alchemy": "catalog:",
"drizzle-kit": "catalog:",
"typescript": "catalog:"
"typescript": "catalog:",
"vitest": "catalog:"
}
}
52 changes: 52 additions & 0 deletions apps/server/tests/server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import app from '../src/index';
import { describe, it, expect } from 'vitest';

async function rpc(path: string, input?: unknown) {
const urlPath = path.replace(/\./g, '/');
const resp = await app.fetch(
new Request(`http://localhost/rpc/${urlPath}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ json: input }),
}),
);
const raw = (await resp.json()) as { json?: unknown };
return { status: resp.status, body: raw.json };
}

describe('Server smoke test', () => {
it('GET / returns hello message', async () => {
const res = await app.fetch(new Request('http://localhost/'));
expect(res.status).toBe(200);
expect(await res.text()).toBe('Hello nn stack server!');
});

it('GET /rpc/planet/list returns 8 planets', async () => {
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test title says GET /rpc/planet/list..., but the rpc() helper always issues a POST request. Please update the description to match the actual HTTP method to avoid confusion when reading test output.

Suggested change
it('GET /rpc/planet/list returns 8 planets', async () => {
it('POST /rpc/planet/list returns 8 planets', async () => {

Copilot uses AI. Check for mistakes.
const { status, body } = await rpc('planet.list');
expect(status).toBe(200);
const planets = body as Array<{ name: string }>;
expect(planets).toHaveLength(8);
expect(planets[0].name).toBe('Mercury');
});
});

describe('Todos CRUD (D1 integration)', () => {
it('creates and retrieves a todo', async () => {
const { status: createStatus, body: created } = await rpc('todos.createTodo', {
text: 'Test todo',
});
expect(createStatus).toBe(200);

const todo = created as { id: number; text: string; completed: boolean };
expect(todo.text).toBe('Test todo');
expect(todo.completed).toBe(false);

const { status: listStatus, body: todos } = await rpc('todos.getTodos');
expect(listStatus).toBe(200);

const allTodos = todos as Array<{ id: number; text: string }>;
const found = allTodos.find((t) => t.id === todo.id);
expect(found).toBeTruthy();
expect(found!.text).toBe('Test todo');
});
});
15 changes: 15 additions & 0 deletions apps/server/tests/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { env } from 'cloudflare:workers';
import { applyD1Migrations } from 'cloudflare:test';

declare global {
namespace Cloudflare {
interface Env {
DB: D1Database;
KV: KVNamespace;
CORS_ORIGIN: string;
TEST_MIGRATIONS: unknown[];
}
}
}

await applyD1Migrations(env.DB, env.TEST_MIGRATIONS as Parameters<typeof applyD1Migrations>[1]);
Comment on lines +10 to +15
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TEST_MIGRATIONS is typed as unknown[] but then cast to Parameters<typeof applyD1Migrations>[1] at the call site. Prefer typing TEST_MIGRATIONS directly as Parameters<typeof applyD1Migrations>[1] (or the specific migration type) to avoid the unsafe cast.

Suggested change
TEST_MIGRATIONS: unknown[];
}
}
}
await applyD1Migrations(env.DB, env.TEST_MIGRATIONS as Parameters<typeof applyD1Migrations>[1]);
TEST_MIGRATIONS: Parameters<typeof applyD1Migrations>[1];
}
}
}
await applyD1Migrations(env.DB, env.TEST_MIGRATIONS);

Copilot uses AI. Check for mistakes.
44 changes: 44 additions & 0 deletions apps/server/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { cloudflareTest, readD1Migrations } from '@cloudflare/vitest-pool-workers';
import { defineConfig } from 'vitest/config';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default defineConfig(async () => {
const migrationsPath = path.resolve(
__dirname,
'../../packages/db/migrations',
);
const migrations = await readD1Migrations(migrationsPath);

return {
plugins: [
cloudflareTest({
main: './src/index.ts',
miniflare: {
compatibilityDate: '2025-01-01',
compatibilityFlags: ['nodejs_compat'],
bindings: {
CORS_ORIGIN: 'http://localhost:3000',
TEST_MIGRATIONS: migrations,
},
d1Databases: {
DB: {
id: 'test-db',
},
},
kvNamespaces: {
KV: {
id: 'test-kv',
},
},
},
}),
],
test: {
setupFiles: ['./tests/setup.ts'],
},
};
Comment on lines +10 to +43
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This config file is indented with tabs, but the repo standard is 2-space indentation per biome.json. Please format the file to match the configured style.

Suggested change
const migrationsPath = path.resolve(
__dirname,
'../../packages/db/migrations',
);
const migrations = await readD1Migrations(migrationsPath);
return {
plugins: [
cloudflareTest({
main: './src/index.ts',
miniflare: {
compatibilityDate: '2025-01-01',
compatibilityFlags: ['nodejs_compat'],
bindings: {
CORS_ORIGIN: 'http://localhost:3000',
TEST_MIGRATIONS: migrations,
},
d1Databases: {
DB: {
id: 'test-db',
},
},
kvNamespaces: {
KV: {
id: 'test-kv',
},
},
},
}),
],
test: {
setupFiles: ['./tests/setup.ts'],
},
};
const migrationsPath = path.resolve(
__dirname,
'../../packages/db/migrations',
);
const migrations = await readD1Migrations(migrationsPath);
return {
plugins: [
cloudflareTest({
main: './src/index.ts',
miniflare: {
compatibilityDate: '2025-01-01',
compatibilityFlags: ['nodejs_compat'],
bindings: {
CORS_ORIGIN: 'http://localhost:3000',
TEST_MIGRATIONS: migrations,
},
d1Databases: {
DB: {
id: 'test-db',
},
},
kvNamespaces: {
KV: {
id: 'test-kv',
},
},
},
}),
],
test: {
setupFiles: ['./tests/setup.ts'],
},
};

Copilot uses AI. Check for mistakes.
});
7 changes: 7 additions & 0 deletions apps/web/e2e/seed.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { test, expect } from '@playwright/test';

test.describe('Test group', () => {
test('seed', async ({ page }) => {
Comment on lines +1 to +4
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This placeholder seed test will be executed by Playwright (it matches *.spec.ts) but doesn’t perform any setup and includes unused imports. If the intent is a shared seed helper for generated tests, consider moving it to a non-test module (e.g. e2e/seed.ts) or marking it test.skip() so it doesn’t run in CI.

Suggested change
import { test, expect } from '@playwright/test';
test.describe('Test group', () => {
test('seed', async ({ page }) => {
import { test } from '@playwright/test';
test.describe('Test group', () => {
test.skip('seed', async () => {

Copilot uses AI. Check for mistakes.
// generate code here.
});
Comment on lines +1 to +6
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seed.spec.ts currently contains a placeholder test that does nothing (and a placeholder comment). If this file is meant to bootstrap state for generated tests, implement the seeding/setup or mark the test as skipped with an explanation; otherwise it becomes a misleading always-green test in CI.

Suggested change
import { test, expect } from '@playwright/test';
test.describe('Test group', () => {
test('seed', async ({ page }) => {
// generate code here.
});
import { test } from '@playwright/test';
test.describe('Test group', () => {
test.skip(
'seed',
'Pending implementation: this file is reserved for E2E seed/setup logic and should not be an always-green placeholder test.',
async ({ page }) => {
// Intentionally skipped until real seed/setup steps are implemented.
}
);

Copilot uses AI. Check for mistakes.
});
16 changes: 16 additions & 0 deletions apps/web/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { test, expect } from '@playwright/test';

test.describe('Smoke tests', () => {
test('homepage loads', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/nn-stack|Next/i);
});

test('hello API route returns JSON', async ({ request }) => {
const res = await request.get('/api/hello');
expect(res.ok()).toBeTruthy();
const body = await res.json();
expect(body.message).toBe('Hello from Next.js on Cloudflare Workers!');
expect(body.timestamp).toBeDefined();
});
Comment on lines +4 to +15
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is indented with tabs, which conflicts with the repo’s Biome formatter settings (2 spaces). Please run the formatter or adjust indentation to avoid style CI issues.

Suggested change
test('homepage loads', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/nn-stack|Next/i);
});
test('hello API route returns JSON', async ({ request }) => {
const res = await request.get('/api/hello');
expect(res.ok()).toBeTruthy();
const body = await res.json();
expect(body.message).toBe('Hello from Next.js on Cloudflare Workers!');
expect(body.timestamp).toBeDefined();
});
test('homepage loads', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/nn-stack|Next/i);
});
test('hello API route returns JSON', async ({ request }) => {
const res = await request.get('/api/hello');
expect(res.ok()).toBeTruthy();
const body = await res.json();
expect(body.message).toBe('Hello from Next.js on Cloudflare Workers!');
expect(body.timestamp).toBeDefined();
});

Copilot uses AI. Check for mistakes.
});
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"destroy:dev": "alchemy destroy --stage dev",
"destroy:prod": "alchemy destroy --stage prod",
"destroy": "alchemy destroy",
"test:e2e": "playwright test",
"lint": "biome lint --write",
"format": "biome format --write",
"start": "next start"
Expand Down Expand Up @@ -37,6 +38,7 @@
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@playwright/test": "catalog:",
"alchemy": "catalog:",
"typescript": "catalog:"
}
Expand Down
24 changes: 24 additions & 0 deletions apps/web/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { defineConfig } from '@playwright/test';

export default defineConfig({
testDir: './e2e',
timeout: 30000,
retries: 1,
expect: { timeout: 10000 },
use: {
baseURL: 'http://localhost:3000',
headless: true,
video: 'retain-on-failure',
trace: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
],
webServer: {
command: 'pnpm run dev',
cwd: '../../',
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webServer.command: 'pnpm run dev' with cwd: '../../' starts the root recursive dev script (all workspaces), which is heavier than needed for E2E and can introduce port conflicts / unrelated logs. Prefer starting only the web app dev server (e.g. running the apps/web dev script via pnpm --filter @nn-stack/web dev or equivalent).

Suggested change
cwd: '../../',
cwd: '.',

Copilot uses AI. Check for mistakes.
url: 'http://localhost:3000',
reuseExistingServer: true,
timeout: 60000,
Comment on lines +17 to +22
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Playwright webServer.command runs pnpm run dev from the repo root, which (per root package.json) starts all workspace dev servers via pnpm -r dev. This can slow E2E runs and may introduce port conflicts/unrelated failures; consider starting only the web app (and any required deps) via pnpm --filter @nn-stack/web dev or using the existing root dev:web script instead.

Copilot uses AI. Check for mistakes.
},
Comment on lines +4 to +23
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file uses tab indentation, but the repo Biome config enforces 2-space indentation (biome.jsonformatter.indentStyle: "space", indentWidth: 2). Running biome format (or adjusting indentation) will prevent formatting churn and CI style failures.

Suggested change
testDir: './e2e',
timeout: 30000,
retries: 1,
expect: { timeout: 10000 },
use: {
baseURL: 'http://localhost:3000',
headless: true,
video: 'retain-on-failure',
trace: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
],
webServer: {
command: 'pnpm run dev',
cwd: '../../',
url: 'http://localhost:3000',
reuseExistingServer: true,
timeout: 60000,
},
testDir: './e2e',
timeout: 30000,
retries: 1,
expect: { timeout: 10000 },
use: {
baseURL: 'http://localhost:3000',
headless: true,
video: 'retain-on-failure',
trace: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { browserName: 'chromium' } },
],
webServer: {
command: 'pnpm run dev',
cwd: '../../',
url: 'http://localhost:3000',
reuseExistingServer: true,
timeout: 60000,
},

Copilot uses AI. Check for mistakes.
});
3 changes: 3 additions & 0 deletions apps/web/specs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Specs

This is a directory for test plans.
Loading