
Chrome Devtools
- 36 installs
- 7 repo stars
- Updated June 18, 2026
- duc01226/easyplatform
Automates the browser and runs debugging and performance analysis using Puppeteer CLI scripts.
About
Provides browser automation, debugging, and performance analysis using Puppeteer CLI scripts. A developer uses it to drive Chrome for testing and diagnosing page issues.
- Browser automation and debugging via Puppeteer CLI scripts
- Performance analysis of pages
Chrome Devtools by the numbers
- 36 all-time installs (skills.sh)
- Ranked #1,308 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/duc01226/easyplatform --skill chrome-devtoolsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 36 |
|---|---|
| repo stars | ★ 7 |
| Last updated | June 18, 2026 |
| Repository | duc01226/easyplatform ↗ |
What it does
Automates the browser and runs debugging and performance analysis using Puppeteer CLI scripts.
Files
Codex compatibility note:
>
- Invoke repository skills with$skill-namein Codex; this mirrored copy rewrites legacy Claude/skill-namereferences.
- Task tracker mandate: BEFORE executing any workflow or skill step, create/update task tracking for all steps and keep it synchronized as progress changes.
- User-question prompts mean to ask the user directly in Codex.
- Ignore Claude-specific mode-switch instructions when they appear.
- Strict execution contract: when a user explicitly invokes a skill, execute that skill protocol as written.
- Subagent authorization: when a skill is user-invoked or AI-detected and its protocol requires subagents, that skill activation authorizes use of the required spawn_agent subagent(s) for that task.- Do not skip, reorder, or merge protocol steps unless the user explicitly approves the deviation first.
- For workflow skills, execute each listed child-skill step explicitly and report step-by-step evidence.
- If a required step/tool cannot run in this environment, stop and ask the user before adapting.
<!-- CODEX:PROJECT-REFERENCE-LOADING:START -->
Codex Project-Reference Loading (No Hooks)
Codex does not receive Claude hook-based doc injection. When coding, planning, debugging, testing, or reviewing, open project docs explicitly using this routing.
Always read:
docs/project-config.json(project-specific paths, commands, modules, and workflow/test settings)docs/project-reference/docs-index-reference.md(routes to the fulldocs/project-reference/*catalog)docs/project-reference/lessons.md(always-on guardrails and anti-patterns)
Situation-based docs:
- Backend/CQRS/API/domain/entity changes:
backend-patterns-reference.md,domain-entities-reference.md,project-structure-reference.md - Frontend/UI/styling/design-system:
frontend-patterns-reference.md,scss-styling-guide.md,design-system/README.md - Spec/test-case planning or TC mapping:
feature-docs-reference.md - Integration test implementation/review:
integration-test-reference.md - E2E test implementation/review:
e2e-test-reference.md - Code review/audit work:
code-review-rules.mdplus domain docs above based on changed files
Do not read all docs blindly. Start from docs-index-reference.md, then open only relevant files for the task.
<!-- CODEX:PROJECT-REFERENCE-LOADING:END -->
Quick Summary
Goal: Automate browser interactions, debugging, and performance analysis using Puppeteer CLI scripts with persistent sessions.
Workflow:
1. Discover Structure — Use aria-snapshot.js to get ARIA tree of unknown pages 2. Interact by Ref — Use select-ref.js to click, fill, hover elements by stable refs 3. Capture Evidence — Take screenshots, collect console logs, monitor network traffic 4. Analyze — Performance profiling, accessibility audits, visual regression checks
Key Rules:
- Use headed mode on Windows/macOS, headless only on Linux/WSL/CI
- All scripts output JSON for structured processing
- Store snapshots in
.claude/chrome-devtools/snapshots/with timestamps
Be skeptical. Apply critical thinking, sequential thinking. Every claim needs traced proof, confidence percentages (Idea should be more than 80%).
Chrome DevTools Agent Skill
Browser automation via Puppeteer scripts with persistent sessions. All scripts output JSON.
Skill Location
Skills can exist in project-scope or user-scope. Priority: project-scope > user-scope.
# Detect skill location
SKILL_DIR=""
if [ -d ".claude/skills/chrome-devtools/scripts" ]; then
SKILL_DIR=".claude/skills/chrome-devtools/scripts"
elif [ -d "$HOME/.claude/skills/chrome-devtools/scripts" ]; then
SKILL_DIR="$HOME/.claude/skills/chrome-devtools/scripts"
fi
cd "$SKILL_DIR"Choosing Your Approach
| Scenario | Approach |
|---|---|
| Source-available sites | Read source code first, write selectors directly |
| Unknown layouts | Use aria-snapshot.js for semantic discovery |
| Visual inspection | Take screenshots to verify rendering |
| Debug issues | Collect console logs, analyze with session storage |
| Accessibility audit | Use ARIA snapshot for semantic structure analysis |
Automation Browsing Running Mode
- Detect current OS and launch browser as headless only when running on Linux, WSL, or CI environments.
- For macOS/Windows, browser always runs in headed mode for better debugging.
- Run multiple scripts/sessions in parallel to simulate real user interactions.
- Run multiple scripts/sessions in parallel to simulate different device types (mobile, tablet, desktop).
- Skills can exist in project-scope or user-scope. Priority: project-scope > user-scope.
ARIA Snapshot (Element Discovery)
When page structure is unknown, use aria-snapshot.js to get a YAML-formatted accessibility tree with semantic roles, accessible names, states, and stable element references.
Get ARIA Snapshot
# Generate ARIA snapshot and output to stdout
node aria-snapshot.js --url https://example.com
# Save to file in snapshots directory
node aria-snapshot.js --url https://example.com --output ./.claude/chrome-devtools/snapshots/page.yamlExample YAML Output
- banner:
- link "Hacker News" [ref=e1]
/url: https://news.ycombinator.com
- navigation:
- link "new" [ref=e2]
- link "past" [ref=e3]
- link "comments" [ref=e4]
- main:
- list:
- listitem:
- link "Show HN: My new project" [ref=e8]
- text: "128 points by user 3 hours ago"
- contentinfo:
- textbox [ref=e10]
/placeholder: "Search"Interpreting ARIA Notation
| Notation | Meaning |
|---|---|
[ref=eN] | Stable identifier for interactive elements |
[checked] | Checkbox/radio is selected |
[disabled] | Element is inactive |
[expanded] | Accordion/dropdown is open |
[level=N] | Heading hierarchy (1-6) |
/url: | Link destination |
/placeholder: | Input placeholder text |
/value: | Current input value |
Interact by Ref
Skills can exist in project-scope or user-scope. Priority: project-scope > user-scope. Use select-ref.js to interact with elements by their ref:
# Click element with ref e5
node select-ref.js --ref e5 --action click
# Fill input with ref e10
node select-ref.js --ref e10 --action fill --value "search query"
# Get text content
node select-ref.js --ref e8 --action text
# Screenshot specific element
node select-ref.js --ref e1 --action screenshot --output ./logo.png
# Focus element
node select-ref.js --ref e10 --action focus
# Hover over element
node select-ref.js --ref e5 --action hoverStore Snapshots
Skills can exist in project-scope or user-scope. Priority: project-scope > user-scope. Store snapshots for analysis in <project>/.claude/chrome-devtools/snapshots/:
# Create snapshots directory
mkdir -p .claude/chrome-devtools/snapshots
# Capture and store with timestamp
SESSION="$(date +%Y%m%d-%H%M%S)"
node aria-snapshot.js --url https://example.com --output .claude/chrome-devtools/snapshots/$SESSION.yamlWorkflow: Unknown Page Structure
1. Get snapshot to discover elements:
node aria-snapshot.js --url https://example.com2. Identify target from YAML output (e.g., [ref=e5] for a button)
3. Interact by ref:
node select-ref.js --ref e5 --action click4. Verify result with screenshot or new snapshot:
node screenshot.js --output ./result.pngLocal HTML Files
Skills can exist in project-scope or user-scope. Priority: project-scope > user-scope. IMPORTANT: Never browse local HTML files via file:// protocol. Always serve via local server: Why: file:// protocol blocks many browser features (CORS, ES modules, fetch API, service workers). Local server ensures proper HTTP behavior.
# Option 1: npx serve (recommended)
npx serve ./dist -p 3000 &
node navigate.js --url http://localhost:3000
# Option 2: Python http.server
python -m http.server 3000 --directory ./dist &
node navigate.js --url http://localhost:3000Note: when port 3000 is busy, find an available port with lsof -i:3000 and use a different one.
Quick Start
# Install dependencies
cd .claude/skills/chrome-devtools/scripts
npm install # Installs puppeteer, sharp, debug, yargs
# Test (browser stays running for session reuse)
node navigate.js --url https://example.com
# Output: {"success": true, "url": "...", "title": "..."}Linux/WSL only: Run ./install-deps.sh first for Chrome system libraries.
Session Persistence
Browser state persists across script executions via WebSocket endpoint file (.browser-session.json).
Default behavior: Scripts disconnect but keep browser running for session reuse.
# First script: launches browser, navigates, disconnects (browser stays running)
node navigate.js --url https://example.com/login
# Subsequent scripts: connect to existing browser, reuse page state
node fill.js --selector "#email" --value "user@example.com"
node fill.js --selector "#password" --value "secret"
node click.js --selector "button[type=submit]"
# Close browser when done
node navigate.js --url about:blank --close trueSession management:
--close true: Close browser and clear session- Default (no flag): Keep browser running for next script
Available Scripts
Skills can exist in project-scope or user-scope. Priority: project-scope > user-scope. All in .claude/skills/chrome-devtools/scripts/:
| Script | Purpose |
|---|---|
navigate.js | Navigate to URLs |
screenshot.js | Capture screenshots (auto-compress >5MB via Sharp) |
click.js | Click elements |
fill.js | Fill form fields |
evaluate.js | Execute JS in page context |
snapshot.js | Extract interactive elements (JSON format) |
aria-snapshot.js | Get ARIA accessibility tree (YAML format with refs) |
select-ref.js | Interact with elements by ref from ARIA snapshot |
console.js | Monitor console messages/errors |
network.js | Track HTTP requests/responses |
performance.js | Measure Core Web Vitals |
Workflow Loop
1. Execute focused script for single task 2. Observe JSON output 3. Assess completion status 4. Decide next action 5. Repeat until done
Writing Custom Test Scripts
Skills can exist in project-scope or user-scope. Priority: project-scope > user-scope. For complex automation, write scripts to <project>/.claude/chrome-devtools/tmp/:
# Create tmp directory for test scripts
mkdir -p $SKILL_DIR/.claude/chrome-devtools/tmp
# Write a test script
cat > $SKILL_DIR/.claude/chrome-devtools/tmp/login-test.js << 'EOF'
import { getBrowser, getPage, disconnectBrowser, outputJSON } from '../scripts/lib/browser.js';
async function loginTest() {
const browser = await getBrowser();
const page = await getPage(browser);
await page.goto('https://example.com/login');
await page.type('#email', 'user@example.com');
await page.type('#password', 'secret');
await page.click('button[type=submit]');
await page.waitForNavigation();
outputJSON({
success: true,
url: page.url(),
title: await page.title()
});
await disconnectBrowser();
}
loginTest();
EOF
# Run the test
node $SKILL_DIR/.claude/chrome-devtools/tmp/login-test.jsKey principles for custom scripts:
- Single-purpose: one script, one task
- Always call
disconnectBrowser()at the end (keeps browser running) - Use
closeBrowser()only when ending session completely - Output JSON for easy parsing
- Plain JavaScript only in
page.evaluate()callbacks
Screenshots
Skills can exist in project-scope or user-scope. Priority: project-scope > user-scope. Store screenshots for analysis in <project>/.claude/chrome-devtools/screenshots/:
# Basic screenshot
node screenshot.js --url https://example.com --output ./.claude/chrome-devtools/screenshots/page.png
# Full page
node screenshot.js --url https://example.com --output ./.claude/chrome-devtools/screenshots/page.png --full-page true
# Specific element
node screenshot.js --url https://example.com --selector ".main-content" --output ./.claude/chrome-devtools/screenshots/element.pngAuto-Compression (Sharp)
Screenshots >5MB auto-compress using Sharp (4-5x faster than ImageMagick):
# Default: compress if >5MB
node screenshot.js --url https://example.com --output ./.claude/chrome-devtools/screenshots/page.png
# Custom threshold (3MB)
node screenshot.js --url https://example.com --output ./.claude/chrome-devtools/screenshots/page.png --max-size 3
# Disable compression
node screenshot.js --url https://example.com --output ./.claude/chrome-devtools/screenshots/page.png --no-compressStore screenshots for analysis in <project>/.claude/chrome-devtools/screenshots/.
Console Log Collection & Analysis
Skills can exist in project-scope or user-scope. Priority: project-scope > user-scope.
Capture Logs
# Capture all logs for 10 seconds
node console.js --url https://example.com --duration 10000
# Filter by type
node console.js --url https://example.com --types error,warn --duration 5000Session Storage Pattern
Store logs for analysis in <project>/.claude/chrome-devtools/logs/<session>/:
# Create session directory
SESSION="$(date +%Y%m%d-%H%M%S)"
mkdir -p .claude/chrome-devtools/logs/$SESSION
# Capture and store
node console.js --url https://example.com --duration 10000 > .claude/chrome-devtools/logs/$SESSION/console.json
node network.js --url https://example.com > .claude/chrome-devtools/logs/$SESSION/network.json
# View errors
jq '.messages[] | select(.type=="error")' .claude/chrome-devtools/logs/$SESSION/console.jsonRoot Cause Analysis
# 1. Check for JavaScript errors
node console.js --url https://example.com --types error,pageerror --duration 5000 | jq '.messages'
# 2. Correlate with network failures
node network.js --url https://example.com | jq '.requests[] | select(.response.status >= 400)'
# 3. Check specific error stack traces
node console.js --url https://example.com --types error --duration 5000 | jq '.messages[].stack'Finding Elements
Skills can exist in project-scope or user-scope. Priority: project-scope > user-scope. Use snapshot.js to discover selectors before interacting:
# Get all interactive elements
node snapshot.js --url https://example.com | jq '.elements[] | {tagName, text, selector}'
# Find buttons
node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="button")'
# Find by text content
node snapshot.js --url https://example.com | jq '.elements[] | select(.text | contains("Submit"))'Error Recovery
Skills can exist in project-scope or user-scope. Priority: project-scope > user-scope. If script fails:
# 1. Capture current state (without navigating to preserve state)
node screenshot.js --output ./.claude/skills/chrome-devtools/screenshots/debug.png
# 2. Get console errors
node console.js --url about:blank --types error --duration 1000
# 3. Discover correct selector
node snapshot.js | jq '.elements[] | select(.text | contains("Submit"))'
# 4. Try XPath if CSS fails
node click.js --selector "//button[contains(text(),'Submit')]"Common Patterns
Web Scraping
node evaluate.js --url https://example.com --script "
Array.from(document.querySelectorAll('.item')).map(el => ({
title: el.querySelector('h2')?.textContent,
link: el.querySelector('a')?.href
}))
" | jq '.result'Form Automation
node navigate.js --url https://example.com/form
node fill.js --selector "#search" --value "query"
node click.js --selector "button[type=submit]"Performance Testing
node performance.js --url https://example.com | jq '.vitals'Script Options
All scripts support:
--headless false- Show browser window--close true- Close browser completely (default: stay running)--timeout 30000- Set timeout (ms)--wait-until networkidle2- Wait strategy
Skills can exist in project-scope or user-scope. Priority: project-scope > user-scope.
Troubleshooting
Skills can exist in project-scope or user-scope. Priority: project-scope > user-scope.
| Error | Solution |
|---|---|
Cannot find package 'puppeteer' | Run npm install in scripts directory |
libnss3.so missing (Linux) | Run ./install-deps.sh |
| Element not found | Use snapshot.js to find correct selector |
| Script hangs | Use --timeout 60000 or --wait-until load |
| Screenshot >5MB | Auto-compressed; use --max-size 3 for lower |
| Session stale | Delete .browser-session.json and retry |
Screenshot Analysis: Missing Images
If images don't appear in screenshots, they may be waiting for animation triggers:
1. Scroll-triggered animations: Scroll element into view first
node evaluate.js --script "document.querySelector('.lazy-image').scrollIntoView()"
# Wait for animation
node evaluate.js --script "await new Promise(r => setTimeout(r, 1000))"
node screenshot.js --output ./result.png2. Sequential animation queue: Wait longer and retry
# First attempt
node screenshot.js --url http://localhost:3000 --output ./attempt1.png
# Wait for animations to complete
node evaluate.js --script "await new Promise(r => setTimeout(r, 2000))"
# Retry screenshot
node screenshot.js --output ./attempt2.png3. Intersection Observer animations: Trigger by scrolling through page
node evaluate.js --script "window.scrollTo(0, document.body.scrollHeight)"
node evaluate.js --script "await new Promise(r => setTimeout(r, 1500))"
node evaluate.js --script "window.scrollTo(0, 0)"
node screenshot.js --output ./full-loaded.png --full-page trueReference Documentation
./references/cdp-domains.md- Chrome DevTools Protocol domains./references/puppeteer-reference.md- Puppeteer API patterns./references/performance-guide.md- Core Web Vitals optimization./scripts/README.md- Detailed script options
---
[IMPORTANT] Use task tracking to break ALL work into small tasks BEFORE starting — including tasks for each file read. This prevents context loss from long files. For simple tasks, AI MUST ATTENTION ask user whether to skip.
<!-- SYNC:ai-mistake-prevention -->
AI Mistake Prevention — Failure modes to avoid on every task:
>
Check downstream references before deleting. Deleting components causes documentation and code staleness cascades. Map all referencing files before removal.
Verify AI-generated content against actual code. AI hallucinates APIs, class names, and method signatures. Always grep to confirm existence before documenting or referencing.
Trace full dependency chain after edits. Changing a definition misses downstream variables and consumers derived from it. Always trace the full chain.
Trace ALL code paths when verifying correctness. Confirming code exists is not confirming it executes. Always trace early exits, error branches, and conditional skips — not just happy path.
When debugging, ask "whose responsibility?" before fixing. Trace whether bug is in caller (wrong data) or callee (wrong handling). Fix at responsible layer — never patch symptom site.
Assume existing values are intentional — ask WHY before changing. Before changing any constant, limit, flag, or pattern: read comments, check git blame, examine surrounding code.
Verify ALL affected outputs, not just the first. Changes touching multiple stacks require verifying EVERY output. One green check is not all green checks.
Holistic-first debugging — resist nearest-attention trap. When investigating any failure, list EVERY precondition first (config, env vars, DB names, endpoints, DI registrations, data preconditions), then verify each against evidence before forming any code-layer hypothesis.
Surgical changes — apply the diff test. Bug fix: every changed line must trace directly to the bug. Don't restyle or improve adjacent code. Enhancement task: implement improvements AND announce them explicitly.
Surface ambiguity before coding — don't pick silently. If request has multiple interpretations, present each with effort estimate and ask. Never assume all-records, file-based, or more complex path.
<!-- /SYNC:ai-mistake-prevention -->
<!-- SYNC:critical-thinking-mindset -->
Critical Thinking Mindset — Apply critical thinking, sequential thinking. Every claim needs traced proof, confidence >80% to act.
Anti-hallucination: Never present guess as fact — cite sources for every claim, admit uncertainty freely, self-check output for errors, cross-reference independently, stay skeptical of own confidence — certainty without evidence root of all hallucination.
<!-- /SYNC:critical-thinking-mindset -->
<!-- SYNC:critical-thinking-mindset:reminder -->
MUST ATTENTION apply critical thinking — every claim needs traced proof, confidence >80% to act. Anti-hallucination: never present guess as fact.
<!-- /SYNC:critical-thinking-mindset:reminder -->
<!-- SYNC:ai-mistake-prevention:reminder -->
MUST ATTENTION apply AI mistake prevention — holistic-first debugging, fix at responsible layer, surface ambiguity before coding, re-read files after compaction.
<!-- /SYNC:ai-mistake-prevention:reminder -->
Closing Reminders
- MANDATORY IMPORTANT MUST ATTENTION break work into small todo tasks using task tracking BEFORE starting
- MANDATORY IMPORTANT MUST ATTENTION search codebase for 3+ similar patterns before creating new code
- MANDATORY IMPORTANT MUST ATTENTION cite
file:lineevidence for every claim (confidence >80% to act) - MANDATORY IMPORTANT MUST ATTENTION add a final review todo task to verify work quality
[TASK-PLANNING] Before acting, analyze task scope and systematically break it into small todo tasks and sub-tasks using task tracking.
<!-- CODEX:SYNC-PROMPT-PROTOCOLS:START -->
Hookless Prompt Protocol Mirror (Auto-Synced)
Source: .claude/hooks/lib/prompt-injections.cjs + .claude/.ck.json
[WORKFLOW-EXECUTION-PROTOCOL] [BLOCKING] Workflow Execution Protocol — MANDATORY IMPORTANT MUST CRITICAL. Do not skip for any reason.
Generic portability boundary: Reusable skills and protocol text stay project-neutral; project-specific conventions are discovered from docs/project-config.json and docs/project-reference/. Apply shared AI-SDD from shared/sdd-artifact-contract.md. Read docs/project-config.json and docs/project-reference/docs-index-reference.md, then open the project reference docs named there. Any supported AI tool may execute when this shared context and local docs are available.
1. DETECT: Match prompt against workflow catalog 2. ANALYZE: Find best-match workflow AND evaluate if a custom step combination would fit better 3. ASK (REQUIRED FORMAT): Use a direct user question with this structure unless the user explicitly invoked a workflow/skill and the local protocol treats explicit invocation as confirmation:
- Question: "Which workflow do you want to activate?"
- Option 1: "Activate [BestMatch Workflow] (Recommended)"
- Option 2: "Activate custom workflow: [step1 → step2 → ...]" (include one-line rationale)
4. ACTIVATE (if confirmed): Call $workflow-start <workflowId> for standard; sequence custom steps manually 5. CREATE TASKS: task tracking for ALL workflow steps 6. EXECUTE: Follow each step in sequence [CRITICAL-THINKING-MINDSET] Apply critical thinking, sequential thinking. Every claim needs traced proof, confidence >80% to act. Anti-hallucination principle: Never present guess as fact — cite sources for every claim, admit uncertainty freely, self-check output for errors, cross-reference independently, stay skeptical of own confidence — certainty without evidence root of all hallucination. AI Attention principle (Primacy-Recency): Put the 3 most critical rules at both top and bottom of long prompts/protocols so instruction adherence survives long context windows. Goal-driven execution: Define success criteria first, loop until verified, and stop only when observable checks pass. Tests verify intent: Tests must protect business rules/invariants and fail when the protected intent breaks, not only mirror current behavior.
[LESSON-LEARNED-REMINDER] [BLOCKING] Task Planning & Continuous Improvement — MANDATORY. Do not skip.
Break work into small tasks (task tracking) before starting. Add final task: "Analyze AI mistakes & lessons learned".
Extract lessons — ROOT CAUSE ONLY, not symptom fixes:
1. Name the FAILURE MODE (reasoning/assumption failure), not symptom — "assumed API existed without reading source" not "used wrong enum value". 2. Generality test: does this failure mode apply to ≥3 contexts/codebases? If not, abstract one level up. 3. Write as a universal rule — strip project-specific names/paths/classes. Useful on any codebase. 4. Consolidate: multiple mistakes sharing one failure mode → ONE lesson. 5. Recurrence gate: "Would this recur in future session WITHOUT this reminder?" — No → skip $learn. 6. Auto-fix gate: "Could $code-review/$code-simplifier/$security/$lint catch this?" — Yes → improve review skill instead. 7. BOTH gates pass → ask user to run $learn. [TASK-PLANNING] [MANDATORY] BEFORE executing any workflow or skill step, create/update task tracking for all planned steps, then keep it synchronized as each step starts/completes.
<!-- CODEX:SYNC-PROMPT-PROTOCOLS:END -->
Chrome DevTools Protocol (CDP) Domains Reference
Complete reference of CDP domains and their capabilities for browser automation and debugging.
Overview
CDP is organized into 47 domains, each providing specific browser capabilities. Domains are grouped by functionality:
- Core - Fundamental browser control
- DOM & Styling - Page structure and styling
- Network & Fetch - HTTP traffic management
- Page & Navigation - Page lifecycle control
- Storage & Data - Browser storage APIs
- Performance & Profiling - Metrics and analysis
- Emulation & Simulation - Device and network emulation
- Worker & Service - Background tasks
- Developer Tools - Debugging support
---
Core Domains
Runtime
Purpose: Execute JavaScript, manage objects, handle promises
Key Commands:
Runtime.evaluate(expression)- Execute JavaScriptRuntime.callFunctionOn(functionDeclaration, objectId)- Call function on objectRuntime.getProperties(objectId)- Get object propertiesRuntime.awaitPromise(promiseObjectId)- Wait for promise resolution
Key Events:
Runtime.consoleAPICalled- Console message loggedRuntime.exceptionThrown- Uncaught exception
Use Cases:
- Execute custom JavaScript
- Access page data
- Monitor console output
- Handle exceptions
---
Debugger
Purpose: JavaScript debugging, breakpoints, stack traces
Key Commands:
Debugger.enable()- Enable debuggerDebugger.setBreakpoint(location)- Set breakpointDebugger.pause()- Pause executionDebugger.resume()- Resume executionDebugger.stepOver/stepInto/stepOut()- Step through code
Key Events:
Debugger.paused- Execution pausedDebugger.resumed- Execution resumedDebugger.scriptParsed- Script loaded
Use Cases:
- Debug JavaScript errors
- Inspect call stacks
- Set conditional breakpoints
- Source map support
---
Console (Deprecated - Use Runtime/Log)
Purpose: Legacy console message access
Note: Use Runtime.consoleAPICalled event instead for new implementations.
---
DOM & Styling Domains
DOM
Purpose: Access and manipulate DOM tree
Key Commands:
DOM.getDocument()- Get root document nodeDOM.querySelector(nodeId, selector)- Query selectorDOM.querySelectorAll(nodeId, selector)- Query allDOM.getAttributes(nodeId)- Get element attributesDOM.setOuterHTML(nodeId, outerHTML)- Replace elementDOM.getBoxModel(nodeId)- Get element layout boxDOM.focus(nodeId)- Focus element
Key Events:
DOM.documentUpdated- Document changedDOM.setChildNodes- Child nodes updated
Use Cases:
- Navigate DOM tree
- Query elements
- Modify DOM structure
- Get element positions
---
CSS
Purpose: Inspect and modify CSS styles
Key Commands:
CSS.enable()- Enable CSS domainCSS.getComputedStyleForNode(nodeId)- Get computed stylesCSS.getInlineStylesForNode(nodeId)- Get inline stylesCSS.getMatchedStylesForNode(nodeId)- Get matched CSS rulesCSS.setStyleTexts(edits)- Modify styles
Key Events:
CSS.styleSheetAdded- Stylesheet addedCSS.styleSheetChanged- Stylesheet modified
Use Cases:
- Inspect element styles
- Debug CSS issues
- Modify styles dynamically
- Extract stylesheet data
---
Accessibility
Purpose: Access accessibility tree
Key Commands:
Accessibility.enable()- Enable accessibilityAccessibility.getFullAXTree()- Get complete AX treeAccessibility.getPartialAXTree(nodeId)- Get node subtreeAccessibility.queryAXTree(nodeId, role, name)- Query AX tree
Use Cases:
- Accessibility testing
- Screen reader simulation
- ARIA attribute inspection
- AX tree analysis
---
Network & Fetch Domains
Network
Purpose: Monitor and control HTTP traffic
Key Commands:
Network.enable()- Enable network trackingNetwork.setCacheDisabled(cacheDisabled)- Disable cacheNetwork.setExtraHTTPHeaders(headers)- Add custom headersNetwork.getCookies(urls)- Get cookiesNetwork.setCookie(name, value, domain)- Set cookieNetwork.getResponseBody(requestId)- Get response bodyNetwork.emulateNetworkConditions(offline, latency, downloadThroughput, uploadThroughput)- Throttle network
Key Events:
Network.requestWillBeSent- Request startingNetwork.responseReceived- Response receivedNetwork.loadingFinished- Request completedNetwork.loadingFailed- Request failed
Use Cases:
- Monitor API calls
- Intercept requests
- Analyze response data
- Simulate slow networks
- Manage cookies
---
Fetch
Purpose: Intercept and modify network requests
Key Commands:
Fetch.enable(patterns)- Enable request interceptionFetch.continueRequest(requestId, url, method, headers)- Continue/modify requestFetch.fulfillRequest(requestId, responseCode, headers, body)- Mock responseFetch.failRequest(requestId, errorReason)- Fail request
Key Events:
Fetch.requestPaused- Request intercepted
Use Cases:
- Mock API responses
- Block requests
- Modify request/response
- Test error scenarios
---
Page & Navigation Domains
Page
Purpose: Control page lifecycle and navigation
Key Commands:
Page.enable()- Enable page domainPage.navigate(url)- Navigate to URLPage.reload(ignoreCache)- Reload pagePage.goBack()/goForward()- Navigate historyPage.captureScreenshot(format, quality)- Take screenshotPage.printToPDF(landscape, displayHeaderFooter)- Generate PDFPage.getLayoutMetrics()- Get page dimensionsPage.createIsolatedWorld(frameId)- Create isolated contextPage.handleJavaScriptDialog(accept, promptText)- Handle alerts/confirms
Key Events:
Page.loadEventFired- Page loadedPage.domContentEventFired- DOM readyPage.frameNavigated- Frame navigatedPage.javascriptDialogOpening- Alert/confirm shown
Use Cases:
- Navigate pages
- Capture screenshots
- Generate PDFs
- Handle popups
- Monitor page lifecycle
---
Target
Purpose: Manage browser targets (tabs, workers, frames)
Key Commands:
Target.getTargets()- List all targetsTarget.createTarget(url)- Open new tabTarget.closeTarget(targetId)- Close tabTarget.attachToTarget(targetId)- Attach debuggerTarget.detachFromTarget(sessionId)- Detach debuggerTarget.setDiscoverTargets(discover)- Auto-discover targets
Key Events:
Target.targetCreated- New target createdTarget.targetDestroyed- Target closedTarget.targetInfoChanged- Target updated
Use Cases:
- Multi-tab automation
- Service worker debugging
- Frame inspection
- Extension debugging
---
Input
Purpose: Simulate user input
Key Commands:
Input.dispatchKeyEvent(type, key, code)- Keyboard inputInput.dispatchMouseEvent(type, x, y, button)- Mouse inputInput.dispatchTouchEvent(type, touchPoints)- Touch inputInput.synthesizePinchGesture(x, y, scaleFactor)- Pinch gestureInput.synthesizeScrollGesture(x, y, xDistance, yDistance)- Scroll
Use Cases:
- Simulate clicks
- Type text
- Drag and drop
- Touch gestures
- Scroll pages
---
Storage & Data Domains
Storage
Purpose: Manage browser storage
Key Commands:
Storage.getCookies(browserContextId)- Get cookiesStorage.setCookies(cookies)- Set cookiesStorage.clearCookies(browserContextId)- Clear cookiesStorage.clearDataForOrigin(origin, storageTypes)- Clear storageStorage.getUsageAndQuota(origin)- Get storage usage
Storage Types:
- appcache, cookies, file_systems, indexeddb, local_storage, shader_cache, websql, service_workers, cache_storage
Use Cases:
- Cookie management
- Clear browser data
- Inspect storage usage
- Test quota limits
---
DOMStorage
Purpose: Access localStorage/sessionStorage
Key Commands:
DOMStorage.enable()- Enable storage trackingDOMStorage.getDOMStorageItems(storageId)- Get itemsDOMStorage.setDOMStorageItem(storageId, key, value)- Set itemDOMStorage.removeDOMStorageItem(storageId, key)- Remove item
Key Events:
DOMStorage.domStorageItemsCleared- Storage clearedDOMStorage.domStorageItemAdded/Updated/Removed- Item changed
---
IndexedDB
Purpose: Query IndexedDB databases
Key Commands:
IndexedDB.requestDatabaseNames(securityOrigin)- List databasesIndexedDB.requestDatabase(securityOrigin, databaseName)- Get DB structureIndexedDB.requestData(securityOrigin, databaseName, objectStoreName)- Query data
Use Cases:
- Inspect IndexedDB data
- Debug database issues
- Extract stored data
---
CacheStorage
Purpose: Manage Cache API
Key Commands:
CacheStorage.requestCacheNames(securityOrigin)- List cachesCacheStorage.requestCachedResponses(cacheId, securityOrigin)- List cached responsesCacheStorage.deleteCache(cacheId)- Delete cache
Use Cases:
- Service worker cache inspection
- Offline functionality testing
---
Performance & Profiling Domains
Performance
Purpose: Collect performance metrics
Key Commands:
Performance.enable()- Enable performance trackingPerformance.disable()- Disable trackingPerformance.getMetrics()- Get current metrics
Metrics:
- Timestamp, Documents, Frames, JSEventListeners, Nodes, LayoutCount, RecalcStyleCount, LayoutDuration, RecalcStyleDuration, ScriptDuration, TaskDuration, JSHeapUsedSize, JSHeapTotalSize
Use Cases:
- Monitor page metrics
- Track memory usage
- Measure render times
---
PerformanceTimeline
Purpose: Access Performance Timeline API
Key Commands:
PerformanceTimeline.enable(eventTypes)- Subscribe to events
Event Types:
- mark, measure, navigation, resource, longtask, paint, layout-shift
Key Events:
PerformanceTimeline.timelineEventAdded- New performance entry
---
Tracing
Purpose: Record Chrome trace
Key Commands:
Tracing.start(categories, options)- Start recordingTracing.end()- Stop recordingTracing.requestMemoryDump()- Capture memory snapshot
Trace Categories:
- blink, cc, devtools, gpu, loading, navigation, rendering, v8, disabled-by-default-\*
Key Events:
Tracing.dataCollected- Trace chunk receivedTracing.tracingComplete- Recording finished
Use Cases:
- Deep performance analysis
- Frame rendering profiling
- CPU flame graphs
- Memory profiling
---
Profiler
Purpose: CPU profiling
Key Commands:
Profiler.enable()- Enable profilerProfiler.start()- Start CPU profilingProfiler.stop()- Stop and get profile
Use Cases:
- Find CPU bottlenecks
- Optimize JavaScript
- Generate flame graphs
---
HeapProfiler (via Memory domain)
Purpose: Memory profiling
Key Commands:
Memory.getDOMCounters()- Get DOM object countsMemory.prepareForLeakDetection()- Prepare leak detectionMemory.forciblyPurgeJavaScriptMemory()- Force GCMemory.setPressureNotificationsSuppressed(suppressed)- Control memory warningsMemory.simulatePressureNotification(level)- Simulate memory pressure
Use Cases:
- Detect memory leaks
- Analyze heap snapshots
- Monitor object counts
---
Emulation & Simulation Domains
Emulation
Purpose: Emulate device conditions
Key Commands:
Emulation.setDeviceMetricsOverride(width, height, deviceScaleFactor, mobile)- Emulate deviceEmulation.setGeolocationOverride(latitude, longitude, accuracy)- Fake locationEmulation.setEmulatedMedia(media, features)- Emulate media typeEmulation.setTimezoneOverride(timezoneId)- Override timezoneEmulation.setLocaleOverride(locale)- Override languageEmulation.setUserAgentOverride(userAgent)- Change user agent
Use Cases:
- Mobile device testing
- Geolocation testing
- Print media emulation
- Timezone/locale testing
---
DeviceOrientation
Purpose: Simulate device orientation
Key Commands:
DeviceOrientation.setDeviceOrientationOverride(alpha, beta, gamma)- Set orientation
Use Cases:
- Test accelerometer features
- Orientation-dependent layouts
---
Worker & Service Domains
ServiceWorker
Purpose: Manage service workers
Key Commands:
ServiceWorker.enable()- Enable trackingServiceWorker.unregister(scopeURL)- Unregister workerServiceWorker.startWorker(scopeURL)- Start workerServiceWorker.stopWorker(versionId)- Stop workerServiceWorker.inspectWorker(versionId)- Debug worker
Key Events:
ServiceWorker.workerRegistrationUpdated- Registration changedServiceWorker.workerVersionUpdated- Version updated
---
WebAuthn
Purpose: Simulate WebAuthn/FIDO2
Key Commands:
WebAuthn.enable()- Enable virtual authenticatorsWebAuthn.addVirtualAuthenticator(options)- Add virtual deviceWebAuthn.removeVirtualAuthenticator(authenticatorId)- Remove deviceWebAuthn.addCredential(authenticatorId, credential)- Add credential
Use Cases:
- Test WebAuthn flows
- Simulate biometric auth
- Test security keys
---
Developer Tools Support
Inspector
Purpose: Protocol-level debugging
Key Events:
Inspector.detached- Debugger disconnectedInspector.targetCrashed- Target crashed
---
Log
Purpose: Collect browser logs
Key Commands:
Log.enable()- Enable log collectionLog.clear()- Clear logs
Key Events:
Log.entryAdded- New log entry
Use Cases:
- Collect console logs
- Monitor violations
- Track deprecations
---
DOMDebugger
Purpose: DOM-level debugging
Key Commands:
DOMDebugger.setDOMBreakpoint(nodeId, type)- Break on DOM changesDOMDebugger.setEventListenerBreakpoint(eventName)- Break on eventDOMDebugger.setXHRBreakpoint(url)- Break on XHR
Breakpoint Types:
- subtree-modified, attribute-modified, node-removed
---
DOMSnapshot
Purpose: Capture complete DOM snapshot
Key Commands:
DOMSnapshot.captureSnapshot(computedStyles)- Capture full DOM
Use Cases:
- Export page structure
- Offline analysis
- DOM diffing
---
Audits (Lighthouse Integration)
Purpose: Run automated audits
Key Commands:
Audits.enable()- Enable auditsAudits.getEncodingIssues()- Check encoding issues
---
LayerTree
Purpose: Inspect rendering layers
Key Commands:
LayerTree.enable()- Enable layer trackingLayerTree.compositingReasons(layerId)- Get why layer created
Key Events:
LayerTree.layerTreeDidChange- Layers changed
Use Cases:
- Debug rendering performance
- Identify layer creation
- Optimize compositing
---
Other Domains
Browser
Purpose: Browser-level control
Key Commands:
Browser.getVersion()- Get browser infoBrowser.getBrowserCommandLine()- Get launch argsBrowser.setPermission(permission, setting, origin)- Set permissionsBrowser.grantPermissions(permissions, origin)- Grant permissions
Permissions:
- geolocation, midi, notifications, push, camera, microphone, background-sync, sensors, accessibility-events, clipboard-read, clipboard-write, payment-handler
---
IO
Purpose: File I/O operations
Key Commands:
IO.read(handle, offset, size)- Read streamIO.close(handle)- Close stream
Use Cases:
- Read large response bodies
- Process binary data
---
Media
Purpose: Inspect media players
Key Commands:
Media.enable()- Track media players
Key Events:
Media.playerPropertiesChanged- Player state changedMedia.playerEventsAdded- Player events
---
BackgroundService
Purpose: Track background services
Key Commands:
BackgroundService.startObserving(service)- Track service
Services:
- backgroundFetch, backgroundSync, pushMessaging, notifications, paymentHandler, periodicBackgroundSync
---
Domain Dependencies
Some domains depend on others and must be enabled in order:
Runtime (no dependencies)
↓
DOM (depends on Runtime)
↓
CSS (depends on DOM)
Network (no dependencies)
Page (depends on Runtime)
↓
Target (depends on Page)
Debugger (depends on Runtime)Quick Command Reference
Most Common Commands
// Navigation
Page.navigate(url)
Page.reload()
// JavaScript Execution
Runtime.evaluate(expression)
// DOM Access
DOM.getDocument()
DOM.querySelector(nodeId, selector)
// Screenshots
Page.captureScreenshot(format, quality)
// Network Monitoring
Network.enable()
// Listen for Network.requestWillBeSent events
// Console Messages
// Listen for Runtime.consoleAPICalled events
// Cookies
Network.getCookies(urls)
Network.setCookie(...)
// Device Emulation
Emulation.setDeviceMetricsOverride(width, height, ...)
// Performance
Performance.getMetrics()
Tracing.start(categories)
Tracing.end()---
Best Practices
1. Enable domains before use: Always call .enable() for stateful domains 2. Handle events: Subscribe to events for real-time updates 3. Clean up: Disable domains when done to reduce overhead 4. Use sessions: Attach to specific targets for isolated debugging 5. Handle errors: Implement proper error handling for command failures 6. Version awareness: Check browser version for experimental API support
---
Additional Resources
- Protocol Viewer - Interactive domain browser
- Protocol JSON - Machine-readable specification
- Getting Started with CDP
- devtools-protocol NPM - TypeScript definitions
Performance Analysis Guide
Comprehensive guide to analyzing web performance using Chrome DevTools Protocol, Puppeteer, and chrome-devtools skill.
Table of Contents
- Core Web Vitals
- Performance Tracing
- Network Analysis
- JavaScript Performance
- Rendering Performance
- Memory Analysis
- Optimization Strategies
---
Core Web Vitals
Overview
Core Web Vitals are Google's standardized metrics for measuring user experience:
- LCP (Largest Contentful Paint) - Loading performance (< 2.5s good)
- FID (First Input Delay) - Interactivity (< 100ms good)
- CLS (Cumulative Layout Shift) - Visual stability (< 0.1 good)
Measuring with chrome-devtools-mcp
// Start performance trace
await useTool('performance_start_trace', {
categories: ['loading', 'rendering', 'scripting']
});
// Navigate to page
await useTool('navigate_page', {
url: 'https://example.com'
});
// Wait for complete load
await useTool('wait_for', {
waitUntil: 'networkidle'
});
// Stop trace and get data
await useTool('performance_stop_trace');
// Get AI-powered insights
const insights = await useTool('performance_analyze_insight');
// insights will include:
// - LCP timing
// - FID analysis
// - CLS score
// - Performance recommendationsMeasuring with Puppeteer
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Measure Core Web Vitals
await page.goto('https://example.com', {
waitUntil: 'networkidle2'
});
const vitals = await page.evaluate(() => {
return new Promise(resolve => {
const vitals = {
LCP: null,
FID: null,
CLS: 0
};
// LCP
new PerformanceObserver(list => {
const entries = list.getEntries();
vitals.LCP = entries[entries.length - 1].renderTime || entries[entries.length - 1].loadTime;
}).observe({ entryTypes: ['largest-contentful-paint'] });
// FID
new PerformanceObserver(list => {
vitals.FID = list.getEntries()[0].processingStart - list.getEntries()[0].startTime;
}).observe({ entryTypes: ['first-input'] });
// CLS
new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
if (!entry.hadRecentInput) {
vitals.CLS += entry.value;
}
});
}).observe({ entryTypes: ['layout-shift'] });
// Wait 5 seconds for metrics
setTimeout(() => resolve(vitals), 5000);
});
});
console.log('Core Web Vitals:', vitals);Other Important Metrics
TTFB (Time to First Byte)
const ttfb = await page.evaluate(() => {
const [navigationEntry] = performance.getEntriesByType('navigation');
return navigationEntry.responseStart - navigationEntry.requestStart;
});FCP (First Contentful Paint)
const fcp = await page.evaluate(() => {
const paintEntries = performance.getEntriesByType('paint');
const fcpEntry = paintEntries.find(e => e.name === 'first-contentful-paint');
return fcpEntry ? fcpEntry.startTime : null;
});TTI (Time to Interactive)
// Requires lighthouse or manual calculation
const tti = await page.evaluate(() => {
// Complex calculation based on network idle and long tasks
// Best to use Lighthouse for accurate TTI
});---
Performance Tracing
Chrome Trace Categories
Loading:
- Page load events
- Resource loading
- Parser activity
Rendering:
- Layout calculations
- Paint operations
- Compositing
Scripting:
- JavaScript execution
- V8 compilation
- Garbage collection
Network:
- HTTP requests
- WebSocket traffic
- Resource fetching
Input:
- User input processing
- Touch/scroll events
GPU:
- GPU operations
- Compositing work
Record Performance Trace
Using chrome-devtools-mcp:
// Start trace with specific categories
await useTool('performance_start_trace', {
categories: ['loading', 'rendering', 'scripting', 'network']
});
// Perform actions
await useTool('navigate_page', { url: 'https://example.com' });
await useTool('wait_for', { waitUntil: 'networkidle' });
// Optional: Interact with page
await useTool('click', { uid: 'button-uid' });
// Stop trace
const traceData = await useTool('performance_stop_trace');
// Analyze trace
const insights = await useTool('performance_analyze_insight');Using Puppeteer:
// Start tracing
await page.tracing.start({
path: 'trace.json',
categories: ['devtools.timeline', 'disabled-by-default-devtools.timeline', 'disabled-by-default-v8.cpu_profiler']
});
// Navigate
await page.goto('https://example.com', {
waitUntil: 'networkidle2'
});
// Stop tracing
await page.tracing.stop();
// Analyze in Chrome DevTools (chrome://tracing)Analyze Trace Data
Key Metrics from Trace:
1. Main Thread Activity
- JavaScript execution time
- Layout/reflow time
- Paint time
- Long tasks (> 50ms)
2. Network Waterfall
- Request start times
- DNS lookup
- Connection time
- Download time
3. Rendering Pipeline
- DOM construction
- Style calculation
- Layout
- Paint
- Composite
Common Issues to Look For:
- Long tasks blocking main thread
- Excessive JavaScript execution
- Layout thrashing
- Unnecessary repaints
- Slow network requests
- Large bundle sizes
---
Network Analysis
Monitor Network Requests
Using chrome-devtools-mcp:
// Navigate to page
await useTool('navigate_page', { url: 'https://example.com' });
// Wait for all requests
await useTool('wait_for', { waitUntil: 'networkidle' });
// List all requests
const requests = await useTool('list_network_requests', {
resourceTypes: ['Document', 'Script', 'Stylesheet', 'Image', 'XHR', 'Fetch'],
pageSize: 100
});
// Analyze specific request
for (const req of requests.requests) {
const details = await useTool('get_network_request', {
requestId: req.id
});
console.log({
url: details.url,
method: details.method,
status: details.status,
size: details.encodedDataLength,
time: details.timing.receiveHeadersEnd - details.timing.requestTime,
cached: details.fromCache
});
}Using Puppeteer:
const requests = [];
// Capture all requests
page.on('request', request => {
requests.push({
url: request.url(),
method: request.method(),
resourceType: request.resourceType(),
headers: request.headers()
});
});
// Capture responses
page.on('response', response => {
const request = response.request();
console.log({
url: response.url(),
status: response.status(),
size: response.headers()['content-length'],
cached: response.fromCache(),
timing: response.timing()
});
});
await page.goto('https://example.com');Network Performance Metrics
Calculate Total Page Weight:
let totalBytes = 0;
let resourceCounts = {};
page.on('response', async response => {
const type = response.request().resourceType();
const buffer = await response.buffer();
totalBytes += buffer.length;
resourceCounts[type] = (resourceCounts[type] || 0) + 1;
});
await page.goto('https://example.com');
console.log('Total size:', (totalBytes / 1024 / 1024).toFixed(2), 'MB');
console.log('Resources:', resourceCounts);Identify Slow Requests:
page.on('response', response => {
const timing = response.timing();
const totalTime = timing.receiveHeadersEnd - timing.requestTime;
if (totalTime > 1000) {
// Slower than 1 second
console.log('Slow request:', {
url: response.url(),
time: totalTime.toFixed(2) + 'ms',
size: response.headers()['content-length']
});
}
});Network Throttling
Simulate Slow Connection:
// Using chrome-devtools-mcp
await useTool('emulate_network', {
throttlingOption: 'Slow 3G' // or 'Fast 3G', 'Slow 4G'
});
// Using Puppeteer
const client = await page.createCDPSession();
await client.send('Network.emulateNetworkConditions', {
offline: false,
downloadThroughput: (400 * 1024) / 8, // 400 Kbps
uploadThroughput: (400 * 1024) / 8,
latency: 2000 // 2000ms RTT
});---
JavaScript Performance
Identify Long Tasks
Using Performance Observer:
await page.evaluate(() => {
return new Promise(resolve => {
const longTasks = [];
const observer = new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
longTasks.push({
name: entry.name,
duration: entry.duration,
startTime: entry.startTime
});
});
});
observer.observe({ entryTypes: ['longtask'] });
// Collect for 10 seconds
setTimeout(() => {
observer.disconnect();
resolve(longTasks);
}, 10000);
});
});CPU Profiling
Using Puppeteer:
// Start CPU profiling
const client = await page.createCDPSession();
await client.send('Profiler.enable');
await client.send('Profiler.start');
// Navigate and interact
await page.goto('https://example.com');
await page.click('.button');
// Stop profiling
const { profile } = await client.send('Profiler.stop');
// Analyze profile (flame graph data)
// Import into Chrome DevTools for visualizationJavaScript Coverage
Identify Unused Code:
// Start coverage
await Promise.all([page.coverage.startJSCoverage(), page.coverage.startCSSCoverage()]);
// Navigate
await page.goto('https://example.com');
// Stop coverage
const [jsCoverage, cssCoverage] = await Promise.all([page.coverage.stopJSCoverage(), page.coverage.stopCSSCoverage()]);
// Calculate unused bytes
function calculateUnusedBytes(coverage) {
let usedBytes = 0;
let totalBytes = 0;
for (const entry of coverage) {
totalBytes += entry.text.length;
for (const range of entry.ranges) {
usedBytes += range.end - range.start - 1;
}
}
return {
usedBytes,
totalBytes,
unusedBytes: totalBytes - usedBytes,
unusedPercentage: (((totalBytes - usedBytes) / totalBytes) * 100).toFixed(2)
};
}
console.log('JS Coverage:', calculateUnusedBytes(jsCoverage));
console.log('CSS Coverage:', calculateUnusedBytes(cssCoverage));Bundle Size Analysis
Analyze JavaScript Bundles:
page.on('response', async response => {
const url = response.url();
const type = response.request().resourceType();
if (type === 'script') {
const buffer = await response.buffer();
const size = buffer.length;
console.log({
url: url.split('/').pop(),
size: (size / 1024).toFixed(2) + ' KB',
gzipped: response.headers()['content-encoding'] === 'gzip'
});
}
});---
Rendering Performance
Layout Thrashing Detection
Monitor Layout Recalculations:
// Using Performance Observer
await page.evaluate(() => {
return new Promise(resolve => {
const measurements = [];
const observer = new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
if (entry.entryType === 'measure' && entry.name.includes('layout')) {
measurements.push({
name: entry.name,
duration: entry.duration,
startTime: entry.startTime
});
}
});
});
observer.observe({ entryTypes: ['measure'] });
setTimeout(() => {
observer.disconnect();
resolve(measurements);
}, 5000);
});
});Paint and Composite Metrics
Get Paint Metrics:
const paintMetrics = await page.evaluate(() => {
const paints = performance.getEntriesByType('paint');
return {
firstPaint: paints.find(p => p.name === 'first-paint')?.startTime,
firstContentfulPaint: paints.find(p => p.name === 'first-contentful-paint')?.startTime
};
});Frame Rate Analysis
Monitor FPS:
await page.evaluate(() => {
return new Promise(resolve => {
let frames = 0;
let lastTime = performance.now();
function countFrames() {
frames++;
requestAnimationFrame(countFrames);
}
countFrames();
setTimeout(() => {
const now = performance.now();
const elapsed = (now - lastTime) / 1000;
const fps = frames / elapsed;
resolve(fps);
}, 5000);
});
});Layout Shifts (CLS)
Track Individual Shifts:
await page.evaluate(() => {
return new Promise(resolve => {
const shifts = [];
let totalCLS = 0;
const observer = new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
if (!entry.hadRecentInput) {
totalCLS += entry.value;
shifts.push({
value: entry.value,
time: entry.startTime,
elements: entry.sources?.map(s => s.node)
});
}
});
});
observer.observe({ entryTypes: ['layout-shift'] });
setTimeout(() => {
observer.disconnect();
resolve({ totalCLS, shifts });
}, 10000);
});
});---
Memory Analysis
Memory Metrics
Get Memory Usage:
// Using chrome-devtools-mcp
await useTool('evaluate_script', {
expression: `
({
usedJSHeapSize: performance.memory?.usedJSHeapSize,
totalJSHeapSize: performance.memory?.totalJSHeapSize,
jsHeapSizeLimit: performance.memory?.jsHeapSizeLimit
})
`,
returnByValue: true
});
// Using Puppeteer
const metrics = await page.metrics();
console.log({
jsHeapUsed: (metrics.JSHeapUsedSize / 1024 / 1024).toFixed(2) + ' MB',
jsHeapTotal: (metrics.JSHeapTotalSize / 1024 / 1024).toFixed(2) + ' MB',
domNodes: metrics.Nodes,
documents: metrics.Documents,
jsEventListeners: metrics.JSEventListeners
});Memory Leak Detection
Monitor Memory Over Time:
async function detectMemoryLeak(page, duration = 30000) {
const samples = [];
const interval = 1000; // Sample every second
const samples_count = duration / interval;
for (let i = 0; i < samples_count; i++) {
const metrics = await page.metrics();
samples.push({
time: i,
heapUsed: metrics.JSHeapUsedSize
});
await page.waitForTimeout(interval);
}
// Analyze trend
const firstSample = samples[0].heapUsed;
const lastSample = samples[samples.length - 1].heapUsed;
const increase = (((lastSample - firstSample) / firstSample) * 100).toFixed(2);
return {
samples,
memoryIncrease: increase + '%',
possibleLeak: increase > 50 // > 50% increase indicates possible leak
};
}
const leakAnalysis = await detectMemoryLeak(page, 30000);
console.log('Memory Analysis:', leakAnalysis);Heap Snapshot
Capture Heap Snapshot:
const client = await page.createCDPSession();
// Take snapshot
await client.send('HeapProfiler.enable');
const { result } = await client.send('HeapProfiler.takeHeapSnapshot');
// Snapshot is streamed in chunks
// Save to file or analyze programmatically---
Optimization Strategies
Image Optimization
Detect Unoptimized Images:
const images = await page.evaluate(() => {
const images = Array.from(document.querySelectorAll('img'));
return images.map(img => ({
src: img.src,
naturalWidth: img.naturalWidth,
naturalHeight: img.naturalHeight,
displayWidth: img.width,
displayHeight: img.height,
oversized: img.naturalWidth > img.width * 1.5 || img.naturalHeight > img.height * 1.5
}));
});
const oversizedImages = images.filter(img => img.oversized);
console.log('Oversized images:', oversizedImages);Font Loading
Detect Render-Blocking Fonts:
const fonts = await page.evaluate(() => {
return Array.from(document.fonts).map(font => ({
family: font.family,
weight: font.weight,
style: font.style,
status: font.status,
loaded: font.status === 'loaded'
}));
});
console.log('Fonts:', fonts);Third-Party Scripts
Measure Third-Party Impact:
const thirdPartyDomains = ['googletagmanager.com', 'facebook.net', 'doubleclick.net'];
page.on('response', async response => {
const url = response.url();
const isThirdParty = thirdPartyDomains.some(domain => url.includes(domain));
if (isThirdParty) {
const buffer = await response.buffer();
console.log({
url: url,
size: (buffer.length / 1024).toFixed(2) + ' KB',
type: response.request().resourceType()
});
}
});Critical Rendering Path
Identify Render-Blocking Resources:
await page.goto('https://example.com');
const renderBlockingResources = await page.evaluate(() => {
const resources = performance.getEntriesByType('resource');
return resources
.filter(resource => {
return (
(resource.initiatorType === 'link' && resource.name.includes('.css')) ||
(resource.initiatorType === 'script' && !resource.name.includes('async'))
);
})
.map(r => ({
url: r.name,
duration: r.duration,
startTime: r.startTime
}));
});
console.log('Render-blocking resources:', renderBlockingResources);Lighthouse Integration
Run Lighthouse Audit:
import lighthouse from 'lighthouse';
import { launch } from 'chrome-launcher';
// Launch Chrome
const chrome = await launch({ chromeFlags: ['--headless'] });
// Run Lighthouse
const { lhr } = await lighthouse('https://example.com', {
port: chrome.port,
onlyCategories: ['performance']
});
// Get scores
console.log({
performanceScore: lhr.categories.performance.score * 100,
metrics: {
FCP: lhr.audits['first-contentful-paint'].displayValue,
LCP: lhr.audits['largest-contentful-paint'].displayValue,
TBT: lhr.audits['total-blocking-time'].displayValue,
CLS: lhr.audits['cumulative-layout-shift'].displayValue,
SI: lhr.audits['speed-index'].displayValue
},
opportunities: lhr.audits['opportunities']
});
await chrome.kill();---
Performance Budgets
Set Performance Budgets
const budgets = {
// Core Web Vitals
LCP: 2500, // ms
FID: 100, // ms
CLS: 0.1, // score
// Other metrics
FCP: 1800, // ms
TTI: 3800, // ms
TBT: 300, // ms
// Resource budgets
totalPageSize: 2 * 1024 * 1024, // 2 MB
jsSize: 500 * 1024, // 500 KB
cssSize: 100 * 1024, // 100 KB
imageSize: 1 * 1024 * 1024, // 1 MB
// Request counts
totalRequests: 50,
jsRequests: 10,
cssRequests: 5
};
async function checkBudgets(page, budgets) {
// Measure actual values
const vitals = await measureCoreWebVitals(page);
const resources = await analyzeResources(page);
// Compare against budgets
const violations = [];
if (vitals.LCP > budgets.LCP) {
violations.push(`LCP: ${vitals.LCP}ms exceeds budget of ${budgets.LCP}ms`);
}
if (resources.totalSize > budgets.totalPageSize) {
violations.push(`Page size: ${resources.totalSize} exceeds budget of ${budgets.totalPageSize}`);
}
// ... check other budgets
return {
passed: violations.length === 0,
violations
};
}---
Automated Performance Testing
CI/CD Integration
// performance-test.js
import puppeteer from 'puppeteer';
async function performanceTest(url) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Measure metrics
await page.goto(url, { waitUntil: 'networkidle2' });
const metrics = await page.metrics();
const vitals = await measureCoreWebVitals(page);
await browser.close();
// Check against thresholds
const thresholds = {
LCP: 2500,
FID: 100,
CLS: 0.1,
jsHeapSize: 50 * 1024 * 1024 // 50 MB
};
const failed = [];
if (vitals.LCP > thresholds.LCP) failed.push('LCP');
if (vitals.FID > thresholds.FID) failed.push('FID');
if (vitals.CLS > thresholds.CLS) failed.push('CLS');
if (metrics.JSHeapUsedSize > thresholds.jsHeapSize) failed.push('Memory');
if (failed.length > 0) {
console.error('Performance test failed:', failed);
process.exit(1);
}
console.log('Performance test passed');
}
performanceTest(process.env.TEST_URL);---
Best Practices
Performance Testing Checklist
1. Measure Multiple Times
- Run tests 3-5 times
- Use median values
- Account for variance
2. Test Different Conditions
- Fast 3G
- Slow 3G
- Offline
- CPU throttling
3. Test Different Devices
- Mobile (low-end)
- Mobile (high-end)
- Desktop
- Tablet
4. Monitor Over Time
- Track metrics in CI/CD
- Set up alerts for regressions
- Create performance dashboards
5. Focus on User Experience
- Prioritize Core Web Vitals
- Test real user journeys
- Consider perceived performance
6. Optimize Critical Path
- Minimize render-blocking resources
- Defer non-critical JavaScript
- Optimize font loading
- Lazy load images
---
Resources
Puppeteer Quick Reference
Complete guide to browser automation with Puppeteer - a high-level API over Chrome DevTools Protocol.
Table of Contents
- Setup
- Browser & Page Management
- Navigation
- Element Interaction
- JavaScript Execution
- Screenshots & PDFs
- Network Interception
- Device Emulation
- Performance
- Common Patterns
---
Setup
Installation
# Install Puppeteer
npm install puppeteer
# Install core only (bring your own Chrome)
npm install puppeteer-coreBasic Usage
import puppeteer from 'puppeteer';
// Launch browser
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox']
});
// Open page
const page = await browser.newPage();
// Navigate
await page.goto('https://example.com');
// Do work...
// Cleanup
await browser.close();---
Browser & Page Management
Launch Browser
const browser = await puppeteer.launch({
// Visibility
headless: false, // Show browser UI
headless: 'new', // New headless mode (Chrome 112+)
// Chrome location
executablePath: '/path/to/chrome',
channel: 'chrome', // or 'chrome-canary', 'chrome-beta'
// Browser context
userDataDir: './user-data', // Persistent profile
// Window size
defaultViewport: {
width: 1920,
height: 1080,
deviceScaleFactor: 1,
isMobile: false
},
// Advanced options
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-web-security',
'--disable-features=IsolateOrigins',
'--disable-site-isolation-trials',
'--start-maximized'
],
// Debugging
devtools: true, // Open DevTools automatically
slowMo: 250, // Slow down by 250ms per action
// Network
proxy: {
server: 'http://proxy.com:8080'
}
});Connect to Running Browser
// Launch Chrome with debugging
// google-chrome --remote-debugging-port=9222
const browser = await puppeteer.connect({
browserURL: 'http://localhost:9222'
// or browserWSEndpoint: 'ws://localhost:9222/devtools/browser/...'
});Page Management
// Create new page
const page = await browser.newPage();
// Get all pages
const pages = await browser.pages();
// Close page
await page.close();
// Multiple pages
const page1 = await browser.newPage();
const page2 = await browser.newPage();
// Switch between pages
await page1.bringToFront();Browser Context (Incognito)
// Create isolated context
const context = await browser.createBrowserContext();
const page = await context.newPage();
// Cleanup context
await context.close();---
Navigation
Basic Navigation
// Navigate to URL
await page.goto('https://example.com');
// Navigate with options
await page.goto('https://example.com', {
waitUntil: 'networkidle2', // or 'load', 'domcontentloaded', 'networkidle0'
timeout: 30000 // Max wait time (ms)
});
// Reload page
await page.reload({ waitUntil: 'networkidle2' });
// Navigation history
await page.goBack();
await page.goForward();
// Wait for navigation
await page.waitForNavigation({
waitUntil: 'networkidle2'
});Wait Until Options
load- Wait for load eventdomcontentloaded- Wait for DOMContentLoaded eventnetworkidle0- Wait until no network connections for 500msnetworkidle2- Wait until max 2 network connections for 500ms
---
Element Interaction
Selectors
// CSS selectors
await page.$('#id');
await page.$('.class');
await page.$('div > p');
// XPath
await page.$x('//button[text()="Submit"]');
// Get all matching elements
await page.$$('.item');
await page.$$x('//div[@class="item"]');Click Elements
// Click by selector
await page.click('.button');
// Click with options
await page.click('.button', {
button: 'left', // or 'right', 'middle'
clickCount: 1, // 2 for double-click
delay: 100 // Delay between mousedown and mouseup
});
// ElementHandle click
const button = await page.$('.button');
await button.click();Type Text
// Type into input
await page.type('#search', 'query text');
// Type with delay
await page.type('#search', 'slow typing', { delay: 100 });
// Clear and type
await page.$eval('#search', el => (el.value = ''));
await page.type('#search', 'new text');Form Interaction
// Fill input
await page.type('#username', 'john@example.com');
await page.type('#password', 'secret123');
// Select dropdown option
await page.select('#country', 'US'); // By value
await page.select('#country', 'USA', 'UK'); // Multiple
// Check/uncheck checkbox
await page.click('input[type="checkbox"]');
// Choose radio button
await page.click('input[value="option2"]');
// Upload file
const input = await page.$('input[type="file"]');
await input.uploadFile('/path/to/file.pdf');
// Submit form
await page.click('button[type="submit"]');
await page.waitForNavigation();Hover & Focus
// Hover over element
await page.hover('.menu-item');
// Focus element
await page.focus('#input');
// Blur
await page.$eval('#input', el => el.blur());Drag & Drop
const source = await page.$('.draggable');
const target = await page.$('.drop-zone');
await source.drag(target);
await source.drop(target);---
JavaScript Execution
Evaluate in Page Context
// Execute JavaScript
const title = await page.evaluate(() => document.title);
// With arguments
const text = await page.evaluate(selector => document.querySelector(selector).textContent, '.heading');
// Return complex data
const data = await page.evaluate(() => ({
title: document.title,
url: location.href,
cookies: document.cookie
}));
// With ElementHandle
const element = await page.$('.button');
const text = await page.evaluate(el => el.textContent, element);Query & Modify DOM
// Get element property
const value = await page.$eval('#input', el => el.value);
// Get multiple elements
const items = await page.$$eval('.item', elements => elements.map(el => el.textContent));
// Modify element
await page.$eval(
'#input',
(el, value) => {
el.value = value;
},
'new value'
);
// Add class
await page.$eval('.element', el => el.classList.add('active'));Expose Functions
// Expose Node.js function to page
await page.exposeFunction('md5', text => crypto.createHash('md5').update(text).digest('hex'));
// Call from page context
const hash = await page.evaluate(async () => {
return await window.md5('hello world');
});---
Screenshots & PDFs
Screenshots
// Full page screenshot
await page.screenshot({
path: 'screenshot.png',
fullPage: true
});
// Viewport screenshot
await page.screenshot({
path: 'viewport.png',
fullPage: false
});
// Element screenshot
const element = await page.$('.chart');
await element.screenshot({
path: 'chart.png'
});
// Screenshot options
await page.screenshot({
path: 'page.png',
type: 'png', // or 'jpeg', 'webp'
quality: 80, // JPEG quality (0-100)
clip: {
// Crop region
x: 0,
y: 0,
width: 500,
height: 500
},
omitBackground: true // Transparent background
});
// Screenshot to buffer
const buffer = await page.screenshot();PDF Generation
// Generate PDF
await page.pdf({
path: 'page.pdf',
format: 'A4', // or 'Letter', 'Legal', etc.
printBackground: true,
margin: {
top: '1cm',
right: '1cm',
bottom: '1cm',
left: '1cm'
}
});
// Custom page size
await page.pdf({
path: 'custom.pdf',
width: '8.5in',
height: '11in',
landscape: true
});
// Header and footer
await page.pdf({
path: 'report.pdf',
displayHeaderFooter: true,
headerTemplate: '<div style="font-size:10px;">Header</div>',
footerTemplate: '<div style="font-size:10px;">Page <span class="pageNumber"></span></div>'
});---
Network Interception
Request Interception
// Enable request interception
await page.setRequestInterception(true);
// Intercept requests
page.on('request', request => {
// Block specific resource types
if (request.resourceType() === 'image') {
request.abort();
}
// Block URLs
else if (request.url().includes('ads')) {
request.abort();
}
// Modify request
else if (request.url().includes('api')) {
request.continue({
headers: {
...request.headers(),
Authorization: 'Bearer token'
}
});
}
// Continue normally
else {
request.continue();
}
});Mock Responses
await page.setRequestInterception(true);
page.on('request', request => {
if (request.url().includes('/api/user')) {
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: 1,
name: 'Mock User'
})
});
} else {
request.continue();
}
});Monitor Network
// Track requests
page.on('request', request => {
console.log('Request:', request.method(), request.url());
});
// Track responses
page.on('response', response => {
console.log('Response:', response.status(), response.url());
});
// Track failed requests
page.on('requestfailed', request => {
console.log('Failed:', request.failure().errorText, request.url());
});
// Get response body
page.on('response', async response => {
if (response.url().includes('/api/data')) {
const json = await response.json();
console.log('API Data:', json);
}
});---
Device Emulation
Predefined Devices
import { devices } from 'puppeteer';
// Emulate iPhone
const iPhone = devices['iPhone 13 Pro'];
await page.emulate(iPhone);
// Common devices
const iPad = devices['iPad Pro'];
const pixel = devices['Pixel 5'];
const galaxy = devices['Galaxy S9+'];
// Navigate after emulation
await page.goto('https://example.com');Custom Device
await page.emulate({
viewport: {
width: 375,
height: 812,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
isLandscape: false
},
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)...'
});Viewport Only
await page.setViewport({
width: 1920,
height: 1080,
deviceScaleFactor: 1
});Geolocation
// Set geolocation
await page.setGeolocation({
latitude: 37.7749,
longitude: -122.4194,
accuracy: 100
});
// Grant permissions
const context = browser.defaultBrowserContext();
await context.overridePermissions('https://example.com', ['geolocation']);Timezone & Locale
// Set timezone
await page.emulateTimezone('America/New_York');
// Set locale
await page.emulateMediaType('screen');
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'language', {
get: () => 'en-US'
});
});---
Performance
CPU & Network Throttling
// CPU throttling
const client = await page.createCDPSession();
await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });
// Network throttling
await page.emulateNetworkConditions({
offline: false,
downloadThroughput: (1.5 * 1024 * 1024) / 8, // 1.5 Mbps
uploadThroughput: (750 * 1024) / 8, // 750 Kbps
latency: 40 // 40ms RTT
});
// Predefined profiles
await page.emulateNetworkConditions(puppeteer.networkConditions['Fast 3G']);
// Disable throttling
await page.emulateNetworkConditions({
offline: false,
downloadThroughput: -1,
uploadThroughput: -1,
latency: 0
});Performance Metrics
// Get metrics
const metrics = await page.metrics();
console.log(metrics);
// {
// Timestamp, Documents, Frames, JSEventListeners,
// Nodes, LayoutCount, RecalcStyleCount,
// LayoutDuration, RecalcStyleDuration,
// ScriptDuration, TaskDuration,
// JSHeapUsedSize, JSHeapTotalSize
// }Performance Tracing
// Start tracing
await page.tracing.start({
path: 'trace.json',
categories: ['devtools.timeline', 'disabled-by-default-devtools.timeline']
});
// Navigate
await page.goto('https://example.com');
// Stop tracing
await page.tracing.stop();
// Analyze trace in chrome://tracingCoverage (Code Usage)
// Start JS coverage
await page.coverage.startJSCoverage();
// Start CSS coverage
await page.coverage.startCSSCoverage();
// Navigate
await page.goto('https://example.com');
// Stop and get coverage
const jsCoverage = await page.coverage.stopJSCoverage();
const cssCoverage = await page.coverage.stopCSSCoverage();
// Calculate unused bytes
let totalBytes = 0;
let usedBytes = 0;
for (const entry of [...jsCoverage, ...cssCoverage]) {
totalBytes += entry.text.length;
for (const range of entry.ranges) {
usedBytes += range.end - range.start - 1;
}
}
console.log(`Used: ${(usedBytes / totalBytes) * 100}%`);---
Common Patterns
Wait for Elements
// Wait for selector
await page.waitForSelector('.element', {
visible: true,
timeout: 5000
});
// Wait for XPath
await page.waitForXPath('//button[text()="Submit"]');
// Wait for function
await page.waitForFunction(() => document.querySelector('.loading') === null, { timeout: 10000 });
// Wait for timeout
await page.waitForTimeout(2000);Handle Dialogs
// Alert, confirm, prompt
page.on('dialog', async dialog => {
console.log(dialog.type(), dialog.message());
// Accept
await dialog.accept();
// or reject
// await dialog.dismiss();
// or provide input for prompt
// await dialog.accept('input text');
});Handle Downloads
// Set download path
const client = await page.createCDPSession();
await client.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: '/path/to/downloads'
});
// Trigger download
await page.click('a[download]');Multiple Pages (Tabs)
// Listen for new pages
browser.on('targetcreated', async target => {
if (target.type() === 'page') {
const newPage = await target.page();
console.log('New page opened:', newPage.url());
}
});
// Click link that opens new tab
const [newPage] = await Promise.all([
new Promise(resolve => browser.once('targetcreated', target => resolve(target.page()))),
page.click('a[target="_blank"]')
]);
console.log('New page URL:', newPage.url());Frames (iframes)
// Get all frames
const frames = page.frames();
// Find frame by name
const frame = page.frames().find(f => f.name() === 'myframe');
// Find frame by URL
const frame = page.frames().find(f => f.url().includes('example.com'));
// Main frame
const mainFrame = page.mainFrame();
// Interact with frame
await frame.click('.button');
await frame.type('#input', 'text');Infinite Scroll
async function autoScroll(page) {
await page.evaluate(async () => {
await new Promise(resolve => {
let totalHeight = 0;
const distance = 100;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
resolve();
}
}, 100);
});
});
}
await autoScroll(page);Cookies
// Get cookies
const cookies = await page.cookies();
// Set cookies
await page.setCookie({
name: 'session',
value: 'abc123',
domain: 'example.com',
path: '/',
httpOnly: true,
secure: true,
sameSite: 'Strict'
});
// Delete cookies
await page.deleteCookie({ name: 'session' });Local Storage
// Set localStorage
await page.evaluate(() => {
localStorage.setItem('key', 'value');
});
// Get localStorage
const value = await page.evaluate(() => {
return localStorage.getItem('key');
});
// Clear localStorage
await page.evaluate(() => localStorage.clear());Error Handling
try {
await page.goto('https://example.com', {
waitUntil: 'networkidle2',
timeout: 30000
});
} catch (error) {
if (error.name === 'TimeoutError') {
console.error('Page load timeout');
} else {
console.error('Navigation failed:', error);
}
// Take screenshot on error
await page.screenshot({ path: 'error.png' });
}Stealth Mode (Avoid Detection)
// Hide automation indicators
await page.evaluateOnNewDocument(() => {
// Override navigator.webdriver
Object.defineProperty(navigator, 'webdriver', {
get: () => false
});
// Mock chrome object
window.chrome = {
runtime: {}
};
// Mock permissions
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = parameters =>
parameters.name === 'notifications' ? Promise.resolve({ state: 'granted' }) : originalQuery(parameters);
});
// Set realistic user agent
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');---
Debugging Tips
Take Screenshots on Error
page.on('pageerror', async error => {
console.error('Page error:', error);
await page.screenshot({ path: `error-${Date.now()}.png` });
});Console Logging
// Forward console to Node
page.on('console', msg => {
console.log('PAGE LOG:', msg.text());
});Slow Down Execution
const browser = await puppeteer.launch({
slowMo: 250 // 250ms delay between actions
});Keep Browser Open
const browser = await puppeteer.launch({
headless: false,
devtools: true
});
// Prevent auto-close
await page.evaluate(() => debugger);---
Best Practices
1. Always close browser: Use try/finally or process cleanup 2. Wait appropriately: Use waitForSelector, not setTimeout 3. Handle errors: Wrap navigation in try/catch 4. Optimize selectors: Use specific selectors for reliability 5. Avoid race conditions: Wait for navigation after clicks 6. Reuse pages: Don't create new pages unnecessarily 7. Set timeouts: Always specify reasonable timeouts 8. Clean up: Close unused pages and contexts
---
Resources
/**
* Tests for selector parsing library
* Run with: node --test __tests__/selector.test.js
*/
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { parseSelector } from '../lib/selector.js';
describe('parseSelector', () => {
describe('CSS Selectors', () => {
it('should detect simple CSS selectors', () => {
const result = parseSelector('button');
assert.strictEqual(result.type, 'css');
assert.strictEqual(result.selector, 'button');
});
it('should detect class selectors', () => {
const result = parseSelector('.btn-submit');
assert.strictEqual(result.type, 'css');
assert.strictEqual(result.selector, '.btn-submit');
});
it('should detect ID selectors', () => {
const result = parseSelector('#email-input');
assert.strictEqual(result.type, 'css');
assert.strictEqual(result.selector, '#email-input');
});
it('should detect attribute selectors', () => {
const result = parseSelector('button[type="submit"]');
assert.strictEqual(result.type, 'css');
assert.strictEqual(result.selector, 'button[type="submit"]');
});
it('should detect complex CSS selectors', () => {
const result = parseSelector('div.container > button.btn-primary:hover');
assert.strictEqual(result.type, 'css');
});
});
describe('XPath Selectors', () => {
it('should detect absolute XPath', () => {
const result = parseSelector('/html/body/button');
assert.strictEqual(result.type, 'xpath');
assert.strictEqual(result.selector, '/html/body/button');
});
it('should detect relative XPath', () => {
const result = parseSelector('//button');
assert.strictEqual(result.type, 'xpath');
assert.strictEqual(result.selector, '//button');
});
it('should detect XPath with text matching', () => {
const result = parseSelector('//button[text()="Click Me"]');
assert.strictEqual(result.type, 'xpath');
});
it('should detect XPath with contains', () => {
const result = parseSelector('//button[contains(text(),"Submit")]');
assert.strictEqual(result.type, 'xpath');
});
it('should detect XPath with attributes', () => {
const result = parseSelector('//input[@type="email"]');
assert.strictEqual(result.type, 'xpath');
});
it('should detect grouped XPath', () => {
const result = parseSelector('(//button)[1]');
assert.strictEqual(result.type, 'xpath');
});
});
describe('Security Validation', () => {
it('should block javascript: injection', () => {
assert.throws(() => parseSelector('//button[@onclick="javascript:alert(1)"]'), /XPath injection detected.*javascript:/i);
});
it('should block <script tag injection', () => {
assert.throws(() => parseSelector('//div[contains(text(),"<script>alert(1)</script>")]'), /XPath injection detected.*<script/i);
});
it('should block onerror= injection', () => {
assert.throws(() => parseSelector('//img[@onerror="alert(1)"]'), /XPath injection detected.*onerror=/i);
});
it('should block onload= injection', () => {
assert.throws(() => parseSelector('//body[@onload="malicious()"]'), /XPath injection detected.*onload=/i);
});
it('should block onclick= injection', () => {
assert.throws(() => parseSelector('//a[@onclick="steal()"]'), /XPath injection detected.*onclick=/i);
});
it('should block eval( injection', () => {
assert.throws(() => parseSelector('//div[eval("malicious")]'), /XPath injection detected.*eval\(/i);
});
it('should block Function( injection', () => {
assert.throws(() => parseSelector('//div[Function("return 1")()]'), /XPath injection detected.*Function\(/i);
});
it('should block constructor( injection', () => {
assert.throws(() => parseSelector('//div[constructor("alert(1)")()]'), /XPath injection detected.*constructor\(/i);
});
it('should be case-insensitive for security checks', () => {
assert.throws(() => parseSelector('//div[@ONERROR="alert(1)"]'), /XPath injection detected/i);
});
it('should block extremely long selectors (DoS prevention)', () => {
const longSelector = '//' + 'a'.repeat(1001);
assert.throws(() => parseSelector(longSelector), /XPath selector too long/i);
});
});
describe('Edge Cases', () => {
it('should throw on empty string', () => {
assert.throws(() => parseSelector(''), /Selector must be a non-empty string/);
});
it('should throw on null', () => {
assert.throws(() => parseSelector(null), /Selector must be a non-empty string/);
});
it('should throw on undefined', () => {
assert.throws(() => parseSelector(undefined), /Selector must be a non-empty string/);
});
it('should throw on non-string input', () => {
assert.throws(() => parseSelector(123), /Selector must be a non-empty string/);
});
it('should handle selectors with special characters', () => {
const result = parseSelector('button[data-test="submit-form"]');
assert.strictEqual(result.type, 'css');
});
it('should allow safe XPath with parentheses', () => {
const result = parseSelector('//button[contains(text(),"Save")]');
assert.strictEqual(result.type, 'xpath');
// Should not throw
});
});
describe('Real-World Examples', () => {
it('should handle common button selector', () => {
const result = parseSelector('//button[contains(text(),"Submit")]');
assert.strictEqual(result.type, 'xpath');
});
it('should handle complex form selector', () => {
const result = parseSelector('//form[@id="login-form"]//input[@type="email"]');
assert.strictEqual(result.type, 'xpath');
});
it('should handle descendant selector', () => {
const result = parseSelector('//div[@class="modal"]//button[@class="close"]');
assert.strictEqual(result.type, 'xpath');
});
it('should handle nth-child equivalent', () => {
const result = parseSelector('(//li)[3]');
assert.strictEqual(result.type, 'xpath');
});
});
});
node_modules
.browser-session.json
.auth-session.json#!/usr/bin/env node
/**
* Get ARIA-based accessibility snapshot with stable element refs
* Usage: node aria-snapshot.js [--url https://example.com] [--output snapshot.yaml]
*
* Returns YAML-formatted accessibility tree with:
* - Semantic roles (button, link, textbox, heading, etc.)
* - Accessible names (what screen readers announce)
* - Element states (checked, disabled, expanded)
* - Stable refs [ref=eN] that persist for interaction
*
* Session behavior:
* By default, browser stays running for session persistence
* Use --close true to fully close browser
*/
import { getBrowser, getPage, closeBrowser, disconnectBrowser, navigateWithAuth, parseArgs, outputJSON, outputError } from './lib/browser.js';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* Get ARIA snapshot script to inject into page
* Builds YAML-formatted accessibility tree with element references
*/
function getAriaSnapshotScript() {
return `
(function() {
// Store refs on window for later retrieval via selectRef
window.__chromeDevToolsRefs = window.__chromeDevToolsRefs || new Map();
let refCounter = window.__chromeDevToolsRefCounter || 1;
// ARIA roles we care about for interaction
const INTERACTIVE_ROLES = new Set([
'button', 'link', 'textbox', 'checkbox', 'radio', 'combobox',
'listbox', 'option', 'menuitem', 'menuitemcheckbox', 'menuitemradio',
'tab', 'switch', 'slider', 'spinbutton', 'searchbox', 'tree', 'treeitem',
'grid', 'gridcell', 'row', 'rowheader', 'columnheader'
]);
// Landmark roles for structure
const LANDMARK_ROLES = new Set([
'banner', 'navigation', 'main', 'complementary', 'contentinfo',
'search', 'form', 'region', 'article', 'dialog', 'alertdialog'
]);
// Implicit ARIA roles from HTML elements
const IMPLICIT_ROLES = {
'A': (el) => el.href ? 'link' : null,
'BUTTON': () => 'button',
'INPUT': (el) => {
const type = el.type?.toLowerCase();
if (type === 'checkbox') return 'checkbox';
if (type === 'radio') return 'radio';
if (type === 'submit' || type === 'button' || type === 'reset') return 'button';
if (type === 'search') return 'searchbox';
if (type === 'range') return 'slider';
if (type === 'number') return 'spinbutton';
return 'textbox';
},
'TEXTAREA': () => 'textbox',
'SELECT': () => 'combobox',
'OPTION': () => 'option',
'IMG': () => 'img',
'NAV': () => 'navigation',
'MAIN': () => 'main',
'HEADER': () => 'banner',
'FOOTER': () => 'contentinfo',
'ASIDE': () => 'complementary',
'ARTICLE': () => 'article',
'SECTION': (el) => el.getAttribute('aria-label') || el.getAttribute('aria-labelledby') ? 'region' : null,
'FORM': () => 'form',
'UL': () => 'list',
'OL': () => 'list',
'LI': () => 'listitem',
'H1': () => 'heading',
'H2': () => 'heading',
'H3': () => 'heading',
'H4': () => 'heading',
'H5': () => 'heading',
'H6': () => 'heading',
'TABLE': () => 'table',
'TR': () => 'row',
'TH': () => 'columnheader',
'TD': () => 'cell',
'DIALOG': () => 'dialog'
};
function getRole(el) {
// Explicit role takes precedence
const explicitRole = el.getAttribute('role');
if (explicitRole) return explicitRole;
// Check implicit role
const implicitFn = IMPLICIT_ROLES[el.tagName];
if (implicitFn) return implicitFn(el);
return null;
}
function getAccessibleName(el) {
// aria-label takes precedence
const ariaLabel = el.getAttribute('aria-label');
if (ariaLabel) return ariaLabel.trim();
// aria-labelledby
const labelledBy = el.getAttribute('aria-labelledby');
if (labelledBy) {
const labels = labelledBy.split(' ')
.map(id => document.getElementById(id)?.textContent?.trim())
.filter(Boolean)
.join(' ');
if (labels) return labels;
}
// Input associated label
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT') {
if (el.id) {
const label = document.querySelector('label[for="' + el.id + '"]');
if (label) return label.textContent?.trim();
}
// Check parent label
const parentLabel = el.closest('label');
if (parentLabel) {
const labelText = parentLabel.textContent?.replace(el.value || '', '')?.trim();
if (labelText) return labelText;
}
}
// Button/link content
if (el.tagName === 'BUTTON' || el.tagName === 'A') {
const text = el.textContent?.trim();
if (text) return text.substring(0, 100);
}
// Alt text for images
if (el.tagName === 'IMG') {
return el.alt || null;
}
// Title attribute fallback
if (el.title) return el.title.trim();
// Placeholder for inputs
if (el.placeholder) return null; // Return null, will add as /placeholder
return null;
}
function getStateFlags(el) {
const flags = [];
// Checked state
if (el.checked || el.getAttribute('aria-checked') === 'true') {
flags.push('checked');
}
// Disabled state
if (el.disabled || el.getAttribute('aria-disabled') === 'true') {
flags.push('disabled');
}
// Expanded state
if (el.getAttribute('aria-expanded') === 'true') {
flags.push('expanded');
}
// Selected state
if (el.selected || el.getAttribute('aria-selected') === 'true') {
flags.push('selected');
}
// Pressed state
if (el.getAttribute('aria-pressed') === 'true') {
flags.push('pressed');
}
// Required state
if (el.required || el.getAttribute('aria-required') === 'true') {
flags.push('required');
}
return flags;
}
function isVisible(el) {
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden') return false;
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function isInteractiveOrLandmark(role) {
return INTERACTIVE_ROLES.has(role) || LANDMARK_ROLES.has(role);
}
function shouldInclude(el) {
if (!isVisible(el)) return false;
const role = getRole(el);
if (!role) return false;
// Include interactive, landmarks, and structural elements
return isInteractiveOrLandmark(role) ||
role === 'heading' ||
role === 'img' ||
role === 'list' ||
role === 'listitem' ||
role === 'table' ||
role === 'row' ||
role === 'cell' ||
role === 'columnheader';
}
function assignRef(el, role) {
// Only assign refs to interactive elements
if (!INTERACTIVE_ROLES.has(role)) return null;
const ref = 'e' + refCounter++;
window.__chromeDevToolsRefs.set(ref, el);
return ref;
}
function buildYaml(el, indent = 0) {
const role = getRole(el);
if (!role) return '';
const prefix = ' '.repeat(indent) + '- ';
const lines = [];
// Build the line: role "name" [flags] [ref=eN]
let line = prefix + role;
const name = getAccessibleName(el);
if (name) {
line += ' "' + name.replace(/"/g, '\\\\"') + '"';
}
// Add heading level
if (role === 'heading') {
const level = el.tagName.match(/H(\\d)/)?.[1] || el.getAttribute('aria-level');
if (level) line += ' [level=' + level + ']';
}
// Add state flags
const flags = getStateFlags(el);
flags.forEach(flag => {
line += ' [' + flag + ']';
});
// Add ref for interactive elements
const ref = assignRef(el, role);
if (ref) {
line += ' [ref=' + ref + ']';
}
lines.push(line);
// Add metadata on subsequent lines
if (el.tagName === 'A' && el.href) {
lines.push(' '.repeat(indent + 1) + '/url: ' + el.href);
}
if (el.placeholder) {
lines.push(' '.repeat(indent + 1) + '/placeholder: "' + el.placeholder + '"');
}
if (el.tagName === 'INPUT' && el.value && el.type !== 'password') {
lines.push(' '.repeat(indent + 1) + '/value: "' + el.value.substring(0, 50) + '"');
}
// Process children
const children = Array.from(el.children);
children.forEach(child => {
const childYaml = buildYaml(child, indent + 1);
if (childYaml) lines.push(childYaml);
});
return lines.join('\\n');
}
function getSnapshot() {
const lines = [];
// Start from body
const children = Array.from(document.body.children);
children.forEach(child => {
const yaml = buildYaml(child, 0);
if (yaml) lines.push(yaml);
});
// Save ref counter for next snapshot
window.__chromeDevToolsRefCounter = refCounter;
return lines.join('\\n');
}
return getSnapshot();
})();
`;
}
async function ariaSnapshot() {
const args = parseArgs(process.argv.slice(2));
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
// Navigate if URL provided
if (args.url) {
await navigateWithAuth(page, args.url, {
waitUntil: args['wait-until'] || 'networkidle2'
});
}
// Get ARIA snapshot
const snapshot = await page.evaluate(getAriaSnapshotScript());
// Build result
const result = {
success: true,
url: page.url(),
title: await page.title(),
format: 'yaml',
snapshot: snapshot
};
// Output to file or stdout
if (args.output) {
const outputPath = args.output;
// Ensure snapshots directory exists
const outputDir = path.dirname(outputPath);
await fs.mkdir(outputDir, { recursive: true });
// Write YAML snapshot
await fs.writeFile(outputPath, snapshot, 'utf8');
outputJSON({
success: true,
output: path.resolve(outputPath),
url: page.url()
});
} else {
// Output to stdout
outputJSON(result);
}
// Default: disconnect to keep browser running for session persistence
// Use --close true to fully close browser
if (args.close === 'true') {
await closeBrowser();
} else {
await disconnectBrowser();
}
} catch (error) {
outputError(error);
}
}
ariaSnapshot();
#!/usr/bin/env node
/**
* Click an element
* Usage: node click.js --selector ".button" [--url https://example.com] [--wait-for ".result"]
* Supports both CSS and XPath selectors:
* - CSS: node click.js --selector "button.submit"
* - XPath: node click.js --selector "//button[contains(text(),'Submit')]"
*/
import { getBrowser, getPage, closeBrowser, disconnectBrowser, navigateWithAuth, parseArgs, outputJSON, outputError } from './lib/browser.js';
import { parseSelector, waitForElement, clickElement, enhanceError } from './lib/selector.js';
async function click() {
const args = parseArgs(process.argv.slice(2));
if (!args.selector) {
outputError(new Error('--selector is required'));
return;
}
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
// Navigate if URL provided
if (args.url) {
await navigateWithAuth(page, args.url, {
waitUntil: args['wait-until'] || 'networkidle2'
});
}
// Parse and validate selector
const parsed = parseSelector(args.selector);
// Wait for element based on selector type
await waitForElement(page, parsed, {
visible: true,
timeout: parseInt(args.timeout || '5000')
});
// Set up navigation promise BEFORE clicking (in case click triggers immediate navigation)
const navigationPromise = page
.waitForNavigation({
waitUntil: 'load',
timeout: 5000
})
.catch(() => null); // Catch timeout - navigation may not occur
// Click element
await clickElement(page, parsed);
// Wait for optional selector after click
if (args['wait-for']) {
await page.waitForSelector(args['wait-for'], {
timeout: parseInt(args.timeout || '5000')
});
} else {
// Wait for navigation to complete (or timeout if no navigation)
await navigationPromise;
}
outputJSON({
success: true,
url: page.url(),
title: await page.title()
});
// Default: disconnect to keep browser running for session persistence
// Use --close true to fully close browser
if (args.close === 'true') {
await closeBrowser();
} else {
await disconnectBrowser();
}
} catch (error) {
// Enhance error message with troubleshooting tips
const enhanced = enhanceError(error, args.selector);
outputError(enhanced);
process.exit(1);
}
}
click();
#!/usr/bin/env node
/**
* Monitor console messages
* Usage: node console.js --url https://example.com [--types error,warn] [--duration 5000]
*/
import { getBrowser, getPage, closeBrowser, disconnectBrowser, navigateWithAuth, parseArgs, outputJSON, outputError } from './lib/browser.js';
async function monitorConsole() {
const args = parseArgs(process.argv.slice(2));
if (!args.url) {
outputError(new Error('--url is required'));
return;
}
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
const messages = [];
const filterTypes = args.types ? args.types.split(',') : null;
// Listen for console messages
page.on('console', msg => {
const type = msg.type();
if (!filterTypes || filterTypes.includes(type)) {
messages.push({
type: type,
text: msg.text(),
location: msg.location(),
timestamp: Date.now()
});
}
});
// Listen for page errors
page.on('pageerror', error => {
messages.push({
type: 'pageerror',
text: error.message,
stack: error.stack,
timestamp: Date.now()
});
});
// Navigate
await navigateWithAuth(page, args.url, {
waitUntil: args['wait-until'] || 'networkidle2'
});
// Wait for additional time if specified
if (args.duration) {
await new Promise(resolve => setTimeout(resolve, parseInt(args.duration)));
}
outputJSON({
success: true,
url: page.url(),
messageCount: messages.length,
messages: messages
});
// Default: disconnect to keep browser running for session persistence
// Use --close true to fully close browser
if (args.close === 'true') {
await closeBrowser();
} else {
await disconnectBrowser();
}
} catch (error) {
outputError(error);
}
}
monitorConsole();
#!/usr/bin/env node
/**
* Execute JavaScript in page context
* Usage: node evaluate.js --script "document.title" [--url https://example.com]
*/
import { getBrowser, getPage, closeBrowser, disconnectBrowser, navigateWithAuth, parseArgs, outputJSON, outputError } from './lib/browser.js';
async function evaluate() {
const args = parseArgs(process.argv.slice(2));
if (!args.script) {
outputError(new Error('--script is required'));
return;
}
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
// Navigate if URL provided
if (args.url) {
await navigateWithAuth(page, args.url, {
waitUntil: args['wait-until'] || 'networkidle2'
});
}
const result = await page.evaluate(script => {
// eslint-disable-next-line no-eval
return eval(script);
}, args.script);
outputJSON({
success: true,
result: result,
url: page.url()
});
// Default: disconnect to keep browser running for session persistence
// Use --close true to fully close browser
if (args.close === 'true') {
await closeBrowser();
} else {
await disconnectBrowser();
}
} catch (error) {
outputError(error);
}
}
evaluate();
#!/usr/bin/env node
/**
* Fill form fields
* Usage: node fill.js --selector "#input" --value "text" [--url https://example.com]
* Supports both CSS and XPath selectors:
* - CSS: node fill.js --selector "#email" --value "user@example.com"
* - XPath: node fill.js --selector "//input[@type='email']" --value "user@example.com"
*/
import { getBrowser, getPage, closeBrowser, disconnectBrowser, navigateWithAuth, parseArgs, outputJSON, outputError } from './lib/browser.js';
import { parseSelector, waitForElement, typeIntoElement, enhanceError } from './lib/selector.js';
async function fill() {
const args = parseArgs(process.argv.slice(2));
if (!args.selector) {
outputError(new Error('--selector is required'));
return;
}
if (!args.value) {
outputError(new Error('--value is required'));
return;
}
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
// Navigate if URL provided
if (args.url) {
await navigateWithAuth(page, args.url, {
waitUntil: args['wait-until'] || 'networkidle2'
});
}
// Parse and validate selector
const parsed = parseSelector(args.selector);
// Wait for element based on selector type
await waitForElement(page, parsed, {
visible: true,
timeout: parseInt(args.timeout || '5000')
});
// Type into element
await typeIntoElement(page, parsed, args.value, {
clear: args.clear === 'true',
delay: parseInt(args.delay || '0')
});
outputJSON({
success: true,
selector: args.selector,
value: args.value,
url: page.url()
});
// Default: disconnect to keep browser running for session persistence
// Use --close true to fully close browser
if (args.close === 'true') {
await closeBrowser();
} else {
await disconnectBrowser();
}
} catch (error) {
// Enhance error message with troubleshooting tips
const enhanced = enhanceError(error, args.selector);
outputError(enhanced);
process.exit(1);
}
}
fill();
#!/usr/bin/env node
/**
* Inject authentication cookies/tokens into browser session
* Usage: node inject-auth.js --url https://example.com --cookies '[{"name":"token","value":"xxx","domain":".example.com"}]'
* node inject-auth.js --url https://example.com --token "Bearer xxx" [--header Authorization]
* node inject-auth.js --url https://example.com --local-storage '{"key":"value"}'
* node inject-auth.js --url https://example.com --session-storage '{"key":"value"}'
*
* This script injects authentication data into browser session for testing protected routes.
* The session persists across script executions until --close true is used.
*
* Workflow for testing protected routes:
* 1. User manually logs into the site in their browser
* 2. User extracts cookies/tokens from browser DevTools
* 3. Run this script to inject auth into puppeteer session
* 4. Run other scripts (screenshot, navigate, etc.) which will use authenticated session
*
* Session behavior:
* --close false : Keep browser running (default for chaining)
* --close true : Close browser completely and clear session
*/
import { getBrowser, getPage, closeBrowser, disconnectBrowser, parseArgs, outputJSON, outputError, saveAuthSession, clearAuthSession } from './lib/browser.js';
/**
* Parse cookies from JSON string or file
* @param {string} cookiesInput - JSON string or file path
* @returns {Array} - Array of cookie objects
*/
function parseCookies(cookiesInput) {
try {
// Try parsing as JSON string
return JSON.parse(cookiesInput);
} catch {
throw new Error(`Invalid cookies format. Expected JSON array: [{"name":"cookie_name","value":"cookie_value","domain":".example.com"}]`);
}
}
/**
* Parse storage data from JSON string
* @param {string} storageInput - JSON string
* @returns {Object} - Storage key-value pairs
*/
function parseStorage(storageInput) {
try {
return JSON.parse(storageInput);
} catch {
throw new Error(`Invalid storage format. Expected JSON object: {"key":"value"}`);
}
}
async function injectAuth() {
const args = parseArgs(process.argv.slice(2));
if (!args.url) {
outputError(new Error('--url is required (base URL for the protected site)'));
return;
}
// Validate at least one auth method provided
if (!args.cookies && !args.token && !args['local-storage'] && !args['session-storage']) {
outputError(new Error('At least one auth method required: --cookies, --token, --local-storage, or --session-storage'));
return;
}
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
// Navigate to the URL first to set the domain context
await page.goto(args.url, {
waitUntil: args['wait-until'] || 'networkidle2',
timeout: parseInt(args.timeout || '30000')
});
const result = {
success: true,
url: args.url,
injected: []
};
let normalizedCookies = null;
let tokenStorageData = null;
// Inject cookies
if (args.cookies) {
const cookies = parseCookies(args.cookies);
// Validate and normalize cookies
normalizedCookies = cookies.map(cookie => {
if (!cookie.name || !cookie.value) {
throw new Error(`Cookie must have 'name' and 'value' properties`);
}
// Extract domain from URL if not provided
if (!cookie.domain) {
const urlObj = new URL(args.url);
cookie.domain = urlObj.hostname;
}
return {
name: cookie.name,
value: cookie.value,
domain: cookie.domain,
path: cookie.path || '/',
httpOnly: cookie.httpOnly !== undefined ? cookie.httpOnly : false,
secure: cookie.secure !== undefined ? cookie.secure : args.url.startsWith('https'),
sameSite: cookie.sameSite || 'Lax',
...(cookie.expires && { expires: cookie.expires })
};
});
await page.setCookie(...normalizedCookies);
result.injected.push({
type: 'cookies',
count: normalizedCookies.length,
names: normalizedCookies.map(c => c.name)
});
}
// Inject Bearer token via localStorage (common pattern)
if (args.token) {
const tokenKey = args['token-key'] || 'access_token';
const token = args.token.startsWith('Bearer ') ? args.token.slice(7) : args.token;
tokenStorageData = { [tokenKey]: token };
await page.evaluate(
(key, value) => {
localStorage.setItem(key, value);
},
tokenKey,
token
);
result.injected.push({
type: 'token',
key: tokenKey,
storage: 'localStorage'
});
// Also set Authorization header for future requests if header option provided
if (args.header) {
await page.setExtraHTTPHeaders({
[args.header]: args.token.startsWith('Bearer ') ? args.token : `Bearer ${args.token}`
});
result.injected.push({
type: 'header',
name: args.header
});
}
}
// Inject localStorage items
if (args['local-storage']) {
const storageData = parseStorage(args['local-storage']);
await page.evaluate(data => {
Object.entries(data).forEach(([key, value]) => {
localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value));
});
}, storageData);
result.injected.push({
type: 'localStorage',
keys: Object.keys(storageData)
});
}
// Inject sessionStorage items
if (args['session-storage']) {
const storageData = parseStorage(args['session-storage']);
await page.evaluate(data => {
Object.entries(data).forEach(([key, value]) => {
sessionStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value));
});
}, storageData);
result.injected.push({
type: 'sessionStorage',
keys: Object.keys(storageData)
});
}
// Reload page to apply auth (optional, use --reload true)
if (args.reload === 'true') {
await page.reload({ waitUntil: 'networkidle2' });
result.reloaded = true;
}
// Save auth session to file for persistence across script executions
const authSessionData = {};
if (normalizedCookies) {
authSessionData.cookies = normalizedCookies;
}
if (tokenStorageData) {
authSessionData.localStorage = tokenStorageData;
}
if (args['local-storage']) {
authSessionData.localStorage = {
...(authSessionData.localStorage || {}),
...parseStorage(args['local-storage'])
};
}
if (args['session-storage']) {
authSessionData.sessionStorage = parseStorage(args['session-storage']);
}
if (args.token && args.header) {
authSessionData.headers = {
[args.header]: args.token.startsWith('Bearer ') ? args.token : `Bearer ${args.token}`
};
}
// Clear existing auth if --clear flag used
if (args.clear === 'true') {
clearAuthSession();
result.cleared = true;
} else if (Object.keys(authSessionData).length > 0) {
saveAuthSession(authSessionData);
result.persisted = true;
}
// Verify auth by checking page title and URL after injection
result.finalUrl = page.url();
result.title = await page.title();
outputJSON(result);
// Default: disconnect to keep browser running for session persistence
if (args.close === 'true') {
await closeBrowser();
} else {
await disconnectBrowser();
}
} catch (error) {
outputError(error);
}
}
injectAuth();
#!/bin/bash
# System dependencies installation script for Chrome DevTools Agent Skill
# This script installs required system libraries for running Chrome/Chromium
set -e
echo "🚀 Installing system dependencies for Chrome/Chromium..."
echo ""
# Detect OS
if [ -f /etc/os-release ]; then
. /etc/os-release
OS=$ID
else
echo "❌ Cannot detect OS. This script supports Debian/Ubuntu-based systems."
exit 1
fi
# Check if running as root
if [ "$EUID" -ne 0 ]; then
SUDO="sudo"
echo "⚠️ This script requires root privileges to install system packages."
echo " You may be prompted for your password."
echo ""
else
SUDO=""
fi
# Install dependencies based on OS
case $OS in
ubuntu|debian|pop)
echo "Detected: $PRETTY_NAME"
echo "Installing dependencies with apt..."
echo ""
$SUDO apt-get update
# Install Chrome dependencies
$SUDO apt-get install -y \
ca-certificates \
fonts-liberation \
libasound2t64 \
libatk-bridge2.0-0 \
libatk1.0-0 \
libc6 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgbm1 \
libgcc1 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libstdc++6 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxss1 \
libxtst6 \
lsb-release \
wget \
xdg-utils
echo ""
echo "✅ System dependencies installed successfully!"
;;
fedora|rhel|centos)
echo "Detected: $PRETTY_NAME"
echo "Installing dependencies with dnf/yum..."
echo ""
# Try dnf first, fallback to yum
if command -v dnf &> /dev/null; then
PKG_MGR="dnf"
else
PKG_MGR="yum"
fi
$SUDO $PKG_MGR install -y \
alsa-lib \
atk \
at-spi2-atk \
cairo \
cups-libs \
dbus-libs \
expat \
fontconfig \
glib2 \
gtk3 \
libdrm \
libgbm \
libX11 \
libxcb \
libXcomposite \
libXcursor \
libXdamage \
libXext \
libXfixes \
libXi \
libxkbcommon \
libXrandr \
libXrender \
libXScrnSaver \
libXtst \
mesa-libgbm \
nspr \
nss \
pango
echo ""
echo "✅ System dependencies installed successfully!"
;;
arch|manjaro)
echo "Detected: $PRETTY_NAME"
echo "Installing dependencies with pacman..."
echo ""
$SUDO pacman -Sy --noconfirm \
alsa-lib \
at-spi2-core \
cairo \
cups \
dbus \
expat \
glib2 \
gtk3 \
libdrm \
libx11 \
libxcb \
libxcomposite \
libxcursor \
libxdamage \
libxext \
libxfixes \
libxi \
libxkbcommon \
libxrandr \
libxrender \
libxshmfence \
libxss \
libxtst \
mesa \
nspr \
nss \
pango
echo ""
echo "✅ System dependencies installed successfully!"
;;
*)
echo "❌ Unsupported OS: $OS"
echo " This script supports: Ubuntu, Debian, Fedora, RHEL, CentOS, Arch, Manjaro"
echo ""
echo " Please install Chrome/Chromium dependencies manually for your OS."
echo " See: https://pptr.dev/troubleshooting"
exit 1
;;
esac
echo ""
echo "📝 Next steps:"
echo " 1. Run: cd $(dirname "$0")"
echo " 2. Run: npm install"
echo " 3. Test: node navigate.js --url https://example.com"
echo ""
#!/bin/bash
# Installation script for Chrome DevTools Agent Skill
set -e
echo "🚀 Installing Chrome DevTools Agent Skill..."
echo ""
# Check Node.js version
echo "Checking Node.js version..."
NODE_VERSION=$(node --version | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -lt 18 ]; then
echo "❌ Error: Node.js 18+ is required. Current version: $(node --version)"
echo " Please upgrade Node.js: https://nodejs.org/"
exit 1
fi
echo "✓ Node.js version: $(node --version)"
echo ""
# Check for system dependencies (Linux only)
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
echo "Checking system dependencies (Linux)..."
# Check for critical Chrome dependencies
MISSING_DEPS=()
if ! ldconfig -p | grep -q libnss3.so; then
MISSING_DEPS+=("libnss3")
fi
if ! ldconfig -p | grep -q libnspr4.so; then
MISSING_DEPS+=("libnspr4")
fi
if ! ldconfig -p | grep -q libgbm.so; then
MISSING_DEPS+=("libgbm1")
fi
if [ ${#MISSING_DEPS[@]} -gt 0 ]; then
echo "⚠️ Missing system dependencies: ${MISSING_DEPS[*]}"
echo ""
echo " Chrome/Chromium requires system libraries to run."
echo " Install them with:"
echo ""
echo " ./install-deps.sh"
echo ""
echo " Or manually:"
echo " sudo apt-get install -y libnss3 libnspr4 libgbm1 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2"
echo ""
read -p " Continue anyway? (y/N) " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Installation cancelled."
exit 1
fi
else
echo "✓ System dependencies found"
fi
echo ""
elif [[ "$OSTYPE" == "darwin"* ]]; then
echo "Platform: macOS (no system dependencies needed)"
echo ""
elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
echo "Platform: Windows (no system dependencies needed)"
echo ""
fi
# Install Node.js dependencies
echo "Installing Node.js dependencies..."
npm install
echo ""
echo "✅ Installation complete!"
echo ""
echo "Test the installation:"
echo " node navigate.js --url https://example.com"
echo ""
echo "For more information:"
echo " cat README.md"
echo ""
#!/usr/bin/env node
/**
* Navigate to a URL
* Usage: node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000]
*
* Session behavior:
* --close false : Keep browser running, disconnect from it (default for chaining)
* --close true : Close browser completely and clear session
*/
import { getBrowser, getPage, closeBrowser, disconnectBrowser, navigateWithAuth, parseArgs, outputJSON, outputError } from './lib/browser.js';
async function navigate() {
const args = parseArgs(process.argv.slice(2));
if (!args.url) {
outputError(new Error('--url is required'));
return;
}
try {
const browser = await getBrowser({
headless: args.headless !== 'false'
});
const page = await getPage(browser);
const options = {
waitUntil: args['wait-until'] || 'networkidle2',
timeout: parseInt(args.timeout || '30000')
};
await navigateWithAuth(page, args.url, options);
const result = {
success: true,
url: page.url(),
title: await page.title()
};
outputJSON(result);
// Default: disconnect to keep browser running for session persistence
// Use --close true to fully close browser
if (args.close === 'true') {
await closeBrowser();
} else {
await disconnectBrowser();
}
} catch (error) {
outputError(error);
}
}
navigate();
{
"name": "chrome-devtools-scripts",
"version": "1.1.0",
"description": "Browser automation scripts for Chrome DevTools Agent Skill",
"type": "module",
"scripts": {},
"dependencies": {
"debug": "^4.4.0",
"puppeteer": "^24.15.0",
"sharp": "^0.33.5",
"yargs": "^17.7.2"
},
"engines": {
"node": ">=18.0.0"
}
}