Playwright E2E Testing (TypeScript)
Comprehensive toolkit for end-to-end testing of web applications using Playwright with TypeScript. Enables robust UI testing, UI-dependent API setup, and responsive design verification following best practices.
Activation: This skill is triggered when authoring or maintaining versioned Playwright UI specs and their test infrastructure.
When to Use This Skill
- Write E2E tests for user flows, forms, navigation, and authentication
- UI-dependent API setup via the
requestfixture or network interception - Responsive testing across mobile, tablet, and desktop viewports
- Debug flaky tests using traces, screenshots, videos, and Playwright Inspector
- Setup test infrastructure with Page Object Model and fixtures
- Mock/intercept APIs for isolated, deterministic testing
- Visual regression testing with screenshot comparisons
Do NOT Use For
- Standalone API/contract testing with no browser (use
api-testing). - Driving a live browser interactively for exploration or debugging (use
playwright-cli). - Governing a large regression suite, tiers, or CI sharding strategy (use
playwright-regression-testing). - Selenium/Java browser automation (use
webapp-selenium-testing).
Prerequisites
| Requirement | Details |
|---|---|
| Node.js | v18+ recommended |
| Package Manager | npm, yarn, or pnpm |
| Playwright | @playwright/test package |
| TypeScript | typescript + ts-node (optional but recommended) |
| Browsers | Installed via npx playwright install |
Quick Setup
bash# Initialize new project npm init playwright@latest # Or add to existing project npm install -D @playwright/test npx playwright install
First Questions to Ask
Before writing tests, clarify:
- App URL: Local dev server command + port, or staging URL?
- Critical flows: Which user journeys must be covered (happy path + error states)?
- Browsers/devices: Chrome, Firefox, Safari? Mobile viewports?
- API strategy: Real backend, mocked responses, or hybrid?
- Test data: Seed data available? Reset/cleanup strategy?
Core Principles
1. Test Runner & TypeScript
Always use @playwright/test with TypeScript for type safety and better IDE support.
typescriptimport { test, expect } from "@playwright/test"; test("user can login", async ({ page }) => { await page.goto("/login"); await page.getByLabel("Email").fill("user@test.com"); await page.getByLabel("Password").fill("password123"); await page.getByRole("button", { name: "Sign in" }).click(); await expect(page).toHaveURL(/.*dashboard/); });
2. Locator Strategy (Priority Order)
Prefer role-based locators (getByRole) with accessible names, then label → placeholder → text → test ID → CSS (last resort). XPath is never used.
➡️ Full priority hierarchy, role reference, and examples: Locator Strategies: Priority — the single source of truth.
3. Auto-Waiting & Web-First Assertions
Playwright auto-waits for elements. Never use sleep() or arbitrary timeouts.
typescript// [ok] Web-first assertions (auto-retry) await expect(page.getByRole("alert")).toBeVisible(); await expect(page).toHaveURL(/dashboard/); await expect(page.getByTestId("status")).toHaveText("Success!"); // [no] Avoid manual waits await page.waitForTimeout(2000); // Bad practice
4. Test Structure with Steps
Use test.step() for readable reports and failure localization:
typescripttest("checkout flow", async ({ page }) => { await test.step("Add item to cart", async () => { await page.goto("/products/1"); await page.getByRole("button", { name: "Add to Cart" }).click(); }); await test.step("Complete checkout", async () => { await page.goto("/checkout"); await page.getByRole("button", { name: "Pay Now" }).click(); }); await test.step("Verify confirmation", async () => { await expect(page.getByRole("heading")).toContainText("Order Confirmed"); }); });
Key Workflows
Forms & Navigation
typescript// Form submit and wait for navigation (auto-waiting) await page.getByRole("button", { name: "Login" }).click(); await expect(page).toHaveURL(/.*dashboard/); // Form with API response validation const responsePromise = page.waitForResponse( (r) => r.url().includes("/api/login") && r.status() === 200, ); await page.getByRole("button", { name: "Login" }).click(); const response = await responsePromise;
API Testing (Request Fixture)
typescripttest("API health check", async ({ request }) => { const response = await request.get("/api/health"); expect(response.ok()).toBeTruthy(); expect(await response.json()).toMatchObject({ status: "ok" }); });
API Mocking & Interception
typescripttest("handles API error", async ({ page }) => { await page.route("**/api/users", (route) => route.fulfill({ status: 500, body: JSON.stringify({ error: "Server error" }), }), ); await page.goto("/users"); await expect(page.getByRole("alert")).toContainText("Something went wrong"); });
Responsive Testing
typescriptconst viewports = [ { width: 375, height: 667, name: "mobile" }, { width: 768, height: 1024, name: "tablet" }, { width: 1280, height: 720, name: "desktop" }, ]; for (const vp of viewports) { test(`navigation works on ${vp.name}`, async ({ page }) => { await page.setViewportSize(vp); await page.goto("/"); // Mobile: hamburger menu if (vp.width < 768) { await page.getByRole("button", { name: /menu/i }).click(); } await page.getByRole("link", { name: "About" }).click(); await expect(page).toHaveURL(/about/); }); }
Configuration
Use playwright.config.ts for project-wide settings:
typescriptimport { defineConfig, devices } from "@playwright/test"; export default defineConfig({ testDir: "./tests", retries: process.env.CI ? 2 : 0, reporter: [["html"], ["junit", { outputFile: "results.xml" }]], use: { baseURL: "http://localhost:3000", trace: "on-first-retry", screenshot: "only-on-failure", video: "retain-on-failure", }, projects: [ { name: "chromium", use: devices["Desktop Chrome"] }, { name: "mobile", use: devices["Pixel 5"] }, ], webServer: { command: "npm run dev", url: "http://localhost:3000", reuseExistingServer: !process.env.CI, }, });
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Element not found | Wrong locator or not rendered | Use PWDEBUG=1 to inspect, verify with getByRole |
| Timeout waiting | Element hidden or slow load | Check for overlays, increase timeout, use waitFor() |
| Flaky tests | Race conditions, animations | Add test.step(), use proper waits, disable animations |
| Strict mode violation | Multiple elements match | Use .first(), .filter(), or more specific locator |
| Screenshots differ | Dynamic content | Mask dynamic areas, use deterministic data |
| CI fails, local passes | Environment differences | Check baseURL, timeouts, webServer config |
| API mock not working | Route pattern mismatch | Use **/api/... glob, verify with page.on('request') |
CLI Quick Reference
| Command | Description |
|---|---|
npx playwright test | Run all tests headless |
npx playwright test --ui | Open UI mode (interactive) |
npx playwright test --headed | Run with visible browser |
npx playwright test --debug | Run with Playwright Inspector |
npx playwright test -g "login" | Run tests matching pattern |
npx playwright test --project=chromium | Run specific project |
npx playwright show-report | Open HTML report |
npx playwright codegen | Generate tests by recording |
PWDEBUG=1 npx playwright test | Debug with Inspector |
DEBUG=pw:api npx playwright test | Verbose API logging |
Red Flags
- CSS/XPath locators when a role/label/testId is available — brittle and breaks on refactor.
waitForTimeout/ manual sleeps instead of web-first auto-retrying assertions.- Tests sharing state and depending on execution order — flaky and order-coupled.
- Assertions only on status/URL with no visible-state check — hides render regressions.
- Inline page setup repeated across tests instead of fixtures — duplication and drift.
References
| Document | Content |
|---|---|
| Snippets: Setup | Config, auth setup, custom fixtures & logging |
| Snippets: Interactions | Form interactions, API testing & network interception |
| Snippets: Viewports & Auth | Responsive viewports & authentication patterns |
| Snippets: Assertions & Debug | Assertions, debug commands & utility helpers |
| Locator Strategies: Priority | Locator priority hierarchy & role-based locators |
| Locator Strategies: Text | Label, text, placeholder, alt-text & test-ID locators |
| Locator Strategies: Filtering | Filtering, chaining & complex locator patterns |
| Locator Strategies: Anti & Debug | Anti-patterns, CSS last-resort, debugging & quick reference |
| POM: Basics | POM concepts, directory structure, base page & fluent API |
| POM: Components | Page object & reusable component object implementation |
| POM: Fixtures | Custom & authenticated page-object fixtures |
| POM: Practices | Best practices, anti-patterns & a complete worked example |
| Debugging: Tools & UI | Debugging tools, UI mode, Inspector & headed mode |
| Debugging: Tracing & Logs | Trace viewer, verbose logging, screenshots & videos |
| Debugging: Errors & Network | Console/page errors & network debugging |
| Debugging: Flaky & Locators | Flaky-test fixes, locator debugging & quick commands |
Verification
- Uses custom fixture injection — No
new PageObject()calls in spec files; all POMs injected via fixtures - Locators use recommended strategies — All locators use
getByRole(),getByTestId(), orgetByText(); no CSS selectors for interactive elements - Tests are independent — Each test sets up and tears down its own state; no
beforeAllwith shared mutable state - Error states covered — At least one test verifies error/empty/loading states alongside happy path

