com um clique
browser-automation
// Playwright browser automation via MCP. Covers E2E testing, UI review, web scraping, screenshot capture, and general browser interaction. MCP-first — CLI is fallback only.
// Playwright browser automation via MCP. Covers E2E testing, UI review, web scraping, screenshot capture, and general browser interaction. MCP-first — CLI is fallback only.
Session bootstrap + workflows for Pathfinder semantic navigation tools. Covers: discovery protocol, tool chaining patterns (explore, impact, audit, debug), search optimization, LSP degraded mode, and error recovery.
Safe command execution: input sanitization, timeout handling, output capture, error propagation. For spawning processes, shell commands, system calls.
Git conventions: conventional commits, branch naming, PR hygiene, release tagging.
Structured incident workflow: severity classification, triage, diagnosis, mitigation, postmortem, and prevention. Template-driven with blameless review.
Constructs, validates, and traverses a Directed Acyclic Graph (DAG) from scope cards for safe level-based parallel dispatch. Determines execution order via topological sort. Detects cycles and invalid dependencies.
Decomposes broad tasks into MECE, parallelizable sub-tasks with explicit scope cards. Core skill for intra-domain parallel dispatch. Produces scope cards consumed by parallel-dispatch-dag, parallel-dispatch-ownership, and parallel-dispatch-merge skills.
| name | browser-automation |
| description | Playwright browser automation via MCP. Covers E2E testing, UI review, web scraping, screenshot capture, and general browser interaction. MCP-first — CLI is fallback only. |
PRIMARY — MCP tools (always prefer):
mcp_playwright_browser_snapshot — read page structure, get element refsmcp_playwright_browser_navigate — go to URLmcp_playwright_browser_click — click elementmcp_playwright_browser_type — type into elementmcp_playwright_browser_fill_form — fill multiple form fieldsmcp_playwright_browser_select_option — dropdown selectionmcp_playwright_browser_hover — hover elementmcp_playwright_browser_drag — drag and dropmcp_playwright_browser_drop — drop files/data onto elementmcp_playwright_browser_press_key — keyboard inputmcp_playwright_browser_evaluate — execute JS on pagemcp_playwright_browser_wait_for — wait for text/disappearance/timemcp_playwright_browser_console_messages — read console outputmcp_playwright_browser_network_requests — list network activitymcp_playwright_browser_network_request — inspect single requestmcp_playwright_browser_file_upload — upload filesmcp_playwright_browser_handle_dialog — accept/dismiss dialogsmcp_playwright_browser_tabs — manage tabsmcp_playwright_browser_resize — change viewportmcp_playwright_browser_run_code_unsafe — arbitrary Playwright code (escape hatch)FALLBACK — CLI via run_command (only when MCP lacks capability):
npx playwright install)NEVER mix MCP and CLI for the same session. One approach per workflow.
Every browser interaction follows this loop:
mcp_playwright_browser_snapshot to understand current stateRules:
ref="e5") for targeting — not CSS selectorsdepth param to limit snapshot size for large pagestarget param to scope snapshot to a subtreeWhen writing test code or run_code_unsafe scripts, prefer selectors in this order:
Role-based (most resilient, mirrors user experience)
page.getByRole('button', { name: 'Submit' })
page.getByRole('textbox', { name: 'Email' })
page.getByLabel('Password')
page.getByText('Sign in')
page.getByPlaceholder('Enter your email')
page.getByTitle('Close dialog')
If you can't locate an element by role/label, it signals an accessibility gap in the component.
Test ID attributes (stable, decoupled from UI)
page.getByTestId('submit-button') // uses data-testid by default
page.locator('[data-testid="submit-button"]') // explicit attribute form
Semantic HTML (acceptable)
page.locator('button[type="submit"]')
page.locator('input[name="email"]')
CSS class/ID (avoid — fragile, changes with styling)
page.locator('.btn-primary') // AVOID
page.locator('#submit') // AVOID
Complex CSS/XPath (last resort — brittle)
page.locator('div.container > form > button') // FRAGILE
mcp_playwright_browser_wait_for with text or textGonewaitForURL, waitForSelector, waitForLoadState in test codepage.locator('.el').waitFor({ state: 'visible' }) for explicit waitspage.waitForTimeout(N) — arbitrary delays cause flakinesswaitForLoadState('networkidle') — unreliable, race-prone, slowpage.waitForTimeout as a "fix" for timing issuestoBeVisible() (don't re-assert the text)toHaveText(), toContainText()toHaveValue(), toBeEmpty()toBeChecked() / not.toBeChecked()toMatchAriaSnapshot() for partial page structureawait expect(page).toHaveTitle('My App');
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator('.message')).toBeVisible();
await expect(page.locator('.spinner')).toBeHidden();
await expect(page.locator('button')).toBeEnabled();
await expect(page.locator('input')).toHaveValue('test@example.com');
await expect(page.locator('.item')).toHaveCount(5);
Use mcp_playwright_browser_run_code_unsafe to set up route handlers:
async (page) => {
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Test User' }])
});
});
}
async (page) => {
await page.route('**/api/data', route => route.abort('internetdisconnected'));
}
// Abort reasons: connectionrefused, timedout, connectionreset, internetdisconnected
async (page) => {
await page.route('**/api/user', async route => {
const response = await route.fetch();
const json = await response.json();
json.isPremium = true;
await route.fulfill({ response, json });
});
}
async (page) => {
await page.route('**/*.{png,jpg,jpeg,gif,svg}', route => route.abort());
}
When a browser interaction fails or produces unexpected results:
mcp_playwright_browser_snapshot
mcp_playwright_browser_console_messages level="error"
mcp_playwright_browser_network_requests
mcp_playwright_browser_evaluate function="() => document.title"
Common failure causes:
CI artifact retention — always configure Playwright to save traces, screenshots, and videos on failure:
// playwright.config.ts
use: {
trace: 'on-first-retry', // trace on first failure, not every run
screenshot: 'only-on-failure',
video: 'retain-on-failure',
}
Each test file contains exactly one test() call. This ensures:
tests/e2e/<group>/<kebab-case-scenario>.spec.ts
import { test, expect } from '@playwright/test';
test.describe('<Group Name>', () => {
test('<scenario description>', async ({ page }) => {
// 1. Navigate
await page.goto('/path');
// 2. Act
await page.getByRole('textbox', { name: 'Email' }).fill('user@test.com');
await page.getByRole('button', { name: 'Submit' }).click();
// 3. Assert
await expect(page).toHaveURL(/.*success/);
await expect(page.getByRole('heading')).toContainText('Welcome');
});
});
Authenticate once per suite run via API — reuse across all tests. Never log in via UI per test.
// tests/auth.setup.ts
import { test as setup } from '@playwright/test';
setup('authenticate', async ({ request, page }) => {
// Prefer API auth — faster, no UI dependency
const response = await request.post('/api/auth/login', {
data: { email: process.env.TEST_EMAIL, password: process.env.TEST_PASSWORD }
});
// OR via UI login if API not available
// await page.goto('/login'); ...
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { storageState: 'playwright/.auth/user.json' },
dependencies: ['setup'],
},
],
});
Rules:
playwright/.auth/ to .gitignore — contains live session tokensprocess.env for credentials — never hardcodetestInfo.parallelIndex to map each worker to a separate test accountrequest fixture to create/destroy data via backend endpoints (fast, no UI dependency)crypto.randomUUID() or faker-js to prevent collisions in parallel runs// fixtures.ts
export const test = base.extend<{ createdUser: User }>({
createdUser: async ({ request }, use) => {
// Setup
const user = await request.post('/api/users', {
data: { email: `test-${crypto.randomUUID()}@example.com` }
}).then(r => r.json());
// Inject into test
await use(user);
// Teardown — runs even if test fails
await request.delete(`/api/users/${user.id}`);
},
});
beforeEach for setup not used by every testimport { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true, // tests within a file run concurrently
forbidOnly: !!process.env.CI, // fail CI if test.only left in source
retries: process.env.CI ? 2 : 0, // retry flaky tests on CI only
workers: process.env.CI ? 4 : undefined,
reporter: process.env.CI ? 'blob' : 'html', // blob enables shard merging
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry', // trace on first failure — not every run
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
Sharding for large suites (horizontal CI scaling):
# Run shard 1 of 4 — spread across 4 CI nodes
npx playwright test --shard=1/4
# Merge shard reports after all complete
npx playwright merge-reports ./blob-reports
Use toHaveScreenshot() for pixel-level regression detection. Baselines are committed to VCS.
test('homepage layout', async ({ page }) => {
await page.goto('/');
// First run creates baseline; subsequent runs diff against it
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100, // tolerate minor rendering differences
threshold: 0.2,
mask: [page.locator('.timestamp'), page.locator('.ad-banner')], // hide dynamic content
});
});
// Element-scoped screenshot (preferred over full-page for components)
await expect(page.locator('.chart')).toHaveScreenshot('chart.png');
Rules:
npx playwright test --update-snapshotsreducedMotion: 'reduce'toMatchAriaSnapshot)Built into Playwright. Validates accessibility tree — catches semantic changes axe-core misses.
await expect(page.locator('nav')).toMatchAriaSnapshot(`
- navigation:
- link "Home"
- link "Products"
- link "Sign in"
`);
@axe-core/playwright)Catches WCAG violations: missing labels, contrast failures, duplicate IDs.
import { AxeBuilder } from '@axe-core/playwright';
test('no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toHaveLength(0);
});
Axe-core does not verify keyboard usability — must be explicit:
// Verify tab order and focus trap in modals
await page.keyboard.press('Tab');
await expect(page.locator(':focus')).toHaveAttribute('data-testid', 'first-focusable');
// Escape dismisses modal
await page.keyboard.press('Escape');
await expect(page.locator('[role="dialog"]')).toBeHidden();
Rule: if role-based locators can't find an element during regular test writing, flag it — it means the component has an accessibility gap.
browserContext.request — shares cookies with browserUse when API action must be reflected immediately in the UI:
test('create post then see it', async ({ page, context }) => {
// API call shares session with the browser context
await context.request.post('/api/posts', { data: { title: 'Test' } });
await page.reload();
await expect(page.getByText('Test')).toBeVisible();
});
request.newContext() — fully isolated API contextUse for pure API/contract testing with no browser:
test('POST /api/users returns 201', async ({ playwright }) => {
const apiContext = await playwright.request.newContext({
baseURL: process.env.API_URL,
extraHTTPHeaders: { Authorization: `Bearer ${process.env.API_TOKEN}` }
});
const response = await apiContext.post('/api/users', {
data: { email: 'test@example.com' }
});
expect(response.status()).toBe(201);
const body = await response.json();
expect(body).toMatchObject({ email: 'test@example.com' });
await apiContext.dispose();
});
test('newly created item appears in list', async ({ request, page }) => {
// Fast API seed
await request.post('/api/items', { data: { name: 'Widget' } });
// Verify UI
await page.goto('/items');
await expect(page.getByText('Widget')).toBeVisible();
});
Rules:
baseURL and auth headers in playwright.config.tsresponse.ok() or assert status explicitly — Playwright returns response regardless of status codebrowserContext.request for UI-reflected API calls; request.newContext() for isolated API smoke tests| Layer | Target | Tool | When to Write |
|---|---|---|---|
| Unit | ~70% of logic | Vitest/Jest | Individual functions, pure logic, edge cases |
| Integration/API | ~20% | Playwright request, API clients | Service contracts, DB interactions, API responses |
| E2E | ~10% critical paths | Playwright UI | Money paths only — login, checkout, key user journeys |
E2E is expensive: slow to run, costly to maintain, prone to flakiness. Write E2E tests only when:
When a bug is found in E2E, ask: could this have been caught earlier (unit/integration)? If yes, add that lower-level test too.
For repeated interaction patterns across multiple tests:
class LoginPage {
constructor(page) {
this.page = page;
this.emailInput = page.getByRole('textbox', { name: 'Email' });
this.passwordInput = page.getByRole('textbox', { name: 'Password' });
this.submitButton = page.getByRole('button', { name: 'Sign in' });
}
async login(email, password) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
Use POM when:
mcp_playwright_browser_navigatemcp_playwright_browser_resizemcp_playwright_browser_snapshotmcp_playwright_browser_evaluateUse browser_subagent tool for screenshot workflows — it automatically records as WebP video.
async (page) => {
return await page.$$eval('.item', els =>
els.map(el => ({
title: el.querySelector('h2')?.textContent?.trim(),
link: el.querySelector('a')?.href,
}))
);
}
Use mcp_playwright_browser_fill_form for multi-field forms — single call, atomic:
fields: [
{ target: "ref", name: "Email", type: "textbox", value: "user@test.com" },
{ target: "ref", name: "Remember", type: "checkbox", value: "true" }
]
| ❌ Don't | ✅ Do Instead |
|---|---|
page.waitForTimeout(2000) | page.locator('.el').waitFor({ state: 'visible' }) |
waitForLoadState('networkidle') | waitForURL() or waitForSelector() |
| CSS class/ID selectors in tests | Role-based or getByTestId() selectors |
| Skip hooks as a "fix" | Find and fix the root cause |
Silent test.skip() | test.fixme('reason or issue link') |
| Chain test scenarios | Independent tests from clean state |
| Assert text from text-based locator | Use toBeVisible() for text locators |
| Multiple tests per file | One test per file for AI predictability |
| Mix MCP and CLI in same session | Choose one approach per workflow |
| UI login in every test | storageState fixture via setup project |
| UI-driven test data setup | API request fixture for seeding |
| Static shared test accounts | Unique dynamic data per test / per-worker accounts |
| E2E tests for all business logic | E2E for critical paths only — unit/integration for the rest |
| Hardcoded credentials | process.env.* always |
Committing .auth/ files | Add playwright/.auth/ to .gitignore |