import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, act } from "@testing-library/react"; import { EventToastBridge } from "./event-toast-bridge"; import { useEventContext } from "../state/events"; import { getUserConfig } from "../api/settings"; import { handleEventToast } from "./toast-rules"; import type { InstanceEventPayload } from "../types/events"; vi.mock("../state/events", () => ({ useEventContext: vi.fn(), })); vi.mock("../api/settings", () => ({ getUserConfig: vi.fn(), })); vi.mock("./toast-rules", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, handleEventToast: vi.fn(), clearToastDedup: vi.fn(), }; }); const mockedUseEventContext = vi.mocked(useEventContext); const mockedGetUserConfig = vi.mocked(getUserConfig); const mockedHandleEventToast = vi.mocked(handleEventToast); function makeEvent( eventType: string, overrides?: Partial, ): InstanceEventPayload { return { event: eventType, instance_id: "i-1", status: undefined, message: undefined, metadata: {}, timestamp: "2026-05-29T10:00:00Z", correlation_id: "c1", ...overrides, }; } async function flushPromises() { await act(async () => { await Promise.resolve(); }); } describe("EventToastBridge preference checks", () => { beforeEach(() => { vi.clearAllMocks(); mockedUseEventContext.mockReturnValue({ events: [], connected: false, reconnectCount: 0, }); mockedGetUserConfig.mockResolvedValue({ theme: "system", default_editor: null, git_user_name: null, git_user_email: null, last_session_id: null, notification_toast_level: "all", notification_mute_categories: [], } as unknown as Awaited>); }); afterEach(() => { vi.restoreAllMocks(); }); it("shows toast when level is all and category not muted", async () => { const event = makeEvent("instance.started"); mockedUseEventContext.mockReturnValue({ events: [event], connected: false, reconnectCount: 0, }); render(); await flushPromises(); expect(mockedHandleEventToast).toHaveBeenCalledWith(event); }); it("suppresses toast when level is none", async () => { mockedGetUserConfig.mockResolvedValue({ notification_toast_level: "none", notification_mute_categories: [], } as unknown as Awaited>); const event = makeEvent("instance.started"); mockedUseEventContext.mockReturnValue({ events: [event], connected: false, reconnectCount: 0, }); render(); await flushPromises(); expect(mockedHandleEventToast).not.toHaveBeenCalled(); }); it("suppresses info toast when level is errors", async () => { mockedGetUserConfig.mockResolvedValue({ notification_toast_level: "errors", notification_mute_categories: [], } as unknown as Awaited>); const event = makeEvent("instance.started"); mockedUseEventContext.mockReturnValue({ events: [event], connected: false, reconnectCount: 0, }); render(); await flushPromises(); expect(mockedHandleEventToast).not.toHaveBeenCalled(); }); it("shows error toast when level is errors", async () => { mockedGetUserConfig.mockResolvedValue({ notification_toast_level: "errors", notification_mute_categories: [], } as unknown as Awaited>); const event = makeEvent("instance.error"); mockedUseEventContext.mockReturnValue({ events: [event], connected: false, reconnectCount: 0, }); render(); await flushPromises(); expect(mockedHandleEventToast).toHaveBeenCalledWith(event); }); it("suppresses toast when category is muted", async () => { mockedGetUserConfig.mockResolvedValue({ notification_toast_level: "all", notification_mute_categories: ["instance"], } as unknown as Awaited>); const event = makeEvent("instance.started"); mockedUseEventContext.mockReturnValue({ events: [event], connected: false, reconnectCount: 0, }); render(); await flushPromises(); expect(mockedHandleEventToast).not.toHaveBeenCalled(); }); it("applies preference change immediately via custom event", async () => { const event1 = makeEvent("instance.started"); mockedUseEventContext.mockReturnValue({ events: [event1], connected: false, reconnectCount: 0, }); const { rerender } = render(); await flushPromises(); expect(mockedHandleEventToast).toHaveBeenCalledTimes(1); act(() => { window.dispatchEvent( new CustomEvent("userconfig:updated", { detail: { notification_toast_level: "none" }, }), ); }); const event2 = makeEvent("instance.started"); mockedUseEventContext.mockReturnValue({ events: [event1, event2], connected: false, reconnectCount: 0, }); rerender(); await flushPromises(); expect(mockedHandleEventToast).toHaveBeenCalledTimes(1); }); it("muted category overrides all level", async () => { mockedGetUserConfig.mockResolvedValue({ notification_toast_level: "all", notification_mute_categories: ["instance"], } as unknown as Awaited>); const event = makeEvent("instance.error"); mockedUseEventContext.mockReturnValue({ events: [event], connected: false, reconnectCount: 0, }); render(); await flushPromises(); expect(mockedHandleEventToast).not.toHaveBeenCalled(); }); it("deduplication still works with preferences", async () => { const event = makeEvent("instance.started"); mockedUseEventContext.mockReturnValue({ events: [event, event], connected: false, reconnectCount: 0, }); render(); await flushPromises(); expect(mockedHandleEventToast).toHaveBeenCalledTimes(1); }); it("unmapped event defaults to system/info and shows when level is all", async () => { const event = makeEvent("system.announcement"); mockedUseEventContext.mockReturnValue({ events: [event], connected: false, reconnectCount: 0, }); render(); await flushPromises(); expect(mockedHandleEventToast).toHaveBeenCalledWith(event); }); });