Playwright patterns
Skill Inteligentsensingsolutions/tdd-dev-workflow/skills/playwright-patterns
This skill should be used when the user asks about "Playwright API", "Playwright config", "playwright.config.ts", "Playwright fixtures", "Playwright selectors", "Playwright locators", "page.waitFor", "Playwright trace viewer", "Playwright debugging", "Playwright CI setup", "Playwright visual regression", "Playwright codegen", or needs Playwright-specific API patterns, configuration, and tooling. For general E2E test strategy (when to test, what to test, test structure principles), see e2e-testing instead. Provides Playwright implementation patterns.From its SKILL.md
npx -y skills add Inteligentsensingsolutions/tdd-dev-workflow --skill playwright-patternsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
12.5 KB, as published. Nobody here has run it
Playwright Patterns
Setup & Configuration
Define a single playwright.config.ts at the project root:
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: 'mobile', use: { ...devices['iPhone 14'] } },
],
webServer: {
command: 'pnpm dev',
port: 3000,
reuseExistingServer: !process.env.CI,
},
});
Key Configuration Decisions
fullyParallel: true-- run tests in parallel by default; isolate state per test.forbidOnly: !!process.env.CI-- prevent.only()from leaking into CI.retries: 2in CI -- absorb transient failures while flagging persistent ones.webServer-- auto-start the dev server; skip if already running locally.
Test Structure
Basic Test
import { test, expect } from '@playwright/test';
test.describe('Feature Name', () => {
test('should do X when Y', async ({ page }) => {
await page.goto('/path');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Success')).toBeVisible();
});
});
Naming Conventions
- Group related tests with
test.describe('Feature Name', ...). - Name tests as
'should [expected behavior] when [condition]'. - Mirror user stories:
'should display error when credentials are invalid'.
Selector Priority
Choose selectors in this order of preference:
| Priority | Method | When to Use |
|---|---|---|
| 1 | getByRole | Buttons, links, headings, form elements with ARIA roles |
| 2 | getByLabel | Form inputs with associated labels |
| 3 | getByText | Static content, messages, headings without roles |
| 4 | getByTestId | Elements with explicit data-testid attributes |
| 5 | CSS selectors | Last resort only; fragile and couples tests to markup |
// Preferred
page.getByRole('button', { name: 'Submit' })
page.getByLabel('Email address')
page.getByText('Registration successful')
page.getByTestId('user-avatar')
// Avoid
page.locator('.btn-primary.mt-3')
page.locator('div > form > button:nth-child(2)')
Page Object Model
Encapsulate page interactions in classes. Tests read as user stories, and selector changes propagate from one place.
// e2e/pages/login.page.ts
import { type Page, type Locator } from '@playwright/test';
export class LoginPage {
private readonly emailInput: Locator;
private readonly passwordInput: Locator;
private readonly submitButton: Locator;
constructor(private page: Page) {
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
}
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 getErrorMessage() {
return this.page.getByRole('alert').textContent();
}
}
Fixtures
Extend the base test with custom fixtures to inject page objects and shared state:
// 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;
};
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
});
export { expect } from '@playwright/test';
Use in tests:
import { test, expect } from './fixtures';
test('user can log in', async ({ loginPage, dashboardPage }) => {
await loginPage.goto();
await loginPage.login('[email protected]', 'password');
await expect(dashboardPage.heading).toBeVisible();
});
Waiting & Assertions
Auto-Waiting
Playwright auto-waits for elements to be actionable before performing actions. Do not add manual waits before clicks, fills, or checks.
Assertion Retries
expect(locator) assertions retry automatically until timeout:
await expect(page.getByText('Welcome')).toBeVisible();
await expect(page.getByRole('list')).toHaveCount(5);
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveTitle(/Dashboard/);
Waiting for Network
Wait for specific API responses when tests depend on server data:
const responsePromise = page.waitForResponse(
resp => resp.url().includes('/api/users') && resp.ok()
);
await page.getByRole('button', { name: 'Load' }).click();
await responsePromise;
Hard Rule
Never use page.waitForTimeout(). Always wait for a specific condition -- visible element, URL change, or network response.
Visual Regression
Capture and compare screenshots to detect unintended visual changes:
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixelRatio: 0.01,
});
await expect(page.getByTestId('chart')).toHaveScreenshot('chart.png', {
maxDiffPixelRatio: 0.02,
});
Update baselines after intentional UI changes:
npx playwright test --update-snapshots
Store baseline images in version control under the test directory.
API Testing
Use Playwright's built-in request context for complementary API tests:
test('API returns user list', async ({ request }) => {
const response = await request.get('/api/users');
expect(response.ok()).toBeTruthy();
const data = await response.json();
expect(data.users).toHaveLength(3);
});
Combine UI and API testing in the same suite for full-stack validation.
Debugging
Interactive Debug Mode
npx playwright test --debug
Opens the Playwright Inspector with step-through controls and selector highlighting.
Trace Viewer
npx playwright test --trace on
npx playwright show-trace trace.zip
Traces capture DOM snapshots, network requests, and console logs for every action. Use trace: 'on-first-retry' in config to capture traces only for retried failures.
In-Test Breakpoints
await page.pause(); // Opens Inspector at this point
Headed Mode
npx playwright test --headed
Watch tests run in a visible browser window.
HTML Report
npx playwright show-report
Interactive report with screenshots, traces, and step-by-step execution details.
CI Integration
Install Dependencies
npx playwright install --with-deps
Install browsers and OS-level dependencies in CI. Run once, cache the result.
Headless Execution
Playwright runs headless by default. No configuration needed for CI.
Artifact Upload
Upload traces and screenshots on failure for post-mortem debugging:
# GitHub Actions example
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-traces
path: test-results/
retention-days: 7
Test Sharding
Split tests across CI workers for faster pipelines:
npx playwright test --shard=1/3
npx playwright test --shard=2/3
npx playwright test --shard=3/3
Dev Server Management
Single Server
Use the webServer config to auto-start and auto-stop the dev server:
webServer: {
command: 'pnpm dev',
port: 3000,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
Multiple Servers
For frontend + backend, use an array:
webServer: [
{ command: 'pnpm dev:api', port: 4000, reuseExistingServer: !process.env.CI },
{ command: 'pnpm dev:web', port: 3000, reuseExistingServer: !process.env.CI },
],
Playwright starts them in order and waits for each port to respond.
Network Interception
Mock API responses to test UI behavior independently of the backend:
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ users: [{ id: 1, name: 'Mock User' }] }),
});
});
Modify Responses
Intercept and modify real responses to test edge cases:
await page.route('**/api/config', async (route) => {
const response = await route.fetch();
const body = await response.json();
body.featureFlags.newFeature = true;
await route.fulfill({ response, body: JSON.stringify(body) });
});
Abort Requests
Block analytics, tracking, or other non-essential requests:
await page.route('**/analytics/**', (route) => route.abort());
Test Data Management
Isolation Principle
Each test must create its own data and clean up after itself. Never rely on data from other tests or a shared database state.
API-Based Seeding
Seed data via API calls in beforeEach for speed and reliability:
test.beforeEach(async ({ request }) => {
await request.post('/api/test/seed', {
data: { scenario: 'user-with-orders' },
});
});
test.afterEach(async ({ request }) => {
await request.post('/api/test/cleanup');
});
Storage State for Auth
Save and reuse authenticated browser state across tests to avoid repeated login:
// Save state after login
await page.context().storageState({ path: 'e2e/.auth/user.json' });
// Reuse in config
use: { storageState: 'e2e/.auth/user.json' }
Anti-Patterns
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
page.waitForTimeout(3000) | Arbitrary; slows tests or causes flakes | Wait for specific conditions |
| Brittle CSS selectors | Break on styling or markup changes | Use getByRole, getByLabel, getByTestId |
| Testing third-party UIs | Outside your control; breaks unpredictably | Mock at the API boundary |
Skipping webServer config | Manual server management; inconsistent CI | Use webServer in config |
| Ignoring flaky tests | Erodes trust in the test suite | Investigate root cause; use retries sparingly |
| Shared mutable state between tests | Test order dependence; parallel failures | Isolate state per test |
Screenshots without maxDiffPixelRatio | Font rendering differences cause false failures | Set a small tolerance |
| Asserting internal JS state | Couples tests to implementation details | Assert visible UI outcomes only |
Quick Reference
| Concern | Approach |
|---|---|
| Selectors | getByRole > getByLabel > getByText > getByTestId |
| Waits | Auto-wait + expect().toBeVisible() + waitForResponse() |
| Page encapsulation | Page Object Model classes |
| Reusable setup | Custom fixtures via test.extend<>() |
| Visual testing | toHaveScreenshot() with maxDiffPixelRatio |
| Debugging | --debug, --headed, page.pause(), trace viewer |
| CI | --shard, headless, upload trace artifacts |
| Config | Single playwright.config.ts with webServer |
References
- For the complete Playwright API reference (selectors, actions, waiting, assertions, network, visual testing, mobile, debugging, CI/CD, accessibility), read
references/api-reference.md. - For common Playwright recipes and advanced patterns, read
references/common-recipes.md.
Dependencies
- testing-patterns -- foundational test structure, factory functions, assertion patterns, and TDD workflow integration.
- e2e-testing -- end-to-end testing principles, when to write E2E tests, and integration with the TDD development cycle.
- typescript-pro (transitive via testing-patterns) -- type safety for page objects, fixtures, and test utilities.
Used By
None -- this is a top-layer integration skill.