From jm-claude-plugin
End-to-end testing with Playwright for TypeScript/React applications. Use this when setting up Playwright, writing E2E specs, building Page Object Models, choosing selectors, configuring projects (Chromium/Firefox/WebKit), or wiring E2E into CI. Trigger on: "add E2E test", "Playwright setup", "test the login flow", "POM", "data-testid", any critical user-flow test (login/signup/checkout/payment), or any flaky-E2E investigation.
How this skill is triggered — by the user, by Claude, or both
Slash command
/jm-claude-plugin:e2etestingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Staff Engineer standard: E2E tests cover **critical flows only** — they're expensive, so spend them where unit tests can't reach.
references/advanced/authentication-flows.mdreferences/advanced/authentication.mdreferences/advanced/clock-mocking.mdreferences/advanced/mobile-testing.mdreferences/advanced/multi-context.mdreferences/advanced/multi-user.mdreferences/advanced/network-advanced.mdreferences/advanced/third-party.mdreferences/architecture/pom-vs-fixtures.mdreferences/architecture/test-architecture.mdreferences/architecture/when-to-mock.mdreferences/browser-apis/browser-apis.mdreferences/browser-apis/iframes.mdreferences/browser-apis/service-workers.mdreferences/browser-apis/websockets.mdreferences/core/annotations.mdreferences/core/assertions-waiting.mdreferences/core/configuration.mdreferences/core/fixtures-hooks.mdreferences/core/global-setup.mdStaff Engineer standard: E2E tests cover critical flows only — they're expensive, so spend them where unit tests can't reach.
✅ USE FOR: ❌ DON'T USE FOR:
- Critical user flows - Logic unit tests (use Vitest)
(login, signup, checkout, payment) - Component-level tests (use RTL)
- Cross-browser regressions - Pure functions (use Vitest)
- Auth + session persistence - Anything you can mock cheaply
- Visual regression (snapshots)
Rule of thumb: if breakage would page someone at 2am, it deserves an E2E test.
pnpm create playwright@latest
tests/e2e/
├── playwright.config.ts # Config (browsers, base URL, retries)
├── fixtures/ # Test data, auth helpers
├── pages/ # Page Object Models
│ ├── login.page.ts
│ └── dashboard.page.ts
└── specs/ # Test files
├── auth.spec.ts
└── checkout.spec.ts
POMs separate what the page is from what the test does. Without POMs, refactoring a form selector means editing every spec.
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests/e2e/specs",
timeout: 30_000,
retries: process.env.CI ? 2 : 0, // retry only in CI — local flakes need fixing
workers: process.env.CI ? 1 : undefined,
reporter: [["html"], ["list"]],
use: {
baseURL: process.env.BASE_URL ?? "http://localhost:3000",
screenshot: "only-on-failure",
video: "retain-on-failure",
trace: "on-first-retry",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
],
});
Why these defaults:
retries: 2 in CI hides infra flakes (network, cold start); locally 0 forces you to fix the spectrace: "on-first-retry" — full timeline + DOM snapshots, only when needed (large files)workers: 1 in CI avoids race conditions on shared backend state// pages/login.page.ts
import { Page, Locator, expect } from "@playwright/test";
export class LoginPage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(private readonly page: Page) {
this.emailInput = page.getByTestId("email-input");
this.passwordInput = page.getByTestId("password-input");
this.submitButton = page.getByRole("button", { name: /sign in/i });
this.errorMessage = page.getByTestId("error-message");
}
async goto() {
await this.page.goto("/login");
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectError(message: string | RegExp) {
await expect(this.errorMessage).toContainText(message);
}
}
Rules:
login()), not mechanics (clickAndType())// specs/auth.spec.ts
import { test, expect } from "@playwright/test";
import { LoginPage } from "../pages/login.page";
test.describe("Authentication", () => {
test("successful login redirects to dashboard", async ({ page }) => {
const login = new LoginPage(page);
await login.goto();
await login.login("[email protected]", "validPassword123");
await expect(page).toHaveURL("/dashboard");
await expect(page.getByRole("heading", { name: /welcome/i })).toBeVisible();
});
test("invalid credentials show error", async ({ page }) => {
const login = new LoginPage(page);
await login.goto();
await login.login("[email protected]", "wrongPassword");
await login.expectError(/invalid credentials/i);
});
});
// ✅ BEST — stable, intentional
page.getByTestId("submit-button");
// ✅ GOOD — accessibility-friendly, survives copy changes via name regex
page.getByRole("button", { name: /submit/i });
// ⚠️ OK — for unique static text
page.getByText("Submit order");
// ❌ AVOID — couples test to styling
page.locator(".btn-primary-xl");
// ❌ AVOID — couples test to DOM structure
page.locator("form > div:nth-child(3) > button");
Pattern: add data-testid to interactive elements as you build them — retrofitting tests later is twice the work.
// playwright.config.ts
projects: [
{
name: "setup",
testMatch: /global\.setup\.ts/,
},
{
name: "chromium",
use: { ...devices["Desktop Chrome"], storageState: "playwright/.auth/user.json" },
dependencies: ["setup"],
},
],
// global.setup.ts — runs once, persists session
import { test as setup } from "@playwright/test";
setup("authenticate", async ({ page }) => {
await page.goto("/login");
await page.getByTestId("email-input").fill(process.env.TEST_EMAIL!);
await page.getByTestId("password-input").fill(process.env.TEST_PASSWORD!);
await page.getByRole("button", { name: /sign in/i }).click();
await page.waitForURL("/dashboard");
await page.context().storageState({ path: "playwright/.auth/user.json" });
});
Cuts spec time by ~80% on auth-heavy suites.
pnpm exec playwright test # All specs, all browsers
pnpm exec playwright test auth.spec.ts # Single file
pnpm exec playwright test --headed # Watch the browser
pnpm exec playwright test --debug # Inspector + step-through
pnpm exec playwright test --ui # Time-travel UI (best DX)
pnpm exec playwright show-report # HTML report after run
# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps
- run: pnpm exec playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
Artifacts on failure only — passing runs don't need 50MB of video evidence.
1. Reproduce locally with --repeat-each=10
2. Open trace: pnpm exec playwright show-trace trace.zip
3. Common culprits:
- waitForSelector instead of web-first assertions (expect().toBeVisible())
- Network races — use page.waitForResponse() or route().fulfill()
- Animation timing — disable via CSS or wait for stable state
- Shared backend state — isolate via test-scoped users/data
Rule: a flake fixed by retry is a bug deferred, not solved.
references/ holds the full Playwright best-practices library:
| Subdir | Contents |
|---|---|
core/ | Locators, assertions, fixtures, page objects |
browser-apis/ | Network interception, file uploads, dialogs, geolocation |
testing-patterns/ | Auth, data isolation, visual testing, API mocking |
debugging/ | Trace viewer, inspector, video, slow-mo |
architecture/ | Multi-project setup, custom workers, shared state |
frameworks/ | React/Vue/Angular-specific patterns |
infrastructure-ci-cd/ | Docker, GitHub Actions, sharding, parallelism |
advanced/ | Custom reporters, component testing, Playwright API |
[ ] data-testid on every interactive element
[ ] POM per page — no raw locators in specs
[ ] storageState reused for auth-required tests
[ ] Web-first assertions (expect().toBeVisible()) — not waitFor*
[ ] Specs cover critical flows only (login/checkout/payment)
[ ] CI runs against ephemeral test users, not prod data
[ ] Failure artifacts uploaded (screenshots, video, trace)
[ ] No skipped/disabled specs without an open ticket link
npx claudepluginhub jjmendezrodriguez/jm-claude-plugin --plugin jm-claude-pluginProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.