8c7affc933
Fixed import paths for 43 components moved into features/ directories. Key fixes: - api/, types/, hooks/, state/, utils/ imports need ../../../ from features/*/ - components/ imports need ../../ from features/*/ - Cross-feature imports use relative paths (e.g., ../tool/tools-bottom-sheet) - app-shell.tsx updated to import from features/ subdirectories Frontend typecheck now passes except for one pre-existing error: xterm-addon-webgl missing type declarations. Quality gates: ruff passed on backend, py_compile passed on all backend files.
178 lines
5.2 KiB
TypeScript
178 lines
5.2 KiB
TypeScript
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<string, unknown>) => ({
|
|
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 <NotificationProvider>{children}</NotificationProvider>;
|
|
}
|
|
|
|
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(<NotificationCenter />, { wrapper });
|
|
expect(
|
|
screen.getByRole("button", { name: /notifications/i }),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows badge when unread count > 0", async () => {
|
|
mockedGetUnreadCount.mockResolvedValue(3);
|
|
render(<NotificationCenter />, { wrapper });
|
|
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
expect(screen.getByText("3")).toBeInTheDocument();
|
|
});
|
|
|
|
it("hides badge when unread count is 0", () => {
|
|
render(<NotificationCenter />, { wrapper });
|
|
expect(screen.queryByText("0")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("opens dropdown on bell click", () => {
|
|
render(<NotificationCenter />, { wrapper });
|
|
fireEvent.click(screen.getByRole("button", { name: /notifications/i }));
|
|
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
});
|
|
|
|
it("closes dropdown on outside click", () => {
|
|
render(
|
|
<div>
|
|
<div data-testid="outside">Outside</div>
|
|
<NotificationCenter />
|
|
</div>,
|
|
{ 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(<NotificationCenter />, { 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(<NotificationCenter />, { 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(<NotificationCenter />, { 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(<NotificationCenter />, { 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(<NotificationCenter />, { 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(<NotificationCenter />, { wrapper });
|
|
fireEvent.click(screen.getByRole("button", { name: /notifications/i }));
|
|
|
|
await vi.advanceTimersByTimeAsync(100);
|
|
expect(mockedGetNotifications).toHaveBeenCalled();
|
|
});
|
|
});
|