petrkindlmann/qa-skillsMIT9 files

Playwright Automation

>-

Specification
Skill ID
petrkindlmann/qa-skills/playwright-automation
Publisher
petrkindlmann
Repository
qa-skills
Installs
0
Files
9
License
MIT
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

petrkindlmann/qa-skills/playwright-automationInstalls these files
  • SKILL.md
  • references/anti-patterns.md
  • references/auth-patterns.md
  • references/ci-recipes.md
  • references/debugging-and-triage.md
  • references/fixtures-and-projects.md
  • references/multi-site-architecture.md
  • references/network-and-mocking.md
  • references/selector-strategies.md

What this skill tells the agent

<objective> How an expert agent writes stable, maintainable, production-grade Playwright tests in TypeScript. The failure this prevents: AI agents reflexively reach for the three patterns that produce suites which pass once and flake forever — never use waitForTimeout, never default to CSS selectors, and avoid the legacy page.click() family. This skill encodes the auto-waiting, user-facing-locator, fixture-based discipline that makes a suite survive a refactor. </objective>

Discovery Questions

Check .agents/qa-project-context.md first — if it exists, use it and skip any question answered there. Then ask only what's missing:

  1. TypeScript or JavaScript? TypeScript is strongly recommended — it catches locator and assertion mistakes at compile time, and every example here assumes it.
  2. Which browsers? Chromium for local dev; add Firefox and WebKit in CI. Mobile viewports are separate Playwright projects, not separate test files — they change the device descriptor.
  3. Existing suite or fresh start? Migrating from Cypress/Selenium, rewrite the flakiest tests first; never big-bang. Changes the sequencing entirely.
  4. Single site or multi-site? Multi-site needs shared fixtures and per-site config objects — see references/multi-site-architecture.md.

Core Principles

  1. User-facing locators first. getByRole > getByLabel > getByTestId > CSS (last resort). Locators must reflect what the user sees, not how the DOM is structured. See references/selector-strategies.md.
  2. Auto-waiting — NEVER use `waitForTimeout`. Every Playwright action and web-first assertion auto-waits. If you think you need a timeout, you need a better locator or assertion.
  3. Test isolation. Each test gets a fresh BrowserContext. Tests must never depend on other tests' state or execution order.
  4. Parallel by default, serial only when necessary. Use fullyParallel: true. Reserve test.describe.serial for flows that genuinely cannot be isolated (rare).
  5. Fixtures for setup, not hooks. Fixtures compose, provide type safety, and tear down automatically. Prefer them over beforeEach/afterEach for anything non-trivial. See references/fixtures-and-projects.md.
Calibrate to your team maturity (set team_maturity in .agents/qa-project-context.md): - startup — Chromium only, 5–10 critical-path tests, basic CI run on PR. Skip sharding and visual baselines until the suite is stable. - growing — Chromium + Firefox, POM structure, parallel execution, sharding in CI, HTML report artifacts. - established — Full browser matrix, auth fixtures, API mocking layer, visual regression baseline, trace-on-failure, flakiness tracking.

Project Structure

project-root/
├── playwright.config.ts
├── e2e/
│   ├── fixtures/              # base.fixture.ts, auth.fixture.ts, data.fixture.ts
│   ├── pages/                 # Page objects by feature
│   │   ├── base.page.ts
│   │   ├── dashboard.page.ts
│   │   └── components/        # Reusable component objects (data-table, modal)
│   ├── tests/                 # Test files by feature (auth/, dashboard/, settings/)
│   ├── helpers/               # test-data.ts, api-client.ts
│   └── global-setup.ts
├── .auth/                     # Git-ignored storageState files
└── test-results/              # Git-ignored artifacts

playwright.config.ts

import { defineConfig, devices } from '@playwright/test';

const isCI = !!process.env.CI;
const baseURL = process.env.BASE_URL ?? 'http://localhost:3000';

