by thetestingacademy
Headless Chrome automation with Puppeteer for web scraping, PDF generation, and E2E testing
npx @qaskills/cli add puppeteer-automationAuto-detects your AI agent and installs the skill. Works with Claude Code, Cursor, Copilot, and more.
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.- name: Install QA Skills
run: npx @qaskills/cli add puppeteer-automation10 of 29 agents supported
Go from zero to Playwright pro: Page Object Model, fixtures, and CI/CD on real projects.
Use code PROMODE at checkout