
chrome-devtools-testing
Browser testing and debugging with Playwright. QA testing, screenshots, form interactions, console e
by nicobailon|Open Source
Chrome DevTools Testing
A Playwright-based browser testing skill for Claude Code that connects directly to Chrome via CDP. Supports batch scripting (multiple actions per turn), auto-wait, ARIA snapshots, and full DevTools debugging.
Inspired by SawyerHood/dev-browser.
Features
- Batch Scripting: Execute multiple browser actions in a single heredoc script
- Persistent Pages: Browser pages survive client disconnections
- Smart Page Load Detection: Filters ads/tracking, handles stuck requests gracefully
- ARIA Snapshots: Accessibility tree with refs for element targeting
- Playwright Locators: Robust element selection with auto-wait
- DevTools Debugging: Console capture, network monitoring, performance tracing
Installation
cd ~/.claude/skills/chrome-devtools-testing
bun install
Usage
1. Start the Server
bun run start-server
Options:
--headless- Run browser without UI--port=9333- Use custom port (default: 9222)
2. Run Scripts
bun x tsx <<'EOF'
import { connect } from "./src/client.js";
const client = await connect();
const page = await client.page("main");
await page.goto("http://localhost:3000");
await page.fill('[name="email"]', "test@example.com");
await page.click('button[type="submit"]');
await page.screenshot({ path: "/tmp/result.png" });
await client.disconnect();
EOF
API Reference
Client
import { connect } from "./src/client.js";
const client = await connect("http://localhost:9222");
// Page management
const page = await client.page("main"); // Get or create named page
const pages = await client.listPages(); // List all pages
await client.closePage("main"); // Close a page
await client.disconnect(); // Disconnect (pages persist)
// ARIA snapshots
const snapshot = await client.getAISnapshot("main");
const element = await client.selectSnapshotRef("main", "e5");
Page (Playwright)
// Navigation
await page.goto("https://example.com");
await page.goBack();
await page.reload();
// Locators (recommended)
await page.getByRole("button", { name: "Submit" }).click();
await page.getByLabel("Email").fill("test@example.com");
await page.getByText("Welcome").waitFor();
// CSS selectors
await page.locator(".submit-btn").click();
await page.locator("#email").fill("test@example.com");
// Screenshots
await page.screenshot({ path: "/tmp/page.png" });
await page.screenshot({ path: "/tmp/full.png", fullPage: true });
Wait for Page Load
import { waitForPageLoad } from "./src/client.js";
const result = await waitForPageLoad(page, {
timeout: 10000, // Max wait (default: 10s)
pollInterval: 50, // Check frequency (default: 50ms)
minimumWait: 100, // Initial wait (default: 100ms)
waitForNetworkIdle: true // Wait for no pending requests
});
// Result: { success, readyState, pendingRequests, waitTimeMs, timedOut }
Smart filtering ignores:
- Ad/tracking domains (Google Analytics, Facebook, Hotjar, etc.)
- Non-critical resources after 3s (images, fonts)
- Stuck requests (>10s)
Debugging
// Console messages
const logs = await client.getConsoleMessages(page, {
types: ["error", "warn"],
limit: 50
});
// Network requests
const requests = await client.getNetworkRequests(page, {
types: ["xhr", "fetch"]
});
// Performance metrics
const metrics = await client.getPerformanceMetrics(page);
// { lcp, fcp, cls, ttfb, domContentLoaded, load }
// Core Web Vitals
const vitals = await client.getCoreWebVitals(page);
// { lcp, fid, cls, ttfb }
// Performance tracing
await client.startPerformanceTrace(page);
await page.reload();
const trace = await client.stopPerformanceTrace(page);
ARIA Snapshots
const snapshot = await client.getAISnapshot("main");
console.log(snapshot);
Output:
- navigation:
- link "Home" [ref=e1]
- link "Products" [ref=e2]
- main:
- heading "Welcome" [level=1]
- form:
- textbox "Email" [ref=e5]
- button "Submit" [ref=e6]
Interact via refs:
const button = await client.selectSnapshotRef("main", "e6");
await button?.click();
Examples
Form Submission
import { connect, waitForPageLoad } from "./src/client.js";
const client = await connect();
const page = await client.page("main");
await page.goto("http://localhost:3000/login");
await waitForPageLoad(page);
await page.getByLabel("Email").fill("user@example.com");
await page.getByLabel("Password").fill("secret123");
await page.getByRole("button", { name: "Sign In" }).click();
await page.getByText("Welcome").waitFor();
await page.screenshot({ path: "/tmp/logged-in.png" });
await client.disconnect();
Debug Page Issues
import { connect } from "./src/client.js";
const client = await connect();
const page = await client.page("debug");
await page.goto("http://localhost:3000");
const errors = await client.getConsoleMessages(page, { types: ["error"] });
console.log("Errors:", errors);
const requests = await client.getNetworkRequests(page, { types: ["xhr", "fetch"] });
console.log("API calls:", requests);
const vitals = await client.getCoreWebVitals(page);
console.log("Web Vitals:", vitals);
await client.disconnect();
Architecture
├── src/
│ ├── client.ts # Client: connect, page management, waitForPageLoad
│ ├── server.ts # Server: persistent browser, REST API
│ ├── devtools.ts # CDP debugging: console, network, performance
│ ├── types.ts # Shared TypeScript types
│ └── snapshot/
│ ├── browser-script.ts # ARIA tree extraction (browser-injected)
│ └── index.ts # Snapshot injection wrapper
├── scripts/
│ └── start-server.ts # Server entry point
├── package.json
├── tsconfig.json
└── SKILL.md # Claude Code skill documentation
License
MIT