grahamcrackers/skills

playwright

Playwright patterns for end-to-end testing including page object model, fixtures, locators, assertions, API testing, visual regression, and CI integration.

First seen Feb 28, 2026

Installation

$ npx skills add grahamcrackers/skills --skill playwright

Summary

  • Playwright patterns for end-to-end testing including page object model, fixtures, locators, assertions, API testing, visual regression, and CI integration.
  • Use when writing E2E tests, setting up Playwright, creating page objects, testing user flows, or when the user asks about end-to-end testing, browser testing, or Playwright configuration.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from grahamcrackers/skills · top by installs.

npx skills add grahamcrackers/skills

Browse all from grahamcrackers/skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

License LICENSE
Default branch main
Open issues 0
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 9,504 B
  • docs SUMMARY.md 361 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 7 installs

SKILL.md

Playwright Best Practices

Setup

npm init playwright@latest
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
    testDir: "./e2e",
    fullyParallel: true,
    forbidOnly: !!process.env.CI,
    retries: process.env.CI ? 2 : 0,
    workers: process.env.CI ? 1 : undefined,
    reporter: process.env.CI ? "github" : "html",
    use: {
        baseURL: "http://localhost:3000",
        trace: "on-first-retry",
        screenshot: "only-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 14"] } },
    ],
    webServer: {
        command: "npm run dev",
        url: "http://localhost:3000",
        reuseExistingServer: !process.env.CI,
    },
});

Locators

Use user-facing locators for resilient tests:

// Preferred (resilient)
page.getByRole("button", { name: "Submit" });
page.getByLabel("Email");
page.getByPlaceholder("Search...");
page.getByText("Welcome back");
page.getByTestId("user-avatar"); // last resort

// Avoid (brittle)
page.locator(".btn-primary");
page.locator("#submit-btn");
page.locator("div > span:nth-child(2)");

Locator Priority

  1. getByRole — buttons, links, headings, inputs (most accessible)
  2. getByLabel — form fields
  3. getByPlaceholder — inputs without visible labels
  4. getByText — non-interactive elements
  5. getByTestId — when no better option exists

Filtering and Chaining

page.getByRole("listitem").filter({ hasText: "Product 1" });
page.getByRole("list").getByRole("listitem").first();
page.getByRole("dialog").getByRole("button", { name: "Confirm" });

Assertions

Playwright auto-waits for assertions to pass:

// Element assertions
await expect(page.getByText("Success")).toBeVisible();
await expect(page.getByRole("button")).toBeEnabled();
await expect(page.getByLabel("Email")).toHaveValue("[email protected]");
await expect(page.getByRole("alert")).toContainText("Saved");
await expect(page.getByRole("listitem")).toHaveCount(5);

// Page assertions
await expect(page).toHaveURL("/dashboard");
await expect(page).toHaveTitle("Dashboard");

Never use manual waitForTimeout — use auto-waiting assertions and locators instead.

Page Object Model

Encapsulate page interactions in classes:

// e2e/pages/login-page.ts
import { type Locator, type Page, expect } from "@playwright/test";

export class LoginPage {
    readonly emailInput: Locator;
    readonly passwordInput: Locator;
    readonly submitButton: Locator;
    readonly errorMessage: Locator;

    constructor(private page: Page) {
        this.emailInput = page.getByLabel("Email");
        this.passwordInput = page.getByLabel("Password");
        this.submitButton = page.getByRole("button", { name: "Sign in" });
        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.submitButton.click();
    }

    async expectError(message: string) {
        await expect(this.errorMessage).toContainText(message);
    }
}

Fixtures

Extend the base test with reusable setup:

// e2e/fixtures.ts
import { test as base } from "@playwright/test";
import { LoginPage } from "./pages/login-page";
import { DashboardPage } from "./pages/dashboard-page";

type Fixtures = {
    loginPage: LoginPage;
    dashboardPage: DashboardPage;
    authenticatedPage: Page;
};

export const test = base.extend<Fixtures>({
    loginPage: async ({ page }, use) => {
        await use(new LoginPage(page));
    },
    dashboardPage: async ({ page }, use) => {
        await use(new DashboardPage(page));
    },
    authenticatedPage: async ({ page }, use) => {
        await page.goto("/login");
        await page.getByLabel("Email").fill("[email protected]");
        await page.getByLabel("Password").fill("password");
        await page.getByRole("button", { name: "Sign in" }).click();
        await page.waitForURL("/dashboard");
        await use(page);
    },
});

