Autenticación con storageState en Playwright: login una vez, tests sin login

Login una vez, guardar cookies en JSON, tests sin login. Un typo de 30s, la prueba sin archivo, y lo que en Selenium es código custom acá son 2 líneas.

Reporte HTML de Playwright mostrando 7 de 7 tests passed con auth setup en 8.4 segundos y 6 tests sin login en Chromium Firefox y WebKit
7/7. El setup hace login una vez (8.4s). Los 6 tests van directo al contenido sin tocar la página de login.

El problema

Hasta ahora, cada test hace login. Incluso con el custom fixture del Post Fixtures en Playwright, cada test ejecuta: ir a /Account/Login, llenar usuario, llenar contraseña, clickear "Iniciar sesión", esperar "Tablero".

Eso son 4-5 acciones por test. Si tengo 20 tests, son 100 acciones de login. Si tengo 100 tests, son 500. Cada una contra la red, contra el servidor, con tiempos de carga.

El login no es lo que estoy testeando. Es un prerequisito. Y repetirlo en cada test es tiempo desperdiciado.

storageState resuelve esto: hacés login una vez, guardás las cookies en un archivo JSON, y todos los tests arrancan ya logueados. Sin tocar la página de login.


Cómo funciona

El flujo:

1. auth.setup.ts hace login → guarda cookies en .auth/user.json
2. playwright.config.ts dice: "antes de correr tests, corré setup"
3. Cada browser carga las cookies de .auth/user.json
4. Los tests arrancan logueados

No hay código de login en los tests. No hay beforeEach con login. No hay fixture de login. Solo cookies.


auth.setup.ts — el archivo que hace login una vez

Creé tests/auth.setup.ts:

import { test as setup, expect } from '@playwright/test';

const authFile = '.auth/user.json';

setup('autenticación', async ({ page }) => {
  await page.goto('/Account/Login');
  await page.getByRole('textbox', { name: '* Nombre de usuario' }).fill('admin');
  await page.getByRole('textbox', { name: '* Contraseña' }).fill('serenity');
  await page.getByRole('button', { name: 'Iniciar sesión', exact: true }).click();

  await expect(page.getByRole('heading', { name: 'Tablero' })).toBeVisible();

  await page.context().storageState({ path: authFile });
});
Código de auth.setup.ts en VS Code con 14 líneas mostrando login contra demo.serenity.is y storageState guardando cookies en auth user.json
14 líneas. Login, verificar dashboard, guardar cookies. Este archivo no es un test — es el setup que alimenta a todos los tests.

Es un "test" que no testea nada. Solo hace login y guarda el estado del browser en un archivo JSON. La línea clave es page.context().storageState({ path: authFile }) — toma todas las cookies del context actual y las escribe en .auth/user.json.

Importo test as setup para que quede claro en los reportes que esto no es un test, es un setup.


Primer error: un typo de 30 segundos

Corrí npx playwright test y esto pasó:

Error: locator.fill: Test timeout of 30000ms exceeded.
Call log:
  - waiting for getByRole('textbox', { name: '* Constraseña' })
Reporte HTML mostrando error de timeout 30 segundos con Fill serenity fallando en 23.4 segundos buscando textbox Constraseña con s de más
Primer error: "Constraseña" con una 's' de más. Playwright esperó 23.4 segundos buscando un campo que no existe. Auto-retry trabajando contra un typo.

30 segundos esperando un campo que no existe. 36 tests que no corrieron. El problema: escribí "Constraseña" en vez de "Contraseña". Una 's' de más.

Playwright no te dice "este locator no existe". Se queda esperando hasta que el timeout explota. Por eso el error dice timeout exceeded, no locator not found. Es auto-retry trabajando — pero cuando el locator tiene un typo, el auto-retry es tu enemigo. 30 segundos buscando algo que nunca va a aparecer.

Corregí el typo. Una letra. ConstraseñaContraseña.


La config: dependencies y storageState

El playwright.config.ts necesita dos cosas: un proyecto setup que corra el login, y que los browsers dependan de ese setup.

