Claude Cursor Skill

browser-automation

Imported from alexei-led/cc-thingz/dist/codex/browser/skills/browser-automation.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download alexei-led-cc-thingz-dist_codex_browser_skills_browser-automation-ce56bb4.zip · 20 KB
Part of alexei-led/cc-thingz — 91 skills

Install

skills CLI npx skills add https://github.com/alexei-led/cc-thingz/tree/master/dist/codex/browser/skills/browser-automation
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install alexei-led-cc-thingz@llmmart
Git git clone https://github.com/alexei-led/cc-thingz.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole alexei-led/cc-thingz collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Browser Automation

Prove rendered behavior in a real browser and report pass, fail, or blocked with evidence. Keep automation temporary unless the user asks for permanent tests.

Runtime

Use the cheapest runtime that proves the claim:

  1. Browser tools exposed in the current session. See references/platform-browser-tools.md.
  2. The project's configured browser runner. Infer the package manager from the lockfile; do not invent a runner.
  3. The bundled Playwright scripts in this skill's scripts/ directory. Read references/playwright.md for setup, the script skeleton, helpers, and custom headers.
  4. None available: report blocked and name the missing tool or package.

Rules

  • Target: reuse a reachable dev server; start one only when its command is known. Ask when no server or several servers are found.
  • Data: use seeded users, fixed dates, reset state, and mocked external services. Credentials, production data, and destructive actions need explicit user approval.
  • Locators: role, label, text, or test id first; CSS last.
  • Waiting: wait on observable state such as a selector, URL, network response, or accessibility snapshot. Never add fixed sleeps. For SPA or HTMX pages, assert the DOM after swaps and client-side route changes.
  • Headless: when the platform exposes no visible browser (Pi, CI, most CLIs), use headless screenshots plus a manifest as visual evidence. Use headed mode only when the user can see the browser.
  • Files: write generated scripts and artifacts to /tmp/playwright-*. Write to the project only when the user asked for permanent tests, and never write into the skill directory.
  • Failures: fix the app or tests only when that is in scope. After two failed scoped attempts, save evidence, quote the failing line or UI state, and stop.
  • Permanent tests: done when the relevant build/test/lint checks pass on what you changed, or you name each check that did not run and why.

Bundled Playwright scripts

Run them by absolute path from the caller's working directory, where <skill-dir> is the directory that contains this SKILL.md. Prefer the screenshot scripts over custom batch scripts:

node <skill-dir>/scripts/screenshot-url.js --url <url> --selector <ready-selector> \
  --out /tmp/playwright-page.png --json
node <skill-dir>/scripts/screenshot-sequence.js --url-template '<url/{n}>' --from 1 --to 10 \
  --selector <ready-selector> --out-dir /tmp/playwright-shots --json
node <skill-dir>/scripts/run.js --json /tmp/playwright-check.js
  • Manifests record URL, title, screenshot path, viewport, console errors, network failures, and HTTP responses with status >=400.
  • run.js keeps the caller's working directory and writes its status logs to stderr. Pass --json or --quiet whenever stdout must carry only the script's JSON: without them, a first-run Playwright install also writes to stdout. Playwright globals such as chromium and helpers stay available when the script also uses require("fs") or require("path").

Platform additions

No target-specific additions.

Output

## Browser Automation Result

Target: <page, feature, or flow>
Runtime: <built-in browser | project runner | bundled Playwright | blocked>
Actions: <commands or browser actions>
Result: <pass | fail | blocked>
Evidence: <screenshot, manifest, or trace paths, or the key observation>
Next fix: <only when failing or blocked>

Report blocked, not pass, when the check did not run.

