E2etesting
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.From its SKILL.md
npx -y skills add jjmendezrodriguez/jm-claude-plugin --skill e2etestingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
8.9 KB, as published. Nobody here has run it
E2E Testing — Playwright / TypeScript
Staff Engineer standard: E2E tests cover critical flows only — they're expensive, so spend them where unit tests can't reach.
1. When to Reach for Playwright
✅ 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.
2. Installation & Structure
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.
3. Configuration
// 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: 2in CI hides infra flakes (network, cold start); locally0forces you to fix the spectrace: "on-first-retry"— full timeline + DOM snapshots, only when needed (large files)workers: 1in CI avoids race conditions on shared backend state
4. Page Object Model
// 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:
- One POM per page or major section
- Locators live in the constructor — refactor-friendly
- Methods describe user intent (
login()), not mechanics (clickAndType())
5. Writing Specs
// 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);
});
});
6. Selector Priority
// ✅ 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.
7. Auth Reuse — Avoid Logging In Every Test
// 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.
8. Running Tests
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
9. CI Integration
# .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.
10. Flaky Test Triage
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.
11. Deep-Dive References
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 |
12. Quick Checklist
[ ] 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