Token导航 LogoToken导航TokenDH.com
研究检索敏感数据github未标认证来源可访问许可证需确认审计提醒

browser浏览器

Agent Skill

browser 用于处理浏览器自动化、网页检查和页面信息提取,适合在 Codex、Claude、Cursor、Gemini CLI 中需要让 Agent 打开页面、读取网页或验证前端流程时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

总安装

715

周安装

49

GitHub Stars

10

下载量

388
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

2

许可证

unknown

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:browser(浏览器)
来源仓库:https://github.com/inkeep/team-skills
仓库路径:skills/browser
安装命令:
npx skills add https://github.com/inkeep/team-skills --skill browser
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/inkeep/team-skills --skill browser

简介

browser 用于处理浏览器自动化、网页检查和页面信息提取。

  • 适合在 Codex、Claude、Cursor、Gemini CLI 中打开页面或验证前端流程。
  • 通过 npx skills add 命令从指定 GitHub 仓库安装使用。
  • 安装前需确认权限范围、维护状态及是否触发联网或文件操作。
  • browser 属于研究检索类 Skill,可作为该场景下的辅助能力补充。

SKILL.md

Browser Automation

Two engines: agent-browser (default for interactive work) and Playwright scripts (for compound/programmatic operations). The engine is determined by what you're trying to do.


Engine Selection

Agent-browser (default for interactive work)

Use agent-browser when you need to:

  • Navigate to a URL and read/inspect the page
  • Take a screenshot (including annotated, full-page)
  • Click, fill, select, check form elements
  • Read page structure (ARIA snapshot with element refs)
  • Compare before/after state (diff snapshot, diff screenshot)
  • Extract text from elements
  • Basic cookie/storage read
  • Quick visual verification ("does this page look right?")
  • Device emulation, viewport changes
  • PDF generation

Auth modes:

ModeCommandWhen to use
No auth (fresh)agent-browser open <url>Public pages, dev servers, CI/CD
Profile copy--profile DefaultQuick access to user's logged-in state (stale after launch)
State file--state./auth.json or state save/loadPersistent auth across sessions, shareable
Session name--session-name myappAuto-save/restore for recurring tasks
Auth vaultauth save/loginEncrypted credential storage
Auto-connect--auto-connectLive Chrome session (requires --remote-debugging-port=9222)

Visibility modes:

ModeHowWhen
Headless (default)Default behaviorCI/CD, automated runs, background tasks
Headed--headedUser wants to watch, debugging, manual intervention (2FA/CAPTCHA)
Dashboardagent-browser dashboardMonitor without visible window, live streaming

Playwright scripts (for compound and capability-gated operations)

Use Playwright scripts when you need operations in these two categories:

A. Compound orchestration (always Playwright — architectural necessity):

  • Capture console errors/warnings during a multi-step flow (event listener before navigation)
  • Capture network requests during a specific action (real-time event filtering)
  • Record video of a browser session
  • Run accessibility audits (axe-core injection + structured WCAG results)
  • Take responsive screenshots across multiple breakpoints (viewport sweep)
  • Generate GIFs from screenshot sequences
  • Intercept/mock network requests with custom status, headers, and conditional routing
  • Transfer a session between headless and headed mode mid-flow (handoff())
  • Run pre-script code before a capture (screengrabs pipeline)
  • Register auto-dismiss handlers for overlays/modals (addLocatorHandler)

B. Capability gaps (Playwright today, may migrate to agent-browser when supported):

  • Pierce shadow DOM for web component interaction
  • Execute complex JS with arguments passed to the function (page.evaluate(fn, args))
  • Full iframe context isolation (basic iframe interaction works in agent-browser, eval/screenshot don't scope to frame)
  • Simulate slow network conditions (agent-browser has offline only, no latency/bandwidth)
  • Capture Web Vitals / Navigation Timing performance metrics

Migration watchlist: Monitor agent-browser releases for: shadow DOM selectors, eval with argument passing, iframe context scoping, network throttling. When supported, update this routing tree.

Auth modes for Playwright scripts:

ModeHowWhen to use
No auth (fresh)Default node run.jsPublic pages, dev servers
State filehelpers.loadAuthState(browser, authPath)Restored from pre-saved storageState
Connect to Chromenode run.js --connectLive session via MCP Bridge extension (dev machines only)
Session servernode run.js --session startPersistent browser across script invocations

Agent-browser Quick Reference

Core workflow — every agent-browser interaction follows this pattern:

# 1. Navigate
agent-browser open https://example.com

# 2. Snapshot — get element refs (@e1, @e2, ...)
agent-browser snapshot -i

# 3. Interact using refs
agent-browser fill @e1 "user@example.com"
agent-browser click @e3

# 4. Re-snapshot after DOM changes
agent-browser snapshot -i

Batch execution — chain multiple commands in one call:

agent-browser batch "fill @e1 user@example.com" "fill @e2 password123" "click @e3"

Diffing — compare before/after state:

agent-browser snapshot -i          # take baseline
# ... perform action ...
agent-browser diff snapshot        # text diff of ARIA tree (+ additions, - removals)
agent-browser diff screenshot      # pixel diff with mismatch %

For the full command reference (80+ commands, auth patterns, batch workflows, templates), load the agent-browser skill (provided by the agent-browser Claude Code plugin — requires claude plugin install agent-browser@agent-browser from setup).


Playwright Scripts — Compound Operations

IMPORTANT - Path Resolution: This skill is installed via the plugin system. Before executing any commands, determine the skill directory based on where you loaded this SKILL.md file, and use that path in all commands below. Replace $SKILL_DIR with the actual discovered path.

Expected plugin path: ~/.claude/plugins/marketplaces/inkeep-team-skills/plugins/eng/skills/browser

Playwright Workflow

General-purpose browser automation via Playwright scripts. Write custom Playwright code for any compound automation task and execute it via the universal executor.

CRITICAL WORKFLOW - Follow these steps in order:

  1. Start a session - If you expect to run more than one script (debugging, iterating, multi-step flows), start a persistent browser session FIRST. This is the default mode for all interactive work: cd $SKILL_DIR && node run.js --session start Scripts auto-detect the session and connect via WebSocket (~50ms) instead of launching a new browser (~2-3s). Login state, cookies, and localStorage persist between runs. Skip this step only for true one-off scripts or CI/CD environments.
  2. Auto-detect dev servers - For localhost testing, run server detection: cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(servers => console.log(JSON.stringify(servers)))"

- If 1 server found: Use it automatically, inform user - If multiple servers found: Ask user which one to test - If no servers found: Ask for URL or offer to help start dev server

  1. Write scripts to /tmp - NEVER write test files to skill directory; always use /tmp/playwright-test-*.js
  2. Parameterize URLs - Always make URLs configurable via environment variable or constant at top of script
  3. Stop session when done - Clean up the persistent browser when the task is complete: cd $SKILL_DIR && node run.js --session stop Sessions also auto-stop after 10 minutes of inactivity.

How It Works

  1. You describe what you want to test/automate
  2. Start a session (--session start) — browser stays warm for all subsequent scripts
  3. Auto-detect running dev servers (or ask for URL if testing external site)
  4. Write custom Playwright code in /tmp/playwright-test-*.js (won't clutter your project)
  5. Execute it via: cd $SKILL_DIR && node run.js /tmp/playwright-test-*.js — auto-connects to session
  6. Results displayed in real-time; login state and pages persist between runs
  7. Stop session when done (--session stop); test files auto-cleaned from /tmp by OS

Local browser mode (user's Chrome)

When the user asks you to interact with their actual browser — using their auth, cookies, or extensions — use the local browser connector instead of headless Playwright.

When to use: User directs you to do something in their browser on their behalf, or you need their authenticated session. Only available on the user's local machine (not Docker/sandbox).

Prerequisites: Chrome running + Playwright MCP Bridge extension installed.

Execute: cd $SKILL_DIR && node scripts/connect-local.js /tmp/my-script.js or cd $SKILL_DIR && node run.js --connect /tmp/my-script.js

Load: references/local-browser.md for routing guidance, limitations, and examples.

Setup (First Time)

cd $SKILL_DIR
npm run setup

This installs Playwright and Chromium browser. Only needed once.

Execution Pattern

Step 1: Detect dev servers (for localhost testing)

cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(s => console.log(JSON.stringify(s)))"

Step 2: Write test script to /tmp with URL parameter

// /tmp/playwright-test-page.js
const { chromium } = require('playwright');

// Parameterized URL (detected or user-provided)
const TARGET_URL = 'http://localhost:3001'; // <-- Auto-detected or from user

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  await page.goto(TARGET_URL);
  console.log('Page loaded:', await page.title());

  await page.screenshot({ path: '/tmp/screenshot.png', fullPage: true });
  console.log('Screenshot saved to /tmp/screenshot.png');

  await browser.close();
})();

Step 3: Execute from skill directory

cd $SKILL_DIR && node run.js /tmp/playwright-test-page.js

State Inspection: ARIA Snapshots First, Screenshots Second

When you need to understand what's on the page, use getPageStructure() — NOT page.screenshot().

NeedUseWhy
What elements are on the page?getPageStructure(page)Text-based, compact, includes roles and selectors
What does the page look like?page.screenshot()Visual layout, colors, spacing — requires vision
Did the right element render?getPageStructure(page, {root: '#my-section'})Scoped ARIA tree — smaller, faster
Is the form accessible?getPageStructure(page, {interactiveOnly: true})Only interactive elements — focused
What can I interact with (by ref ID)?getPageStructureWithRefs(page)Returns short ref IDs (e1, e2) for token-efficient interaction
Visual layout + interactive elements in one artifact?annotatedScreenshot(page)Screenshot with numbered badges on each interactive element
What changed after an action?diffPageStructure(page, previousYaml)Unified diff of ARIA snapshots — shows added/removed/changed elements

Why: ARIA snapshots produce ~200-400 tokens of structured text. Screenshots produce ~2000-5000 tokens of base64 image data that requires vision processing. For a 10-scenario QA run, this difference compounds significantly.

When to still use screenshots:

  • Visual regression checking (layout, spacing, alignment)
  • Capturing evidence for PR bodies
  • Debugging CSS/style issues
  • Any time you need to see how something *looks*, not what *elements exist*

Observe-Then-Write (for multi-step flows)

For scripts with 3+ interactions (form submissions, multi-page flows, complex UIs), do NOT guess at selectors. Explore first, then write.

Step 1: Explore the page structure

// /tmp/playwright-explore.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001/login';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto(TARGET_URL);

  // Get the actual page structure — roles, names, selectors
  const structure = await helpers.getPageStructure(page, { interactiveOnly: true });
  console.log('=== Interactive Elements ===');
  console.log(JSON.stringify(structure.tree, null, 2));
  console.log('\n=== Summary ===');
  console.log(JSON.stringify(structure.summary));

  await browser.close();
})();

