Merge branch 'fix/progress-panel-sse-listeners' into dev

This commit is contained in:
Developer
2026-06-13 09:30:45 +00:00
2 changed files with 32 additions and 6 deletions
+16 -5
View File
@@ -14,10 +14,13 @@ const mockedCreateEventSource = vi.mocked(createEventSource);
const mockedProbeEventStreamStatus = vi.mocked(probeEventStreamStatus); const mockedProbeEventStreamStatus = vi.mocked(probeEventStreamStatus);
describe("useEvents", () => { describe("useEvents", () => {
let mockEs: EventSource; let mockEs: EventSource & {
emit: (type: string, data: string) => void;
};
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers({ shouldAdvanceTime: true }); vi.useFakeTimers({ shouldAdvanceTime: true });
const listeners = new Map<string, Set<(e: MessageEvent) => void>>();
mockEs = { mockEs = {
close: vi.fn(), close: vi.fn(),
onopen: null, onopen: null,
@@ -27,7 +30,17 @@ describe("useEvents", () => {
return EventSource.OPEN; return EventSource.OPEN;
}, },
url: "http://localhost:8000/events/stream", url: "http://localhost:8000/events/stream",
} as unknown as EventSource; addEventListener: vi.fn((type: string, handler: (e: MessageEvent) => void) => {
if (!listeners.has(type)) listeners.set(type, new Set());
listeners.get(type)!.add(handler);
}),
removeEventListener: vi.fn((type: string, handler: (e: MessageEvent) => void) => {
listeners.get(type)?.delete(handler);
}),
emit: (type: string, data: string) => {
listeners.get(type)?.forEach((handler) => handler({ data } as MessageEvent));
},
} as unknown as EventSource & { emit: (type: string, data: string) => void };
mockedCreateEventSource.mockReturnValue(mockEs); mockedCreateEventSource.mockReturnValue(mockEs);
mockedProbeEventStreamStatus.mockResolvedValue(null); mockedProbeEventStreamStatus.mockResolvedValue(null);
}); });
@@ -56,9 +69,7 @@ describe("useEvents", () => {
}; };
act(() => { act(() => {
mockEs.onmessage?.({ mockEs.emit("instance.started", JSON.stringify(payload));
data: JSON.stringify(payload),
} as MessageEvent);
}); });
await waitFor(() => { await waitFor(() => {
+16 -1
View File
@@ -12,6 +12,16 @@ export interface UseEventsReturn {
const MAX_DELAY = 30000; const MAX_DELAY = 30000;
const BASE_DELAY = 1000; const BASE_DELAY = 1000;
const LIFECYCLE_EVENT_TYPES = [
"instance.created",
"instance.started",
"instance.stopped",
"instance.restarted",
"instance.deleted",
"instance.error",
"instance.health_changed",
] as const;
export function useEvents(): UseEventsReturn { export function useEvents(): UseEventsReturn {
const [events, setEvents] = useState<InstanceEventPayload[]>([]); const [events, setEvents] = useState<InstanceEventPayload[]>([]);
const [connected, setConnected] = useState(false); const [connected, setConnected] = useState(false);
@@ -39,7 +49,7 @@ export function useEvents(): UseEventsReturn {
setReconnectCount(0); setReconnectCount(0);
}; };
es.onmessage = (e) => { const handleEventMessage = (e: MessageEvent) => {
if (!isMountedRef.current) return; if (!isMountedRef.current) return;
try { try {
const payload: InstanceEventPayload = JSON.parse(e.data); const payload: InstanceEventPayload = JSON.parse(e.data);
@@ -49,6 +59,11 @@ export function useEvents(): UseEventsReturn {
} }
}; };
es.addEventListener("message", handleEventMessage);
for (const eventType of LIFECYCLE_EVENT_TYPES) {
es.addEventListener(eventType, handleEventMessage);
}
es.onerror = () => { es.onerror = () => {
if (!isMountedRef.current) return; if (!isMountedRef.current) return;
setConnected(false); setConnected(false);