··
Mantener la documentación visual de una aplicación que evoluciona cada semana es una pesadilla. Te contamos cómo usamos Playwright para automatizar la generación de más de 60 capturas de pantalla en Ofusca, con optimización WebP incluida.

Cada vez que cambiábamos un botón, un diálogo o un panel en Ofusca, había que repetir el mismo ritual: abrir la app, navegar al estado correcto, hacer captura, recortar, exportar, reemplazar el archivo. Multiplicado por más de 60 capturas de pantalla repartidas en 46 secciones de ayuda, el proceso consumía horas que deberían haberse dedicado a escribir código. Para una comparativa más detallada, en regresión visual con pixelmatch analizamos las diferencias. Hablamos de esto con más detalle en variables de entorno en scripts E2E.
Decidimos que un navegador automatizado lo hiciera por nosotros. Este artículo explica cómo usamos Playwright para generar todas las capturas de la página de Ayuda de Ofusca de forma reproducible, consistente y en dos formatos optimizados.
Ofusca es una herramienta web de censura de documentos que funciona 100 % en el navegador, ningún archivo sale del dispositivo del usuario. Permite aplicar efectos de censura (sólido, blur, píxel, trama, trazo, sello), detectar caras automáticamente, buscar texto con OCR, procesar lotes de imágenes, firmar documentos con esteganografía y mucho más.
La página de Ayuda integrada tiene 7 categorías y 46 secciones con documentación visual completa. Cada sección muestra la interfaz en un estado específico: un diálogo abierto, un panel desplegado, un resultado de detección. Mantener esas imágenes actualizadas manualmente era insostenible.
Evaluamos varias opciones antes de decidirnos:
Playwright ofrece exactamente lo que necesitábamos: lanzar un navegador, llevar la interfaz a un estado preciso y fotografiarla. Sin más.
El punto de partida es un playwright.config.ts mínimo. No necesitamos reporters, ni paralelismo agresivo, ni reintentos: el script corre en local contra el servidor de desarrollo.
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./screenshots",
timeout: 60_000,
use: {
baseURL: "http://localhost:5173",
screenshot: "off", // las tomamos manualmente
viewport: { width: 1440, height: 900 },
},
projects: [
{
name: "desktop",
use: { ...devices["Desktop Chrome"] },
},
],
});Fijamos el viewport a 1440 × 900 para todas las capturas de escritorio. Esto garantiza que cada imagen tenga las mismas dimensiones y que los elementos de la interfaz aparezcan siempre en la misma posición relativa.
El núcleo es una función auxiliar que encapsula la lógica de captura y conversión:
import { type Page } from "@playwright/test";
import sharp from "sharp";
import path from "node:path";
const HELP_DIR = path.resolve("public/help");
async function capture(page: Page, name: string) {
const pngPath = path.join(HELP_DIR, `${name}.png`);
const webpPath = path.join(HELP_DIR, `${name}.webp`);
// Captura PNG a resolución completa
await page.screenshot({ path: pngPath, fullPage: false });
// Convierte a WebP con calidad 80 (buen equilibrio tamaño/calidad)
await sharp(pngPath).webp({ quality: 80 }).toFile(webpPath);
console.log(`✓ ${name} → PNG + WebP`);
}Cada llamada a capture() produce dos archivos: el PNG original (fallback para navegadores antiguos) y una versión WebP optimizada. Así, el componente de la ayuda puede servir el formato más eficiente según el soporte del navegador.
Antes de fotografiar, hay que llevar la aplicación al estado exacto que queremos documentar. Aquí es donde Playwright brilla: podemos cargar un documento de ejemplo, abrir diálogos, seleccionar herramientas y esperar a que las animaciones terminen.
import { test } from "@playwright/test";
test("capture help screenshots", async ({ page }) => {
await page.goto("/");
// Cargar documento de demostración
await page.click('button:has-text("Probar con documento de ejemplo")');
await page.waitForSelector("canvas", { state: "visible" });
// ─── Panel de efectos ───
await capture(page, "effects-panel");
// ─── Detección de caras ───
await page.click('button:has-text("Detección de caras")');
await page.waitForSelector('[role="dialog"]', { state: "visible" });
await capture(page, "det-caras");
await page.click('button:has-text("Cancelar")');
// ─── Procesamiento por lotes ───
await page.click('button:has-text("Lotes")');
await page.waitForSelector('[role="dialog"]', { state: "visible" });
await capture(page, "batch-mode");
await page.click('button:has-text("Cancelar")');
// ─── Censura automática ───
await page.click('button:has-text("Auto-censurar")');
await page.waitForTimeout(1500); // esperar resultado de detección
await capture(page, "auto-censurar");
});Cada bloque sigue el mismo patrón, navegar → esperar → capturar → cerrar. Al encadenar todas las capturas en un solo test, aprovechamos que la aplicación ya está cargada y evitamos reiniciar el navegador 60 veces.
No todas las capturas son tan simples como hacer clic en un botón. Algunos estados requieren interacciones elaboradas:
Ofusca muestra diálogos para detección de caras, verificación de censura, procesamiento por lotes y más. Cada uno se abre con una acción específica y necesita tiempo para cargar datos.
El diálogo de detección de caras, por ejemplo, necesita que el modelo de IA local termine de procesar antes de que podamos capturar el resultado con las tres caras identificadas y sus porcentajes de confianza.
El panel lateral muestra los siete efectos de censura disponibles. Para capturarlo en su estado completo, necesitamos que un documento esté cargado y el panel visible:
El modo de lotes permite aplicar un perfil de censura a múltiples imágenes. El diálogo necesita estar limpio, sin archivos cargados, para la captura de documentación:
La censura automática combina detección de caras y OCR para identificar datos sensibles. La captura muestra el resumen de elementos censurados:
Cada captura se genera en dos formatos. El PNG sirve como fallback universal; el WebP reduce el peso drásticamente sin pérdida perceptible de calidad.
La conversión usa sharp, la librería de procesamiento de imágenes más rápida del ecosistema Node.js:
import sharp from "sharp";
import { readdir } from "node:fs/promises";
import path from "node:path";
async function convertAllToWebP(dir: string) {
const files = await readdir(dir);
const pngs = files.filter((f) => f.endsWith(".png"));
for (const file of pngs) {
const input = path.join(dir, file);
const output = input.replace(/\.png$/, ".webp");
const { size: pngSize } = await sharp(input).metadata();
await sharp(input).webp({ quality: 80 }).toFile(output);
const { size: webpSize } = await sharp(output).metadata();
const reduction = ((1 - (webpSize ?? 0) / (pngSize ?? 1)) * 100).toFixed(1);
console.log(`${file}: ${reduction}% más pequeño en WebP`);
}
}Los resultados de compresión son notables. Algunos ejemplos reales de nuestras capturas:
De media, las versiones WebP pesan entre un 90 % y un 97 % menos que los PNG originales. Para una página de ayuda que carga docenas de imágenes bajo demanda, la diferencia es enorme.
En el lado del frontend, un componente Img se encarga de servir el formato adecuado usando la etiqueta <picture> de HTML5:
export function Img({ src, alt, className = "" }: {
src: string;
alt: string;
className?: string;
}) {
const webpSrc = src.replace(/\.(png|jpe?g)$/i, ".webp");
return (
<picture>
<source srcSet={webpSrc} type="image/webp" />
<img
src={src}
alt={alt}
className={className}
loading="lazy"
/>
</picture>
);
}El navegador elige automáticamente WebP si lo soporta; en caso contrario, carga el PNG. La propiedad loading="lazy" asegura que solo se descarguen las imágenes visibles en el viewport, lo cual es crítico cuando la página contiene más de 60.
Y así es como se ve la página de Ayuda completa, con todas las capturas generadas por Playwright:
La experiencia es idéntica en escritorio y en móvil: las imágenes se adaptan al ancho disponible y la navegación cambia de barra lateral a acordeón.
Después de implementar la automatización, estos son los datos concretos:
Lo que antes llevaba una mañana entera ahora toma menos de dos minutos: el tiempo que Playwright necesita para recorrer todos los estados de la interfaz, capturar y convertir.
Si mantienes documentación visual de cualquier tipo, estas son las ideas clave que nos llevamos:
waitForSelector o waitForTimeout antes de capturar. Las animaciones y las cargas asíncronas producen capturas incompletas si no se espera lo suficiente.<picture> en el frontend. Es la forma estándar de servir formatos condicionales sin JavaScript adicional.loading="lazy" nativo, las imágenes fuera del viewport no se descargan hasta que el usuario hace scroll. Imprescindible cuando tu página tiene docenas de capturas.La mejor documentación es la que se actualiza sola. Si cada cambio en la interfaz requiere trabajo manual para mantener las capturas al día, tarde o temprano dejarás de hacerlo. Automatízalo desde el principio.
El código de Ofusca y su página de ayuda están en producción. Si quieres ver el resultado final de este flujo de trabajo, visita ofusca.josemanuelortega.dev y pulsa el botón de ayuda (?).
Otra entrega de la serie Playwright en profundidad. El siguiente post es Playwright como motor de testing de JMO Labs.

Jose, autor del blog
QA Engineer. Escribo en voz alta sobre automatización, IA y arquitectura de software. Si algo te ha servido, escríbeme y cuéntamelo.
¿Qué te ha parecido? ¿Qué añadirías? Cada comentario afina la siguiente entrada.
Si esto te ha gustado

Playwright crece un 180%, Selenium cae al 22%, el 82% de los QA cree que la IA será clave y una nueva ley europea convierte la accesibilidad en obligación. Análisis de las herramientas y tendencias que están redefiniendo el perfil del tester en España.

Los tests E2E se rompen con cada cambio de interfaz. En JMO Labs construimos un pipeline de 5 fases con IA que planifica, ejecuta, repara selectores, diagnostica fallos y verifica resultados de forma autónoma. La caché de selectores hace que cada ejecución sea más rápida que la anterior.

Playwright no es solo para tests E2E. En JMO Labs lo usamos como motor completo: 9 fases de comprobación, localizador de 9 estrategias con self-healing, grabación de vídeo, testing responsive con viewports reales y accesibilidad con axe-core.