Step 2: Write the real script using observed selectors

Use the selectors and roles from the exploration output. Never guess at input[name="..."] or button.class-name — use what you actually saw in the ARIA tree.

When to use: Multi-step forms, unfamiliar UIs, authenticated flows, any page where selector guessing has already failed once.

When to skip: Single-action scripts (take a screenshot, check a title, navigate and verify).

Ref-based interaction (token-efficient alternative)

For multi-step flows where you'll interact with many elements, use getPageStructureWithRefs() + resolveRef() instead of manually copying selector strings. Ref IDs (e1, e2,...) are shorter and less error-prone.

// /tmp/playwright-ref-interact.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001/login';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto(TARGET_URL);

  // Get structure with ref IDs
  const { tree, refMap } = await helpers.getPageStructureWithRefs(page, { interactiveOnly: true });
  console.log('Interactive elements:');
  tree.forEach(el => console.log(`  ${el.ref || '-'} ${el.role}: "${el.name}"`));
  // e.g.: e1 textbox: "Email"   e2 textbox: "Password"   e3 button: "Sign in"

  // Interact by ref (resolveRef is async — includes staleness check)
  await (await helpers.resolveRef(page, refMap, 'e1')).fill('test@example.com');
  await (await helpers.resolveRef(page, refMap, 'e2')).fill('password123');
  await (await helpers.resolveRef(page, refMap, 'e3')).click();

  await page.waitForURL('**/dashboard');
  console.log('Login successful');

  await browser.close();
})();

Annotated screenshot — see the page AND its interactive elements in one image:

// /tmp/playwright-annotated.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto('http://localhost:3001');

  const result = await helpers.annotatedScreenshot(page, { path: '/tmp/annotated.png' });
  console.log(`Annotated screenshot: ${result.path} (${result.elementCount} elements labeled)`);
  console.log('Ref map:', result.refMap);
  // Then: Read tool → /tmp/annotated.png to see the visual with numbered badges

  await browser.close();
})();

When to use refs vs selectors: Refs are best for multi-step flows where you interact with 3+ elements from the same page state. For single interactions or when you need to understand the selector (debugging), use getPageStructure() directly.

Snapshot Diffing

Compare ARIA tree snapshots before and after an action to see exactly what changed. Useful for verifying that an action had the expected effect without manually comparing raw YAML.

// /tmp/playwright-diff.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto('http://localhost:3001/settings');

  // Take a baseline snapshot
  const { yaml: before } = await helpers.getPageStructure(page);

  // Perform an action
  await page.click('button.save');
  await page.waitForLoadState('networkidle');

  // Diff — see exactly what changed
  const result = await helpers.diffPageStructure(page, before);
  if (result.changed) {
    console.log('Changes detected:\n', result.diff);
  } else {
    console.log('No changes — action had no effect');
  }

  // Chain: use currentYaml as next baseline
  // const result2 = await helpers.diffPageStructure(page, result.currentYaml);

  await browser.close();
})();

When to use: After form submissions, navigation, AJAX updates — any action where you need to verify the page changed (or didn't). The line-by-line diff format (+ added, - removed, `` unchanged) is compact and human-readable.

Authentication

Credentials

The browser skill reads credentials from environment variables. Set these before invoking browser automation:

Env varPurpose
BROWSER_AUTH_USERDefault username/email
BROWSER_AUTH_PASSDefault password
BROWSER_AUTH_TOTP_SECRETBase32-encoded TOTP secret for 2FA

Multi-site credentials: For workflows that authenticate against multiple services, use domain-specific env vars with a fallback to the default:

Env var patternExample
BROWSER_AUTH_<DOMAIN>_USERBROWSER_AUTH_GITHUB_USER
BROWSER_AUTH_<DOMAIN>_PASSBROWSER_AUTH_GOOGLE_PASS
BROWSER_AUTH_<DOMAIN>_TOTP_SECRETBROWSER_AUTH_GITHUB_TOTP_SECRET

Match the credential set to the domain you're authenticating against. Fall back to the default set (BROWSER_AUTH_USER etc.) when no domain-specific var exists.

Auth wall classification

When your browser automation encounters an auth wall, classify it and act accordingly. Try everything in the automatable categories before concluding you need a human. The classification is a guide, not a rulebook — if something looks automatable, try it. Only classify as "hard wall" after a genuine attempt fails.

CategoryExamplesAgent actionConfidence
Automatable — form fillUsername/password login, registration forms, password resetFill fields with helpers.authenticate(page, {username, password}) or manual fill via helpers.safeType().High
Automatable — TOTP2FA code entry after loginCall helpers.generateTOTP(process.env.BROWSER_AUTH_TOTP_SECRET), fill the code field.High (if secret available)
Automatable — OAuth/SSO"Sign in with Google/GitHub/Microsoft" buttons, SAML redirectsClick SSO button, follow redirects to IdP, fill credentials on IdP page, follow callback back.Medium (depends on IdP bot detection)
Automatable — consent/bannerCookie consent, OAuth "Authorize" buttons, terms acceptanceClick accept/authorize. Use helpers.handleCookieBanner() for cookies.High
Automatable — email verification"Click the link in your email"Only if agent has email access (API to mailbox). Try; if no access, classify as wall.Low-Medium
Try — may need humanreCAPTCHA v2 checkbox, Cloudflare Turnstile, reCAPTCHA v3 (score-based)Click/interact. May pass with warm profile. If escalated to image challenge, classify as wall.30-50%
Hard wall — needs humanhCaptcha, reCAPTCHA image challenges, SMS/push MFA, WebAuthn/passkeys, enterprise anti-bot (Akamai, PerimeterX, DataDome)In supervised mode: call helpers.handoff(page, {reason, successUrl}). In headless mode: document and skip.~0% automated

Handoff (last resort — supervised mode only)

When you hit a hard wall in supervised mode, helpers.handoff() opens a headed browser for the human to resolve the block, then resumes headless:

const { page: newPage, context: newCtx } = await helpers.handoff(page, {
  reason: 'hCaptcha on login page',
  successUrl: '/dashboard',
  timeout: 120000
});
// newPage is authenticated — continue testing

The handoff automatically detects whether you're in session mode (shared daemon) or standalone mode and handles each correctly. Never call handoff in headless/CI mode — document the wall and move on.

Common Patterns

Test a Page (Multiple Viewports)

// /tmp/playwright-test-responsive.js
const { chromium } = require('playwright');

const TARGET_URL = 'http://localhost:3001'; // Auto-detected

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  // Desktop test
  await page.setViewportSize({ width: 1920, height: 1080 });
  await page.goto(TARGET_URL);
  console.log('Desktop - Title:', await page.title());
  await page.screenshot({ path: '/tmp/desktop.png', fullPage: true });

  // Mobile test
  await page.setViewportSize({ width: 375, height: 667 });
  await page.screenshot({ path: '/tmp/mobile.png', fullPage: true });

  await browser.close();
})();

