
E2e Testing Patterns
Give your coding agent Playwright-ready E2E patterns—config, page objects, and CI settings—so solo builders can add reliable browser tests without reinventing structure.
Overview
e2e-testing-patterns is an agent skill most often used in Ship (also Build) that teaches Playwright E2E configuration and Page Object Model examples for reliable browser tests.
Install
npx skills add https://github.com/wshobson/agents --skill e2e-testing-patternsWhat is this skill?
- Full playwright.config.ts template with chromium, firefox, webkit, and iPhone 13 project profiles
- Page Object Model pattern with typed Locators and role/label-based selectors
- CI-oriented defaults: forbidOnly, retries, single worker, HTML and JUnit reporters
- Trace on-first-retry, screenshot only-on-failure, and video retain-on-failure capture
- Parallel fullyParallel runs with configurable expect and test timeouts
- 4 browser projects in sample config: chromium, firefox, webkit, mobile iPhone 13
Adoption & trust: 17k installs on skills.sh; 36.5k GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
You want automated browser tests but your agent keeps generating inconsistent Playwright setup and flaky selectors.
Who is it for?
Solo builders adding or standardizing Playwright E2E suites on Next.js, Vite, or similar web stacks before shipping.
Skip if: Teams that only need unit tests, pure API contract testing without a browser, or non-Playwright frameworks without adaptation.
When should I use this skill?
User asks to add, structure, or improve Playwright end-to-end tests, page objects, or e2e CI configuration.
What do I get? / Deliverables
You get reusable Playwright config and page-object scaffolding aligned with CI retries, multi-browser projects, and failure artifacts.
- playwright.config.ts baseline
- Page Object Model module examples
- e2e test directory conventions
Recommended Skills
Journey fit
Spans multiple journey phases - primary shelf plus alternate fits below.
End-to-end testing is the canonical Ship-phase quality gate before release; patterns land here first even when you scaffold tests during Build. The skill is organized around test authoring and execution workflows, which maps directly to the testing subphase rather than security or launch prep.
Where it fits
Scaffold page objects while implementing a new checkout UI so selectors stay centralized.
Drop in playwright.config.ts with trace and video-on-failure before merging a release branch.
Extend existing POM classes when fixing regressions reported from production-like staging runs.
How it compares
Use as a pattern library inside the repo—not as a one-shot “write my tests” prompt without reading your actual routes and selectors.
Common Questions / FAQ
Who is e2e-testing-patterns for?
Indie and solo developers shipping web or hybrid apps who use AI coding agents to implement Playwright tests with maintainable structure.
When should I use e2e-testing-patterns?
During Ship when hardening release quality, and during Build when you first add an e2e folder—especially before CI gates on main.
Is e2e-testing-patterns safe to install?
Review the Security Audits panel on this Prism page before installing; the skill content is documentation patterns and does not define its own network calls.
SKILL.md
READMESKILL.md - E2e Testing Patterns
# e2e-testing-patterns — detailed patterns and worked examples ## Playwright Patterns ### Setup and Configuration ```typescript // playwright.config.ts import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ testDir: "./e2e", timeout: 30000, expect: { timeout: 5000, }, fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, 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: "firefox", use: { ...devices["Desktop Firefox"] } }, { name: "webkit", use: { ...devices["Desktop Safari"] } }, { name: "mobile", use: { ...devices["iPhone 13"] } }, ], }); ``` ### Pattern 1: Page Object Model ```typescript // pages/LoginPage.ts import { Page, Locator } from "@playwright/test"; export class LoginPage { readonly page: Page; readonly emailInput: Locator; readonly passwordInput: Locator; readonly loginButton: Locator; readonly errorMessage: Locator; constructor(page: Page) { this.page = page; this.emailInput = page.getByLabel("Email"); this.passwordInput = page.getByLabel("Password"); this.loginButton = page.getByRole("button", { name: "Login" }); this.errorMessage = page.getByRole("alert"); } 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.loginButton.click(); } async getErrorMessage(): Promise<string> { return (await this.errorMessage.textContent()) ?? ""; } } // Test using Page Object import { test, expect } from "@playwright/test"; import { LoginPage } from "./pages/LoginPage"; test("successful login", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login("user@example.com", "password123"); await expect(page).toHaveURL("/dashboard"); await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible(); }); test("failed login shows error", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login("invalid@example.com", "wrong"); const error = await loginPage.getErrorMessage(); expect(error).toContain("Invalid credentials"); }); ``` ### Pattern 2: Fixtures for Test Data ```typescript // fixtures/test-data.ts import { test as base } from "@playwright/test"; type TestData = { testUser: { email: string; password: string; name: string; }; adminUser: { email: string; password: string; }; }; export const test = base.extend<TestData>({ testUser: async ({}, use) => { const user = { email: `test-${Date.now()}@example.com`, password: "Test123!@#", name: "Test User", }; // Setup: Create user in database await createTestUser(user); await use(user); // Teardown: Clean up user await deleteTestUser(user.email); }, adminUser: async ({}, use) => { await use({ email: "admin@example.com", password: process.env.ADMIN_PASSWORD!, }); }, }); // Usage in tests import { test } from "./fixtures/test-data"; test("user can update profile", async ({ page, testUser }) => { await page.goto("/login"); await page.getByLabel("Email").fill(testUser.email); await page.getByLabel("Password").fill(testUser.password); await page.getByRole("button", { name: "Login" }).click(); await page.goto("/profile"); await page.getByLabel("Name").fill("Updated Name"); await page.getByRole("button", { name: "Save" }).click(); await expect(page.getByText("Profile updated")).toBeVisible(); }); ``` ### Pattern 3: Waiting Strategies ```typescript // ❌ B