import assert from "node:assert/strict"; import { test } from "node:test"; import { catalogKeys, inventoryKeys } from "@metis/assets/client"; import { companyKeys } from "@metis/companies/client"; import { identityKeys } from "@metis/identity/client"; import type { SessionResponseDTO, UserDTO } from "@metis/identity/common"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { renderToStaticMarkup } from "react-dom/server"; import { I18nextProvider } from "react-i18next"; import { DashboardNavigationButton, DashboardStateBadge, DashboardTable, dashboardKeys, entryDestination, } from "../../../modules/companies/src/web/dashboard"; import { App } from "./app"; import { createAppI18n } from "./i18n"; import { workspaceAssets, workspaceCatalog, workspaceCompany } from "./test-fixtures/workspace"; const user: UserDTO = { id: "c90b4d83-b06a-4d27-a402-e64899a2dfc2", name: "Usuario de prueba", email: "user@example.test", active: true, isInitialAdmin: false, }; test("las acciones del dashboard conservan nombre, selección y deshabilitado sin enviar formularios", () => { const markup = renderToStaticMarkup( 2 ); assert.ok(markup.includes('aria-label="Open tickets: 2"')); assert.ok(markup.includes('aria-pressed="true"')); assert.ok(markup.includes('disabled=""')); assert.ok(markup.includes('type="button"')); assert.ok(markup.includes(">2")); }); test("los estados del dashboard mantienen texto observable además del color", () => { for (const state of ["urgent", "high", "in_progress", "open", null]) { const label = state ?? "Unknown"; const markup = renderToStaticMarkup( {label} ); assert.ok(markup.includes(`>${label}`)); if (state === null) { assert.equal(markup.includes("data-state="), false); } else { assert.ok(markup.includes(`data-state="${state}"`)); } } }); test("las tablas del dashboard conservan caption, cabeceras semánticas y contenido", () => { const markup = renderToStaticMarkup( Reference T-2026-001 ); assert.match(markup, /]*>Pending work<\/caption>/); assert.ok(markup.includes('Reference')); assert.ok(markup.includes("T-2026-001")); }); async function renderPage( language: string, session?: SessionResponseDTO, seed?: (client: QueryClient) => void ) { const { instance, initialization } = createAppI18n(language); await initialization; const client = new QueryClient({ defaultOptions: { queries: { retry: false, retryOnMount: false, staleTime: Number.POSITIVE_INFINITY }, }, }); if (session) { client.setQueryData(identityKeys.session, session); } seed?.(client); try { return renderToStaticMarkup( ); } finally { client.clear(); } } test("el acceso protege el contenido y traduce la carga de sesión", async () => { for (const [language, pending] of [ ["es", "Comprobando acceso"], ["en", "Checking access"], ] as const) { const markup = await renderPage(language); assert.equal(markup.includes("Tu espacio de soporte"), false); assert.equal(markup.includes("Your support workspace"), false); assert.match(markup, /role="combobox"/); assert.ok(markup.includes(language === "es" ? "Español" : "English")); assert.ok(markup.includes(pending)); assert.ok(markup.includes('role="status"')); } }); test("la configuración única y el login presentan sus formularios traducidos sin acceso al contenido", async () => { for (const [language, setupTitle, loginTitle] of [ ["es", "Configura Metis Desk", "Accede a Metis Desk"], ["en", "Set up Metis Desk", "Sign in to Metis Desk"], ] as const) { const snapshot = { user: null, csrfToken: "a".repeat(64), setupRequired: true }; const setup = await renderPage(language, snapshot); assert.ok(setup.includes(setupTitle)); assert.match(setup, /autoComplete="new-password"/); assert.match(setup, /minLength="8"/); assert.equal(setup.includes("Tu espacio de soporte"), false); const login = await renderPage(language, { ...snapshot, setupRequired: false }); assert.ok(login.includes(loginTitle)); assert.match(login, /autoComplete="current-password"/); assert.match(login, /minLength="1"/); assert.equal(login.includes('autoComplete="name"'), false); } }); test("solo el administrador inicial ve la navegación de gestión de usuarios", async () => { const snapshot = { user, csrfToken: "a".repeat(64), setupRequired: false }; const normal = await renderPage("es", snapshot); assert.ok(normal.includes("Tu espacio de soporte")); assert.ok(normal.includes("Mi contraseña")); assert.ok(normal.includes("Salir")); assert.equal(normal.includes('href="/settings/users"'), false); const admin = await renderPage("es", { ...snapshot, user: { ...user, isInitialAdmin: true } }); assert.ok(admin.includes('href="/settings/users"')); }); test("el espacio abre Inicio tras el login y mantiene acceso a las compañías sin formularios visibles", async () => { for (const [language, spaces, panel] of [ ["es", "Espacios de compañías", "Ficha del activo seleccionado"], ["en", "Company spaces", "Selected asset details"], ]) { const html = await renderPage( language ?? "es", { user, csrfToken: "a".repeat(64), setupRequired: false }, (client) => { client.setQueryData(companyKeys.list, { companies: [workspaceCompany] }); client.setQueryData(companyKeys.detail(workspaceCompany.id), { company: workspaceCompany }); client.setQueryData(inventoryKeys.company(workspaceCompany.id), { assets: workspaceAssets, relationships: [], workstations: [], }); client.setQueryData(catalogKeys.scope(workspaceCompany.id), workspaceCatalog); } ); assert.ok(html.includes(`aria-label="${spaces}"`)); assert.ok(html.includes(language === "es" ? "Trabajo pendiente" : "Pending work")); assert.ok(html.includes(language === "es" ? "Resumen por compañías" : "Company summary")); assert.ok(html.includes(`aria-label="${language === "es" ? "Inicio" : "Home"}"`)); assert.equal(html.includes(`aria-label="${panel}"`), false); assert.ok( html.includes(language === "es" ? "Buscar en Metis Desk..." : "Search Metis Desk...") ); assert.ok(html.includes(language === "es" ? "Configuración" : "Settings")); assert.equal(html.includes("shell.catalogs"), false); assert.ok(html.includes(user.name)); assert.equal(html.includes(" { const session = { user, csrfToken: "a".repeat(64), setupRequired: false }; for (const language of ["es", "en"]) { const loading = await renderPage(language, session); assert.ok(loading.includes("—")); assert.ok(loading.includes('role="status"')); const empty = await renderPage(language, session, (client) => { client.setQueryData(companyKeys.list, { companies: [] }); client.setQueryData(dashboardKeys.block("summary"), { companies: 0, tickets: 0, interventions: 0, assets: 0, resources: 0, }); for (const block of ["work", "activity", "companies"]) { client.setQueryData(dashboardKeys.block(block), { items: [], total: 0 }); } client.setQueryData(dashboardKeys.block("attention"), { items: [] }); }); assert.ok( empty.includes( language === "es" ? "No hay tickets ni intervenciones pendientes." : "There are no pending tickets or interventions." ) ); assert.ok(!empty.includes('role="alert"')); const partial = await renderPage(language, session, (client) => { client.setQueryData(dashboardKeys.block("summary"), { companies: 1, tickets: 23, interventions: 0, assets: 5, resources: 6, }); const query = client .getQueryCache() .build(client, { queryKey: dashboardKeys.block("activity") }); query.setState({ status: "error", error: new Error("failed"), fetchStatus: "idle" }); }); assert.ok(partial.includes('role="alert"')); assert.ok(partial.includes(">23")); assert.ok(!partial.includes("dashboard.blockError")); } }); test("los enlaces del dashboard mantienen la compañía y la ficha exacta", () => { const item = { id: "entry", companyId: "company" }; assert.deepEqual(entryDestination({ ...item, kind: "ticket" }), { area: "tickets", companyId: "company", id: "entry", }); assert.deepEqual(entryDestination({ ...item, kind: "asset" }), { area: "company", id: "company", section: "inventory", entityId: "entry", }); assert.deepEqual(entryDestination({ ...item, kind: "resource" }), { area: "company", id: "company", section: "resources", entityId: "entry", }); assert.deepEqual(entryDestination({ ...item, kind: "profile" }), { area: "company", id: "company", section: "profiles", entityId: "entry", }); }); test("los bloques del dashboard presentan datos, avisos y actividad reales en ambos idiomas", async () => { const session = { user, csrfToken: "a".repeat(64), setupRequired: false }; for (const language of ["es", "en"]) { const html = await renderPage(language, session, (client) => { client.setQueryData(companyKeys.list, { companies: [workspaceCompany] }); client.setQueryData(dashboardKeys.block("summary"), { companies: 1, tickets: 2, interventions: 3, assets: 4, resources: 5, }); const entry = { id: workspaceCompany.id, companyId: workspaceCompany.id, companyName: workspaceCompany.name, name: "Revisar acceso", kind: "ticket", reference: "T-2026-001", state: "open", priority: "high", recordedAt: "2026-10-01T10:30:00.000Z", actorName: "Operador test", }; client.setQueryData(dashboardKeys.block("work"), { items: [entry], total: 1 }); client.setQueryData(dashboardKeys.block("activity"), { items: [entry], total: 1 }); client.setQueryData(dashboardKeys.block("attention"), { items: [{ kind: "missing_site", count: 2 }], }); client.setQueryData(dashboardKeys.block("companies"), { items: [ { id: workspaceCompany.id, name: workspaceCompany.name, companies: 1, tickets: 2, interventions: 3, assets: 4, resources: 5, people: 6, }, ], total: 1, }); }); assert.ok(html.includes("Revisar acceso")); assert.ok(html.includes("T-2026-001")); assert.ok(html.includes("Operador test")); assert.ok(html.includes(workspaceCompany.name)); assert.ok(html.includes('dateTime="2026-10-01T10:30:00.000Z"')); assert.ok(html.includes('data-state="high"')); assert.ok(html.includes('data-state="open"')); assert.ok( html.includes( `aria-label="${language === "es" ? "Tickets abiertos" : "Open tickets"} · ${workspaceCompany.name}: 2"` ) ); assert.equal(html.includes("dashboard.issues.missing_site"), false); assert.equal(html.includes('role="alert"'), false); assert.equal(html.includes("