agent-browser/skills/playwright/SKILL.md
Use to write or set up Playwright / @playwright/test E2E tests — test specs, locators, assertions, fixtures, config. Focuses on structuring tests with high-precision locator strategies. Not for live page exploration or debugging (use agent-browser or web-test).
npx skillsauth add musingfox/cc-plugins playwrightInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Write browser tests using @playwright/test, the official Playwright test runner. It provides auto-waiting, test isolation, built-in web assertions with auto-retry, and parallel execution out of the box.
Prefer @playwright/test for all testing scenarios. Use library mode (playwright) only for direct browser scripting needs: console error capture, network inspection, or custom automation outside a test context.
# Initialize Playwright in a project
npm init playwright@latest
# Or add to existing project
npm install -D @playwright/test
npx playwright install
Essential playwright.config.ts (for full multi-browser, reporter, and webServer config, see references/api-patterns.md):
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
});
Tests follow the pattern: tests/<feature>.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Feature Name', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should perform expected behavior', async ({ page }) => {
// Arrange
const submitButton = page.getByRole('button', { name: 'Submit' });
// Act
await submitButton.click();
// Assert
await expect(page.getByText('Success')).toBeVisible();
});
});
Always use the AAA pattern: Arrange (locate elements), Act (interact), Assert (verify outcome).
Apply locators in strict priority order — always prefer the highest-precision option available:
| Priority | Locator | When to Use | Stability |
|----------|---------|-------------|-----------|
| 1 | getByTestId('id') | Element has data-testid attribute | Highest — immune to text/structure changes |
| 2 | getByRole('role', { name }) | Element has clear ARIA role and accessible name | Very high — maps from accessibility tree |
| 3 | getByLabel('label') | Form inputs with associated <label> | High — tied to label text |
| 4 | getByPlaceholder('text') | Input with placeholder text | Medium-high — placeholder may change |
| 5 | getByText('text', { exact: true }) | Visible text content | Medium — text may change |
| 6 | page.locator('[data-attr="val"]') | Custom data attributes | Medium — depends on attribute stability |
| 7 | page.locator('css') | Last resort — specific CSS selector | Low — fragile to DOM restructuring |
Rules:
nth(0)) unless testing a list where index is semantically meaningful.{ exact: true } to getByText() to prevent partial matches.getByRole, getByLabel) — they match what users see and interact with..filter({ hasText: 'unique' }) or scope to a parent.For locator disambiguation patterns (filter, chaining, scoping), see references/api-patterns.md.
Playwright's expect API auto-retries assertions until timeout (default 5s):
// Visibility
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
// Text content
await expect(locator).toHaveText('exact text');
await expect(locator).toContainText('partial');
// Input values
await expect(locator).toHaveValue('input value');
// Attributes
await expect(locator).toHaveAttribute('href', '/path');
// Count
await expect(locator).toHaveCount(3);
// Element state
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toBeChecked();
// Page-level
await expect(page).toHaveURL(/\/dashboard/);
await expect(page).toHaveTitle('Dashboard');
Critical: Always use await expect(locator).toHaveText() — not expect(await locator.textContent()).toBe(). Web assertions auto-retry; manual extraction does not.
// Click
await page.getByRole('button', { name: 'Save' }).click();
// Fill (clears existing content first)
await page.getByLabel('Email').fill('[email protected]');
// Type sequentially (appends, triggers key events)
await page.getByLabel('Search').pressSequentially('query');
// Select dropdown
await page.getByLabel('Country').selectOption('US');
// Check / uncheck
await page.getByLabel('Agree to terms').check();
// Keyboard
await page.keyboard.press('Enter');
await page.keyboard.press('Escape');
// File upload
await page.getByLabel('Upload').setInputFiles('path/to/file.pdf');
// Wait for navigation
await page.waitForURL('/dashboard');
test.describe('Suite', () => {
test.beforeAll(async () => {
// Run once before all tests in this suite
});
test.beforeEach(async ({ page }) => {
// Run before each test — common setup (e.g., navigation)
});
test.afterEach(async ({ page }) => {
// Run after each test — cleanup
});
test.afterAll(async () => {
// Run once after all tests in this suite
});
});
Extend the test object with reusable setup logic:
import { test as base, Page } from '@playwright/test';
const test = base.extend<{ authenticatedPage: 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);
},
});
test('should show user profile', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/profile');
await expect(authenticatedPage.getByRole('heading', { name: 'Profile' })).toBeVisible();
});
npx playwright test # Run all tests
npx playwright test login.spec.ts # Run specific file
npx playwright test --headed # Visible browser
npx playwright test --ui # Interactive UI mode
npx playwright test --debug # Step-through debugger
npx playwright show-report # View HTML report
Use playwright (not @playwright/test) when direct browser control is needed outside a test runner:
import { chromium } from 'playwright';
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
// Console error capture
page.on('console', msg => {
if (msg.type() === 'error') console.log('Console error:', msg.text());
});
// Network request inspection
page.on('response', response => {
if (response.status() >= 400)
console.log(`Failed: ${response.status()} ${response.url()}`);
});
// JavaScript execution in page context
const title = await page.evaluate(() => document.title);
// Cookie inspection
const cookies = await context.cookies();
await browser.close();
Use library mode for: console error capture, network inspection, page.evaluate(), iframe/shadow DOM exploration, cookie/localStorage checks.
For detailed locator mapping, assertion catalog, and advanced configuration:
references/api-patterns.md — Locator disambiguation patterns, assertion quick reference, advanced fixtures and configurationdata-ai
Unified entry point for Obsidian daily-note captures and long-form notes. Triggers on "記一下 / log / 紀錄 / capture this / 寫到 journal" (→ cap mode) and "建立筆記 / new note / 寫一份筆記 / create a note on" (→ note mode). Also via `/obw:cap` and `/obw:note`. Requires `.obsidian.yaml`.
tools
Use the `gog` CLI to operate Google Workspace — Gmail (read/search/send/labels/drafts), Calendar (events/RSVP/freebusy/focus-time/out-of-office), and Drive (list/search/upload/ download/share/move). Triggers on any Gmail, inbox, email, calendar, agenda, meeting, schedule, RSVP, Drive, Google Doc/Sheet/Slides, file share, or upload/download request.
documentation
Interactively create .obsidian.yaml for a project and install starter templates (task / doc / adr) into the vault's Templates folder. Skips templates that already exist; never overwrites.
tools
Manage project hook-guard installation — set up, diagnose, or update Claude Code hooks, git pre-commit, and commit-msg scripts with security checks, code-quality gates, and CLAUDECODE skip logic. Triggers on "set up hooks", "configure pre-commit", "add linting hooks", "initialize hook-guard", "check hooks", "hook doctor", "verify hook setup", "troubleshoot hooks", "update hooks", "regenerate hooks", "sync hooks with current tools", or similar requests.