Puppeteer: Automatización de Navegador, Testing y Web Scraping

La guía completa para controlar Chrome headless programáticamente. Desde capturas simples hasta testing E2E complejo, auditorías de rendimiento con Lighthouse y web scraping de grado producción. Patrones reales de automatizar 348 exportaciones de capturas e integración CI/CD.

PuppeteerHeadless ChromeCDPLighthouseJestpuppeteer-extraDockerGitHub ActionsPlaywright

1. ¿Qué es Puppeteer?

Puppeteer es una biblioteca Node.js desarrollada por el equipo de Chrome DevTools que provee una API de alto nivel para controlar navegadores Chrome o Chromium programáticamente. Lanza una instancia del navegador (headless por defecto, pero opcionalmente con UI visible), y te da control completo: navegar URLs, hacer clic en botones, llenar formularios, tomar capturas, generar PDFs, interceptar peticiones de red y ejecutar JavaScript en el contexto de la página.

Puppeteer se comunica con el navegador vía el Chrome DevTools Protocol (CDP), el mismo protocolo usado por Chrome DevTools. Esto significa que cualquier cosa que puedas hacer manualmente en un navegador, Puppeteer lo puede automatizar programáticamente. Es el estándar de facto para automatización de navegador headless en el ecosistema Node.js.

Ventajas clave: Puppeteer viene con un binario Chromium incluido (configuración cero), soporta las últimas funcionalidades de Chrome inmediatamente, provee acceso nativo a CDP para debugging avanzado y se integra con Lighthouse para auditorías de rendimiento. La versión actual es v25.3.0 (julio de 2026). Desde v23, WebDriver BiDi está production-ready y es ahora el protocolo por defecto para conexiones con Firefox. CDP sigue siendo el default para Chrome.

2. Características Principales

CORE

Control de Chrome Headless

Lanza Chrome en modo headless o con interfaz visible. Controla el ciclo de vida del navegador, crea múltiples páginas/pestañas, gestiona cookies y sesiones, configura tamaños de viewport, emula dispositivos y configura proxy.

CORE

Navegación e Interacción de Páginas

Navega con condiciones de espera configurables. Haz clic, escribe, selecciona, arrastra, hover y maneja eventos de teclado/mouse. Espera por elementos, selectores o condiciones personalizadas.

CAPTURE

Capturas de Pantalla y Generación de PDF

Capturas de página completa, a nivel de elemento o de viewport en PNG/JPEG/WebP. Genera PDFs con tamaños personalizados, márgenes, encabezados y pies de página. Regiones de recorte, configuración de calidad, fondos transparentes.

FORM

Llenado y Envío de Formularios

Escribe con timing realista entre teclas, selecciona dropdowns, marca/desmarca checkboxes, sube archivos y envía. Maneja formularios multi-paso, CAPTCHAs y validación dinámica.

NET

Intercepción de Red

Intercepta, modifica o bloquea peticiones y respuestas. Mockea APIs para testing, bloquea anuncios para scraping, modifica headers para auth y registra toda la actividad de red.

TEST

Testing End-to-End

Construye suites de tests E2E con interacción real del navegador. Combina con Jest, Mocha o cualquier framework de tests. Assert sobre contenido, estado visual, red, consola y errores JS.

PERF

Testing de Rendimiento (Lighthouse)

Auditorías Lighthouse automatizadas midiendo Core Web Vitals, accesibilidad, SEO y mejores prácticas. Ejecuta en CI/CD para detectar regresiones de rendimiento.

SCRAPE

Web Scraping

Extrae datos de páginas renderizadas con JavaScript. Maneja scroll infinito, lazy loading, paginación y contenido dinámico. Rate limiting y prácticas de scraping responsable.

CORE

Ejecución de JavaScript

Ejecuta JS arbitrario en el contexto de la página. Accede y modifica el DOM, llama funciones de la página, extrae datos de variables JS, inyecta scripts e interactúa con frameworks del lado del cliente.

ENV

Emulación de Dispositivos y Red

Emula dispositivos móviles, viewports personalizados, redes lentas, geolocalización, zona horaria y preferencias de esquema de color. 50+ descriptores de dispositivos integrados.

ENV

Docker e Integración CI/CD

Ejecuta en Docker con Chromium preconfigurado. Imágenes oficiales disponibles. Integra con GitHub Actions, GitLab CI, Jenkins. El modo headless elimina requisitos de display.

CAPTURE

Tracing y Cobertura

Graba traces de Chrome para análisis de rendimiento. Mide cobertura de código JS y CSS. Captura archivos HAR. Integración CDP nativa para profiling avanzado.

3. Cómo Lo Uso

