80014e8fb8
Corrige dos bugs en los fixtures de Playwright: 1. startThreadOnLastMessage usaba client.getRooms()[0] (el primer room unido) en lugar del room activo, causando que el mensaje threaded se enviara al room equivocado. Ahora lee el room ID de window.location.hash (#/room/!xxx:server), con resolucion de alias si la URL contiene un alias en lugar de un ID. 2. waitForThreadReplyViaSdk iteraba todos los rooms unidos, lo que podia devolver falsos positivos de otros rooms. Ahora esta acotado al room de la URL actual, con logica de fallback para aliases canonicos y alternativos.
628 lines
21 KiB
TypeScript
628 lines
21 KiB
TypeScript
import { Page, expect } from "@playwright/test";
|
|
|
|
/**
|
|
* Cierra toasts de Element que bloquean clicks.
|
|
* Duplicado aqui para evitar imports circulares con persistent-context.ts.
|
|
*/
|
|
async function dismissToasts(page: Page) {
|
|
await page.waitForTimeout(1_000);
|
|
const buttons = [
|
|
page.getByRole("button", { name: "Dismiss" }),
|
|
page.locator("button").filter({ hasText: /^OK$/ }),
|
|
page.getByRole("button", { name: "Close" }),
|
|
page.getByRole("button", { name: "Not now" }),
|
|
page.getByRole("button", { name: "Got it" }),
|
|
];
|
|
for (const btn of buttons) {
|
|
try {
|
|
if (await btn.first().isVisible()) {
|
|
await btn.first().click({ force: true });
|
|
await page.waitForTimeout(300);
|
|
}
|
|
} catch { /* ignore */ }
|
|
}
|
|
}
|
|
|
|
export interface WaitForReplyOptions {
|
|
/** Timeout en ms para esperar la respuesta (default: 30s) */
|
|
timeout?: number;
|
|
/** Filtrar por sender display name si se especifica */
|
|
sender?: string;
|
|
}
|
|
|
|
/**
|
|
* Navega a un room por nombre.
|
|
* Primero intenta click directo en el sidebar, luego usa el buscador.
|
|
*/
|
|
export async function goToRoom(page: Page, roomName: string) {
|
|
console.log(`[goToRoom] Buscando room: "${roomName}"`);
|
|
|
|
// Estrategia 1: Click directo en el room del sidebar (mas robusto)
|
|
const sidebarRoom = page.locator(
|
|
`.mx_RoomTile, [role="treeitem"]`
|
|
).filter({ hasText: new RegExp(roomName, "i") }).first();
|
|
|
|
const directMatch = await sidebarRoom
|
|
.waitFor({ state: "visible", timeout: 5_000 })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (directMatch) {
|
|
console.log(`[goToRoom] Room "${roomName}" encontrado en sidebar, click directo`);
|
|
await sidebarRoom.click();
|
|
await waitForRoomLoaded(page, roomName);
|
|
return;
|
|
}
|
|
|
|
// Estrategia 2: Usar busqueda (Ctrl+K es mas confiable que click en Search)
|
|
console.log("[goToRoom] Room no visible en sidebar, usando busqueda...");
|
|
await page.keyboard.press("Control+k");
|
|
|
|
// Esperar a que aparezca el dialog de busqueda
|
|
const searchInput = page.locator(
|
|
'[role="searchbox"], input[type="search"], .mx_SpotlightDialog input'
|
|
).first();
|
|
const hasSearch = await searchInput
|
|
.waitFor({ state: "visible", timeout: 5_000 })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (!hasSearch) {
|
|
// Fallback: click en el boton de Search con force para evitar toasts
|
|
console.log("[goToRoom] Ctrl+K no abrio busqueda, intentando click en Search...");
|
|
const searchButton = page.locator('[aria-label="Search"]').first();
|
|
await searchButton.click({ force: true });
|
|
await searchInput.waitFor({ state: "visible", timeout: 5_000 });
|
|
}
|
|
|
|
await searchInput.fill(roomName);
|
|
console.log(`[goToRoom] Texto ingresado: "${roomName}"`);
|
|
|
|
// Seleccionar el room de los resultados
|
|
const roomResult = page.locator(
|
|
'[role="option"], .mx_SpotlightDialog_result'
|
|
).filter({ hasText: new RegExp(roomName, "i") }).first();
|
|
|
|
const hasResult = await roomResult
|
|
.waitFor({ state: "visible", timeout: 10_000 })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (!hasResult) {
|
|
console.error(`[goToRoom] No se encontro room "${roomName}" en resultados`);
|
|
await page.screenshot({
|
|
path: `test-results/ERROR-goToRoom-no-result-${Date.now()}.png`,
|
|
fullPage: true,
|
|
});
|
|
// Cerrar el dialog de busqueda
|
|
await page.keyboard.press("Escape");
|
|
throw new Error(`Room "${roomName}" no encontrado en busqueda`);
|
|
}
|
|
|
|
console.log(`[goToRoom] Seleccionando room "${roomName}"`);
|
|
await roomResult.click();
|
|
|
|
await waitForRoomLoaded(page, roomName);
|
|
}
|
|
|
|
/**
|
|
* Espera a que un room termine de cargar (header visible + composer listo).
|
|
*/
|
|
async function waitForRoomLoaded(page: Page, roomName: string) {
|
|
// Esperar header del room o composer — ambos indican que el room cargo
|
|
const roomHeader = page.locator(
|
|
`[data-testid="room-header-name"], .mx_RoomHeader_heading, h2`
|
|
).filter({ hasText: new RegExp(roomName, "i") }).first();
|
|
|
|
const headerVisible = await roomHeader
|
|
.waitFor({ state: "visible", timeout: 10_000 })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (!headerVisible) {
|
|
// Fallback: verificar que al menos el composer esta visible
|
|
const composer = page.getByRole("textbox", { name: /message/i });
|
|
await composer.waitFor({ state: "visible", timeout: 10_000 });
|
|
}
|
|
|
|
console.log(`[goToRoom] Estamos en room "${roomName}"`);
|
|
}
|
|
|
|
/**
|
|
* Envia un mensaje de texto en el room actual.
|
|
*/
|
|
export async function sendMessage(page: Page, text: string) {
|
|
console.log(`[sendMessage] Enviando: "${text}"`);
|
|
|
|
const composer = page.getByRole("textbox", { name: /message/i });
|
|
const hasComposer = await composer
|
|
.waitFor({ state: "visible", timeout: 10_000 })
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (!hasComposer) {
|
|
console.error("[sendMessage] Composer no encontrado");
|
|
await page.screenshot({
|
|
path: `test-results/ERROR-sendMessage-no-composer-${Date.now()}.png`,
|
|
fullPage: true,
|
|
});
|
|
throw new Error("Composer de mensajes no encontrado");
|
|
}
|
|
|
|
await composer.fill(text);
|
|
await composer.press("Enter");
|
|
console.log("[sendMessage] Enter presionado");
|
|
|
|
// Esperar a que el mensaje aparezca en el timeline
|
|
await expect(
|
|
page.locator(".mx_EventTile_body, .mx_MTextBody").filter({ hasText: text }).last()
|
|
).toBeVisible({ timeout: 10_000 });
|
|
console.log(`[sendMessage] Mensaje visible en timeline: "${text}"`);
|
|
}
|
|
|
|
/**
|
|
* Espera una respuesta de un bot en el timeline.
|
|
* Detecta "Unable to decrypt" y falla con mensaje claro.
|
|
*/
|
|
export async function waitForBotReply(
|
|
page: Page,
|
|
options?: WaitForReplyOptions
|
|
): Promise<string> {
|
|
const timeout = options?.timeout ?? 30_000;
|
|
console.log(
|
|
`[waitForBotReply] Esperando respuesta (timeout: ${timeout}ms, sender: ${options?.sender || "any"})...`
|
|
);
|
|
|
|
// Esperar a que aparezca un nuevo mensaje (que no sea del usuario actual)
|
|
const startTime = Date.now();
|
|
let lastLoggedMsg = "";
|
|
let lastLoggedSender = "";
|
|
|
|
while (Date.now() - startTime < timeout) {
|
|
// Detectar mensajes que no se pueden descifrar
|
|
const undecryptable = page.locator(
|
|
'.mx_DecryptionFailureBody, [class*="UnableToDecrypt"]'
|
|
);
|
|
const undecryptableCount = await undecryptable.count();
|
|
if (undecryptableCount > 0) {
|
|
console.error(
|
|
`[waitForBotReply] E2EE ERROR: ${undecryptableCount} mensajes sin descifrar`
|
|
);
|
|
await page.screenshot({
|
|
path: `test-results/ERROR-e2ee-${Date.now()}.png`,
|
|
fullPage: true,
|
|
});
|
|
throw new Error(
|
|
"E2EE error: se detectaron mensajes 'Unable to decrypt'. " +
|
|
"Verificar que cross-signing esta configurado correctamente " +
|
|
"y que la recovery key es valida."
|
|
);
|
|
}
|
|
|
|
// Buscar el ultimo mensaje en el timeline
|
|
const lastMessage = await getLastMessage(page);
|
|
if (lastMessage) {
|
|
// Si se especifica sender, verificar que coincide
|
|
if (options?.sender) {
|
|
const lastSender = await getLastMessageSender(page);
|
|
|
|
// Log periodico para debug (cada mensaje nuevo)
|
|
if (lastMessage !== lastLoggedMsg || lastSender !== lastLoggedSender) {
|
|
console.log(
|
|
`[waitForBotReply] Ultimo msg: "${lastMessage?.substring(0, 80)}..." | sender: "${lastSender}"`
|
|
);
|
|
lastLoggedMsg = lastMessage;
|
|
lastLoggedSender = lastSender || "";
|
|
}
|
|
|
|
if (lastSender && lastSender.includes(options.sender)) {
|
|
console.log(
|
|
`[waitForBotReply] Respuesta recibida de "${options.sender}" (${Date.now() - startTime}ms)`
|
|
);
|
|
return lastMessage;
|
|
}
|
|
} else {
|
|
console.log(
|
|
`[waitForBotReply] Respuesta recibida (${Date.now() - startTime}ms)`
|
|
);
|
|
return lastMessage;
|
|
}
|
|
}
|
|
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
// Timeout — tomar screenshot antes de lanzar error
|
|
console.error(
|
|
`[waitForBotReply] TIMEOUT despues de ${timeout}ms esperando respuesta`
|
|
);
|
|
await page.screenshot({
|
|
path: `test-results/ERROR-timeout-waitForBotReply-${Date.now()}.png`,
|
|
fullPage: true,
|
|
});
|
|
|
|
// Log del estado actual del timeline para debug
|
|
const allMessages = page.locator(".mx_EventTile_body, .mx_MTextBody");
|
|
const msgCount = await allMessages.count();
|
|
console.log(`[waitForBotReply] Mensajes en timeline: ${msgCount}`);
|
|
for (let i = Math.max(0, msgCount - 5); i < msgCount; i++) {
|
|
const text = await allMessages.nth(i).textContent();
|
|
console.log(`[waitForBotReply] [${i}]: "${text?.substring(0, 100)}"`);
|
|
}
|
|
|
|
const allSenders = page.locator(
|
|
".mx_DisambiguatedProfile_displayName, .mx_SenderProfile_name"
|
|
);
|
|
const senderCount = await allSenders.count();
|
|
console.log(`[waitForBotReply] Senders en timeline: ${senderCount}`);
|
|
for (let i = Math.max(0, senderCount - 5); i < senderCount; i++) {
|
|
const name = await allSenders.nth(i).textContent();
|
|
console.log(`[waitForBotReply] [${i}]: "${name}"`);
|
|
}
|
|
|
|
throw new Error(
|
|
`Timeout (${timeout}ms): no se recibio respuesta del bot` +
|
|
(options?.sender ? ` (sender esperado: ${options.sender})` : "")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Obtiene el texto del ultimo mensaje visible en el timeline.
|
|
*/
|
|
export async function getLastMessage(page: Page): Promise<string | null> {
|
|
const messages = page.locator(".mx_EventTile_body, .mx_MTextBody");
|
|
const count = await messages.count();
|
|
if (count === 0) return null;
|
|
return messages.last().textContent();
|
|
}
|
|
|
|
/**
|
|
* Obtiene el display name del sender del ultimo mensaje.
|
|
*/
|
|
async function getLastMessageSender(page: Page): Promise<string | null> {
|
|
const senders = page.locator(
|
|
".mx_DisambiguatedProfile_displayName, .mx_SenderProfile_name"
|
|
);
|
|
const count = await senders.count();
|
|
if (count === 0) return null;
|
|
return senders.last().textContent();
|
|
}
|
|
|
|
/**
|
|
* Inicia un thread sobre el ultimo mensaje del timeline.
|
|
*
|
|
* En headless Chromium, la hover action bar de Element Web no se renderiza
|
|
* (es React onMouseEnter, no CSS :hover). Usamos el Matrix SDK expuesto
|
|
* en window para enviar un mensaje threaded directamente, luego abrimos
|
|
* el thread panel via la UI.
|
|
*/
|
|
export async function startThreadOnLastMessage(page: Page) {
|
|
console.log("[startThread] Dismissing toasts...");
|
|
await dismissToasts(page);
|
|
|
|
// Obtener el event ID del ultimo mensaje y el room ID via el SDK de Element
|
|
const threadInfo = await page.evaluate(async () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const client = (window as any).mxMatrixClientPeg?.get?.();
|
|
if (!client) throw new Error("Matrix client no disponible en window");
|
|
|
|
// Obtener el room actual desde la URL (mas fiable que getRooms()[0])
|
|
const hash = window.location.hash; // e.g. "#/room/!xxx:server" or "#/room/#alias:server"
|
|
const match = hash.match(/#\/room\/([^?/]+)/);
|
|
if (!match) throw new Error(`No se pudo obtener room ID de la URL: ${hash}`);
|
|
const roomIdOrAlias = decodeURIComponent(match[1]);
|
|
|
|
let roomId: string;
|
|
if (roomIdOrAlias.startsWith("!")) {
|
|
roomId = roomIdOrAlias;
|
|
} else {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const resolved = await (client as any).getRoomIdForAlias(roomIdOrAlias);
|
|
if (!resolved?.room_id) throw new Error(`No se pudo resolver alias: ${roomIdOrAlias}`);
|
|
roomId = resolved.room_id;
|
|
}
|
|
|
|
if (!roomId) throw new Error("No hay room activo");
|
|
|
|
const room = client.getRoom(roomId);
|
|
if (!room) throw new Error("Room no encontrado");
|
|
|
|
// Obtener el ultimo evento de mensaje en el timeline
|
|
const timeline = room.getLiveTimeline().getEvents();
|
|
const lastMsgEvent = [...timeline].reverse().find(
|
|
(e: { getType: () => string }) => e.getType() === "m.room.message"
|
|
);
|
|
if (!lastMsgEvent) throw new Error("No hay mensajes en el timeline");
|
|
|
|
return {
|
|
roomId,
|
|
eventId: lastMsgEvent.getId(),
|
|
sender: lastMsgEvent.getSender(),
|
|
};
|
|
});
|
|
|
|
console.log(`[startThread] Room: ${threadInfo.roomId}, Event: ${threadInfo.eventId}`);
|
|
|
|
// Enviar un mensaje threaded via el SDK
|
|
await page.evaluate(async ({ roomId, eventId }) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const client = (window as any).mxMatrixClientPeg.get();
|
|
await client.sendMessage(roomId, {
|
|
msgtype: "m.text",
|
|
body: "Hola desde el thread, respondeme aqui por favor",
|
|
"m.relates_to": {
|
|
rel_type: "m.thread",
|
|
event_id: eventId,
|
|
is_falling_back: true,
|
|
"m.in_reply_to": { event_id: eventId },
|
|
},
|
|
});
|
|
}, threadInfo);
|
|
|
|
console.log("[startThread] Mensaje threaded enviado via SDK");
|
|
// El thread ya esta creado. La verificacion de respuesta se hace via SDK.
|
|
}
|
|
|
|
/**
|
|
* Envia un mensaje en el panel de thread abierto.
|
|
*/
|
|
export async function sendThreadMessage(page: Page, text: string) {
|
|
console.log(`[sendThreadMessage] Enviando en thread: "${text}"`);
|
|
|
|
// El composer del thread esta dentro del panel derecho
|
|
const threadPanel = page.locator(
|
|
".mx_ThreadView, .mx_ThreadPanel, .mx_RightPanel .mx_BaseCard"
|
|
);
|
|
const composer = threadPanel.getByRole("textbox", { name: /message/i });
|
|
await composer.fill(text);
|
|
await composer.press("Enter");
|
|
|
|
// Esperar a que el mensaje aparezca dentro del thread
|
|
await expect(
|
|
threadPanel
|
|
.locator(".mx_EventTile_body, .mx_MTextBody")
|
|
.filter({ hasText: text })
|
|
.last()
|
|
).toBeVisible({ timeout: 10_000 });
|
|
console.log("[sendThreadMessage] Mensaje visible en thread");
|
|
}
|
|
|
|
/**
|
|
* Espera la respuesta de un bot dentro del panel de thread.
|
|
* Similar a waitForBotReply pero busca solo dentro del thread panel.
|
|
*/
|
|
export async function waitForThreadReply(
|
|
page: Page,
|
|
options?: WaitForReplyOptions
|
|
): Promise<string> {
|
|
const timeout = options?.timeout ?? 30_000;
|
|
const startTime = Date.now();
|
|
console.log(
|
|
`[waitForThreadReply] Esperando respuesta en thread (timeout: ${timeout}ms)...`
|
|
);
|
|
|
|
const threadPanel = page.locator(
|
|
".mx_ThreadView, .mx_ThreadPanel, .mx_RightPanel .mx_BaseCard"
|
|
);
|
|
|
|
while (Date.now() - startTime < timeout) {
|
|
// Detectar errores de E2EE dentro del thread
|
|
const undecryptable = threadPanel.locator(
|
|
'.mx_DecryptionFailureBody, [class*="UnableToDecrypt"]'
|
|
);
|
|
if ((await undecryptable.count()) > 0) {
|
|
console.error("[waitForThreadReply] E2EE error en thread");
|
|
await page.screenshot({
|
|
path: `test-results/ERROR-e2ee-thread-${Date.now()}.png`,
|
|
fullPage: true,
|
|
});
|
|
throw new Error(
|
|
"E2EE error en thread: se detectaron mensajes 'Unable to decrypt'."
|
|
);
|
|
}
|
|
|
|
// Buscar mensajes en el thread panel
|
|
const messages = threadPanel.locator(
|
|
".mx_EventTile_body, .mx_MTextBody"
|
|
);
|
|
const count = await messages.count();
|
|
|
|
// Necesitamos al menos 2 mensajes (el del usuario + la respuesta del bot)
|
|
if (count >= 2) {
|
|
const lastMsg = await messages.last().textContent();
|
|
if (lastMsg && options?.sender) {
|
|
const senders = threadPanel.locator(
|
|
".mx_DisambiguatedProfile_displayName, .mx_SenderProfile_name"
|
|
);
|
|
const senderCount = await senders.count();
|
|
if (senderCount > 0) {
|
|
const lastSender = await senders.last().textContent();
|
|
if (lastSender?.includes(options.sender)) {
|
|
console.log(
|
|
`[waitForThreadReply] Respuesta de "${options.sender}" en thread (${Date.now() - startTime}ms)`
|
|
);
|
|
return lastMsg;
|
|
}
|
|
}
|
|
} else if (lastMsg) {
|
|
console.log(
|
|
`[waitForThreadReply] Respuesta en thread (${Date.now() - startTime}ms)`
|
|
);
|
|
return lastMsg;
|
|
}
|
|
}
|
|
|
|
await page.waitForTimeout(500);
|
|
}
|
|
|
|
console.error(
|
|
`[waitForThreadReply] TIMEOUT despues de ${timeout}ms`
|
|
);
|
|
await page.screenshot({
|
|
path: `test-results/ERROR-timeout-thread-${Date.now()}.png`,
|
|
fullPage: true,
|
|
});
|
|
|
|
throw new Error(
|
|
`Timeout (${timeout}ms): el bot no respondio dentro del thread` +
|
|
(options?.sender ? ` (sender esperado: ${options.sender})` : "")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Verifica que el bot NO respondio en el timeline principal tras enviar un thread.
|
|
* Busca mensajes del bot en el timeline principal que no deberian estar ahi.
|
|
* Retorna true si el timeline principal NO tiene respuesta del bot (correcto).
|
|
*/
|
|
export async function assertBotDidNotReplyInMainTimeline(
|
|
page: Page,
|
|
botName: string,
|
|
afterText: string,
|
|
checkDurationMs: number = 5_000
|
|
): Promise<void> {
|
|
// Esperar un poco para dar tiempo al bot a responder (incorrectamente) en main
|
|
await page.waitForTimeout(checkDurationMs);
|
|
|
|
// Buscar mensajes en el timeline principal (fuera del thread panel)
|
|
const mainTimeline = page.locator(".mx_RoomView_body");
|
|
const botMessages = mainTimeline.locator(
|
|
".mx_EventTile_body, .mx_MTextBody"
|
|
);
|
|
const senders = mainTimeline.locator(
|
|
".mx_DisambiguatedProfile_displayName, .mx_SenderProfile_name"
|
|
);
|
|
|
|
const msgCount = await botMessages.count();
|
|
const senderCount = await senders.count();
|
|
|
|
// Verificar que el ultimo mensaje del timeline principal no es del bot
|
|
// (despues de nuestro mensaje de thread)
|
|
if (msgCount > 0 && senderCount > 0) {
|
|
const lastSender = await senders.last().textContent();
|
|
if (lastSender?.includes(botName)) {
|
|
const lastMsg = await botMessages.last().textContent();
|
|
// Si el ultimo mensaje del main timeline es del bot y es posterior
|
|
// a nuestro mensaje original, el bot respondio fuera del thread
|
|
if (lastMsg && lastMsg !== afterText) {
|
|
throw new Error(
|
|
`El bot respondio en el timeline principal en vez de en el thread. ` +
|
|
`Ultimo mensaje del bot: "${lastMsg}"`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Espera la respuesta del bot en un thread usando el Matrix SDK de Element.
|
|
* No depende del panel de thread UI — consulta el timeline directamente.
|
|
*/
|
|
export async function waitForThreadReplyViaSdk(
|
|
page: Page,
|
|
options?: WaitForReplyOptions
|
|
): Promise<string> {
|
|
const timeout = options?.timeout ?? 30_000;
|
|
const startTime = Date.now();
|
|
const senderFilter = options?.sender;
|
|
|
|
console.log(
|
|
`[waitForThreadReplyViaSdk] Esperando respuesta en thread (timeout: ${timeout}ms, sender: ${senderFilter || "any"})...`
|
|
);
|
|
|
|
while (Date.now() - startTime < timeout) {
|
|
const reply = await page.evaluate(({ senderFilter }) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const client = (window as any).mxMatrixClientPeg?.get?.();
|
|
if (!client) return null;
|
|
|
|
// Scoped to current room only (via URL) to avoid false positives
|
|
const hash = window.location.hash;
|
|
const match = hash.match(/#\/room\/([^?/]+)/);
|
|
const roomIdOrAlias = match ? decodeURIComponent(match[1]) : null;
|
|
|
|
const rooms = client.getRooms().filter(
|
|
(r: { getMyMembership: () => string; roomId: string }) => {
|
|
if (r.getMyMembership() !== "join") return false;
|
|
if (roomIdOrAlias) {
|
|
return r.roomId === roomIdOrAlias ||
|
|
r.roomId === roomIdOrAlias; // alias resolution handled below
|
|
}
|
|
return true;
|
|
}
|
|
);
|
|
|
|
for (const room of rooms) {
|
|
// Skip rooms that don't match the current URL room
|
|
if (roomIdOrAlias && !roomIdOrAlias.startsWith("!")) {
|
|
// For aliases, check if the room has this alias
|
|
const aliases = room.getAltAliases?.() || [];
|
|
const canonicalAlias = room.getCanonicalAlias?.();
|
|
if (canonicalAlias !== roomIdOrAlias && !aliases.includes(roomIdOrAlias)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const timeline = room.getLiveTimeline().getEvents();
|
|
// Buscar eventos que sean respuestas de thread (m.relates_to.rel_type === "m.thread")
|
|
const threadReplies = timeline.filter((e: {
|
|
getType: () => string;
|
|
getContent: () => { "m.relates_to"?: { rel_type?: string } };
|
|
getSender: () => string;
|
|
}) => {
|
|
if (e.getType() !== "m.room.message") return false;
|
|
const content = e.getContent();
|
|
const relatesTo = content["m.relates_to"];
|
|
if (!relatesTo || relatesTo.rel_type !== "m.thread") return false;
|
|
// Filtrar por sender si se especifico
|
|
if (senderFilter) {
|
|
const sender = e.getSender();
|
|
// Verificar por display name
|
|
const member = room.getMember(sender);
|
|
const displayName = member?.name || sender;
|
|
if (!displayName.includes(senderFilter)) return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (threadReplies.length > 0) {
|
|
const lastReply = threadReplies[threadReplies.length - 1];
|
|
const content = lastReply.getContent();
|
|
return content.body || content.formatted_body || "";
|
|
}
|
|
}
|
|
return null;
|
|
}, { senderFilter });
|
|
|
|
if (reply) {
|
|
console.log(
|
|
`[waitForThreadReplyViaSdk] Respuesta encontrada (${Date.now() - startTime}ms): "${reply.substring(0, 80)}..."`
|
|
);
|
|
return reply;
|
|
}
|
|
|
|
await page.waitForTimeout(1_000);
|
|
}
|
|
|
|
throw new Error(
|
|
`Timeout (${timeout}ms): el bot no respondio en el thread` +
|
|
(senderFilter ? ` (sender: ${senderFilter})` : "")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Verifica que no hay mensajes "Unable to decrypt" en el timeline visible.
|
|
* Lanza error descriptivo si los encuentra.
|
|
*/
|
|
export async function assertNoDecryptionErrors(page: Page) {
|
|
const undecryptable = page.locator(
|
|
'.mx_DecryptionFailureBody, [class*="UnableToDecrypt"]'
|
|
);
|
|
const texts = await undecryptable.allTextContents();
|
|
if (texts.length > 0) {
|
|
throw new Error(
|
|
`E2EE error: ${texts.length} mensaje(s) no pudieron descifrarse. ` +
|
|
"Verificar cross-signing y recovery key."
|
|
);
|
|
}
|
|
}
|