← All skills

Puppeteer Skill

E2e testingJavaScriptTypeScript

Copy and Paste in your Terminal

npx skills add https://github.com/LambdaTest/agent-skills.git --skill puppeteer-skill

Playbook

Complete implementation guide with code samples, patterns, and best practices.

Puppeteer — Advanced Implementation Playbook

§1 — Production Setup & Configuration

// puppeteer.config.ts
import puppeteer, { Browser, Page, LaunchOptions } from 'puppeteer';

const defaultLaunchOptions: LaunchOptions = {
  headless: process.env.CI ? 'new' : false,
  args: [
    '--no-sandbox',
    '--disable-setuid-sandbox',
    '--disable-dev-shm-usage',
    '--disable-gpu',
    '--window-size=1920,1080',
    '--force-device-scale-factor=1',
  ],
  defaultViewport: { width: 1920, height: 1080 },
  slowMo: parseInt(process.env.SLOW_MO || '0'),
  timeout: 30000,
};

export async function createBrowser(overrides: Partial<LaunchOptions> = {}): Promise<Browser> {
  return puppeteer.launch({ ...defaultLaunchOptions, ...overrides });
}

export async function createPage(browser: Browser): Promise<Page> {
  const page = await browser.newPage();

  // Console & error logging
  page.on('console', msg => {
    if (msg.type() === 'error') console.error('[PAGE ERROR]', msg.text());
  });
  page.on('pageerror', err => console.error('[UNCAUGHT]', err.message));
  page.on('requestfailed', req =>
    console.error('[REQUEST FAILED]', req.url(), req.failure()?.errorText));

  // Default timeouts
  await page.setDefaultNavigationTimeout(30000);
  await page.setDefaultTimeout(15000);

  // Block unnecessary resources in CI
  if (process.env.CI) {
    await page.setRequestInterception(true);
    page.on('request', req => {
      const type = req.resourceType();
      if (['image', 'font', 'media'].includes(type)) req.abort();
      else req.continue();
    });
  }

  return page;
}

Jest + Puppeteer Integration

// jest-puppeteer.config.js
module.exports = {
  launch: {
    headless: process.env.CI ? 'new' : false,
    args: ['--no-sandbox', '--disable-setuid-sandbox'],
    defaultViewport: { width: 1920, height: 1080 },
  },
  server: {
    command: 'npm start',
    port: 3000,
    launchTimeout: 30000,
    debug: true,
  },
};

// jest.config.js
module.exports = {
  preset: 'jest-puppeteer',
  testMatch: ['**/tests/**/*.test.ts'],
  transform: { '^.+\\.tsx?$': 'ts-jest' },
  testTimeout: 60000,
};

§2 — Page Object Pattern

class BasePage {
  constructor(protected page: Page) {}

  async goto(path: string) {
    await this.page.goto(`${process.env.BASE_URL || 'http://localhost:3000'}${path}`, {
      waitUntil: 'networkidle0',
    });
  }

  async waitAndClick(selector: string) {
    await this.page.waitForSelector(selector, { visible: true });
    await this.page.click(selector);
  }

  async waitAndType(selector: string, text: string) {
    await this.page.waitForSelector(selector, { visible: true });
    await this.page.click(selector, { clickCount: 3 });  // select all
    await this.page.type(selector, text);
  }

  async getText(selector: string): Promise<string> {
    await this.page.waitForSelector(selector);
    return this.page.$eval(selector, el => el.textContent?.trim() || '');
  }

  async isVisible(selector: string): Promise<boolean> {
    try {
      await this.page.waitForSelector(selector, { visible: true, timeout: 3000 });
      return true;
    } catch { return false; }
  }

  async screenshot(name: string) {
    await this.page.screenshot({ path: `screenshots/${name}.png`, fullPage: true });
  }

  async waitForNetworkIdle() {
    await this.page.waitForNetworkIdle({ idleTime: 500, timeout: 15000 });
  }
}

class LoginPage extends BasePage {
  async open() { await this.goto('/login'); }

  async login(email: string, password: string) {
    await this.waitAndType('[data-testid="email"]', email);
    await this.waitAndType('[data-testid="password"]', password);
    await this.waitAndClick('[data-testid="submit"]');
    await this.page.waitForNavigation({ waitUntil: 'networkidle0' });
    return new DashboardPage(this.page);
  }

  async getError(): Promise<string> {
    return this.getText('.error-message');
  }
}

class DashboardPage extends BasePage {
  async isLoaded(): Promise<boolean> {
    return this.isVisible('[data-testid="dashboard"]');
  }

  async getWelcomeText(): Promise<string> {
    return this.getText('[data-testid="welcome-msg"]');
  }
}

§3 — Network Interception & Mocking

