{"manifest":{"name":"Multi-Device Screenshot Auditor","version":"1.0.0","description":"Capture pixel-perfect screenshots of any website across 19 device profiles — from iPhone SE to 4K TV — using Playwright. Produces a labeled gallery + markdown report. Supports authenticated pages via storage-state, cookies, or Bearer token. Perfect for visual QA, responsive design audits, and sharing with AI agents for UI/UX feedback.","tags":["playwright","screenshots","responsive","visual-testing","ui-audit","device-emulation","tool"],"standard":"agentskills.io","standard_version":"1.0","content_checksum":"42c28d22c4a364ea3d678e6959a4817ca0c493c688767bae58eedab0d19e24e8","bundle_checksum":null,"metadata":{"produces_images":"true"},"files":[]},"files":{"SKILL.md":"# Multi-Device Screenshot Auditor\n\nCapture pixel-perfect screenshots of any website across 19 device profiles using Playwright — from iPhone SE to 4K TV. Each run produces a labeled image gallery and a markdown report. Screenshots are immediately useful to share with an AI agent for UI/UX diagnosis.\n\n## What It Does\n\n1. Launches a headless Chromium browser via Playwright\n2. Iterates through all target device profiles (or a subset you specify)\n3. Sets exact viewport size + device pixel ratio + user-agent for each device\n4. Navigates to the target URL and waits for network idle\n5. Captures a screenshot (viewport-only or full-page)\n6. Saves all images to `./screenshots/` with device-slug filenames\n7. Emits a `REPORT.md` table: device, dimensions, DPR, file path, status\n\n## Tools Required\n\n- **Node.js** 18+ (or 20+)\n- **Playwright**: `npm install -D playwright` then `npx playwright install chromium`\n- **Run from a directory that has `playwright` in `node_modules`** — the script uses `import { chromium } from 'playwright'` which requires a local install. If you prefer no install, use `npx playwright` (see Quickstart).\n\n## Device Profiles (19 targets)\n\n| Slug | Label | Width × Height | DPR | Category |\n|------|-------|---------------|-----|----------|\n| `iphone-se` | iPhone SE | 375 × 667 | 2 | Phone |\n| `iphone-14` | iPhone 14 | 390 × 844 | 3 | Phone |\n| `iphone-14-pro-max` | iPhone 14 Pro Max | 430 × 932 | 3 | Phone |\n| `pixel-7` | Pixel 7 | 412 × 915 | 2.6 | Phone |\n| `galaxy-s8` | Galaxy S8 | 360 × 740 | 3 | Phone |\n| `iphone-14-landscape` | iPhone 14 Landscape | 844 × 390 | 3 | Phone (landscape) |\n| `ipad-mini` | iPad Mini | 768 × 1024 | 2 | Tablet |\n| `ipad-pro-11` | iPad Pro 11\" | 834 × 1194 | 2 | Tablet |\n| `ipad-pro-12` | iPad Pro 12.9\" | 1024 × 1366 | 2 | Tablet |\n| `ipad-pro-landscape` | iPad Pro Landscape | 1366 × 1024 | 2 | Tablet (landscape) |\n| `macbook-air` | MacBook Air 13\" | 1280 × 800 | 2 | Laptop |\n| `laptop-hd` | Laptop HD | 1366 × 768 | 1 | Laptop |\n| `macbook-pro-16` | MacBook Pro 16\" | 1728 × 1117 | 2 | Laptop |\n| `desktop-1080p` | Desktop 1080p | 1920 × 1080 | 1 | Desktop |\n| `desktop-1440p` | Desktop 1440p | 2560 × 1440 | 1 | Desktop |\n| `desktop-4k` | Desktop 4K | 3840 × 2160 | 1 | Desktop |\n| `ultrawide` | Ultrawide 21:9 | 3440 × 1440 | 1 | Desktop |\n| `tv-1080p` | TV 1080p | 1920 × 1080 | 1 | TV |\n| `tv-4k` | TV 4K | 3840 × 2160 | 1 | TV |\n\n## Script Template\n\nSave as `screenshot-audit.mjs` and run with `node screenshot-audit.mjs`:\n\n```js\n#!/usr/bin/env node\n/**\n * Multi-Device Screenshot Auditor\n * Usage: node screenshot-audit.mjs [options]\n *\n * Options (via environment variables):\n *   AUDIT_URL          Target URL (required)\n *   AUDIT_DEVICES      Comma-separated device slugs (default: all)\n *   AUDIT_FULL_PAGE    \"true\" for full-page screenshots (default: viewport only)\n *   AUDIT_AUTH_STATE   Path to Playwright storage state JSON (for authenticated pages)\n *   AUDIT_AUTH_COOKIE  Raw cookie string, e.g. \"session=abc; csrf=xyz\"\n *   AUDIT_AUTH_TOKEN   Bearer token (added as Authorization header)\n *   AUDIT_OUTPUT_DIR   Output directory (default: ./screenshots)\n *   AUDIT_PAGES        JSON array of paths to capture, e.g. '[\"/\",\"/dashboard\"]'\n *   AUDIT_TIMEOUT      Navigation timeout in ms (default: 30000)\n */\n\nimport { chromium } from 'playwright'\nimport { writeFileSync, mkdirSync, existsSync } from 'fs'\nimport { join, resolve } from 'path'\n\nconst DEVICES = [\n  { slug: 'iphone-se',           label: 'iPhone SE',           width: 375,  height: 667,  dpr: 2,   ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1' },\n  { slug: 'iphone-14',           label: 'iPhone 14',           width: 390,  height: 844,  dpr: 3,   ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },\n  { slug: 'iphone-14-pro-max',   label: 'iPhone 14 Pro Max',   width: 430,  height: 932,  dpr: 3,   ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },\n  { slug: 'pixel-7',             label: 'Pixel 7',             width: 412,  height: 915,  dpr: 2.6, ua: 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Mobile Safari/537.36' },\n  { slug: 'galaxy-s8',           label: 'Galaxy S8',           width: 360,  height: 740,  dpr: 3,   ua: 'Mozilla/5.0 (Linux; Android 8.0.0; SM-G950F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36' },\n  { slug: 'iphone-14-landscape', label: 'iPhone 14 Landscape', width: 844,  height: 390,  dpr: 3,   ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },\n  { slug: 'ipad-mini',           label: 'iPad Mini',           width: 768,  height: 1024, dpr: 2,   ua: 'Mozilla/5.0 (iPad; CPU OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1' },\n  { slug: 'ipad-pro-11',         label: 'iPad Pro 11\"',        width: 834,  height: 1194, dpr: 2,   ua: 'Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },\n  { slug: 'ipad-pro-12',         label: 'iPad Pro 12.9\"',      width: 1024, height: 1366, dpr: 2,   ua: 'Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },\n  { slug: 'ipad-pro-landscape',  label: 'iPad Pro Landscape',  width: 1366, height: 1024, dpr: 2,   ua: 'Mozilla/5.0 (iPad; CPU OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' },\n  { slug: 'macbook-air',         label: 'MacBook Air 13\"',     width: 1280, height: 800,  dpr: 2,   ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },\n  { slug: 'laptop-hd',           label: 'Laptop HD',           width: 1366, height: 768,  dpr: 1,   ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },\n  { slug: 'macbook-pro-16',      label: 'MacBook Pro 16\"',     width: 1728, height: 1117, dpr: 2,   ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },\n  { slug: 'desktop-1080p',       label: 'Desktop 1080p',       width: 1920, height: 1080, dpr: 1,   ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },\n  { slug: 'desktop-1440p',       label: 'Desktop 1440p',       width: 2560, height: 1440, dpr: 1,   ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },\n  { slug: 'desktop-4k',          label: 'Desktop 4K',          width: 3840, height: 2160, dpr: 1,   ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },\n  { slug: 'ultrawide',           label: 'Ultrawide 21:9',      width: 3440, height: 1440, dpr: 1,   ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' },\n  { slug: 'tv-1080p',            label: 'TV 1080p',            width: 1920, height: 1080, dpr: 1,   ua: 'Mozilla/5.0 (SMART-TV; Linux; Tizen 6.0) AppleWebKit/538.1 (KHTML, like Gecko) Version/6.0 TV Safari/538.1' },\n  { slug: 'tv-4k',               label: 'TV 4K',               width: 3840, height: 2160, dpr: 1,   ua: 'Mozilla/5.0 (SMART-TV; Linux; Tizen 6.0) AppleWebKit/538.1 (KHTML, like Gecko) Version/6.0 TV Safari/538.1' },\n]\n\nasync function run() {\n  const url = process.env.AUDIT_URL\n  if (!url) throw new Error('AUDIT_URL is required')\n\n  const targetDeviceSlugs = process.env.AUDIT_DEVICES\n    ? process.env.AUDIT_DEVICES.split(',').map(s => s.trim())\n    : null\n  const devices = targetDeviceSlugs\n    ? DEVICES.filter(d => targetDeviceSlugs.includes(d.slug))\n    : DEVICES\n\n  const fullPage = process.env.AUDIT_FULL_PAGE === 'true'\n  const outputDir = resolve(process.env.AUDIT_OUTPUT_DIR || './screenshots')\n  const timeout = parseInt(process.env.AUDIT_TIMEOUT || '30000', 10)\n  const pages = process.env.AUDIT_PAGES\n    ? JSON.parse(process.env.AUDIT_PAGES)\n    : [new URL(url).pathname || '/']\n\n  // Auth options\n  const authState = process.env.AUDIT_AUTH_STATE   // path to Playwright storage state JSON\n  const authCookie = process.env.AUDIT_AUTH_COOKIE  // raw cookie string\n  const authToken = process.env.AUDIT_AUTH_TOKEN    // Bearer token\n\n  mkdirSync(outputDir, { recursive: true })\n\n  // --no-sandbox is required on Linux and in CI/Docker environments\n  // It is safe to use when you control the content being rendered\n  const browser = await chromium.launch({\n    args: ['--no-sandbox', '--disable-setuid-sandbox'],\n  })\n  const results = []\n\n  for (const device of devices) {\n    for (const pagePath of pages) {\n      const pageUrl = pagePath.startsWith('http') ? pagePath : new URL(pagePath, url).href\n      const fileSuffix = pages.length > 1 ? `-${pagePath.replace(/\\//g, '_').replace(/^_/, '') || 'home'}` : ''\n      const fileName = `${device.slug}${fileSuffix}-${device.width}x${device.height}.png`\n      const filePath = join(outputDir, fileName)\n\n      const contextOptions = {\n        viewport: { width: device.width, height: device.height },\n        deviceScaleFactor: device.dpr,\n        userAgent: device.ua,\n        ...(authState && existsSync(authState) ? { storageState: authState } : {}),\n      }\n\n      const context = await browser.newContext(contextOptions)\n\n      // Inject auth cookie if provided (never logged)\n      if (authCookie) {\n        const cookieEntries = authCookie.split(';').map(c => {\n          const [name, ...rest] = c.trim().split('=')\n          return { name: name.trim(), value: rest.join('=').trim(), url: pageUrl }\n        })\n        await context.addCookies(cookieEntries)\n      }\n\n      const page = await context.newPage()\n\n      // Inject auth token header if provided (never logged)\n      if (authToken) {\n        await page.setExtraHTTPHeaders({ Authorization: `Bearer ${authToken}` })\n      }\n\n      let status = 'ok'\n      try {\n        await page.goto(pageUrl, { waitUntil: 'networkidle', timeout })\n        // Wait for fonts, lazy images, and CSS animations to settle\n        await page.waitForTimeout(1000)\n        await page.screenshot({ path: filePath, fullPage })\n      } catch (err) {\n        status = err.message.includes('timeout') ? 'timeout' : `error: ${err.message}`\n      }\n\n      results.push({ device: device.label, slug: device.slug, width: device.width, height: device.height, dpr: device.dpr, file: filePath, status, page: pagePath })\n      console.log(`[${status === 'ok' ? '✓' : '✗'}] ${device.label} (${device.width}×${device.height}) → ${fileName}`)\n\n      await context.close()\n    }\n  }\n\n  await browser.close()\n\n  // Write report\n  const reportLines = [\n    `# Screenshot Audit Report`,\n    ``,\n    `**URL:** ${url}`,\n    `**Date:** ${new Date().toISOString()}`,\n    `**Mode:** ${fullPage ? 'Full page' : 'Viewport only'}`,\n    `**Devices:** ${devices.length} | **Pages:** ${pages.length} | **Total:** ${results.length}`,\n    ``,\n    `## Results`,\n    ``,\n    `| Device | Size | DPR | Page | File | Status |`,\n    `|--------|------|-----|------|------|--------|`,\n    ...results.map(r =>\n      `| ${r.device} | ${r.width}×${r.height} | ${r.dpr} | ${r.page} | \\`${r.file.split('/').pop()}\\` | ${r.status === 'ok' ? '✅' : '❌ ' + r.status} |`\n    ),\n    ``,\n    `## Usage with AI Agents`,\n    ``,\n    `Share screenshots with an AI agent for layout analysis:`,\n    ``,\n    '```',\n    `# Share mobile screenshots for responsive feedback`,\n    `ls ${outputDir}/iphone-*.png ${outputDir}/ipad-*.png`,\n    ``,\n    `# Share all desktop sizes`,\n    `ls ${outputDir}/desktop-*.png ${outputDir}/macbook-*.png`,\n    '```',\n  ]\n  writeFileSync(join(outputDir, 'REPORT.md'), reportLines.join('\\n'))\n  console.log(`\\nReport: ${join(outputDir, 'REPORT.md')}`)\n}\n\nrun().catch(err => { console.error(err); process.exit(1) })\n```\n\n## Quick Start\n\n```bash\n# Install dependencies (run once from your project directory)\nnpm install -D playwright\nnpx playwright install chromium\n\n# Screenshot a public page across all devices\nAUDIT_URL=https://example.com node screenshot-audit.mjs\n\n# Screenshot only mobile devices\nAUDIT_URL=https://example.com AUDIT_DEVICES=iphone-se,iphone-14,pixel-7 node screenshot-audit.mjs\n\n# Authenticated page via Playwright storage state\nAUDIT_URL=https://myapp.com AUDIT_AUTH_STATE=.auth/session.json node screenshot-audit.mjs\n\n# Full-page screenshots of multiple paths\nAUDIT_URL=https://example.com \\\n  AUDIT_FULL_PAGE=true \\\n  AUDIT_PAGES='[\"/\",\"/pricing\",\"/blog\"]' \\\n  node screenshot-audit.mjs\n\n# Screenshot localhost (dev server)\nAUDIT_URL=http://localhost:3000 node screenshot-audit.mjs\n```\n\n## Generating a Playwright Storage State (for auth)\n\n```bash\n# Log in interactively and save session to .auth/session.json\nnpx playwright codegen --save-storage=.auth/session.json https://myapp.com/login\n```\n\nThe saved `.auth/session.json` contains cookies and localStorage. Pass its path as `AUDIT_AUTH_STATE`.\n**Never commit this file** — add `.auth/` to `.gitignore`.\n\n## Auth Credential Security\n\nCredentials are **never written to the report or any log file**:\n- `AUDIT_AUTH_STATE` — only the file *path* appears in output; file contents are not logged\n- `AUDIT_AUTH_COOKIE` — injected via Playwright context, not logged\n- `AUDIT_AUTH_TOKEN` — injected via HTTP headers, not logged\n\nFor CI/CD, pass these as secrets (GitHub Actions secrets, Vercel env vars, etc.).\n\n## Troubleshooting\n\n**`Cannot find package 'playwright'`**\nRun the script from a directory where Playwright is installed (`node_modules/playwright` exists). Either:\n```bash\nnpm install -D playwright && npx playwright install chromium\nnode screenshot-audit.mjs\n```\nOr install globally: `npm install -g playwright && playwright install chromium`\n\n**Chromium fails to launch on Linux / CI / Docker**\nThe script already includes `--no-sandbox` and `--disable-setuid-sandbox` which are required on Linux and in containerized environments. If you still get sandbox errors, also try:\n```bash\n# In Docker, you may also need:\nPLAYWRIGHT_CHROMIUM_SANDBOX=false node screenshot-audit.mjs\n```\n\n**`net::ERR_CONNECTION_REFUSED` on localhost**\nMake sure your dev server is running before starting the audit:\n```bash\nnpm run dev &   # Start dev server in background\nAUDIT_URL=http://localhost:3000 node screenshot-audit.mjs\n```\n\n**Screenshots are blank / all black**\nIncrease the settle timeout by setting a longer wait, or check that the page doesn't require JavaScript-heavy rendering that exceeds the default wait. The script already waits 1000ms after `networkidle` — for heavier apps you may want to adjust the `page.waitForTimeout(1000)` call.\n\n**Some devices time out**\nLarge viewports (4K, ultrawide) take longer to render. Increase the timeout:\n```bash\nAUDIT_TIMEOUT=60000 AUDIT_URL=https://example.com node screenshot-audit.mjs\n```\n\n## Output Structure\n\n```\nscreenshots/\n  iphone-se-375x667.png\n  iphone-14-390x844.png\n  iphone-14-pro-max-430x932.png\n  pixel-7-412x915.png\n  galaxy-s8-360x740.png\n  iphone-14-landscape-844x390.png\n  ipad-mini-768x1024.png\n  ipad-pro-11-834x1194.png\n  ipad-pro-12-1024x1366.png\n  ipad-pro-landscape-1366x1024.png\n  macbook-air-1280x800.png\n  laptop-hd-1366x768.png\n  macbook-pro-16-1728x1117.png\n  desktop-1080p-1920x1080.png\n  desktop-1440p-2560x1440.png\n  desktop-4k-3840x2160.png\n  ultrawide-3440x1440.png\n  tv-1080p-1920x1080.png\n  tv-4k-3840x2160.png\n  REPORT.md\n```\n\n## Using Screenshots with AI Agents\n\nAfter capturing, share the screenshots with Claude (or any multimodal model) for analysis:\n\n```\nHere are screenshots of my website at different device sizes. Please review them and identify:\n1. Any layout breakpoints that look broken or cramped\n2. Text that's too small or hard to read on mobile\n3. Elements that overflow or get cut off\n4. Navigation/header issues on smaller screens\n5. Any spacing or alignment problems\n\n[attach: iphone-se-375x667.png, ipad-mini-768x1024.png, desktop-1080p-1920x1080.png]\n```\n\n## Playground\n\n<!DOCTYPE html><html><head><meta charset='utf-8'><style>*{box-sizing:border-box;margin:0;padding:0}body{background:#0d1117;color:#e6edf3;font-family:monospace;font-size:12px;height:100vh;display:flex;flex-direction:column;overflow:hidden}.header{background:#161b22;border-bottom:1px solid #30363d;padding:8px 14px;font-size:11px;color:#8b949e;display:flex;justify-content:space-between;align-items:center;flex-shrink:0}.title{color:#58a6ff;font-weight:bold;font-size:13px}.panels{display:flex;flex:1;overflow:hidden}.panel{flex:1;overflow:auto;padding:12px;border-right:1px solid #30363d}.panel:last-child{border-right:none}.label{font-size:10px;color:#8b949e;text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}pre{white-space:pre-wrap;word-break:break-word;line-height:1.5}</style></head><body><div class='header'><span class='title'>Multi-Device Screenshot Auditor</span><span>Example · SkillSlap</span></div><div class='panels'><div class='panel'><div class='label'>Input: Audit request</div><pre><span style='color:#8b949e'>URL: https://myapp.com/dashboard</span>\n<span style='color:#8b949e'>Devices: iPhone 15, Pixel 8,</span>\n<span style='color:#8b949e'>  iPad Air, Galaxy S24</span>\n<span style='color:#8b949e'>Check: layout, overflow,</span>\n<span style='color:#8b949e'>  touch targets, contrast</span></pre></div><div class='panel'><div class='label'>Output: Audit report</div><pre><span style='color:#58a6ff'>Multi-Device Audit — dashboard</span>\n\n<span style='color:#3fb950'>✅ iPhone 15 (390x844) — PASS</span>\n<span style='color:#8b949e'>  Layout intact, no overflow</span>\n\n<span style='color:#f85149'>❌ Galaxy S24 (360x780) — FAIL</span>\n<span style='color:#8b949e'>  Nav overflow at 360px width</span>\n<span style='color:#8b949e'>  → hamburger menu missing</span>\n<span style='color:#8b949e'>  Touch targets: 2 buttons &lt;44px</span>\n\n<span style='color:#e3b341'>⚠ iPad Air (820x1180) — WARN</span>\n<span style='color:#8b949e'>  Sidebar only 180px at tablet</span>\n<span style='color:#8b949e'>  breakpoint — content too narrow</span>\n\n<span style='color:#3fb950'>✅ Pixel 8 (412x915) — PASS</span></pre></div></div></body></html>"}}