Del código suelto a las clases.
El último escalón antes de la arquitectura profesional de automation.
Sesión 2 de la serie. Prerequisito: Coding Base (valores, variables, funciones, loops, objetos, async/await, anatomía de un test). Hoy: el código moderno que ven en specs reales + clases y herencia — EL concepto que toda arquitectura de automation usa. Final: construyen su primer Page Object y lo ven ejecutarse en vivo.
Valores · variables · operadores · funciones · if/else · loops · arrays · objetos · async/await · tu primer test ejecutado.
JS moderno (el que ves en specs reales) → arrays con superpoderes → clases y herencia → tu primer Page Object.
// El zoom-out sigue creciendo: función ← ya la dominas ↓ class ← hoy: funciones + datos juntos ↓ Page Object ← hoy: una clase que envuelve una página ↓ Framework KATA ← próxima parada: el repo profesional
Recuerda la mecánica del zoom-out de la sesión 1: nada aparece de la nada. La función que dominan es el ladrillo; hoy lo apilan en clases; el Page Object es una clase con propósito de testing; KATA (el framework profesional) es Page Objects con esteroides. Hoy cierran el gap más grande del roadmap.
const creds = { email: "ana@qa.com", password: "secreto123" };
creds, dame email". Lo usaste 20 veces en la sesión 1 — y hoy lo vas a usar con this: this.email significa "de ESTA instancia, dame email". Guarda esa idea.Calentamiento con doble propósito: reactivar el conocimiento de objetos Y plantar la semilla de this (la explicación menciona this.email). Si la sala falla este quiz, dedica 5 minutos a repasar objetos de Coding Base antes de seguir — clases sin objetos claros no funcionan.
const msg = "Hola, " + name + "! Tienes " + bugs + " bugs";
Funciona, pero con 3+ variables se vuelve sopa de comillas y signos +.
const msg = `Hola, ${name}! Tienes ${bugs} bugs`;
Backticks ` (no comillas) + ${...} = hueco donde entra cualquier variable o expresión.
El backtick ` está junto al 1 en teclado US, o AltGr + } en teclado ES/LA. Práctica obligada: lo usarás en cada test.
Concepto trivial pero de uso diario — el spec real está lleno de template literals. Truco de retención: "string con huecos". Importante señalar dónde está el backtick en SU teclado — es la fricción real #1 con esta sintaxis. Todo lo de sesión 1 con "+" sigue siendo válido; esto es ergonomía.
La misma función, en 3 pasos de transformación:
// 1. La clásica de la sesión 1 function double(n) { return n * 2; } // 2. Misma función, con flecha const double = (n) => { return n * 2; }; // 3. Una sola expresión → return implícito const double = (n) => n * 2;
=>Se lee: "recibe n → devuelve n * 2". Función compacta, ideal para pasarla a OTRA función.
test("login", async ({ page }) => { ... });
¡Tu test de la sesión 1 ERA una arrow function! Hoy por fin sabes leer cada símbolo.
El momento "¡ahhh!" del acto: el test() que escribieron en sesión 1 ya usaba arrow + async. La transformación en 3 pasos es el camino mental — muéstrala despacio. Para qué sirve la compacidad: pasarle funciones a map/filter (siguiente acto). No menciones diferencias de this entre function y arrow — ruido innecesario en este nivel.
const user = { name: "Ana", role: "admin" }; // ayer: uno por uno const name = user.name; const role = user.role; // hoy: desempaca de un tirón const { name, role } = user;
Llaves a la IZQUIERDA del = significan "extrae estas propiedades del objeto de la derecha". Los nombres deben coincidir.
async ({ page }) => { ... }
Playwright entrega un objeto LLENO de herramientas. ({ page }) = "del paquete que me das, solo desempaco page". Misterio de la sesión 1: cerrado. 🔓
Segundo "¡ahhh!" del acto: ({ page }) por fin explicado. Playwright pasa un objeto de fixtures (page, request, context, browser...) y el test desempaca lo que necesita. Cuando lleguen a KATA verán ({ api }), ({ ui }), ({ test }) — exactamente el mismo patrón. Esa conexión vale la sesión entera.
Los 3 conceptos del acto en una celda. Error esperable del reto 1: usar comillas normales en vez de backticks → ${} sale literal en el texto. Es el error PERFECTO: muéstralo y todos recordarán la diferencia para siempre.
test("login", async ({ page }) => {...}) — ¿qué hace ({ page })?page, request, context…) y tú desempacas solo lo que usas. Por eso a veces verás ({ page, request }) — desempacando dos.Valida el concepto más transferible del acto. La mención de ({ page, request }) prepara terreno para tests híbridos UI+API que verán en Playwright Pro. Si dudan entre A y B: vuelve al ejemplo de user = {name, role} — mismo patrón, solo que dentro de paréntesis de función.
const users = ["ana", "luis", "mia"]; const emails = users.map(u => `${u}@qa.com`); // ["ana@qa.com", "luis@qa.com", "mia@qa.com"]
Se lee: "por cada u del array, devuélveme `${u}@qa.com`". Array nuevo, mismo tamaño, elementos transformados.
const emails = []; for (const u of users) { emails.push(`${u}@qa.com`); }
Mismo resultado, 4 líneas. map lo dice en 1: arrow functions pagando dividendos.
Generar emails de prueba, extraer ids de una respuesta de API, preparar la columna de datos que el test necesita.
map/filter/find existen porque las arrow functions hacen barato pasar "mini-recetas" a otros métodos. Lectura en voz alta del patrón: "por cada u, devuélveme X". El loop de ayer NO queda obsoleto — map es el atajo para el caso más común (transformar lista completa).
const users = [ { name: "Ana", role: "admin" }, { name: "Luis", role: "viewer" }, { name: "Mia", role: "admin" }, ]; const admins = users.filter(u => u.role === "admin"); // [Ana, Mia] — array, puede venir vacío []
const luis = users.find(u => u.name === "Luis"); // { name: "Luis", role: "viewer" } // UN objeto — o undefined si nadie cumple
Tu tabla de test data, filtrada con código: "dame solo los admins", "encuentra el usuario inactivo". La condición es la MISMA del if/else de ayer.
Distinción clave: filter devuelve ARRAY (posiblemente vacío), find devuelve UN elemento (o undefined — conecta con el undefined que vieron en sesión 1). La condición dentro es una expresión booleana idéntica a las del if. Trío completo: map transforma, filter selecciona varios, find busca uno.
El reto 2 introduce encadenamiento (.filter().map()) — patrón que verán constantemente en código real. Si alguien pregunta por qué console.log(names) imprime con corchetes: los arrays se imprimen completos, útil para depurar. Deja 3-4 minutos de juego libre con los datos.
[1, 2, 3, 4].filter(n => n > 2)?filter = colador: deja pasar los elementos cuya condición da true y devuelve un array nuevo con ellos. La opción B describe a find (que devolvería 3, sin corchetes) — esa es exactamente la diferencia entre ambos.La opción B está diseñada para discutir filter vs find una vez más — distinción que en código real causa bugs (esperar un objeto y recibir un array). Cierre del acto: ya saben transformar y seleccionar datos como profesionales.
const tester1 = { name: "Ana", bugs: 0 }; const tester2 = { name: "Luis", bugs: 0 }; function findBug(tester) { tester.bugs = tester.bugs + 1; } // ¿qué funciones van con qué objetos? // ¿quién me garantiza que tester tiene .bugs?
Objetos por un lado, funciones por otro. Con 5 objetos y 15 funciones, nadie sabe qué combina con qué.
Una clase junta en un solo lugar: los datos (propiedades) + las funciones que operan sobre ellos (métodos). Un molde 🍪 que fabrica objetos ya equipados.
Motivación ANTES de sintaxis: el dolor es real (desorden de funciones y objetos sueltos) y la clase es la solución natural. Analogía molde de galletas: la clase es el MOLDE, los objetos que fabrica son las GALLETAS — cada una independiente pero con la misma forma. Esa analogía carga toda la sección.
class Tester { constructor(name) { this.name = name; this.bugs = 0; } findBug() { this.bugs = this.bugs + 1; return `${this.name}: ${this.bugs} bugs`; } }
"Defino el molde Tester." Nombre SIEMPRE con mayúscula inicial — convención universal.
La función que corre automáticamente al fabricar cada objeto. Recibe los datos iniciales.
"Este objeto en particular" — la galleta que se está fabricando o usando, no el molde.
Un método: función que vive DENTRO de la clase y trabaja con los datos de this. Sin la palabra function.
Cuatro piezas, una por tarjeta. this es LA dificultad — definición operativa: "el objeto concreto con el que se está trabajando ahora". No filosofar más: la siguiente slide fabrica instancias y la 16 lo muestra paso a paso en memoria. Detalle sintáctico: métodos sin la palabra function, sin comas entre ellos.
const ana = new Tester("Ana"); const luis = new Tester("Luis"); ana.findBug(); // "Ana: 1 bugs" ana.findBug(); // "Ana: 2 bugs" luis.findBug(); // "Luis: 1 bugs" ← ¡SU propio contador!
"Fabrica un objeto con el molde Tester" → corre el constructor con name = "Ana" → entrega la instancia.
ana.bugs y luis.bugs son contadores independientes. El molde es uno; las galletas, muchas.
page de Playwright… es una instancia de una clase. page.click() = método. Llevas una sesión entera usando clases.
Independencia de instancias = concepto del quiz 3, márcalo fuerte: cada new crea memoria nueva. El spoiler de page es otro puente potente: no van hacia territorio desconocido, van hacia entender lo que YA usan. Pregunta de control antes de seguir: "si hago new Tester('Mia') y llamo findBug una vez, ¿cuánto vale mia.bugs?"
— vacía —
La slide más importante de la sesión. Pausa en el paso 3 (this → instancia ana durante el constructor) y en el 5 (this → ana durante greet). Mensaje final: this apunta SIEMPRE al objeto a la izquierda del punto en la llamada — ana.greet() → this es ana. Con esa regla resuelven el 95% de los casos que verán.
Primera clase escrita por ellos. El reto (método report) valida la sintaxis de métodos: sin function, usando this. Error esperable: olvidar this y escribir name a secas → ReferenceError: name is not defined — error pedagógicamente perfecto: dentro de un método, los datos de la instancia SIEMPRE viajan con this.
luis.bugs?const ana = new Tester("Ana"); const luis = new Tester("Luis"); ana.findBug(); ana.findBug(); ana.findBug();
new fabrica objetos independientes: los 3 findBug() fueron sobre ana (this = ana), así que luis.bugs sigue en su valor inicial 0 del constructor. El molde es uno, cada galleta lleva su propia cuenta. 🍪Valida los DOS conceptos críticos juntos: independencia de instancias + this resolviéndose al objeto de la llamada. La opción C tienta a quien cree que las propiedades solo existen si se usan — el constructor ya las inicializó. Si aciertan masivamente: el acto más difícil está ganado.
class Admin { constructor(name) { this.name = name; } login() { return `${this.name} entró`; } deleteProject() { return "💥 borrado"; } }
class Viewer { constructor(name) { this.name = name; } login() { return `${this.name} entró`; } watch() { return "👀 mirando"; } }
constructor y login() están copiados y pegados. ¿Te suena? Es el mismo dolor de los test cases repetidos de la sesión 1 — y la solución tiene la misma forma: escribir lo común UNA vez.
Herencia motivada por el mismo principio que motivó funciones en sesión 1: no repetir. Admin y Viewer comparten constructor y login; difieren en un método propio cada uno. Pregunta a la sala: "¿dónde han visto este copy-paste antes?" — la respuesta (test cases duplicados) les pertenece y hace suya la solución.
class User { constructor(name) { this.name = name; } login() { return `${this.name} entró`; } } class Admin extends User { deleteProject() { return "💥 borrado"; } } const ana = new Admin("Ana"); ana.login(); // heredado del padre ✅ ana.deleteProject(); // propio de Admin ✅
"Admin es un User, más sus extras." Hereda constructor, propiedades y métodos del padre — gratis.
Si el hijo define su PROPIO constructor, la primera línea debe ser super(name): "padre, haz tu parte primero". Si no define constructor, hereda el del padre directo.
El framework KATA al que vas es una cadena de extends: tu Page hereda docenas de helpers ya escritos. Entender esto = leer KATA sin miedo.
Lectura de extends: "es un... más extras". super solo cuando el hijo define constructor propio — el ejemplo lo evita a propósito para mostrar el caso simple primero; el playground siguiente SÍ usa super. La tarjeta KATA es el porqué de toda la sesión: en el repo profesional, LoginPage extends UiBase extends TestContext — leerán esa cadena con naturalidad.
Aquí super aparece con su comentario inline. Experimento valioso en vivo: borrar la línea super(name) y ejecutar → "ReferenceError: Must call super constructor..." — el lenguaje OBLIGA a llamar al padre primero. El reto Viewer es deliberadamente simétrico al ejemplo de la slide 19: cierra el círculo del dolor → solución.
class Admin extends User — ¿qué hace super(name)?super(name) = "padre, ejecuta TU constructor con este dato". Así User asigna this.name y luego Admin agrega lo suyo. Sin esa llamada, el lenguaje lanza error: el hijo no puede usar this antes de que el padre haga su parte.Último concepto duro de la sesión. Si vieron el experimento de borrar super en el playground, este quiz es cosecha directa. Con esto cerrado, todo lo que queda (Page Object) es APLICAR lo aprendido — anúncialo así para bajar la tensión: "lo difícil ya pasó".
test("login exitoso", async ({ page }) => { await page.fill("#email", "ana@qa.com"); ┐ await page.fill("#password", "secreto123"); │ los mismos await page.click("#login-btn"); ┘ 3 pasos… await expect(page.locator("#welcome")).toBeVisible(); }); test("login con password inválida", async ({ page }) => { await page.fill("#email", "ana@qa.com"); ┐ await page.fill("#password", "malamala"); │ …otra vez… await page.click("#login-btn"); ┘ await expect(page.locator("#error-msg")).toBeVisible(); }); // test 3, test 4, test 5… ¿y si mañana #login-btn cambia de id? 💀
Deja que ELLOS nombren el problema antes de decirlo: pasos duplicados + selectores regados por todos lados. La pregunta asesina: "si el dev renombra #login-btn, ¿cuántos lugares corriges?" Con 50 tests, 50 lugares. Ya saben la herramienta para encapsular: una clase. La siguiente slide la presenta con nombre y apellido.
class LoginPage { constructor(page) { this.page = page; // guarda el "control remoto" } async login(email, password) { await this.page.fill("#email", email); await this.page.fill("#password", password); await this.page.click("#login-btn"); } }
TODO lo que se puede hacer en la página de login, encapsulado en UNA clase. Los selectores viven aquí y solo aquí.
Recibe el page de Playwright y lo guarda en this.page — el control remoto del navegador, ahora de la instancia.
class ✓ · constructor ✓ · this ✓ · método async ✓ · await ✓. Cero conceptos nuevos — solo composición.
Énfasis: NO hay nada nuevo en esta slide — es la clase de Tester con page adentro. this.page.fill = "del control remoto de ESTA instancia, llama fill". Nombre oficial: Page Object Model (POM), patrón estándar de la industria — suelta el término para que lo reconozcan en artículos y entrevistas.
test("login exitoso", async ({ page }) => { await page.fill("#email", "ana@qa.com"); await page.fill("#password", "secreto123"); await page.click("#login-btn"); await expect(page.locator("#welcome")) .toBeVisible(); });
test("login exitoso", async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.login("ana@qa.com", "secreto123"); await expect(page.locator("#welcome")) .toBeVisible(); });
El test ahora se lee como un test case manual: "crea la página de login, entra con estas credenciales, verifica la bienvenida". Y si #login-btn cambia → corriges 1 línea dentro de LoginPage, no 50 tests.
Doble victoria a destacar: legibilidad (el test se lee como su test case manual — círculo completo con la slide 2 de Coding Base) y mantenimiento (selector cambia → 1 solo lugar). La aserción se queda en el test a propósito: las acciones van al Page Object, los expect se quedan donde se decide pasa/falla. Esa división reaparece en KATA.
Acceso a tu cuenta
Clímax de la serie: a diferencia del simulador de Coding Base (que interpretaba línea a línea), este EJECUTA el JavaScript de verdad — su clase LoginPage corre tal cual correría en Playwright. Demos en vivo: (1) ejecutar tal cual → PASS; (2) password incorrecta → FAIL con expected/received; (3) el experimento estrella: cambiar "#login-btn" por "#boton-entrar" DENTRO de la clase → error "No existe el elemento" → corregir EN UN SOLO LUGAR → verde. Ese momento ES el argumento del Page Object.
#login-btn a #signin-btn. Tienes 50 tests que hacen login usando LoginPage. ¿Cuántos lugares corriges?loginPage.login(...) y ni se enteran del cambio. Acabas de entender por qué TODA arquitectura profesional de automation (incluido KATA) se construye sobre clases.Si hicieron el experimento del selector en el simulador, este quiz es la formalización de lo que VIVIERON. La opción B caza al optimista mágico — ninguna herramienta adivina renombres. Respuesta correcta masiva aquí = sesión cumplida.
Cada capa es una clase que extends la anterior — la cadena de herencia que acabas de dominar. Tu LoginPage de hoy es el corazón del framework profesional.
Teaser, NO lección: no expliques cada capa — solo muestra que la estrella verde (la Page que construyeron HOY) es el centro del framework, y que el resto son las mismas dos herramientas de esta sesión (extends + fixtures/destructuring) aplicadas con más capas. El miedo a la arquitectura muere cuando reconocen las piezas. Próximos pasos del roadmap: práctica real con Playwright contra el dojo, luego el puente KATA completo.
Este archivo funciona offline: re-juega los 4 playgrounds y los retos.
Escribe un SignupPage imaginario en papel: ¿qué métodos tendría? ¿qué selectores guardaría?
Playwright real contra UPEX Dojo: instalas el kit y tus Page Objects controlan un navegador DE VERDAD. 🚀
Hoy: JS moderno → map/filter/find → clases → this → herencia → tu primer Page Object. El gap más grande del camino: cerrado.
Celebra el score y nombra el logro exacto: hoy cruzaron la frontera entre "escribir scripts" y "construir arquitectura". El reto puente (SignupPage en papel) consolida sin computadora. Anuncia la sesión Dojo Lab: misma LoginPage, navegador real, app real (dojo.upexgalaxy.com). Comparte el archivo HTML.