Playwright testing
Universal AI development toolkit. 74 production-ready skills for every coding agent. Works with Claude Code, Cursor, Codex.
npx -y skills add medy-gribkov/arcana --skill playwright-testingAssembled 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.
What its author says it does
Copied from the file, not written here
End-to-end testing with Playwright using role-based locators, auto-waiting, network mocking, visual regression, fixtures for test isolation, parallel execution, CI integration, authentication state reuse, and trace viewer debugging. Use when building reliable browser automation tests that catch regressions before production.
SKILL.md
9.3 KB, as published. Nobody here has run it
Locator Strategies
BAD: CSS selectors couple tests to implementation. Breaks on refactors.
await page.locator('.btn-primary.submit-form').click();
await page.locator('#username-input').fill('alice');
GOOD: Semantic locators match user perception. Resilient to markup changes.
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Username').fill('alice');
await page.getByText('Welcome back').waitFor();
await page.getByPlaceholder('Search...').fill('query');
await page.getByTestId('checkout-total').textContent(); // Only when no semantic option
BAD: Fragile global selectors. Breaks with duplicate elements.
await page.getByRole('button', { name: 'Delete' }).click(); // Which delete button?
GOOD: Chain locators to scope within a parent container.
const row = page.getByRole('row', { name: 'Alice' });
await row.getByRole('button', { name: 'Delete' }).click();
await page.getByRole('listitem').filter({ hasText: 'Active' }).first().click();
Auto-Waiting and Assertions
BAD: Hardcoded delays cause flakiness and slow tests.
await page.waitForTimeout(2000);
await page.locator('.spinner').waitFor({ state: 'hidden' });
await page.locator('button').click();
GOOD: Use auto-waiting assertions. Retry until condition is met.
await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();
await expect(page.getByText('Loading...')).not.toBeVisible();
await expect(page.getByLabel('Email')).toHaveValue('[email protected]');
await expect(page.getByRole('checkbox', { name: 'Terms' })).toBeChecked();
await expect(page.getByText('Report')).toBeVisible({ timeout: 30000 }); // Custom timeout
Network Mocking
BAD: Hitting real APIs in tests. Flaky, slow, pollutes production data.
await page.goto('/users');
GOOD: Mock API responses with route.fulfill(). Fast, deterministic.
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
body: JSON.stringify([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]),
});
});
await page.goto('/users');
await expect(page.getByText('Alice')).toBeVisible();
// Conditional mocking
await page.route('**/api/**', async (route) => {
route.request().url().includes('/logout') ? route.fulfill({ status: 200 }) : route.continue();
});
// Speed up tests by blocking resources
await page.route('**/*.{png,jpg,jpeg,webp}', (route) => route.abort());
Visual Regression Testing
await expect(page).toHaveScreenshot('homepage.png');
await expect(page.getByRole('banner')).toHaveScreenshot('header.png'); // Component-level
await expect(page).toHaveScreenshot({ mask: [page.getByText(/Last updated:.*/)] }); // Mask dynamic
Update baselines: npx playwright test --update-snapshots
Fixtures and Test Isolation
BAD: Shared state leaks between tests. Failure in test 1 breaks test 2.
let page;
test.beforeAll(async ({ browser }) => { page = await browser.newPage(); });
test('test 1', async () => { await page.fill('input', 'admin'); });
test('test 2', async () => { /* Still has 'admin' from test 1 */ });
GOOD: Isolated context per test. No side effects.
test('login as admin', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Username').fill('admin');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText('Welcome, admin')).toBeVisible();
});
GOOD: Custom fixtures for reusable setup.
import { test as base } from '@playwright/test';
type Fixtures = { authenticatedPage: Page };
export const test = base.extend<Fixtures>({
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('admin123');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page.getByText('Dashboard')).toBeVisible();
await use(page);
},
});
test('view admin dashboard', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/admin');
await expect(authenticatedPage.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
});
Authentication State Reuse
Avoid logging in for every test. Save auth state once, reuse across tests.
// auth.setup.ts
import { test as setup } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('admin123');
await page.getByRole('button', { name: 'Login' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: 'auth.json' });
});
// test.spec.ts
test.use({ storageState: 'auth.json' });
test('access protected page', async ({ page }) => {
await page.goto('/admin');
await expect(page.getByRole('heading', { name: 'Admin Panel' })).toBeVisible();
});
Configure in playwright.config.ts:
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{ name: 'chromium', use: { storageState: 'auth.json' }, dependencies: ['setup'] },
],
});
Parallel Execution
export default defineConfig({
workers: process.env.CI ? 2 : 4,
fullyParallel: true,
});
// Disable for tests that share global state
test.describe.serial('checkout flow', () => {
test('add item to cart', async ({ page }) => { /* ... */ });
test('proceed to checkout', async ({ page }) => { /* ... */ });
});
// Use unique data per worker to avoid collisions
test('create user', async ({ page }) => {
const workerId = test.info().parallelIndex;
const email = `user${workerId}@example.com`;
await page.getByLabel('Email').fill(email);
});
CI Integration
name: Playwright Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
# Shard tests for faster CI
strategy:
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
- run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
Debugging
npx playwright test --trace on # Record trace, view with show-report
npx playwright test --headed --slowmo=1000 # Watch tests in real-time
npx playwright test --debug # Step through with inspector
npx playwright codegen http://localhost:3000 # Generate selectors
Capture traces on failure:
export default defineConfig({
use: { trace: 'on-first-retry' },
retries: process.env.CI ? 2 : 0,
});
Set breakpoints with page.pause():
test('debug login', async ({ page }) => {
await page.goto('/login');
await page.pause(); // Playwright Inspector opens
await page.getByLabel('Email').fill('[email protected]');
});
Capture console and network logs:
page.on('console', (msg) => msg.type() === 'error' && console.log('Error:', msg.text()));
page.on('request', (req) => console.log('Request:', req.url()));
Page Object Model
BAD: Duplicating selectors and workflows across tests.
test('user can login', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByRole('button', { name: 'Login' }).click();
});
GOOD: Centralize page logic. Tests express intent, not mechanics.
class LoginPage {
constructor(private page: Page) {}
async goto() { await this.page.goto('/login'); }
async login(email: string, password: string) {
await this.page.getByLabel('Email').fill(email);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Login' }).click();
}
async expectWelcome() {
await expect(this.page.getByText('Welcome')).toBeVisible();
}
}
test('user can login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'pass123');
await loginPage.expectWelcome();
});
Troubleshooting
Flaky tests: Enable retries and traces. Use auto-waiting assertions instead of hardcoded delays.
Selector not found: Use npx playwright codegen to validate locators. Prefer semantic locators over CSS.
Timeouts: Increase timeout for slow operations: await expect(page.getByText('Data')).toBeVisible({ timeout: 30000 });
Network mocking not working: Ensure page.route() is called before navigation. Routes apply to requests made after registration.
Parallel tests interfering: Use test.describe.serial() or unique test data per worker.
Gives 0 of the 12 instructions most e2e browser skills give
Counted across 407 of the 410 authors here whose files we hold, read 2026-08-06
- use page object model patternin 35 of 407, across 25 files
- Snapshot to get element refsin 24 of 407, across 14 files
- keep tests independentin 23 of 407, across 18 files
- Interact using refs from the latest snapshotin 23 of 407, across 11 files
- clean up test data after each testin 21 of 407, across 15 files
- test user behavior not implementationin 20 of 407, across 14 files
- quarantine flaky tests explicitlyin 19 of 407, across 10 files
- wait for specific network conditionsin 18 of 407, across 8 files
- re-snapshot after navigation or dom changesin 17 of 407, across 10 files
- Detect running dev servers before writing test codein 17 of 407, across 7 files
- use web-first assertionsin 17 of 407, across 14 files
- capture screenshots or videos on test failurein 17 of 407, across 14 files
Said here and by no other author read
- chain locators to scope within parent containers
- mock api responses with route fulfill
- block unnecessary resources to speed up tests
- create custom fixtures for reusable setup
- use unique test data per parallel worker
- use a page object model to centralize logic
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once.