import { Page, expect } from "@playwright/test"; import * as path from "path"; export interface LoginOptions { url: string; user: string; password: string; recoveryKey: string; /** Directorio donde guardar screenshots de debug (opcional) */ screenshotsDir?: string; } async function screenshot(page: Page, dir: string | undefined, name: string) { if (!dir) return; const filePath = path.join(dir, name); await page.screenshot({ path: filePath, fullPage: true }); console.log(`[login] Screenshot: ${name}`); } /** * Ejecuta el flujo completo de login en Element Web: * 1. Navegar a Element Web * 2. Click "Sign in" * 3. Ingresar usuario y contraseña * 4. Detectar errores (M_LIMIT_EXCEEDED, etc.) y reintentar * 5. Manejar verificacion de dispositivo con recovery key * 6. Verificar login exitoso (lista de rooms visible) */ export async function loginToElement(page: Page, opts: LoginOptions) { const ssDir = opts.screenshotsDir; console.log(`[login] Navegando a ${opts.url}`); await page.goto(opts.url); await screenshot(page, ssDir, "01-element-loaded.png"); // Esperar a que cargue Element y aparezca el boton de login console.log("[login] Buscando boton 'Sign in'..."); const signInLink = page.getByRole("link", { name: "Sign in" }); const hasSignIn = await signInLink .waitFor({ state: "visible", timeout: 15_000 }) .then(() => true) .catch(() => false); if (hasSignIn) { console.log("[login] Click en 'Sign in'"); await signInLink.click(); } else { console.log( "[login] No se encontro 'Sign in' link — puede que ya estemos en la pagina de login" ); await screenshot(page, ssDir, "01b-no-signin-link.png"); } await screenshot(page, ssDir, "02-signin-page.png"); // Intentar login con reintentos para M_LIMIT_EXCEEDED const maxRetries = 3; for (let attempt = 1; attempt <= maxRetries; attempt++) { console.log(`[login] Intento de login ${attempt}/${maxRetries}`); // Rellenar credenciales console.log(`[login] Rellenando credenciales para: ${opts.user}`); const usernameField = page.getByRole("textbox", { name: "Username" }); const hasUsername = await usernameField .waitFor({ state: "visible", timeout: 10_000 }) .then(() => true) .catch(() => false); if (!hasUsername) { // Puede que ya hayamos pasado la pantalla de login (sesion activa) console.log("[login] Campo Username no encontrado — verificando si ya hay sesion..."); await screenshot(page, ssDir, `ERROR-no-username-attempt${attempt}.png`); const roomsTree = page.locator('[role="tree"][aria-label="Rooms"], .mx_RoomList, .mx_LeftPanel_roomListContainer, .mx_RoomTile').first(); const alreadyLoggedIn = await roomsTree .waitFor({ state: "visible", timeout: 5_000 }) .then(() => true) .catch(() => false); if (alreadyLoggedIn) { console.log("[login] Ya hay sesion activa, saltando login"); return; } throw new Error("Campo Username no encontrado y no hay sesion activa"); } await usernameField.fill(opts.user); await page.getByRole("textbox", { name: "Password" }).fill(opts.password); await screenshot(page, ssDir, `02b-credentials-filled-attempt${attempt}.png`); console.log("[login] Click en 'Sign in' button"); await page.getByRole("button", { name: "Sign in" }).click(); await screenshot(page, ssDir, `03-after-signin-click-attempt${attempt}.png`); // Esperar resultado: o bien aparece el verify prompt / rooms, // o bien aparece un error console.log("[login] Esperando resultado del login..."); const result = await waitForLoginResult(page); if (result === "success") { console.log("[login] Login exitoso (rooms visibles o verify prompt)"); break; } if (result === "rate_limited") { const waitSecs = 10 * attempt; // 10s, 20s, 30s console.log( `[login] Rate limited (M_LIMIT_EXCEEDED). Esperando ${waitSecs}s antes de reintentar...` ); await screenshot(page, ssDir, `ERROR-rate-limited-attempt${attempt}.png`); if (attempt === maxRetries) { throw new Error( `Login fallido despues de ${maxRetries} intentos: M_LIMIT_EXCEEDED. ` + "El homeserver esta limitando los intentos de login. Esperar unos minutos." ); } await page.waitForTimeout(waitSecs * 1000); // Recargar pagina para limpiar estado await page.goto(opts.url + "/#/login"); await page.waitForTimeout(2_000); continue; } // Otro error console.error(`[login] Error de login: ${result}`); await screenshot(page, ssDir, `ERROR-login-attempt${attempt}.png`); if (attempt === maxRetries) { throw new Error(`Login fallido despues de ${maxRetries} intentos: ${result}`); } // Esperar un poco antes de reintentar await page.waitForTimeout(3_000); } // Manejar cross-signing: verificar con recovery key console.log("[login] Esperando prompt de cross-signing..."); await handleCrossSigning(page, opts.recoveryKey, ssDir); // Verificar login exitoso: rooms visibles en el sidebar console.log("[login] Verificando que rooms sidebar es visible..."); const roomsVisible = await page .locator('[role="tree"][aria-label="Rooms"]') .waitFor({ state: "visible", timeout: 30_000 }) .then(() => true) .catch(() => false); if (!roomsVisible) { await screenshot(page, ssDir, "ERROR-no-rooms-after-login.png"); throw new Error("Rooms sidebar no visible despues del login completo"); } await screenshot(page, ssDir, "04-rooms-visible.png"); console.log("[login] Login completado exitosamente"); } /** * Espera el resultado del login: exito, rate_limited, u otro error. * Retorna "success" si el login progresó (verify prompt o rooms visibles), * "rate_limited" si hay M_LIMIT_EXCEEDED, o el texto del error. */ async function waitForLoginResult(page: Page): Promise { const timeout = 20_000; const startTime = Date.now(); while (Date.now() - startTime < timeout) { // Verificar si hay un error visible en la pagina de login const errorAlert = page.locator('[role="alert"], .mx_Login_error, .mx_ErrorMessage'); const errorCount = await errorAlert.count(); if (errorCount > 0) { const errorText = await errorAlert.first().textContent(); if (errorText) { console.log(`[login] Error detectado: "${errorText}"`); if (errorText.includes("M_LIMIT_EXCEEDED") || errorText.includes("rate")) { return "rate_limited"; } return errorText; } } // Verificar si hay texto de error generico en la pagina const pageText = await page.locator(".mx_Login_header, .mx_AuthBody").textContent().catch(() => ""); if (pageText && pageText.includes("M_LIMIT_EXCEEDED")) { return "rate_limited"; } // Verificar si el login progresó (verify prompt) const verifyButton = page.getByRole("button", { name: /verify with security key|use security key/i, }); if (await verifyButton.isVisible().catch(() => false)) { return "success"; } // Verificar si ya estamos en el home (rooms visibles) const roomsTree = page.locator('[role="tree"][aria-label="Rooms"], .mx_RoomList, .mx_LeftPanel_roomListContainer, .mx_RoomTile').first(); if (await roomsTree.isVisible().catch(() => false)) { return "success"; } // Verificar si hay un dialogo de "Verify this device" u otro post-login const verifyDialog = page.locator('.mx_AuthPage_modal, .mx_Dialog, [role="dialog"]'); if (await verifyDialog.isVisible().catch(() => false)) { return "success"; } await page.waitForTimeout(500); } // Si despues del timeout seguimos en la pagina de Sign In, es error const stillOnLogin = await page.locator('button:has-text("Sign in")').isVisible().catch(() => false); if (stillOnLogin) { // Capturar cualquier texto de error visible const bodyText = await page.locator(".mx_AuthBody").textContent().catch(() => ""); if (bodyText && bodyText.includes("M_LIMIT_EXCEEDED")) { return "rate_limited"; } return `Timeout: aun en pagina de login. Body: ${bodyText?.substring(0, 200)}`; } return "success"; } /** * Maneja el prompt de verificacion de dispositivo despues del login. * Element puede mostrar un dialogo pidiendo verificar el dispositivo * via otro dispositivo o via recovery key. */ async function handleCrossSigning( page: Page, recoveryKey: string, ssDir?: string ) { // Element muestra un dialogo de verificacion de dispositivo. // Intentar usar "Verify with Security Key" si aparece. const verifyButton = page.getByRole("button", { name: /verify with security key|use security key/i, }); // El dialogo puede tardar en aparecer tras el login const hasVerifyPrompt = await verifyButton .waitFor({ state: "visible", timeout: 15_000 }) .then(() => true) .catch(() => false); if (!hasVerifyPrompt) { console.log( "[login] No hubo prompt de verificacion — sesion ya verificada o login directo" ); await screenshot(page, ssDir, "03b-no-verify-prompt.png"); return; } console.log("[login] Prompt de verificacion detectado, clickeando..."); await screenshot(page, ssDir, "03c-verify-prompt.png"); await verifyButton.click(); // Ingresar recovery key en el campo de texto console.log("[login] Ingresando recovery key..."); const keyInput = page.getByRole("textbox"); await keyInput.fill(recoveryKey); await screenshot(page, ssDir, "03d-recovery-key-filled.png"); // Confirmar console.log("[login] Click en 'Continue'..."); await page.getByRole("button", { name: /continue/i }).click(); // Esperar a que se complete la verificacion (el dialogo desaparece) console.log("[login] Esperando boton 'Done'..."); const doneButton = page.getByRole("button", { name: /done/i }); const hasDone = await doneButton .waitFor({ state: "visible", timeout: 15_000 }) .then(() => true) .catch(() => false); if (hasDone) { await doneButton.click(); console.log("[login] Verificacion completada (Done)"); } else { console.log("[login] No se encontro 'Done' — verificacion puede haber terminado automaticamente"); await screenshot(page, ssDir, "03e-no-done-button.png"); } }