Files (cc-thingz)
  • references
    • platform-browser-tools.md 907 B
      # Platform Browser Tools
      
      Choose the runtime from the tools visible in the current session. Prefer an
      exposed browser tool over installing a new runtime. When none fits, fall back to
      the project runner, then the bundled Playwright scripts.
      
      ## Claude Code
      
      Use Claude in Chrome browser tools when they are visible, for interactive
      exploration, authenticated sessions, form flows, screenshots, console logs, and
      rendered-state checks. If the user wants Claude in Chrome and no browser tools
      are visible, tell them to enable `claude-in-chrome` with `/mcp`.
      
      ## Codex
      
      Use the Codex app browser tools when Browser use is visible. Codex CLI has no
      native browser tool; use Playwright MCP when its tools are visible, otherwise
      the fallbacks above.
      
      ## Pi
      
      Use browser tools if the session exposes them. Pi usually does not show the
      agent a visible browser, so use headless screenshots and manifests as evidence.
      
    • playwright.md 4.6 KB
      # Bundled Playwright Scripts
      
      Read this when browser-automation falls back to the scripts in `scripts/`:
      runtime setup, the runner contract, a script skeleton, helpers, screenshot
      flags, and custom headers. For the Playwright API itself, use the official docs:
      [locators](https://playwright.dev/docs/locators),
      [auto-waiting](https://playwright.dev/docs/actionability),
      [network](https://playwright.dev/docs/network),
      [emulation](https://playwright.dev/docs/emulation),
      [authentication](https://playwright.dev/docs/auth).
      
      The scripts derive from lackeyjb's playwright-skill, MIT License.
      
      `<skill-dir>` below is the directory that contains the browser-automation
      `SKILL.md`. Run every command from the caller's working directory.
      
      ## Runtime setup
      
      - Requires Node.js 22+ and npm.
      - The runner uses the caller project's Playwright package first, keeping that
        project's version. Otherwise it installs exactly Playwright 1.63.0 into
        `$XDG_CACHE_HOME/cc-thingz/playwright/1.63.0` (default `~/.cache`), never
        into the skill directory.
      - Browser binaries are a separate install and may need network access:
      
        ```bash
        node <skill-dir>/scripts/setup-runtime.js chromium
        node <skill-dir>/scripts/setup-runtime.js firefox webkit
        ```
      
      - A missing-browser error prints the matching Playwright CLI install command;
        use it so binaries match the package version.
      - A successful package import does not prove the browser launches. Verify with
        a screenshot of `https://example.com` and check the manifest.
      - Chromium sandboxing stays on. Set `PLAYWRIGHT_SKILL_NO_SANDBOX=1` only when the
        environment requires it and the trust boundary permits it.
      
      ## Dev server detection
      
      ```bash
      node <skill-dir>/scripts/run.js --json \
        "console.log(JSON.stringify(await helpers.detectDevServers()))"
      ```
      
      ## Runner contract
      
      `node <skill-dir>/scripts/run.js [--quiet|--json] <file | inline code>`, or code
      on stdin:
      
      - Keeps the caller's working directory and resolves input paths before running.
      - Wraps code so top-level `await` works.
      - Writes status logs to stderr. `--quiet` and `--json` also silence the
        first-run Playwright install, which otherwise writes to stdout. The script
        must keep its own logs off stdout too.
      - Exposes `chromium`, `firefox`, `webkit`, `devices`, `helpers`, and
        `getContextOptionsWithHeaders(opts)` as globals, alongside normal
        `require("fs")`, `require("path")`, or `require("playwright")`.
      - Writes no temporary files into the skill directory.
      
      ## Script skeleton
      
      ```javascript
      const TARGET_URL = process.env.TARGET_URL || "http://localhost:3000";
      const SCREENSHOT = "/tmp/playwright-check.png";
      
      const browser = await chromium.launch({ headless: true });
      const context = await browser.newContext(getContextOptionsWithHeaders());
      const page = await context.newPage();
      
      try {
        await page.goto(TARGET_URL, { waitUntil: "domcontentloaded" });
        await helpers.waitForStablePage(page, { selector: "main" });
      
        // user flow here
      
        await page.screenshot({ path: SCREENSHOT, fullPage: true });
        console.log(JSON.stringify({ screenshot: SCREENSHOT }));
      } finally {
        await browser.close();
      }
      ```
      
      `networkidle` can be too weak or too strict for SPAs. Wait for a selector that
      proves the target UI rendered; `helpers.waitForStablePage(page, { selector,
      animationFrames })` also waits for fonts and animation frames.
      
      ## Helpers
      
      Signatures are in `scripts/lib/helpers.js`. Main ones: `launchBrowser`,
      `createContext`, `waitForStablePage`, `waitForPageReady`, `safeClick`,
      `safeType`, `takeScreenshot`, `authenticate`, and `detectDevServers`.
      
      ## Screenshot flags
      
      Both screenshot scripts accept `--selector`, `--out` or `--out-dir`,
      `--manifest`, `--json`, `--viewport 1280x720`, `--title-selector h1`,
      `--viewport-only`, and `--headed`. `screenshot-sequence.js` adds
      `--url-template` with `{n}`, `--from`, `--to`, `--step -1`, and
      `--continue-on-error`.
      
      ## Custom headers
      
      Set these before running `run.js` or a screenshot script to add headers to
      every request:
      
      ```bash
      PW_HEADER_NAME=X-Automated-By PW_HEADER_VALUE=browser-automation \
        node <skill-dir>/scripts/run.js /tmp/playwright-check.js
      PW_EXTRA_HEADERS='{"X-Automated-By":"browser-automation","X-Debug":"true"}' \
        node <skill-dir>/scripts/run.js /tmp/playwright-check.js
      ```
      
      `helpers.createContext(browser)` applies them. For a raw
      `browser.newContext(...)`, wrap the options with
      `getContextOptionsWithHeaders(...)`.
      
      ## Troubleshooting
      
      - `run.js` not found: use the absolute `<skill-dir>` path.
      - Element not found: check iframes, visibility, and locator uniqueness.
      - Timeout: inspect load state, network activity, and selector state before
        raising timeouts.
      - Syntax error: quote the failing line and fix that section before rerunning.
      
  • scripts
    • lib
      • helpers.js 18.2 KB
        // Reusable utility functions for Playwright automation.
        
        const fs = require("fs");
        const os = require("os");
        const path = require("path");
        const {
          loadPlaywright,
          ensureBrowserAvailable,
          sandboxOptions,
        } = require("./runtime");
        
        function isQuiet() {
          return process.env.PLAYWRIGHT_SKILL_QUIET === "1";
        }
        
        let playwrightBrowsers = null;
        
        /**
         * Ensure Playwright is installed and its browser launchers (chromium,
         * firefox, webkit) are loaded, memoizing the result. Loading Playwright can
         * trigger a blocking install, so callers must invoke this explicitly (e.g.
         * at the top of an entry point) instead of relying on import-time side
         * effects. Safe to call more than once; only loads once per process.
         * @param {Object} options - forwarded to loadPlaywright (e.g. { quiet })
         * @returns {Object} { chromium, firefox, webkit }
         */
        function ensurePlaywrightReady(options = {}) {
          if (!playwrightBrowsers) {
            playwrightBrowsers = loadPlaywright({ quiet: isQuiet(), ...options });
          }
          return playwrightBrowsers;
        }
        
        function log(...parts) {
          if (!isQuiet()) {
            console.error(...parts);
          }
        }
        
        function warn(...parts) {
          console.error(...parts);
        }
        
        /**
         * Parse extra HTTP headers from environment variables.
         * Supports two formats:
         * - PW_HEADER_NAME + PW_HEADER_VALUE: Single header (simple, common case)
         * - PW_EXTRA_HEADERS: JSON object for multiple headers (advanced)
         * Single header format takes precedence if both are set.
         * @returns {Object|null} Headers object or null if none configured
         */
        function getExtraHeadersFromEnv() {
          const headerName = process.env.PW_HEADER_NAME;
          const headerValue = process.env.PW_HEADER_VALUE;
        
          if (headerName && headerValue) {
            return { [headerName]: headerValue };
          }
        
          const headersJson = process.env.PW_EXTRA_HEADERS;
          if (headersJson) {
            try {
              const parsed = JSON.parse(headersJson);
              if (
                typeof parsed === "object" &&
                parsed !== null &&
                !Array.isArray(parsed)
              ) {
                return parsed;
              }
              warn("PW_EXTRA_HEADERS must be a JSON object, ignoring...");
            } catch (error) {
              warn("Failed to parse PW_EXTRA_HEADERS as JSON:", error.message);
            }
          }
        
          return null;
        }
        
        /**
         * Merge environment-provided extra HTTP headers into browser context options.
         * @param {Object} options - Browser context options
         * @returns {Object} Options with extraHTTPHeaders merged in
         */
        function getContextOptionsWithHeaders(options = {}) {
          const extraHeaders = getExtraHeadersFromEnv();
        
          if (!extraHeaders) {
            return options;
          }
        
          return {
            ...options,
            extraHTTPHeaders: {
              ...extraHeaders,
              ...(options.extraHTTPHeaders || {}),
            },
          };
        }
        
        /**
         * Launch browser with standard configuration.
         * @param {string} browserType - 'chromium', 'firefox', or 'webkit'
         * @param {Object} options - Additional launch options
         */
        async function launchBrowser(browserType = "chromium", options = {}) {
          const defaultOptions = {
            headless: process.env.HEADLESS !== "false",
            slowMo: process.env.SLOW_MO ? parseInt(process.env.SLOW_MO, 10) : 0,
            ...(browserType === "chromium" ? sandboxOptions() : {}),
          };
        
          const browsers = ensurePlaywrightReady();
          const browser = browsers[browserType];
        
          if (!browser) {
            throw new Error(`Invalid browser type: ${browserType}`);
          }
        
          if (!options.executablePath && !options.channel) {
            ensureBrowserAvailable(browser, browserType);
          }
          return await browser.launch({ ...defaultOptions, ...options });
        }
        
        /**
         * Create a new page with viewport and user agent.
         * @param {Object} context - Browser context
         * @param {Object} options - Page options
         */
        async function createPage(context, options = {}) {
          const page = await context.newPage();
        
          if (options.viewport) {
            await page.setViewportSize(options.viewport);
          }
        
          if (options.userAgent) {
            await page.setExtraHTTPHeaders({
              "User-Agent": options.userAgent,
            });
          }
        
          page.setDefaultTimeout(options.timeout || 30000);
          return page;
        }
        
        async function waitForAnimationFrames(page, frames = 2) {
          const frameCount = Math.max(0, Number(frames) || 0);
          if (frameCount === 0) {
            return;
          }
        
          await page.evaluate(
            (count) =>
              new Promise((resolve) => {
                let remaining = count;
                function tick() {
                  remaining -= 1;
                  if (remaining <= 0) {
                    resolve();
                    return;
                  }
                  window.requestAnimationFrame(tick);
                }
                window.requestAnimationFrame(tick);
              }),
            frameCount,
          );
        }
        
        async function waitForFonts(page) {
          await page.evaluate(async () => {
            if (document.fonts?.ready) {
              await document.fonts.ready;
            }
          });
        }
        
        // Playwright's internal TargetClosedError isn't exported from the public
        // "playwright" package (only errors.TimeoutError is), so a closed
        // page/context/browser surfaces as a plain Error whose message matches one
        // of these shapes. See lib/client/errors.js in playwright-core.
        const CLOSED_TARGET_MESSAGE_PATTERN =
          /Target (?:page, context or browser )?(?:has been |is )?closed|Browser has been closed/i;
        
        function isClosedTargetError(error) {
          if (!error) {
            return false;
          }
          if (error.name === "TargetClosedError") {
            return true;
          }
          return (
            typeof error.message === "string" &&
            CLOSED_TARGET_MESSAGE_PATTERN.test(error.message)
          );
        }
        
        async function waitForStableBoundingBox(page, selector, options = {}) {
          const timeout = options.timeout || 10000;
          const requiredStableFrames = options.frames || 2;
          const locator = page.locator(selector).first();
          const deadline = Date.now() + timeout;
          let lastBox = null;
          let stableFrames = 0;
        
          while (Date.now() < deadline) {
            let box = null;
            try {
              box = await locator.boundingBox({ timeout: Math.min(1000, timeout) });
            } catch (error) {
              if (isClosedTargetError(error)) {
                throw error;
              }
              // Dynamic pages can briefly detach/recreate nodes during animation.
            }
        
            if (!box) {
              stableFrames = 0;
              await waitForAnimationFrames(page, 1);
              continue;
            }
        
            const stable =
              lastBox &&
              Math.abs(lastBox.x - box.x) < 0.5 &&
              Math.abs(lastBox.y - box.y) < 0.5 &&
              Math.abs(lastBox.width - box.width) < 0.5 &&
              Math.abs(lastBox.height - box.height) < 0.5;
        
            stableFrames = stable ? stableFrames + 1 : 0;
            lastBox = box;
        
            if (stableFrames >= requiredStableFrames) {
              return;
            }
        
            await waitForAnimationFrames(page, 1);
          }
        
          throw new Error(`Element bounding box did not stabilize: ${selector}`);
        }
        
        /**
         * Wait for SPA/page readiness using load state, a rendered selector, fonts, and
         * animation frames. Prefer this before screenshots of dynamic pages.
         * @param {Object} page - Playwright page
         * @param {Object} options - Wait options
         */
        async function waitForStablePage(page, options = {}) {
          const timeout = options.timeout || 30000;
          const waitUntil = options.waitUntil || "domcontentloaded";
          const selector = options.selector || options.waitForSelector;
          const selectorState = options.state || "visible";
          const animationFrames = options.animationFrames ?? 2;
        
          if (waitUntil) {
            try {
              await page.waitForLoadState(waitUntil, { timeout });
            } catch (_error) {
              log(
                `Page did not reach load state '${waitUntil}' before timeout; continuing...`,
              );
            }
          }
        
          if (selector) {
            await page.waitForSelector(selector, { state: selectorState, timeout });
          }
        
          if (options.fonts !== false) {
            try {
              await waitForFonts(page);
            } catch (_error) {
              log("Font readiness check failed; continuing...");
            }
          }
        
          await waitForAnimationFrames(page, animationFrames);
        
          if (selector && options.stableBoundingBox) {
            await waitForStableBoundingBox(page, selector, {
              timeout,
              frames: options.stableFrames || 2,
            });
          }
        }
        
        /**
         * Smart wait for page to be ready. Kept for compatibility; prefer
         * waitForStablePage for SPA screenshots.
         * @param {Object} page - Playwright page
         * @param {Object} options - Wait options
         */
        async function waitForPageReady(page, options = {}) {
          await waitForStablePage(page, {
            waitUntil: options.waitUntil || "networkidle",
            selector: options.selector || options.waitForSelector,
            timeout: options.timeout || 30000,
            animationFrames: options.animationFrames ?? 2,
            fonts: options.fonts,
            stableBoundingBox: options.stableBoundingBox,
            stableFrames: options.stableFrames,
          });
        }
        
        /**
         * Safe click with retry logic.
         * @param {Object} page - Playwright page
         * @param {string} selector - Element selector
         * @param {Object} options - Click options
         */
        async function safeClick(page, selector, options = {}) {
          const maxRetries = options.retries || 3;
          const retryDelay = options.retryDelay || 1000;
        
          for (let i = 0; i < maxRetries; i += 1) {
            try {
              await page.waitForSelector(selector, {
                state: "visible",
                timeout: options.timeout || 5000,
              });
              await page.click(selector, {
                force: options.force || false,
                timeout: options.timeout || 5000,
              });
              return true;
            } catch (error) {
              if (i === maxRetries - 1) {
                console.error(
                  `Failed to click ${selector} after ${maxRetries} attempts`,
                );
                throw error;
              }
              log(`Retry ${i + 1}/${maxRetries} for clicking ${selector}`);
              await page.waitForTimeout(retryDelay);
            }
          }
        
          return false;
        }
        
        /**
         * Safe text input with clear before type.
         * @param {Object} page - Playwright page
         * @param {string} selector - Input selector
         * @param {string} text - Text to type
         * @param {Object} options - Type options
         */
        async function safeType(page, selector, text, options = {}) {
          await page.waitForSelector(selector, {
            state: "visible",
            timeout: options.timeout || 10000,
          });
        
          if (options.clear !== false) {
            await page.fill(selector, "");
          }
        
          if (options.slow) {
            await page.type(selector, text, { delay: options.delay || 100 });
          } else {
            await page.fill(selector, text);
          }
        }
        
        /**
         * Extract text from multiple elements.
         * @param {Object} page - Playwright page
         * @param {string} selector - Elements selector
         */
        async function extractTexts(page, selector) {
          await page.waitForSelector(selector, { timeout: 10000 });
          return await page.$$eval(selector, (elements) =>
            elements.map((el) => el.textContent?.trim()).filter(Boolean),
          );
        }
        
        function timestampedScreenshotPath(name) {
          const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
          const parsed = path.parse(name);
          const stem = parsed.ext ? parsed.name : name;
          const filename = `${stem}-${timestamp}.png`;
        
          if (path.isAbsolute(name)) {
            return parsed.ext ? name : `${name}-${timestamp}.png`;
          }
        
          if (name.includes(path.sep) || name.includes("/")) {
            const resolved = path.resolve(process.cwd(), name);
            const resolvedParsed = path.parse(resolved);
            return resolvedParsed.ext ? resolved : `${resolved}-${timestamp}.png`;
          }
        
          return path.join(os.tmpdir(), `playwright-${filename}`);
        }
        
        /**
         * Take screenshot with timestamp. Simple names are saved under /tmp.
         * @param {Object} page - Playwright page
         * @param {string} name - Screenshot name or path
         * @param {Object} options - Screenshot options
         */
        async function takeScreenshot(page, name, options = {}) {
          const filename = timestampedScreenshotPath(name);
          fs.mkdirSync(path.dirname(filename), { recursive: true });
        
          await page.screenshot({
            path: filename,
            fullPage: options.fullPage !== false,
            ...options,
          });
        
          log(`Screenshot saved: ${filename}`);
          return filename;
        }
        
        /**
         * Handle authentication.
         * @param {Object} page - Playwright page
         * @param {Object} credentials - Username and password
         * @param {Object} selectors - Login form selectors
         */
        async function authenticate(page, credentials, selectors = {}) {
          const defaultSelectors = {
            username: 'input[name="username"], input[name="email"], #username, #email',
            password: 'input[name="password"], #password',
            submit:
              'button[type="submit"], input[type="submit"], button:has-text("Login"), button:has-text("Sign in")',
          };
        
          const finalSelectors = { ...defaultSelectors, ...selectors };
        
          await safeType(page, finalSelectors.username, credentials.username);
          await safeType(page, finalSelectors.password, credentials.password);
          await safeClick(page, finalSelectors.submit);
        
          await Promise.race([
            page.waitForNavigation({ waitUntil: "networkidle" }),
            page.waitForSelector(
              selectors.successIndicator || ".dashboard, .user-menu, .logout",
              { timeout: 10000 },
            ),
          ]).catch(() => {
            log("Login might have completed without navigation");
          });
        }
        
        /**
         * Scroll page.
         * @param {Object} page - Playwright page
         * @param {string} direction - 'down', 'up', 'top', 'bottom'
         * @param {number} distance - Pixels to scroll (for up/down)
         */
        async function scrollPage(page, direction = "down", distance = 500) {
          switch (direction) {
            case "down":
              await page.evaluate((d) => window.scrollBy(0, d), distance);
              break;
            case "up":
              await page.evaluate((d) => window.scrollBy(0, -d), distance);
              break;
            case "top":
              await page.evaluate(() => window.scrollTo(0, 0));
              break;
            case "bottom":
              await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
              break;
            default:
              throw new Error(`Invalid scroll direction: ${direction}`);
          }
          await waitForAnimationFrames(page, 2);
        }
        
        /**
         * Extract table data.
         * @param {Object} page - Playwright page
         * @param {string} tableSelector - Table selector
         */
        async function extractTableData(page, tableSelector) {
          await page.waitForSelector(tableSelector);
        
          return await page.evaluate((selector) => {
            const table = document.querySelector(selector);
            if (!table) return null;
        
            const headers = Array.from(table.querySelectorAll("thead th")).map((th) =>
              th.textContent?.trim(),
            );
        
            const rows = Array.from(table.querySelectorAll("tbody tr")).map((tr) => {
              const cells = Array.from(tr.querySelectorAll("td"));
              if (headers.length > 0) {
                return cells.reduce((obj, cell, index) => {
                  obj[headers[index] || `column_${index}`] = cell.textContent?.trim();
                  return obj;
                }, {});
              }
              return cells.map((cell) => cell.textContent?.trim());
            });
        
            return { headers, rows };
          }, tableSelector);
        }
        
        /**
         * Wait for and dismiss cookie banners.
         * @param {Object} page - Playwright page
         * @param {number} timeout - Max time to wait
         */
        async function handleCookieBanner(page, timeout = 3000) {
          const commonSelectors = [
            'button:has-text("Accept")',
            'button:has-text("Accept all")',
            'button:has-text("OK")',
            'button:has-text("Got it")',
            'button:has-text("I agree")',
            ".cookie-accept",
            "#cookie-accept",
            '[data-testid="cookie-accept"]',
          ];
        
          for (const selector of commonSelectors) {
            try {
              const element = await page.waitForSelector(selector, {
                timeout: timeout / commonSelectors.length,
                state: "visible",
              });
              if (element) {
                await element.click();
                log("Cookie banner dismissed");
                return true;
              }
            } catch (_error) {
              // Continue to next selector.
            }
          }
        
          return false;
        }
        
        /**
         * Retry a function with exponential backoff.
         * @param {Function} fn - Function to retry
         * @param {number} maxRetries - Maximum retry attempts
         * @param {number} initialDelay - Initial delay in ms
         */
        async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
          let lastError;
        
          for (let i = 0; i < maxRetries; i += 1) {
            try {
              return await fn();
            } catch (error) {
              lastError = error;
              const delay = initialDelay * 2 ** i;
              log(`Attempt ${i + 1} failed, retrying in ${delay}ms...`);
              await new Promise((resolve) => setTimeout(resolve, delay));
            }
          }
        
          throw lastError;
        }
        
        /**
         * Create browser context with common settings.
         * @param {Object} browser - Browser instance
         * @param {Object} options - Context options
         */
        async function createContext(browser, options = {}) {
          const defaultOptions = {
            viewport: { width: 1280, height: 720 },
            userAgent: options.mobile
              ? "Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1"
              : undefined,
            permissions: options.permissions || [],
            geolocation: options.geolocation,
            locale: options.locale || "en-US",
            timezoneId: options.timezoneId || "America/New_York",
          };
        
          return await browser.newContext(
            getContextOptionsWithHeaders({ ...defaultOptions, ...options }),
          );
        }
        
        function requestUrl(port) {
          return `http://localhost:${port}`;
        }
        
        async function probeHttpPort(http, port) {
          return await new Promise((resolve) => {
            const req = http.request(
              {
                hostname: "localhost",
                port,
                path: "/",
                method: "HEAD",
                timeout: 500,
              },
              (res) => {
                resolve(res.statusCode < 500);
              },
            );
        
            req.on("error", () => resolve(false));
            req.on("timeout", () => {
              req.destroy();
              resolve(false);
            });
        
            req.end();
          });
        }
        
        /**
         * Detect running dev servers on common ports.
         * @param {Array<number>} customPorts - Additional ports to check
         * @returns {Promise<Array>} Array of detected server URLs
         */
        async function detectDevServers(customPorts = []) {
          const http = require("http");
          const commonPorts = [
            3000, 3001, 3002, 3030, 4173, 4200, 4321, 5000, 5173, 5174, 6006, 8000,
            8080, 9000, 1234,
          ];
          const allPorts = [...new Set([...commonPorts, ...customPorts])];
          const detectedServers = [];
        
          log("🔍 Checking for running dev servers...");
        
          for (const port of allPorts) {
            try {
              if (await probeHttpPort(http, port)) {
                detectedServers.push(requestUrl(port));
                log(`  ✅ Found server on port ${port}`);
              }
            } catch (_error) {
              // Port not available, continue.
            }
          }
        
          if (detectedServers.length === 0) {
            log("  ❌ No dev servers detected");
          }
        
          return detectedServers;
        }
        
        module.exports = {
          ensurePlaywrightReady,
          launchBrowser,
          createPage,
          waitForPageReady,
          waitForStablePage,
          waitForAnimationFrames,
          waitForStableBoundingBox,
          safeClick,
          safeType,
          extractTexts,
          takeScreenshot,
          authenticate,
          scrollPage,
          extractTableData,
          handleCookieBanner,
          retryWithBackoff,
          createContext,
          detectDevServers,
          getExtraHeadersFromEnv,
          getContextOptionsWithHeaders,
        };
        
      • runtime.js 2.8 KB
        const { execFileSync } = require("child_process");
        const fs = require("fs");
        const os = require("os");
        const path = require("path");
        
        const SCRIPT_DIR = path.resolve(__dirname, "..");
        const PLAYWRIGHT_VERSION = "1.63.0";
        
        function log(options, ...parts) {
          if (!options?.quiet) console.error(...parts);
        }
        
        function cacheDirectory(options = {}) {
          return (
            options.cacheDir ||
            path.join(
              process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"),
              "cc-thingz",
              "playwright",
              PLAYWRIGHT_VERSION,
            )
          );
        }
        
        function resolvePlaywright(options = {}) {
          const projectDir = options.projectDir || process.cwd();
          const candidates = [
            () => require.resolve("playwright", { paths: [projectDir] }),
            () =>
              require.resolve("playwright", {
                paths: [require.resolve("@playwright/test", { paths: [projectDir] })],
              }),
            () =>
              require.resolve(
                path.join(cacheDirectory(options), "node_modules", "playwright"),
              ),
          ];
          for (const resolve of candidates) {
            try {
              return resolve();
            } catch (error) {
              if (error.code !== "MODULE_NOT_FOUND") throw error;
            }
          }
          return null;
        }
        
        function isPlaywrightInstalled(options = {}) {
          return resolvePlaywright(options) !== null;
        }
        
        function ensurePlaywrightInstalled(options = {}) {
          if (isPlaywrightInstalled(options)) return;
          const cacheDir = cacheDirectory(options);
          fs.mkdirSync(cacheDir, { recursive: true });
          log(options, `Installing Playwright ${PLAYWRIGHT_VERSION} in ${cacheDir}`);
          execFileSync(
            "npm",
            [
              "install",
              "--prefix",
              cacheDir,
              "--no-save",
              "--package-lock=false",
              "--ignore-scripts",
              `playwright@${PLAYWRIGHT_VERSION}`,
            ],
            { cwd: cacheDir, stdio: options.quiet ? "ignore" : "inherit" },
          );
          if (!isPlaywrightInstalled(options))
            throw new Error("Playwright installation failed");
        }
        
        function loadPlaywright(options = {}) {
          ensurePlaywrightInstalled(options);
          return require(resolvePlaywright(options));
        }
        
        function ensureBrowserAvailable(browser, browserName, options = {}) {
          if (!fs.existsSync(browser.executablePath())) {
            const entry = resolvePlaywright(options);
            const cli = path.join(path.dirname(entry), "cli.js");
            throw new Error(
              `Playwright package is ready but ${browserName} is missing. Install it with: node ${JSON.stringify(cli)} install ${browserName}`,
            );
          }
        }
        
        function sandboxOptions() {
          return process.env.PLAYWRIGHT_SKILL_NO_SANDBOX === "1"
            ? {
                chromiumSandbox: false,
                args: ["--no-sandbox", "--disable-setuid-sandbox"],
              }
            : { chromiumSandbox: true, args: [] };
        }
        
        module.exports = {
          SCRIPT_DIR,
          PLAYWRIGHT_VERSION,
          cacheDirectory,
          resolvePlaywright,
          ensurePlaywrightInstalled,
          isPlaywrightInstalled,
          loadPlaywright,
          ensureBrowserAvailable,
          sandboxOptions,
          log,
        };
        
      • screenshot.js 8.9 KB
        const fs = require("fs");
        const os = require("os");
        const path = require("path");
        const runtime = require("./runtime");
        
        const DEFAULT_TITLE_SELECTOR = 'h1, h2, [role="heading"]';
        
        function parseInteger(value, name, options = {}) {
          if (value === undefined || value === null || value === "") {
            if (Object.prototype.hasOwnProperty.call(options, "defaultValue")) {
              return options.defaultValue;
            }
            throw new Error(`${name} is required`);
          }
        
          const number = Number(value);
          const min = options.min;
        
          if (!Number.isInteger(number) || (min !== undefined && number < min)) {
            const suffix = min === undefined ? "" : ` >= ${min}`;
            throw new Error(`${name} must be an integer${suffix}`);
          }
        
          return number;
        }
        
        function parseViewport(value) {
          if (!value) {
            return { width: 1280, height: 720 };
          }
        
          if (typeof value === "object") {
            const width = parseInteger(value.width, "viewport.width", { min: 1 });
            const height = parseInteger(value.height, "viewport.height", { min: 1 });
            return { ...value, width, height };
          }
        
          const match = String(value).match(/^(\d+)x(\d+)$/i);
          if (!match) {
            throw new Error(
              `Invalid viewport '${value}'. Use WIDTHxHEIGHT, e.g. 1280x720.`,
            );
          }
        
          return {
            width: parseInteger(match[1], "viewport width", { min: 1 }),
            height: parseInteger(match[2], "viewport height", { min: 1 }),
          };
        }
        
        function sanitizeFilename(value) {
          return String(value)
            .replace(/^https?:\/\//, "")
            .replace(/[^a-z0-9._-]+/gi, "-")
            .replace(/^-+|-+$/g, "")
            .slice(0, 120);
        }
        
        function defaultScreenshotPath(url) {
          const filename = sanitizeFilename(url) || "screenshot";
          return path.join(os.tmpdir(), `playwright-${filename}.png`);
        }
        
        function resolveOutputPath(outputPath, fallbackUrl) {
          const target = outputPath || defaultScreenshotPath(fallbackUrl);
          return path.isAbsolute(target) ? target : path.resolve(process.cwd(), target);
        }
        
        function ensureParentDir(filePath) {
          fs.mkdirSync(path.dirname(filePath), { recursive: true });
        }
        
        function attachDiagnostics(page) {
          const consoleErrors = [];
          const networkFailures = [];
          const badResponses = [];
        
          page.on("console", (message) => {
            if (message.type() !== "error") {
              return;
            }
        
            consoleErrors.push({
              type: message.type(),
              text: message.text(),
              location: message.location(),
            });
          });
        
          page.on("requestfailed", (request) => {
            networkFailures.push({
              url: request.url(),
              method: request.method(),
              failure: request.failure()?.errorText || "unknown",
            });
          });
        
          page.on("response", (response) => {
            if (response.status() < 400) {
              return;
            }
        
            badResponses.push({
              url: response.url(),
              status: response.status(),
              statusText: response.statusText(),
            });
          });
        
          return { consoleErrors, networkFailures, badResponses };
        }
        
        async function firstVisibleText(page, selector) {
          if (!selector) {
            return "";
          }
        
          const locator = page.locator(selector).filter({ hasText: /\S/ }).first();
        
          try {
            const text = await locator.textContent({ timeout: 1500 });
            return text?.trim() || "";
          } catch (_error) {
            return "";
          }
        }
        
        async function extractTitle(page, titleSelector) {
          const selectorTitle = await firstVisibleText(
            page,
            titleSelector === undefined ? DEFAULT_TITLE_SELECTOR : titleSelector,
          );
        
          if (selectorTitle) {
            return selectorTitle;
          }
        
          return (await page.title()).trim();
        }
        
        function normalizeLaunchOptions(options) {
          const browserName = options.browser || "chromium";
          const headless = options.headed ? false : options.headless !== false;
        
          return {
            browserName,
            headless,
            ...(browserName === "chromium" ? runtime.sandboxOptions() : {}),
          };
        }
        
        async function captureUrl(options) {
          if (!options.url) {
            throw new Error("--url is required");
          }
        
          const quiet = Boolean(options.quiet);
          const playwright = runtime.loadPlaywright({ quiet });
          const helpers = require("./helpers");
          helpers.ensurePlaywrightReady({ quiet });
          const launch = normalizeLaunchOptions(options);
          const browserType = playwright[launch.browserName];
        
          if (!browserType) {
            throw new Error(`Invalid browser '${launch.browserName}'`);
          }
        
          runtime.ensureBrowserAvailable(browserType, launch.browserName);
        
          const timeout = parseInteger(options.timeout, "--timeout", {
            defaultValue: 30000,
            min: 0,
          });
          const animationFrames = parseInteger(
            options.animationFrames,
            "--animation-frames",
            { defaultValue: 2, min: 0 },
          );
          const viewport = parseViewport(options.viewport);
          const screenshotPath = resolveOutputPath(options.out, options.url);
          ensureParentDir(screenshotPath);
        
          const browser = await browserType.launch({
            headless: launch.headless,
            args: launch.args,
            chromiumSandbox: launch.chromiumSandbox,
          });
        
          try {
            const context = await browser.newContext(
              helpers.getContextOptionsWithHeaders({ viewport }),
            );
            const page = await context.newPage();
            page.setDefaultTimeout(timeout);
            const diagnostics = attachDiagnostics(page);
        
            await page.goto(options.url, {
              waitUntil: options.gotoWaitUntil || "domcontentloaded",
              timeout,
            });
        
            await helpers.waitForStablePage(page, {
              selector: options.selector,
              waitUntil: options.waitUntil || "domcontentloaded",
              timeout,
              animationFrames,
              stableBoundingBox: options.stableBoundingBox || false,
            });
        
            const title = await extractTitle(page, options.titleSelector);
        
            await page.screenshot({
              path: screenshotPath,
              fullPage: options.fullPage !== false,
            });
        
            return {
              url: options.url,
              title,
              screenshotPath,
              selector: options.selector || null,
              viewport,
              fullPage: options.fullPage !== false,
              browser: launch.browserName,
              headless: launch.headless,
              consoleErrors: diagnostics.consoleErrors,
              networkFailures: diagnostics.networkFailures,
              badResponses: diagnostics.badResponses,
            };
          } finally {
            await browser.close();
          }
        }
        
        function urlForIndex(template, index) {
          if (!template.includes("{n}")) {
            throw new Error("--url-template must include {n}");
          }
          return template.replaceAll("{n}", String(index));
        }
        
        async function captureSequence(options) {
          if (!options.urlTemplate) {
            throw new Error("--url-template is required");
          }
        
          const from = parseInteger(options.from, "--from");
          const to = parseInteger(options.to, "--to");
          const step = parseInteger(options.step, "--step", {
            defaultValue: from <= to ? 1 : -1,
          });
        
          if (step === 0) {
            throw new Error("--step must not be 0");
          }
        
          if ((from < to && step < 0) || (from > to && step > 0)) {
            throw new Error("--step direction must move from --from toward --to");
          }
        
          const outDir = path.resolve(
            options.outDir || path.join(os.tmpdir(), "playwright-screenshots"),
          );
          fs.mkdirSync(outDir, { recursive: true });
        
          const padWidth = Math.max(
            String(Math.abs(from)).length,
            String(Math.abs(to)).length,
            2,
          );
          const prefix = options.prefix || "screenshot-";
          const manifestPath = options.manifest
            ? path.resolve(options.manifest)
            : path.join(outDir, "manifest.json");
        
          const results = [];
          const indices = [];
        
          if (step > 0) {
            for (let index = from; index <= to; index += step) indices.push(index);
          } else {
            for (let index = from; index >= to; index += step) indices.push(index);
          }
        
          if (indices.length === 0) {
            throw new Error("screenshot sequence is empty");
          }
        
          for (const index of indices) {
            const url = urlForIndex(options.urlTemplate, index);
            const screenshotPath = path.join(
              outDir,
              `${prefix}${String(index).padStart(padWidth, "0")}.png`,
            );
        
            try {
              const result = await captureUrl({
                ...options,
                url,
                out: screenshotPath,
              });
              results.push({ index, ...result });
            } catch (error) {
              const failed = {
                index,
                url,
                screenshotPath,
                error: error.message,
              };
              results.push(failed);
        
              if (!options.continueOnError) {
                const partialManifest = {
                  urlTemplate: options.urlTemplate,
                  from,
                  to,
                  step,
                  outDir,
                  results,
                };
                ensureParentDir(manifestPath);
                fs.writeFileSync(
                  manifestPath,
                  JSON.stringify(partialManifest, null, 2),
                );
                throw error;
              }
            }
          }
        
          const manifest = {
            urlTemplate: options.urlTemplate,
            from,
            to,
            step,
            outDir,
            results,
          };
        
          ensureParentDir(manifestPath);
          fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
        
          return { manifestPath, ...manifest };
        }
        
        function writeManifest(manifestPath, data) {
          if (!manifestPath) {
            return;
          }
        
          const resolved = path.resolve(manifestPath);
          ensureParentDir(resolved);
          fs.writeFileSync(resolved, JSON.stringify(data, null, 2));
        }
        
        module.exports = {
          captureSequence,
          captureUrl,
          extractTitle,
          parseInteger,
          parseViewport,
          writeManifest,
        };
        
    • .gitignore 14 B · in bundle
    • package.json 792 B
      {
        "name": "browser-automation-scripts",
        "version": "6.17.0",
        "description": "Support runtime for browser-automation using Playwright with dev-server detection and helper utilities",
        "author": "lackeyjb",
        "main": "run.js",
        "scripts": {
          "setup": "node setup-runtime.js chromium",
          "setup:npm": "node setup-runtime.js chromium",
          "screenshot-url": "node screenshot-url.js",
          "screenshot-sequence": "node screenshot-sequence.js",
          "install-all-browsers": "node setup-runtime.js chromium firefox webkit"
        },
        "keywords": [
          "playwright",
          "automation",
          "browser-testing",
          "web-automation",
          "claude-skill",
          "general-purpose"
        ],
        "dependencies": {
          "playwright": "1.63.0"
        },
        "engines": {
          "node": ">=22.0.0"
        },
        "license": "MIT"
      }
      
    • run.js 5.5 KB
      #!/usr/bin/env node
      /**
       * Playwright support-runtime executor.
       *
       * Executes Playwright automation code from:
       * - File path: node run.js script.js
       * - Inline code: node run.js 'await page.goto("...")'
       * - Stdin: cat script.js | node run.js
       *
       * The runner preserves the caller's working directory, keeps runner logs on
       * stderr, and exposes Playwright primitives as globals without blocking normal
       * require("fs"), require("path"), or require("playwright") usage.
       */
      
      const fs = require("fs");
      const os = require("os");
      const path = require("path");
      const Module = require("module");
      const runtime = require("./lib/runtime");
      
      const SCRIPT_DIR = __dirname;
      const ORIGINAL_CWD = process.cwd();
      
      function usage() {
        console.error(`Usage:
        node scripts/run.js [--quiet|--json] script.js
        node scripts/run.js [--quiet|--json] "await browser code"
        cat script.js | node scripts/run.js [--quiet|--json]
      
      Options:
        --quiet, -q   suppress runner status logs; script stdout stays untouched
        --json        alias for --quiet, intended for scripts that print JSON
        --help, -h    show this help`);
      }
      
      function parseArgs(argv) {
        const options = { quiet: false, help: false };
        const input = [];
      
        for (let i = 0; i < argv.length; i += 1) {
          const arg = argv[i];
      
          if (arg === "--quiet" || arg === "-q") {
            options.quiet = true;
            continue;
          }
      
          if (arg === "--json") {
            options.quiet = true;
            continue;
          }
      
          if (arg === "--help" || arg === "-h") {
            options.help = true;
            continue;
          }
      
          if (arg === "--") {
            input.push(...argv.slice(i + 1));
            break;
          }
      
          input.push(arg);
        }
      
        return { options, input };
      }
      
      function log(options, ...parts) {
        if (!options.quiet) {
          console.error(...parts);
        }
      }
      
      function resolveFromCaller(inputPath) {
        if (path.isAbsolute(inputPath)) {
          return inputPath;
        }
        return path.resolve(ORIGINAL_CWD, inputPath);
      }
      
      function stripShebang(code) {
        return code.replace(/^#!.*(?:\r?\n|$)/, "");
      }
      
      function tempFilename(label) {
        const safeLabel = label.replace(/[^a-z0-9_-]/gi, "-").toLowerCase();
        return path.join(
          os.tmpdir(),
          `playwright-skill-${safeLabel}-${process.pid}-${Date.now()}.js`,
        );
      }
      
      function getCodeToExecute(input, options) {
        if (input.length > 0) {
          const candidate = resolveFromCaller(input[0]);
          if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
            log(options, `📄 Executing file: ${candidate}`);
            return {
              code: fs.readFileSync(candidate, "utf8"),
              filename: candidate,
              kind: "file",
            };
          }
      
          log(options, "⚡ Executing inline code");
          return {
            code: input.join(" "),
            filename: tempFilename("inline"),
            kind: "inline",
          };
        }
      
        if (!process.stdin.isTTY) {
          log(options, "📥 Reading from stdin");
          return {
            code: fs.readFileSync(0, "utf8"),
            filename: tempFilename("stdin"),
            kind: "stdin",
          };
        }
      
        usage();
        process.exit(1);
      }
      
      function getContextOptionsWithHeaders(helpers, options = {}) {
        return helpers.getContextOptionsWithHeaders(options);
      }
      
      function installRuntimeGlobals(playwright, helpers) {
        const globals = {
          playwright,
          chromium: playwright.chromium,
          firefox: playwright.firefox,
          webkit: playwright.webkit,
          devices: playwright.devices,
          helpers,
          getContextOptionsWithHeaders: (options = {}) =>
            getContextOptionsWithHeaders(helpers, options),
        };
      
        for (const [name, value] of Object.entries(globals)) {
          Object.defineProperty(globalThis, name, {
            configurable: true,
            enumerable: false,
            writable: true,
            value,
          });
        }
      }
      
      function wrapCode(code) {
        return `
      module.exports = (async () => {
        try {
      ${stripShebang(code)}
        } catch (error) {
          console.error('❌ Automation error:', error.message);
          if (error.stack) {
            console.error(error.stack);
          }
          process.exitCode = 1;
        }
      })();
      `;
      }
      
      function createExecutionModule(filename) {
        const executionModule = new Module(filename, module.parent);
        executionModule.filename = filename;
        executionModule.paths = [
          ...Module._nodeModulePaths(path.dirname(filename)),
          ...Module._nodeModulePaths(SCRIPT_DIR),
        ];
        return executionModule;
      }
      
      async function executeCode(source) {
        const executionModule = createExecutionModule(source.filename);
        executionModule._compile(wrapCode(source.code), source.filename);
      
        if (
          executionModule.exports &&
          typeof executionModule.exports.then === "function"
        ) {
          await executionModule.exports;
        }
      }
      
      async function main() {
        const { options, input } = parseArgs(process.argv.slice(2));
      
        if (options.help) {
          usage();
          return;
        }
      
        process.env.PLAYWRIGHT_SKILL_QUIET = options.quiet ? "1" : "0";
      
        log(options, "🎭 Playwright Skill - Universal Executor");
      
        let playwright;
        try {
          playwright = runtime.loadPlaywright(options);
        } catch (error) {
          console.error("❌", error.message);
          process.exit(1);
        }
      
        const helpers = require("./lib/helpers");
        helpers.ensurePlaywrightReady({ quiet: options.quiet });
        installRuntimeGlobals(playwright, helpers);
      
        const source = getCodeToExecute(input, options);
      
        try {
          log(options, "🚀 Starting automation...");
          await executeCode(source);
        } catch (error) {
          console.error("❌ Execution failed:", error.message);
          if (error.stack) {
            console.error("\n📋 Stack trace:");
            console.error(error.stack);
          }
          process.exit(1);
        }
      }
      
      main().catch((error) => {
        console.error("❌ Fatal error:", error.message);
        process.exit(1);
      });
      
    • screenshot-sequence.js 3.5 KB
      #!/usr/bin/env node
      
      const { captureSequence } = require("./lib/screenshot");
      
      function usage() {
        console.error(`Usage:
        node scripts/screenshot-sequence.js --url-template 'http://localhost:3030/{n}' --from 1 --to 10 --out-dir /tmp/shots
      
      Options:
        --url-template TEMPLATE URL template containing {n}
        --from N                first index
        --to N                  last index
        --step N                step, default 1 or -1 based on range
        --out-dir DIR           output dir, default /tmp/playwright-screenshots
        --prefix TEXT           screenshot filename prefix, default screenshot-
        --manifest FILE         manifest path, default <out-dir>/manifest.json
        --selector CSS          wait for selector before screenshot
        --viewport WIDTHxHEIGHT viewport, default 1280x720
        --title-selector CSS    selector used for manifest title, default heading
        --timeout MS            timeout, default 30000
        --animation-frames N    animation frames after readiness, default 2
        --wait-until STATE      load state for readiness, default domcontentloaded
        --browser NAME          chromium, firefox, or webkit; default chromium
        --headed                use visible browser
        --headless              force headless browser
        --full-page             capture full page, default
        --viewport-only         capture viewport only
        --continue-on-error     keep going and record failed items in manifest
        --quiet, -q             suppress status logs
        --json                  print manifest JSON to stdout and imply --quiet`);
      }
      
      function parseArgs(argv) {
        const options = { fullPage: true, quiet: false, json: false };
      
        for (let i = 0; i < argv.length; i += 1) {
          const arg = argv[i];
      
          switch (arg) {
            case "--help":
            case "-h":
              options.help = true;
              break;
            case "--quiet":
            case "-q":
              options.quiet = true;
              break;
            case "--json":
              options.json = true;
              options.quiet = true;
              break;
            case "--headed":
              options.headed = true;
              options.headless = false;
              break;
            case "--headless":
              options.headless = true;
              options.headed = false;
              break;
            case "--full-page":
              options.fullPage = true;
              break;
            case "--viewport-only":
            case "--no-full-page":
              options.fullPage = false;
              break;
            case "--continue-on-error":
              options.continueOnError = true;
              break;
            case "--stable-bounding-box":
              options.stableBoundingBox = true;
              break;
            default: {
              if (!arg.startsWith("--")) {
                throw new Error(`Unexpected argument: ${arg}`);
              }
              const key = arg
                .slice(2)
                .replace(/-([a-z])/g, (_, char) => char.toUpperCase());
              const value = argv[i + 1];
              if (value === undefined || value.startsWith("--")) {
                throw new Error(`${arg} requires a value`);
              }
              options[key] = value;
              i += 1;
            }
          }
        }
      
        return options;
      }
      
      async function main() {
        const options = parseArgs(process.argv.slice(2));
      
        if (options.help) {
          usage();
          return;
        }
      
        if (
          !options.urlTemplate ||
          options.from === undefined ||
          options.to === undefined
        ) {
          usage();
          process.exit(1);
        }
      
        process.env.PLAYWRIGHT_SKILL_QUIET = options.quiet ? "1" : "0";
      
        const result = await captureSequence(options);
      
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
      
        console.log(result.manifestPath);
      }
      
      main().catch((error) => {
        console.error("❌", error.message);
        process.exit(1);
      });
      
    • screenshot-url.js 3.1 KB
      #!/usr/bin/env node
      
      const { captureUrl, writeManifest } = require("./lib/screenshot");
      
      function usage() {
        console.error(`Usage:
        node scripts/screenshot-url.js --url URL [--out FILE] [--selector CSS]
      
      Options:
        --url URL               target URL
        --out FILE              screenshot path; defaults to /tmp/playwright-<url>.png
        --selector CSS          wait for selector before screenshot
        --viewport WIDTHxHEIGHT viewport, default 1280x720
        --manifest FILE         write JSON manifest to file
        --title-selector CSS    selector used for manifest title, default heading
        --timeout MS            timeout, default 30000
        --animation-frames N    animation frames after readiness, default 2
        --wait-until STATE      load state for readiness, default domcontentloaded
        --browser NAME          chromium, firefox, or webkit; default chromium
        --headed                use visible browser
        --headless              force headless browser
        --full-page             capture full page, default
        --viewport-only         capture viewport only
        --quiet, -q             suppress status logs
        --json                  print manifest JSON to stdout and imply --quiet`);
      }
      
      function parseArgs(argv) {
        const options = { fullPage: true, quiet: false, json: false };
      
        for (let i = 0; i < argv.length; i += 1) {
          const arg = argv[i];
      
          switch (arg) {
            case "--help":
            case "-h":
              options.help = true;
              break;
            case "--quiet":
            case "-q":
              options.quiet = true;
              break;
            case "--json":
              options.json = true;
              options.quiet = true;
              break;
            case "--headed":
              options.headed = true;
              options.headless = false;
              break;
            case "--headless":
              options.headless = true;
              options.headed = false;
              break;
            case "--full-page":
              options.fullPage = true;
              break;
            case "--viewport-only":
            case "--no-full-page":
              options.fullPage = false;
              break;
            case "--stable-bounding-box":
              options.stableBoundingBox = true;
              break;
            default: {
              if (!arg.startsWith("--")) {
                throw new Error(`Unexpected argument: ${arg}`);
              }
              const key = arg
                .slice(2)
                .replace(/-([a-z])/g, (_, char) => char.toUpperCase());
              const value = argv[i + 1];
              if (value === undefined || value.startsWith("--")) {
                throw new Error(`${arg} requires a value`);
              }
              options[key] = value;
              i += 1;
            }
          }
        }
      
        return options;
      }
      
      async function main() {
        const options = parseArgs(process.argv.slice(2));
      
        if (options.help) {
          usage();
          return;
        }
      
        if (!options.url) {
          usage();
          process.exit(1);
        }
      
        process.env.PLAYWRIGHT_SKILL_QUIET = options.quiet ? "1" : "0";
      
        const result = await captureUrl(options);
        writeManifest(options.manifest, result);
      
        if (options.json) {
          console.log(JSON.stringify(result, null, 2));
          return;
        }
      
        console.log(result.screenshotPath);
      }
      
      main().catch((error) => {
        console.error("❌", error.message);
        process.exit(1);
      });
      
    • setup-runtime.js 588 B
      const { execFileSync } = require("child_process");
      const path = require("path");
      const runtime = require("./lib/runtime");
      
      const browsers = process.argv.slice(2);
      if (
        browsers.some((name) => !["chromium", "firefox", "webkit"].includes(name))
      ) {
        console.error("Usage: node setup-runtime.js [chromium firefox webkit]");
        process.exit(2);
      }
      runtime.ensurePlaywrightInstalled();
      const cli = path.join(path.dirname(runtime.resolvePlaywright()), "cli.js");
      execFileSync(
        process.execPath,
        [cli, "install", ...(browsers.length ? browsers : ["chromium"])],
        { stdio: "inherit" },
      );
      
  • SKILL.md 4.1 KB
    ---
    {"description":"Browser automation for rendered UI exploration, validation, screenshots, recordings, and end-to-end flows. Use when a task needs an actual browser or rendered DOM: inspect UI state, click/fill forms, debug frontend behavior, capture evidence, verify a feature, or run/generate browser tests. NOT for API checks or pure logic tests where curl, unit tests, or JSDOM is cheaper.","name":"browser-automation"}
    ---
    <!-- Codex platform guidance -->
    <!-- Use this platform's installed tool names exactly for shell, file reads, and search. If a referenced helper or optional tool is unavailable, say so and continue with built-in tools. -->
    
    
    # Browser Automation
    
    Prove rendered behavior in a real browser and report pass, fail, or blocked with
    evidence. Keep automation temporary unless the user asks for permanent tests.
    
    ## Runtime
    
    Use the cheapest runtime that proves the claim:
    
    1. Browser tools exposed in the current session. See
       [`references/platform-browser-tools.md`](references/platform-browser-tools.md).
    2. The project's configured browser runner. Infer the package manager from the
       lockfile; do not invent a runner.
    3. The bundled Playwright scripts in this skill's `scripts/` directory. Read
       [`references/playwright.md`](references/playwright.md) for setup, the script
       skeleton, helpers, and custom headers.
    4. None available: report blocked and name the missing tool or package.
    
    ## Rules
    
    - Target: reuse a reachable dev server; start one only when its command is
      known. Ask when no server or several servers are found.
    - Data: use seeded users, fixed dates, reset state, and mocked external
      services. Credentials, production data, and destructive actions need explicit
      user approval.
    - Locators: role, label, text, or test id first; CSS last.
    - Waiting: wait on observable state such as a selector, URL, network response,
      or accessibility snapshot. Never add fixed sleeps. For SPA or HTMX pages,
      assert the DOM after swaps and client-side route changes.
    - Headless: when the platform exposes no visible browser (Pi, CI, most CLIs),
      use headless screenshots plus a manifest as visual evidence. Use headed mode
      only when the user can see the browser.
    - Files: write generated scripts and artifacts to `/tmp/playwright-*`. Write to
      the project only when the user asked for permanent tests, and never write into
      the skill directory.
    - Failures: fix the app or tests only when that is in scope. After two failed
      scoped attempts, save evidence, quote the failing line or UI state, and stop.
    - Permanent tests: done when the relevant build/test/lint checks pass on what
      you changed, or you name each check that did not run and why.
    
    ## Bundled Playwright scripts
    
    Run them by absolute path from the caller's working directory, where
    `<skill-dir>` is the directory that contains this `SKILL.md`. Prefer the
    screenshot scripts over custom batch scripts:
    
    ```bash
    node <skill-dir>/scripts/screenshot-url.js --url <url> --selector <ready-selector> \
      --out /tmp/playwright-page.png --json
    node <skill-dir>/scripts/screenshot-sequence.js --url-template '<url/{n}>' --from 1 --to 10 \
      --selector <ready-selector> --out-dir /tmp/playwright-shots --json
    node <skill-dir>/scripts/run.js --json /tmp/playwright-check.js
    ```
    
    - Manifests record URL, title, screenshot path, viewport, console errors,
      network failures, and HTTP responses with status >=400.
    - `run.js` keeps the caller's working directory and writes its status logs to
      stderr. Pass `--json` or `--quiet` whenever stdout must carry only the
      script's JSON: without them, a first-run Playwright install also writes to
      stdout. Playwright globals such as `chromium` and `helpers` stay available when the script also
      uses `require("fs")` or `require("path")`.
    
    ## Platform additions
    
    No target-specific additions.
    
    ## Output
    
    ```markdown
    ## Browser Automation Result
    
    Target: <page, feature, or flow>
    Runtime: <built-in browser | project runner | bundled Playwright | blocked>
    Actions: <commands or browser actions>
    Result: <pass | fail | blocked>
    Evidence: <screenshot, manifest, or trace paths, or the key observation>
    Next fix: <only when failing or blocked>
    ```
    
    Report blocked, not pass, when the check did not run.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related