playwright.config.ts: cada propiedad explicada, 3 experimentos y lo que Selenium no centraliza

Cada propiedad del config explicada. 3 experimentos: timeout 5s (0/12), 15s (11/12), retries. Lo que en Selenium distribuí entre 5 archivos.

Código de playwright.config.ts limpio con 43 líneas mostrando testDir fullyParallel forbidOnly retries workers reporter use y 3 projects chromium firefox webkit
La config limpia: 43 líneas, sin comentarios, solo lo que está activo. De ~70 líneas del template original a esto.

El archivo que todos copian

Cuando corrés npm init playwright@latest, Playwright genera un playwright.config.ts con valores por defecto, comentarios explicativos y secciones comentadas. Ocupa unas 40 líneas de código real y otras 30 de comentarios.

La config por defecto funciona, pero deja 30 líneas de ruido que oscurecen lo que realmente está activo.

Este post desarma el archivo propiedad por propiedad y lo valida con tres experimentos que muestran el impacto real de cada valor.


Paso 1: limpiar los comentarios

Saqué todo lo comentado: dotenv, mobile viewports, branded browsers, webServer. Dejé solo lo que está activo:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',

  use: {
    trace: 'on-first-retry',
    locale: 'es-AR',
    baseURL: 'https://demo.serenity.is',
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
});
UI Mode mostrando 12 de 12 tests passed con playwright.config.ts abierto en VS Code al costado mostrando la config limpia completa
12/12 con la config limpia. A la derecha, el archivo completo. A la izquierda, los tests corriendo contra demo.serenity.is.

De ~70 líneas a ~30. Mismo resultado: 12/12 passed. Ahora sé exactamente qué está activo.


Propiedad por propiedad

testDir: './tests'

Dónde busca los archivos *.spec.ts. Si mañana reorganizo los tests en subcarpetas dentro de tests/, Playwright los encuentra igual. Si los muevo a otra carpeta, cambio acá.

fullyParallel: true

Cada test corre en paralelo, no solo cada archivo. Si lo pongo en false, los tests dentro de un mismo archivo esperan turno — corren uno después del otro. Archivos distintos sí corren en paralelo.

Con true: más rápido, pero cada test tiene que ser independiente. No puede depender de que otro haya corrido antes. Con los fixtures del Post 5 eso ya está garantizado — cada test recibe su propia page aislada.

forbidOnly: !!process.env.CI

Cuando estoy desarrollando, a veces pongo test.only(...) para correr un solo test. Es útil para debugging. Pero si me olvido de sacar ese .only y pusheo a CI, solo corre un test en vez de toda la suite.

forbidOnly en CI hace que el pipeline falle si detecta un .only en el código. Es una red de seguridad: me avisa "te olvidaste de sacar el .only antes de pushear".

En local (process.env.CI es undefined) → false, puedo usar .only tranquilo. En CI → true, .only rompe el build.

retries: process.env.CI ? 2 : 0

Cuántas veces Playwright reintenta un test que falla. En local: 0 (quiero ver el fallo inmediato). En CI: 2 (porque los fallos por timing en un servidor de CI son comunes).

Si un test falla en el primer intento pero pasa en el retry, Playwright lo marca como "flaky" en el reporte. No lo oculta — te avisa que pasó, pero con dudas.

Más adelante lo probamos con timeout corto para ver esto en acción.

workers: process.env.CI ? 1 : undefined

Workers son los procesos paralelos que corren tests. undefined en local = Playwright decide cuántos según mis CPUs (por defecto, la mitad de los cores lógicos). En CI pone 1 porque los servidores de CI suelen tener pocos recursos y correr muchos workers en paralelo puede hacer que todo sea más lento o inestable.

Si pongo workers: 1 en local, los tests corren de a uno. Más lento, pero útil para debugging cuando algo falla y quiero ver el orden exacto.

reporter: 'html'

Qué tipo de reporte genera. 'html' es el reporte visual que abro con npx playwright show-report — el que mostré en el Post 1 con los 6 tests.

