
Chrome Devtools
- 703 installs
- 2.2k repo stars
- Updated April 3, 2026
- mrgoonie/claudekit-skills
Chrome DevTools is a browser automation agent skill that runs Puppeteer CLI scripts for screenshots, network monitoring, performance analysis, and form automation for developers who need reliable headless browser testing
About
Chrome DevTools is a claudekit-skills agent integration (Apache-2.0) providing browser automation through executable Puppeteer CLI scripts that output JSON for agent parsing. It supports screenshots, performance analysis, network traffic monitoring, web scraping, form automation, and JavaScript debugging. Installation may require Linux/WSL system libraries for headless Chrome. Developers reach for Chrome DevTools when agents must verify frontend behavior, capture page evidence, or profile load performance without manual browser sessions—checking working directory before script execution as documented in the skill quick start.
- Browser automation via executable Puppeteer scripts that output structured JSON
- Supports screenshots, performance analysis, network traffic monitoring, web scraping, form automation, and JavaScript de
- Includes one-click Linux/WSL dependency installer for Chrome libraries
- Optional ImageMagick integration automatically compresses screenshots under 5MB
- All scripts designed for easy agent parsing and chaining into larger workflows
Chrome Devtools by the numbers
- 703 all-time installs (skills.sh)
- +10 installs in the week ending Jul 26, 2026 (Skillselion tracking)
- Ranked #352 of 2,719 Automation & Workflows skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mrgoonie/claudekit-skills --skill chrome-devtoolsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 703 |
|---|---|
| repo stars | ★ 2.2k |
| Security audit | 1 / 3 scanners passed |
| Last updated | April 3, 2026 |
| Repository | mrgoonie/claudekit-skills ↗ |
How do you automate browser testing from coding agents?
Add reliable browser automation, screenshot capture, network monitoring, and performance analysis directly from Claude or Cursor agents.
Who is it for?
Frontend developers who need agent-driven Puppeteer browser automation for screenshots, network debugging, scraping, and performance analysis.
Skip if: Developers who only need static code review without browser execution or environments where headless Chrome cannot be installed.
When should I use this skill?
The user asks for browser automation, screenshots, network traffic analysis, web scraping, form filling, or frontend performance profiling.
What you get
JSON-formatted screenshots, network request logs, performance traces, scraped page data, and form automation results.
- screenshots
- network logs
- performance traces
Files
Chrome DevTools Agent Skill
Browser automation via executable Puppeteer scripts. All scripts output JSON for easy parsing.
Quick Start
CRITICAL: Always check pwd before running scripts.
Installation
Step 1: Install System Dependencies (Linux/WSL only)
On Linux/WSL, Chrome requires system libraries. Install them first:
pwd # Should show current working directory
cd .claude/skills/chrome-devtools/scripts
./install-deps.sh # Auto-detects OS and installs required libsSupports: Ubuntu, Debian, Fedora, RHEL, CentOS, Arch, Manjaro
macOS/Windows: Skip this step (dependencies bundled with Chrome)
Step 2: Install Node Dependencies
npm install # Installs puppeteer, debug, yargsStep 3: Install ImageMagick (Optional, Recommended)
ImageMagick enables automatic screenshot compression to keep files under 5MB:
macOS:
brew install imagemagickUbuntu/Debian/WSL:
sudo apt-get install imagemagickVerify:
magick -version # or: convert -versionWithout ImageMagick, screenshots >5MB will not be compressed (may fail to load in Gemini/Claude).
Test
node navigate.js --url https://example.com
# Output: {"success": true, "url": "https://example.com", "title": "Example Domain"}Available Scripts
All scripts are in .claude/skills/chrome-devtools/scripts/
CRITICAL: Always check pwd before running scripts.
Script Usage
./scripts/README.md
Core Automation
navigate.js- Navigate to URLsscreenshot.js- Capture screenshots (full page or element)click.js- Click elementsfill.js- Fill form fieldsevaluate.js- Execute JavaScript in page context
Analysis & Monitoring
snapshot.js- Extract interactive elements with metadataconsole.js- Monitor console messages/errorsnetwork.js- Track HTTP requests/responsesperformance.js- Measure Core Web Vitals + record traces
Usage Patterns
Single Command
pwd # Should show current working directory
cd .claude/skills/chrome-devtools/scripts
node screenshot.js --url https://example.com --output ./docs/screenshots/page.pngImportant: Always save screenshots to ./docs/screenshots directory.
Automatic Image Compression
Screenshots are automatically compressed if they exceed 5MB to ensure compatibility with Gemini API and Claude Code (which have 5MB limits). This uses ImageMagick internally:
# Default: auto-compress if >5MB
node screenshot.js --url https://example.com --output page.png
# Custom size threshold (e.g., 3MB)
node screenshot.js --url https://example.com --output page.png --max-size 3
# Disable compression
node screenshot.js --url https://example.com --output page.png --no-compressCompression behavior:
- PNG: Resizes to 90% + quality 85 (or 75% + quality 70 if still too large)
- JPEG: Quality 80 + progressive encoding (or quality 60 if still too large)
- Other formats: Converted to JPEG with compression
- Requires ImageMagick installed (see imagemagick skill)
Output includes compression info:
{
"success": true,
"output": "/path/to/page.png",
"compressed": true,
"originalSize": 8388608,
"size": 3145728,
"compressionRatio": "62.50%",
"url": "https://example.com"
}Chain Commands (reuse browser)
# Keep browser open with --close false
node navigate.js --url https://example.com/login --close false
node fill.js --selector "#email" --value "user@example.com" --close false
node fill.js --selector "#password" --value "secret" --close false
node click.js --selector "button[type=submit]"Parse JSON Output
# Extract specific fields with jq
node performance.js --url https://example.com | jq '.vitals.LCP'
# Save to file
node network.js --url https://example.com --output /tmp/requests.jsonExecution Protocol
Working Directory Verification
BEFORE executing any script: 1. Check current working directory with pwd 2. Verify in .claude/skills/chrome-devtools/scripts/ directory 3. If wrong directory, cd to correct location 4. Use absolute paths for all output files
Example:
pwd # Should show: .../chrome-devtools/scripts
# If wrong:
cd .claude/skills/chrome-devtools/scriptsOutput Validation
AFTER screenshot/capture operations: 1. Verify file created with ls -lh <output-path> 2. Read screenshot using Read tool to confirm content 3. Check JSON output for success:true 4. Report file size and compression status
Example:
node screenshot.js --url https://example.com --output ./docs/screenshots/page.png
ls -lh ./docs/screenshots/page.png # Verify file exists
# Then use Read tool to visually inspect5. Restart working directory to the project root.
Error Recovery
If script fails: 1. Check error message for selector issues 2. Use snapshot.js to discover correct selectors 3. Try XPath selector if CSS selector fails 4. Verify element is visible and interactive
Example:
# CSS selector fails
node click.js --url https://example.com --selector ".btn-submit"
# Error: waiting for selector ".btn-submit" failed
# Discover correct selector
node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="BUTTON")'
# Try XPath
node click.js --url https://example.com --selector "//button[contains(text(),'Submit')]"Common Mistakes
❌ Wrong working directory → output files go to wrong location ❌ Skipping output validation → silent failures ❌ Using complex CSS selectors without testing → selector errors ❌ Not checking element visibility → timeout errors
✅ Always verify pwd before running scripts ✅ Always validate output after screenshots ✅ Use snapshot.js to discover selectors ✅ Test selectors with simple commands first
Common Workflows
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'Performance Testing
PERF=$(node performance.js --url https://example.com)
LCP=$(echo $PERF | jq '.vitals.LCP')
if (( $(echo "$LCP < 2500" | bc -l) )); then
echo "✓ LCP passed: ${LCP}ms"
else
echo "✗ LCP failed: ${LCP}ms"
fiForm Automation
node fill.js --url https://example.com --selector "#search" --value "query" --close false
node click.js --selector "button[type=submit]"Error Monitoring
node console.js --url https://example.com --types error,warn --duration 5000 | jq '.messageCount'Script Options
All scripts support:
--headless false- Show browser window--close false- Keep browser open for chaining--timeout 30000- Set timeout (milliseconds)--wait-until networkidle2- Wait strategy
See ./scripts/README.md for complete options.
Output Format
All scripts output JSON to stdout:
{
"success": true,
"url": "https://example.com",
... // script-specific data
}Errors go to stderr:
{
"success": false,
"error": "Error message"
}Finding Elements
Use snapshot.js to discover selectors:
node snapshot.js --url https://example.com | jq '.elements[] | {tagName, text, selector}'Troubleshooting
Common Errors
"Cannot find package 'puppeteer'"
- Run:
npm installin the scripts directory
"error while loading shared libraries: libnss3.so" (Linux/WSL)
- Missing system dependencies
- Fix: Run
./install-deps.shin scripts directory - Manual install:
sudo apt-get install -y libnss3 libnspr4 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1
"Failed to launch the browser process"
- Check system dependencies installed (Linux/WSL)
- Verify Chrome downloaded:
ls ~/.cache/puppeteer - Try:
npm rebuildthennpm install
Chrome not found
- Puppeteer auto-downloads Chrome during
npm install - If failed, manually trigger:
npx puppeteer browsers install chrome
Script Issues
Element not found
- Get snapshot first to find correct selector:
node snapshot.js --url <url>
Script hangs
- Increase timeout:
--timeout 60000 - Change wait strategy:
--wait-until loador--wait-until domcontentloaded
Blank screenshot
- Wait for page load:
--wait-until networkidle2 - Increase timeout:
--timeout 30000
Permission denied on scripts
- Make executable:
chmod +x *.sh
Screenshot too large (>5MB)
- Install ImageMagick for automatic compression
- Manually set lower threshold:
--max-size 3 - Use JPEG format instead of PNG:
--format jpeg --quality 80 - Capture specific element instead of full page:
--selector .main-content
Compression not working
- Verify ImageMagick installed:
magick -versionorconvert -version - Check file was actually compressed in output JSON:
"compressed": true - For very large pages, use
--selectorto capture only needed area
Reference Documentation
Detailed guides available in ./references/:
- CDP Domains Reference - 47 Chrome DevTools Protocol domains
- Puppeteer Quick Reference - Complete Puppeteer API patterns
- Performance Analysis Guide - Core Web Vitals optimization
Advanced Usage
Custom Scripts
Create custom scripts using shared library:
import { getBrowser, getPage, closeBrowser, outputJSON } from './lib/browser.js';
// Your automation logicDirect CDP Access
const client = await page.createCDPSession();
await client.send('Emulation.setCPUThrottlingRate', { rate: 4 });See reference documentation for advanced patterns and complete API coverage.
External Resources
- Puppeteer Documentation
- Chrome DevTools Protocol
- Scripts README
.browser-endpointChrome 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#!/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, 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 page.goto(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()
});
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
// Enhance error message with troubleshooting tips
const enhanced = enhanceError(error, args.selector);
outputError(enhanced);
process.exit(1);
}
}
click();
#!/usr/bin/env node
/**
* Close the persistent Chrome browser
*/
import puppeteer from 'puppeteer';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ENDPOINT_FILE = path.join(__dirname, '.browser-endpoint');
async function main() {
if (!fs.existsSync(ENDPOINT_FILE)) {
console.log('No persistent browser found.');
process.exit(0);
}
const wsEndpoint = fs.readFileSync(ENDPOINT_FILE, 'utf8');
console.log('Connecting to browser...');
try {
const browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint });
await browser.close();
fs.unlinkSync(ENDPOINT_FILE);
console.log('✓ Browser closed successfully.');
} catch (error) {
console.error('Error closing browser:', error.message);
// Clean up stale endpoint file
if (fs.existsSync(ENDPOINT_FILE)) {
fs.unlinkSync(ENDPOINT_FILE);
}
}
}
main();
#!/usr/bin/env node
/**
* Monitor console messages
* Usage: node console.js --url https://example.com [--types error,warn] [--duration 5000]
*/
import { getBrowser, getPage, closeBrowser, 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 page.goto(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
});
if (args.close !== 'false') {
await closeBrowser();
}
} 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, 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 page.goto(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()
});
if (args.close !== 'false') {
await closeBrowser();
}
} 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, 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 page.goto(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()
});
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
// Enhance error message with troubleshooting tips
const enhanced = enhanceError(error, args.selector);
outputError(enhanced);
process.exit(1);
}
}
fill();
#!/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
/**
* Launch a persistent Chrome browser that can be reused across multiple commands
* Saves the WebSocket endpoint to a file for other scripts to connect to
*/
import puppeteer from 'puppeteer';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ENDPOINT_FILE = path.join(__dirname, '.browser-endpoint');
async function main() {
// Parse command line arguments
const args = process.argv.slice(2);
const headless = !args.includes('--headless=false') && !args.includes('--no-headless');
const url = args.find(arg => arg.startsWith('--url='))?.split('=')[1] || 'about:blank';
console.log('Launching persistent Chrome browser...');
const browser = await puppeteer.launch({
headless,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--remote-debugging-port=9222'
],
defaultViewport: {
width: 1920,
height: 1080
}
});
const wsEndpoint = browser.wsEndpoint();
// Save endpoint to file
fs.writeFileSync(ENDPOINT_FILE, wsEndpoint);
console.log(`Browser launched. WebSocket endpoint saved to: ${ENDPOINT_FILE}`);
console.log(`WebSocket: ${wsEndpoint}`);
// Navigate to initial URL if provided
if (url !== 'about:blank') {
const page = (await browser.pages())[0];
console.log(`Navigating to: ${url}`);
await page.goto(url, { waitUntil: 'networkidle2' });
}
console.log('\n✓ Browser is ready for commands!');
console.log(' Use other scripts normally - they will connect to this browser.');
console.log(' Run "node close-persistent.js" or press Ctrl+C to close.\n');
// Keep process alive
process.on('SIGINT', async () => {
console.log('\nClosing browser...');
await browser.close();
if (fs.existsSync(ENDPOINT_FILE)) {
fs.unlinkSync(ENDPOINT_FILE);
}
process.exit(0);
});
// Keep alive indefinitely
await new Promise(() => {});
}
main().catch(error => {
console.error('Error launching browser:', error);
process.exit(1);
});
/**
* Shared browser utilities for Chrome DevTools scripts
*/
import puppeteer from 'puppeteer';
import debug from 'debug';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ENDPOINT_FILE = path.join(__dirname, '..', '.browser-endpoint');
const log = debug('chrome-devtools:browser');
let browserInstance = null;
let pageInstance = null;
/**
* Launch or connect to browser
*/
export async function getBrowser(options = {}) {
if (browserInstance && browserInstance.isConnected()) {
log('Reusing existing browser instance');
return browserInstance;
}
// Check for persistent browser endpoint
if (!options.browserUrl && !options.wsEndpoint && fs.existsSync(ENDPOINT_FILE)) {
try {
const wsEndpoint = fs.readFileSync(ENDPOINT_FILE, 'utf8').trim();
log('Found persistent browser endpoint, connecting...');
browserInstance = await puppeteer.connect({ browserWSEndpoint: wsEndpoint });
return browserInstance;
} catch (error) {
log('Failed to connect to persistent browser, launching new one:', error.message);
// Clean up stale endpoint file
if (fs.existsSync(ENDPOINT_FILE)) {
fs.unlinkSync(ENDPOINT_FILE);
}
}
}
const launchOptions = {
headless: options.headless !== false,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
...(options.args || [])
],
defaultViewport: options.viewport || {
width: 1920,
height: 1080
},
...options
};
if (options.browserUrl || options.wsEndpoint) {
log('Connecting to existing browser');
browserInstance = await puppeteer.connect({
browserURL: options.browserUrl,
browserWSEndpoint: options.wsEndpoint
});
} else {
log('Launching new browser');
browserInstance = await puppeteer.launch(launchOptions);
}
return browserInstance;
}
/**
* Get current page or create new one
*/
export async function getPage(browser) {
if (pageInstance && !pageInstance.isClosed()) {
log('Reusing existing page');
return pageInstance;
}
const pages = await browser.pages();
if (pages.length > 0) {
pageInstance = pages[0];
} else {
pageInstance = await browser.newPage();
}
return pageInstance;
}
/**
* Close browser
*/
export async function closeBrowser() {
if (browserInstance) {
await browserInstance.close();
browserInstance = null;
pageInstance = null;
}
}
/**
* Parse command line arguments
*/
export function parseArgs(argv, options = {}) {
const args = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg.startsWith('--')) {
const key = arg.slice(2);
const nextArg = argv[i + 1];
if (nextArg && !nextArg.startsWith('--')) {
args[key] = nextArg;
i++;
} else {
args[key] = true;
}
}
}
return args;
}
/**
* Output JSON result
*/
export function outputJSON(data) {
console.log(JSON.stringify(data, null, 2));
}
/**
* Output error
*/
export function outputError(error) {
console.error(JSON.stringify({
success: false,
error: error.message,
stack: error.stack
}, null, 2));
process.exit(1);
}
/**
* Shared selector parsing and validation library
* Supports CSS and XPath selectors with security validation
*/
// Allowlist of safe XPath axes (standard DOM traversal)
const ALLOWED_XPATH_AXES = [
'ancestor', 'ancestor-or-self', 'attribute', 'child', 'descendant',
'descendant-or-self', 'following', 'following-sibling', 'namespace',
'parent', 'preceding', 'preceding-sibling', 'self'
];
// Allowlist of safe XPath functions (node selection and string ops)
const ALLOWED_XPATH_FUNCTIONS = [
'text', 'contains', 'starts-with', 'normalize-space', 'string-length',
'concat', 'substring', 'substring-before', 'substring-after', 'translate',
'not', 'true', 'false', 'boolean', 'string', 'number', 'sum', 'floor',
'ceiling', 'round', 'count', 'name', 'local-name', 'namespace-uri',
'last', 'position', 'id', 'lang', 'comment', 'processing-instruction', 'node'
];
/**
* Parse and validate selector
* @param {string} selector - CSS or XPath selector
* @returns {{type: 'css'|'xpath', selector: string}}
* @throws {Error} If selector fails validation
*/
export function parseSelector(selector) {
if (!selector || typeof selector !== 'string') {
throw new Error('Selector must be a non-empty string');
}
// Detect XPath selectors
if (selector.startsWith('/') || selector.startsWith('(//')) {
validateXPath(selector);
return { type: 'xpath', selector };
}
// CSS selector
validateCSS(selector);
return { type: 'css', selector };
}
/**
* Validate XPath selector using allowlist approach
* @param {string} xpath - XPath expression to validate
* @throws {Error} If XPath fails validation
*/
function validateXPath(xpath) {
// Length limit to prevent DoS
if (xpath.length > 1000) {
throw new Error('XPath selector too long (max 1000 characters)');
}
// Complexity limits
const predicateCount = (xpath.match(/\[/g) || []).length;
if (predicateCount > 10) {
throw new Error('XPath too complex: max 10 predicates allowed');
}
const nestingDepth = Math.max(...xpath.split('').reduce((depths, char, i) => {
if (char === '[') depths.push((depths[depths.length - 1] || 0) + 1);
else if (char === ']') depths.push(Math.max(0, (depths[depths.length - 1] || 0) - 1));
else if (depths.length) depths.push(depths[depths.length - 1]);
else depths.push(0);
return depths;
}, []));
if (nestingDepth > 5) {
throw new Error('XPath too deeply nested: max 5 levels allowed');
}
// Extract and validate function calls using allowlist
const functionPattern = /([a-z][a-z0-9-]*)\s*\(/gi;
let match;
while ((match = functionPattern.exec(xpath)) !== null) {
const funcName = match[1].toLowerCase();
if (!ALLOWED_XPATH_FUNCTIONS.includes(funcName)) {
throw new Error(`XPath function not allowed: ${funcName}. Allowed: ${ALLOWED_XPATH_FUNCTIONS.join(', ')}`);
}
}
// Validate axes using allowlist
const axisPattern = /([a-z][a-z-]*)::/gi;
while ((match = axisPattern.exec(xpath)) !== null) {
const axisName = match[1].toLowerCase();
if (!ALLOWED_XPATH_AXES.includes(axisName)) {
throw new Error(`XPath axis not allowed: ${axisName}. Allowed: ${ALLOWED_XPATH_AXES.join(', ')}`);
}
}
// Block obvious non-XPath content (URLs, scripts, HTML)
if (/^https?:\/\//i.test(xpath)) {
throw new Error('XPath cannot be a URL');
}
if (/<[a-z]/i.test(xpath)) {
throw new Error('XPath cannot contain HTML tags');
}
}
/**
* Validate CSS selector for security
* @param {string} css - CSS selector to validate
* @throws {Error} If CSS selector fails validation
*/
function validateCSS(css) {
// Length limit to prevent DoS
if (css.length > 500) {
throw new Error('CSS selector too long (max 500 characters)');
}
// Complexity limits
const selectorParts = css.split(/\s*,\s*/);
if (selectorParts.length > 10) {
throw new Error('CSS selector too complex: max 10 comma-separated selectors');
}
// Check nesting depth (combinators indicate depth)
const maxDepth = Math.max(...selectorParts.map(part =>
(part.match(/[\s>+~]/g) || []).length
));
if (maxDepth > 10) {
throw new Error('CSS selector too deeply nested: max 10 levels');
}
// Block non-CSS content
if (/^https?:\/\//i.test(css)) {
throw new Error('CSS selector cannot be a URL');
}
if (/<[a-z]/i.test(css)) {
throw new Error('CSS selector cannot contain HTML tags');
}
// Block url() which could be used for data exfiltration in some contexts
if (/url\s*\(/i.test(css)) {
throw new Error('CSS selector cannot contain url()');
}
}
/**
* Wait for element based on selector type
* @param {Object} page - Puppeteer page instance
* @param {{type: string, selector: string}} parsed - Parsed selector
* @param {Object} options - Wait options (visible, timeout)
* @returns {Promise<void>}
*/
export async function waitForElement(page, parsed, options = {}) {
const defaultOptions = {
visible: true,
timeout: 5000,
...options
};
if (parsed.type === 'xpath') {
// Use locator API for XPath (Puppeteer v24+)
const locator = page.locator(`::-p-xpath(${parsed.selector})`);
// setVisibility and setTimeout are the locator options
await locator
.setVisibility(defaultOptions.visible ? 'visible' : null)
.setTimeout(defaultOptions.timeout)
.wait();
} else {
await page.waitForSelector(parsed.selector, defaultOptions);
}
}
/**
* Click element based on selector type
* @param {Object} page - Puppeteer page instance
* @param {{type: string, selector: string}} parsed - Parsed selector
* @returns {Promise<void>}
*/
export async function clickElement(page, parsed) {
if (parsed.type === 'xpath') {
// Use locator API for XPath (Puppeteer v24+)
const locator = page.locator(`::-p-xpath(${parsed.selector})`);
await locator.click();
} else {
await page.click(parsed.selector);
}
}
/**
* Type into element based on selector type
* @param {Object} page - Puppeteer page instance
* @param {{type: string, selector: string}} parsed - Parsed selector
* @param {string} value - Text to type
* @param {Object} options - Type options (delay, clear)
* @returns {Promise<void>}
*/
export async function typeIntoElement(page, parsed, value, options = {}) {
if (parsed.type === 'xpath') {
// Use locator API for XPath (Puppeteer v24+)
const locator = page.locator(`::-p-xpath(${parsed.selector})`);
// Clear if requested
if (options.clear) {
await locator.fill('');
}
await locator.fill(value);
} else {
// CSS selector
if (options.clear) {
await page.$eval(parsed.selector, el => el.value = '');
}
await page.type(parsed.selector, value, { delay: options.delay || 0 });
}
}
/**
* Get element handle based on selector type
* @param {Object} page - Puppeteer page instance
* @param {{type: string, selector: string}} parsed - Parsed selector
* @returns {Promise<ElementHandle|null>}
*/
export async function getElement(page, parsed) {
if (parsed.type === 'xpath') {
// For XPath, use page.evaluate with XPath evaluation
// This returns the first matching element
const element = await page.evaluateHandle((xpath) => {
const result = document.evaluate(
xpath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
);
return result.singleNodeValue;
}, parsed.selector);
// Convert JSHandle to ElementHandle
const elementHandle = element.asElement();
return elementHandle;
} else {
return await page.$(parsed.selector);
}
}
/**
* Get enhanced error message for selector failures
* @param {Error} error - Original error
* @param {string} selector - Selector that failed
* @returns {Error} Enhanced error with troubleshooting tips
*/
export function enhanceError(error, selector) {
if (error.message.includes('waiting for selector') ||
error.message.includes('waiting for XPath') ||
error.message.includes('No node found')) {
error.message += '\n\nTroubleshooting:\n' +
'1. Use snapshot.js to find correct selector: node snapshot.js --url <url>\n' +
'2. Try XPath selector: //button[text()="Click"] or //button[contains(text(),"Click")]\n' +
'3. Check element is visible on page (not display:none or hidden)\n' +
'4. Increase --timeout value: --timeout 10000\n' +
'5. Change wait strategy: --wait-until load or --wait-until domcontentloaded';
}
return error;
}
#!/usr/bin/env node
/**
* Navigate to a URL
* Usage: node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000]
*/
import { getBrowser, getPage, closeBrowser, 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 page.goto(args.url, options);
const result = {
success: true,
url: page.url(),
title: await page.title()
};
outputJSON(result);
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
outputError(error);
}
}
navigate();
#!/usr/bin/env node
/**
* Monitor network requests
* Usage: node network.js --url https://example.com [--types xhr,fetch] [--output requests.json]
*/
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
import fs from 'fs/promises';
async function monitorNetwork() {
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 requests = [];
const filterTypes = args.types ? args.types.split(',').map(t => t.toLowerCase()) : null;
// Monitor requests
page.on('request', (request) => {
const resourceType = request.resourceType().toLowerCase();
if (!filterTypes || filterTypes.includes(resourceType)) {
requests.push({
id: request._requestId || requests.length,
url: request.url(),
method: request.method(),
resourceType: resourceType,
headers: request.headers(),
postData: request.postData(),
timestamp: Date.now()
});
}
});
// Monitor responses
const responses = new Map();
page.on('response', async (response) => {
const request = response.request();
const resourceType = request.resourceType().toLowerCase();
if (!filterTypes || filterTypes.includes(resourceType)) {
try {
responses.set(request._requestId || request.url(), {
status: response.status(),
statusText: response.statusText(),
headers: response.headers(),
fromCache: response.fromCache(),
timing: response.timing()
});
} catch (e) {
// Ignore errors for some response types
}
}
});
// Navigate
await page.goto(args.url, {
waitUntil: args['wait-until'] || 'networkidle2'
});
// Merge requests with responses
const combined = requests.map(req => ({
...req,
response: responses.get(req.id) || responses.get(req.url) || null
}));
const result = {
success: true,
url: page.url(),
requestCount: combined.length,
requests: combined
};
if (args.output) {
await fs.writeFile(args.output, JSON.stringify(result, null, 2));
outputJSON({
success: true,
output: args.output,
requestCount: combined.length
});
} else {
outputJSON(result);
}
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
outputError(error);
}
}
monitorNetwork();
{
"name": "chrome-devtools-scripts",
"version": "1.0.0",
"description": "Browser automation scripts for Chrome DevTools Agent Skill",
"type": "module",
"scripts": {},
"dependencies": {
"puppeteer": "^24.15.0",
"debug": "^4.4.0",
"yargs": "^17.7.2"
},
"engines": {
"node": ">=18.0.0"
}
}
#!/usr/bin/env node
/**
* Measure performance metrics and record trace
* Usage: node performance.js --url https://example.com [--trace trace.json] [--metrics]
*/
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
import fs from 'fs/promises';
async function measurePerformance() {
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);
// Start tracing if requested
if (args.trace) {
await page.tracing.start({
path: args.trace,
categories: [
'devtools.timeline',
'disabled-by-default-devtools.timeline',
'disabled-by-default-devtools.timeline.frame'
]
});
}
// Navigate
await page.goto(args.url, {
waitUntil: 'networkidle2'
});
// Stop tracing
if (args.trace) {
await page.tracing.stop();
}
// Get performance metrics
const metrics = await page.metrics();
// Get Core Web Vitals
const vitals = await page.evaluate(() => {
return new Promise((resolve) => {
const vitals = {
LCP: null,
FID: null,
CLS: 0,
FCP: null,
TTFB: null
};
// LCP
try {
new PerformanceObserver((list) => {
const entries = list.getEntries();
if (entries.length > 0) {
const lastEntry = entries[entries.length - 1];
vitals.LCP = lastEntry.renderTime || lastEntry.loadTime;
}
}).observe({ entryTypes: ['largest-contentful-paint'], buffered: true });
} catch (e) {}
// CLS
try {
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (!entry.hadRecentInput) {
vitals.CLS += entry.value;
}
});
}).observe({ entryTypes: ['layout-shift'], buffered: true });
} catch (e) {}
// FCP
try {
const paintEntries = performance.getEntriesByType('paint');
const fcpEntry = paintEntries.find(e => e.name === 'first-contentful-paint');
if (fcpEntry) {
vitals.FCP = fcpEntry.startTime;
}
} catch (e) {}
// TTFB
try {
const [navigationEntry] = performance.getEntriesByType('navigation');
if (navigationEntry) {
vitals.TTFB = navigationEntry.responseStart - navigationEntry.requestStart;
}
} catch (e) {}
// Wait a bit for metrics to stabilize
setTimeout(() => resolve(vitals), 1000);
});
});
// Get resource timing
const resources = await page.evaluate(() => {
return performance.getEntriesByType('resource').map(r => ({
name: r.name,
type: r.initiatorType,
duration: r.duration,
size: r.transferSize,
startTime: r.startTime
}));
});
const result = {
success: true,
url: page.url(),
metrics: {
...metrics,
JSHeapUsedSizeMB: (metrics.JSHeapUsedSize / 1024 / 1024).toFixed(2),
JSHeapTotalSizeMB: (metrics.JSHeapTotalSize / 1024 / 1024).toFixed(2)
},
vitals: vitals,
resources: {
count: resources.length,
totalDuration: resources.reduce((sum, r) => sum + r.duration, 0),
items: args.resources === 'true' ? resources : undefined
}
};
if (args.trace) {
result.trace = args.trace;
}
outputJSON(result);
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
outputError(error);
}
}
measurePerformance();
Persistent Browser Mode
These scripts enable you to launch Chrome once and keep it open for multiple commands, making it easier to interact with authenticated sessions and multi-step workflows.
Quick Start
1. Launch Persistent Browser
# Launch browser (visible window)
node launch-persistent.js --headless=false
# Launch with initial URL
node launch-persistent.js --headless=false --url=https://example.com/login
# Launch headless
node launch-persistent.jsThe browser will stay open and print a message confirming it's ready.
2. Run Commands
All existing scripts will automatically connect to the persistent browser:
# Navigate to a page
node navigate.js --url https://example.com/dashboard
# Take a screenshot
node screenshot.js --output ./screenshot.png
# Run JavaScript
node evaluate.js --script "document.title"
# Get page snapshot
node snapshot.js
# Fill forms
node fill.js --selector "#username" --value "user@example.com"
node click.js --selector "button[type=submit]"3. Close Browser
# Close the persistent browser
node close-persistent.js
# Or press Ctrl+C in the launch-persistent.js terminalHow It Works
1. launch-persistent.js starts Chrome with remote debugging enabled and saves the WebSocket endpoint to .browser-endpoint 2. All other scripts check for this file first and connect to the existing browser if found 3. If no persistent browser exists, scripts fall back to launching their own temporary browser (original behavior)
Use Cases
Authenticated Sessions
# Launch browser
node launch-persistent.js --headless=false --url=https://app.example.com/login
# Manually log in through the visible browser window
# Then run commands on authenticated pages
node navigate.js --url=https://app.example.com/dashboard
node screenshot.js --output ./dashboard.png
node evaluate.js --script "document.querySelector('.user-name').textContent"Multi-Step Workflows
# Launch browser
node launch-persistent.js --headless=false
# Fill out multi-page form
node navigate.js --url=https://example.com/signup
node fill.js --selector "#email" --value "user@example.com"
node click.js --selector ".next-button"
# Wait for page to load, then continue...
node fill.js --selector "#password" --value "secret123"
node click.js --selector ".submit-button"Accessibility Audits on Live Pages
# Launch and navigate manually to complex authenticated state
node launch-persistent.js --headless=false
# After manual navigation/interaction, run audits
node evaluate.js --script "/* accessibility check script */"
node screenshot.js --output ./audit.pngAdvantages
- Manual interaction: Log in, navigate, or manipulate the page manually
- Persistent state: Cookies, localStorage, and session state preserved
- Faster iterations: No browser startup delay for each command
- Real-world testing: Test on actual authenticated or complex application states
Backwards Compatibility
All existing scripts work exactly as before if no persistent browser is running. The enhancement is completely opt-in.
Chrome DevTools Scripts
CLI scripts for browser automation using Puppeteer.
CRITICAL: Always check pwd before running scripts.
Installation
Quick Install
pwd # Should show current working directory
cd .claude/skills/chrome-devtools/scripts
./install.sh # Auto-checks dependencies and installsManual Installation
Linux/WSL - Install system dependencies first:
./install-deps.sh # Auto-detects OS (Ubuntu, Debian, Fedora, etc.)Or manually:
sudo apt-get install -y libnss3 libnspr4 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1All platforms - Install Node dependencies:
npm installScripts
CRITICAL: Always check pwd before running scripts.
navigate.js
Navigate to a URL.
node navigate.js --url https://example.com [--wait-until networkidle2] [--timeout 30000]screenshot.js
Take a screenshot with automatic compression.
Important: Always save screenshots to ./docs/screenshots directory.
node screenshot.js --output screenshot.png [--url https://example.com] [--full-page true] [--selector .element] [--max-size 5] [--no-compress]Automatic Compression: Screenshots >5MB are automatically compressed using ImageMagick to ensure compatibility with Gemini API and Claude Code. Install ImageMagick for this feature:
- macOS:
brew install imagemagick - Linux:
sudo apt-get install imagemagick
Options:
--max-size N- Custom size threshold in MB (default: 5)--no-compress- Disable automatic compression--format png|jpeg- Output format (default: png)--quality N- JPEG quality 0-100 (default: auto)
click.js
Click an element.
node click.js --selector ".button" [--url https://example.com] [--wait-for ".result"]fill.js
Fill form fields.
node fill.js --selector "#input" --value "text" [--url https://example.com] [--clear true]evaluate.js
Execute JavaScript in page context.
node evaluate.js --script "document.title" [--url https://example.com]snapshot.js
Get DOM snapshot with interactive elements.
node snapshot.js [--url https://example.com] [--output snapshot.json]console.js
Monitor console messages.
node console.js --url https://example.com [--types error,warn] [--duration 5000]network.js
Monitor network requests.
node network.js --url https://example.com [--types xhr,fetch] [--output requests.json]performance.js
Measure performance metrics and record trace.
node performance.js --url https://example.com [--trace trace.json] [--metrics] [--resources true]Common Options
--headless false- Show browser window--close false- Keep browser open--timeout 30000- Set timeout in milliseconds--wait-until networkidle2- Wait strategy (load, domcontentloaded, networkidle0, networkidle2)
Selector Support
Scripts that accept --selector (click.js, fill.js, screenshot.js) support both CSS and XPath selectors.
CSS Selectors (Default)
# Element tag
node click.js --selector "button" --url https://example.com
# Class selector
node click.js --selector ".btn-submit" --url https://example.com
# ID selector
node fill.js --selector "#email" --value "user@example.com" --url https://example.com
# Attribute selector
node click.js --selector 'button[type="submit"]' --url https://example.com
# Complex selector
node screenshot.js --selector "div.container > button.btn-primary" --output btn.pngXPath Selectors
XPath selectors start with / or (// and are automatically detected:
# Text matching - exact
node click.js --selector '//button[text()="Submit"]' --url https://example.com
# Text matching - contains
node click.js --selector '//button[contains(text(),"Submit")]' --url https://example.com
# Attribute matching
node fill.js --selector '//input[@type="email"]' --value "user@example.com"
# Multiple conditions
node click.js --selector '//button[@type="submit" and contains(text(),"Save")]'
# Descendant selection
node screenshot.js --selector '//div[@class="modal"]//button[@class="close"]' --output modal.png
# Nth element
node click.js --selector '(//button)[2]' # Second button on pageDiscovering Selectors
Use snapshot.js to discover correct selectors:
# Get all interactive elements
node snapshot.js --url https://example.com | jq '.elements[]'
# Find buttons
node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="BUTTON")'
# Find inputs
node snapshot.js --url https://example.com | jq '.elements[] | select(.tagName=="INPUT")'Security
XPath selectors are validated to prevent injection attacks. The following patterns are blocked:
javascript:<scriptonerror=,onload=,onclick=eval(,Function(,constructor(
Selectors exceeding 1000 characters are rejected (DoS prevention).
Output Format
All scripts output JSON to stdout:
{
"success": true,
"url": "https://example.com",
"title": "Example Domain",
...
}Errors are output to stderr:
{
"success": false,
"error": "Error message",
"stack": "..."
}#!/usr/bin/env node
/**
* Take a screenshot
* Usage: node screenshot.js --output screenshot.png [--url https://example.com] [--full-page true] [--selector .element] [--max-size 5] [--no-compress]
* Supports both CSS and XPath selectors:
* - CSS: node screenshot.js --selector ".main-content" --output page.png
* - XPath: node screenshot.js --selector "//div[@class='main-content']" --output page.png
*/
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
import { parseSelector, getElement, enhanceError } from './lib/selector.js';
import fs from 'fs/promises';
import path from 'path';
import { execFileSync } from 'child_process';
/**
* Compress image using ImageMagick if it exceeds max size
* @param {string} filePath - Path to the image file
* @param {number} maxSizeMB - Maximum file size in MB (default: 5)
* @returns {Promise<{compressed: boolean, originalSize: number, finalSize: number}>}
*/
async function compressImageIfNeeded(filePath, maxSizeMB = 5) {
const stats = await fs.stat(filePath);
const originalSize = stats.size;
const maxSizeBytes = maxSizeMB * 1024 * 1024;
if (originalSize <= maxSizeBytes) {
return { compressed: false, originalSize, finalSize: originalSize };
}
try {
// Check if ImageMagick is available and determine the command name
let magickCmd = 'magick';
try {
execFileSync('magick', ['-version'], { stdio: 'pipe' });
} catch {
try {
execFileSync('convert', ['-version'], { stdio: 'pipe' });
magickCmd = 'convert';
} catch {
console.error('Warning: ImageMagick not found. Install it to enable automatic compression.');
return { compressed: false, originalSize, finalSize: originalSize };
}
}
const ext = path.extname(filePath).toLowerCase();
const tempPath = filePath.replace(ext, `.temp${ext}`);
// Determine compression strategy based on file type
// Using execFileSync with array args prevents command injection
let compressionArgs;
if (ext === '.png') {
// For PNG: resize and compress with quality
compressionArgs = [filePath, '-strip', '-resize', '90%', '-quality', '85', tempPath];
} else if (ext === '.jpg' || ext === '.jpeg') {
// For JPEG: compress with quality and progressive
compressionArgs = [filePath, '-strip', '-quality', '80', '-interlace', 'Plane', tempPath];
} else {
// For other formats: convert to JPEG with compression
compressionArgs = [filePath, '-strip', '-quality', '80', tempPath.replace(ext, '.jpg')];
}
// Try compression - execFileSync doesn't invoke shell, preventing injection
execFileSync(magickCmd, compressionArgs, { stdio: 'pipe' });
const compressedStats = await fs.stat(tempPath);
const compressedSize = compressedStats.size;
// If still too large, try more aggressive compression
if (compressedSize > maxSizeBytes) {
const finalPath = filePath.replace(ext, `.final${ext}`);
let aggressiveArgs;
if (ext === '.png') {
aggressiveArgs = [tempPath, '-strip', '-resize', '75%', '-quality', '70', finalPath];
} else {
aggressiveArgs = [tempPath, '-strip', '-quality', '60', '-sampling-factor', '4:2:0', finalPath];
}
execFileSync(magickCmd, aggressiveArgs, { stdio: 'pipe' });
await fs.unlink(tempPath);
await fs.rename(finalPath, filePath);
} else {
await fs.rename(tempPath, filePath);
}
const finalStats = await fs.stat(filePath);
return { compressed: true, originalSize, finalSize: finalStats.size };
} catch (error) {
console.error('Compression error:', error.message);
// If compression fails, keep original file
try {
const tempPath = filePath.replace(path.extname(filePath), '.temp' + path.extname(filePath));
await fs.unlink(tempPath).catch(() => {});
} catch {}
return { compressed: false, originalSize, finalSize: originalSize };
}
}
async function screenshot() {
const args = parseArgs(process.argv.slice(2));
if (!args.output) {
outputError(new Error('--output 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 page.goto(args.url, {
waitUntil: args['wait-until'] || 'networkidle2'
});
}
const screenshotOptions = {
path: args.output,
type: args.format || 'png',
fullPage: args['full-page'] === 'true'
};
if (args.quality) {
screenshotOptions.quality = parseInt(args.quality);
}
let buffer;
if (args.selector) {
// Parse and validate selector
const parsed = parseSelector(args.selector);
// Get element based on selector type
const element = await getElement(page, parsed);
if (!element) {
throw new Error(`Element not found: ${args.selector}`);
}
buffer = await element.screenshot(screenshotOptions);
} else {
buffer = await page.screenshot(screenshotOptions);
}
const result = {
success: true,
output: path.resolve(args.output),
size: buffer.length,
url: page.url()
};
// Compress image if needed (unless --no-compress flag is set)
if (args['no-compress'] !== 'true') {
const maxSize = args['max-size'] ? parseFloat(args['max-size']) : 5;
const compressionResult = await compressImageIfNeeded(args.output, maxSize);
if (compressionResult.compressed) {
result.compressed = true;
result.originalSize = compressionResult.originalSize;
result.size = compressionResult.finalSize;
result.compressionRatio = ((1 - compressionResult.finalSize / compressionResult.originalSize) * 100).toFixed(2) + '%';
}
}
outputJSON(result);
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
// Enhance error message if selector-related
if (args.selector) {
const enhanced = enhanceError(error, args.selector);
outputError(enhanced);
} else {
outputError(error);
}
process.exit(1);
}
}
screenshot();
#!/usr/bin/env node
/**
* Get DOM snapshot with selectors
* Usage: node snapshot.js [--url https://example.com] [--output snapshot.json]
*/
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
import fs from 'fs/promises';
async function snapshot() {
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 page.goto(args.url, {
waitUntil: args['wait-until'] || 'networkidle2'
});
}
// Get interactive elements with metadata
const elements = await page.evaluate(() => {
const interactiveSelectors = [
'a[href]',
'button',
'input',
'textarea',
'select',
'[onclick]',
'[role="button"]',
'[role="link"]',
'[contenteditable]'
];
const elements = [];
const selector = interactiveSelectors.join(', ');
const nodes = document.querySelectorAll(selector);
nodes.forEach((el, index) => {
const rect = el.getBoundingClientRect();
// Generate unique selector
let uniqueSelector = '';
if (el.id) {
uniqueSelector = `#${el.id}`;
} else if (el.className) {
const classes = Array.from(el.classList).join('.');
uniqueSelector = `${el.tagName.toLowerCase()}.${classes}`;
} else {
uniqueSelector = el.tagName.toLowerCase();
}
elements.push({
index: index,
tagName: el.tagName.toLowerCase(),
type: el.type || null,
id: el.id || null,
className: el.className || null,
name: el.name || null,
value: el.value || null,
text: el.textContent?.trim().substring(0, 100) || null,
href: el.href || null,
selector: uniqueSelector,
xpath: getXPath(el),
visible: rect.width > 0 && rect.height > 0,
position: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
}
});
});
function getXPath(element) {
if (element.id) {
return `//*[@id="${element.id}"]`;
}
if (element === document.body) {
return '/html/body';
}
let ix = 0;
const siblings = element.parentNode?.childNodes || [];
for (let i = 0; i < siblings.length; i++) {
const sibling = siblings[i];
if (sibling === element) {
return getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']';
}
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
ix++;
}
}
return '';
}
return elements;
});
const result = {
success: true,
url: page.url(),
title: await page.title(),
elementCount: elements.length,
elements: elements
};
if (args.output) {
await fs.writeFile(args.output, JSON.stringify(result, null, 2));
outputJSON({
success: true,
output: args.output,
elementCount: elements.length
});
} else {
outputJSON(result);
}
if (args.close !== 'false') {
await closeBrowser();
}
} catch (error) {
outputError(error);
}
}
snapshot();
Related skills
Forks & variants (1)
Chrome Devtools has 1 known copy in the catalog totaling 5 installs. They canonicalize to this original listing.
- aia-11-hn-mib - 5 installs
How it compares
Pick Chrome DevTools for bundled Puppeteer CLI scripts with JSON output; use dedicated E2E frameworks when full test-suite CI integration is the primary goal.
FAQ
What does the Chrome DevTools skill automate?
Chrome DevTools runs Puppeteer CLI scripts for browser automation including screenshots, performance analysis, network monitoring, web scraping, form automation, and JavaScript debugging—with JSON output for agent consumption.
What setup does Chrome DevTools require on Linux?
Chrome DevTools notes that Linux and WSL environments need headless Chrome system libraries installed before Puppeteer scripts run, and agents should verify the working directory before executing bundled CLI scripts.
Is Chrome Devtools safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.