// Full request interception
async function setupMocks(page: Page) {
  await page.setRequestInterception(true);

  const mocks: Map<string, any> = new Map([
    ['/api/products', { status: 200, data: [{ id: 1, name: 'Mock Product' }] }],
    ['/api/user', { status: 200, data: { name: 'Test User', role: 'admin' } }],
  ]);

  page.on('request', req => {
    const mockEntry = [...mocks.entries()].find(([url]) => req.url().includes(url));
    if (mockEntry) {
      req.respond({
        status: mockEntry[1].status,
        contentType: 'application/json',
        body: JSON.stringify(mockEntry[1].data),
      });
    } else if (req.resourceType() === 'image') {
      req.abort();
    } else {
      req.continue();
    }
  });
}

// Capture API responses
async function captureApiResponse(page: Page, urlPattern: string): Promise<any> {
  const response = await page.waitForResponse(
    r => r.url().includes(urlPattern) && r.status() === 200
  );
  return response.json();
}

// Block specific domains
page.on('request', req => {
  const blockedDomains = ['analytics.google.com', 'ads.example.com', 'tracking.io'];
  if (blockedDomains.some(d => req.url().includes(d))) req.abort();
  else req.continue();
});

§4 — Wait Strategies

// Wait for DOM element
await page.waitForSelector('.loaded', { visible: true, timeout: 15000 });

// Wait for element to disappear
await page.waitForSelector('.loading-spinner', { hidden: true });

// Wait for custom condition
await page.waitForFunction(
  () => document.querySelectorAll('.item').length > 5,
  { timeout: 10000 }
);

// Wait for network idle after action
await Promise.all([
  page.waitForNetworkIdle({ idleTime: 500 }),
  page.click('#load-more'),
]);

// Wait for navigation after click
await Promise.all([
  page.waitForNavigation({ waitUntil: 'networkidle0' }),
  page.click('a.next-page'),
]);

// Wait for specific API response
const [response] = await Promise.all([
  page.waitForResponse(r => r.url().includes('/api/data') && r.status() === 200),
  page.click('#fetch-data'),
]);
const data = await response.json();

// Wait for frame
const frame = await page.waitForFrame(f => f.url().includes('payment'));

// Retry with polling
async function waitForCondition(page: Page, fn: () => Promise<boolean>, timeout = 10000) {
  const start = Date.now();
  while (Date.now() - start < timeout) {
    if (await fn()) return;
    await new Promise(r => setTimeout(r, 250));
  }
  throw new Error('Condition not met within timeout');
}

§5 — Screenshots, PDF & Media

// Full page screenshot
await page.screenshot({ path: 'full.png', fullPage: true });

// Element screenshot
const element = await page.$('[data-testid="chart"]');
await element?.screenshot({ path: 'chart.png' });

// Clip region
await page.screenshot({
  path: 'region.png',
  clip: { x: 0, y: 0, width: 800, height: 600 },
});

// PDF generation
await page.pdf({
  path: 'report.pdf',
  format: 'A4',
  printBackground: true,
  margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' },
  displayHeaderFooter: true,
  headerTemplate: '<span style="font-size:10px">Report</span>',
  footerTemplate: '<span style="font-size:10px">Page <span class="pageNumber"></span>/<span class="totalPages"></span></span>',
});

// Video recording (via screen capture)
const recorder = await page.screencast({ path: 'recording.webm' });
// ... run test actions ...
await recorder.stop();

// Emulate devices
const iPhone = puppeteer.KnownDevices['iPhone 15'];
await page.emulate(iPhone);

§6 — Authentication & Cookies