Mi proyecto más intensivo con Puppeteer fue el desafío de exportación de capturas de la app Xiaomi. La app Xiaomi Health almacena reportes de composición corporal que no se pueden exportar. Construye una automatización con Puppeteer que navegó la versión web, se autenticó, se desplazó por 348 reportes individuales, capturó capturas de alta resolución y las organizó por fecha. Completado en menos de 2 horas.

Integro Puppeteer con Lighthouse en pipelines CI/CD para testing de regresión de rendimiento. Cada PR dispara una auditoría Lighthouse. Si alguna métrica cae bajo el umbral, el pipeline falla. Esto ha prevenido docenas de regresiones de llegar a producción.

Para el dashboard de salud de este sitio web, uso Puppeteer para validación visual automatizada. Trabajos programados capturan capturas, las comparan contra líneas base y alertan sobre cambios visuales.

Puppeteer también potencia la automatización del navegador en mi despliegue de OpenClaw. Cuando mi asistente AI necesita interactuar con aplicaciones web, delega a Puppeteer.

Experiencia de Jose: 348 reportes de composición corporal de Xiaomi exportados automáticamente, Lighthouse CI asegurando presupuestos de rendimiento en cada PR, y testing de regresión visual protegiendo el dashboard de salud de roturas silenciosas.

4. Primeros Pasos

Puppeteer se instala con un solo comando npm y trae su propio binario Chromium incluido. Cero configuración externa requerida.

# Install Puppeteer (downloads Chromium automatically)
npm install puppeteer

# Or install without bundled Chromium (use system Chrome)
npm install puppeteer-core

# Verify installation
node -e "import('puppeteer').then(p => p.default.launch().then(b => { console.log('OK'); b.close(); }))"

6. Capturas de Pantalla y Generación de PDF

Puppeteer soporta capturas de página completa, capturas a nivel de elemento, regiones de recorte y múltiples formatos de salida. La generación de PDF incluye tamaños de página personalizados, márgenes, encabezados, pies de página y CSS específico de impresión.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080 });
await page.goto('https://example.com', { waitUntil: 'networkidle0' });

// Full-page screenshot (captures entire scrollable area)
await page.screenshot({
  path: 'full-page.png',
  fullPage: true
});

// Viewport screenshot (only visible area)
await page.screenshot({
  path: 'viewport.png',
  fullPage: false
});

// Element-level screenshot
const element = await page.$('.hero-section');
await element.screenshot({ path: 'hero.png' });

// Screenshot with clip region and quality settings
await page.screenshot({
  path: 'clipped.jpeg',
  type: 'jpeg',
  quality: 85,
  clip: { x: 0, y: 0, width: 800, height: 600 }
});

// WebP format with transparent background
await page.screenshot({
  path: 'transparent.webp',
  type: 'webp',
  omitBackground: true
});

// PDF with custom layout
await page.pdf({
  path: 'document.pdf',
  format: 'A4',
  printBackground: true,
  margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
  displayHeaderFooter: true,
  headerTemplate: '<div style="font-size:10px;text-align:center;width:100%">Report</div>',
  footerTemplate: '<div style="font-size:10px;text-align:center;width:100%">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>'
});

// Batch screenshots (used for my 348 Xiaomi exports)
const reportUrls = getReportUrls(); // array of URLs
for (const [i, url] of reportUrls.entries()) {
  await page.goto(url, { waitUntil: 'networkidle0' });
  await page.screenshot({
    path: `reports/report-${String(i).padStart(4, '0')}.png`,
    fullPage: true
  });
}

await browser.close();

7. Llenado de Formularios e Interacción

Puppeteer puede simular interacciones de usuario realistas incluyendo escritura con delays entre teclas, selección de opciones de dropdown, subida de archivos y manejo de formularios multi-paso con validación dinámica.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://example.com/login');

// Type with realistic delay between keystrokes
await page.type('#email', '[email protected]', { delay: 50 });
await page.type('#password', 'secure-password', { delay: 50 });

// Click submit and wait for navigation
await Promise.all([
  page.waitForNavigation({ waitUntil: 'networkidle0' }),
  page.click('#submit-btn')
]);

// Select dropdown option
await page.select('#country', 'CO');

// Check/uncheck checkboxes
await page.click('#terms-checkbox');

// Upload a file
const fileInput = await page.$('input[type="file"]');
await fileInput.uploadFile('/path/to/document.pdf');

// Clear an input field before typing
await page.click('#search', { clickCount: 3 });
await page.type('#search', 'new search term');

// Handle a multi-step form
await page.type('#step1-name', 'Jose Nobile');
await page.click('#next-step');
await page.waitForSelector('#step2-address', { visible: true });
await page.type('#step2-address', '123 Main St');
await page.click('#next-step');
await page.waitForSelector('#step3-confirm', { visible: true });
await page.click('#submit-final');

