refactor: migrar tests E2E a persistent context
global-setup.ts: - Usa launchPersistentContext en vez de browser.newContext() - Reemplaza storageState por marker file para cache de sesion - Captura logs de consola del browser para debug - Screenshots y HTML dump en caso de error playwright.config.ts: - Elimina storageState (ahora via persistent context fixture) - Screenshots siempre activas, video y trace en failures Tests (login, assistant-bot, asistente-2): - Importan test/expect desde persistent-context fixture - Usan handleElementDialogs() en vez de espera manual de rooms - Nuevo test de threads en asistente-2: verifica que el bot responde dentro del thread cuando se le habla por thread Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+68
-19
@@ -6,14 +6,19 @@ import { loginToElement } from "./fixtures/element-auth";
|
|||||||
|
|
||||||
dotenv.config({ path: path.resolve(__dirname, ".env") });
|
dotenv.config({ path: path.resolve(__dirname, ".env") });
|
||||||
|
|
||||||
const AUTH_DIR = path.resolve(__dirname, ".auth");
|
const USER_DATA_DIR = path.resolve(__dirname, ".auth", "chrome-profile");
|
||||||
const STATE_PATH = path.join(AUTH_DIR, "state.json");
|
const MARKER_PATH = path.resolve(__dirname, ".auth", "login-done.marker");
|
||||||
|
const SCREENSHOTS_DIR = path.resolve(__dirname, "test-results", "global-setup");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global setup: ejecuta login una vez y guarda storageState
|
* Global setup: ejecuta login una vez usando persistent context.
|
||||||
* para reutilizar en todos los tests sin repetir el login.
|
|
||||||
*
|
*
|
||||||
* Si state.json ya existe y no esta expirado, lo reutiliza.
|
* A diferencia de storageState, el persistent context preserva IndexedDB
|
||||||
|
* (crypto keys de E2EE). Los tests usan el mismo userDataDir via el
|
||||||
|
* custom fixture persistent-context.ts.
|
||||||
|
*
|
||||||
|
* Si el marker file existe y no esta expirado, asumimos que la sesion
|
||||||
|
* sigue activa y saltamos el login.
|
||||||
*/
|
*/
|
||||||
async function globalSetup() {
|
async function globalSetup() {
|
||||||
const elementURL = process.env.ELEMENT_URL || "http://localhost:8080";
|
const elementURL = process.env.ELEMENT_URL || "http://localhost:8080";
|
||||||
@@ -27,20 +32,45 @@ async function globalSetup() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reutilizar sesion cacheada si existe y tiene menos de 12 horas
|
// Reutilizar sesion cacheada si el marker existe y tiene menos de 12 horas
|
||||||
if (isStateFresh(STATE_PATH, 12 * 60 * 60 * 1000)) {
|
if (isMarkerFresh(MARKER_PATH, 12 * 60 * 60 * 1000)) {
|
||||||
console.log("[global-setup] Reutilizando sesion cacheada");
|
console.log("[global-setup] Reutilizando sesion de persistent context");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[global-setup] Ejecutando login en Element Web...");
|
console.log("[global-setup] Ejecutando login en Element Web con persistent context...");
|
||||||
|
console.log(`[global-setup] URL: ${elementURL}`);
|
||||||
|
console.log(`[global-setup] User: ${user}`);
|
||||||
|
console.log(`[global-setup] UserDataDir: ${USER_DATA_DIR}`);
|
||||||
|
|
||||||
// Asegurar que el directorio .auth existe
|
// Limpiar perfil anterior para login fresco
|
||||||
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
if (fs.existsSync(USER_DATA_DIR)) {
|
||||||
|
fs.rmSync(USER_DATA_DIR, { recursive: true });
|
||||||
|
console.log("[global-setup] Perfil anterior eliminado");
|
||||||
|
}
|
||||||
|
|
||||||
const browser = await chromium.launch();
|
// Asegurar que los directorios existen
|
||||||
const context = await browser.newContext();
|
fs.mkdirSync(USER_DATA_DIR, { recursive: true });
|
||||||
const page = await context.newPage();
|
fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
||||||
|
|
||||||
|
// Usar persistent context — preserva IndexedDB (crypto keys E2EE)
|
||||||
|
const context = await chromium.launchPersistentContext(USER_DATA_DIR, {
|
||||||
|
headless: true,
|
||||||
|
viewport: { width: 1280, height: 720 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = context.pages()[0] || (await context.newPage());
|
||||||
|
|
||||||
|
// Capturar logs de consola del browser
|
||||||
|
page.on("console", (msg) => {
|
||||||
|
const type = msg.type();
|
||||||
|
if (type === "error" || type === "warning") {
|
||||||
|
console.log(`[browser-${type}] ${msg.text()}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
page.on("pageerror", (err) => {
|
||||||
|
console.error(`[browser-error] ${err.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loginToElement(page, {
|
await loginToElement(page, {
|
||||||
@@ -48,17 +78,36 @@ async function globalSetup() {
|
|||||||
user,
|
user,
|
||||||
password,
|
password,
|
||||||
recoveryKey,
|
recoveryKey,
|
||||||
|
screenshotsDir: SCREENSHOTS_DIR,
|
||||||
});
|
});
|
||||||
|
|
||||||
await context.storageState({ path: STATE_PATH });
|
// Crear marker de sesion exitosa
|
||||||
console.log("[global-setup] Sesion guardada en", STATE_PATH);
|
fs.writeFileSync(MARKER_PATH, new Date().toISOString());
|
||||||
|
console.log("[global-setup] Login completado, marker creado");
|
||||||
|
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(SCREENSHOTS_DIR, "05-login-complete.png"),
|
||||||
|
fullPage: true,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[global-setup] ERROR durante login:", err);
|
||||||
|
await page.screenshot({
|
||||||
|
path: path.join(SCREENSHOTS_DIR, "ERROR-login-failed.png"),
|
||||||
|
fullPage: true,
|
||||||
|
});
|
||||||
|
const html = await page.content();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(SCREENSHOTS_DIR, "ERROR-page-content.html"),
|
||||||
|
html
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
await browser.close();
|
await context.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Verifica si el archivo de estado existe y tiene menos de maxAge ms. */
|
/** Verifica si el marker file existe y tiene menos de maxAge ms. */
|
||||||
function isStateFresh(filePath: string, maxAge: number): boolean {
|
function isMarkerFresh(filePath: string, maxAge: number): boolean {
|
||||||
try {
|
try {
|
||||||
const stat = fs.statSync(filePath);
|
const stat = fs.statSync(filePath);
|
||||||
return Date.now() - stat.mtimeMs < maxAge;
|
return Date.now() - stat.mtimeMs < maxAge;
|
||||||
|
|||||||
@@ -19,12 +19,15 @@ export default defineConfig({
|
|||||||
use: {
|
use: {
|
||||||
baseURL: process.env.ELEMENT_URL || "http://localhost:8080",
|
baseURL: process.env.ELEMENT_URL || "http://localhost:8080",
|
||||||
headless: true,
|
headless: true,
|
||||||
screenshot: "only-on-failure",
|
screenshot: "on",
|
||||||
trace: "on-first-retry",
|
trace: "retain-on-failure",
|
||||||
|
video: "retain-on-failure",
|
||||||
actionTimeout: 30_000,
|
actionTimeout: 30_000,
|
||||||
storageState: path.resolve(__dirname, ".auth/state.json"),
|
// NO usamos storageState — usamos persistent context para preservar IndexedDB
|
||||||
},
|
},
|
||||||
|
|
||||||
|
outputDir: "./test-results",
|
||||||
|
|
||||||
globalSetup: "./global-setup.ts",
|
globalSetup: "./global-setup.ts",
|
||||||
|
|
||||||
projects: [
|
projects: [
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
import { test, expect, handleElementDialogs } from "../fixtures/persistent-context";
|
||||||
import {
|
import {
|
||||||
goToRoom,
|
goToRoom,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
waitForBotReply,
|
waitForBotReply,
|
||||||
assertNoDecryptionErrors,
|
assertNoDecryptionErrors,
|
||||||
|
startThreadOnLastMessage,
|
||||||
|
sendThreadMessage,
|
||||||
|
waitForThreadReply,
|
||||||
} from "../fixtures/matrix-room";
|
} from "../fixtures/matrix-room";
|
||||||
|
|
||||||
test.describe("asistente-2", () => {
|
test.describe("asistente-2", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
await handleElementDialogs(page);
|
||||||
// Esperar a que la sesion este lista
|
|
||||||
await expect(
|
|
||||||
page.locator('[role="tree"][aria-label="Rooms"]')
|
|
||||||
).toBeVisible({ timeout: 30_000 });
|
|
||||||
|
|
||||||
await goToRoom(page, "Asistente 2");
|
await goToRoom(page, "Asistente 2");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -65,6 +63,37 @@ test.describe("asistente-2", () => {
|
|||||||
expect(reply.toLowerCase()).toContain("ping");
|
expect(reply.toLowerCase()).toContain("ping");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("responde dentro del thread cuando se le habla por thread", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
// 1. Enviar un mensaje normal (sera el thread root)
|
||||||
|
await sendMessage(page, "Mensaje para iniciar thread");
|
||||||
|
|
||||||
|
// Esperar a que el bot responda al mensaje original
|
||||||
|
await waitForBotReply(page, {
|
||||||
|
timeout: 60_000,
|
||||||
|
sender: "Asistente 2",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Iniciar thread sobre el mensaje del usuario
|
||||||
|
await startThreadOnLastMessage(page);
|
||||||
|
|
||||||
|
// 3. Enviar mensaje dentro del thread
|
||||||
|
await sendThreadMessage(
|
||||||
|
page,
|
||||||
|
"Hola desde el thread, respondeme aqui por favor"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Esperar que el bot responda DENTRO del thread
|
||||||
|
const threadReply = await waitForThreadReply(page, {
|
||||||
|
timeout: 60_000,
|
||||||
|
sender: "Asistente 2",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(threadReply).toBeTruthy();
|
||||||
|
expect(threadReply.length).toBeGreaterThan(5);
|
||||||
|
});
|
||||||
|
|
||||||
test("no hay errores de E2EE en el timeline", async ({ page }) => {
|
test("no hay errores de E2EE en el timeline", async ({ page }) => {
|
||||||
await assertNoDecryptionErrors(page);
|
await assertNoDecryptionErrors(page);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
import { test, expect, handleElementDialogs } from "../fixtures/persistent-context";
|
||||||
import {
|
import {
|
||||||
goToRoom,
|
goToRoom,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
@@ -9,12 +9,7 @@ import {
|
|||||||
test.describe("assistant-bot", () => {
|
test.describe("assistant-bot", () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
await handleElementDialogs(page);
|
||||||
// Esperar a que la sesion este lista
|
|
||||||
await expect(
|
|
||||||
page.locator('[role="tree"][aria-label="Rooms"]')
|
|
||||||
).toBeVisible({ timeout: 30_000 });
|
|
||||||
|
|
||||||
await goToRoom(page, "Assistant");
|
await goToRoom(page, "Assistant");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+10
-26
@@ -1,54 +1,38 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
import { test, expect, handleElementDialogs } from "../fixtures/persistent-context";
|
||||||
import { assertNoDecryptionErrors } from "../fixtures/matrix-room";
|
import { assertNoDecryptionErrors } from "../fixtures/matrix-room";
|
||||||
|
|
||||||
test.describe("Login y sesion E2EE", () => {
|
test.describe("Login y sesion E2EE", () => {
|
||||||
test("storageState cargado — rooms visibles en sidebar", async ({
|
test("sesion cargada — rooms visibles en sidebar", async ({ page }) => {
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
await handleElementDialogs(page);
|
||||||
|
|
||||||
// Si la sesion esta cacheada, Element debe mostrar rooms directamente
|
// Si llegamos aqui, handleElementDialogs ya verifico rooms sidebar
|
||||||
await expect(
|
const rooms = page.locator('[role="treeitem"]');
|
||||||
page.locator('[role="tree"][aria-label="Rooms"]')
|
const roomCount = await rooms.count();
|
||||||
).toBeVisible({ timeout: 30_000 });
|
expect(roomCount).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("no hay mensajes Unable to decrypt en rooms recientes", async ({
|
test("no hay mensajes Unable to decrypt en rooms recientes", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
await handleElementDialogs(page);
|
||||||
// Esperar a que cargue la lista de rooms
|
|
||||||
await expect(
|
|
||||||
page.locator('[role="tree"][aria-label="Rooms"]')
|
|
||||||
).toBeVisible({ timeout: 30_000 });
|
|
||||||
|
|
||||||
// Abrir el primer room visible para verificar mensajes
|
// Abrir el primer room visible para verificar mensajes
|
||||||
const firstRoom = page
|
const firstRoom = page.locator('[role="treeitem"]').first();
|
||||||
.locator('[role="treeitem"]')
|
|
||||||
.first();
|
|
||||||
const roomCount = await firstRoom.count();
|
const roomCount = await firstRoom.count();
|
||||||
|
|
||||||
if (roomCount > 0) {
|
if (roomCount > 0) {
|
||||||
await firstRoom.click();
|
await firstRoom.click();
|
||||||
|
|
||||||
// Esperar a que cargue el timeline
|
|
||||||
await page.waitForTimeout(3_000);
|
await page.waitForTimeout(3_000);
|
||||||
|
|
||||||
// Verificar que no hay errores de desencriptado
|
|
||||||
await assertNoDecryptionErrors(page);
|
await assertNoDecryptionErrors(page);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("helpers de room navegan correctamente", async ({ page }) => {
|
test("helpers de room navegan correctamente", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
await handleElementDialogs(page);
|
||||||
|
|
||||||
// Esperar a que la sesion este lista
|
|
||||||
await expect(
|
|
||||||
page.locator('[role="tree"][aria-label="Rooms"]')
|
|
||||||
).toBeVisible({ timeout: 30_000 });
|
|
||||||
|
|
||||||
// Verificar que hay al menos un room en el sidebar
|
|
||||||
const rooms = page.locator('[role="treeitem"]');
|
const rooms = page.locator('[role="treeitem"]');
|
||||||
const roomCount = await rooms.count();
|
const roomCount = await rooms.count();
|
||||||
expect(roomCount).toBeGreaterThan(0);
|
expect(roomCount).toBeGreaterThan(0);
|
||||||
|
|||||||
Reference in New Issue
Block a user