Test Login Flow

// /tmp/playwright-test-login.js
const { chromium } = require('playwright');

const TARGET_URL = 'http://localhost:3001'; // Auto-detected

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  await page.goto(`${TARGET_URL}/login`);

  await page.fill('input[name="email"]', 'test@example.com');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');

  // Wait for redirect
  await page.waitForURL('**/dashboard');
  console.log('Login successful, redirected to dashboard');

  await browser.close();
})();

Test Authenticated Pages

Login once, save the session, and reuse it across multiple test runs. Avoids re-logging in every time.

Step 1: Login and save auth state (run once)

// /tmp/playwright-auth-save.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001/login';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const context = await helpers.createContext(browser);
  const page = await context.newPage();

  await page.goto(TARGET_URL);
  await helpers.authenticate(page, {
    username: 'admin@example.com',
    password: 'password123'
  });

  // Save session for reuse
  const saved = await helpers.saveAuthState(context);
  console.log('Auth state saved:', saved.path);
  console.log(`  ${saved.cookies} cookies, ${saved.origins} origins`);

  // For Firebase/Supabase/modern auth that stores tokens in IndexedDB:
  // const saved = await helpers.saveAuthState(context, '/tmp/auth.json', { indexedDB: true });

  await browser.close();
})();

Step 2: Reuse saved auth in subsequent tests

// /tmp/playwright-test-dashboard.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001/dashboard';

(async () => {
  const browser = await chromium.launch({ headless: true });

  // Load saved auth — skips login entirely
  const context = await helpers.loadAuthState(browser);
  const page = await context.newPage();

  await page.goto(TARGET_URL);
  console.log('Page title:', await page.title());
  // You're now on the authenticated dashboard
  await page.screenshot({ path: '/tmp/dashboard.png', fullPage: true });

  await browser.close();
})();

Extract Auth from Running Chrome

Use when you need to test authenticated pages but don't want to hardcode credentials in scripts — or when the app uses SSO, OAuth, or 2FA that's hard to automate. Connect to your already-logged-in Chrome, extract the session, and reuse it in any headless script.

Requires: Chrome running + Playwright MCP Bridge extension installed.

Step 1: Extract auth from your running Chrome (run once)

// /tmp/playwright-extract-auth.js
const { connectToLocalBrowser, extractAuthState } = require('./lib/local-browser');

(async () => {
  const conn = await connectToLocalBrowser();

  // Extracts cookies + localStorage from Chrome's current session
  await extractAuthState(conn.context, { path: '/tmp/auth-state.json' });
  console.log('Auth extracted to /tmp/auth-state.json');

  // For Firebase/Supabase/modern auth stored in IndexedDB:
  // await extractAuthState(conn.context, { path: '/tmp/auth-state.json', indexedDB: true });

  await conn.close();
})();

Execute: cd $SKILL_DIR && node run.js --connect /tmp/playwright-extract-auth.js

Step 2: Reuse extracted auth in headless scripts

// /tmp/playwright-test-with-chrome-auth.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001/dashboard';

(async () => {
  const browser = await chromium.launch({ headless: true });

  // Load Chrome's session — no credentials needed
  const context = await helpers.loadAuthState(browser, '/tmp/auth-state.json');
  const page = await context.newPage();

  await page.goto(TARGET_URL);
  console.log('Page title:', await page.title());

  await browser.close();
})();

When to use this vs "Test Authenticated Pages":

ScenarioUse
You have credentials and want a self-contained script"Test Authenticated Pages" (login → saveAuthState)
You're already logged in to Chrome and want to reuse that sessionThis pattern (extractAuthStateloadAuthState)
The app uses SSO, OAuth, or 2FA that's hard to scriptThis pattern

See references/local-browser.md for what auth state transfers reliably (most session cookies and JWTs) vs what doesn't (Google DBSC, Cloudflare, WebAuthn).

Fill and Submit Form

// /tmp/playwright-test-form.js
const { chromium } = require('playwright');

const TARGET_URL = 'http://localhost:3001'; // Auto-detected

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  await page.goto(`${TARGET_URL}/contact`);

  await page.fill('input[name="name"]', 'John Doe');
  await page.fill('input[name="email"]', 'john@example.com');
  await page.fill('textarea[name="message"]', 'Test message');
  await page.click('button[type="submit"]');

  // Verify submission
  await page.waitForSelector('.success-message');
  console.log('Form submitted successfully');

  await browser.close();
})();

Network Request Inspection

// /tmp/playwright-test-network.js
const { chromium } = require('playwright');

const TARGET_URL = 'http://localhost:3001';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  // Capture all API requests
  const apiRequests = [];
  page.on('request', request => {
    if (request.url().includes('/api/')) {
      apiRequests.push({
        method: request.method(),
        url: request.url(),
        headers: request.headers()
      });
    }
  });

  page.on('response', response => {
    if (response.url().includes('/api/')) {
      console.log(`${response.status()} ${response.url()}`);
    }
  });

  await page.goto(TARGET_URL);
  await page.waitForLoadState('networkidle');

  console.log('API requests captured:', JSON.stringify(apiRequests, null, 2));

  await browser.close();
})();

JavaScript Injection

// /tmp/playwright-test-js-inject.js
const { chromium } = require('playwright');

const TARGET_URL = 'http://localhost:3001';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  await page.goto(TARGET_URL);

  // Inject and execute JavaScript
  const result = await page.evaluate(() => {
    return {
      title: document.title,
      links: document.querySelectorAll('a').length,
      meta: Array.from(document.querySelectorAll('meta')).map(m => ({
        name: m.getAttribute('name'),
        content: m.getAttribute('content')
      })).filter(m => m.name),
      localStorage: Object.keys(window.localStorage),
      cookies: document.cookie
    };
  });

  console.log('Page analysis:', JSON.stringify(result, null, 2));

  await browser.close();
})();

Check for Broken Links

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  await page.goto('http://localhost:3000');

  const links = await page.locator('a[href^="http"]').all();
  const results = { working: 0, broken: [] };

  for (const link of links) {
    const href = await link.getAttribute('href');
    try {
      const response = await page.request.head(href);
      if (response.ok()) {
        results.working++;
      } else {
        results.broken.push({ url: href, status: response.status() });
      }
    } catch (e) {
      results.broken.push({ url: href, error: e.message });
    }
  }

  console.log(`Working links: ${results.working}`);
  console.log(`Broken links:`, results.broken);

  await browser.close();
})();

