manus-skills/.kortix/opencode/skills/coding/puppeteer-skill/reference/playbook.md
zhukang 1c7abd81d1 All 310 Manus skill-hub community skills as a Kortix marketplace registry
- 310 registry:skill items, 1528 files, 7 categories (hub tags)
- registry.json generated by @kortix/registry buildRegistry
- nested SKILL.md in sn-da-* renamed SUBSKILL.md (no phantom skills)
- provenance + licenses: SOURCES.md
2026-09-15 13:37:20 +08:00

14 KiB

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

Problem Cause Fix
page.click() fails Element hidden or overlapped Use JS click: page.evaluate(el => el.click(), elem)
Navigation timeout SPA doesn't trigger navigation Use waitForSelector instead of waitForNavigation
Element not found DOM not ready await page.waitForSelector() before interaction
Memory leak Unclosed pages or browsers Close pages: page.close(), browser: browser.close()
Headless rendering differs Missing GPU/fonts Use --force-device-scale-factor=1, fixed viewport
Request interception errors Multiple handlers conflict Use single interception handler with routing
iFrame content not accessible Wrong frame reference Use frame.waitForSelector(), not page.waitForSelector()
File upload fails No file chooser triggered Use page.waitForFileChooser() before click
Dialog not handled Event listener not set Register page.on('dialog') before triggering action
Slow in CI Resource loading Block images/fonts, use networkidle0
Screenshot blank Page not loaded Wait 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/