Files
siftlode/frontend/src/lib/inboxOrder.test.ts
T
peter f49dc6532f feat(notifications): what asks for you comes first, then one stream; old read rows fold
The center showed two fixed sections, the server's above this browser's, so an
admin's "access requests waiting" sat under every server row, week-old read
"Download ready" notices among them. One order now, for both sources
(lib/inboxOrder): "Needs you" (unread, an access-requests nudge, a notice
waiting on an interaction), then everything else newest first, and read rows
older than a week fold under "Earlier (n)", closed by default.

A row that asked in this visit keeps its place once read: marking it read no
longer moves it to another list under the reader. Mark all read empties the
band, the client notices of the visit included (the server rows read at once,
not when the write lands). Marking a row read from its button hands the focus
to the row instead of dropping it on <body> as the button goes.
2026-10-28 20:01:21 +01:00

114 lines
3.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { FOLD_AFTER_MS, entryKey, orderInbox, type InboxOrder } from "./inboxOrder";
import type { AppNotification } from "./api";
import type { Notification as ClientNotif } from "./notifications";
const NOW = Date.parse("2026-10-28T12:00:00Z");
const ago = (ms: number) => NOW - ms;
const H = 3600 * 1000;
const D = 24 * H;
const server = (id: number, at: number | null, read: boolean) =>
({
id,
type: "download_done",
title: "t",
body: null,
data: null,
read,
dismissed: false,
created_at: at === null ? null : new Date(at).toISOString(),
}) as AppNotification;
const client = (id: number, at: number, extra: Partial<ClientNotif> = {}) =>
({
id,
level: "info",
message: "m",
requiresInteraction: false,
ts: at,
read: true,
dismissed: true,
transient: false,
repeat: 1,
...extra,
}) as ClientNotif;
const keys = (o: InboxOrder) => ({
needsYou: o.needsYou.map(entryKey),
recent: o.recent.map(entryKey),
earlier: o.earlier.map(entryKey),
});
const order = (s: AppNotification[], c: ClientNotif[], held: ReadonlySet<string> = new Set()) =>
keys(orderInbox(s, c, { now: NOW, held }));
describe("orderInbox", () => {
it("puts a waiting access-requests nudge above every server row, read or not", () => {
const o = order(
[server(1, ago(1 * H), false), server(2, ago(1 * D), true)],
[client(1, ago(5 * 60 * 1000), { meta: { kind: "access-requests" } })],
);
expect(o.needsYou).toEqual(["client:1", "server:1"]);
expect(o.recent).toEqual(["server:2"]);
});
it("an old unread row still asks: it never folds or drifts below newer read ones", () => {
const o = order([server(1, ago(30 * D), false), server(2, ago(1 * H), true)], []);
expect(o.needsYou).toEqual(["server:1"]);
expect(o.recent).toEqual(["server:2"]);
expect(o.earlier).toEqual([]);
});
it("merges both sources into one stream, newest first", () => {
const o = order(
[server(1, ago(3 * H), true), server(2, ago(1 * H), true)],
[client(7, ago(2 * H)), client(8, ago(4 * H))],
);
expect(o.recent).toEqual(["server:2", "client:7", "server:1", "client:8"]);
});
it("folds read rows older than a week, and only those", () => {
const o = order(
[server(1, ago(FOLD_AFTER_MS + H), true), server(2, ago(FOLD_AFTER_MS - H), true)],
[client(3, ago(9 * D))],
);
expect(o.recent).toEqual(["server:2"]);
expect(o.earlier).toEqual(["server:1", "client:3"]);
});
it("keeps a row that asked earlier in the visit in its place once read, from either source", () => {
// Opening the center marks the client notices read: the band must not empty as it renders; and
// a server row marked read must not jump lists under the reader.
const o = order(
[server(1, ago(30 * D), true), server(2, ago(3 * H), true)],
[client(4, ago(2 * H)), client(5, ago(1 * H))],
new Set(["client:4", "server:1"]),
);
expect(o.needsYou).toEqual(["client:4", "server:1"]);
expect(o.recent).toEqual(["client:5", "server:2"]);
expect(o.earlier).toEqual([]);
});
it("an unread client notice (raised while the center is open) asks", () => {
const o = order([], [client(6, ago(H), { read: false })]);
expect(o.needsYou).toEqual(["client:6"]);
});
it("a notice waiting on an interaction asks until it is dealt with", () => {
const o = order(
[],
[
client(1, ago(H), { requiresInteraction: true, dismissed: false }),
client(2, ago(H), { requiresInteraction: true, dismissed: true }),
],
);
expect(o.needsYou).toEqual(["client:1"]);
expect(o.recent).toEqual(["client:2"]);
});
it("a server row of no known date sorts last and never folds", () => {
const o = order([server(1, null, true), server(2, ago(30 * D), true)], [client(3, ago(H))]);
expect(o.recent).toEqual(["client:3", "server:1"]);
expect(o.earlier).toEqual(["server:2"]);
});
});