a9e2dd3552
During refactoring verification found several remaining inconsistencies: API files (kebab-case naming): - Rename config_profiles.ts → config-profiles.ts - Rename tool_definitions.ts → tool-definitions.ts - Update all imports across 8 files Missing Python __init__.py (backend package structure): - Add utils/__init__.py - Add services/__init__.py - Add schemas/__init__.py - Add services/build/__init__.py Test file import fixes (component reorganization fallout): - DashboardPage.test.tsx: import from ./dashboard → ./DashboardPage - ProjectsPage.test.tsx: import from ./projects → ./ProjectsPage - event-toast-bridge.test.tsx: fix relative paths for moved components (../state/events → ../../../state/events, ./toast-rules → ../../toast-rules) - notification-center.test.tsx: fix relative path (../state/notifications → ../../../state/notifications) Quality gates: tsc --noEmit (pass), build (pass), py_compile (pass) Tests: 9/12 test files pass (3 pre-existing UI test failures unrelated to refactoring)
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();
|
|
});
|
|
});
|