// Verify success
const welcomeText = await page.$eval(
  '.welcome-message',
  el => el.textContent
);
console.log('Logged in:', welcomeText);

await browser.close();

8. Intercepción de Red (Request y Response)

Intercepta y modifica peticiones de red para mockear APIs, bloquear recursos, inyectar headers o registrar tráfico. Puppeteer también soporta intercepción de respuestas. Esencial para testing, scraping y debugging.

// Request interception: block, modify, or log
await page.setRequestInterception(true);

page.on('request', (request) => {
  // Block images and stylesheets for faster scraping
  if (['image', 'stylesheet', 'font'].includes(
    request.resourceType()
  )) {
    request.abort();
    return;
  }

  // Add auth header to API requests
  if (request.url().includes('/api/')) {
    request.continue({
      headers: {
        ...request.headers(),
        'Authorization': 'Bearer ' + token
      }
    });
    return;
  }

  request.continue();
});

// Response interception: log and inspect responses
page.on('response', async (response) => {
  const url = response.url();
  if (url.includes('/api/')) {
    console.log(`${response.status()} ${url}`);
    try {
      const body = await response.json();
      console.log('Response body:', JSON.stringify(body).slice(0, 200));
    } catch {
      // Response was not JSON
    }
  }
});

// Mock an API endpoint entirely
await page.setRequestInterception(true);
page.on('request', (request) => {
  if (request.url().includes('/api/user/profile')) {
    request.respond({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        name: 'Test User',
        email: '[email protected]',
        plan: 'premium'
      })
    });
    return;
  }
  request.continue();
});

// Capture all network requests as a HAR-like log
const networkLog = [];
page.on('request', (req) => {
  networkLog.push({
    url: req.url(),
    method: req.method(),
    resourceType: req.resourceType(),
    timestamp: Date.now()
  });
});

page.on('response', (res) => {
  const entry = networkLog.find(e => e.url === res.url());
  if (entry) {
    entry.status = res.status();
    entry.duration = Date.now() - entry.timestamp;
  }
});

9. Testing End-to-End

Construye suites de tests E2E completas que ejercitan tu aplicación a través de un navegador real. Puppeteer se integra con Jest, Mocha o cualquier framework de tests de Node.js. Assert sobre contenido, estado visual, actividad de red, salida de consola y errores JavaScript.

// e2e.test.js (with Jest)
import puppeteer from 'puppeteer';

let browser, page;

beforeAll(async () => {
  browser = await puppeteer.launch();
  page = await browser.newPage();

  // Capture console errors
  page.on('console', msg => {
    if (msg.type() === 'error') {
      console.error('PAGE ERROR:', msg.text());
    }
  });

  // Capture uncaught exceptions
  page.on('pageerror', err => {
    console.error('UNCAUGHT:', err.message);
  });
});

afterAll(async () => {
  await browser.close();
});

describe('Login flow', () => {
  test('should login with valid credentials', async () => {
    await page.goto('http://localhost:3000/login');
    await page.type('#email', '[email protected]');
    await page.type('#password', 'valid-password');

    await Promise.all([
      page.waitForNavigation(),
      page.click('#login-btn')
    ]);

    const url = page.url();
    expect(url).toContain('/dashboard');

    const welcome = await page.$eval('h1', el => el.textContent);
    expect(welcome).toContain('Welcome');
  });

  test('should show error for invalid credentials', async () => {
    await page.goto('http://localhost:3000/login');
    await page.type('#email', '[email protected]');
    await page.type('#password', 'wrong');
    await page.click('#login-btn');

    await page.waitForSelector('.error-message', { visible: true });
    const error = await page.$eval('.error-message', el => el.textContent);
    expect(error).toContain('Invalid');
  });

  test('should pass visual regression', async () => {
    await page.goto('http://localhost:3000/dashboard');
    const screenshot = await page.screenshot();
    expect(screenshot).toMatchImageSnapshot({
      failureThreshold: 0.01,
      failureThresholdType: 'percent'
    });
  });
});

10. Integración con Lighthouse (API Programática)

Ejecuta auditorías Lighthouse programáticamente en tu pipeline CI/CD. Configura presupuestos de rendimiento y falla builds que los excedan. Combina con navegación de Puppeteer para auditar páginas autenticadas.

// lighthouse-audit.js
import puppeteer from 'puppeteer';
import lighthouse from 'lighthouse';

const browser = await puppeteer.launch({
  args: ['--remote-debugging-port=9222']
});

// Optionally navigate to authenticated pages first
const page = await browser.newPage();
await page.goto('https://mysite.com/login');
await page.type('#email', process.env.TEST_EMAIL);
await page.type('#password', process.env.TEST_PASSWORD);
await Promise.all([
  page.waitForNavigation(),
  page.click('#login-btn')
]);
await page.close();

