cfd932205f
asistente-2.spec.ts: usa waitForThreadReplyViaSdk en lugar de sendThreadMessage + waitForThreadReply (que dependian del panel UI). Elimina la importacion de sendThreadMessage. Agrega test.setTimeout(120_000) al test de threads para dar tiempo suficiente al ciclo completo. login.spec.ts: ampliar locators de room tiles con .mx_RoomTile para mayor compatibilidad con Element Web moderno que no siempre usa role=treeitem. element-auth.ts: ampliar locator de roomsTree con .mx_RoomList, .mx_LeftPanel_roomListContainer, .mx_RoomTile para detectar sesion existente de forma mas robusta, tanto en loginToElement como en waitForLoginResult.
287 lines
10 KiB
TypeScript
287 lines
10 KiB
TypeScript
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<string> {
|
|
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");
|
|
}
|
|
}
|