Cuando escribes async ({ page }) => {...} en un test de Playwright, estás recibiendo un fixture: Playwright creó una página de navegador nueva antes de que corriera tu test y la cerrará automáticamente después. Los fixtures personalizados funcionan de la misma manera, declarados con test.extend() y recibidos por nombre en la firma del test. La diferencia es que tú defines el setup y el teardown, con await use(value) como la línea divisoria. Esta guía cubre los cinco fixtures integrados, el patrón test.extend() para fixtures personalizados, las opciones de scope para compartir setup costoso entre tests, y cómo los fixtures personalizados se componen entre sí.
Qué es un fixture
Un fixture es un valor (u objeto) que Playwright prepara antes de que corra tu test y limpia después. Es inyección de dependencias para tests.
En lugar de escribir esto:
test('el usuario puede iniciar sesión', async () => {
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
// código del test
await page.close();
await context.close();
await browser.close();
});Escribes esto:
test('el usuario puede iniciar sesión', async ({ page }) => {
// page está lista para usar: el setup y el teardown están manejados
});Playwright gestiona el ciclo de vida. Obtenés una page limpia para cada test, y se cierra automáticamente después.
Fixtures integrados
Playwright provee estos fixtures de fábrica:
| Fixture | Tipo | Qué es |
|---------|------|--------|
| page | Page | Una nueva página del navegador (pestaña) para cada test |
| browser | Browser | La instancia del navegador (compartida entre tests en un worker) |
| context | BrowserContext | Contexto del navegador, como una ventana de incógnito |
| browserName | string | El navegador actual: 'chromium', 'firefox', 'webkit' |
| request | APIRequestContext | Cliente HTTP para peticiones a API |
page
El fixture más usado. Cada test obtiene su propia página aislada. Después del test, se cierra automáticamente.
test('la página carga correctamente', async ({ page }) => {
await page.goto('https://lab.becomeqa.com');
await expect(page).toHaveTitle(/BecomeQA/);
});context
Un contexto del navegador es como una ventana de incógnito: tiene sus propias cookies, almacenamiento y sesión. Si necesitás múltiples páginas en un test, crealas desde el mismo contexto:
test('dos páginas comparten la misma sesión', async ({ context }) => {
const page1 = await context.newPage();
const page2 = await context.newPage();
await page1.goto('/login');
// Iniciar sesión en page1
// page2 también ve la sesión (mismo contexto = mismas cookies)
await page2.goto('/dashboard');
await expect(page2.getByTestId('user-name')).toBeVisible();
});browser
Generalmente no necesitas browser directamente. Úsalo cuando necesitas crear contextos con ajustes específicos:
test('viewport móvil', async ({ browser }) => {
const context = await browser.newContext({
viewport: { width: 390, height: 844 },
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)...',
});
const page = await context.newPage();
await page.goto('/');
// Test en página de tamaño móvil
await context.close();
});request
Hace peticiones HTTP sin un navegador. Se usa para testing de API y para configurar datos de test vía API antes de tests de UI.
test('crear usuario vía API', async ({ request }) => {
const response = await request.post('/api/users', {
data: { email: 'nuevo@test.com', password: 'ClaveValida1' },
});
expect(response.status()).toBe(201);
});browserName
Úsalo para saltear condicionalmente tests en navegadores específicos:
test('descarga de archivo', async ({ page, browserName }) => {
test.skip(browserName === 'firefox', 'API de descarga diferente en Firefox');
// ...
});Fixtures personalizados
El verdadero poder de los fixtures: puedes crear los tuyos. Los fixtures personalizados funcionan exactamente igual que los integrados: se declaran una vez, se usan en cualquier lugar mediante destructuring.
Fixture personalizado simple: una página pre-navegada
// fixtures/index.ts
import { test as base, expect, Page } from '@playwright/test';
// Definir qué fixtures personalizados existen
type MyFixtures = {
loggedInPage: Page;
};
export const test = base.extend<MyFixtures>({
loggedInPage: async ({ page }, use) => {
// SETUP
await page.goto('/login');
await page.fill('[data-testid="email"]', 'usuario@test.com');
await page.fill('[data-testid="password"]', 'ClaveValida1');
await page.click('[data-testid="submit"]');
await page.waitForURL('/dashboard');
// Darle al test acceso a la página
await use(page);
// TEARDOWN (corre después del test)
// Nada necesario aquí: la página se cierra automáticamente
},
});
export { expect };// tests/dashboard.spec.ts
import { test, expect } from '../fixtures'; // importá TU test, no @playwright/test
test('el dashboard muestra el mensaje de bienvenida', async ({ loggedInPage }) => {
// Ya autenticado: loggedInPage ES la página, después del login
await expect(loggedInPage.getByTestId('welcome')).toBeVisible();
});Fixture personalizado: Page Object
El patrón más común: un fixture que provee una clase de page object inicializada:
// fixtures/index.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
type PageObjects = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
};
export const test = base.extend<PageObjects>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
});// tests/login.spec.ts
import { test, expect } from '../fixtures';
test('login exitoso', async ({ loginPage, dashboardPage }) => {
await loginPage.goto();
await loginPage.login('usuario@test.com', 'ClaveValida1');
await expect(dashboardPage.welcomeMessage).toBeVisible();
});No hace falta instanciar page objects en cada test.
Fixture personalizado con teardown
Si tu fixture crea algo que necesita limpieza:
type TestFixtures = {
testUser: { id: number; email: string; token: string };
};
export const test = base.extend<TestFixtures>({
testUser: async ({ request }, use) => {
// SETUP: crear un usuario
const response = await request.post('/api/users', {
data: {
email: `test_${Date.now()}@ejemplo.com`,
password: 'ClaveValida1',
role: 'member',
},
});
const user = await response.json();
// Iniciar sesión para obtener el token
const loginResp = await request.post('/api/auth/login', {
data: { email: user.email, password: 'ClaveValida1' },
});
const { token } = await loginResp.json();
// Darle acceso al test
await use({ id: user.id, email: user.email, token });
// TEARDOWN: eliminar el usuario
await request.delete(`/api/users/${user.id}`, {
headers: { Authorization: `Bearer ${adminToken}` },
});
},
});test('el usuario puede actualizar su perfil', async ({ page, testUser }) => {
// testUser tiene id, email, token: fresco y único por test
await page.goto(`/users/${testUser.id}`);
// ...
// Después del test, el usuario se elimina automáticamente
});Scope de fixtures
Por defecto, los fixtures tienen scope 'test': se recrean para cada test. Puedes establecer el scope en 'worker' para fixtures costosos y seguros de compartir:
export const test = base.extend<{}, { sharedToken: string }>({
sharedToken: [async ({ request }, use) => {
// Corre una vez por worker, no una vez por test
const response = await request.post('/api/auth/login', {
data: { email: 'admin@test.com', password: 'AdminPass1' },
});
const { token } = await response.json();
await use(token);
}, { scope: 'worker' }],
});Usa scope 'worker' para cosas que son costosas de recrear (seeding de base de datos, generación de archivos), de solo lectura (tokens de autenticación que solo lees, no modificas) y seguras de compartir (sin estado que un test pueda corromper para otro).
Combinar fixtures
Los fixtures personalizados pueden depender de otros fixtures (incluyendo otros personalizados):
export const test = base.extend<{
loginPage: LoginPage;
authenticatedPage: Page;
}>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
// Este fixture USA loginPage
authenticatedPage: async ({ page, loginPage }, use) => {
await loginPage.goto();
await loginPage.login('usuario@test.com', 'ClaveValida1');
await page.waitForURL('/dashboard');
await use(page);
},
});Una estructura de proyecto limpia
proyecto/
├── fixtures/
│ └── index.ts ← exporta tu test extendido + expect
├── pages/
│ ├── LoginPage.ts
│ └── DashboardPage.ts
└── tests/
├── login.spec.ts ← importa desde fixtures/index.ts
└── dashboard.spec.tsTodos los archivos de test importan desde fixtures/index.ts, no desde @playwright/test directamente. Esto significa que cada test tiene acceso automático a todos los fixtures personalizados.
Resumen
| | Integrados | Personalizados |
|-|------------|----------------|
| Dónde se definen | Internos de Playwright | test.extend en tu código |
| Dónde se usan | Cualquier test con { page }, { request }, etc. | Cualquier test que use tu test exportado |
| Ejemplos | page, browser, request | loginPage, testUser, authToken |
| Ciclo de vida | Playwright lo gestiona | Tú defines setup + await use() + teardown |