import type { MonitoringView } from "@batch-cooking/shared"; import "../../src/i18n/i18n"; import type { MonitoringConnectionState, MonitoringSocketClientLike, } from "../../src/api/monitoring-socket"; import { MonitoringPage } from "../../src/pages/admin/monitoring/MonitoringPage"; // MonitoringPage takes its WebSocket client as a prop specifically so it // can be driven like this — no real socket.io server involved. See that // prop's own doc comment in MonitoringPage.tsx for why. class FakeMonitoringSocketClient implements MonitoringSocketClientLike { public connectCallCount = 0; public disconnectCallCount = 0; private readonly _updateListeners = new Set<(data: MonitoringView) => void>(); private readonly _stateListeners = new Set<(state: MonitoringConnectionState) => void>(); public connect(): void { this.connectCallCount += 1; } public disconnect(): void { this.disconnectCallCount += 1; } public onUpdate(listener: (data: MonitoringView) => void): () => void { this._updateListeners.add(listener); return () => this._updateListeners.delete(listener); } public onConnectionStateChange(listener: (state: MonitoringConnectionState) => void): () => void { this._stateListeners.add(listener); return () => this._stateListeners.delete(listener); } /** Test-only: simulates the server pushing a snapshot. */ public emitUpdate(data: MonitoringView): void { for (const listener of this._updateListeners) listener(data); } /** Test-only: simulates a connection-state change. */ public emitState(state: MonitoringConnectionState): void { for (const listener of this._stateListeners) listener(state); } } function monitoringFixture(): MonitoringView { return { generatedAt: "2026-09-05T09:15:00.000Z", services: [ { key: "postgres", status: "up", latencyMs: 3.2, detail: null, checkedAt: "2026-09-05T09:15:00.000Z", }, { key: "api", status: "up", latencyMs: 0, detail: "uptime 3 h · RSS 120 Mo", checkedAt: "2026-09-05T09:15:00.000Z", }, { key: "intent-service", status: "down", latencyMs: null, detail: "fetch failed", checkedAt: "2026-09-05T09:15:00.000Z", }, { key: "tech-step-llm-worker", status: "degraded", latencyMs: null, detail: "dernier battement il y a 9 j", checkedAt: "2026-09-05T09:15:00.000Z", lastRunAt: "2026-08-19T03:00:00.000Z", lastResult: { job: "audit-low-confidence", ok: true, counts: { suggestions: 2 } }, }, ], }; } describe("MonitoringPage", () => { it("shows a loading placeholder before the first update arrives, and connects on mount", () => { const client = new FakeMonitoringSocketClient(); cy.mount(); cy.contains("Chargement").should("be.visible"); cy.get(".monitoring-grid").should("not.exist"); // `.should(...)` (not a bare `cy.then` + chai `expect`) so Cypress // retries: `connect()` is called from a `useEffect`, which commits // asynchronously after `cy.mount` itself has already resolved. cy.wrap(client).its("connectCallCount").should("equal", 1); }); it("renders one card per service once a snapshot arrives", () => { const client = new FakeMonitoringSocketClient(); cy.mount(); cy.then(() => { client.emitState("connected"); client.emitUpdate(monitoringFixture()); }); cy.get(".monitoring-card").should("have.length", 4); cy.contains(".monitoring-card", "Base de données").should("have.class", "monitoring-card--up"); cy.contains(".monitoring-card", "Service NLP (spaCy)").should( "have.class", "monitoring-card--down", ); cy.get(".monitoring-connection-badge").should("not.exist"); }); it("shows a reconnecting badge without hiding the last known board", () => { const client = new FakeMonitoringSocketClient(); cy.mount(); cy.then(() => { client.emitState("connected"); client.emitUpdate(monitoringFixture()); }); cy.get(".monitoring-card").should("have.length", 4); cy.then(() => client.emitState("reconnecting")); cy.get(".monitoring-connection-badge--reconnecting").should("be.visible"); // The board itself is still the one from before the drop — never // replaced by a loading state just because the connection blipped. cy.get(".monitoring-card").should("have.length", 4); }); it("shows an error placeholder when the connection fails before any data ever arrived", () => { const client = new FakeMonitoringSocketClient(); cy.mount(); cy.then(() => client.emitState("error")); cy.contains("Impossible de charger").should("be.visible"); cy.get(".monitoring-grid").should("not.exist"); }); });