// Run Lighthouse audit on the authenticated session
const result = await lighthouse('https://mysite.com/dashboard', {
  port: 9222,
  output: 'json',
  onlyCategories: ['performance', 'accessibility', 'seo', 'best-practices'],
  settings: {
    formFactor: 'desktop',
    screenEmulation: { disabled: true },
    throttling: {
      rttMs: 40,
      throughputKbps: 10240,
      cpuSlowdownMultiplier: 1,
    },
  },
});

const { lhr } = result;
const scores = {
  performance: lhr.categories.performance.score * 100,
  accessibility: lhr.categories.accessibility.score * 100,
  seo: lhr.categories.seo.score * 100,
  bestPractices: lhr.categories['best-practices'].score * 100,
  lcp: lhr.audits['largest-contentful-paint'].numericValue,
  cls: lhr.audits['cumulative-layout-shift'].numericValue,
  tbt: lhr.audits['total-blocking-time'].numericValue,
};

console.table(scores);

// Fail CI if below threshold
const THRESHOLD = 90;
for (const [key, value] of Object.entries(scores)) {
  if (['performance', 'accessibility', 'seo', 'bestPractices'].includes(key)
      && value < THRESHOLD) {
    console.error(`FAIL: ${key} = ${value} (threshold: ${THRESHOLD})`);
    process.exit(1);
  }
}

await browser.close();
Experiencia de Jose: Ejecuto Lighthouse CI en cada pull request para josenobile.co, manteniendo scores 100/100/100/100. La API programática me permite auditar páginas autenticadas como el dashboard de salud que el CLI no puede alcanzar sin un paso de login.

11. Web Scraping (Paginación y Scroll Infinito)

Puppeteer sobresale en el scraping de páginas renderizadas con JavaScript que clientes HTTP estáticos no pueden manejar. Soporta paginación, scroll infinito, contenido lazy-loaded y scraping autenticado. Siempre respeta robots.txt e implementa rate limiting.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();

// --- Pagination scraping ---
const allProducts = [];
let currentPage = 1;
const maxPages = 20;

while (currentPage <= maxPages) {
  await page.goto(`https://example.com/products?page=${currentPage}`, {
    waitUntil: 'networkidle0'
  });

  const products = await page.$$eval('.product-card', cards =>
    cards.map(card => ({
      name: card.querySelector('.name')?.textContent?.trim(),
      price: card.querySelector('.price')?.textContent?.trim(),
      url: card.querySelector('a')?.href
    }))
  );

  if (products.length === 0) break;
  allProducts.push(...products);
  currentPage++;

  // Rate limiting: wait between requests
  await new Promise(r => setTimeout(r, 1500));
}

// --- Infinite scroll scraping ---
await page.goto('https://example.com/feed', {
  waitUntil: 'networkidle0'
});

let previousHeight = 0;
let scrollAttempts = 0;
const maxScrolls = 50;

while (scrollAttempts < maxScrolls) {
  // Scroll to bottom
  await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));

  // Wait for new content to load
  await new Promise(r => setTimeout(r, 2000));
  await page.waitForFunction(
    `document.body.scrollHeight > ${previousHeight}`,
    { timeout: 5000 }
  ).catch(() => null);

  const newHeight = await page.evaluate(() => document.body.scrollHeight);
  if (newHeight === previousHeight) break;

  previousHeight = newHeight;
  scrollAttempts++;
}

// Extract all loaded items
const feedItems = await page.$$eval('.feed-item', items =>
  items.map(item => ({
    title: item.querySelector('h2')?.textContent?.trim(),
    content: item.querySelector('.body')?.textContent?.trim(),
    date: item.querySelector('time')?.getAttribute('datetime')
  }))
);

console.log(`Scraped ${feedItems.length} items`);
await browser.close();

12. Gestión de Cookies y Sesiones

Puppeteer provee control completo sobre cookies y almacenamiento del navegador. Guarda y restaura sesiones para evitar re-autenticación, comparte cookies entre páginas y gestiona almacenamiento entre contextos de navegador para aislamiento.

import puppeteer from 'puppeteer';
import { writeFileSync, readFileSync, existsSync } from 'fs';

const COOKIES_FILE = './session-cookies.json';

const browser = await puppeteer.launch();
const page = await browser.newPage();

// Restore cookies from a previous session
if (existsSync(COOKIES_FILE)) {
  const cookies = JSON.parse(readFileSync(COOKIES_FILE, 'utf-8'));
  await page.setCookie(...cookies);
  console.log('Session restored from cookies file');
}

await page.goto('https://example.com/dashboard');

