원클릭으로
writing-e2e-tests
// Write Playwright E2E tests with direct DB seeding, API-based login, accessible selectors, and dual-config awareness. Use when creating end-to-end tests, testing user flows, or verifying UI behavior.
// Write Playwright E2E tests with direct DB seeding, API-based login, accessible selectors, and dual-config awareness. Use when creating end-to-end tests, testing user flows, or verifying UI behavior.
Scaffold new Next.js API route handlers with auth guards, Zod validation, error handling, and integration tests. Use when creating new API endpoints, adding CRUD operations, or building route handlers.
Scaffold background jobs with node-cron scheduling, pessimistic locking via acquireJobLock, and instrumentation.ts registration. Use when creating scheduled jobs, adding cron tasks, or implementing background processing.
Scaffold TypeORM EntitySchema definitions with TypeScript interfaces, database migrations, and Zod validation schemas. Use when creating new database entities, adding tables, or modifying existing entity structures.
Write Vitest integration tests against a real PostgreSQL test database with proper mock setup, seed functions, and assertion patterns. Use when creating tests for API routes, lib modules, or database operations.
| name | writing-e2e-tests |
| description | Write Playwright E2E tests with direct DB seeding, API-based login, accessible selectors, and dual-config awareness. Use when creating end-to-end tests, testing user flows, or verifying UI behavior. |
Creates Playwright E2E tests using direct database seeding and API-based authentication — the project's established pattern.
Use the checklist below and track your progress:
Progress:
- [ ] Step 1: Set up the test file structure
- [ ] Step 2: Create seed and cleanup functions
- [ ] Step 3: Write test cases
- [ ] Step 4: Run both configs
Step 1: Set up the test file structure
Create the test file at e2e/{feature-name}.spec.ts:
import { test, expect } from "@playwright/test";
import { seedTestUser, loginViaApi } from "./helpers/auth";
import { getClient } from "./helpers/db";
Key imports:
seedTestUser(username, password) — inserts user with bcrypt-hashed passwordloginViaApi(page, username, password) — POSTs to /api/auth/login to set session cookiegetClient() — returns a pg.Client connected to the test databaseStep 2: Create seed and cleanup functions
Define all seed functions locally in the test file — NOT shared across files:
async function seedConfiguration() {
const client = await getClient();
await client.query(
`INSERT INTO configuration ("apiMode", "entityName", "singletonKey") VALUES ($1, $2, 'GLOBAL')
ON CONFLICT ("singletonKey") DO NOTHING`,
["organisation", "TestOrg"]
);
await client.end();
}
async function seedTeam(name: string): Promise<number> {
const client = await getClient();
const result = await client.query(
`INSERT INTO team (name) VALUES ($1) RETURNING id`, [name]
);
await client.end();
return result.rows[0].id;
}
async function clearAll() {
const client = await getClient();
// Delete in foreign-key dependency order
await client.query("DELETE FROM team_member_snapshot");
await client.query("DELETE FROM team");
await client.query("DELETE FROM session");
await client.query("DELETE FROM app_user");
await client.query("DELETE FROM configuration");
await client.end();
}
Rules:
$1, $2) — never string interpolationclient.end() after each operationclearAll() deletes in FK dependency order (children before parents)ON CONFLICT DO NOTHING for idempotent configuration seedingSet up lifecycle hooks:
test.beforeEach(async () => {
await clearAll();
await seedConfiguration();
await seedTestUser("admin", "password123");
});
test.afterAll(async () => {
await clearAll();
});
Step 3: Write test cases
test("displays empty state when no teams exist", async ({ page }) => {
await loginViaApi(page, "admin", "password123");
await page.goto("/management?tab=teams");
await expect(page.getByText(/no teams found/i)).toBeVisible();
});
test("creates a new team via modal", async ({ page }) => {
await loginViaApi(page, "admin", "password123");
await page.goto("/management?tab=teams");
await page.getByRole("button", { name: "Add Team" }).click();
await page.getByLabel("Team Name").fill("Alpha Team");
await page.getByRole("button", { name: "Create" }).click();
await expect(page.getByText("Alpha Team")).toBeVisible();
});
Locator rules:
getByRole(), getByText(), getByLabel()getByText(/no teams found/i)page.waitForURL() for route transitionsCommon patterns:
await loginViaApi(page, username, password) — always before page.goto()await page.goto("/path") — use absolute paths from rootawait expect(locator).toBeVisible() — use Playwright's auto-waitingStep 4: Run both configs
Both must pass before the feature is complete:
# Credentials-mode tests (excludes azure-login.spec.ts)
npx playwright test
# Azure-mode tests (azure-login.spec.ts only)
npx playwright test --config playwright.azure.config.ts
If the test only applies to credentials mode, no need to worry about the Azure config. But always state which mode the test targets.
writing-integration-tests — complement E2E tests with API-level integration testscreating-api-routes — understand the API routes that E2E tests interact with