Take Screenshot with Error Handling

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  try {
    await page.goto('http://localhost:3000', {
      waitUntil: 'networkidle',
      timeout: 10000,
    });

    await page.screenshot({
      path: '/tmp/screenshot.png',
      fullPage: true,
    });

    console.log('Screenshot saved to /tmp/screenshot.png');
  } catch (error) {
    console.error('Error:', error.message);
  } finally {
    await browser.close();
  }
})();

Test Responsive Design

// /tmp/playwright-test-responsive-full.js
const { chromium } = require('playwright');

const TARGET_URL = 'http://localhost:3001'; // Auto-detected

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  const viewports = [
    { name: 'Desktop', width: 1920, height: 1080 },
    { name: 'Tablet', width: 768, height: 1024 },
    { name: 'Mobile', width: 375, height: 667 },
  ];

  for (const viewport of viewports) {
    console.log(
      `Testing ${viewport.name} (${viewport.width}x${viewport.height})`,
    );

    await page.setViewportSize({
      width: viewport.width,
      height: viewport.height,
    });

    await page.goto(TARGET_URL);
    await page.waitForTimeout(1000);

    await page.screenshot({
      path: `/tmp/${viewport.name.toLowerCase()}.png`,
      fullPage: true,
    });
  }

  console.log('All viewports tested');
  await browser.close();
})();

Monitor Console Errors During a Flow

Use when verifying a UI flow doesn't produce silent JS errors.

// /tmp/playwright-test-console.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  // Start capturing BEFORE navigation
  const consoleLogs = helpers.startConsoleCapture(page);

  await page.goto(TARGET_URL);
  await page.waitForLoadState('networkidle');

  // Interact with the page
  await page.click('button.submit').catch(() => {});
  await page.waitForTimeout(1000);

  // Check for errors
  const errors = helpers.getConsoleErrors(consoleLogs);
  if (errors.length > 0) {
    console.log(`FAIL: ${errors.length} console error(s):`);
    errors.forEach(e => console.log(`  [${e.type}] ${e.text}`));
  } else {
    console.log('PASS: No console errors');
  }

  // Optionally filter for specific logs
  const apiLogs = helpers.getConsoleLogs(consoleLogs, /api|fetch/i);
  console.log(`API-related logs: ${apiLogs.length}`);

  await browser.close();
})();

Verify Network Requests During UI Flow

Use when checking that the right API calls fire with the right status codes.

// /tmp/playwright-test-network-verify.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  // Capture only API requests
  const network = helpers.startNetworkCapture(page, '/api/');

  await page.goto(`${TARGET_URL}/dashboard`);
  await page.waitForLoadState('networkidle');

  // Check for failed API calls
  const failed = helpers.getFailedRequests(network);
  if (failed.length > 0) {
    console.log(`FAIL: ${failed.length} failed API request(s):`);
    failed.forEach(r => console.log(`  ${r.method} ${r.url} -> ${r.status || r.failure}`));
  } else {
    console.log('PASS: All API requests succeeded');
  }

  // Review all captured requests
  const all = helpers.getCapturedRequests(network);
  console.log(`Total API requests: ${all.length}`);
  all.forEach(r => console.log(`  ${r.status} ${r.method} ${r.url}`));

  await browser.close();
})();

Record Video of a Flow

Use when you need a recording of multi-step browser interaction.

// /tmp/playwright-test-video.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const context = await helpers.createVideoContext(browser, {
    outputDir: '/tmp/playwright-videos'
  });
  const page = await context.newPage();

  await page.goto(TARGET_URL);
  await page.click('nav a:first-child');
  await page.waitForTimeout(1000);
  await page.click('button.submit').catch(() => {});
  await page.waitForTimeout(1000);

  // Video is saved when page closes
  const videoPath = await page.video().path();
  await page.close();
  await context.close();

  console.log(`Video saved: ${videoPath}`);
  await browser.close();
})();

Inspect Browser State After Mutation

Use when verifying that a UI action correctly persisted data.

// /tmp/playwright-test-state.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext();
  const page = await context.newPage();

  await page.goto(TARGET_URL);

  // Check state before action
  const storageBefore = await helpers.getLocalStorage(page);
  console.log('localStorage before:', JSON.stringify(storageBefore));

  const cookies = await helpers.getCookies(context);
  console.log('Cookies:', cookies.map(c => `${c.name}=${c.value}`));

  // Perform some action that should change state
  await page.click('button.save-preferences').catch(() => {});
  await page.waitForTimeout(500);

  // Check state after action
  const storageAfter = await helpers.getLocalStorage(page);
  console.log('localStorage after:', JSON.stringify(storageAfter));

  // Clean up for next test
  await helpers.clearAllStorage(page);

  await browser.close();
})();

Discover Page Structure

Use when you don't know a page's DOM structure — third-party sites, authenticated dashboards, or unfamiliar UIs. Get the ARIA snapshot to find the right selectors before writing interactions.

Returns yaml (raw ARIA snapshot string preserving hierarchy), tree (parsed nodes with suggested selectors), and summary (counts by role type).

// /tmp/playwright-test-discover.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const context = await helpers.createContext(browser);
  const page = await context.newPage();
  await page.goto(TARGET_URL, { waitUntil: 'networkidle' });

  // Get full page structure
  const structure = await helpers.getPageStructure(page);
  console.log('Page:', structure.title);
  console.log('Elements:', JSON.stringify(structure.summary));

  // Raw YAML preserves nesting — useful for understanding page hierarchy
  console.log('ARIA snapshot:\n', structure.yaml);

  // Parsed tree has suggested selectors for each element
  console.log('Interactive elements:');
  structure.tree.filter(el =>
    ['button','link','textbox','checkbox','combobox'].includes(el.role)
  ).forEach(el => console.log(`  ${el.role}: "${el.name}" → ${el.selector}`));

  // Scope to a specific section
  const formElements = await helpers.getPageStructure(page, {
    interactiveOnly: true,
    root: 'form'
  });
  console.log('Form inputs:', JSON.stringify(formElements.tree, null, 2));

  await browser.close();
})();

Visual Inspection (look at a page)

Use when you need to see what a page looks like — before taking final screenshots, during exploration, after an action, or to verify a UI state. This is for your own understanding, not for output.

The pattern: take a temporary screenshot, then read it.

// /tmp/playwright-test-inspect.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const context = await helpers.createContext(browser);
  const page = await context.newPage();
  await page.goto(`${TARGET_URL}/dashboard`, { waitUntil: 'networkidle' });

  // Take a quick screenshot to see the page
  await page.screenshot({ path: '/tmp/inspect.png' });

  // Inspect a specific section
  const section = page.locator('.settings-panel');
  await section.screenshot({ path: '/tmp/inspect-section.png' });

  await browser.close();
})();

After running the script, read the image file to see what the page looks like:

Read tool → /tmp/inspect.png

Claude renders PNG files visually, so you can see the actual page layout, content, popups, loading states, and any issues.

When to use this vs getPageStructure():

NeedUse
Find selectors, understand DOM hierarchygetPageStructure() (text — faster, more precise)
See what the page actually looks likeVisual inspection (screenshot — layout, colors, overlays, rendering)
Both — unfamiliar pageDo both: structure first for selectors, then screenshot to see the visual result

Tip: For iterative work (exploring a page, debugging a pre-script), use a persistent session so you don't relaunch the browser each time. The screenshot file gets overwritten on each run.

Capture Screenshots for Documentation

Use when writing docs, help articles, or PR screenshots that need consistent, high-quality images of the running UI.

// /tmp/playwright-test-doc-screenshot.js
const { chromium } = require('playwright');

