Files
headquarter/apps/web/src/components/notification-item.test.tsx
T
alex 2bec205a30 feat: notification center frontend core (PR-3)
- Bell icon in icon registry (Phosphor Bell)
- NotificationProvider context with polling (15s unread / 30s list)
- useNotifications() hook with optimistic updates
- NotificationCenter component: bell + badge + dropdown panel
- NotificationItem component: severity icon, title, relative time, actions
- AppShell integration: mount in header-actions, hidden on mobile
- CSS styles: dropdown, items, unread/read states, empty state
- formatRelativeTime utility (custom, no new deps)
- 25 frontend tests (9 hook + 6 item + 10 center)

Quality gates: vitest 25 passed, tsc clean, eslint clean
2026-05-29 13:17:22 +02:00

105 lines
2.5 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from "vitest";
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
import { NotificationItem } from "./notification-item";
afterEach(() => {
cleanup();
});
const makeNotification = (overrides?: Record<string, unknown>) => ({
id: "1",
user_id: "user-1",
category: "instance",
severity: "info" as const,
title: "Container started",
message: null,
source_type: null,
source_id: null,
metadata: {},
read_at: null,
dismissed_at: null,
created_at: "2026-05-29T10:00:00Z",
...overrides,
});
describe("NotificationItem", () => {
it("displays title and relative time", () => {
render(
<NotificationItem
notification={makeNotification()}
onMarkRead={vi.fn()}
onDismiss={vi.fn()}
/>,
);
expect(screen.getByText("Container started")).toBeInTheDocument();
expect(screen.getByText(/ago|just now/)).toBeInTheDocument();
});
it("applies unread styling when read_at is null", () => {
render(
<NotificationItem
notification={makeNotification({ read_at: null })}
onMarkRead={vi.fn()}
onDismiss={vi.fn()}
/>,
);
const row = screen.getByRole("listitem");
expect(row.className).toContain("notification-item--unread");
});
it("applies read styling when read_at is set", () => {
render(
<NotificationItem
notification={makeNotification({ read_at: "2026-05-29T10:01:00Z" })}
onMarkRead={vi.fn()}
onDismiss={vi.fn()}
/>,
);
const row = screen.getByRole("listitem");
expect(row.className).toContain("notification-item--read");
});
it("calls onMarkRead when mark read clicked", () => {
const onMarkRead = vi.fn();
render(
<NotificationItem
notification={makeNotification()}
onMarkRead={onMarkRead}
onDismiss={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /mark read/i }));
expect(onMarkRead).toHaveBeenCalledWith("1");
});
it("calls onDismiss when dismiss clicked", () => {
const onDismiss = vi.fn();
render(
<NotificationItem
notification={makeNotification()}
onMarkRead={vi.fn()}
onDismiss={onDismiss}
/>,
);
fireEvent.click(screen.getByRole("button", { name: /dismiss/i }));
expect(onDismiss).toHaveBeenCalledWith("1");
});
it("displays severity icon", () => {
render(
<NotificationItem
notification={makeNotification({ severity: "error" })}
onMarkRead={vi.fn()}
onDismiss={vi.fn()}
/>,
);
expect(screen.getByRole("img", { hidden: true })).toBeInTheDocument();
});
});