// Check if session is still valid
const isLoggedIn = await page.evaluate(
  () => !!document.querySelector('.user-profile')
);

if (!isLoggedIn) {
  // Re-authenticate
  await page.goto('https://example.com/login');
  await page.type('#email', '[email protected]');
  await page.type('#password', 'password');
  await Promise.all([
    page.waitForNavigation(),
    page.click('#login-btn')
  ]);
}

// Save cookies for next run
const cookies = await page.cookies();
writeFileSync(COOKIES_FILE, JSON.stringify(cookies, null, 2));

// Manage localStorage and sessionStorage
await page.evaluate(() => {
  localStorage.setItem('theme', 'dark');
  localStorage.setItem('language', 'en-US');
});

// Use incognito context for isolated sessions
const context = await browser.createBrowserContext();
const privatePage = await context.newPage();
// This page has no cookies or storage from the main context
await privatePage.goto('https://example.com');
await context.close();

await browser.close();

13. Emulación de Dispositivos

Puppeteer incluye descriptores de dispositivo integrados para 50+ dispositivos. Emula viewports móviles, eventos táctiles, ratios de píxeles, user agents, redes lentas, geolocalización, zona horaria y preferencias de esquema de color.

import puppeteer, { KnownDevices } from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();

// Emulate a specific device
const iPhone14 = KnownDevices['iPhone 14 Pro Max'];
await page.emulate(iPhone14);
await page.goto('https://example.com');
await page.screenshot({ path: 'iphone14.png' });

// Emulate Pixel 5
const pixel5 = KnownDevices['Pixel 5'];
await page.emulate(pixel5);
await page.goto('https://example.com');
await page.screenshot({ path: 'pixel5.png' });

// Custom viewport with device pixel ratio
await page.setViewport({
  width: 375,
  height: 812,
  deviceScaleFactor: 3,
  isMobile: true,
  hasTouch: true
});

// Simulate slow 3G network
const client = await page.createCDPSession();
await client.send('Network.emulateNetworkConditions', {
  offline: false,
  downloadThroughput: (500 * 1024) / 8,  // 500 Kbps
  uploadThroughput: (500 * 1024) / 8,
  latency: 400
});

// Set geolocation (Bogota, Colombia)
await page.setGeolocation({
  latitude: 4.7110,
  longitude: -74.0721
});

// Set timezone
await page.emulateTimezone('America/Bogota');

// Emulate dark mode
await page.emulateMediaFeatures([
  { name: 'prefers-color-scheme', value: 'dark' }
]);
await page.screenshot({ path: 'dark-mode.png' });

// Emulate light mode
await page.emulateMediaFeatures([
  { name: 'prefers-color-scheme', value: 'light' }
]);
await page.screenshot({ path: 'light-mode.png' });

await browser.close();

14. Ejecución de JavaScript en Contexto de Página

Puppeteer te permite ejecutar JavaScript arbitrario en el contexto de la página, conectando Node.js con el navegador. Usa page.evaluate() para ejecución puntual, page.exposeFunction() para llamar Node.js desde el navegador, y page.evaluateHandle() para referencias del DOM.page.evaluate() for one-off execution, page.exposeFunction() to call Node.js functions from the browser, and page.evaluateHandle() for DOM object references.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');

// Execute JS and return a primitive value
const title = await page.evaluate(() => document.title);
const linkCount = await page.evaluate(
  () => document.querySelectorAll('a').length
);

// Pass arguments from Node.js to page context
const selector = '.article';
const articles = await page.evaluate((sel) => {
  return Array.from(document.querySelectorAll(sel)).map(el => ({
    title: el.querySelector('h2')?.textContent,
    href: el.querySelector('a')?.href
  }));
}, selector);

// Access framework state (React example)
const reactState = await page.evaluate(() => {
  const fiber = document.querySelector('#app')._reactRootContainer
    ?._internalRoot?.current;
  return fiber?.memoizedState;
});

// Expose a Node.js function to the page
await page.exposeFunction('saveToFile', async (data) => {
  const { writeFileSync } = await import('fs');
  writeFileSync('scraped-data.json', JSON.stringify(data));
});

// Call the exposed function from page context
await page.evaluate(async () => {
  const data = { timestamp: Date.now(), items: ['a', 'b', 'c'] };
  await window.saveToFile(data);
});

// Get a JSHandle for complex objects
const bodyHandle = await page.evaluateHandle(() => document.body);
const html = await page.evaluate(body => body.innerHTML, bodyHandle);
await bodyHandle.dispose();

// Inject a script into the page
await page.addScriptTag({
  content: 'window.__injected = true; console.log("Script injected");'
});
await page.addStyleTag({
  content: '.debug { outline: 2px solid red !important; }'
});