export default defineConfig({
  testDir: './e2e/tests',
  fullyParallel: true,
  forbidOnly: isCI,
  retries: isCI ? 2 : 0,
  workers: isCI ? '50%' : undefined,
  reporter: isCI
    ? [['blob'], ['github'], ['json', { outputFile: 'test-results/results.json' }]]
    : [['html', { open: 'on-failure' }]],
  use: {
    baseURL,
    trace: isCI ? 'on-first-retry' : 'retain-on-failure',
    screenshot: 'only-on-failure',
    video: isCI ? 'on-first-retry' : 'off',
    navigationTimeout: 30_000,
    // Avoid a global actionTimeout — it can mask a genuinely slow auto-waited
    // action. Set per-action only where a known-slow widget needs it.
  },
  projects: [
    { name: 'setup', testMatch: /global-setup\.ts/, teardown: 'teardown' },
    { name: 'teardown', testMatch: /global-teardown\.ts/ },
    { name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: '.auth/user.json' }, dependencies: ['setup'] },
    { name: 'firefox', use: { ...devices['Desktop Firefox'], storageState: '.auth/user.json' }, dependencies: ['setup'] },
    { name: 'webkit', use: { ...devices['Desktop Safari'], storageState: '.auth/user.json' }, dependencies: ['setup'] },
  ],
  webServer: isCI ? undefined : {
    command: 'npm run dev', url: baseURL, reuseExistingServer: !isCI, timeout: 120_000,
  },
});

The blob reporter in CI is what makes sharded runs mergeable — see the sharding section. The setup project writes storageState once before the browser projects depend on it.

Global setup (storageState)

import { test as setup, expect } from '@playwright/test';

setup('authenticate as default user', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
  await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page).toHaveURL(/.*dashboard/);
  await page.context().storageState({ path: '.auth/user.json' });
});

This is the setup project pattern: the setup project (or a globalSetup file) runs UI login once, and every browser project replays the saved cookies/localStorage via storageState in config. For multi-role auth (admin/user/guest) and token seeding, see references/auth-patterns.md.


Page Object Model

import { type Page, type Locator, expect } from '@playwright/test';

export abstract class BasePage {
  constructor(protected readonly page: Page) {}
  abstract readonly path: string;
  async goto(): Promise<void> {
    await this.page.goto(this.path);
    await this.page.waitForLoadState('domcontentloaded');
  }
}

Component objects represent reusable UI fragments (modals, tables, nav). They take a root Locator, not a Page:

export class DataTable {
  readonly rows: Locator;
  constructor(private readonly root: Locator) {
    this.rows = root.getByRole('row');
  }
  getRowByText(text: string | RegExp): Locator {
    return this.rows.filter({ hasText: text });
  }
}

Compose, don't inherit deep. A page holds its components; it does not extend a five-level hierarchy:

export class UsersPage extends BasePage {
  readonly path = '/admin/users';
  readonly table: DataTable;
  constructor(page: Page) {
    super(page);
    this.table = new DataTable(page.getByRole('table', { name: 'Users' }));
  }
}

Inject page objects via fixtures, not constructors in test files:

export const test = base.extend<{ usersPage: UsersPage }>({
  usersPage: async ({ page }, use) => { await use(new UsersPage(page)); },
});
export { expect } from '@playwright/test';

POM methods return state (locators, values); they do not assert. Assertions live in the test so failures point at the test, not the page object.


Test Patterns

Form interactions with test.step

Wrap logical action groups in test.step() for readable trace-viewer output:

test('submits a multi-step form', async ({ page }) => {
  await page.goto('/onboarding');
  await test.step('fill personal info', async () => {
    await page.getByLabel('First name').fill('Jane');
    await page.getByRole('button', { name: 'Next' }).click();
  });
  await test.step('submit', async () => {
    await page.getByRole('button', { name: 'Complete setup' }).click();
  });
  await expect(page).toHaveURL('/dashboard');
});

Use this skill in a project

Open any RiverX project, search the Skills panel for petrkindlmann/qa-skills/playwright-automation, and install it in one click.

Use this skill