projects: [
  { name: 'setup', testMatch: /auth\.setup\.ts/ },

  {
    name: 'chromium',
    use: { ...devices['Desktop Chrome'], storageState: '.auth/user.json' },
    dependencies: ['setup'],
  },

  {
    name: 'firefox',
    use: { ...devices['Desktop Firefox'], storageState: '.auth/user.json' },
    dependencies: ['setup'],
  },

  {
    name: 'webkit',
    use: { ...devices['Desktop Safari'], storageState: '.auth/user.json' },
    dependencies: ['setup'],
  },
],
Archivo playwright.config.ts en VS Code mostrando proyecto setup con testMatch auth y tres browsers con storageState y dependencies setup
La config completa. Un proyecto setup que corre primero. Tres browsers que dependen de él y cargan las cookies de user.json.

testMatch: /auth\.setup\.ts/ le dice a Playwright que el proyecto setup solo corre ese archivo. dependencies: ['setup'] dice que chromium, firefox y webkit esperan a que setup termine antes de arrancar. storageState: '.auth/user.json' dice que cada browser cargue las cookies de ese archivo.

Punto importante: agregar .auth/ al .gitignore. Las cookies tienen tokens de autenticación.


37/37 — todo verde

Con el typo corregido:

Running 37 tests using 2 workers
  37 passed (1.7m)
Reporte HTML de Playwright mostrando 37 de 37 tests passed con auth setup example saucedemo y serenity assertions expandidos en tres browsers
37/37. El setup corre primero (7.3s), después los 36 tests corren con las cookies precargadas.

37 tests. auth.setup.ts corre primero (7.3s), hace login, guarda cookies. Después los 36 tests restantes corren en 3 browsers con esas cookies precargadas.

Un detalle: los tests de serenity-assertions, serenity-fixtures y serenity-locators están haciendo doble login. storageState ya los logueó (las cookies están cargadas), y después el beforeEach o el código inline vuelve a hacer login encima. Funciona porque demo.serenity.is no se rompe con doble login, pero es redundante. Los tests nuevos que escriba no necesitan login en absoluto.


Tests sin login — el punto de storageState

Creé tests/serenity-storagestate.spec.ts:

import { test, expect } from '@playwright/test';

test.describe('Tests sin login (storageState)', () => {

  test('acceder al dashboard sin login', async ({ page }) => {
    await page.goto('/');
    await expect(page.getByRole('heading', { name: 'Tablero' })).toBeVisible();
  });

  test('acceder a clientes sin login', async ({ page }) => {
    await page.goto('/Northwind/Customer');
    await expect(page.locator('#GridDiv')).toContainText('ANTON');
  });

});
Código de serenity-storagestate.spec.ts en VS Code con 15 líneas y dos tests sin login accediendo a dashboard y clientes directamente
15 líneas. Cero imports de LoginPage. Cero beforeEach. Va directo a la página y verifica. Si storageState funciona, esto pasa.

Lo relevante de este archivo es lo que no necesita: no importa LoginPage. No tiene beforeEach. No llena campos. No clickea botones de login. Va directo a la página y verifica.

Lo relevante de este archivo es lo que no necesita.

Resultado:

Running 7 tests using 2 workers
  7 passed (38.2s)
Reporte HTML mostrando 7 de 7 passed con auth setup 8.4 segundos y tests sin login storageState en Chromium Firefox y WebKit con tiempos entre 4 y 9 segundos
Setup: 8.4s. Después, 6 tests accediendo a dashboard y clientes sin hacer login. Las cookies hicieron todo.

7 tests: 1 de setup + 6 de contenido (2 tests × 3 browsers). Los 6 tests entraron al dashboard y a clientes sin hacer login. Las cookies hicieron todo.


Qué hay adentro de user.json

Abrí .auth/user.json:

{
  "cookies": [
    {
      "name": ".AspNetCore.Antiforgery",
      "value": "CfDJ8CI7v...",
      "domain": "demo.serenity.is",
      "path": "/",
      "httpOnly": true,
      "sameSite": "Strict"
    },
    {
      "name": ".AspNetAuth",
      "value": "CfDJ8CI7v...",
      "domain": "demo.serenity.is",
      "httpOnly": true,
      "secure": true,
      "sameSite": "Lax"
    },
    {
      "name": "CSRF-TOKEN",
      "value": "CfDJ8CI7v...",
      "domain": "demo.serenity.is",
      "httpOnly": false,
      "sameSite": "Lax"
    }
  ],
  "origins": []
}
Archivo user.json abierto en VS Code mostrando tres cookies AspNetCore Antiforgery AspNetAuth y CSRF-TOKEN del dominio demo.serenity.is con origins vacío
Lo que Playwright guardó: 3 cookies. Antiforgery, autenticación y CSRF. Esto es lo que el servidor necesita para reconocerte como logueado.

