102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
|
|
import { describe, expect, it } from "vitest";
|
||
|
|
|
||
|
|
import {
|
||
|
|
advanceCatchUpWindow,
|
||
|
|
chatBadge,
|
||
|
|
mergeChatMessages,
|
||
|
|
messageError,
|
||
|
|
rootReadIsProven,
|
||
|
|
} from "./chat-utils";
|
||
|
|
|
||
|
|
describe("chat client state", () => {
|
||
|
|
it("retains through across pages, then starts a new interval after exhaustion", () => {
|
||
|
|
const first = advanceCatchUpWindow(
|
||
|
|
{ after: "5" },
|
||
|
|
{ next: "8", through: "10", hasMore: true },
|
||
|
|
);
|
||
|
|
expect(first).toEqual({ after: "8", through: "10" });
|
||
|
|
const second = advanceCatchUpWindow(first, {
|
||
|
|
next: "9",
|
||
|
|
through: "10",
|
||
|
|
hasMore: true,
|
||
|
|
});
|
||
|
|
expect(second).toEqual({ after: "9", through: "10" });
|
||
|
|
expect(
|
||
|
|
advanceCatchUpWindow(second, {
|
||
|
|
next: "9",
|
||
|
|
through: "10",
|
||
|
|
hasMore: false,
|
||
|
|
}),
|
||
|
|
).toEqual({ after: "10" });
|
||
|
|
});
|
||
|
|
|
||
|
|
it("does not lose the cursor on a stalled page or fold concurrent sends into the old interval", () => {
|
||
|
|
const window = { after: "8", through: "10" };
|
||
|
|
expect(
|
||
|
|
advanceCatchUpWindow(window, { next: "8", through: "10", hasMore: true }),
|
||
|
|
).toBe(window);
|
||
|
|
expect(
|
||
|
|
advanceCatchUpWindow(window, {
|
||
|
|
next: "10",
|
||
|
|
through: "10",
|
||
|
|
hasMore: false,
|
||
|
|
}),
|
||
|
|
).toEqual({ after: "10" });
|
||
|
|
expect(
|
||
|
|
advanceCatchUpWindow(
|
||
|
|
{ after: "10" },
|
||
|
|
{ next: "11", through: "12", hasMore: true },
|
||
|
|
),
|
||
|
|
).toEqual({ after: "11", through: "12" });
|
||
|
|
});
|
||
|
|
it("never replaces a newer edit or tombstone with an older page", () => {
|
||
|
|
const original = {
|
||
|
|
id: "m",
|
||
|
|
sequence: "1",
|
||
|
|
revision: 1,
|
||
|
|
body: "old",
|
||
|
|
status: "active",
|
||
|
|
};
|
||
|
|
const edited = { ...original, revision: 2, body: "new" };
|
||
|
|
const deleted = { ...edited, revision: 3, body: "", status: "deleted" };
|
||
|
|
expect(mergeChatMessages([edited], [original])).toEqual([edited]);
|
||
|
|
expect(mergeChatMessages([deleted], [edited])).toEqual([deleted]);
|
||
|
|
});
|
||
|
|
it("requires a channel-wide scan and does not claim unseen replies as read", () => {
|
||
|
|
expect(rootReadIsProven("20", null, null, "0")).toBe(false);
|
||
|
|
expect(rootReadIsProven("20", "19", null, "0")).toBe(false);
|
||
|
|
expect(rootReadIsProven("20", "25", "18", "0")).toBe(false);
|
||
|
|
expect(rootReadIsProven("20", "25", "22", "0")).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("deduplicates history and catch-up in sequence order", () => {
|
||
|
|
expect(
|
||
|
|
mergeChatMessages(
|
||
|
|
[
|
||
|
|
{ id: "b", sequence: "9" },
|
||
|
|
{ id: "a", sequence: "2" },
|
||
|
|
],
|
||
|
|
[
|
||
|
|
{ id: "b", sequence: "9" },
|
||
|
|
{ id: "c", sequence: "10" },
|
||
|
|
],
|
||
|
|
).map((m) => m.id),
|
||
|
|
).toEqual(["a", "b", "c"]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("distinguishes unread dots from numeric direct and mention activity", () => {
|
||
|
|
expect(
|
||
|
|
chatBadge({ unreadCount: 3, mentionCount: 0, directUnreadCount: 0 }),
|
||
|
|
).toEqual({ dot: true, count: 0 });
|
||
|
|
expect(
|
||
|
|
chatBadge({ unreadCount: 3, mentionCount: 2, directUnreadCount: 1 }),
|
||
|
|
).toEqual({ dot: true, count: 3 });
|
||
|
|
});
|
||
|
|
|
||
|
|
it("maps failed sends to actionable messages", () => {
|
||
|
|
expect(messageError("NOT_FOUND")).toMatch(/access/i);
|
||
|
|
expect(messageError("CONFLICT")).toMatch(/different message/i);
|
||
|
|
expect(messageError("INTERNAL_SERVER_ERROR")).toMatch(/retry/i);
|
||
|
|
});
|
||
|
|
});
|