import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { render, screen, fireEvent, cleanup } from "@testing-library/react"; import { NotificationCenter } from "./notification-center"; import { NotificationProvider } from "../../../state/notifications"; vi.mock("../../../api/notifications", () => ({ getNotifications: vi.fn(), getUnreadCount: vi.fn(), markNotificationRead: vi.fn(), markAllNotificationsRead: vi.fn(), dismissNotification: vi.fn(), clearAllNotifications: vi.fn(), })); import { getNotifications, getUnreadCount } from "../../../api/notifications"; const mockedGetNotifications = vi.mocked(getNotifications); const mockedGetUnreadCount = vi.mocked(getUnreadCount); const makeNotification = (id: string, overrides?: Record) => ({ id, user_id: "user-1", category: "instance", severity: "info" as const, title: `Notification ${id}`, message: null, source_type: null, source_id: null, metadata: {}, read_at: null, dismissed_at: null, created_at: "2026-05-29T10:00:00Z", ...overrides, }); function wrapper({ children }: { children: React.ReactNode }) { return {children}; } describe("NotificationCenter", () => { beforeEach(() => { vi.useFakeTimers({ shouldAdvanceTime: true }); mockedGetNotifications.mockResolvedValue({ items: [], total: 0, limit: 20, offset: 0, }); mockedGetUnreadCount.mockResolvedValue(0); }); afterEach(() => { vi.useRealTimers(); vi.clearAllMocks(); cleanup(); }); it("renders bell icon", () => { render(, { wrapper }); expect( screen.getByRole("button", { name: /notifications/i }), ).toBeInTheDocument(); }); it("shows badge when unread count > 0", async () => { mockedGetUnreadCount.mockResolvedValue(3); render(, { wrapper }); await vi.advanceTimersByTimeAsync(100); expect(screen.getByText("3")).toBeInTheDocument(); }); it("hides badge when unread count is 0", () => { render(, { wrapper }); expect(screen.queryByText("0")).not.toBeInTheDocument(); }); it("opens dropdown on bell click", () => { render(, { wrapper }); fireEvent.click(screen.getByRole("button", { name: /notifications/i })); expect(screen.getByRole("dialog")).toBeInTheDocument(); }); it("closes dropdown on outside click", () => { render(
Outside
, { wrapper }, ); fireEvent.click(screen.getByRole("button", { name: /notifications/i })); expect(screen.getByRole("dialog")).toBeInTheDocument(); fireEvent.mouseDown(screen.getByTestId("outside")); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); it("closes dropdown on escape", () => { render(, { wrapper }); fireEvent.click(screen.getByRole("button", { name: /notifications/i })); expect(screen.getByRole("dialog")).toBeInTheDocument(); fireEvent.keyDown(document, { key: "Escape" }); expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); it("renders empty state when no notifications", () => { render(, { wrapper }); fireEvent.click(screen.getByRole("button", { name: /notifications/i })); expect(screen.getByText("No notifications")).toBeInTheDocument(); }); it("renders notification items", async () => { mockedGetNotifications.mockResolvedValue({ items: [makeNotification("1"), makeNotification("2")], total: 2, limit: 20, offset: 0, }); render(, { wrapper }); fireEvent.click(screen.getByRole("button", { name: /notifications/i })); await vi.advanceTimersByTimeAsync(100); expect(screen.getByText("Notification 1")).toBeInTheDocument(); expect(screen.getByText("Notification 2")).toBeInTheDocument(); }); it("calls markAllRead on footer button click", async () => { mockedGetNotifications.mockResolvedValue({ items: [makeNotification("1")], total: 1, limit: 20, offset: 0, }); render(, { wrapper }); fireEvent.click(screen.getByRole("button", { name: /notifications/i })); await vi.advanceTimersByTimeAsync(100); fireEvent.click(screen.getByRole("button", { name: /mark all as read/i })); const { markAllNotificationsRead: mockMarkAll } = await import( "../../../api/notifications" ); expect(vi.mocked(mockMarkAll)).toHaveBeenCalled(); }); it("calls clearAll on clear-all button click", async () => { mockedGetNotifications.mockResolvedValue({ items: [makeNotification("1")], total: 1, limit: 20, offset: 0, }); render(, { wrapper }); fireEvent.click(screen.getByRole("button", { name: /notifications/i })); await vi.advanceTimersByTimeAsync(100); fireEvent.click(screen.getByRole("button", { name: /clear all/i })); const { clearAllNotifications: mockClearAll } = await import( "../../../api/notifications" ); expect(vi.mocked(mockClearAll)).toHaveBeenCalled(); }); it("refreshes list immediately on open", async () => { render(, { wrapper }); fireEvent.click(screen.getByRole("button", { name: /notifications/i })); await vi.advanceTimersByTimeAsync(100); expect(mockedGetNotifications).toHaveBeenCalled(); }); });