await browser.close();

15. Tracing y Cobertura

Graba traces de Chrome DevTools para análisis de rendimiento y mide cobertura de código JavaScript/CSS para identificar código no utilizado. Los traces se pueden ver en chrome://tracing. Los datos de cobertura ayudan a optimizar tamaños de bundle.

import puppeteer from 'puppeteer';
import { writeFileSync } from 'fs';

const browser = await puppeteer.launch();
const page = await browser.newPage();

// --- Chrome Tracing ---
// Start recording a trace
await page.tracing.start({
  path: 'trace.json',
  categories: ['devtools.timeline', 'blink.user_timing', 'v8']
});

await page.goto('https://example.com', { waitUntil: 'networkidle0' });

// Perform some interactions while tracing
await page.click('.menu-toggle');
await page.waitForSelector('.menu-open', { visible: true });

// Stop tracing (writes to trace.json)
await page.tracing.stop();
// Open trace.json in chrome://tracing for analysis

// --- JavaScript Coverage ---
await page.coverage.startJSCoverage();
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
const jsCoverage = await page.coverage.stopJSCoverage();

let totalBytes = 0;
let usedBytes = 0;
for (const entry of jsCoverage) {
  totalBytes += entry.text.length;
  for (const range of entry.ranges) {
    usedBytes += range.end - range.start;
  }
}
console.log(`JS coverage: ${((usedBytes / totalBytes) * 100).toFixed(1)}%`);
console.log(`Unused JS: ${((1 - usedBytes / totalBytes) * 100).toFixed(1)}%`);

// --- CSS Coverage ---
await page.coverage.startCSSCoverage();
await page.goto('https://example.com', { waitUntil: 'networkidle0' });
const cssCoverage = await page.coverage.stopCSSCoverage();

let totalCSS = 0;
let usedCSS = 0;
for (const entry of cssCoverage) {
  totalCSS += entry.text.length;
  for (const range of entry.ranges) {
    usedCSS += range.end - range.start;
  }
}
console.log(`CSS coverage: ${((usedCSS / totalCSS) * 100).toFixed(1)}%`);

// Write unused CSS report
const unusedCSS = cssCoverage.map(entry => ({
  url: entry.url,
  total: entry.text.length,
  used: entry.ranges.reduce((a, r) => a + (r.end - r.start), 0),
})).filter(e => e.used / e.total < 0.5);
writeFileSync('unused-css-report.json', JSON.stringify(unusedCSS, null, 2));

await browser.close();

16. Plugins Stealth y Anti-Detección

Para escenarios de scraping donde los sitios detectan navegadores headless, puppeteer-extra con el plugin stealth parchea vectores de detección comunes. Siempre combina stealth con prácticas éticas de scraping.navigator.webdriver flag, missing browser plugins, incorrect Chrome runtime properties, and WebGL vendor strings. Always combine stealth with ethical scraping practices.

import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';

// Apply stealth plugin (patches 10+ detection vectors)
puppeteer.use(StealthPlugin());

const browser = await puppeteer.launch({
  headless: true,
  args: [
    '--no-sandbox',
    '--disable-setuid-sandbox',
    '--disable-blink-features=AutomationControlled'
  ]
});

const page = await browser.newPage();

// Randomize viewport to avoid fingerprinting
const viewports = [
  { width: 1920, height: 1080 },
  { width: 1366, height: 768 },
  { width: 1440, height: 900 },
  { width: 1536, height: 864 },
];
const vp = viewports[Math.floor(Math.random() * viewports.length)];
await page.setViewport(vp);

// Rotate user agents
const userAgents = [
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
  'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
  'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
];
await page.setUserAgent(
  userAgents[Math.floor(Math.random() * userAgents.length)]
);

// Randomize timing between actions
function randomDelay(min = 500, max = 2000) {
  return new Promise(r =>
    setTimeout(r, min + Math.random() * (max - min))
  );
}

await page.goto('https://example.com');
await randomDelay();

// Simulate human-like mouse movement before clicking
await page.mouse.move(100, 200);
await randomDelay(100, 300);
await page.mouse.move(250, 350);
await randomDelay(100, 300);
await page.click('.target-element');

// Verify stealth is working
const isWebdriver = await page.evaluate(
  () => navigator.webdriver
);
console.log('webdriver detected:', isWebdriver); // should be false

await browser.close();

17. Patrones de Manejo de Errores

Scripts de Puppeteer robustos requieren manejo de errores completo. Implementa lógica de reintentos, degradación elegante y limpieza apropiada para timeouts de navegación, selectores faltantes, errores de red e instancias de navegador caídas.

import puppeteer from 'puppeteer';

