E2e Test
Write and run a full Playwright E2E test suite for the current feature branch
Claude CodeGeneric
---
description: Write and run a full Playwright E2E test suite for the current feature branch
argument-hint: <feature description> - e.g. "document tagging", "file sharing", "workspace settings"
allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, Agent]
---
# Write & Run E2E Tests for a Feature
You are writing a comprehensive Playwright end-to-end test suite for the following feature:
**$ARGUMENTS**
## Your Task
1. **Understand the feature** — Read the code changes on the current branch to understand what was built
2. **Write the test file** — Create a Playwright test at `e2e/<feature-name>.spec.ts`
3. **Run the tests** — Execute them and iterate until they all pass
4. **Report results** — Show the user the final test output and any screenshots
## Step 1: Understand What Changed
Before writing any tests, understand the feature:
```bash
# See what files changed on this branch vs main
git diff main --name-only
git diff main --stat
Read the changed files to understand:
- What new UI components were added?
- What new API routes/tRPC endpoints were added?
- What new database tables or schema changes exist?
- What user-facing flows were introduced?
Step 2: Write the Test
Create e2e/<feature-name>.spec.ts following these patterns exactly.
Project Context
- Stack: Next.js 16 + tRPC + Drizzle ORM + React 19 + Shadcn UI
- Config:
playwright.config.ts— testDir:./e2e, baseURL:http://localhost:3000, single chromium browser, serial execution, 30s timeout - Dev server: Configured to auto-start via
pnpm devbut reuses existing server if running
Test Structure Template
import { test, expect, type Page } from "@playwright/test";
const TEST_USER = {
name: "Feature Test User",
email: `feature-test-${Date.now()}@example.com`,
password: "TestPassword123!",
};
test.describe.serial("Feature name flows", () => {
// First test MUST register, onboard, and set up test data
test("setup: register and create test data", async ({ page }) => {
// Register
await page.goto("/register");
await page.getByPlaceholder("Your name").fill(TEST_USER.name);
await page.getByPlaceholder("you@example.com").fill(TEST_USER.email);
await page.getByPlaceholder("Choose a password").fill(TEST_USER.password);
await page.getByRole("button", { name: /create account/i }).click();
// Onboard — create workspace (REQUIRED for new users)
await page.waitForURL("/onboarding", { timeout: 15000 });
await page.getByPlaceholder("e.g. Acme Inc").fill("Test Workspace");
await page.getByRole("button", { name: /create workspace/i }).click();
await page.waitForURL(/\/w\//, { timeout: 15000 });
await page.waitForTimeout(1000);
// ... set up test data (upload files, create folders, etc.) ...
});
// Subsequent tests log in and exercise the feature
test("test name", async ({ page }) => {
await loginAs(page);
// ... test steps ...
});
});
// ── Helpers ──────────────────────────────────────────────────────────────
async function loginAs(page: Page) {
await page.goto("/login");
await page.getByPlaceholder("you@example.com").fill(TEST_USER.email);
await page.getByPlaceholder("Enter password").fill(TEST_USER.password);
await page.getByRole("button", { name: /sign in/i }).click();
await page.waitForURL(/\/w\//, { timeout: 15000 });
await page.waitForTimeout(1000);
}
async function openFileContextMenu(page: Page, fileName: string) {
const row = page.locator("div.grid", { hasText: fileName }).first();
await row.hover();
await page.waitForTimeout(300);
const menuBtn = row.locator("button").last();
await menuBtn.click({ force: true });
await page.waitForTimeout(300);
}
Critical Rules
Test lifecycle:
- ALL tests use
test.describe.serial()— they share state and run in order - First test registers a fresh user (unique email via
Date.now()) and creates a workspace - Subsequent tests call
loginAs(page)which navigates to/loginand waits for/w/redirect - If one test fails, all subsequent tests are skipped (serial dependency)
Locator strategies (in priority order):
page.getByRole("button", { name: /text/i })— buttons, links, headings, menu itemspage.getByPlaceholder("placeholder text")— form inputs (works with React controlled inputs)page.locator('[data-slot="dialog-content"]')— dialog wrapperspage.locator('[data-sidebar="menu-button"]', { hasText: "Label" })— sidebar nav itemspage.locator("div.grid", { hasText: "filename" }).first()— file/folder rows in file explorerpage.getByText("exact text")— visible text content (use.first()if ambiguous)
NEVER do these:
- NEVER use
input[value="..."]selectors — React controlled inputs don't reflect value in DOM attributes - NEVER use
page.getByText()for text that appears in both a heading and description — usepage.getByRole("heading", { name: "..." })instead - NEVER use
{ hasText: "Tags" }when a workspace is named "Tags Test Workspace" — use{ name: "Tags", exact: true }or a more specific locator - NEVER assume dialogs auto-close — explicitly press
Escapeor click a close/done button
Timing patterns:
waitForTimeout(300)— after hover (dropdown triggers need time to appear)waitForTimeout(500)— after dialog open/close animationswaitForTimeout(1000)— after page navigation or login (async state settling)waitForTimeout(2000)— after mutations that update the list (API round-trip)waitForTimeout(3000)— after file upload completion- Always use
{ timeout: 5000 }ontoBeVisible()assertions for dynamic content - Always use
{ timeout: 15000 }onwaitForURL()for page navigation
File upload pattern:
await page
.getByRole("button", { name: /^upload$/i })
.first()
.click();
await page.waitForTimeout(500);
const fileInput = page.locator(
'[data-slot="dialog-content"] input[type="file"]',
);
await fileInpu
Maintain E2e Test?
Let people know it's listed here — add the badge (live metrics, light/dark aware) or a plain link to your README or docs.
[](https://getagentictools.com/loops/zmeyer44-write-run-e2e-tests-for-a-feature?ref=badge)