3 cookies: antiforgery, autenticación y CSRF. Eso es lo que el servidor necesita para reconocerte como logueado. Playwright las inyecta en el browser context antes de que el test empiece.


La prueba: sin cookies, todo se rompe

Borré .auth/user.json y corrí solo los tests de storageState sin el setup:

npx playwright test tests/serenity-storagestate.spec.ts --project=chromium --no-deps

--no-deps salta el proyecto setup. Sin cookies, no hay login.

Error: Error reading storage state from .auth/user.json:
ENOENT: no such file or directory
Terminal de VS Code mostrando 2 tests fallados con error ENOENT no such file or directory al buscar auth user.json ejecutados con no-deps
Sin archivo de cookies, sin sesión. --no-deps salta el setup. ENOENT: el archivo no existe. 2 failed en 9ms.
Reporte HTML en browser mostrando 2 failed 0 passed con ambos tests de storageState fallando en 9ms en proyecto chromium
2 failed, 9ms cada uno. Playwright ni llegó a abrir una página — falló al crear el context.
Detalle del test acceder al dashboard sin login mostrando error ENOENT en Fixture context fallando en 3ms con browser lanzado pero context sin crear
El browser se lanzó (1.1s). Pero al crear el context, Playwright buscó user.json, no lo encontró, y falló en 3ms. Sin cookies, no hay test.

2 failed en 9ms cada uno. Playwright ni siquiera llegó a abrir una página — falló al crear el context porque el archivo de cookies no existe. Esto confirma que storageState es lo que mantiene los tests logueados. Sin el archivo, no hay sesión.


Comparación con Selenium

En mi framework de Selenium (qa-automation-lab), para reutilizar sesión tendría que:

// Guardar cookies después del login
Set<Cookie> cookies = driver.manage().getCookies();
// Serializar con Gson o Jackson
String json = new Gson().toJson(cookies);
Files.write(Paths.get("cookies.json"), json.getBytes());

// En cada test: leer y restaurar
String json = new String(Files.readAllBytes(Paths.get("cookies.json")));
Type type = new TypeToken<Set<Cookie>>(){}.getType();
Set<Cookie> cookies = new Gson().fromJson(json, type);
for (Cookie cookie : cookies) {
    driver.manage().addCookie(cookie);
}
driver.navigate().refresh();

Código custom. Dependencia extra (Gson). Serialización manual. Y encima, hay que navegar primero al dominio antes de poder agregar cookies — si no, Selenium tira invalid cookie domain.

En Playwright: storageState({ path: authFile }) para guardar. storageState: '.auth/user.json' en el config para restaurar. Dos líneas. Sin dependencias extra. Sin serialización manual. Sin problemas de dominio.

Concepto Selenium + Java Playwright
Guardar sesión getCookies() + Gson + Files.write() storageState({ path })
Restaurar sesión readAllBytes + Gson + loop addCookie() storageState en config
Navegar al dominio antes Obligatorio (si no, invalid cookie domain) No necesario
Dependencias extra Gson o Jackson Ninguna
Integración con config No existe (código manual) Propiedad del config
Ejecutar login una vez para toda la suite Custom con @BeforeSuite + variable global Proyecto setup con dependencies

Estado actual del proyecto

playwright-typescript-framework/
├── .auth/
│   └── user.json          ← cookies (gitignored)
├── fixtures/
│   └── test-fixtures.ts
├── pages/
│   ├── LoginPage.ts
│   ├── DashboardPage.ts
│   └── ClientesPage.ts
├── tests/
│   ├── auth.setup.ts      ← login una vez, guarda cookies
│   ├── serenity-storagestate.spec.ts  ← tests sin login
│   ├── serenity-assertions.spec.ts
│   ├── serenity-fixtures.spec.ts
│   ├── serenity-locators.spec.ts
│   ├── example.spec.ts
│   └── saucedemo.spec.ts
├── playwright.config.ts   ← projects con setup + dependencies
└── package.json

El siguiente post cubre tests parametrizados con múltiples datasets — login válido, inválido, campos vacíos. Equivalente a los DataProviders de TestNG.

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