f13a63dc2f
- Custom ToastContext + ToastProvider + ToastContainer (~170 lines, no deps) - useEvents() SSE hook with exponential backoff reconnect - EventProvider context for app-wide SSE stream sharing - Event-to-toast bridge with severity mapping and deduplication - Real-time status badge updates replacing 30s polling - EventSource auth probe (401/429 detection via fetch) - 14 frontend tests (useEvents + toast-rules) Quality gates: vitest 14 passed, tsc clean, eslint clean
149 lines
3.9 KiB
TypeScript
149 lines
3.9 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { handleEventToast, clearToastDedup } from "./toast-rules";
|
|
import type { InstanceEventPayload } from "../types/events";
|
|
|
|
const mockToastInfo = vi.fn();
|
|
const mockToastSuccess = vi.fn();
|
|
const mockToastWarning = vi.fn();
|
|
const mockToastError = vi.fn();
|
|
|
|
vi.mock("../state/toast", () => ({
|
|
toast: {
|
|
info: (...args: unknown[]) => mockToastInfo(...args),
|
|
success: (...args: unknown[]) => mockToastSuccess(...args),
|
|
warning: (...args: unknown[]) => mockToastWarning(...args),
|
|
error: (...args: unknown[]) => mockToastError(...args),
|
|
},
|
|
}));
|
|
|
|
describe("toast-rules", () => {
|
|
beforeEach(() => {
|
|
clearToastDedup();
|
|
mockToastInfo.mockClear();
|
|
mockToastSuccess.mockClear();
|
|
mockToastWarning.mockClear();
|
|
mockToastError.mockClear();
|
|
});
|
|
|
|
it("maps instance.started to info toast", () => {
|
|
const event: InstanceEventPayload = {
|
|
event: "instance.started",
|
|
instance_id: "inst-1",
|
|
status: "starting",
|
|
message: "Container starting...",
|
|
metadata: {},
|
|
timestamp: "2026-05-28T12:00:00Z",
|
|
correlation_id: "corr-1",
|
|
};
|
|
|
|
handleEventToast(event);
|
|
expect(mockToastInfo).toHaveBeenCalledWith("Container starting...", {
|
|
duration: 3000,
|
|
});
|
|
});
|
|
|
|
it("maps health_changed to running to success toast", () => {
|
|
const event: InstanceEventPayload = {
|
|
event: "instance.health_changed",
|
|
instance_id: "inst-1",
|
|
status: "running",
|
|
message: "Container is running",
|
|
metadata: { previous_status: "starting" },
|
|
timestamp: "2026-05-28T12:00:00Z",
|
|
correlation_id: "corr-1",
|
|
};
|
|
|
|
handleEventToast(event);
|
|
expect(mockToastSuccess).toHaveBeenCalledWith("Container running", {
|
|
duration: 3000,
|
|
});
|
|
});
|
|
|
|
it("maps health_changed to unhealthy to warning toast", () => {
|
|
const event: InstanceEventPayload = {
|
|
event: "instance.health_changed",
|
|
instance_id: "inst-1",
|
|
status: "unhealthy",
|
|
message: "Container is unhealthy",
|
|
metadata: { previous_status: "running" },
|
|
timestamp: "2026-05-28T12:00:00Z",
|
|
correlation_id: "corr-1",
|
|
};
|
|
|
|
handleEventToast(event);
|
|
expect(mockToastWarning).toHaveBeenCalledWith("Container unhealthy", {
|
|
duration: 5000,
|
|
});
|
|
});
|
|
|
|
it("maps instance.error to error toast with exit code", () => {
|
|
const event: InstanceEventPayload = {
|
|
event: "instance.error",
|
|
instance_id: "inst-1",
|
|
status: "error",
|
|
message: "Container crashed",
|
|
metadata: { exit_code: 137 },
|
|
timestamp: "2026-05-28T12:00:00Z",
|
|
correlation_id: "corr-1",
|
|
};
|
|
|
|
handleEventToast(event);
|
|
expect(mockToastError).toHaveBeenCalledWith(
|
|
"Container crashed (exit code: 137)",
|
|
{ duration: 10000 },
|
|
);
|
|
});
|
|
|
|
it("maps instance.error to error toast without exit code", () => {
|
|
const event: InstanceEventPayload = {
|
|
event: "instance.error",
|
|
instance_id: "inst-1",
|
|
status: "error",
|
|
message: "Build failed",
|
|
metadata: {},
|
|
timestamp: "2026-05-28T12:00:00Z",
|
|
correlation_id: "corr-1",
|
|
};
|
|
|
|
handleEventToast(event);
|
|
expect(mockToastError).toHaveBeenCalledWith("Build failed", {
|
|
duration: 10000,
|
|
});
|
|
});
|
|
|
|
it("deduplicates within one second", () => {
|
|
const event: InstanceEventPayload = {
|
|
event: "instance.started",
|
|
instance_id: "inst-1",
|
|
status: "starting",
|
|
message: "Container starting...",
|
|
metadata: {},
|
|
timestamp: "2026-05-28T12:00:00Z",
|
|
correlation_id: "corr-1",
|
|
};
|
|
|
|
handleEventToast(event);
|
|
handleEventToast(event);
|
|
expect(mockToastInfo).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("allows duplicate after one second", () => {
|
|
vi.useFakeTimers();
|
|
const event: InstanceEventPayload = {
|
|
event: "instance.started",
|
|
instance_id: "inst-1",
|
|
status: "starting",
|
|
message: "Container starting...",
|
|
metadata: {},
|
|
timestamp: "2026-05-28T12:00:00Z",
|
|
correlation_id: "corr-1",
|
|
};
|
|
|
|
handleEventToast(event);
|
|
vi.advanceTimersByTime(1100);
|
|
handleEventToast(event);
|
|
expect(mockToastInfo).toHaveBeenCalledTimes(2);
|
|
vi.useRealTimers();
|
|
});
|
|
});
|