🎯 0/6 · ⭐ 0/4
1 / 23
← → navegar · N notas presentador · F pantalla completa · click en comandos = copiar
🎙 Notas del presentador
Sesión 4 · QA Engineering

Playwright Pro 🚀

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.

describe + hookslocators proPOM realfixturesAPI testing6 misiones 🎯

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.

Diagnóstico

Tu suite funciona. Ahora mírala con ojos de senior.

🔁

Síntoma 1: login repetido

4 de tus 5 specs empiezan con los mismos pasos de login. Copy-paste detectado.

🗺️

Síntoma 2: selectores regados

El locator del botón de login vive en 4 archivos. Cambio de UI = cacería.

🐌

Síntoma 3: todo por UI

Cada verificación pasa por el navegador — lento y frágil para cosas que la API responde en milisegundos.

🗺️ El plan de hoy

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: capítulos para tus tests 📂

test.describe("Login", () => {

  test("con credenciales válidas entra al dashboard", async ({ page }) => {
    // ...
  });

  test("con password inválida muestra error", async ({ page }) => {
    // ...
  });
});

📖 Agrupa por feature

El reporte se lee como índice: Login → sus casos. Con 100 tests, los capítulos salvan vidas.

🎯 Corre un capítulo

npx playwright test -g "Login" ejecuta solo ese grupo.

🔧 Herramientas de foco

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.

beforeEach: el login se escribe UNA vez 🪝

❌ Antes — login copiado en cada test
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 😩
✅ Después — el hook lo hace por todos
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.

MISIÓN P1

Refactoriza con describe + beforeEach

🎯 Reorganizar tu suite del lab: agrupa los tests por feature y mueve el login repetido a un beforeEach.

1. Agrupa tus specs con test.describe por feature
2. Donde 2+ tests repitan login → test.beforeEach
3. Corre la suite completa de nuevo:

$npx playwright test📋
Pista: un test ahora falla

¿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.

Criterio de éxito: misma cantidad de tests, todos verdes, login escrito UNA sola vez por grupo.

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.

⭐ QUIZ 1 · Hooks

Un describe tiene 3 tests y un beforeEach con login. ¿Cuántas veces se ejecuta el login al correr el grupo?

💡 beforeEach = antes de CADA test. Eso garantiza aislamiento: cada test arranca limpio, sin heredar el estado (ni el desorden) del anterior. La opción A describe a 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.

La jerarquía: elige locators que sobreviven 🛡️

PrioridadLocatorPor 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.
getByTestId("submit-btn")Ancla dedicada a testing (data-testid) — pídela al equipo dev.
🚨 últimopage.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.

Encadenar y filtrar: apuntar fino 🎯

El problema: hay 10 filas, quiero UNA

// "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.

Aserciones web-first (esperan solas)

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.

MISIÓN P2

Caza de locators frágiles

🎯 Auditar tu suite y reemplazar todo selector CSS estructural por locators semánticos (getByRole / getByLabel / getByText).

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:

$npx playwright test --ui📋

3. Reemplaza, corre, verde.

Pista: el dojo no tiene label en un input

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.

Criterio de éxito: cero selectores CSS estructurales en tu suite (o cada excepción justificada con comentario) y todo verde.

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.

⭐ QUIZ 2 · Locators

El equipo rediseña el CSS completo de la app (clases nuevas, estructura nueva, mismos textos). ¿Cuál locator sigue funcionando?

💡 El rediseño cambió clases y estructura (matando A y B) pero el botón sigue SIENDO un botón que dice "Sign in" — el rol y el nombre accesible son el contrato con el usuario, y ese contrato es lo último que cambia. Locator semántico = apostarle al contrato, no a la implementación.

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.

Tu LoginPage… ahora de verdad 📄

pages/login-page.ts
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();
  }
}

🆕 vs Coding Classes

Misma clase que construiste en el simulador — con 3 upgrades de producción.

📍 Locators como campos

Declarados UNA vez en el constructor, tipados como Locator, reutilizados en todos los métodos.

🏷️ TypeScript real

type Page importado, readonly (no reasignable), private page = parámetro y propiedad en un paso.

📤 export

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.

El spec se queda limpio y legible

tests/login.spec.ts
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/);
  });
});

🗂️ Estructura nueva

mi-dojo-lab/
├─ pages/ ← tus Page Objects
│  └─ login-page.ts
└─ tests/ ← solo specs
   └─ login.spec.ts

📜 La división sagrada

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.

MISIÓN P3

Tu primer Page Object de producción

