import { Page, expect } from "@playwright/test"; 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 usando el buscador de Element. */ export async function goToRoom(page: Page, roomName: string) { // Abrir busqueda de rooms (Ctrl+K o click en el search) const searchButton = page.locator('[aria-label="Search"]').first(); await searchButton.click(); // Escribir nombre del room en el campo de busqueda const searchInput = page.getByRole("searchbox").first(); await searchInput.fill(roomName); // Seleccionar el room de los resultados const roomResult = page .getByRole("option", { name: new RegExp(roomName, "i") }) .first(); await roomResult.click(); // Verificar que estamos en el room correcto (header muestra el nombre) await expect( page.locator(`[data-testid="room-header-name"], h2`).filter({ hasText: roomName }).first() ).toBeVisible({ timeout: 10_000 }); } /** * Envia un mensaje de texto en el room actual. */ export async function sendMessage(page: Page, text: string) { const composer = page.getByRole("textbox", { name: /message/i }); await composer.fill(text); await composer.press("Enter"); // 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 }); } /** * 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 { const timeout = options?.timeout ?? 30_000; // Esperar a que aparezca un nuevo mensaje (que no sea del usuario actual) // Los mensajes de bots tienen un sender distinto al del usuario autenticado const startTime = Date.now(); 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) { 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); if (lastSender && lastSender.includes(options.sender)) { return lastMessage; } } else { // Sin filtro de sender, cualquier mensaje nuevo sirve return lastMessage; } } await page.waitForTimeout(500); } 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 { 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 { const senders = page.locator( ".mx_DisambiguatedProfile_displayName, .mx_SenderProfile_name" ); const count = await senders.count(); if (count === 0) return null; return senders.last().textContent(); } /** * 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." ); } }