// Retry wrapper with exponential backoff
async function withRetry(fn, maxRetries = 3, baseDelay = 1000) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxRetries) throw error;
      const delay = baseDelay * Math.pow(2, attempt - 1);
      console.warn(`Attempt ${attempt} failed: ${error.message}. Retrying in ${delay}ms...`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

// Safe navigation with error handling
async function safeGoto(page, url, options = {}) {
  try {
    const response = await page.goto(url, {
      waitUntil: 'networkidle0',
      timeout: 30000,
      ...options
    });

    if (!response) {
      throw new Error(`No response from ${url}`);
    }

    if (!response.ok() && response.status() !== 304) {
      throw new Error(`HTTP ${response.status()} at ${url}`);
    }

    return response;
  } catch (error) {
    if (error.message.includes('net::ERR_')) {
      console.error(`Network error navigating to ${url}: ${error.message}`);
    } else if (error.name === 'TimeoutError') {
      console.error(`Timeout navigating to ${url}`);
    }
    throw error;
  }
}

// Safe element interaction
async function safeClick(page, selector, timeout = 5000) {
  try {
    await page.waitForSelector(selector, { visible: true, timeout });
    await page.click(selector);
  } catch (error) {
    console.error(`Failed to click ${selector}: ${error.message}`);
    // Take debug screenshot
    await page.screenshot({
      path: `debug-${Date.now()}.png`,
      fullPage: true
    });
    throw error;
  }
}

// Browser crash recovery
async function withBrowserRecovery(task) {
  let browser;
  try {
    browser = await puppeteer.launch({
      args: ['--no-sandbox', '--disable-dev-shm-usage']
    });

    browser.on('disconnected', () => {
      console.error('Browser disconnected unexpectedly');
    });

    await task(browser);
  } catch (error) {
    console.error('Task failed:', error.message);
    throw error;
  } finally {
    if (browser) {
      try {
        await browser.close();
      } catch {
        // Browser already closed or crashed
      }
    }
  }
}

// Usage example
await withBrowserRecovery(async (browser) => {
  const page = await browser.newPage();
  page.setDefaultTimeout(15000);

  await withRetry(async () => {
    await safeGoto(page, 'https://example.com');
    await safeClick(page, '#dynamic-button');
    const data = await page.$eval('.result', el => el.textContent);
    console.log('Result:', data);
  });
});

18. Docker y Despliegue CI/CD

Ejecutar Puppeteer en Docker requiere dependencias de sistema específicas para Chromium. La configuración a continuación provee un Dockerfile listo para producción y un workflow de GitHub Actions.

# Dockerfile for Puppeteer
FROM node:20-slim

# Install Chromium dependencies
RUN apt-get update && apt-get install -y \
    chromium \
    fonts-liberation \
    libatk-bridge2.0-0 \
    libatk1.0-0 \
    libcups2 \
    libdbus-1-3 \
    libgbm1 \
    libnspr4 \
    libnss3 \
    libx11-xcb1 \
    libxcomposite1 \
    libxdamage1 \
    libxrandr2 \
    xdg-utils \
    --no-install-recommends \
  && rm -rf /var/lib/apt/lists/*

# Set Puppeteer to use system Chromium
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .

# Run as non-root user
RUN groupadd -r pptruser && useradd -r -g pptruser pptruser
USER pptruser

CMD ["node", "index.js"]
# .github/workflows/puppeteer-tests.yml
name: Puppeteer E2E Tests

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  e2e:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Start application
        run: npm start &

      - name: Wait for server
        run: npx wait-on http://localhost:3000

      - name: Run Puppeteer tests
        run: npm run test:e2e
        env:
          PUPPETEER_ARGS: '--no-sandbox --disable-setuid-sandbox'

      - name: Run Lighthouse audit
        run: node lighthouse-audit.js

      - name: Upload screenshots on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: debug-screenshots
          path: debug-*.png

19. Comparación con Playwright

Playwright, creado por ex miembros del equipo de Puppeteer en Microsoft, es la alternativa principal. Ambas herramientas automatizan navegadores, pero difieren en arquitectura, diseño de API y soporte cross-browser.

Característica Puppeteer Playwright
Soporte de navegadores Chrome/Chromium (CDP), Firefox (BiDi default) Chromium, Firefox, WebKit
Auto-espera Manual (waitForSelector, etc.) Integrada (acciones auto-esperan)
Motor de selectores Selectores CSS, XPath CSS, XPath, texto, rol, test-id
Aislamiento paralelo Contextos de navegador Contextos + test fixtures
Mocking de red API de intercepción de requests API route() con glob matching
Emulación móvil Descriptores KnownDevices Descriptores devices + projects
Acceso CDP Nativo, de primera clase Soportado pero menos directo
Integración Lighthouse Nativa vía puerto CDP Requiere configuración de adaptador
Test runner Externo (Jest, Mocha) Integrado (@playwright/test)
Lenguajes JavaScript/TypeScript JS/TS, Python, Java, C#

When to choose Puppeteer: You need native CDP access, Lighthouse integration, stealth plugins (puppeteer-extra ecosystem), or your project is Chrome-only. Puppeteer is also the right choice for scripts that rely on Chrome-specific DevTools features like tracing, coverage, and performance profiling.

When to choose Playwright: You need cross-browser testing (Safari via WebKit), built-in auto-waiting, a batteries-included test runner, or multi-language support. Playwright's test fixtures and built-in parallelization make it stronger for large test suites. Its API design avoids many common timing pitfalls found in Puppeteer scripts.

20. Resultados Reales

348 reportes exportados

Reportes de composición corporal de Xiaomi Health extraídos vía navegación automatizada con Puppeteer. Completado en menos de 2 horas lo que hubiera tomado días manualmente.

Lighthouse CI en cada PR

Auditorías de rendimiento automatizadas detectan regresiones de Core Web Vitals antes del merge. Umbrales de presupuesto bloquean PRs sub-estándar de llegar a producción.

Detección de regresiones visuales

Capturas programadas comparadas contra líneas base detectan regresiones CSS y bugs de renderizado que los tests unitarios no pueden detectar.

Interacción web potenciada por AI

Puppeteer integrado con OpenClaw le da al asistente AI capacidades de navegador para navegar sitios, llenar formularios y extraer datos.

21. Últimas Actualizaciones (2025-2026)

Puppeteer v25.3.0 y WebDriver BiDi (v23+)

La versión actual es Puppeteer v25.3.0 (julio de 2026), empaquetando Chrome 150 y Firefox 152. WebDriver BiDi está production-ready desde v23 y es el protocolo por defecto para conexiones con Firefox. BiDi provee un protocolo estandarizado de automatización cross-browser, mientras que CDP sigue siendo el default para Chrome, proveyendo integración más profunda con funcionalidades específicas de Chrome como tracing, cobertura y Lighthouse. Puppeteer no está eliminando el soporte CDP -- ambos protocolos coexistirán.

Stagehand v3: Automatización AI Nativa con CDP

Stagehand v3 eliminó su dependencia de Playwright y ahora habla directamente con el navegador vía CDP, minimizando el round-trip time. Introduce un sistema modular de drivers que funciona transparentemente con Puppeteer. Puedes usar objetos Page de Puppeteer directamente con los métodos AI de Stagehand (act(), extract(), observe()). Este cambio arquitectónico prioriza throughput y control sobre el auto-waiting orientado a testing, haciendo de Stagehand v3 un compañero natural para workflows de Puppeteer en producción que necesitan interacción con el navegador impulsada por AI.act(), extract(), observe()). This architectural shift prioritizes throughput and control over testing-first auto-waiting, making Stagehand v3 a natural companion for production Puppeteer workflows that need AI-driven browser interaction.

Modo de Intercepción Cooperativa (v24+)

Puppeteer v24+ introduce el Modo de Intercepción Cooperativa, un rediseño fundamental de la intercepción de requests que resuelve el conflicto de larga data cuando múltiples handlers necesitan procesar la misma solicitud de red. En versiones anteriores, solo el primer handler en llamar request.continue(), request.abort() o request.respond() ganaba, y los handlers subsiguientes eran ignorados silenciosamente. Esto hacía que componer interceptores independientes (bloqueador de anuncios + inyector de headers auth + cache de respuestas) fuera poco confiable.

El modo cooperativo introduce un sistema de votación donde cada handler registrado emite un voto sobre qué debería pasar con la solicitud: abort, continue o respond. La acción final sigue orden de prioridad -- abort gana sobre respond, que gana sobre continue. Si múltiples handlers votan respond, se usa la respuesta del primer handler registrado. Cada handler llama a los mismos métodos de siempre, pero Puppeteer recolecta todos los votos antes de ejecutar la decisión final. Esto habilita patrones de middleware verdaderamente componibles para intercepción de red.

// Enable cooperative intercept mode
await page.setRequestInterception(true, { mode: 'cooperative' });

// Handler 1: Block tracking scripts
page.on('request', req => {
  if (req.url().includes('analytics')) req.abort();
  else req.continue();
});

// Handler 2: Add auth headers to API calls
page.on('request', req => {
  if (req.url().startsWith('https://api.example.com')) {
    req.continue({ headers: { ...req.headers(), Authorization: 'Bearer tok' } });
  } else {
    req.continue();
  }
});

// Both handlers compose correctly:
// analytics requests get aborted, API requests get auth headers

Tecnologías Relacionadas