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");
});
});