Tu suite del Dojo Lab funciona… pero repite código y vive desordenada.
Hoy la conviertes en una suite profesional: hooks, POM real, fixtures y API testing.
Sesión 5 de la serie (deck 4). Prerrequisito duro: la suite del Dojo Lab I funcionando (5 specs verdes). Todo lo de hoy REFACTORIZA esa suite — quien no la tenga, que clone la de un compañero o complete M6 del lab primero. Formato híbrido: concepto corto → misión aplicada. Duración: 2-3 horas.
4 de tus 5 specs empiezan con los mismos pasos de login. Copy-paste detectado.
El locator del botón de login vive en 4 archivos. Cambio de UI = cacería.
Cada verificación pasa por el navegador — lento y frágil para cosas que la API responde en milisegundos.
Acto 1 → organiza: describe + hooks
Acto 2 → blinda: locators semánticos
Acto 3 → encapsula: tu LoginPage real
Acto 4 → inyecta: fixtures propios
Acto 5 → acelera: API testing
Acto 6 → 🏆 suite reorganizada
Cada herramienta de hoy usa conceptos que YA dominas: hooks = funciones, POM = tu clase de Coding Classes, fixtures = el destructuring de ({ page }).
Abre la sesión con autocrítica de código propio — los 3 síntomas existen de verdad en sus suites del lab (compruébalo en vivo con la de alguien). El mensaje: no escribieron mal código; escribieron el primer borrador que todo profesional escribe. Hoy toca la segunda pasada, la que separa scripts de ingeniería.
test.describe("Login", () => { test("con credenciales válidas entra al dashboard", async ({ page }) => { // ... }); test("con password inválida muestra error", async ({ page }) => { // ... }); });
El reporte se lee como índice: Login → sus casos. Con 100 tests, los capítulos salvan vidas.
npx playwright test -g "Login" ejecuta solo ese grupo.
test.only(...) corre SOLO ese test (debug) · test.skip(...) lo salta documentadamente. ⚠️ Nunca commitees un .only.
Concepto liviano para calentar. describe recibe una arrow function con los tests adentro — sintaxis que ya leen fluido. El .only olvidado en un commit es un clásico: la suite "pasa" en CI porque corrió UN test. Anécdota recomendada si tienes una propia.
test("ver dashboard", async ({ page }) => { await page.goto("/login"); await page.getByLabel("Email").fill("testuser@upex.dev"); await page.getByLabel("Password").fill("Test123!"); await page.getByRole("button", { name: /sign in/i }).click(); // ...por fin el test de verdad }); // ...y otra vez en el siguiente test 😩
test.describe("Dashboard", () => { test.beforeEach(async ({ page }) => { await page.goto("/login"); // ...pasos de login, UNA sola vez }); test("ver dashboard", async ({ page }) => { // arranca YA logueado 🎉 }); test("crear tarea", async ({ page }) => { // también logueado }); });
beforeEach corre antes de CADA test del grupo — cada test arranca con página fresca y logueada, independiente de los demás. También existen afterEach, beforeAll, afterAll (limpieza y setup pesado).
Hooks = las precondiciones de su test case manual, en código y compartidas. Punto fino: beforeEach corre por CADA test (aislamiento — un test no hereda el desorden del anterior); beforeAll una vez por grupo (más rápido, menos aislado). Para este nivel: beforeEach siempre, beforeAll cuando duela la lentitud.
1. Agrupa tus specs con test.describe por feature
2. Donde 2+ tests repitan login → test.beforeEach
3. Corre la suite completa de nuevo:
¿Ese test NECESITA estar deslogueado (p.ej. login-invalid)? Entonces no pertenece a ese describe — déjalo en su propio grupo sin hook de login. Los hooks aplican a TODO el grupo: agrupa por precondición compartida.
Métrica de éxito honesta: tu suite debería perder ~15-25 líneas duplicadas sin perder ningún test.
Primera misión = refactor real sobre SU código. El atasco de la pista es pedagógico oro: login-invalid no puede compartir el hook de login válido → descubren que se agrupa por PRECONDICIÓN, no por capricho. 15-20 minutos.
beforeEach con login. ¿Cuántas veces se ejecuta el login al correr el grupo?beforeAll — más rápido pero comparte estado: el trade-off clásico velocidad vs aislamiento.El nombre lo dice (Each) pero la implicación profunda es el aislamiento — la propiedad que hace a una suite confiable y paralelizable. Siembra la palabra "aislamiento": reaparece en API testing y en KATA.
| Prioridad | Locator | Por qué |
|---|---|---|
| 🥇 1° | getByRole("button", { name: "Sign in" }) | Cómo lo percibe un usuario (y un lector de pantalla). Sobrevive rediseños. |
| 🥈 2° | getByLabel("Email") · getByPlaceholder(...) | Anclado al texto visible del formulario. |
| 🥉 3° | getByText("Bienvenida") | Para textos y mensajes no interactivos. |
| 4° | getByTestId("submit-btn") | Ancla dedicada a testing (data-testid) — pídela al equipo dev. |
| 🚨 último | page.locator(".btn > div:nth-child(2)") | CSS estructural: muere con cualquier rediseño. Solo sin alternativa. |
Bonus oculto: si getByRole no encuentra tu botón, a un usuario con lector de pantalla le pasa lo mismo. Locators semánticos = tests robustos Y auditoría de accesibilidad gratis.
La tabla ES la lección — déjala respirar. Regla mental: "¿cómo le describirías el elemento a un humano por teléfono?" → ese es el locator correcto. El bonus de accesibilidad eleva la conversación: el QA que detecta problemas a11y de pasada aporta valor extra. data-testid: la convención para pedir anclas al equipo dev cuando el DOM es hostil.
// "el botón Delete de la fila de Ana" await page .getByRole("row") .filter({ hasText: "Ana" }) .getByRole("button", { name: "Delete" }) .click();
Se lee como la frase: fila → que contenga "Ana" → su botón Delete. Encadenar = acotar el contexto paso a paso.
await expect(locator).toBeVisible(); await expect(locator).toHaveText("Guardado"); await expect(locator).toHaveCount(3); await expect(locator).toBeEnabled(); await expect(page).toHaveURL(/dashboard/);
Reintentan automáticamente hasta cumplirse (o timeout). Por eso nunca necesitas sleep: la aserción ES la espera.
filter + chaining resuelve el caso real más común: tablas y listas. Las web-first assertions cierran el círculo anti-sleep del lab: expect(...).toBeVisible() REINTENTA hasta que aparece — la espera inteligente está integrada en la aserción. Quien entiende esto jamás vuelve a escribir waitForTimeout.
1. Busca en tus specs: page.locator(" con CSS
2. Por cada uno, encuentra el equivalente semántico —
el locator picker del UI mode te lo sugiere:
3. Reemplaza, corre, verde.
Orden de fallback: ¿placeholder? → getByPlaceholder. ¿texto cercano único? → getByText + chaining. ¿nada? → ese es el caso legítimo de data-testid (y en un proyecto real, lo pides al dev). Documenta tu decisión en un comentario.
Prueba ácida: ¿tu locator describe QUÉ es el elemento ("el botón Sign in") o DÓNDE está colgado ("el tercer div del form")? Solo el primero sobrevive.
Misión de auditoría — entrena el ojo crítico sobre código propio. El locator picker del UI mode hace el 80% del trabajo: pasar el mouse y copiar la sugerencia. La prueba ácida QUÉ vs DÓNDE es el heurístico que se llevan para siempre. 15-20 minutos.
Frase clave de la explicación: "apostarle al contrato, no a la implementación" — es un principio de ingeniería general que reaparecerá en API testing (el contrato OpenAPI) y en KATA. Plántala bien.
import { type Page, type Locator } from "@playwright/test"; export class LoginPage { readonly emailInput: Locator; readonly passwordInput: Locator; readonly signInButton: Locator; constructor(private page: Page) { this.emailInput = page.getByLabel("Email"); this.passwordInput = page.getByLabel("Password"); this.signInButton = page.getByRole("button", { name: /sign in/i }); } async goto() { await this.page.goto("https://dojo.upexgalaxy.com/login"); } async login(email: string, password: string) { await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.signInButton.click(); } }
Misma clase que construiste en el simulador — con 3 upgrades de producción.
Declarados UNA vez en el constructor, tipados como Locator, reutilizados en todos los métodos.
type Page importado, readonly (no reasignable), private page = parámetro y propiedad en un paso.
La palabra que permite a otros archivos hacer import { LoginPage }.
El reencuentro: la clase del simulador de Coding Classes, versión producción. Tres novedades a nombrar sin dramatizar: locators-como-campos (estándar moderno del POM), readonly (const para propiedades), private page en el constructor (atajo de TS: parámetro+propiedad). export/import: el mecanismo de módulos que vieron conceptualmente en Coding Base — ahora lo usan de verdad.
import { test, expect } from "@playwright/test"; import { LoginPage } from "../pages/login-page"; test.describe("Login", () => { let loginPage: LoginPage; test.beforeEach(async ({ page }) => { loginPage = new LoginPage(page); await loginPage.goto(); }); test("credenciales válidas → dashboard", async ({ page }) => { await loginPage.login("testuser@upex.dev", "Test123!"); await expect(page).toHaveURL(/dashboard/); }); });
Acciones → viven en el Page Object.
Aserciones (expect) → viven en el test.
El test decide qué es pasar/fallar; la página solo sabe operarse.
La división acciones/aserciones es LA regla de arquitectura de esta sesión (y pregunta de quiz): el Page Object opera la página, el test juzga el resultado. Si el POM hace expects, decide pasa/falla en lugar del test y se vuelve inflexible. Nota la carpeta pages/ separada de tests/ — orden que escala.
1. Crea la carpeta pages/ y el archivo login-page.ts (modelo en slide 11 — tipéalo, no lo pegues: la memoria muscular importa)
2. Refactoriza login.spec.ts y login-invalid.spec.ts para usarlo
3. Verde de nuevo:
La ruta del import es relativa AL ARCHIVO que importa: desde tests/, la carpeta pages/ está un nivel arriba → ../pages/login-page. Sin extensión .ts en el import.
Crea también pages/dashboard-page.ts con un método expectLoaded()... espera, ¿expects en el POM? 🤔 Decide tú dónde va la aserción — y defiende tu decisión en el debrief.
page.fill/page.click directo en los specs — todo pasa por LoginPage.El momento culminante del arco POM iniciado en Coding Classes. "Tipéalo, no lo pegues" en serio: tipear el constructor con tipos fija la sintaxis. El reto extra siembra el debate acciones-vs-aserciones a propósito — recógelo en el debrief: la respuesta canónica es expects en el test, pero métodos de conveniencia tipo expectLoaded() existen en el mundo real y discutirlo es señal de madurez.
expect(page).toHaveURL(/dashboard/)?La explicación da el ARGUMENTO, no solo la regla: el mismo login() sirve para caso válido e inválido precisamente porque no asume resultado. Quien respondió A y escucha esto, entiende POR QUÉ — eso vale más que acertar.
import { test as base } from "@playwright/test"; import { LoginPage } from "./pages/login-page"; export const test = base.extend<{ loginPage: LoginPage }>({ loginPage: async ({ page }, use) => { const loginPage = new LoginPage(page); await loginPage.goto(); await use(loginPage); // ← aquí corre el test }, }); export { expect } from "@playwright/test";
import { test, expect } from "../fixtures"; test("login válido", async ({ loginPage, page }) => { await loginPage.login("testuser@upex.dev", "Test123!"); await expect(page).toHaveURL(/dashboard/); });
({ page }) que usas desde Coding Base ES un fixture de Playwright. Acabas de aprender a fabricar los tuyos: ({ loginPage }) llega ya instanciada y en la página de login — sin new, sin goto, sin beforeEach.
Concepto más abstracto de la sesión — ánclalo en lo conocido: page siempre fue un fixture; extend agrega los propios al mismo mecanismo. El patrón del fixture: preparar (new + goto) → use(objeto) = "aquí corre el test" → (después de use: limpieza si hiciera falta). No profundices en el generic <{...}>: "le dice a TypeScript qué tipo entrega el fixture" basta. La conexión KATA va en el cierre.
loginPage y migrar tus specs de login a usarlo.
1. Crea fixtures.ts en la raíz (modelo en slide 15)
2. En tus specs de login: cambia el import de @playwright/test por ../fixtures
3. Recibe ({ loginPage, page }) y borra el beforeEach que ya no necesitas
4. Verde de nuevo.
Revisa el generic: base.extend<{ loginPage: LoginPage }> — las llaves declaran el "menú" de fixtures nuevos y su tipo. Y el cuerpo del fixture DEBE llamar await use(...) exactamente una vez.
Sembrando futuro: en KATA recibirás ({ api }), ({ ui }), ({ test }) — fixtures exactamente como el que acabas de fabricar, con más capas dentro.
({ loginPage }) del import de fixtures propio y pasan en verde.Misión corta pero conceptualmente densa — circula y revisa los extend. Error típico: olvidar await use(loginPage) (el test nunca corre, timeout silencioso). La conexión KATA aquí es literal: el TestFixture L4 del boilerplate ES este archivo con más músculo. Quien termina esta misión ya entiende la mitad de la arquitectura del repo profesional.
Tu test de login por UI: ~5 segundos (browser, render, animaciones). El mismo login por API: ~200 ms — y no se rompe cuando muevan un botón.
Pruebas el VIAJE del usuario: flujos críticos, visual, interacción real.
Pruebas REGLAS de negocio: validaciones, permisos, datos, errores 4xx. La mayoría de tus casos edge.
Tiene API REST documentada: dojo.upexgalaxy.com/api/docs — ábrela AHORA y mira qué endpoints ofrece.
La pirámide sin dogma: heurístico de costo-beneficio, no religión. Mensaje práctico: "¿estás probando el viaje (UI) o la regla (API)?" Casos edge de validación por UI = lentitud multiplicada; por API son milisegundos. Pide que abran /api/docs en vivo — la misión P5 sale de ahí.
test("API: login devuelve sesión", async ({ request }) => { const res = await request.post("https://dojo.upexgalaxy.com/api/auth/login", { data: { email: "testuser@upex.dev", password: "Test123!" }, }); expect(res.status()).toBe(200); const body = await res.json(); expect(body).toHaveProperty("user"); }); test("API: login inválido responde 401", async ({ request }) => { const res = await request.post(".../api/auth/login", { data: { email: "testuser@upex.dev", password: "mala" }, }); expect(res.status()).toBe(401); });
Otro fixture de la casa — cliente HTTP listo, cero navegador.
post(url, { data }) envía JSON · res.status() el código · res.json() el cuerpo. Los status (200, 401, 404…) ya los conoces del oficio.
Rutas y campos exactos: en /api/docs del dojo. Estas slides muestran la FORMA — regla de oro del lab.
El segundo test (camino triste por API) es el argumento más fuerte: validar el 401 por UI cuesta segundos y depende de mensajes visuales; por API es directo al contrato. Nota que expect aquí no lleva await en status (no es web-first, es valor inmediato) — si alguien pregunta, esa es la razón; si no, no lo compliques.
tests/api/.
1. Abre dojo.upexgalaxy.com/api/docs y elige 2 endpoints
2. Sugerencia: login OK (200) y login inválido (401)
3. Créalos en tests/api/auth.spec.ts y corre solo esa carpeta:
TODO está en /api/docs: método (GET/POST), ruta, cuerpo esperado y respuestas posibles. Leer documentación de API es destreza de QA — esta misión la entrena tanto como el código.
Fíjate en la duración al terminar: tus API tests deberían correr en menos de 2 segundos. Compáralo con tus tests de UI — ese delta ES la pirámide.
tests/api/, validando status y al menos una propiedad del body.Doble entrenamiento: el código (sencillo) y LEER documentación de API (la destreza de fondo). Si el dojo expone endpoints GET públicos, son el segundo test más fácil. Al cierre, compara duraciones en vivo: suite UI vs carpeta api — el delta convence más que cualquier slide de pirámide.
El quiz aterriza la pirámide en una decisión real de diseño de suite. La fórmula "API prueba la regla, UI prueba la experiencia" es citable y se la llevarán al trabajo. La opción C existe porque la presión de tiempo real empuja ahí — nómbralo: con API testing, cubrir todo ES barato.
En el reporte: tus describe como capítulos, API tests en milisegundos, UI tests organizados. Esta estructura es una mini-versión de cómo se ve un framework profesional.
Los que tocan login deben pasar por LoginPage o el fixture. Los demás (navigation, components) pueden seguir directos — no fuerces POM donde no aporta TODAVÍA. Criterio > dogma.
El boss es integración, no contenido nuevo. La pista final enseña juicio: POM donde hay repetición real, directo donde no — "criterio > dogma" es seña de senior. El árbol final es deliberadamente una miniatura de KATA: fixtures.ts ≈ TestFixture, pages/ ≈ components L3. Esa rima se revela en el cierre.
| Lo tuyo (hoy) | KATA (el framework profesional) |
|---|---|
pages/login-page.ts | Page Component (capa L3) — con ATCs registrados |
fixtures.ts con ({ loginPage }) | TestFixture (L4) — entrega ({ ui }), ({ api }), ({ test }) |
| Helpers repetidos en cada POM | UiBase / ApiBase (L2) — heredados con extends |
| Credenciales hardcodeadas 😬 | TestContext (L1) — config + datos por ambiente |
tests/api/auth.spec.ts | Api Components con tipos generados del OpenAPI |
No aprendiste "Playwright avanzado" — construiste, a mano y a escala chica, cada pieza que KATA industrializa. El próximo deck cruza el puente.
Cierre conceptual potente: la tabla muestra que KATA no es un mundo nuevo sino la industrialización de lo que acaban de hacer a mano. La fila de credenciales hardcodeadas (que todos tienen) pica la curiosidad por TestContext. No expliques KATA aquí — eso es el deck 6 completo.
Termina misiones pendientes + crea el POM de una segunda página del dojo.
Git, GitHub y tu primer PR — para que esta suite viva en equipo, no en tu laptop.
El último deck: cruzar de tu mini-framework al boilerplate profesional.
Logro de hoy en una frase: pasaron de "tengo tests" a "tengo un framework chiquito": organización (hooks), robustez (locators), encapsulamiento (POM), inyección (fixtures) y estrategia (API vs UI). Quedan 2 decks: Dev Craft (compartir vía git/PR) y KATA Bridge (el destino final). El progreso de misiones queda guardado en este archivo.