seed-skills/puppeteer-automation/SKILL.md
Automate Chrome with Puppeteer for scraping, screenshots, PDF generation, and E2E checks — correct launch options, reliable waiting with locators and waitForSelector, request interception, and headless CI execution.
npx skillsauth add PramodDutta/qaskills Puppeteer Browser AutomationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill makes an AI agent write reliable Puppeteer scripts: launching Chrome with the right flags, navigating and interacting without race conditions, capturing screenshots and PDFs, and intercepting network requests to mock or block traffic. Trigger it when a project uses puppeteer or puppeteer-core, or when the user asks to scrape a page, generate a PDF from HTML, screenshot a site, or automate Chrome without a full test framework.
page.click() immediately after page.goto() races against rendering. Use page.locator() (auto-waiting, Puppeteer 21+) or explicit waitForSelector before every interaction.await new Promise(r => setTimeout(r, 3000)) is either too short (flaky) or too long (slow). Wait on selectors, network idle, or response predicates instead.finally. A script that throws before browser.close() leaks a Chrome process. In CI those zombies accumulate until the runner dies.waitUntil deliberately. load waits for every image and font; domcontentloaded is enough for interaction; networkidle2 is for SPAs that fetch after load. Pick per page, do not cargo-cult networkidle0.Promise.all. Clicking a link then awaiting waitForNavigation separately misses fast navigations. Start the wait before the click.npm install --save-dev puppeteer typescript tsx
// src/browser.ts
import puppeteer, { Browser } from 'puppeteer';
export async function launchBrowser(): Promise<Browser> {
return puppeteer.launch({
headless: true,
args: [
'--no-sandbox', // required in most Docker/CI containers
'--disable-dev-shm-usage', // /dev/shm is 64MB in Docker; avoids renderer crashes
'--disable-gpu',
'--window-size=1366,768',
],
defaultViewport: { width: 1366, height: 768 },
});
}
A complete script with correct lifecycle handling:
// src/check-login.ts
import { launchBrowser } from './browser';
async function main(): Promise<void> {
const browser = await launchBrowser();
try {
const page = await browser.newPage();
page.setDefaultTimeout(15_000);
await page.goto('https://practice.expandtesting.com/login', {
waitUntil: 'domcontentloaded',
});
// locator() auto-waits for visibility and stability before acting
await page.locator('#username').fill('practice');
await page.locator('#password').fill('SuperSecretPassword!');
await Promise.all([
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
page.locator('button[type="submit"]').click(),
]);
const flash = await page.locator('#flash').waitHandle();
const text = await flash.evaluate((el) => el.textContent?.trim());
if (!text?.includes('You logged into a secure area')) {
throw new Error(`Login failed, flash message: ${text}`);
}
console.log('Login OK');
} finally {
await browser.close();
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
npx tsx src/check-login.ts
import { launchBrowser } from './browser';
const browser = await launchBrowser();
try {
const page = await browser.newPage();
await page.goto('https://qaskills.sh', { waitUntil: 'networkidle2' });
// Full-page screenshot
await page.screenshot({ path: 'homepage.png', fullPage: true });
// Screenshot of one element only
const hero = await page.waitForSelector('main section:first-of-type');
await hero!.screenshot({ path: 'hero.png' });
// PDF requires headless mode; emulate print CSS first
await page.emulateMediaType('print');
await page.pdf({
path: 'homepage.pdf',
format: 'A4',
printBackground: true,
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
});
} finally {
await browser.close();
}
const page = await browser.newPage();
await page.setRequestInterception(true);
page.on('request', (request) => {
const url = request.url();
const type = request.resourceType();
// Block images, fonts, and trackers for a 3-5x speedup on content scraping
if (type === 'image' || type === 'font' || url.includes('google-analytics')) {
return request.abort();
}
// Stub a backend endpoint with deterministic data
if (url.endsWith('/api/feature-flags')) {
return request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ newCheckout: true, darkMode: false }),
});
}
return request.continue();
});
await page.goto('https://app.example.com/dashboard', { waitUntil: 'networkidle2' });
// Wait for the specific XHR the page fires, then read its JSON
const [response] = await Promise.all([
page.waitForResponse(
(res) => res.url().includes('/api/search') && res.status() === 200,
),
page.locator('input[name="q"]').fill('playwright'),
]);
const results = (await response.json()) as { items: { title: string }[] };
// Extract structured data from the DOM in one evaluate call
const rows = await page.$$eval('table#skills tbody tr', (trs) =>
trs.map((tr) => ({
name: tr.querySelector('td:nth-child(1)')?.textContent?.trim() ?? '',
installs: Number(tr.querySelector('td:nth-child(2)')?.textContent ?? 0),
})),
);
console.log(rows.filter((r) => r.installs > 100));
import type { Page } from 'puppeteer';
export async function clearAndType(page: Page, selector: string, value: string): Promise<void> {
const input = await page.waitForSelector(selector, { visible: true });
await input!.click({ clickCount: 3 }); // select existing text
await input!.press('Backspace');
await input!.type(value, { delay: 20 });
}
puppeteer-core + system Chrome is the top source of "works on my machine".page.setDefaultTimeout() once per page instead of passing { timeout } everywhere.ghcr.io/puppeteer/puppeteer image or install the documented dependency list — a bare node:20-slim will fail with cryptic shared-library errors.Browser across many pages; launching Chrome costs 1-2 seconds, browser.newPage() costs milliseconds.catch block before rethrowing — page.screenshot({ path: 'failure.png' }) turns a CI mystery into a one-look diagnosis.page.waitForTimeout(3000) / sleep-based waits. Replace with waitForSelector, waitForResponse, or waitForFunction.headless: false committed to CI scripts. Headful Chrome needs a display server; CI dies with "Missing X server". Gate it behind an env var for local debugging only.page.evaluate with variables captured from Node scope. The callback serializes to the browser; closures over Node objects throw. Pass data as arguments: page.evaluate((sel) => ..., selector).finally close. Zombie Chrome processes exhaust CI memory.request.continue() in the default branch — every request hangs and the page never loads..css-1q2w3e. Use IDs, data-testid, ARIA roles, or stable attribute selectors.puppeteer or puppeteer-core, or has scripts importing them./dev/shm, or missing-library errors.development
Generate test data from the schemas you already have. Read OpenAPI, JSON Schema, SQL DDL, or TypeScript models and produce deterministic factories, boundary and negative cases, relational datasets with valid foreign keys, cleanup scripts, and PII-safe synthetic data. Production records never leave the machine.
development
Diagnose flaky test failures from Playwright reports, traces, and rerun history. Classify each failure as product, test, environment, data, or unknown with cited evidence and a proposed fix. Never auto-modifies code without opt-in.
tools
Test LLM, RAG, MCP, and agentic systems end to end. Build golden datasets, run deterministic checks and LLM judges, score retrieval, probe prompt injection, verify tool use, and gate CI on thresholds. Orchestrates DeepEval, Ragas, promptfoo, and Langfuse.
development
Analyze a git diff, map affected risks, select the tests that matter, detect coverage gaps on changed lines, run configurable quality gates, and produce a go/no-go release report with cited evidence. Recommends only; never merges or deploys.