From 1e896adeaa7820db4c81066e78842c16a30b5b48 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Sun, 8 Mar 2026 15:47:51 +0000 Subject: [PATCH] refactor: migrar tests E2E a persistent context global-setup.ts: - Usa launchPersistentContext en vez de browser.newContext() - Reemplaza storageState por marker file para cache de sesion - Captura logs de consola del browser para debug - Screenshots y HTML dump en caso de error playwright.config.ts: - Elimina storageState (ahora via persistent context fixture) - Screenshots siempre activas, video y trace en failures Tests (login, assistant-bot, asistente-2): - Importan test/expect desde persistent-context fixture - Usan handleElementDialogs() en vez de espera manual de rooms - Nuevo test de threads en asistente-2: verifica que el bot responde dentro del thread cuando se le habla por thread Co-Authored-By: Claude Opus 4.6 --- e2e/global-setup.ts | 87 ++++++++++++++++++++++++++------- e2e/playwright.config.ts | 9 ++-- e2e/tests/asistente-2.spec.ts | 43 +++++++++++++--- e2e/tests/assistant-bot.spec.ts | 9 +--- e2e/tests/login.spec.ts | 36 ++++---------- 5 files changed, 122 insertions(+), 62 deletions(-) diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts index aed4f4f..d377720 100644 --- a/e2e/global-setup.ts +++ b/e2e/global-setup.ts @@ -6,14 +6,19 @@ import { loginToElement } from "./fixtures/element-auth"; dotenv.config({ path: path.resolve(__dirname, ".env") }); -const AUTH_DIR = path.resolve(__dirname, ".auth"); -const STATE_PATH = path.join(AUTH_DIR, "state.json"); +const USER_DATA_DIR = path.resolve(__dirname, ".auth", "chrome-profile"); +const MARKER_PATH = path.resolve(__dirname, ".auth", "login-done.marker"); +const SCREENSHOTS_DIR = path.resolve(__dirname, "test-results", "global-setup"); /** - * Global setup: ejecuta login una vez y guarda storageState - * para reutilizar en todos los tests sin repetir el login. + * Global setup: ejecuta login una vez usando persistent context. * - * Si state.json ya existe y no esta expirado, lo reutiliza. + * A diferencia de storageState, el persistent context preserva IndexedDB + * (crypto keys de E2EE). Los tests usan el mismo userDataDir via el + * custom fixture persistent-context.ts. + * + * Si el marker file existe y no esta expirado, asumimos que la sesion + * sigue activa y saltamos el login. */ async function globalSetup() { const elementURL = process.env.ELEMENT_URL || "http://localhost:8080"; @@ -27,20 +32,45 @@ async function globalSetup() { ); } - // Reutilizar sesion cacheada si existe y tiene menos de 12 horas - if (isStateFresh(STATE_PATH, 12 * 60 * 60 * 1000)) { - console.log("[global-setup] Reutilizando sesion cacheada"); + // Reutilizar sesion cacheada si el marker existe y tiene menos de 12 horas + if (isMarkerFresh(MARKER_PATH, 12 * 60 * 60 * 1000)) { + console.log("[global-setup] Reutilizando sesion de persistent context"); return; } - console.log("[global-setup] Ejecutando login en Element Web..."); + console.log("[global-setup] Ejecutando login en Element Web con persistent context..."); + console.log(`[global-setup] URL: ${elementURL}`); + console.log(`[global-setup] User: ${user}`); + console.log(`[global-setup] UserDataDir: ${USER_DATA_DIR}`); - // Asegurar que el directorio .auth existe - fs.mkdirSync(AUTH_DIR, { recursive: true }); + // Limpiar perfil anterior para login fresco + if (fs.existsSync(USER_DATA_DIR)) { + fs.rmSync(USER_DATA_DIR, { recursive: true }); + console.log("[global-setup] Perfil anterior eliminado"); + } - const browser = await chromium.launch(); - const context = await browser.newContext(); - const page = await context.newPage(); + // Asegurar que los directorios existen + fs.mkdirSync(USER_DATA_DIR, { recursive: true }); + fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true }); + + // Usar persistent context — preserva IndexedDB (crypto keys E2EE) + const context = await chromium.launchPersistentContext(USER_DATA_DIR, { + headless: true, + viewport: { width: 1280, height: 720 }, + }); + + const page = context.pages()[0] || (await context.newPage()); + + // Capturar logs de consola del browser + page.on("console", (msg) => { + const type = msg.type(); + if (type === "error" || type === "warning") { + console.log(`[browser-${type}] ${msg.text()}`); + } + }); + page.on("pageerror", (err) => { + console.error(`[browser-error] ${err.message}`); + }); try { await loginToElement(page, { @@ -48,17 +78,36 @@ async function globalSetup() { user, password, recoveryKey, + screenshotsDir: SCREENSHOTS_DIR, }); - await context.storageState({ path: STATE_PATH }); - console.log("[global-setup] Sesion guardada en", STATE_PATH); + // Crear marker de sesion exitosa + fs.writeFileSync(MARKER_PATH, new Date().toISOString()); + console.log("[global-setup] Login completado, marker creado"); + + await page.screenshot({ + path: path.join(SCREENSHOTS_DIR, "05-login-complete.png"), + fullPage: true, + }); + } catch (err) { + console.error("[global-setup] ERROR durante login:", err); + await page.screenshot({ + path: path.join(SCREENSHOTS_DIR, "ERROR-login-failed.png"), + fullPage: true, + }); + const html = await page.content(); + fs.writeFileSync( + path.join(SCREENSHOTS_DIR, "ERROR-page-content.html"), + html + ); + throw err; } finally { - await browser.close(); + await context.close(); } } -/** Verifica si el archivo de estado existe y tiene menos de maxAge ms. */ -function isStateFresh(filePath: string, maxAge: number): boolean { +/** Verifica si el marker file existe y tiene menos de maxAge ms. */ +function isMarkerFresh(filePath: string, maxAge: number): boolean { try { const stat = fs.statSync(filePath); return Date.now() - stat.mtimeMs < maxAge; diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index b697cff..aa27473 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -19,12 +19,15 @@ export default defineConfig({ use: { baseURL: process.env.ELEMENT_URL || "http://localhost:8080", headless: true, - screenshot: "only-on-failure", - trace: "on-first-retry", + screenshot: "on", + trace: "retain-on-failure", + video: "retain-on-failure", actionTimeout: 30_000, - storageState: path.resolve(__dirname, ".auth/state.json"), + // NO usamos storageState — usamos persistent context para preservar IndexedDB }, + outputDir: "./test-results", + globalSetup: "./global-setup.ts", projects: [ diff --git a/e2e/tests/asistente-2.spec.ts b/e2e/tests/asistente-2.spec.ts index e6bc093..16ae1ee 100644 --- a/e2e/tests/asistente-2.spec.ts +++ b/e2e/tests/asistente-2.spec.ts @@ -1,20 +1,18 @@ -import { test, expect } from "@playwright/test"; +import { test, expect, handleElementDialogs } from "../fixtures/persistent-context"; import { goToRoom, sendMessage, waitForBotReply, assertNoDecryptionErrors, + startThreadOnLastMessage, + sendThreadMessage, + waitForThreadReply, } from "../fixtures/matrix-room"; test.describe("asistente-2", () => { test.beforeEach(async ({ page }) => { await page.goto("/"); - - // Esperar a que la sesion este lista - await expect( - page.locator('[role="tree"][aria-label="Rooms"]') - ).toBeVisible({ timeout: 30_000 }); - + await handleElementDialogs(page); await goToRoom(page, "Asistente 2"); }); @@ -65,6 +63,37 @@ test.describe("asistente-2", () => { expect(reply.toLowerCase()).toContain("ping"); }); + test("responde dentro del thread cuando se le habla por thread", async ({ + page, + }) => { + // 1. Enviar un mensaje normal (sera el thread root) + await sendMessage(page, "Mensaje para iniciar thread"); + + // Esperar a que el bot responda al mensaje original + await waitForBotReply(page, { + timeout: 60_000, + sender: "Asistente 2", + }); + + // 2. Iniciar thread sobre el mensaje del usuario + await startThreadOnLastMessage(page); + + // 3. Enviar mensaje dentro del thread + await sendThreadMessage( + page, + "Hola desde el thread, respondeme aqui por favor" + ); + + // 4. Esperar que el bot responda DENTRO del thread + const threadReply = await waitForThreadReply(page, { + timeout: 60_000, + sender: "Asistente 2", + }); + + expect(threadReply).toBeTruthy(); + expect(threadReply.length).toBeGreaterThan(5); + }); + test("no hay errores de E2EE en el timeline", async ({ page }) => { await assertNoDecryptionErrors(page); }); diff --git a/e2e/tests/assistant-bot.spec.ts b/e2e/tests/assistant-bot.spec.ts index a75b73b..6aa922f 100644 --- a/e2e/tests/assistant-bot.spec.ts +++ b/e2e/tests/assistant-bot.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test"; +import { test, expect, handleElementDialogs } from "../fixtures/persistent-context"; import { goToRoom, sendMessage, @@ -9,12 +9,7 @@ import { test.describe("assistant-bot", () => { test.beforeEach(async ({ page }) => { await page.goto("/"); - - // Esperar a que la sesion este lista - await expect( - page.locator('[role="tree"][aria-label="Rooms"]') - ).toBeVisible({ timeout: 30_000 }); - + await handleElementDialogs(page); await goToRoom(page, "Assistant"); }); diff --git a/e2e/tests/login.spec.ts b/e2e/tests/login.spec.ts index 9f53e84..f3761f3 100644 --- a/e2e/tests/login.spec.ts +++ b/e2e/tests/login.spec.ts @@ -1,54 +1,38 @@ -import { test, expect } from "@playwright/test"; +import { test, expect, handleElementDialogs } from "../fixtures/persistent-context"; import { assertNoDecryptionErrors } from "../fixtures/matrix-room"; test.describe("Login y sesion E2EE", () => { - test("storageState cargado — rooms visibles en sidebar", async ({ - page, - }) => { + test("sesion cargada — rooms visibles en sidebar", async ({ page }) => { await page.goto("/"); + await handleElementDialogs(page); - // Si la sesion esta cacheada, Element debe mostrar rooms directamente - await expect( - page.locator('[role="tree"][aria-label="Rooms"]') - ).toBeVisible({ timeout: 30_000 }); + // Si llegamos aqui, handleElementDialogs ya verifico rooms sidebar + const rooms = page.locator('[role="treeitem"]'); + const roomCount = await rooms.count(); + expect(roomCount).toBeGreaterThan(0); }); test("no hay mensajes Unable to decrypt en rooms recientes", async ({ page, }) => { await page.goto("/"); - - // Esperar a que cargue la lista de rooms - await expect( - page.locator('[role="tree"][aria-label="Rooms"]') - ).toBeVisible({ timeout: 30_000 }); + await handleElementDialogs(page); // Abrir el primer room visible para verificar mensajes - const firstRoom = page - .locator('[role="treeitem"]') - .first(); + const firstRoom = page.locator('[role="treeitem"]').first(); const roomCount = await firstRoom.count(); if (roomCount > 0) { await firstRoom.click(); - - // Esperar a que cargue el timeline await page.waitForTimeout(3_000); - - // Verificar que no hay errores de desencriptado await assertNoDecryptionErrors(page); } }); test("helpers de room navegan correctamente", async ({ page }) => { await page.goto("/"); + await handleElementDialogs(page); - // Esperar a que la sesion este lista - await expect( - page.locator('[role="tree"][aria-label="Rooms"]') - ).toBeVisible({ timeout: 30_000 }); - - // Verificar que hay al menos un room en el sidebar const rooms = page.locator('[role="treeitem"]'); const roomCount = await rooms.count(); expect(roomCount).toBeGreaterThan(0);