const TARGET_URL = 'http://localhost:3001';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    viewport: { width: 1280, height: 720 },
    deviceScaleFactor: 2, // Retina clarity
  });
  const page = await context.newPage();

  await page.goto(`${TARGET_URL}/settings`);
  await page.waitForLoadState('networkidle');

  // Crop to the relevant section — avoid full-page captures with empty space
  const section = page.locator('.api-keys-section');
  await section.screenshot({
    path: '/tmp/doc-settings-api-keys.png',
    type: 'png',
  });

  // Full-page fallback when you need the whole view
  await page.screenshot({
    path: '/tmp/doc-settings-full.png',
    type: 'png',
    fullPage: false, // Viewport-only — keep it tight
  });

  console.log('Doc screenshots saved to /tmp/doc-*.png');
  await browser.close();
})();

Key settings for doc screenshots:

  • viewport: {width: 1280, height: 720} — standard docs width
  • deviceScaleFactor: 2 — retina resolution for sharp text
  • type: 'png' — lossless for UI screenshots
  • Use element.screenshot() to crop to a specific panel instead of full-page
  • Target <200KB per image — crop aggressively

Media Asset Pipeline

Choose the right preset and conversion for your target. Presets set viewport + DPR automatically — no manual config needed.

TargetPresetOutputMax sizeWhy
Docs site screenshotdocs-retina2560×1440 PNG<500 KBRetina-sharp for Next.js Image
GitHub PR screenshotpr-standard1280×720 PNG<200 KBCrisp at GitHub's 894px display width. Upload via /media-upload skill for CDN URLs
GitHub PR GIFgif-compact800×450 animated GIF<10 MBDPR 1 — GIF's 256-color palette is the bottleneck, not pixel density. Upload via /media-upload skill for CDN URLs
Video (internal or customer-facing)video2560×1440 WebM → Bunny or VimeoUpload via /media-upload skill. Both platforms transcode to ABR. DPR 1 is correct for video

Capture a docs-quality screenshot with a preset:

// /tmp/playwright-test-preset-screenshot.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001';

(async () => {
  const browser = await chromium.launch({ headless: true });

  // Preset sets viewport 1280x720 + DPR 2 → 2560x1440 output
  const context = await helpers.createPresetContext(browser, 'docs-retina');
  const page = await context.newPage();

  await page.goto(`${TARGET_URL}/settings`);
  await page.waitForLoadState('networkidle');

  // Element-level crop for tight framing
  const section = page.locator('.api-keys-section');
  await section.screenshot({ path: '/tmp/doc-api-keys.png', type: 'png' });

  console.log('Docs screenshot: 2560x1440 Retina PNG');
  await browser.close();
})();

Create a step-by-step GIF for a PR:

// /tmp/playwright-test-pr-gif.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001';

(async () => {
  const browser = await chromium.launch({ headless: true });
  // gif-compact: 800x450 @ DPR 1 — optimized for GitHub's 10MB limit
  const context = await helpers.createPresetContext(browser, 'gif-compact');
  const page = await context.newPage();

  const frames = [];

  // Frame 1: Starting state
  await page.goto(`${TARGET_URL}/settings`);
  await page.waitForLoadState('networkidle');
  frames.push(await page.screenshot({ type: 'png' }));

  // Frame 2: Click action
  await page.click('button.save');
  await page.waitForTimeout(500);
  frames.push(await page.screenshot({ type: 'png' }));

  // Frame 3: Success state
  await page.waitForSelector('.success-toast');
  frames.push(await page.screenshot({ type: 'png' }));

  // Assemble GIF — 3 frames at 2fps = 1.5s loop
  const result = await helpers.screenshotsToGif(frames, '/tmp/pr-demo.gif', {
    width: 800, height: 450, fps: 2
  });

  console.log(`GIF: ${result.path} (${result.sizeMB} MB, ${result.frames} frames)`);
  await browser.close();
})();

Annotated GIF with click indicators and step labels:

// /tmp/playwright-test-annotated-gif.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const context = await helpers.createPresetContext(browser, 'gif-compact');
  const page = await context.newPage();

  const frames = [];
  const annotations = [];

  // Frame 1: Navigate to page
  await page.goto(TARGET_URL);
  frames.push(await page.screenshot({ type: 'png' }));
  annotations.push({ label: 'Step 1: Open login page' });

  // Frame 2: Click username field
  await page.click('#username');
  frames.push(await page.screenshot({ type: 'png' }));
  annotations.push({ label: 'Step 2: Click username', click: { x: 640, y: 300 } });

  // Frame 3: Type credentials
  await page.fill('#username', 'admin');
  frames.push(await page.screenshot({ type: 'png' }));
  annotations.push({ label: 'Step 3: Enter username' });

  // Frame 4: Click submit
  await page.click('button[type="submit"]');
  frames.push(await page.screenshot({ type: 'png' }));
  annotations.push({ label: 'Step 4: Submit', click: { x: 640, y: 400 } });

  const result = await helpers.screenshotsToGif(frames, '/tmp/login-demo.gif', {
    width: 800, height: 450, fps: 2,
    annotations
  });

  console.log(`Annotated GIF: ${result.path} (${result.sizeMB} MB, ${result.frames} frames)`);
  await browser.close();
})();

Run Accessibility Audit

Use when checking a page for WCAG violations.

// /tmp/playwright-test-a11y.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  await page.goto(TARGET_URL);
  await page.waitForLoadState('networkidle');

  const audit = await helpers.runAccessibilityAudit(page);

  console.log(`Accessibility audit: ${audit.violationCount} violation(s), ${audit.passes} passes`);

  if (audit.violationCount > 0) {
    console.log('\nViolations:');
    audit.summary.forEach(v => {
      console.log(`  [${v.impact}] ${v.id}: ${v.description} (${v.nodes} element(s))`);
      console.log(`    Help: ${v.helpUrl}`);
    });
  }

  // Test keyboard focus order
  const focusOrder = await helpers.checkFocusOrder(page, [
    'a[href]:first-of-type',
    'nav a:nth-child(2)',
    'input[type="search"]'
  ]);
  focusOrder.forEach(f => {
    console.log(`  Tab ${f.step}: expected ${f.expectedSelector} -> ${f.matches ? 'PASS' : 'FAIL'}`);
  });

  await browser.close();
})();

Handle Dialogs and Overlays

Use when pages have alert()/confirm()/prompt() dialogs or blocking overlays (cookie banners, modals) that prevent interaction.

// /tmp/playwright-test-dialogs.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();

  // Auto-accept all dialogs (call BEFORE navigating)
  const dialogLog = helpers.handleDialogs(page);

  // Auto-dismiss cookie banners and common overlays
  await helpers.dismissOverlays(page);

  await page.goto(TARGET_URL);
  await page.click('button.delete'); // triggers confirm()

  // Check what dialogs appeared
  console.log('Dialogs captured:', dialogLog.dialogs.length);
  dialogLog.dialogs.forEach(d =>
    console.log(`  ${d.type}: "${d.message}"`)
  );

  // Custom overlay patterns (beyond the defaults)
  await helpers.dismissOverlays(page, [
    { locator: '.onboarding-modal .close-btn', action: 'click' },
    { locator: '.promo-popup', action: 'remove' }  // remove from DOM entirely
  ]);

  await browser.close();
})();

Debug with Tracing

Use when a flow fails and you need to understand exactly what happened — DOM state, screenshots, network, and console at each step. Produces a .zip viewable in Playwright Trace Viewer.

// /tmp/playwright-test-trace.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const context = await helpers.createContext(browser);

  // Start tracing BEFORE creating pages
  await helpers.startTracing(context);

  const page = await context.newPage();
  await page.goto(TARGET_URL);
  await page.click('button.submit');
  await page.waitForSelector('.result');

  // Stop and save trace
  const trace = await helpers.stopTracing(context, '/tmp/trace.zip');
  console.log(`Trace saved: ${trace.path}`);
  console.log('View with: npx playwright show-trace /tmp/trace.zip');

  await browser.close();
})();

Generate PDF

Use when you need a PDF export of a page — documentation, reports, or print-ready output. Chromium headless only.

