- Skill ID
- petrkindlmann/qa-skills/playwright-automation
- Publisher
- petrkindlmann
- Repository
- qa-skills
- Installs
- 0
- Files
- 9
- License
- MIT
- Synced
- Sep 23, 2026
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:
- TypeScript or JavaScript? TypeScript is strongly recommended — it catches locator and assertion mistakes at compile time, and every example here assumes it.
- 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.
- Existing suite or fresh start? Migrating from Cypress/Selenium, rewrite the flakiest tests first; never big-bang. Changes the sequencing entirely.
- Single site or multi-site? Multi-site needs shared fixtures and per-site config objects — see
references/multi-site-architecture.md.
Core Principles
- User-facing locators first.
getByRole>getByLabel>getByTestId> CSS (last resort). Locators must reflect what the user sees, not how the DOM is structured. Seereferences/selector-strategies.md. - 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.
- Test isolation. Each test gets a fresh
BrowserContext. Tests must never depend on other tests' state or execution order. - Parallel by default, serial only when necessary. Use
fullyParallel: true. Reservetest.describe.serialfor flows that genuinely cannot be isolated (rare). - Fixtures for setup, not hooks. Fixtures compose, provide type safety, and tear down automatically. Prefer them over
beforeEach/afterEachfor anything non-trivial. Seereferences/fixtures-and-projects.md.
Calibrate to your team maturity (setteam_maturityin.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 artifactsplaywright.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');
});More from petrkindlmann
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.