Otras opciones:

  • 'list' → solo texto en consola, una línea por test
  • 'dot' → un punto por test (mínimo)
  • 'json' → para integrar con otras herramientas
  • Varios juntos: reporter: [['html'], ['list']]

Si lo cambio a 'list', no se genera el HTML. Para desarrollo uso 'html'. Para CI probablemente combine ambos.

use: { ... } — opciones compartidas

Todo lo que va dentro de use aplica a todos los projects (chromium, firefox, webkit).

trace: 'on-first-retry'

Playwright puede grabar un trace completo de cada test: screenshots paso a paso, network, consola, timing. Pero eso pesa. 'on-first-retry' significa: solo grabá trace cuando un test falla y se reintenta. Así tengo el trace para investigar qué falló, sin grabar todo cuando las cosas funcionan.

Otras opciones: 'on' (siempre), 'off' (nunca), 'retain-on-failure' (solo en tests que fallan).

locale: 'es-AR'

Lo agregué en el Post 2. demo.serenity.is cambia el idioma de la interfaz según el locale del browser. Sin esto, codegen en headed mode veía "Nombre de usuario" pero en headless veía "Username". Los locators no coincidían.

Una línea en la config resolvió un problema que en Selenium no existe porque Selenium usa el browser real con la configuración del sistema.

baseURL: 'https://demo.serenity.is'

Cuando hago page.goto('/Account/Login'), Playwright le agrega la baseURL adelante. Si mañana cambio el ambiente de testing (staging, producción, local), cambio una línea acá. Lo agregué en el Post 3 — antes tenía la URL completa hardcodeada en LoginPage.ts.

projects: [...] — browsers

Cada proyecto es un browser. Playwright corre todos los tests en cada proyecto. 4 tests × 3 projects = 12 ejecuciones.

devices['Desktop Chrome'] no es solo el browser — incluye viewport (1280×720), user agent, y otras propiedades que simulan un escritorio real.

Playwright también tiene devices móviles:

{
  name: 'Mobile Chrome',
  use: { ...devices['Pixel 5'] },
},
{
  name: 'Mobile Safari',
  use: { ...devices['iPhone 12'] },
},

Esto cambia el tamaño de pantalla, user agent y touch events. Sirve para testear webs responsive. Pero no abre un celular ni una app nativa. Playwright es para web. Para apps móviles nativas (APK, IPA): Appium. Son complementarios.

También podés agregar browsers reales instalados en tu máquina:

{
  name: 'Google Chrome',
  use: { ...devices['Desktop Chrome'], channel: 'chrome' },
},

Eso usa tu Chrome real en vez del Chromium de Playwright. Para la mayoría de los casos, Chromium cubre Chrome y Edge porque comparten el mismo motor.

webServer (comentado, pero vale explicar)

Lo saqué porque no aplica a mi caso. webServer es para cuando testeás tu propia app local:

webServer: {
  command: 'npm run start',
  url: 'http://localhost:3000',
  reuseExistingServer: !process.env.CI,
},

Le dice a Playwright: "antes de correr los tests, levantá mi app". Cuando los tests terminan, la apaga. Útil para CI donde no hay un servidor corriendo. No aplica para demo.serenity.is porque es un servidor externo.


Los 3 experimentos

La documentación explica qué hace cada propiedad. Los experimentos muestran qué pasa cuando se rompen.

Experimento 1: timeout de 5 segundos

Agregué timeout: 5000 a la config. El default de Playwright es 30 segundos por test (30000ms). Con 5 segundos:

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  timeout: 5000,  // ← 5 segundos
  // ...resto igual
});
UI Mode mostrando 0 de 12 passed con Navigate to Account Login tardando 5.9 segundos señalado con flecha naranja y browser en about blank
timeout: 5000. Navigate tardó 5.9s — ya excedió los 5s. El browser queda en about:blank. 0/12.

0/12. Nada pasó. demo.serenity.is ni siquiera terminó de cargar el login en 5 segundos. En la imagen se ve: Navigate to "/Account/Login" tardó 5.9s — ya excedió el timeout. El browser queda en about:blank.

