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.
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'] },
},
],
});

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
});

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.

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

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,

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