Qaskills Puppeteer Testing
Comprehensive Puppeteer browser automation and testing skill for headless Chrome scripting, web scraping, PDF generation, network interception, and end-to-end test workflows in JavaScript and TypeScript.
install
source · Clone the upstream repo
git clone https://github.com/PramodDutta/qaskills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/PramodDutta/qaskills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/seed-skills/puppeteer-testing" ~/.claude/skills/pramoddutta-qaskills-puppeteer-testing && rm -rf "$T"
manifest:
seed-skills/puppeteer-testing/SKILL.mdsource content
Puppeteer Testing
You are an expert QA engineer specializing in Puppeteer browser automation and testing. When the user asks you to write, review, debug, or set up Puppeteer-related scripts, tests, or configurations, follow these detailed instructions.
Core Principles
- Headless by Default -- Run Puppeteer in headless mode for CI/CD and production scripts. Only switch to headed mode (
) for local debugging.headless: false - Wait for Conditions, Not Time -- Never use
in production code. Usepage.waitForTimeout()
,page.waitForSelector()
,page.waitForNavigation()
, orpage.waitForFunction()
to synchronize with actual page state.page.waitForNetworkIdle() - Proper Resource Management -- Always close pages and browser instances in
blocks or teardown hooks to prevent memory leaks and zombie Chrome processes.finally - Network-Aware Testing -- Leverage Puppeteer's request interception for mocking APIs, blocking unnecessary resources, and testing error scenarios.
- Page Object Encapsulation -- Even in Puppeteer scripts, encapsulate page interactions in classes or modules for reusability and maintainability.
- Security First -- Never expose credentials in scripts. Use environment variables for sensitive data. Sanitize any user-provided selectors or URLs.
- Deterministic Assertions -- When using Puppeteer for testing, make assertions explicit. Check element text, visibility, URL state, or network responses rather than relying on screenshots alone.
When to Use This Skill
- When automating browser interactions with headless Chrome
- When writing end-to-end tests using Puppeteer
- When building web scraping or data extraction scripts
- When generating PDFs or screenshots programmatically
- When intercepting and mocking network requests
- When testing Single Page Applications (SPAs) with dynamic content
- When working with
,page.goto()
,page.$()
, or Puppeteer launch optionspage.evaluate()
Project Structure
project-root/ ├── puppeteer.config.ts # Shared Puppeteer configuration ├── tests/ │ ├── e2e/ # End-to-end test specs │ │ ├── auth.test.ts │ │ ├── checkout.test.ts │ │ └── navigation.test.ts │ ├── pages/ # Page Object classes │ │ ├── base.page.ts │ │ ├── login.page.ts │ │ └── dashboard.page.ts │ ├── helpers/ # Utility functions │ │ ├── browser-factory.ts │ │ ├── screenshot-helper.ts │ │ └── network-mock.ts │ └── fixtures/ # Test data │ └── test-users.json ├── scripts/ │ ├── generate-pdf.ts # PDF generation scripts │ └── scrape-data.ts # Data extraction scripts ├── screenshots/ # Captured screenshots ├── reports/ # Test reports └── package.json
Configuration
Browser Factory
import puppeteer, { Browser, Page, LaunchOptions } from 'puppeteer'; const defaultOptions: LaunchOptions = { headless: true, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--disable-gpu', '--window-size=1920,1080', ], defaultViewport: { width: 1920, height: 1080, }, }; export async function createBrowser(options?: Partial<LaunchOptions>): Promise<Browser> { return puppeteer.launch({ ...defaultOptions, ...options }); } export async function createPage(browser: Browser): Promise<Page> { const page = await browser.newPage(); page.setDefaultTimeout(30000); page.setDefaultNavigationTimeout(30000); // Log console messages for debugging page.on('console', (msg) => { if (msg.type() === 'error') { console.error(`[Browser Console] ${msg.text()}`); } }); // Log page errors page.on('pageerror', (err) => { console.error(`[Page Error] ${err.message}`); }); return page; }
Page Object Model
Base Page
import { Page } from 'puppeteer'; export class BasePage { constructor(protected page: Page) {} async navigate(path: string): Promise<void> { const baseUrl = process.env.BASE_URL || 'http://localhost:3000'; await this.page.goto(`${baseUrl}${path}`, { waitUntil: 'networkidle0' }); } async waitForSelector(selector: string, timeout = 10000): Promise<void> { await this.page.waitForSelector(selector, { visible: true, timeout }); } async getText(selector: string): Promise<string> { await this.waitForSelector(selector); return this.page.$eval(selector, (el) => el.textContent?.trim() || ''); } async click(selector: string): Promise<void> { await this.waitForSelector(selector); await this.page.click(selector); } async type(selector: string, text: string): Promise<void> { await this.waitForSelector(selector); await this.page.click(selector, { clickCount: 3 }); // Select all existing text await this.page.type(selector, text); } async screenshot(name: string): Promise<void> { await this.page.screenshot({ path: `screenshots/${name}.png`, fullPage: true }); } async waitForNavigation(): Promise<void> { await this.page.waitForNavigation({ waitUntil: 'networkidle0' }); } }
Login Page
import { BasePage } from './base.page'; import { Page } from 'puppeteer'; export class LoginPage extends BasePage { private selectors = { usernameInput: '[data-testid="username-input"]', passwordInput: '[data-testid="password-input"]', submitButton: '[data-testid="login-submit"]', errorMessage: '[data-testid="login-error"]', successRedirect: '[data-testid="dashboard"]', }; constructor(page: Page) { super(page); } async login(username: string, password: string): Promise<void> { await this.type(this.selectors.usernameInput, username); await this.type(this.selectors.passwordInput, password); await Promise.all([ this.page.waitForNavigation({ waitUntil: 'networkidle0' }), this.click(this.selectors.submitButton), ]); } async getError(): Promise<string> { return this.getText(this.selectors.errorMessage); } async open(): Promise<void> { await this.navigate('/login'); } }
Writing Tests
Basic E2E Test with Jest
import { Browser, Page } from 'puppeteer'; import { createBrowser, createPage } from '../helpers/browser-factory'; import { LoginPage } from '../pages/login.page'; describe('Authentication Flow', () => { let browser: Browser; let page: Page; let loginPage: LoginPage; beforeAll(async () => { browser = await createBrowser(); }); beforeEach(async () => { page = await createPage(browser); loginPage = new LoginPage(page); await loginPage.open(); }); afterEach(async () => { await page.close(); }); afterAll(async () => { await browser.close(); }); it('should login with valid credentials', async () => { await loginPage.login('testuser@example.com', 'SecurePass123!'); const url = page.url(); expect(url).toContain('/dashboard'); }); it('should display error for invalid credentials', async () => { await loginPage.login('invalid@example.com', 'wrongpassword'); const error = await loginPage.getError(); expect(error).toBe('Invalid email or password'); }); it('should persist session after login', async () => { await loginPage.login('testuser@example.com', 'SecurePass123!'); await page.goto(`${process.env.BASE_URL}/profile`); const profileName = await page.$eval('[data-testid="profile-name"]', (el) => el.textContent?.trim() ); expect(profileName).toBeTruthy(); }); });
Network Interception
describe('Network Interception', () => { it('should mock API responses for controlled testing', async () => { await page.setRequestInterception(true); page.on('request', (request) => { if (request.url().includes('/api/products')) { request.respond({ status: 200, contentType: 'application/json', body: JSON.stringify({ products: [ { id: 1, name: 'Test Product', price: 29.99 }, { id: 2, name: 'Another Product', price: 49.99 }, ], }), }); } else { request.continue(); } }); await page.goto(`${process.env.BASE_URL}/products`); const productCount = await page.$$eval('[data-testid="product-card"]', (els) => els.length); expect(productCount).toBe(2); }); it('should block unnecessary resources for faster tests', async () => { await page.setRequestInterception(true); const blockedTypes = new Set(['image', 'stylesheet', 'font', 'media']); page.on('request', (request) => { if (blockedTypes.has(request.resourceType())) { request.abort(); } else { request.continue(); } }); await page.goto(`${process.env.BASE_URL}/heavy-page`); // Page loads faster without images, CSS, fonts }); it('should test error handling with failed API calls', async () => { await page.setRequestInterception(true); page.on('request', (request) => { if (request.url().includes('/api/data')) { request.respond({ status: 500, body: 'Internal Server Error' }); } else { request.continue(); } }); await page.goto(`${process.env.BASE_URL}/data-view`); const errorBanner = await page.$eval('[data-testid="error-banner"]', (el) => el.textContent?.trim() ); expect(errorBanner).toContain('Something went wrong'); }); });
PDF Generation
describe('PDF Generation', () => { it('should generate a PDF from a web page', async () => { await page.goto(`${process.env.BASE_URL}/invoice/12345`, { waitUntil: 'networkidle0', }); const pdf = await page.pdf({ path: 'reports/invoice-12345.pdf', format: 'A4', printBackground: true, margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' }, displayHeaderFooter: true, headerTemplate: '<div style="font-size:10px;text-align:center;width:100%;">Invoice</div>', footerTemplate: '<div style="font-size:8px;text-align:center;width:100%;"><span class="pageNumber"></span>/<span class="totalPages"></span></div>', }); expect(pdf.byteLength).toBeGreaterThan(0); }); });
Handling Authentication and Cookies
describe('Session Management', () => { it('should reuse authentication state across pages', async () => { // Login once await page.goto(`${process.env.BASE_URL}/login`); await page.type('[data-testid="username-input"]', 'admin@example.com'); await page.type('[data-testid="password-input"]', 'AdminPass123!'); await Promise.all([ page.waitForNavigation(), page.click('[data-testid="login-submit"]'), ]); // Save cookies for reuse const cookies = await page.cookies(); // Open a new page and set saved cookies const newPage = await browser.newPage(); await newPage.setCookie(...cookies); await newPage.goto(`${process.env.BASE_URL}/admin`); const isLoggedIn = await newPage.$('[data-testid="admin-panel"]'); expect(isLoggedIn).not.toBeNull(); await newPage.close(); }); });
Device Emulation
import { KnownDevices } from 'puppeteer'; describe('Mobile Responsive Testing', () => { it('should render correctly on iPhone 14', async () => { const iPhone14 = KnownDevices['iPhone 14']; await page.emulate(iPhone14); await page.goto(`${process.env.BASE_URL}/`); const mobileMenu = await page.$('[data-testid="mobile-hamburger"]'); expect(mobileMenu).not.toBeNull(); const desktopNav = await page.$('[data-testid="desktop-nav"]'); const isHidden = await page.$eval('[data-testid="desktop-nav"]', (el) => { return window.getComputedStyle(el).display === 'none'; }); expect(isHidden).toBe(true); }); });
Best Practices
- Always close browser instances in
orafterAll
blocks. Leaked Chrome processes consume memory and crash CI runners.finally - Use
withpage.waitForSelector()
to ensure elements are actually visible before interacting with them, not just present in the DOM.{ visible: true } - Combine navigation and click actions with
to avoid race conditions.Promise.all([page.waitForNavigation(), page.click()]) - Set viewport dimensions explicitly for consistent rendering across environments. Use
.page.setViewport({ width: 1920, height: 1080 }) - Implement request interception to mock APIs, block heavy resources, and test error scenarios in isolation.
- Use
for complex DOM queries that are easier to express as browser-side JavaScript rather than chaining Puppeteer methods.page.evaluate() - Store authentication state (cookies, localStorage) and restore it in subsequent tests to avoid redundant login flows.
- Configure
anddefaultTimeout
at the page level rather than passing timeouts to every individual call.defaultNavigationTimeout - Use
after dynamic content loads to ensure all API calls have completed before making assertions.page.waitForNetworkIdle() - Generate screenshots on test failure using try/catch or test framework hooks for visual debugging of failed assertions.
Anti-Patterns
- Using
-- Static delays make tests slow and unreliable. Wait for specific conditions instead.page.waitForTimeout() - Not closing browser instances -- Leaked processes accumulate and crash CI servers. Always clean up in teardown hooks.
- Hardcoding URLs and credentials -- Use environment variables and configuration files for all environment-specific values.
- Running headed browsers in CI -- Headed mode requires a display server and is slower. Always use headless mode in automated pipelines.
- Ignoring
-- Uncaught JavaScript errors on the page often indicate real bugs. Log and optionally fail tests on page errors.page.on('pageerror') - Using
without waiting -- Clicking elements before they are visible or clickable causes intermittent failures.page.click() - Not setting the viewport -- Different default viewport sizes across environments cause inconsistent test results and layout differences.
- Blocking on
without timeout -- Pages that never fully load can hang tests indefinitely. Always setpage.goto()
and use timeouts.waitUntil - Scraping without rate limiting -- Sending rapid requests without delays can trigger rate limiting or IP bans. Add respectful delays for scraping use cases.
- Using XPath for simple selectors -- XPath is slower and harder to read than CSS selectors. Only use XPath when CSS selectors cannot express the query.
CLI Reference
# Run Puppeteer tests with Jest npx jest --config jest.puppeteer.config.ts # Run specific test file npx jest tests/e2e/auth.test.ts # Run in headed mode for debugging HEADLESS=false npx jest tests/e2e/auth.test.ts # Run with verbose output npx jest --verbose tests/e2e/ # Generate coverage report npx jest --coverage tests/
Setup
# Install Puppeteer (includes bundled Chromium) npm install --save-dev puppeteer # For lighter installs (bring your own Chrome) npm install --save-dev puppeteer-core # With Jest integration npm install --save-dev jest ts-jest @types/jest puppeteer # For TypeScript support npm install --save-dev typescript ts-node @types/node