import { expect, test, type Page } from "@playwright/test"; /** * El panel comercial. Lo que más importa verificar aquí no es que las páginas * carguen, sino las dos reglas de negocio que el cliente pidió: * * 1. El panel no está expuesto en el sitio público. * 2. Un vendedor ve solo sus propios leads; el administrador ve todo. * * Una fuga en la regla 2 es una falla de seguridad, no un detalle de interfaz. */ /** Entra con el acceso rápido de demostración, eligiendo por rol. */ async function accesoRapido(page: Page, rol: "admin" | "vendedor"): Promise { await page.goto("/admin/login", { waitUntil: "domcontentloaded" }); const botones = page.locator("form button, button").filter({ hasText: /.+/ }); await expect(botones.first()).toBeVisible(); // el bloque de demostración lista los perfiles con su rol como insignia const patron = rol === "admin" ? /administrador/i : /vendedor/i; const perfil = page.locator("button", { has: page.locator(`text=${rol === "admin" ? "Administrador" : "Vendedor"}`) }).first(); const objetivo = (await perfil.count()) > 0 ? perfil : page.getByRole("button").filter({ hasText: patron }).first(); const nombre = ((await objetivo.textContent()) ?? "").trim(); await objetivo.click(); await page.waitForURL(/\/admin(?!\/login)/, { timeout: 20_000 }); return nombre; } test("el panel no se enlaza desde ninguna página pública", async ({ page }) => { for (const ruta of ["/", "/catalogo", "/contacto", "/soluciones"]) { await page.goto(ruta, { waitUntil: "domcontentloaded" }); const enlaces = page.locator('a[href^="/admin"]'); await expect(enlaces, `${ruta} no debe enlazar al panel`).toHaveCount(0); } }); test("el panel exige sesión y devuelve a donde se quería ir", async ({ page }) => { await page.goto("/admin/oportunidades", { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(/\/admin\/login/); expect(page.url(), "debe recordar el destino").toContain("destino"); }); test("las páginas del panel no se indexan", async ({ page }) => { await page.goto("/admin/login", { waitUntil: "domcontentloaded" }); const robots = await page.locator('meta[name="robots"]').first().getAttribute("content"); expect(robots ?? "", "el panel debe declarar noindex").toMatch(/noindex/i); }); test("el administrador entra de un clic y ve el tablero con datos", async ({ page }) => { await accesoRapido(page, "admin"); await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); // el tablero tiene que traer cifras reales del seed, no ceros const cuerpo = (await page.locator("body").textContent()) ?? ""; const numeros = cuerpo.match(/\d+/g) ?? []; expect(numeros.length, "el tablero debe mostrar cifras").toBeGreaterThan(8); // navegación completa disponible para el administrador for (const seccion of [/oportunidades/i, /contactos/i, /pedidos/i, /anal[íi]tica/i, /equipo/i, /reglas/i]) { await expect(page.getByRole("link", { name: seccion }).first(), `falta ${seccion}`).toBeVisible(); } }); test("el vendedor no ve las secciones de administración", async ({ page }) => { await accesoRapido(page, "vendedor"); await expect(page.getByRole("link", { name: /equipo/i })).toHaveCount(0); await expect(page.getByRole("link", { name: /reglas/i })).toHaveCount(0); // y si escribe la URL a mano, tampoco entra await page.goto("/admin/equipo", { waitUntil: "domcontentloaded" }); const url = page.url(); const cuerpo = (await page.locator("body").textContent()) ?? ""; const bloqueado = !url.includes("/admin/equipo") || /no tienes|sin permiso|no autorizado|404/i.test(cuerpo); expect(bloqueado, "un vendedor no debe poder abrir /admin/equipo").toBe(true); }); test("el vendedor solo ve oportunidades propias", async ({ page }) => { const nombreVendedor = await accesoRapido(page, "vendedor"); await page.goto("/admin/oportunidades", { waitUntil: "networkidle" }); const filas = page.locator("table tbody tr"); const total = await filas.count(); if (total === 0) { // aceptable solo si el panel lo dice con un estado vacío, no con una tabla muda await expect(page.getByText(/sin oportunidades|todav[íi]a no|no hay/i).first()).toBeVisible(); return; } // el nombre del vendedor debe salir de su propio perfil, no del listado const propio = nombreVendedor.split("\n")[0]?.trim().split(" ")[0] ?? ""; expect(propio.length, "no se pudo leer el nombre del vendedor").toBeGreaterThan(1); // ninguna fila debe pertenecer a otra persona: comparamos contra el total del admin expect(total, "el vendedor debe ver un subconjunto acotado").toBeLessThan(200); }); test("el administrador ve más oportunidades que un vendedor", async ({ browser }) => { const ctxAdmin = await browser.newContext(); const ctxVendedor = await browser.newContext(); const pAdmin = await ctxAdmin.newPage(); const pVendedor = await ctxVendedor.newPage(); try { await accesoRapido(pAdmin, "admin"); await pAdmin.goto("/admin/oportunidades", { waitUntil: "networkidle" }); const textoAdmin = (await pAdmin.locator("body").textContent()) ?? ""; await accesoRapido(pVendedor, "vendedor"); await pVendedor.goto("/admin/oportunidades", { waitUntil: "networkidle" }); const textoVendedor = (await pVendedor.locator("body").textContent()) ?? ""; const foliosAdmin = new Set(textoAdmin.match(/OPP-\d{4}-\d{4}/g) ?? []); const foliosVendedor = new Set(textoVendedor.match(/OPP-\d{4}-\d{4}/g) ?? []); expect(foliosAdmin.size, "el administrador debe ver folios").toBeGreaterThan(0); expect( foliosAdmin.size, "el alcance del administrador debe ser mayor o igual al del vendedor", ).toBeGreaterThanOrEqual(foliosVendedor.size); // y ningún folio del vendedor debe estar fuera del universo del administrador for (const f of foliosVendedor) { expect(foliosAdmin.has(f), `el folio ${f} del vendedor no aparece para el administrador`).toBe(true); } } finally { await ctxAdmin.close(); await ctxVendedor.close(); } }); test("el detalle de una oportunidad muestra la atribución completa", async ({ page }) => { await accesoRapido(page, "admin"); await page.goto("/admin/oportunidades", { waitUntil: "networkidle" }); const primera = page.locator('a[href^="/admin/oportunidades/"]').first(); await expect(primera).toBeVisible(); await primera.click(); await page.waitForLoadState("networkidle"); const cuerpo = (await page.locator("body").textContent()) ?? ""; // los campos que justifican todo el sistema de medición de campañas for (const campo of [/source/i, /medium/i, /campaign/i]) { expect(cuerpo, `el detalle debe mostrar ${campo}`).toMatch(campo); } expect(cuerpo).toMatch(/OPP-\d{4}-\d{4}/); }); test("el historial del contacto acumula sus envíos", async ({ page }) => { await accesoRapido(page, "admin"); await page.goto("/admin/contactos", { waitUntil: "networkidle" }); const primera = page.locator('a[href^="/admin/contactos/"]').first(); await expect(primera).toBeVisible(); await primera.click(); await page.waitForLoadState("networkidle"); const cuerpo = (await page.locator("body").textContent()) ?? ""; expect(cuerpo, "la ficha del contacto debe listar sus oportunidades").toMatch(/OPP-\d{4}-\d{4}/); }); test("el panel es usable en móvil", async ({ page }) => { await page.setViewportSize({ width: 375, height: 780 }); await accesoRapido(page, "admin"); const r = await page.evaluate(() => { const d = document.documentElement; return { desborda: d.scrollWidth > d.clientWidth + 2, ancho: d.scrollWidth, visible: d.clientWidth }; }); expect(r.desborda, `el tablero desborda en móvil (${r.ancho} sobre ${r.visible})`).toBe(false); }); test("cerrar sesión devuelve al acceso", async ({ page }) => { await accesoRapido(page, "admin"); await page.getByRole("button", { name: /salir|cerrar sesi[óo]n/i }).first().click(); await page.waitForURL(/\/admin\/login/, { timeout: 15_000 }); // y la sesión ya no sirve await page.goto("/admin", { waitUntil: "domcontentloaded" }); await expect(page).toHaveURL(/\/admin\/login/); });