export { expect } from "@playwright/test";

Use in tests:

import { test, expect } from "./fixtures";

test("login with valid credentials", async ({ loginPage, page }) => {
    await loginPage.goto();
    await loginPage.login("[email protected]", "password123");
    await expect(page).toHaveURL("/dashboard");
});

test("dashboard shows user data", async ({ authenticatedPage }) => {
    await expect(authenticatedPage.getByText("Welcome")).toBeVisible();
});

Writing Tests

Structure

import { test, expect } from "./fixtures";

test.describe("User Registration", () => {
    test("creates account with valid data", async ({ page }) => {
        await page.goto("/register");
        await page.getByLabel("Name").fill("Alice");
        await page.getByLabel("Email").fill("[email protected]");
        await page.getByLabel("Password").fill("securePass123");
        await page.getByRole("button", { name: "Create Account" }).click();

        await expect(page).toHaveURL("/dashboard");
        await expect(page.getByText("Welcome, Alice")).toBeVisible();
    });

    test("shows error for duplicate email", async ({ page }) => {
        await page.goto("/register");
        await page.getByLabel("Email").fill("[email protected]");
        await page.getByLabel("Password").fill("password123");
        await page.getByRole("button", { name: "Create Account" }).click();

        await expect(page.getByRole("alert")).toContainText("already exists");
    });
});

Principles

  • Each test is independent — no shared state between tests.
  • Test user-visible behavior, not implementation.
  • Use auto-waiting — no sleep() or waitForTimeout().
  • One logical flow per test.

API Testing

Test API endpoints directly:

test("API creates a user", async ({ request }) => {
    const response = await request.post("/api/users", {
        data: { name: "Alice", email: "[email protected]" },
    });

    expect(response.ok()).toBeTruthy();
    const user = await response.json();
    expect(user.name).toBe("Alice");
});

API Setup in Tests

Seed data via API before UI tests:

test.beforeEach(async ({ request }) => {
    await request.post("/api/test/reset");
    await request.post("/api/users", {
        data: { name: "Test User", email: "[email protected]" },
    });
});

Authentication State

Save and reuse auth state to avoid logging in every test:

// e2e/auth.setup.ts
import { test as setup } from "@playwright/test";

const authFile = "e2e/.auth/user.json";

setup("authenticate", async ({ page }) => {
    await page.goto("/login");
    await page.getByLabel("Email").fill("[email protected]");
    await page.getByLabel("Password").fill("password");
    await page.getByRole("button", { name: "Sign in" }).click();
    await page.waitForURL("/dashboard");
    await page.context().storageState({ path: authFile });
});
// playwright.config.ts
projects: [
  { name: "setup", testMatch: /.*\.setup\.ts/ },
  {
    name: "chromium",
    dependencies: ["setup"],
    use: { storageState: "e2e/.auth/user.json" },
  },
],

Visual Regression

test("homepage matches snapshot", async ({ page }) => {
    await page.goto("/");
    await expect(page).toHaveScreenshot("homepage.png", {
        maxDiffPixelRatio: 0.01,
    });
});

test("button variants", async ({ page }) => {
    await page.goto("/storybook/button");
    await expect(page.getByTestId("button-group")).toHaveScreenshot();
});

Update snapshots: npx playwright test --update-snapshots.

CI Integration

# .github/workflows/e2e.yml
jobs:
    e2e:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4
            - uses: actions/setup-node@v4
              with: { node-version: 20, cache: "npm" }
            - run: npm ci
            - run: npx playwright install --with-deps
            - run: npx playwright test
            - uses: actions/upload-artifact@v4
              if: ${{ !cancelled() }}
              with:
                  name: playwright-report
                  path: playwright-report/
                  retention-days: 7

File Structure

e2e/
├── fixtures.ts          # custom test fixtures
├── pages/               # page objects
│   ├── login-page.ts
│   └── dashboard-page.ts
├── auth.setup.ts        # authentication setup
├── .auth/               # stored auth state (gitignored)
├── tests/
│   ├── auth.spec.ts
│   ├── dashboard.spec.ts
│   └── users.spec.ts
└── playwright.config.ts