// /tmp/playwright-test-pdf.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001/report';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto(TARGET_URL, { waitUntil: 'networkidle' });

  // Basic PDF
  const result = await helpers.generatePdf(page, '/tmp/report.pdf');
  console.log('PDF saved:', result.path);

  // Accessible PDF with bookmarks
  await helpers.generatePdf(page, '/tmp/report-accessible.pdf', {
    tagged: true,   // accessible/tagged PDF
    outline: true,  // document outline from headings
    format: 'Letter',
    margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' }
  });

  await browser.close();
})();

Download Files

Use when a button or link triggers a file download and you need to save or inspect the file.

// /tmp/playwright-test-download.js
const { chromium } = require('playwright');
const helpers = require('./lib/helpers');

const TARGET_URL = 'http://localhost:3001/exports';

(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto(TARGET_URL);

  // Trigger download and save
  const file = await helpers.waitForDownload(
    page,
    () => page.click('#export-csv'),  // action that triggers the download
    '/tmp/export.csv'                   // optional save path
  );
  console.log(`Downloaded: ${file.suggestedFilename} → ${file.path}`);

  await browser.close();
})();

Token-Efficient Mode (@playwright/cli)

For high-volume QA runs (5+ browser scenarios in sequence), use @playwright/cli to reduce token consumption. It writes snapshots, screenshots, and logs to disk instead of the LLM context — ~27K tokens per task vs ~114K via inline output (4x reduction).

Setup (one-time):

npm install -g @playwright/cli@latest

Usage pattern:

# Open a page (starts persistent session)
playwright-cli open http://localhost:3001

# Get page structure to disk (reads from .playwright-cli/ dir)
playwright-cli snapshot

# Interact
playwright-cli click "Login"
playwright-cli fill "Email" "test@example.com"
playwright-cli press Enter

# Take screenshot to disk
playwright-cli screenshot

# Read console/network to disk
playwright-cli console
playwright-cli network

Then read the saved files from .playwright-cli/ as needed — only load what's relevant to the current assertion.

When to use: Multi-scenario QA runs where token budget matters. Especially valuable when running /qa with 10+ scenarios in delegated mode.

When to use normal mode instead: Single scenarios, debugging (need inline output), scenarios that need programmatic Playwright API access (complex assertions, custom waits).

Named sessions for testing multi-user flows:

playwright-cli -s=admin open http://localhost:3001
playwright-cli -s=user open http://localhost:3001
playwright-cli -s=admin click "Approve"
playwright-cli -s=user snapshot  # verify the user sees the approval

Note: @playwright/cli is v0.1.1 (Apache-2.0, pre-1.0). The normal script-based mode remains the primary executor — use this as an optimization for high-volume runs.

Inline Execution (Simple Tasks)

For quick one-off tasks, you can execute code inline without creating files:

# Take a quick screenshot
cd $SKILL_DIR && node run.js "
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('http://localhost:3001');
await page.screenshot({ path: '/tmp/quick-screenshot.png', fullPage: true });
console.log('Screenshot saved');
await browser.close();
"

When to use inline vs files:

  • Inline: Quick one-off tasks (screenshot, check if element exists, get page title)
  • Files: Complex tests, responsive design checks, anything user might want to re-run

Session Mode (Persistent Browser) — Default

Session mode is the recommended default for all interactive browser automation. Start a session before running scripts — every subsequent script connects in ~50ms instead of launching a new browser (~2-3s), and login state persists automatically.

Use session mode for: All interactive work — debugging, testing, iterative flows, auth-heavy pages, video recording, multi-step automation. This covers the vast majority of agent use cases.

Use headless (no session) only for: True one-off scripts, CI/CD pipelines, or environments where a persistent daemon is inappropriate.

Quick start

# Start a session (do this first)
cd $SKILL_DIR && node run.js --session start

# Run scripts — they auto-connect to the session
cd $SKILL_DIR && node run.js /tmp/my-script.js
# Cookies, localStorage, and current URL all persist between runs

# Check session status
cd $SKILL_DIR && node run.js --session status

# Stop when done
cd $SKILL_DIR && node run.js --session stop

How it works

  1. --session start launches a headless Chromium via Playwright's launchServer()
  2. The browser runs as a background daemon (detached process)
  3. Session info is written to /tmp/playwright-session.json
  4. When you run a script, run.js auto-detects the session and connects via WebSocket
  5. Your code gets pre-wired browser, context, and page variables
  6. On script exit, cookies/localStorage/current URL are saved to /tmp/playwright-session-state.json
  7. Next script reconnects and restores state — same auth, same URL, ready to continue

What your code gets

In session mode, your code has these variables pre-defined:

VariableDescription
browserConnected browser instance (persists across runs)
contextBrowser context with restored cookies/localStorage from previous run
pagePage navigated to the last URL from previous run (or blank on first run)
saveStateCall before exiting to persist cookies/localStorage/URL (called automatically by wrapper)
helpersAll helper functions from lib/helpers
chromium, devicesPlaywright exports (for creating additional contexts)

Session mode vs headless (no session)

Session mode (default)Headless — no session
Browser launchOnce (on --session start)Every script execution
Startup time~50ms (WebSocket connect)~2-3s (browser launch)
Login statePersists automatically (cookies/localStorage saved between runs)Lost each run (use saveAuthState/loadAuthState)
Current URLRestored from previous runStarts at about:blank
page.route()Full supportFull support
Token costMinimal (no launch/close boilerplate)Higher (launch + close in every script)
Best forAll interactive work — debugging, testing, iterating, auth flowsCI/CD, isolated tests, one-off scripts

Options

# Start with headed browser (visible)
cd $SKILL_DIR && node run.js --session start --headless false

# Start with a resolution preset
cd $SKILL_DIR && node run.js --session start --preset video

Auto-cleanup

  • Session auto-stops after 10 minutes of inactivity (no scripts run)
  • If the session process crashes, the next script detects the stale session and falls back to fresh headless mode
  • The session file and state file are cleaned up automatically

Creating fresh contexts in session mode

The default is to reuse the existing context (for state persistence). If you need a clean context:

// Create an isolated context within the session
const freshContext = await browser.newContext();
const freshPage = await freshContext.newPage();
await freshPage.goto('https://example.com');
// This context has no cookies/localStorage from previous runs

Available Helpers

All helpers live in lib/helpers.js. Use const helpers = require('./lib/helpers'); in scripts. Organized by what you need to do:

Page Interaction

