[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-e1c80c0e-d523-428d-858e-a1cf10b3f4d9":3,"$fYjQBYQC_WHIHwucNS87982iMzMuM9Qe8V1PfsFgAXhU":43},{"id":4,"title":5,"description":6,"categoryId":7,"moduleId":8,"tags":9,"prompt":10,"icon":11,"source":12,"sourceUrl":13,"authorId":14,"authorName":15,"isPublic":16,"stars":17,"runs":18,"createdAt":19,"updatedAt":19,"module":20,"category":27,"packages":34},"e1c80c0e-d523-428d-858e-a1cf10b3f4d9","browser-automation","浏览器自动化推动网页测试、抓取和人工智能代理","cat_prod_automation","mod_productivity","sickn33,productivity","---\nname: browser-automation\ndescription: Browser automation powers web testing, scraping, and AI agent\n  interactions. The difference between a flaky script and a reliable system\n  comes down to understanding selectors, waiting strategies, and anti-detection\n  patterns.\nrisk: unknown\nsource: vibeship-spawner-skills (Apache 2.0)\ndate_added: 2026-02-27\n---\n\n# Browser Automation\n\nBrowser automation powers web testing, scraping, and AI agent interactions.\nThe difference between a flaky script and a reliable system comes down to\nunderstanding selectors, waiting strategies, and anti-detection patterns.\n\nThis skill covers Playwright (recommended) and Puppeteer, with patterns for\ntesting, scraping, and agentic browser control. Key insight: Playwright won\nthe framework war. Unless you need Puppeteer's stealth ecosystem or are\nChrome-only, Playwright is the better choice in 2025.\n\nCritical distinction: Testing automation (predictable apps you control) vs\nscraping\u002Fagent automation (unpredictable sites that fight back). Different\nproblems, different solutions.\n\n## Principles\n\n- Use user-facing locators (getByRole, getByText) over CSS\u002FXPath\n- Never add manual waits - Playwright's auto-wait handles it\n- Each test\u002Ftask should be fully isolated with fresh context\n- Screenshots and traces are your debugging lifeline\n- Headless for CI, headed for debugging\n- Anti-detection is cat-and-mouse - stay current or get blocked\n\n## Capabilities\n\n- browser-automation\n- playwright\n- puppeteer\n- headless-browsers\n- web-scraping\n- browser-testing\n- e2e-testing\n- ui-automation\n- selenium-alternatives\n\n## Scope\n\n- api-testing → backend\n- load-testing → performance-thinker\n- accessibility-testing → accessibility-specialist\n- visual-regression-testing → ui-design\n\n## Tooling\n\n### Frameworks\n\n- Playwright - When: Default choice - cross-browser, auto-waiting, best DX Note: 96% success rate, 4.5s avg execution, Microsoft-backed\n- Puppeteer - When: Chrome-only, need stealth plugins, existing codebase Note: 75% success rate at scale, but best stealth ecosystem\n- Selenium - When: Legacy systems, specific language bindings Note: Slower, more verbose, but widest browser support\n\n### Stealth_tools\n\n- puppeteer-extra-plugin-stealth - When: Need to bypass bot detection with Puppeteer Note: Gold standard for anti-detection\n- playwright-extra - When: Stealth plugins for Playwright Note: Port of puppeteer-extra ecosystem\n- undetected-chromedriver - When: Selenium anti-detection Note: Dynamic bypass of detection\n\n### Cloud_browsers\n\n- Browserbase - When: Managed headless infrastructure Note: Built-in stealth mode, session management\n- BrowserStack - When: Cross-browser testing at scale Note: Real devices, CI integration\n\n## Patterns\n\n### Test Isolation Pattern\n\nEach test runs in complete isolation with fresh state\n\n**When to use**: Testing, any automation that needs reproducibility\n\n# TEST ISOLATION:\n\n\"\"\"\nEach test gets its own:\n- Browser context (cookies, storage)\n- Fresh page\n- Clean state\n\"\"\"\n\n## Playwright Test Example\n\"\"\"\nimport { test, expect } from '@playwright\u002Ftest';\n\n\u002F\u002F Each test runs in isolated browser context\ntest('user can add item to cart', async ({ page }) => {\n  \u002F\u002F Fresh context - no cookies, no storage from other tests\n  await page.goto('\u002Fproducts');\n  await page.getByRole('button', { name: 'Add to Cart' }).click();\n  await expect(page.getByTestId('cart-count')).toHaveText('1');\n});\n\ntest('user can remove item from cart', async ({ page }) => {\n  \u002F\u002F Completely isolated - cart is empty\n  await page.goto('\u002Fcart');\n  await expect(page.getByText('Your cart is empty')).toBeVisible();\n});\n\"\"\"\n\n## Shared Authentication Pattern\n\"\"\"\n\u002F\u002F Save auth state once, reuse across tests\n\u002F\u002F setup.ts\nimport { test as setup } from '@playwright\u002Ftest';\n\nsetup('authenticate', async ({ page }) => {\n  await page.goto('\u002Flogin');\n  await page.getByLabel('Email').fill('user@example.com');\n  await page.getByLabel('Password').fill('password');\n  await page.getByRole('button', { name: 'Sign in' }).click();\n\n  \u002F\u002F Wait for auth to complete\n  await page.waitForURL('\u002Fdashboard');\n\n  \u002F\u002F Save authentication state\n  await page.context().storageState({\n    path: '.\u002Fplaywright\u002F.auth\u002Fuser.json'\n  });\n});\n\n\u002F\u002F playwright.config.ts\nexport default defineConfig({\n  projects: [\n    { name: 'setup', testMatch: \u002F.*\\.setup\\.ts\u002F },\n    {\n      name: 'tests',\n      dependencies: ['setup'],\n      use: {\n        storageState: '.\u002Fplaywright\u002F.auth\u002Fuser.json',\n      },\n    },\n  ],\n});\n\"\"\"\n\n### User-Facing Locator Pattern\n\nSelect elements the way users see them\n\n**When to use**: Always - the default approach for selectors\n\n# USER-FACING LOCATORS:\n\n\"\"\"\nPriority order:\n1. getByRole  - Best: matches accessibility tree\n2. getByText  - Good: matches visible content\n3. getByLabel - Good: matches form labels\n4. getByTestId - Fallback: explicit test contracts\n5. CSS\u002FXPath - Last resort: fragile, avoid\n\"\"\"\n\n## Good Examples (User-Facing)\n\"\"\"\n\u002F\u002F By role - THE BEST CHOICE\nawait page.getByRole('button', { name: 'Submit' }).click();\nawait page.getByRole('link', { name: 'Sign up' }).click();\nawait page.getByRole('heading', { name: 'Dashboard' }).isVisible();\nawait page.getByRole('textbox', { name: 'Search' }).fill('query');\n\n\u002F\u002F By text content\nawait page.getByText('Welcome back').isVisible();\nawait page.getByText(\u002FOrder #\\d+\u002F).click();  \u002F\u002F Regex supported\n\n\u002F\u002F By label (forms)\nawait page.getByLabel('Email address').fill('user@example.com');\nawait page.getByLabel('Password').fill('secret');\n\n\u002F\u002F By placeholder\nawait page.getByPlaceholder('Search...').fill('query');\n\n\u002F\u002F By test ID (when no user-facing option works)\nawait page.getByTestId('submit-button').click();\n\"\"\"\n\n## Bad Examples (Fragile)\n\"\"\"\n\u002F\u002F DON'T - CSS selectors tied to structure\nawait page.locator('.btn-primary.submit-form').click();\nawait page.locator('#header > div > button:nth-child(2)').click();\n\n\u002F\u002F DON'T - XPath tied to structure\nawait page.locator('\u002F\u002Fdiv[@class=\"form\"]\u002Fbutton[1]').click();\n\n\u002F\u002F DON'T - Auto-generated selectors\nawait page.locator('[data-v-12345]').click();\n\"\"\"\n\n## Filtering and Chaining\n\"\"\"\n\u002F\u002F Filter by containing text\nawait page.getByRole('listitem')\n  .filter({ hasText: 'Product A' })\n  .getByRole('button', { name: 'Add to cart' })\n  .click();\n\n\u002F\u002F Filter by NOT containing\nawait page.getByRole('listitem')\n  .filter({ hasNotText: 'Sold out' })\n  .first()\n  .click();\n\n\u002F\u002F Chain locators\nconst row = page.getByRole('row', { name: 'John Doe' });\nawait row.getByRole('button', { name: 'Edit' }).click();\n\"\"\"\n\n### Auto-Wait Pattern\n\nLet Playwright wait automatically, never add manual waits\n\n**When to use**: Always with Playwright\n\n# AUTO-WAIT PATTERN:\n\n\"\"\"\nPlaywright waits automatically for:\n- Element to be attached to DOM\n- Element to be visible\n- Element to be stable (not animating)\n- Element to receive events\n- Element to be enabled\n\nNEVER add manual waits!\n\"\"\"\n\n## Wrong - Manual Waits\n\"\"\"\n\u002F\u002F DON'T DO THIS\nawait page.goto('\u002Fdashboard');\nawait page.waitForTimeout(2000);  \u002F\u002F NO! Arbitrary wait\nawait page.click('.submit-button');\n\n\u002F\u002F DON'T DO THIS\nawait page.waitForSelector('.loading-spinner', { state: 'hidden' });\nawait page.waitForTimeout(500);  \u002F\u002F \"Just to be safe\" - NO!\n\"\"\"\n\n## Correct - Let Auto-Wait Work\n\"\"\"\n\u002F\u002F Auto-waits for button to be clickable\nawait page.getByRole('button', { name: 'Submit' }).click();\n\n\u002F\u002F Auto-waits for text to appear\nawait expect(page.getByText('Success!')).toBeVisible();\n\n\u002F\u002F Auto-waits for navigation to complete\nawait page.goto('\u002Fdashboard');\n\u002F\u002F Page is ready - no manual wait needed\n\"\"\"\n\n## When You DO Need to Wait\n\"\"\"\n\u002F\u002F Wait for specific network request\nconst responsePromise = page.waitForResponse(\n  response => response.url().includes('\u002Fapi\u002Fdata')\n);\nawait page.getByRole('button', { name: 'Load' }).click();\nconst response = await responsePromise;\n\n\u002F\u002F Wait for URL change\nawait Promise.all([\n  page.waitForURL('**\u002Fdashboard'),\n  page.getByRole('button', { name: 'Login' }).click(),\n]);\n\n\u002F\u002F Wait for download\nconst downloadPromise = page.waitForEvent('download');\nawait page.getByText('Export CSV').click();\nconst download = await downloadPromise;\n\"\"\"\n\n### Stealth Browser Pattern\n\nAvoid bot detection for scraping\n\n**When to use**: Scraping sites with anti-bot protection\n\n# STEALTH BROWSER PATTERN:\n\n\"\"\"\nBot detection checks for:\n- navigator.webdriver property\n- Chrome DevTools protocol artifacts\n- Browser fingerprint inconsistencies\n- Behavioral patterns (perfect timing, no mouse movement)\n- Headless indicators\n\"\"\"\n\n## Puppeteer Stealth (Best Anti-Detection)\n\"\"\"\nimport puppeteer from 'puppeteer-extra';\nimport StealthPlugin from 'puppeteer-extra-plugin-stealth';\n\npuppeteer.use(StealthPlugin());\n\nconst browser = await puppeteer.launch({\n  headless: 'new',\n  args: [\n    '--no-sandbox',\n    '--disable-setuid-sandbox',\n    '--disable-blink-features=AutomationControlled',\n  ],\n});\n\nconst page = await browser.newPage();\n\n\u002F\u002F Set realistic viewport\nawait page.setViewport({ width: 1920, height: 1080 });\n\n\u002F\u002F Realistic user agent\nawait page.setUserAgent(\n  'Mozilla\u002F5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\u002F537.36 ' +\n  '(KHTML, like Gecko) Chrome\u002F120.0.0.0 Safari\u002F537.36'\n);\n\n\u002F\u002F Navigate with human-like behavior\nawait page.goto('https:\u002F\u002Ftarget-site.com', {\n  waitUntil: 'networkidle0',\n});\n\"\"\"\n\n## Playwright Stealth\n\"\"\"\nimport { chromium } from 'playwright-extra';\nimport stealth from 'puppeteer-extra-plugin-stealth';\n\nchromium.use(stealth());\n\nconst browser = await chromium.launch({ headless: true });\nconst context = await browser.newContext({\n  viewport: { width: 1920, height: 1080 },\n  userAgent: 'Mozilla\u002F5.0 ...',\n  locale: 'en-US',\n  timezoneId: 'America\u002FNew_York',\n});\n\"\"\"\n\n## Human-Like Behavior\n\"\"\"\n\u002F\u002F Random delays between actions\nconst randomDelay = (min: number, max: number) =>\n  new Promise(r => setTimeout(r, Math.random() * (max - min) + min));\n\nawait page.goto(url);\nawait randomDelay(500, 1500);\n\n\u002F\u002F Mouse movement before click\nconst button = await page.$('button.submit');\nconst box = await button.boundingBox();\nawait page.mouse.move(\n  box.x + box.width \u002F 2,\n  box.y + box.height \u002F 2,\n  { steps: 10 }  \u002F\u002F Move in steps like a human\n);\nawait randomDelay(100, 300);\nawait button.click();\n\n\u002F\u002F Scroll naturally\nawait page.evaluate(() => {\n  window.scrollBy({\n    top: 300 + Math.random() * 200,\n    behavior: 'smooth'\n  });\n});\n\"\"\"\n\n### Error Recovery Pattern\n\nHandle failures gracefully with screenshots and retries\n\n**When to use**: Any production automation\n\n# ERROR RECOVERY PATTERN:\n\n## Automatic Screenshot on Failure\n\"\"\"\n\u002F\u002F playwright.config.ts\nexport default defineConfig({\n  use: {\n    screenshot: 'only-on-failure',\n    trace: 'retain-on-failure',\n    video: 'retain-on-failure',\n  },\n  retries: 2,  \u002F\u002F Retry failed tests\n});\n\"\"\"\n\n## Try-Catch with Debug Info\n\"\"\"\nasync function scrapeProduct(page: Page, url: string) {\n  try {\n    await page.goto(url, { timeout: 30000 });\n\n    const title = await page.getByRole('heading', { level: 1 }).textContent();\n    const price = await page.getByTestId('price').textContent();\n\n    return { title, price, success: true };\n\n  } catch (error) {\n    \u002F\u002F Capture debug info\n    const screenshot = await page.screenshot({\n      path: `errors\u002F${Date.now()}-error.png`,\n      fullPage: true\n    });\n\n    const html = await page.content();\n    await fs.writeFile(`errors\u002F${Date.now()}-page.html`, html);\n\n    console.error({\n      url,\n      error: error.message,\n      currentUrl: page.url(),\n    });\n\n    return { success: false, error: error.message };\n  }\n}\n\"\"\"\n\n## Retry with Exponential Backoff\n\"\"\"\nasync function withRetry\u003CT>(\n  fn: () => Promise\u003CT>,\n  maxRetries = 3,\n  baseDelay = 1000\n): Promise\u003CT> {\n  let lastError: Error;\n\n  for (let attempt = 0; attempt \u003C maxRetries; attempt++) {\n    try {\n      return await fn();\n    } catch (error) {\n      lastError = error;\n\n      if (attempt \u003C maxRetries - 1) {\n        const delay = baseDelay * Math.pow(2, attempt);\n        const jitter = delay * 0.1 * Math.random();\n        await new Promise(r => setTimeout(r, delay + jitter));\n      }\n    }\n  }\n\n  throw lastError;\n}\n\n\u002F\u002F Usage\nconst result = await withRetry(\n  () => scrapeProduct(page, url),\n  3,\n  2000\n);\n\"\"\"\n\n### Parallel Execution Pattern\n\nRun tests\u002Ftasks in parallel for speed\n\n**When to use**: Multiple independent pages or tests\n\n# PARALLEL EXECUTION:\n\n## Playwright Test Parallelization\n\"\"\"\n\u002F\u002F playwright.config.ts\nexport default defineConfig({\n  fullyParallel: true,\n  workers: process.env.CI ? 4 : undefined,  \u002F\u002F CI: 4 workers, local: CPU-based\n\n  projects: [\n    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },\n    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },\n    { name: 'webkit', use: { ...devices['Desktop Safari'] } },\n  ],\n});\n\"\"\"\n\n## Browser Contexts for Parallel Scraping\n\"\"\"\nconst browser = await chromium.launch();\n\nconst urls = ['url1', 'url2', 'url3', 'url4', 'url5'];\n\n\u002F\u002F Create multiple contexts - each is isolated\nconst results = await Promise.all(\n  urls.map(async (url) => {\n    const context = await browser.newContext();\n    const page = await context.newPage();\n\n    try {\n      await page.goto(url);\n      const data = await extractData(page);\n      return { url, data, success: true };\n    } catch (error) {\n      return { url, error: error.message, success: false };\n    } finally {\n      await context.close();\n    }\n  })\n);\n\nawait browser.close();\n\"\"\"\n\n## Rate-Limited Parallel Processing\n\"\"\"\nimport pLimit from 'p-limit';\n\nconst limit = pLimit(5);  \u002F\u002F Max 5 concurrent\n\nconst results = await Promise.all(\n  urls.map(url => limit(async () => {\n    const context = await browser.newContext();\n    const page = await context.newPage();\n\n    \u002F\u002F Random delay between requests\n    await new Promise(r => setTimeout(r, Math.random() * 2000));\n\n    try {\n      return await scrapePage(page, url);\n    } finally {\n      await context.close();\n    }\n  }))\n);\n\"\"\"\n\n### Network Interception Pattern\n\nMock, block, or modify network requests\n\n**When to use**: Testing, blocking ads\u002Fanalytics, modifying responses\n\n# NETWORK INTERCEPTION:\n\n## Block Unnecessary Resources\n\"\"\"\nawait page.route('**\u002F*', (route) => {\n  const url = route.request().url();\n  const resourceType = route.request().resourceType();\n\n  \u002F\u002F Block images, fonts, analytics for faster scraping\n  if (['image', 'font', 'media'].includes(resourceType)) {\n    return route.abort();\n  }\n\n  \u002F\u002F Block tracking\u002Fanalytics\n  if (url.includes('google-analytics') ||\n      url.includes('facebook.com\u002Ftr')) {\n    return route.abort();\n  }\n\n  return route.continue();\n});\n\"\"\"\n\n## Mock API Responses (Testing)\n\"\"\"\nawait page.route('**\u002Fapi\u002Fproducts', async (route) => {\n  await route.fulfill({\n    status: 200,\n    contentType: 'application\u002Fjson',\n    body: JSON.stringify([\n      { id: 1, name: 'Mock Product', price: 99.99 },\n    ]),\n  });\n});\n\n\u002F\u002F Now page will receive mocked data\nawait page.goto('\u002Fproducts');\n\"\"\"\n\n## Capture API Responses\n\"\"\"\nconst apiResponses: any[] = [];\n\npage.on('response', async (response) => {\n  if (response.url().includes('\u002Fapi\u002F')) {\n    const data = await response.json().catch(() => null);\n    apiResponses.push({\n      url: response.url(),\n      status: response.status(),\n      data,\n    });\n  }\n});\n\nawait page.goto('\u002Fdashboard');\n\u002F\u002F apiResponses now contains all API calls\n\"\"\"\n\n## Sharp Edges\n\n### Using waitForTimeout Instead of Proper Waits\n\nSeverity: CRITICAL\n\nSituation: Waiting for elements or page state\n\nSymptoms:\nTests pass locally, fail in CI. Pass 9 times, fail on the 10th.\n\"Element not found\" errors that seem random. Tests take 30+ seconds\nwhen they should take 3.\n\nWhy this breaks:\nwaitForTimeout is a fixed delay. If the page loads in 500ms, you wait\n2000ms anyway. If the page takes 2100ms (CI is slower), you fail.\nThere's no correct value - it's always either too short or too long.\n\nRecommended fix:\n\n# REMOVE all waitForTimeout calls\n\n# WRONG:\nawait page.goto('\u002Fdashboard');\nawait page.waitForTimeout(2000);  # Arbitrary!\nawait page.click('.submit');\n\n# CORRECT - Auto-wait handles it:\nawait page.goto('\u002Fdashboard');\nawait page.getByRole('button', { name: 'Submit' }).click();\n\n# If you need to wait for specific condition:\nawait expect(page.getByText('Dashboard')).toBeVisible();\nawait page.waitForURL('**\u002Fdashboard');\nawait page.waitForResponse(resp => resp.url().includes('\u002Fapi\u002Fdata'));\n\n# For animations, wait for element to be stable:\nawait page.getByRole('button').click();  # Auto-waits for stable\n\n# NEVER use setTimeout or waitForTimeout in production code\n\n### CSS Selectors Tied to Styling Classes\n\nSeverity: HIGH\n\nSituation: Selecting elements for interaction\n\nSymptoms:\nTests break after CSS refactoring. Selectors like .btn-primary stop\nworking. Frontend redesign breaks all tests without changing behavior.\n\nWhy this breaks:\nCSS class names are implementation details for styling, not semantic\nmeaning. When designers change from .btn-primary to .button--primary,\nyour tests break even though behavior is identical.\n\nRecommended fix:\n\n# Use user-facing locators instead:\n\n# WRONG - Tied to CSS:\nawait page.locator('.btn-primary.submit-form').click();\nawait page.locator('#sidebar > div.menu > ul > li:nth-child(3)').click();\n\n# CORRECT - User-facing:\nawait page.getByRole('button', { name: 'Submit' }).click();\nawait page.getByRole('menuitem', { name: 'Settings' }).click();\n\n# If you must use CSS, use data-testid:\n\u003Cbutton data-testid=\"submit-order\">Submit\u003C\u002Fbutton>\n\nawait page.getByTestId('submit-order').click();\n\n# Locator priority:\n# 1. getByRole - matches accessibility\n# 2. getByText - matches visible content\n# 3. getByLabel - matches form labels\n# 4. getByTestId - explicit test contract\n# 5. CSS\u002FXPath - last resort only\n\n### navigator.webdriver Exposes Automation\n\nSeverity: HIGH\n\nSituation: Scraping sites with bot detection\n\nSymptoms:\nImmediate 403 errors. CAPTCHA challenges. Empty pages. \"Access Denied\"\nmessages. Works for 1 request, then gets blocked.\n\nWhy this breaks:\nBy default, headless browsers set navigator.webdriver = true. This is\nthe first thing bot detection checks. It's a bright red flag that\nsays \"I'm automated.\"\n\nRecommended fix:\n\n# Use stealth plugins:\n\n### Puppeteer Stealth (best option):\nimport puppeteer from 'puppeteer-extra';\nimport StealthPlugin from 'puppeteer-extra-plugin-stealth';\n\npuppeteer.use(StealthPlugin());\n\nconst browser = await puppeteer.launch({\n  headless: 'new',\n  args: ['--disable-blink-features=AutomationControlled'],\n});\n\n### Playwright Stealth:\nimport { chromium } from 'playwright-extra';\nimport stealth from 'puppeteer-extra-plugin-stealth';\n\nchromium.use(stealth());\n\n### Manual (partial):\nawait page.evaluateOnNewDocument(() => {\n  Object.defineProperty(navigator, 'webdriver', {\n    get: () => undefined,\n  });\n});\n\n# Note: This is cat-and-mouse. Detection evolves.\n# For serious scraping, consider managed solutions like Browserbase.\n\n### Tests Share State and Affect Each Other\n\nSeverity: HIGH\n\nSituation: Running multiple tests in sequence\n\nSymptoms:\nTests pass individually but fail when run together. Order matters -\ntest B fails if test A runs first. Random failures that \"fix themselves\"\non rerun.\n\nWhy this breaks:\nShared browser context means shared cookies, localStorage, and session\nstate. Test A logs in, test B expects logged-out state. Test A adds\nitem to cart, test B's cart count is wrong.\n\nRecommended fix:\n\n# Each test must be fully isolated:\n\n### Playwright Test (automatic isolation):\ntest('first test', async ({ page }) => {\n  \u002F\u002F Fresh context, fresh page\n});\n\ntest('second test', async ({ page }) => {\n  \u002F\u002F Completely isolated from first test\n});\n\n### Manual isolation:\nconst context = await browser.newContext();  \u002F\u002F Fresh context\nconst page = await context.newPage();\n\u002F\u002F ... test code ...\nawait context.close();  \u002F\u002F Clean up\n\n## Shared authentication (the right way):\n\u002F\u002F 1. Save auth state to file\nawait context.storageState({ path: '.\u002Fauth.json' });\n\n\u002F\u002F 2. Reuse in other tests\nconst context = await browser.newContext({\n  storageState: '.\u002Fauth.json'\n});\n\n# Never modify global state in tests\n# Never rely on previous test's actions\n\n### No Trace Capture for CI Failures\n\nSeverity: MEDIUM\n\nSituation: Debugging test failures in CI\n\nSymptoms:\n\"Test failed in CI\" with no useful information. Can't reproduce\nlocally. Screenshot shows page but not what went wrong. Guessing\nat root cause.\n\nWhy this breaks:\nCI runs headless on different hardware. Timing is different. Network\nis different. Without traces, you can't see what actually happened -\nthe sequence of actions, network requests, console logs.\n\nRecommended fix:\n\n# Enable traces for failures:\n\n### playwright.config.ts:\nexport default defineConfig({\n  use: {\n    trace: 'retain-on-failure',    # Keep trace on failure\n    screenshot: 'only-on-failure', # Screenshot on failure\n    video: 'retain-on-failure',    # Video on failure\n  },\n  outputDir: '.\u002Ftest-results',\n});\n\n### View trace locally:\nnpx playwright show-trace test-results\u002Fpath\u002Fto\u002Ftrace.zip\n\n### In CI, upload test-results as artifact:\n# GitHub Actions:\n- uses: actions\u002Fupload-artifact@v3\n  if: failure()\n  with:\n    name: playwright-traces\n    path: test-results\u002F\n\n# Trace shows:\n# - Timeline of actions\n# - Screenshots at each step\n# - Network requests and responses\n# - Console logs\n# - DOM snapshots\n\n### Tests Pass Headed but Fail Headless\n\nSeverity: MEDIUM\n\nSituation: Running tests in headless mode for CI\n\nSymptoms:\nWorks perfectly when you watch it. Fails mysteriously in CI.\n\"Element not visible\" in headless but visible in headed mode.\n\nWhy this breaks:\nHeadless browsers have no display, which affects some CSS (visibility\ncalculations), viewport sizing, and font rendering. Some animations\nbehave differently. Popup windows may not work.\n\nRecommended fix:\n\n# Set consistent viewport:\nconst browser = await chromium.launch({\n  headless: true,\n});\n\nconst context = await browser.newContext({\n  viewport: { width: 1280, height: 720 },\n});\n\n# Or in config:\nexport default defineConfig({\n  use: {\n    viewport: { width: 1280, height: 720 },\n  },\n});\n\n# Debug headless failures:\n# 1. Run with headed mode locally\nnpx playwright test --headed\n\n# 2. Slow down to watch\nnpx playwright test --headed --slowmo 100\n\n# 3. Use trace viewer for CI failures\nnpx playwright show-trace trace.zip\n\n# 4. For stubborn issues, screenshot at failure point:\nawait page.screenshot({ path: 'debug.png', fullPage: true });\n\n### Getting Blocked by Rate Limiting\n\nSeverity: HIGH\n\nSituation: Scraping multiple pages quickly\n\nSymptoms:\nWorks for first 50 pages, then 429 errors. Suddenly all requests fail.\nIP gets blocked. CAPTCHA starts appearing after successful requests.\n\nWhy this breaks:\nSites monitor request patterns. 100 requests per second from one IP\nis obviously automated. Rate limits protect servers and catch scrapers.\n\nRecommended fix:\n\n# Add delays between requests:\n\nconst randomDelay = () =>\n  new Promise(r => setTimeout(r, 1000 + Math.random() * 2000));\n\nfor (const url of urls) {\n  await randomDelay();  \u002F\u002F 1-3 second delay\n  await page.goto(url);\n  \u002F\u002F ... scrape ...\n}\n\n# Use rotating proxies:\nconst proxies = ['http:\u002F\u002Fproxy1:8080', 'http:\u002F\u002Fproxy2:8080'];\nlet proxyIndex = 0;\n\nconst getNextProxy = () => proxies[proxyIndex++ % proxies.length];\n\nconst context = await browser.newContext({\n  proxy: { server: getNextProxy() },\n});\n\n# Limit concurrent requests:\nimport pLimit from 'p-limit';\nconst limit = pLimit(3);  \u002F\u002F Max 3 concurrent\n\nawait Promise.all(\n  urls.map(url => limit(() => scrapePage(url)))\n);\n\n# Rotate user agents:\nconst userAgents = [\n  'Mozilla\u002F5.0 (Windows...',\n  'Mozilla\u002F5.0 (Macintosh...',\n];\n\nawait page.setExtraHTTPHeaders({\n  'User-Agent': userAgents[Math.floor(Math.random() * userAgents.length)]\n});\n\n### New Windows\u002FPopups Not Handled\n\nSeverity: MEDIUM\n\nSituation: Clicking links that open new windows\n\nSymptoms:\nClick button, nothing happens. Test hangs. \"Window not found\" errors.\nActions succeed but verification fails because you're on wrong page.\n\nWhy this breaks:\ntarget=\"_blank\" links open new windows. Your page reference still\npoints to the original page. The new window exists but you're not\nlistening for it.\n\nRecommended fix:\n\n# Wait for popup BEFORE triggering it:\n\n### New window\u002Ftab:\nconst pagePromise = context.waitForEvent('page');\nawait page.getByRole('link', { name: 'Open in new tab' }).click();\nconst newPage = await pagePromise;\nawait newPage.waitForLoadState();\n\n\u002F\u002F Now interact with new page\nawait expect(newPage.getByRole('heading')).toBeVisible();\n\n\u002F\u002F Close when done\nawait newPage.close();\n\n### Popup windows:\nconst popupPromise = page.waitForEvent('popup');\nawait page.getByRole('button', { name: 'Open popup' }).click();\nconst popup = await popupPromise;\nawait popup.waitForLoadState();\n\n### Multiple windows:\nconst pages = context.pages();  \u002F\u002F Get all open pages\n\n### Can't Interact with Elements in iframes\n\nSeverity: MEDIUM\n\nSituation: Page contains embedded iframes\n\nSymptoms:\nElement clearly visible but \"not found\". Selector works in DevTools\nbut not in Playwright. Parent page selectors work, iframe content\ndoesn't.\n\nWhy this breaks:\niframes are separate documents. page.locator only searches the main\nframe. You need to explicitly get the iframe's frame to interact\nwith its contents.\n\nRecommended fix:\n\n# Get frame by name or selector:\n\n### By frame name:\nconst frame = page.frame('payment-iframe');\nawait frame.getByRole('textbox', { name: 'Card number' }).fill('4242...');\n\n## By selector:\nconst frame = page.frameLocator('iframe#payment');\nawait frame.getByRole('textbox', { name: 'Card number' }).fill('4242...');\n\n### Nested iframes:\nconst outer = page.frameLocator('iframe#outer');\nconst inner = outer.frameLocator('iframe#inner');\nawait inner.getByRole('button').click();\n\n### Wait for iframe to load:\nawait page.waitForSelector('iframe#payment');\nconst frame = page.frameLocator('iframe#payment');\nawait frame.getByText('Secure Payment').waitFor();\n\n## Validation Checks\n\n### Using waitForTimeout\n\nSeverity: ERROR\n\nwaitForTimeout causes flaky tests and slow execution\n\nMessage: Using waitForTimeout - remove it. Playwright auto-waits for elements. Use waitForResponse, waitForURL, or assertions instead.\n\n### Using setTimeout in Test Code\n\nSeverity: WARNING\n\nsetTimeout is unreliable for timing in tests\n\nMessage: Using setTimeout instead of Playwright waits. Replace with await expect(...).toBeVisible() or page.waitFor*.\n\n### Custom Sleep Function\n\nSeverity: WARNING\n\nSleep functions indicate improper waiting strategy\n\nMessage: Custom sleep function detected. Use Playwright's built-in waiting mechanisms instead.\n\n### CSS Class Selector Used\n\nSeverity: WARNING\n\nCSS class selectors are fragile\n\nMessage: Using CSS class selector. Prefer getByRole, getByText, getByLabel, or getByTestId for more stable selectors.\n\n### nth-child CSS Selector\n\nSeverity: WARNING\n\nPosition-based selectors are very fragile\n\nMessage: Using position-based selector. These break when DOM order changes. Use user-facing locators instead.\n\n### XPath Selector Used\n\nSeverity: INFO\n\nXPath should be last resort\n\nMessage: Using XPath selector. Consider getByRole, getByText first. XPath should be last resort for complex DOM traversal.\n\n### Auto-Generated Selector\n\nSeverity: WARNING\n\nFramework-generated selectors are extremely fragile\n\nMessage: Using auto-generated selector. These change on every build. Use data-testid instead.\n\n### Puppeteer Without Stealth Plugin\n\nSeverity: INFO\n\nScraping without stealth is easily detected\n\nMessage: Using Puppeteer without stealth plugin. Consider puppeteer-extra-plugin-stealth for anti-detection.\n\n### navigator.webdriver Not Hidden\n\nSeverity: INFO\n\nnavigator.webdriver exposes automation\n\nMessage: Launching browser without hiding automation flags. For scraping, add stealth measures.\n\n### Scraping Loop Without Error Handling\n\nSeverity: WARNING\n\nOne failure shouldn't crash entire scrape\n\nMessage: Scraping loop without try\u002Fcatch. One page failure will crash the entire scrape. Add error handling.\n\n## Collaboration\n\n### Delegation Triggers\n\n- user needs full desktop control beyond browser -> computer-use-agents (Desktop automation for non-browser apps)\n- user needs API testing alongside browser tests -> backend (API integration and testing patterns)\n- user needs testing strategy -> test-architect (Overall test architecture decisions)\n- user needs visual regression testing -> ui-design (Visual comparison and design validation)\n- user needs browser automation in workflows -> workflow-automation (Durable execution for browser tasks)\n- user building browser tools for agents -> agent-tool-builder (Tool design patterns for LLM agents)\n\n## Related Skills\n\nWorks well with: `agent-tool-builder`, `workflow-automation`, `computer-use-agents`, `test-architect`\n\n## When to Use\n- User mentions or implies: playwright\n- User mentions or implies: puppeteer\n- User mentions or implies: browser automation\n- User mentions or implies: headless\n- User mentions or implies: web scraping\n- User mentions or implies: e2e test\n- User mentions or implies: end-to-end\n- User mentions or implies: selenium\n- User mentions or implies: chromium\n- User mentions or implies: browser test\n- User mentions or implies: page.click\n- User mentions or implies: locator\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.\n","","imported","https:\u002F\u002Fgithub.com\u002Fsickn33\u002Fantigravity-awesome-skills","user_system_seed","SkillOPIC",true,230,1440,"2026-05-16 13:09:26",{"id":8,"name":21,"slug":22,"icon":23,"description":24,"sort":25,"createdAt":26},"效率工具","productivity","mdi-lightning-bolt-outline","文档处理、数据分析、自动化工作流",4,"2026-05-16 12:53:40",{"id":7,"name":28,"slug":29,"icon":30,"description":31,"moduleId":8,"sort":32,"skillCount":33,"createdAt":26},"自动化","automation","mdi-robot-outline","工作流自动化、批处理",3,101,[35],{"id":36,"skillId":4,"version":37,"fileName":38,"fileSize":39,"filePath":40,"fileHash":41,"manifest":42,"createdAt":19},"2a5d9768-5690-49e2-a0c3-8ed40df5d53d","1.0.0","browser-automation.zip",10291,"uploads\u002Fskills\u002Fe1c80c0e-d523-428d-858e-a1cf10b3f4d9\u002Fbrowser-automation.zip","55c3e048c8367eb4d90e0594a2ad8f9caab49cefa329522bf92a31e7ac30fd6f","[{\"path\":\"SKILL.md\",\"isDirectory\":false,\"size\":29490}]",{"code":44,"message":45,"data":46},200,"success",{"items":47,"stats":48,"page":51},[],{"averageRating":49,"totalRatings":49,"ratingCounts":50},0,[49,49,49,49,49],{"limit":52,"offset":49,"hasMore":53,"nextOffset":52,"ratedOnly":16},15,false]