Fragmento de playwright.config.ts mostrando timeout 5000 con comentario 5 segundos entre fullyParallel y forbidOnly
Una línea agregada. Un número. 12 tests rotos.

Un solo número en la config rompe todos los tests.

Experimento 2: timeout de 15 segundos

Subí a 15 segundos. La zona gris.

timeout: 15000,  // ← 15 segundos
UI Mode mostrando 11 de 12 passed con firefox fallando en verificar URL y titulo y flechas naranjas señalando Before Hooks 15.3 segundos y página de login visible
timeout: 15000. Firefox falló: el beforeEach tardó 15.3s, no quedó tiempo para las assertions. La página de login todavía cargada en el preview.

11/12. Chromium y webkit pasaron. Firefox falló en "verificar URL y título" — el beforeEach tardó tanto en hacer login que al llegar a las assertions, el timeout global de 15s ya se había agotado. El preview muestra la página de login todavía cargada — firefox no llegó al dashboard.

Mismo test, mismo código, distinto resultado según el browser. Y todo por un número en la config.

Experimento 3: timeout de 15s + retries: 2

Combiné las dos cosas:

timeout: 15000,
retries: 2,
UI Mode mostrando 12 de 12 passed con timeout 15000 y retries 2 con flechas naranjas señalando warnings en Click y toContainText
timeout: 15000 + retries: 2. 12/12, pero con warnings (⚠). Mismo timeout, distinta corrida, distinto resultado. Eso es un test flaky.

12/12. Firefox pasó — el servidor respondió más rápido en esta corrida. Los warnings (⚠) en algunos pasos indican acciones que tardaron más de lo esperado, pero no llegaron al timeout.

El punto: con el mismo timeout de 15s, la misma config, el mismo test — una vez falla, otra vez pasa. Eso es un test flaky. Y retries existe para eso: si falla, reintenta, y si en el retry pasa, lo marca como "flaky" en vez de fallido.

En Selenium no tenés esto built-in. TestNG tiene IRetryAnalyzer, pero hay que implementarlo uno mismo: crear una clase, configurar el máximo de reintentos, registrar el listener. En Playwright es un número en la config.


Selenium vs Playwright: configuración

En mi qa-automation-lab de Selenium no existe un archivo central de configuración. Distribuí la configuración entre:

Qué Selenium + TestNG Playwright
Browsers Código Java (ChromeDriver, FirefoxDriver) projects en config
Paralelismo testng.xml (thread-count, parallel) fullyParallel + workers en config
Timeouts Código Java (implicitWait, explicitWait) timeout en config
Retries Clase IRetryAnalyzer custom retries en config
Base URL Hardcodeada o en properties baseURL en config
Reportes Plugin Allure en pom.xml + annotations reporter en config
Locale No aplica (usa el browser del sistema) locale en config
Traces No existe equivalente directo trace en config

En Selenium, la configuración está distribuida entre testng.xml, pom.xml, archivos .properties, clases Java (DriverManager, BasePage) y anotaciones. Cada cosa en un lugar distinto.

En Playwright, abro un archivo y veo todo. Browsers, timeouts, retries, paralelismo, reportes, traces. Si necesito cambiar el ambiente de testing, cambio baseURL. Si un test es flaky en CI, agrego retries. Un archivo, un lugar.


La config final

Después de los experimentos, volví al default limpio. Sin timeout explícito (usa 30s), retries solo en CI, workers automáticos en local:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',

  use: {
    trace: 'on-first-retry',
    locale: 'es-AR',
    baseURL: 'https://demo.serenity.is',
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
});

12/12. La misma config que antes, pero ahora sé qué hace cada línea.


Próximo post

Autenticación con storageState. En los últimos 3 posts, cada test hace login desde cero: abre el formulario, escribe usuario, escribe contraseña, clickea "Iniciar sesión". Con storageState, hacemos login una vez, guardamos las cookies, y los tests arrancan ya logueados. Sin repetir el login en cada test.

🔗 Todo el código de esta serie está en: github.com/cesarbeassuarez/playwright-typescript-framework