// Cookie-based auth
async function loginViaApi(page: Page, email: string, password: string) {
  const response = await fetch(`${BASE_URL}/api/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  });
  const { token } = await response.json();

  await page.setCookie({
    name: 'auth_token',
    value: token,
    domain: 'localhost',
    path: '/',
    httpOnly: true,
    secure: false,
  });
}

// Save and restore session
async function saveSession(page: Page, path: string) {
  const cookies = await page.cookies();
  const localStorage = await page.evaluate(() => JSON.stringify(window.localStorage));
  fs.writeFileSync(path, JSON.stringify({ cookies, localStorage }));
}

async function restoreSession(page: Page, path: string) {
  const { cookies, localStorage } = JSON.parse(fs.readFileSync(path, 'utf8'));
  await page.setCookie(...cookies);
  await page.evaluate(data => {
    Object.entries(JSON.parse(data)).forEach(([k, v]) =>
      window.localStorage.setItem(k, v as string));
  }, localStorage);
}

§7 — iFrame, Dialog & File Operations

// iFrame interaction
const frameElement = await page.waitForSelector('iframe#payment');
const frame = await frameElement?.contentFrame();
if (frame) {
  await frame.type('#card-number', '4111111111111111');
  await frame.type('#expiry', '12/28');
  await frame.click('#pay-btn');
}

// Dialog handling (alert, confirm, prompt)
page.on('dialog', async dialog => {
  console.log('Dialog:', dialog.type(), dialog.message());
  if (dialog.type() === 'confirm') await dialog.accept();
  else if (dialog.type() === 'prompt') await dialog.accept('user input');
  else await dialog.dismiss();
});

// File upload
const [fileChooser] = await Promise.all([
  page.waitForFileChooser(),
  page.click('#upload-btn'),
]);
await fileChooser.accept(['./fixtures/test.pdf']);

// File download
const client = await page.target().createCDPSession();
await client.send('Page.setDownloadBehavior', {
  behavior: 'allow',
  downloadPath: './downloads',
});
await page.click('#download-btn');

§8 — Performance & Metrics

// Core Web Vitals
const metrics = await page.metrics();
console.log('JSHeapUsed:', metrics.JSHeapUsedSize);

const timing = await page.evaluate(() => {
  const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
  return {
    domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
    load: nav.loadEventEnd - nav.startTime,
    ttfb: nav.responseStart - nav.requestStart,
    fcp: performance.getEntriesByName('first-contentful-paint')[0]?.startTime,
  };
});

// Lighthouse via puppeteer
const { startFlow } = require('lighthouse');
const flow = await startFlow(page, { name: 'User Flow' });
await flow.navigate(url);
await flow.snapshot({ stepName: 'Homepage loaded' });
const report = await flow.generateReport();

// Coverage (CSS/JS)
await page.coverage.startCSSCoverage();
await page.coverage.startJSCoverage();
await page.goto(url);
const [cssCoverage, jsCoverage] = await Promise.all([
  page.coverage.stopCSSCoverage(),
  page.coverage.stopJSCoverage(),
]);
const unusedCSS = cssCoverage.reduce((acc, entry) => {
  const unused = entry.ranges.reduce((s, r) => s + r.end - r.start, 0);
  return acc + (entry.text.length - unused);
}, 0);

§9 — Accessibility Testing

import { AxePuppeteer } from '@axe-core/puppeteer';

it('should have no accessibility violations', async () => {
  await page.goto('/');
  const results = await new AxePuppeteer(page)
    .withTags(['wcag2a', 'wcag2aa'])
    .analyze();
  expect(results.violations).toEqual([]);
});

§10 — CI/CD Integration

# GitHub Actions
name: Puppeteer Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npx jest --runInBand
        env: { CI: true, BASE_URL: 'http://localhost:3000' }
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: screenshots
          path: screenshots/
# Docker for consistent environment
FROM node:20-slim
RUN apt-get update && apt-get install -y chromium
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "jest", "--runInBand"]

§11 — Debugging Quick-Reference

ProblemCauseFix
page.click() failsElement hidden or overlappedUse JS click: page.evaluate(el => el.click(), elem)
Navigation timeoutSPA doesn't trigger navigationUse waitForSelector instead of waitForNavigation
Element not foundDOM not readyawait page.waitForSelector() before interaction
Memory leakUnclosed pages or browsersClose pages: page.close(), browser: browser.close()
Headless rendering differsMissing GPU/fontsUse --force-device-scale-factor=1, fixed viewport
Request interception errorsMultiple handlers conflictUse single interception handler with routing
iFrame content not accessibleWrong frame referenceUse frame.waitForSelector(), not page.waitForSelector()
File upload failsNo file chooser triggeredUse page.waitForFileChooser() before click
Dialog not handledEvent listener not setRegister page.on('dialog') before triggering action
Slow in CIResource loadingBlock images/fonts, use networkidle0
Screenshot blankPage not loadedWait for networkidle0 or specific selector

§12 — Best Practices Checklist

  • ✅ Always close browser in finally or afterAll block
  • ✅ Use page.waitForNetworkIdle() over arbitrary setTimeout
  • ✅ Use page.setRequestInterception() to mock APIs and block resources
  • ✅ Set defaultViewport for consistent screenshots
  • ✅ Limit concurrent pages to control memory
  • ✅ Use page.on('console') and page.on('pageerror') for error capture
  • ✅ Use Page Object pattern for maintainable tests
  • ✅ Use Promise.all([waitFor..., click()]) for navigation + action
  • ✅ Save/restore cookies for faster auth across tests
  • ✅ Use data-testid attributes for stable selectors
  • ✅ Run headless in CI with headless: 'new'
  • ✅ Use @axe-core/puppeteer for accessibility testing
  • ✅ Structure: tests/, pages/, fixtures/, helpers/, screenshots/