HelperWhen to use
helpers.detectDevServers()CRITICAL — run first for localhost testing. Returns array of detected server URLs.
helpers.createContext(browser, options?)Create browser context with defaults: viewport 1280x720, locale en-US, timezone America/New_York. Pass {mobile: true} for iPhone UA. Auto-merges env headers.
helpers.waitForPageReady(page, options?)Smart wait for page load (networkidle by default). Pass {waitForSelector: '.loaded'} for dynamic content.
helpers.retryWithBackoff(fn, maxRetries?, initialDelay?)Retry an async function with exponential backoff. Default: 3 retries, 1s initial delay.
helpers.safeClick(page, selector, {retries: 3})Click elements that may not be immediately visible/clickable. Auto-retries.
helpers.safeType(page, selector, text)Type into inputs. Clears field first by default.
helpers.extractTexts(page, selector)Get text from multiple matching elements as array.
helpers.scrollPage(page, 'down', 500)Scroll page. Directions: 'down', 'up', 'top', 'bottom'.
helpers.handleCookieBanner(page)Dismiss common cookie consent banners. Run early — clears overlays that block interaction.
helpers.authenticate(page, {username, password})Login flow with common field selectors. Auto-waits for redirect.
helpers.generateTOTP(secret)Generate a 6-digit TOTP code from a base32 secret. RFC 6238, SHA1, 30s period. Use with BROWSER_AUTH_TOTP_SECRET env var for 2FA flows.
helpers.saveAuthState(context, path?, options?)Save login session after authenticating. Default path: /tmp/playwright-auth.json. Pass {indexedDB: true} for Firebase/Supabase auth. Reuse with loadAuthState.
await helpers.handoff(page, {reason, successUrl?, successSelector?, timeout?})Supervised mode only. Last-resort handoff: closes headless, opens headed for human, polls for success, resumes headless. Auto-detects session vs standalone mode. Returns {page, context, completed} — check completed boolean to know if handoff succeeded vs timed out.
helpers.loadAuthState(browser, path?, options?)Create a context with saved auth state. Skips re-login. Inherits createContext defaults.
helpers.getPageStructure(page, {interactiveOnly, root})Discover page structure via ARIA snapshot. Returns yaml (raw hierarchy), tree (parsed with selectors), and summary (counts). Use for unfamiliar pages.
helpers.getPageStructureWithRefs(page, options?)Like getPageStructure but assigns short ref IDs (e1, e2,...) to interactive elements. Returns additional refMap object with structured entries {role, name, level?, nth?}. Disambiguates duplicate role+name pairs using nth indices. Use with resolveRef() for token-efficient multi-step interaction.
await helpers.resolveRef(page, refMap, 'e3')Async. Resolve a ref ID to a Playwright Locator. Includes staleness detection — throws immediately (~5ms) if the element no longer exists on the page, instead of waiting for Playwright's 30s timeout. Returns a Locator you can .click(), .fill(), etc. Use (await helpers.resolveRef(...)).method() syntax.
await helpers.diffPageStructure(page, previousYaml, options?)Compare current page ARIA structure against a previous snapshot. Returns {diff, changed, currentYaml}. The diff is a line-by-line diff string with +/-/` prefixes. Use currentYaml as the baseline for the next diff. Requires the diff` npm package.
helpers.annotatedScreenshot(page, options?)Screenshot with numbered badges on interactive elements. Returns {path, refMap, tree, elementCount}. Options: path (save location), root (scope), fullPage. Combines visual layout with element discovery in one artifact.
helpers.handleDialogs(page, options?)Auto-handle alert/confirm/prompt dialogs. Call BEFORE navigating. Returns {dialogs} for inspection after.
helpers.dismissOverlays(page, overlays?)Auto-dismiss cookie banners, modals, and blocking overlays using addLocatorHandler. Pass custom patterns or use defaults.
helpers.extractTableData(page, 'table.results')Extract structured data from HTML tables (headers + rows).
helpers.takeScreenshot(page, 'name')Save timestamped screenshot.

Console Monitoring — catch silent JS errors

HelperWhen to use
helpers.startConsoleCapture(page)Call BEFORE navigating. Returns a collector that accumulates all console output.
helpers.getConsoleErrors(collector)Get only error-level messages and uncaught exceptions from collector.
helpers.getConsoleLogs(collector, filter?)Get all logs, or filter by string/RegExp/function.

Lightweight alternative (Playwright v1.56+): For quick checks without a collector, use page.consoleMessages() and page.pageErrors() after the fact — they return all messages/errors since page creation.

Network Inspection — verify API calls during UI flows

HelperWhen to use
helpers.startNetworkCapture(page, '/api/')Call BEFORE navigating. Captures request/response pairs. Optional URL filter.
helpers.getFailedRequests(collector)Get 4xx, 5xx, and connection failures from collector.
helpers.getCapturedRequests(collector)Get all captured request/response entries.
helpers.waitForApiResponse(page, '/api/users', {status: 200})Wait for a specific API call to complete. Returns {url, status, body, json}.

Lightweight alternative (Playwright v1.56+): page.requests() returns all requests since page creation — useful for quick post-hoc inspection without setting up a collector.

Browser State — inspect storage and cookies

HelperWhen to use
helpers.getLocalStorage(page)Get all localStorage entries. Pass a key for a single value.
helpers.getSessionStorage(page)Get all sessionStorage entries. Pass a key for a single value.
helpers.getCookies(context)Get all cookies from browser context.
helpers.clearAllStorage(page)Clear localStorage + sessionStorage + cookies. Use for clean-state testing.

Video Recording — record browser interactions

HelperWhen to use
helpers.createVideoContext(browser, {outputDir: '/tmp/videos'})Create a context that records video. Video saved when page/context closes.

Media Upload

Load the /media-upload skill when you need to upload video or files. It provides uploadToVimeo(), uploadToBunnyStream(), and uploadToBunnyStorage(). Setup: ./secrets/setup.sh --skill media-upload.

Resolution Presets — consistent dimensions per target

HelperWhen to use
helpers.RESOLUTION_PRESETSAccess preset configs. Keys: docs-retina, pr-standard, gif-compact. Each has viewport and deviceScaleFactor.
helpers.createPresetContext(browser, 'preset')Create a context with preset viewport + DPR. Replaces manual viewport/DPR config.

Media Conversion — screenshots to GIF

HelperWhen to use
helpers.screenshotsToGif(frames, path, opts)Convert PNG buffers to animated GIF. Options: width, height, fps, quality, annotations (per-frame click indicators + labels).

Accessibility — WCAG audits and keyboard navigation

HelperWhen to use
helpers.runAccessibilityAudit(page)Inject axe-core and run WCAG 2.0 AA audit. Returns violations with impact/description. Requires internet (CDN).
helpers.checkFocusOrder(page, ['#first', '#second', '#third'])Tab through elements and verify focus lands on expected selectors in order.

Performance Metrics — measure page speed

HelperWhen to use
helpers.capturePerformanceMetrics(page)Capture Navigation Timing (TTFB, DOM interactive) and Web Vitals (FCP, LCP, CLS). Call after page load.

Responsive Screenshots — multi-viewport sweep

HelperWhen to use
helpers.captureResponsiveScreenshots(page, url)Screenshot at mobile/tablet/desktop/wide breakpoints. Custom breakpoints and output dir optional.

Network Simulation — test degraded conditions

HelperWhen to use
helpers.simulateSlowNetwork(page, 500)Add artificial latency (ms) to all requests.
helpers.simulateOffline(context)Set browser to offline mode.
helpers.blockResources(page, ['image', 'font'])Block specific resource types (image, font, stylesheet, script, etc.).

Simulating specific failures: Use route.abort('connectionrefused') for targeted error simulation. Error types: 'connectionrefused', 'timedout', 'connectionreset', 'internetdisconnected'.

Tracing & Debugging

HelperWhen to use
helpers.startTracing(context, options?)Start recording a trace (DOM snapshots, screenshots, network). Call before page interactions.
helpers.stopTracing(context, path?)Stop tracing and save .zip. View with npx playwright show-trace trace.zip.

PDF Generation

HelperWhen to use
helpers.generatePdf(page, path?, options?)Generate PDF from current page. Options: format, tagged (accessible), outline (bookmarks), margin. Chromium headless only.

File Downloads

HelperWhen to use
helpers.waitForDownload(page, triggerAction, savePath?)Wait for a download triggered by an action, then save it. Returns {path, suggestedFilename, url}.

Layout Inspection — verify element positioning

HelperWhen to use
helpers.getElementBounds(page, '.selector')Get bounding box, visibility, viewport presence, and computed styles. Returns null for non-existent selectors, {visible: false} for hidden elements.

Page Structure Internals — parse ARIA snapshots standalone

HelperWhen to use
helpers.parseAriaSnapshot(yaml)Parse a Playwright ARIA snapshot YAML string into structured node objects. Each node has role, name, and optional level, checked, disabled, expanded, selected.
helpers.suggestSelector(node)Generate a getByRole(...) selector string from a parsed ARIA node.
helpers.INTERACTIVE_ROLESSet of interactive ARIA roles (button, link, textbox, checkbox, radio, combobox, slider, switch, tab, menuitem, searchbox, spinbutton, option).

Local Browser — connect to user's Chrome

These helpers live in lib/local-browser.js. Use const {connectToLocalBrowser, getConnectedPage, extractAuthState} = require('./lib/local-browser'); in scripts. See references/local-browser.md for full docs.

