feat: auth fixtures y helpers de interaccion E2E
Implementa el issue 0022b — fixtures de Playwright para autenticacion en Element Web y helpers de interaccion con rooms. - element-auth.ts: flujo completo de login + cross-signing con recovery key, preparado para cachear sesion via storageState - global-setup.ts: ejecuta login una vez antes de todos los tests, reutiliza sesion cacheada si tiene menos de 12 horas - matrix-room.ts: helpers goToRoom, sendMessage, waitForBotReply, getLastMessage, assertNoDecryptionErrors (detecta "Unable to decrypt") - login.spec.ts: 3 smoke tests validando sesion, E2EE y navegacion - playwright.config.ts: configurado storageState para inyectar sesion Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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<string> {
|
||||
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<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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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."
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user