import { beforeEach, describe, expect, it, vi } from "vitest";
import { getUnreadCount, hydrateNotifications, listNotifications } from "#lib/api/notifications.ts";
import type { Auth } from "#lib/auth.svelte.ts";
import { createFixtureNotifications, createNotifications } from "#lib/notifications.svelte.ts";
import { notifications } from "#lib/components/notifications/mock.ts";
import type { NotificationSummary } from "#lib/components/notifications/types.ts";
vi.mock("#lib/api/notifications.ts", () => ({
getUnreadCount: vi.fn(),
hydrateNotifications: vi.fn(async () => []),
listNotifications: vi.fn(async () => ({ notifications: [] })),
markAllNotificationsRead: vi.fn(),
markEntityNotificationsRead: vi.fn(),
updateNotificationSeen: vi.fn()
}));
const rows: NotificationSummary[] = notifications.map((notification, index) => ({
...notification,
uri: `at://mock/notif/${index}`,
read: index % 2 === 1
}));
const unreadRow = rows.find((row) => !row.read)!;
const readRow = rows.find((row) => row.read)!;
const baseline = rows.filter((row) => !row.read).length;
const readOf = (
service: { overlay(rows: NotificationSummary[]): NotificationSummary[] },
uri: string
) => service.overlay(rows).find((row) => row.uri === uri)!.read;
describe("notification read state", () => {
it("moves the count with the row it marked", async () => {
const service = createFixtureNotifications(rows);
expect(service.unread).toBe(baseline);
await service.setRead(unreadRow.uri, true);
expect(service.unread).toBe(baseline - 1);
expect(readOf(service, unreadRow.uri)).toBe(true);
await service.setRead(unreadRow.uri, false);
expect(service.unread).toBe(baseline);
expect(readOf(service, unreadRow.uri)).toBe(false);
});
it("ignores a mark that changes nothing", async () => {
const service = createFixtureNotifications(rows);
await service.setRead(unreadRow.uri, true);
await service.setRead(unreadRow.uri, true);
await service.setRead(readRow.uri, true);
expect(service.unread).toBe(baseline - 1);
});
it("empties the count and every known row on mark all read", async () => {
const service = createFixtureNotifications(rows);
await service.markAllRead();
expect(service.unread).toBe(0);
expect(service.overlay(rows).every((row) => row.read)).toBe(true);
});
});
describe("list", () => {
const alice = "did:plc:alice";
const auth = { currentDid: alice, agent: { sub: alice } } as unknown as Auth;
const mockedCount = vi.mocked(getUnreadCount);
const create = () =>
createNotifications(auth, "https://notifs.example", "https://bobbin.example");
beforeEach(() => {
vi.clearAllMocks();
mockedCount.mockResolvedValue({ count: 3 });
});
it("asks for the unread count once when two lists load together", async () => {
const service = create();
await Promise.all([service.list({ read: "unread", limit: 5 }), service.list({ limit: 5 })]);
expect(mockedCount).toHaveBeenCalledTimes(1);
expect(listNotifications).toHaveBeenCalledTimes(2);
expect(hydrateNotifications).toHaveBeenCalledTimes(2);
expect(service.unread).toBe(3);
});
it("asks again once the earlier count has landed", async () => {
const service = create();
await service.list();
mockedCount.mockResolvedValue({ count: 1 });
await service.list();
expect(mockedCount).toHaveBeenCalledTimes(2);
expect(service.unread).toBe(1);
});
});