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.
Scanned 9/5/2026
Install to Claude Code
npx -y skills add PramodDutta/qaskills --skill puppeteer-automation --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Puppeteer Automation?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/pramoddutta-puppeteer-automation)More formats (shields.io, HTML) on the badges page.
---
name: Puppeteer Browser Automation
description: 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.
version: 1.0.0
author: thetestingacademy
license: MIT
tags: [puppeteer, browser-automation, headless-chrome, scraping, screenshots, pdf, e2e, devtools, ci]
testingTypes: [e2e]
frameworks: [puppeteer, jest]
languages: [typescript, javascript]
domains: [web]
agents: [claude-code, cursor, github-copilot, windsurf, codex, aider, continue, cline, zed, bolt, gemini-cli, amp]
---
# Puppeteer Browser Automation
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.
## Core Principles
1. **Every action must wait for its precondition.** `page.click()` immediately after `page.goto()` races against rendering. Use `page.locator()` (auto-waiting, Puppeteer 21+) or explicit `waitForSelector` before every interaction.
2. **Never use fixed sleeps.** `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.
3. **Always close the browser in `finally`.** A script that throws before `browser.close()` leaks a Chrome process. In CI those zombies accumulate until the runner dies.
4. **Set `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`.
5. **Combine navigation-triggering actions with `Promise.all`.** Clicking a link then awaiting `waitForNavigation` separately misses fast navigations. Start the wait before the click.
6. **Request interception is your mock layer.** Block analytics and images for speed, stub API responses for determinism — no proxy server needed.
## Setup
```bash
npm install --save-dev puppeteer typescript tsx
```
```typescript
// 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:
```typescript
// 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);
});
```
```bash
npx tsx src/check-login.ts
```
## Patterns
### Screenshots and PDF Generation
```typescript
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();
}
```
### Request Interception: Block Noise, Stub APIs
```typescript
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' });
```
### Waiting on Responses and Extracting Data
```typescript
// 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));
```
### Reusable Page Helper for Flaky-Free Typing
```typescript
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 });
}
```
## Best Practices
- Pin the Puppeteer version; each release bundles a specific Chrome. Mismatched `puppeteer-core` + system Chrome is the top source of "works on my machine".
- Set `page.setDefaultTimeout()` once per page instead of passing `{ timeout }` everywhere.
- In Docker, use the official `ghcr.io/puppeteer/puppeteer` image or install the documented dependency list — a bare `node:20-slim` will fail with cryptic shared-library errors.
- Reuse one `Browser` across many pages; launching Chrome costs 1-2 seconds, `browser.newPage()` costs milliseconds.
- Capture a screenshot in your `catch` block before rethrowing — `page.screenshot({ path: 'failure.png' })` turns a CI mystery into a one-look diagnosis.
- For E2E test suites with assertions, fixtures, and retries, prefer Playwright; keep Puppeteer for scraping, PDF/screenshot services, and Chrome-extension automation where it excels.
## Anti-Patterns
- **`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.
- **Scraping inside `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)`.
- **One giant try/catch around the whole script with no `finally` close.** Zombie Chrome processes exhaust CI memory.
- **Enabling request interception and forgetting `request.continue()`** in the default branch — every request hangs and the page never loads.
- **Selectors built from generated class names** like `.css-1q2w3e`. Use IDs, `data-testid`, ARIA roles, or stable attribute selectors.
## When to Trigger This Skill
- The repo depends on `puppeteer` or `puppeteer-core`, or has scripts importing them.
- The user asks to scrape a website, generate PDFs from HTML, or capture screenshots programmatically.
- A headless-Chrome task in Docker/CI is failing with sandbox, `/dev/shm`, or missing-library errors.
- Automating Chrome-specific surfaces: extensions, DevTools protocol features, performance traces.
- Existing Puppeteer code is flaky and needs waits, interception, or lifecycle fixes.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!