[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M7149EB2AM8S8FTRDBWZ16SE":3,"skill-same:01M7149EB2AM8S8FTRDBWZ16SE":92},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":26,"publishedAt":46,"linkOnly":47,"markdownContent":48,"markdownBody":49,"frontMatter":50,"refName":52,"commitSha":53,"skillDirectory":54,"forksCount":14,"repositoryHtmlUrl":55,"repositoryHomepageUrl":6,"ownerHtmlUrl":56,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":57,"install":58,"resources":71},"01M7149EB2AM8S8FTRDBWZ16SE","playwright-skill",null,"IMPORTANT - Path Resolution: This skill can be installed in different locations (plugin system, manual installation, global, or project-specific). 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.","sapsapshen\u002Fai-memory-router","sapsapshen","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F20347959?v=4","01M6XDQEF1DP50DKF37H81J3J5","shenmian","01M6XDQEJPE41Y3DT2763X8ZJN",0,"MIT","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcopilot\u002Fplaywright-skill\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45],"playwright","playwright skill","2026","directory","unknown","path","can","manual","you","resolution","commands","project","installation","community","use","global","system","file","plugin","2026-11-10 10:12:30",false,"---\nname: playwright-skill\ndescription: \"IMPORTANT - Path Resolution: This skill can be installed in different locations (plugin system, manual installation, global, or project-specific). 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.\"\nrisk: unknown\nsource: community\ndate_added: \"2026-02-27\"\n---\n\n**IMPORTANT - Path Resolution:**\nThis skill can be installed in different locations (plugin system, manual installation, global, or project-specific). 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.\n\nCommon installation paths:\n\n- Plugin system: `~\u002F.claude\u002Fplugins\u002Fmarketplaces\u002Fplaywright-skill\u002Fskills\u002Fplaywright-skill`\n- Manual global: `~\u002F.claude\u002Fskills\u002Fplaywright-skill`\n- Project-specific: `\u003Cproject>\u002F.claude\u002Fskills\u002Fplaywright-skill`\n\n# Playwright Browser Automation\n\nGeneral-purpose browser automation skill. I'll write custom Playwright code for any automation task you request and execute it via the universal executor.\n\n**CRITICAL WORKFLOW - Follow these steps in order:**\n\n1. **Auto-detect dev servers** - For localhost testing, ALWAYS run server detection FIRST:\n\n   ```bash\n   cd $SKILL_DIR && node -e \"require('.\u002Flib\u002Fhelpers').detectDevServers().then(servers => console.log(JSON.stringify(servers)))\"\n   ```\n\n   - If **1 server found**: Use it automatically, inform user\n   - If **multiple servers found**: Ask user which one to test\n   - If **no servers found**: Ask for URL or offer to help start dev server\n\n2. **Write scripts to \u002Ftmp** - NEVER write test files to skill directory; always use `\u002Ftmp\u002Fplaywright-test-*.js`\n\n3. **Use visible browser by default** - Always use `headless: false` unless user specifically requests headless mode\n\n4. **Parameterize URLs** - Always make URLs configurable via environment variable or constant at top of script\n\n## How It Works\n\n1. You describe what you want to test\u002Fautomate\n2. I auto-detect running dev servers (or ask for URL if testing external site)\n3. I write custom Playwright code in `\u002Ftmp\u002Fplaywright-test-*.js` (won't clutter your project)\n4. I execute it via: `cd $SKILL_DIR && node run.js \u002Ftmp\u002Fplaywright-test-*.js`\n5. Results displayed in real-time, browser window visible for debugging\n6. Test files auto-cleaned from \u002Ftmp by your OS\n\n## Setup (First Time)\n\n```bash\ncd $SKILL_DIR\nnpm run setup\n```\n\nThis installs Playwright and Chromium browser. Only needed once.\n\n## Execution Pattern\n\n**Step 1: Detect dev servers (for localhost testing)**\n\n```bash\ncd $SKILL_DIR && node -e \"require('.\u002Flib\u002Fhelpers').detectDevServers().then(s => console.log(JSON.stringify(s)))\"\n```\n\n**Step 2: Write test script to \u002Ftmp with URL parameter**\n\n```javascript\n\u002F\u002F \u002Ftmp\u002Fplaywright-test-page.js\nconst { chromium } = require('playwright');\n\n\u002F\u002F Parameterized URL (detected or user-provided)\nconst TARGET_URL = 'http:\u002F\u002Flocalhost:3001'; \u002F\u002F \u003C-- Auto-detected or from user\n\n(async () => {\n  const browser = await chromium.launch({ headless: false });\n  const page = await browser.newPage();\n\n  await page.goto(TARGET_URL);\n  console.log('Page loaded:', await page.title());\n\n  await page.screenshot({ path: '\u002Ftmp\u002Fscreenshot.png', fullPage: true });\n  console.log('📸 Screenshot saved to \u002Ftmp\u002Fscreenshot.png');\n\n  await browser.close();\n})();\n```\n\n**Step 3: Execute from skill directory**\n\n```bash\ncd $SKILL_DIR && node run.js \u002Ftmp\u002Fplaywright-test-page.js\n```\n\n## Common Patterns\n\n### Test a Page (Multiple Viewports)\n\n```javascript\n\u002F\u002F \u002Ftmp\u002Fplaywright-test-responsive.js\nconst { chromium } = require('playwright');\n\nconst TARGET_URL = 'http:\u002F\u002Flocalhost:3001'; \u002F\u002F Auto-detected\n\n(async () => {\n  const browser = await chromium.launch({ headless: false, slowMo: 100 });\n  const page = await browser.newPage();\n\n  \u002F\u002F Desktop test\n  await page.setViewportSize({ width: 1920, height: 1080 });\n  await page.goto(TARGET_URL);\n  console.log('Desktop - Title:', await page.title());\n  await page.screenshot({ path: '\u002Ftmp\u002Fdesktop.png', fullPage: true });\n\n  \u002F\u002F Mobile test\n  await page.setViewportSize({ width: 375, height: 667 });\n  await page.screenshot({ path: '\u002Ftmp\u002Fmobile.png', fullPage: true });\n\n  await browser.close();\n})();\n```\n\n### Test Login Flow\n\n```javascript\n\u002F\u002F \u002Ftmp\u002Fplaywright-test-login.js\nconst { chromium } = require('playwright');\n\nconst TARGET_URL = 'http:\u002F\u002Flocalhost:3001'; \u002F\u002F Auto-detected\n\n(async () => {\n  const browser = await chromium.launch({ headless: false });\n  const page = await browser.newPage();\n\n  await page.goto(`${TARGET_URL}\u002Flogin`);\n\n  await page.fill('input[name=\"email\"]', 'test@example.com');\n  await page.fill('input[name=\"password\"]', 'password123');\n  await page.click('button[type=\"submit\"]');\n\n  \u002F\u002F Wait for redirect\n  await page.waitForURL('**\u002Fdashboard');\n  console.log('✅ Login successful, redirected to dashboard');\n\n  await browser.close();\n})();\n```\n\n### Fill and Submit Form\n\n```javascript\n\u002F\u002F \u002Ftmp\u002Fplaywright-test-form.js\nconst { chromium } = require('playwright');\n\nconst TARGET_URL = 'http:\u002F\u002Flocalhost:3001'; \u002F\u002F Auto-detected\n\n(async () => {\n  const browser = await chromium.launch({ headless: false, slowMo: 50 });\n  const page = await browser.newPage();\n\n  await page.goto(`${TARGET_URL}\u002Fcontact`);\n\n  await page.fill('input[name=\"name\"]', 'John Doe');\n  await page.fill('input[name=\"email\"]', 'john@example.com');\n  await page.fill('textarea[name=\"message\"]', 'Test message');\n  await page.click('button[type=\"submit\"]');\n\n  \u002F\u002F Verify submission\n  await page.waitForSelector('.success-message');\n  console.log('✅ Form submitted successfully');\n\n  await browser.close();\n})();\n```\n\n### Check for Broken Links\n\n```javascript\nconst { chromium } = require('playwright');\n\n(async () => {\n  const browser = await chromium.launch({ headless: false });\n  const page = await browser.newPage();\n\n  await page.goto('http:\u002F\u002Flocalhost:3000');\n\n  const links = await page.locator('a[href^=\"http\"]').all();\n  const results = { working: 0, broken: [] };\n\n  for (const link of links) {\n    const href = await link.getAttribute('href');\n    try {\n      const response = await page.request.head(href);\n      if (response.ok()) {\n        results.working++;\n      } else {\n        results.broken.push({ url: href, status: response.status() });\n      }\n    } catch (e) {\n      results.broken.push({ url: href, error: e.message });\n    }\n  }\n\n  console.log(`✅ Working links: ${results.working}`);\n  console.log(`❌ Broken links:`, results.broken);\n\n  await browser.close();\n})();\n```\n\n### Take Screenshot with Error Handling\n\n```javascript\nconst { chromium } = require('playwright');\n\n(async () => {\n  const browser = await chromium.launch({ headless: false });\n  const page = await browser.newPage();\n\n  try {\n    await page.goto('http:\u002F\u002Flocalhost:3000', {\n      waitUntil: 'networkidle',\n      timeout: 10000,\n    });\n\n    await page.screenshot({\n      path: '\u002Ftmp\u002Fscreenshot.png',\n      fullPage: true,\n    });\n\n    console.log('📸 Screenshot saved to \u002Ftmp\u002Fscreenshot.png');\n  } catch (error) {\n    console.error('❌ Error:', error.message);\n  } finally {\n    await browser.close();\n  }\n})();\n```\n\n### Test Responsive Design\n\n```javascript\n\u002F\u002F \u002Ftmp\u002Fplaywright-test-responsive-full.js\nconst { chromium } = require('playwright');\n\nconst TARGET_URL = 'http:\u002F\u002Flocalhost:3001'; \u002F\u002F Auto-detected\n\n(async () => {\n  const browser = await chromium.launch({ headless: false });\n  const page = await browser.newPage();\n\n  const viewports = [\n    { name: 'Desktop', width: 1920, height: 1080 },\n    { name: 'Tablet', width: 768, height: 1024 },\n    { name: 'Mobile', width: 375, height: 667 },\n  ];\n\n  for (const viewport of viewports) {\n    console.log(\n      `Testing ${viewport.name} (${viewport.width}x${viewport.height})`,\n    );\n\n    await page.setViewportSize({\n      width: viewport.width,\n      height: viewport.height,\n    });\n\n    await page.goto(TARGET_URL);\n    await page.waitForTimeout(1000);\n\n    await page.screenshot({\n      path: `\u002Ftmp\u002F${viewport.name.toLowerCase()}.png`,\n      fullPage: true,\n    });\n  }\n\n  console.log('✅ All viewports tested');\n  await browser.close();\n})();\n```\n\n## Inline Execution (Simple Tasks)\n\nFor quick one-off tasks, you can execute code inline without creating files:\n\n```bash\n# Take a quick screenshot\ncd $SKILL_DIR && node run.js \"\nconst browser = await chromium.launch({ headless: false });\nconst page = await browser.newPage();\nawait page.goto('http:\u002F\u002Flocalhost:3001');\nawait page.screenshot({ path: '\u002Ftmp\u002Fquick-screenshot.png', fullPage: true });\nconsole.log('Screenshot saved');\nawait browser.close();\n\"\n```\n\n**When to use inline vs files:**\n\n- **Inline**: Quick one-off tasks (screenshot, check if element exists, get page title)\n- **Files**: Complex tests, responsive design checks, anything user might want to re-run\n\n## Available Helpers\n\nOptional utility functions in `lib\u002Fhelpers.js`:\n\n```javascript\nconst helpers = require('.\u002Flib\u002Fhelpers');\n\n\u002F\u002F Detect running dev servers (CRITICAL - use this first!)\nconst servers = await helpers.detectDevServers();\nconsole.log('Found servers:', servers);\n\n\u002F\u002F Safe click with retry\nawait helpers.safeClick(page, 'button.submit', { retries: 3 });\n\n\u002F\u002F Safe type with clear\nawait helpers.safeType(page, '#username', 'testuser');\n\n\u002F\u002F Take timestamped screenshot\nawait helpers.takeScreenshot(page, 'test-result');\n\n\u002F\u002F Handle cookie banners\nawait helpers.handleCookieBanner(page);\n\n\u002F\u002F Extract table data\nconst data = await helpers.extractTableData(page, 'table.results');\n```\n\nSee `lib\u002Fhelpers.js` for full list.\n\n## Custom HTTP Headers\n\nConfigure custom headers for all HTTP requests via environment variables. Useful for:\n\n- Identifying automated traffic to your backend\n- Getting LLM-optimized responses (e.g., plain text errors instead of styled HTML)\n- Adding authentication tokens globally\n\n### Configuration\n\n**Single header (common case):**\n\n```bash\nPW_HEADER_NAME=X-Automated-By PW_HEADER_VALUE=playwright-skill \\\n  cd $SKILL_DIR && node run.js \u002Ftmp\u002Fmy-script.js\n```\n\n**Multiple headers (JSON format):**\n\n```bash\nPW_EXTRA_HEADERS='{\"X-Automated-By\":\"playwright-skill\",\"X-Debug\":\"true\"}' \\\n  cd $SKILL_DIR && node run.js \u002Ftmp\u002Fmy-script.js\n```\n\n### How It Works\n\nHeaders are automatically applied when using `helpers.createContext()`:\n\n```javascript\nconst context = await helpers.createContext(browser);\nconst page = await context.newPage();\n\u002F\u002F All requests from this page include your custom headers\n```\n\nFor scripts using raw Playwright API, use the injected `getContextOptionsWithHeaders()`:\n\n```javascript\nconst context = await browser.newContext(\n  getContextOptionsWithHeaders({ viewport: { width: 1920, height: 1080 } }),\n);\n```\n\n## Advanced Usage\n\nFor comprehensive Playwright API documentation, see [API_REFERENCE.md](API_REFERENCE.md):\n\n- Selectors & Locators best practices\n- Network interception & API mocking\n- Authentication & session management\n- Visual regression testing\n- Mobile device emulation\n- Performance testing\n- Debugging techniques\n- CI\u002FCD integration\n\n## Tips\n\n- **CRITICAL: Detect servers FIRST** - Always run `detectDevServers()` before writing test code for localhost testing\n- **Custom headers** - Use `PW_HEADER_NAME`\u002F`PW_HEADER_VALUE` env vars to identify automated traffic to your backend\n- **Use \u002Ftmp for test files** - Write to `\u002Ftmp\u002Fplaywright-test-*.js`, never to skill directory or user's project\n- **Parameterize URLs** - Put detected\u002Fprovided URL in a `TARGET_URL` constant at the top of every script\n- **DEFAULT: Visible browser** - Always use `headless: false` unless user explicitly asks for headless mode\n- **Headless mode** - Only use `headless: true` when user specifically requests \"headless\" or \"background\" execution\n- **Slow down:** Use `slowMo: 100` to make actions visible and easier to follow\n- **Wait strategies:** Use `waitForURL`, `waitForSelector`, `waitForLoadState` instead of fixed timeouts\n- **Error handling:** Always use try-catch for robust automation\n- **Console output:** Use `console.log()` to track progress and show what's happening\n\n## Troubleshooting\n\n**Playwright not installed:**\n\n```bash\ncd $SKILL_DIR && npm run setup\n```\n\n**Module not found:**\nEnsure running from skill directory via `run.js` wrapper\n\n**Browser doesn't open:**\nCheck `headless: false` and ensure display available\n\n**Element not found:**\nAdd wait: `await page.waitForSelector('.element', { timeout: 10000 })`\n\n## Example Usage\n\n```\nUser: \"Test if the marketing page looks good\"\n\nClaude: I'll test the marketing page across multiple viewports. Let me first detect running servers...\n[Runs: detectDevServers()]\n[Output: Found server on port 3001]\nI found your dev server running on http:\u002F\u002Flocalhost:3001\n\n[Writes custom automation script to \u002Ftmp\u002Fplaywright-test-marketing.js with URL parameterized]\n[Runs: cd $SKILL_DIR && node run.js \u002Ftmp\u002Fplaywright-test-marketing.js]\n[Shows results with screenshots from \u002Ftmp\u002F]\n```\n\n```\nUser: \"Check if login redirects correctly\"\n\nClaude: I'll test the login flow. First, let me check for running servers...\n[Runs: detectDevServers()]\n[Output: Found servers on ports 3000 and 3001]\nI found 2 dev servers. Which one should I test?\n- http:\u002F\u002Flocalhost:3000\n- http:\u002F\u002Flocalhost:3001\n\nUser: \"Use 3001\"\n\n[Writes login automation to \u002Ftmp\u002Fplaywright-test-login.js]\n[Runs: cd $SKILL_DIR && node run.js \u002Ftmp\u002Fplaywright-test-login.js]\n[Reports: ✅ Login successful, redirected to \u002Fdashboard]\n```\n\n## Notes\n\n- Each automation is custom-written for your specific request\n- Not limited to pre-built scripts - any browser task possible\n- Auto-detects running dev servers to eliminate hardcoded URLs\n- Test scripts written to `\u002Ftmp` for automatic cleanup (no clutter)\n- Code executes reliably with proper module resolution via `run.js`\n- Progressive disclosure - API_REFERENCE.md loaded only when advanced features needed\n\n## When to Use\nThis skill is applicable to execute the workflow or actions described in the overview.\n","**IMPORTANT - Path Resolution:**\nThis skill can be installed in different locations (plugin system, manual installation, global, or project-specific). 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.\n\nCommon installation paths:\n\n- Plugin system: `~\u002F.claude\u002Fplugins\u002Fmarketplaces\u002Fplaywright-skill\u002Fskills\u002Fplaywright-skill`\n- Manual global: `~\u002F.claude\u002Fskills\u002Fplaywright-skill`\n- Project-specific: `\u003Cproject>\u002F.claude\u002Fskills\u002Fplaywright-skill`\n\n# Playwright Browser Automation\n\nGeneral-purpose browser automation skill. I'll write custom Playwright code for any automation task you request and execute it via the universal executor.\n\n**CRITICAL WORKFLOW - Follow these steps in order:**\n\n1. **Auto-detect dev servers** - For localhost testing, ALWAYS run server detection FIRST:\n\n   ```bash\n   cd $SKILL_DIR && node -e \"require('.\u002Flib\u002Fhelpers').detectDevServers().then(servers => console.log(JSON.stringify(servers)))\"\n   ```\n\n   - If **1 server found**: Use it automatically, inform user\n   - If **multiple servers found**: Ask user which one to test\n   - If **no servers found**: Ask for URL or offer to help start dev server\n\n2. **Write scripts to \u002Ftmp** - NEVER write test files to skill directory; always use `\u002Ftmp\u002Fplaywright-test-*.js`\n\n3. **Use visible browser by default** - Always use `headless: false` unless user specifically requests headless mode\n\n4. **Parameterize URLs** - Always make URLs configurable via environment variable or constant at top of script\n\n## How It Works\n\n1. You describe what you want to test\u002Fautomate\n2. I auto-detect running dev servers (or ask for URL if testing external site)\n3. I write custom Playwright code in `\u002Ftmp\u002Fplaywright-test-*.js` (won't clutter your project)\n4. I execute it via: `cd $SKILL_DIR && node run.js \u002Ftmp\u002Fplaywright-test-*.js`\n5. Results displayed in real-time, browser window visible for debugging\n6. Test files auto-cleaned from \u002Ftmp by your OS\n\n## Setup (First Time)\n\n```bash\ncd $SKILL_DIR\nnpm run setup\n```\n\nThis installs Playwright and Chromium browser. Only needed once.\n\n## Execution Pattern\n\n**Step 1: Detect dev servers (for localhost testing)**\n\n```bash\ncd $SKILL_DIR && node -e \"require('.\u002Flib\u002Fhelpers').detectDevServers().then(s => console.log(JSON.stringify(s)))\"\n```\n\n**Step 2: Write test script to \u002Ftmp with URL parameter**\n\n```javascript\n\u002F\u002F \u002Ftmp\u002Fplaywright-test-page.js\nconst { chromium } = require('playwright');\n\n\u002F\u002F Parameterized URL (detected or user-provided)\nconst TARGET_URL = 'http:\u002F\u002Flocalhost:3001'; \u002F\u002F \u003C-- Auto-detected or from user\n\n(async () => {\n  const browser = await chromium.launch({ headless: false });\n  const page = await browser.newPage();\n\n  await page.goto(TARGET_URL);\n  console.log('Page loaded:', await page.title());\n\n  await page.screenshot({ path: '\u002Ftmp\u002Fscreenshot.png', fullPage: true });\n  console.log('📸 Screenshot saved to \u002Ftmp\u002Fscreenshot.png');\n\n  await browser.close();\n})();\n```\n\n**Step 3: Execute from skill directory**\n\n```bash\ncd $SKILL_DIR && node run.js \u002Ftmp\u002Fplaywright-test-page.js\n```\n\n## Common Patterns\n\n### Test a Page (Multiple Viewports)\n\n```javascript\n\u002F\u002F \u002Ftmp\u002Fplaywright-test-responsive.js\nconst { chromium } = require('playwright');\n\nconst TARGET_URL = 'http:\u002F\u002Flocalhost:3001'; \u002F\u002F Auto-detected\n\n(async () => {\n  const browser = await chromium.launch({ headless: false, slowMo: 100 });\n  const page = await browser.newPage();\n\n  \u002F\u002F Desktop test\n  await page.setViewportSize({ width: 1920, height: 1080 });\n  await page.goto(TARGET_URL);\n  console.log('Desktop - Title:', await page.title());\n  await page.screenshot({ path: '\u002Ftmp\u002Fdesktop.png', fullPage: true });\n\n  \u002F\u002F Mobile test\n  await page.setViewportSize({ width: 375, height: 667 });\n  await page.screenshot({ path: '\u002Ftmp\u002Fmobile.png', fullPage: true });\n\n  await browser.close();\n})();\n```\n\n### Test Login Flow\n\n```javascript\n\u002F\u002F \u002Ftmp\u002Fplaywright-test-login.js\nconst { chromium } = require('playwright');\n\nconst TARGET_URL = 'http:\u002F\u002Flocalhost:3001'; \u002F\u002F Auto-detected\n\n(async () => {\n  const browser = await chromium.launch({ headless: false });\n  const page = await browser.newPage();\n\n  await page.goto(`${TARGET_URL}\u002Flogin`);\n\n  await page.fill('input[name=\"email\"]', 'test@example.com');\n  await page.fill('input[name=\"password\"]', 'password123');\n  await page.click('button[type=\"submit\"]');\n\n  \u002F\u002F Wait for redirect\n  await page.waitForURL('**\u002Fdashboard');\n  console.log('✅ Login successful, redirected to dashboard');\n\n  await browser.close();\n})();\n```\n\n### Fill and Submit Form\n\n```javascript\n\u002F\u002F \u002Ftmp\u002Fplaywright-test-form.js\nconst { chromium } = require('playwright');\n\nconst TARGET_URL = 'http:\u002F\u002Flocalhost:3001'; \u002F\u002F Auto-detected\n\n(async () => {\n  const browser = await chromium.launch({ headless: false, slowMo: 50 });\n  const page = await browser.newPage();\n\n  await page.goto(`${TARGET_URL}\u002Fcontact`);\n\n  await page.fill('input[name=\"name\"]', 'John Doe');\n  await page.fill('input[name=\"email\"]', 'john@example.com');\n  await page.fill('textarea[name=\"message\"]', 'Test message');\n  await page.click('button[type=\"submit\"]');\n\n  \u002F\u002F Verify submission\n  await page.waitForSelector('.success-message');\n  console.log('✅ Form submitted successfully');\n\n  await browser.close();\n})();\n```\n\n### Check for Broken Links\n\n```javascript\nconst { chromium } = require('playwright');\n\n(async () => {\n  const browser = await chromium.launch({ headless: false });\n  const page = await browser.newPage();\n\n  await page.goto('http:\u002F\u002Flocalhost:3000');\n\n  const links = await page.locator('a[href^=\"http\"]').all();\n  const results = { working: 0, broken: [] };\n\n  for (const link of links) {\n    const href = await link.getAttribute('href');\n    try {\n      const response = await page.request.head(href);\n      if (response.ok()) {\n        results.working++;\n      } else {\n        results.broken.push({ url: href, status: response.status() });\n      }\n    } catch (e) {\n      results.broken.push({ url: href, error: e.message });\n    }\n  }\n\n  console.log(`✅ Working links: ${results.working}`);\n  console.log(`❌ Broken links:`, results.broken);\n\n  await browser.close();\n})();\n```\n\n### Take Screenshot with Error Handling\n\n```javascript\nconst { chromium } = require('playwright');\n\n(async () => {\n  const browser = await chromium.launch({ headless: false });\n  const page = await browser.newPage();\n\n  try {\n    await page.goto('http:\u002F\u002Flocalhost:3000', {\n      waitUntil: 'networkidle',\n      timeout: 10000,\n    });\n\n    await page.screenshot({\n      path: '\u002Ftmp\u002Fscreenshot.png',\n      fullPage: true,\n    });\n\n    console.log('📸 Screenshot saved to \u002Ftmp\u002Fscreenshot.png');\n  } catch (error) {\n    console.error('❌ Error:', error.message);\n  } finally {\n    await browser.close();\n  }\n})();\n```\n\n### Test Responsive Design\n\n```javascript\n\u002F\u002F \u002Ftmp\u002Fplaywright-test-responsive-full.js\nconst { chromium } = require('playwright');\n\nconst TARGET_URL = 'http:\u002F\u002Flocalhost:3001'; \u002F\u002F Auto-detected\n\n(async () => {\n  const browser = await chromium.launch({ headless: false });\n  const page = await browser.newPage();\n\n  const viewports = [\n    { name: 'Desktop', width: 1920, height: 1080 },\n    { name: 'Tablet', width: 768, height: 1024 },\n    { name: 'Mobile', width: 375, height: 667 },\n  ];\n\n  for (const viewport of viewports) {\n    console.log(\n      `Testing ${viewport.name} (${viewport.width}x${viewport.height})`,\n    );\n\n    await page.setViewportSize({\n      width: viewport.width,\n      height: viewport.height,\n    });\n\n    await page.goto(TARGET_URL);\n    await page.waitForTimeout(1000);\n\n    await page.screenshot({\n      path: `\u002Ftmp\u002F${viewport.name.toLowerCase()}.png`,\n      fullPage: true,\n    });\n  }\n\n  console.log('✅ All viewports tested');\n  await browser.close();\n})();\n```\n\n## Inline Execution (Simple Tasks)\n\nFor quick one-off tasks, you can execute code inline without creating files:\n\n```bash\n# Take a quick screenshot\ncd $SKILL_DIR && node run.js \"\nconst browser = await chromium.launch({ headless: false });\nconst page = await browser.newPage();\nawait page.goto('http:\u002F\u002Flocalhost:3001');\nawait page.screenshot({ path: '\u002Ftmp\u002Fquick-screenshot.png', fullPage: true });\nconsole.log('Screenshot saved');\nawait browser.close();\n\"\n```\n\n**When to use inline vs files:**\n\n- **Inline**: Quick one-off tasks (screenshot, check if element exists, get page title)\n- **Files**: Complex tests, responsive design checks, anything user might want to re-run\n\n## Available Helpers\n\nOptional utility functions in `lib\u002Fhelpers.js`:\n\n```javascript\nconst helpers = require('.\u002Flib\u002Fhelpers');\n\n\u002F\u002F Detect running dev servers (CRITICAL - use this first!)\nconst servers = await helpers.detectDevServers();\nconsole.log('Found servers:', servers);\n\n\u002F\u002F Safe click with retry\nawait helpers.safeClick(page, 'button.submit', { retries: 3 });\n\n\u002F\u002F Safe type with clear\nawait helpers.safeType(page, '#username', 'testuser');\n\n\u002F\u002F Take timestamped screenshot\nawait helpers.takeScreenshot(page, 'test-result');\n\n\u002F\u002F Handle cookie banners\nawait helpers.handleCookieBanner(page);\n\n\u002F\u002F Extract table data\nconst data = await helpers.extractTableData(page, 'table.results');\n```\n\nSee `lib\u002Fhelpers.js` for full list.\n\n## Custom HTTP Headers\n\nConfigure custom headers for all HTTP requests via environment variables. Useful for:\n\n- Identifying automated traffic to your backend\n- Getting LLM-optimized responses (e.g., plain text errors instead of styled HTML)\n- Adding authentication tokens globally\n\n### Configuration\n\n**Single header (common case):**\n\n```bash\nPW_HEADER_NAME=X-Automated-By PW_HEADER_VALUE=playwright-skill \\\n  cd $SKILL_DIR && node run.js \u002Ftmp\u002Fmy-script.js\n```\n\n**Multiple headers (JSON format):**\n\n```bash\nPW_EXTRA_HEADERS='{\"X-Automated-By\":\"playwright-skill\",\"X-Debug\":\"true\"}' \\\n  cd $SKILL_DIR && node run.js \u002Ftmp\u002Fmy-script.js\n```\n\n### How It Works\n\nHeaders are automatically applied when using `helpers.createContext()`:\n\n```javascript\nconst context = await helpers.createContext(browser);\nconst page = await context.newPage();\n\u002F\u002F All requests from this page include your custom headers\n```\n\nFor scripts using raw Playwright API, use the injected `getContextOptionsWithHeaders()`:\n\n```javascript\nconst context = await browser.newContext(\n  getContextOptionsWithHeaders({ viewport: { width: 1920, height: 1080 } }),\n);\n```\n\n## Advanced Usage\n\nFor comprehensive Playwright API documentation, see [API_REFERENCE.md](API_REFERENCE.md):\n\n- Selectors & Locators best practices\n- Network interception & API mocking\n- Authentication & session management\n- Visual regression testing\n- Mobile device emulation\n- Performance testing\n- Debugging techniques\n- CI\u002FCD integration\n\n## Tips\n\n- **CRITICAL: Detect servers FIRST** - Always run `detectDevServers()` before writing test code for localhost testing\n- **Custom headers** - Use `PW_HEADER_NAME`\u002F`PW_HEADER_VALUE` env vars to identify automated traffic to your backend\n- **Use \u002Ftmp for test files** - Write to `\u002Ftmp\u002Fplaywright-test-*.js`, never to skill directory or user's project\n- **Parameterize URLs** - Put detected\u002Fprovided URL in a `TARGET_URL` constant at the top of every script\n- **DEFAULT: Visible browser** - Always use `headless: false` unless user explicitly asks for headless mode\n- **Headless mode** - Only use `headless: true` when user specifically requests \"headless\" or \"background\" execution\n- **Slow down:** Use `slowMo: 100` to make actions visible and easier to follow\n- **Wait strategies:** Use `waitForURL`, `waitForSelector`, `waitForLoadState` instead of fixed timeouts\n- **Error handling:** Always use try-catch for robust automation\n- **Console output:** Use `console.log()` to track progress and show what's happening\n\n## Troubleshooting\n\n**Playwright not installed:**\n\n```bash\ncd $SKILL_DIR && npm run setup\n```\n\n**Module not found:**\nEnsure running from skill directory via `run.js` wrapper\n\n**Browser doesn't open:**\nCheck `headless: false` and ensure display available\n\n**Element not found:**\nAdd wait: `await page.waitForSelector('.element', { timeout: 10000 })`\n\n## Example Usage\n\n```\nUser: \"Test if the marketing page looks good\"\n\nClaude: I'll test the marketing page across multiple viewports. Let me first detect running servers...\n[Runs: detectDevServers()]\n[Output: Found server on port 3001]\nI found your dev server running on http:\u002F\u002Flocalhost:3001\n\n[Writes custom automation script to \u002Ftmp\u002Fplaywright-test-marketing.js with URL parameterized]\n[Runs: cd $SKILL_DIR && node run.js \u002Ftmp\u002Fplaywright-test-marketing.js]\n[Shows results with screenshots from \u002Ftmp\u002F]\n```\n\n```\nUser: \"Check if login redirects correctly\"\n\nClaude: I'll test the login flow. First, let me check for running servers...\n[Runs: detectDevServers()]\n[Output: Found servers on ports 3000 and 3001]\nI found 2 dev servers. Which one should I test?\n- http:\u002F\u002Flocalhost:3000\n- http:\u002F\u002Flocalhost:3001\n\nUser: \"Use 3001\"\n\n[Writes login automation to \u002Ftmp\u002Fplaywright-test-login.js]\n[Runs: cd $SKILL_DIR && node run.js \u002Ftmp\u002Fplaywright-test-login.js]\n[Reports: ✅ Login successful, redirected to \u002Fdashboard]\n```\n\n## Notes\n\n- Each automation is custom-written for your specific request\n- Not limited to pre-built scripts - any browser task possible\n- Auto-detects running dev servers to eliminate hardcoded URLs\n- Test scripts written to `\u002Ftmp` for automatic cleanup (no clutter)\n- Code executes reliably with proper module resolution via `run.js`\n- Progressive disclosure - API_REFERENCE.md loaded only when advanced features needed\n\n## When to Use\nThis skill is applicable to execute the workflow or actions described in the overview.\n",{"name":5,"risk":31,"source":40,"date_added":51,"description":7},"2026-02-27","main","7a38d8e7073e61c16eda85681cb32fb68a8657a5","skills-store\u002Fcopilot\u002Fplaywright-skill","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router","https:\u002F\u002Fgithub.com\u002Fsapsapshen","2026-04-14 21:12:12",{"source":8,"skillName":5,"commands":59,"prompt":69,"zipAvailable":70},[60,63,66],{"tool":61,"command":62},"npx","npx skills add sapsapshen\u002Fai-memory-router --skill playwright-skill",{"tool":64,"command":65},"bunx","bunx skills add sapsapshen\u002Fai-memory-router --skill playwright-skill",{"tool":67,"command":68},"pnpm","pnpm dlx skills add sapsapshen\u002Fai-memory-router --skill playwright-skill","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcopilot\u002Fplaywright-skill\u002FSKILL.md",true,[72,78,84,88],{"path":73,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":76,"sourceHtmlUrl":77},"API_REFERENCE.md","REFERENCE","参考文档",16366,"https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcopilot\u002Fplaywright-skill\u002FAPI_REFERENCE.md",{"path":79,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":82,"sourceHtmlUrl":83},"lib\u002Fhelpers.js","SCRIPT","脚本",12605,"https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcopilot\u002Fplaywright-skill\u002Flib\u002Fhelpers.js",{"path":85,"resourceType":74,"resourceTypeDesc":75,"sizeBytes":86,"sourceHtmlUrl":87},"package.json",652,"https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcopilot\u002Fplaywright-skill\u002Fpackage.json",{"path":89,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":90,"sourceHtmlUrl":91},"run.js",5788,"https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcopilot\u002Fplaywright-skill\u002Frun.js",[93,117,143,153,166,183],{"skillNo":94,"name":95,"nameZh":6,"description":96,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":97,"categories":98,"keywords":104,"publishedAt":46},"01M71ATQRKG48SH06FQ697T5G9","wechat-search","Search WeChat Official Account articles using OpenClaw's web search and fetch capabilities with compliance-focused design.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fwechat-search-release\u002FSKILL.md",[99],{"categoryNo":100,"categoryName":101,"taxonomyType":102,"taxonomyTypeDesc":103},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[105,106,107,108,109,110,111,112,113,114,115,116],"wechat search","wechat","search","capabilities","openclaw","design","web search","compliance","web","official","fetch","with",{"skillNo":118,"name":119,"nameZh":6,"description":120,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":121,"categories":122,"keywords":123,"publishedAt":46},"01M71ATNAVGZB35PFYM2GC5N00","trend-tap","Real-time trending topics aggregator across 7 platforms (X\u002FTwitter, Reddit, Google Trends, Hacker News, Zhihu, Bilibili, Weibo). Trigger: when user says 'trend', 'trends', 'trend tap', '热点', '热搜', or asks what is trending\u002Fhot. Do NOT use web_search — this skill already fetches live data from all platforms with zero API keys.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftrend-tap\u002FSKILL.md",[],[124,125,126,127,128,129,107,130,131,132,133,134,135,136,137,138,139,140,141,142],"trend","trend tap","tap","api keys","Bash","time","trending","hacker","aggregator","do","when","brew","live","data","google","install","read","across","api",{"skillNo":144,"name":145,"nameZh":6,"description":146,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":147,"categories":148,"keywords":149,"publishedAt":46},"01M71ATJ9AM5KM15AA8PS302EB","trend-radar","Real-time trending topics aggregator across 7 platforms (X\u002FTwitter, Reddit, Google Trends, Hacker News, Zhihu, Bilibili, Weibo). Trigger: when user says 'trend', 'trends', 'trend pulse', '热点', '热搜', or asks what is trending\u002Fhot. Do NOT use web_search — this skill already fetches live data from all platforms with zero API keys.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftrend-radar\u002FSKILL.md",[],[150,151,124,135,136,137,138,128,107,140,152,141,142,134,132,131,130,139,129,127],"trend radar","radar","pulse",{"skillNo":154,"name":155,"nameZh":6,"description":156,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":157,"categories":158,"keywords":159,"publishedAt":46},"01M71ATGF095N3EJM9G8SD0BB5","tech-news-daily","每日科技新闻汇——从全球多个 RSS 源抓取最新科技新闻，翻译（英→中）、去重、排版成每日简报。当用户询问科技新闻、科技新鲜事、每日科技、技术动态、Daily Tech News 或类似表达时触发此技能。","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftech-news-daily\u002FSKILL.md",[],[160,161,162,163,164,165],"news","tech","tech news daily","daily","rss","翻译",{"skillNo":167,"name":168,"nameZh":6,"description":169,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":170,"categories":171,"keywords":175,"publishedAt":46},"01M71ATFAEAMQ17M70NQNY7SR8","summarize","Summarize URLs or files with the summarize CLI (web, PDFs, images, audio, YouTube).","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fsummarize\u002FSKILL.md",[172],{"categoryNo":173,"categoryName":174,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000021","资料检索与研究",[168,113,176,177,178,116,135,179,180,181,126,182,139],"Audio","cli","youtube","images","https","files","pdfs",{"skillNo":184,"name":185,"nameZh":6,"description":186,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":187,"categories":188,"keywords":189,"publishedAt":46},"01M71ATDNS3R0M4H936PACQAWZ","sonoscli","Control Sonos speakers (discover\u002Fstatus\u002Fplay\u002Fvolume\u002Fgroup).","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fsonoscli\u002FSKILL.md",[],[185,190,191,192,139,193,180,194,195,196,197,198],"status","play","go","latest","github","discover","control","cmd","volume"]