~james/core

core/web/src/lib/notifications.test.ts -rw-r--r-- 3.3 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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);
	});
});