Files
headquarter/apps/web/src/components/features/notification/notification-center.test.tsx
T
alex 8c7affc933 fix: correct relative imports after component reorganization
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.
2026-06-04 12:46:45 +02:00

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();
});
});