HelperWhen to use
connectToLocalBrowser(options?)Connect to user's running Chrome via extension bridge. Returns {browser, context, page, close()}. Requires Playwright MCP Bridge extension. Set PLAYWRIGHT_MCP_EXTENSION_TOKEN env var to bypass the approval dialog.
getConnectedPage(context, url?)Get the page exposed by the extension and optionally navigate. Note: context.newPage() does NOT work via the extension bridge — use this or the page from connectToLocalBrowser().
extractAuthState(context, options?)Extract cookies + localStorage (+ IndexedDB with {indexedDB: true}) from user's browser. Save to file with {path: '/tmp/auth.json'} for later reuse via helpers.loadAuthState().

Shadow DOM — interact with web components

HelperWhen to use
helpers.pierceShadowDOM(page, hostSelector, fn, args?)Execute a function inside an element's open shadow root. Works with Lit, Stencil, Radix, Shoelace, Salesforce Lightning, Angular Material CDK, and any web component.

The hostSelector can be a CSS selector string or a discovery object:

// CSS selector
await helpers.pierceShadowDOM(page, '#my-component', (sr) => {
  return sr.querySelector('button')?.textContent;
});

// Dynamic discovery — find host whose id contains a substring
await helpers.pierceShadowDOM(page, { matchId: 'my-widget' }, (sr) => {
  sr.querySelector('textarea')?.focus();
});

// Match by attribute
await helpers.pierceShadowDOM(page, { matchAttr: { name: 'data-widget', value: 'chat' } }, (sr) => {
  return sr.querySelectorAll('button').length;
});

// Pass arguments
await helpers.pierceShadowDOM(page, '#player', (sr, vol) => {
  sr.querySelector('input[type=range]').value = vol;
}, [75]);

Why this exists: Standard Playwright selectors (page.click, page.locator) do not pierce shadow DOM boundaries. The common workaround — page.evaluate() with manual element scanning — is verbose and error-prone, especially when shadow host IDs are dynamic (e.g., Radix generates inkeep-shadowradix-:r0:). This helper encapsulates the pierce-and-execute pattern.

DOM Stabilization — wait for streaming / real-time content

HelperWhen to use
helpers.waitForDOMStabilization(page, options?)Wait for a DOM region to stop changing. Use after triggering streaming AI responses, real-time data feeds, collaborative editing updates, or any content that arrives incrementally.
// Wait for chat response to finish streaming
const result = await helpers.waitForDOMStabilization(page, {
  selector: '.chat-messages',
  stableMs: 2000,        // content unchanged for 2s = done
  minContentLength: 100, // don't consider "stable" if still empty/loading
});
console.log(result.stable ? 'Streaming complete' : 'Timed out');

// Wait inside a shadow DOM host
await helpers.waitForDOMStabilization(page, {
  inShadowDOM: { matchId: 'my-widget' },
  selector: '[data-part="messages"]',
  stableMs: 3000,
});

Options: selector (default: 'body'), timeout (30s), stableMs (3s), pollInterval (500ms), minContentLength (0), inShadowDOM (null).

Returns {stable: boolean, elapsed: number, finalLength: number}.

CDP Performance Tracing — GPU rendering metrics

For detailed rendering pipeline measurement (raster tasks, paint events, compositing costs), load the reference:

Load: references/cdp-tracing.md

This covers: CDP Tracing.start/end with correct categories, Performance.getMetrics deltas, headed vs headless mode (critical — headless produces zero GPU events), DPR configuration for high-DPI stress testing, and trace event analysis.

Custom HTTP Headers

Configure custom headers for all HTTP requests via environment variables. Useful for:

  • Identifying automated traffic to your backend
  • Getting LLM-optimized responses (e.g., plain text errors instead of styled HTML)
  • Adding authentication tokens globally

Configuration

Single header (common case):

PW_HEADER_NAME=X-Automated-By PW_HEADER_VALUE=playwright-skill \
  cd $SKILL_DIR && node run.js /tmp/my-script.js

Multiple headers (JSON format):

PW_EXTRA_HEADERS='{"X-Automated-By":"playwright-skill","X-Debug":"true"}' \
  cd $SKILL_DIR && node run.js /tmp/my-script.js

How It Works

Headers are automatically applied when using helpers.createContext():

const context = await helpers.createContext(browser);
const page = await context.newPage();
// All requests from this page include your custom headers

For scripts using raw Playwright API, use the injected getContextOptionsWithHeaders():

const context = await browser.newContext(
  getContextOptionsWithHeaders({ viewport: { width: 1920, height: 1080 } }),
);

Advanced Usage

For comprehensive Playwright API documentation, see API_REFERENCE.md:

  • Selectors & Locators best practices
  • Network interception & API mocking
  • Authentication & session management
  • Visual regression testing
  • Mobile device emulation
  • Performance testing
  • Debugging techniques
  • CI/CD integration

Tips

  • CRITICAL: Detect servers FIRST - Always run detectDevServers() before writing test code for localhost testing
  • Custom headers - Use PW_HEADER_NAME/PW_HEADER_VALUE env vars to identify automated traffic to your backend
  • Use /tmp for test files - Write to /tmp/playwright-test-*.js, never to skill directory or user's project
  • Parameterize URLs - Put detected/provided URL in a TARGET_URL constant at the top of every script
  • DEFAULT: Headless browser - Always use headless: true for Docker/CI compatibility
  • Headed mode - Use headless: false when user specifically requests visible browser or is debugging locally
  • Wait strategies: Use waitForURL, waitForSelector, waitForLoadState instead of fixed timeouts
  • Error handling: Always use try-catch for robust automation
  • Console output: Use console.log() to track progress and show what's happening
  • Docker: The --no-sandbox flag is included by default in helpers for container compatibility
  • Time manipulation (Playwright v1.45+): Use page.clock to control time in tests — await page.clock.install() then await page.clock.fastForward('01:00') to advance, or await page.clock.pauseAt(new Date('2025-01-01')) to freeze at a specific moment. Useful for testing timers, countdowns, session expiry, and time-dependent UI.
  • WebSocket interception (Playwright v1.48+): Use page.routeWebSocket(url, handler) to mock or monitor WebSocket connections. The handler receives a WebSocketRoute with onMessage(), send(), and close(). Useful for testing real-time features (chat, notifications, live updates) without a running server.
  • Performance testing requires headed mode: Headless Chrome skips GPU compositing — Paint and CompositeLayers trace events will always be zero. Use headless: false for any rendering performance measurement. See references/cdp-tracing.md.
  • High-DPI performance testing: Use BOTH --force-device-scale-factor=N (Chrome flag, affects GPU rasterization) AND deviceScaleFactor: N (Playwright context, affects CSS/screenshots). They are different mechanisms. DPR 1 hides many GPU bottlenecks — use DPR 2-3 to stress-test rendering-sensitive paths.
  • Shadow DOM: Standard Playwright selectors don't pierce shadow boundaries. Use helpers.pierceShadowDOM() for any page using web components (Lit, Stencil, Radix, Salesforce Lightning, etc.).

Troubleshooting

Playwright not installed:

cd $SKILL_DIR && npm run setup

Module not found: Ensure running from skill directory via run.js wrapper

Browser doesn't launch in Docker: Ensure --no-sandbox and --disable-setuid-sandbox args are set (included by default in helpers)

Element not found: Add wait: await page.waitForSelector('.element', {timeout: 10000})

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Codex

35.23%
按下载量换算137

Claude

30.35%
按下载量换算118

Cursor

18.63%
按下载量换算72

Gemini CLI

9.35%
按下载量换算36

安全审计

Gen Agent Trust Hub

可疑

Socket

通过

Snyk

可疑

权限和风险

敏感数据

该 Skill 可能接触密钥、Token、环境变量或敏感配置,应进入高风险复核队列,默认不自动发布。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。来源安全扫描存在 warning/failed 结果,不能写成本站确认安全。当前只有一个来源,正式发布前建议补源仓库或其他目录站核验。

来源信息

继续浏览同类 Skills