🎯 Crear pages/login-page.ts y refactorizar tus specs de login para usarlo.

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:

$npx playwright test login📋
Pista: "Cannot find module '../pages/login-page'"

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.

Reto extra si vas sobrado

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.

Criterio de éxito: tests de login verdes SIN ningún 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.

⭐ QUIZ 3 · POM

Según la división sagrada, ¿dónde vive expect(page).toHaveURL(/dashboard/)?

💡 Si login() incluyera el expect del dashboard, no podrías reusarlo para el caso "login inválido se queda en /login" — el método fallaría por diseño. Las acciones van al POM porque se REPITEN; las aserciones van al test porque CAMBIAN según el caso. Reusabilidad vs especificidad.

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.

Fixtures: crea tu propio ({ page }) 💉

fixtures.ts
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";
tests/login.spec.ts — ahora
import { test, expect } from "../fixtures";

test("login válido", async ({ loginPage, page }) => {
  await loginPage.login("testuser@upex.dev", "Test123!");
  await expect(page).toHaveURL(/dashboard/);
});

🤯 El círculo se cierra

({ 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.

MISIÓN P4

Fabrica tu fixture

🎯 Crear fixtures.ts con el fixture 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.

Pista: TypeScript se queja del extend

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.

Criterio de éxito: tus tests de login reciben ({ 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.

No todo merece un navegador: la pirámide 🔺

🖥️ UI / E2E — pocos, lentos, frágiles
🔌 API / integración — más, rápidos, estables
⚙️ Unit — muchos, instantáneos (territorio dev)

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.

🖥️ UI cuando…

Pruebas el VIAJE del usuario: flujos críticos, visual, interacción real.

🔌 API cuando…

Pruebas REGLAS de negocio: validaciones, permisos, datos, errores 4xx. La mayoría de tus casos edge.

🥋 El dojo te espera

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í.

request: Playwright sin navegador 🔌

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

💉 ({ request })

Otro fixture de la casa — cliente HTTP listo, cero navegador.

📨 Anatomía

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.

🗺️ Forma vs realidad

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.

MISIÓN P5

Tus primeros API tests

🎯 Explorar /api/docs del dojo y escribir 2 tests de API en 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:

$npx playwright test tests/api📋
Pista: ¿qué ruta y campos exactos?

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.

Criterio de éxito: 2 API tests verdes en 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.

⭐ QUIZ 4 · Estrategia

Debes probar 12 combinaciones de datos inválidos en el formulario de registro. ¿Estrategia profesional?

💡 Las REGLAS de validación viven en el backend → API: 12 casos en ~2 segundos, estables. Lo que la UI aporta de único es MOSTRAR el error → 1-2 tests de UI lo cubren. División del trabajo: API prueba la regla, UI prueba la experiencia. Cobertura completa, suite rápida.

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.

MISIÓN P6 · BOSS 🏆

La suite profesional, completa

🎯 Todo junto: estructura final ordenada, suite completa verde, reporte como evidencia.
mi-dojo-lab/
├─ fixtures.ts ← tus fixtures (P4)
├─ pages/
│  └─ login-page.ts ← POM (P3)
├─ tests/
│  ├─ login.spec.ts ← describe+fixture (P1, P4)
│  ├─ register.spec.ts
│  ├─ navigation.spec.ts
│  └─ api/
│     └─ auth.spec.ts ← API tests (P5)
└─ playwright.config.ts
$npx playwright test📋
$npx playwright show-report📋
📊

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.

Pista: specs que aún usan page.fill para login

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.

Criterio de éxito: árbol como el de la izquierda, suite completa verde (UI + API), reporte HTML navegable por capítulos.

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 que acabas de construir

Tu carpeta ya rima con KATA 🥋

Lo tuyo (hoy)KATA (el framework profesional)
pages/login-page.tsPage Component (capa L3) — con ATCs registrados
fixtures.ts con ({ loginPage })TestFixture (L4) — entrega ({ ui }), ({ api }), ({ test })
Helpers repetidos en cada POMUiBase / ApiBase (L2) — heredados con extends
Credenciales hardcodeadas 😬TestContext (L1) — config + datos por ambiente
tests/api/auth.spec.tsApi 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.

Debrief

Misiones completadas

🏠

Tarea

Termina misiones pendientes + crea el POM de una segunda página del dojo.

🛠️

Siguiente: Dev Craft

Git, GitHub y tu primer PR — para que esta suite viva en equipo, no en tu laptop.

🥋

Después: KATA Bridge

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.