Skip to content
109 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { PresenceEntry } from "@g1t/contracts";
5
6import {
7 IDLE_MS,
8 STATUS_PRESETS,
9 clearAtFor,
10 customEmojiName,
11 isIdle,
12 liveStatus,
13 localInputValue,
14 mergePeople,
15 pauseUntil,
16 shownAs,
17 statusWords,
18 untilLabel,
19} from "./presence.ts";
20
21// A Wednesday, 14:20 local time.
22const NOW = new Date(2026, 9, 7, 14, 20, 0, 0);
23const MIN = 60_000;
24
25test("clear after: minutes and hours from now, the end of today, the end of the week, or never", () => {
26 assert.equal(clearAtFor("30m", NOW), new Date(NOW.getTime() + 30 * MIN).toISOString());
27 assert.equal(clearAtFor("1h", NOW), new Date(NOW.getTime() + 60 * MIN).toISOString());
28 assert.equal(clearAtFor("4h", NOW), new Date(NOW.getTime() + 240 * MIN).toISOString());
29 assert.equal(clearAtFor("today", NOW), new Date(2026, 9, 8, 0, 0, 0, 0).toISOString());
30 // The coming Monday's midnight: the end of Sunday.
31 assert.equal(clearAtFor("week", NOW), new Date(2026, 9, 12, 0, 0, 0, 0).toISOString());
32 // On a Monday it is the next one.
33 assert.equal(clearAtFor("week", new Date(2026, 9, 12, 10, 0)), new Date(2026, 9, 19, 0, 0, 0, 0).toISOString());
34 assert.equal(clearAtFor("never", NOW), null);
35});
36
37test("a custom time is the viewer's own, and only one still ahead counts", () => {
38 assert.equal(clearAtFor("custom", NOW, "2026-10-07T17:45"), new Date(2026, 9, 7, 17, 45).toISOString());
39 assert.equal(clearAtFor("custom", NOW, "2026-10-07T09:00"), null);
40 assert.equal(clearAtFor("custom", NOW, ""), null);
41 assert.equal(clearAtFor("custom", NOW, "nonsense"), null);
42 assert.equal(localInputValue(new Date(2026, 0, 5, 7, 3)), "2026-01-05T07:03");
43});
44
45test("pausing notifications: half an hour, an hour, or until nine tomorrow morning", () => {
46 assert.equal(pauseUntil("30m", NOW), new Date(NOW.getTime() + 30 * MIN).toISOString());
47 assert.equal(pauseUntil("1h", NOW), new Date(NOW.getTime() + 60 * MIN).toISOString());
48 assert.equal(pauseUntil("tomorrow", NOW), new Date(2026, 9, 8, 9, 0).toISOString());
49 // Just after midnight, tomorrow is still the next day's morning.
50 assert.equal(pauseUntil("tomorrow", new Date(2026, 9, 8, 0, 30)), new Date(2026, 9, 9, 9, 0).toISOString());
51});
52
53test("every preset says something and clears sensibly", () => {
54 const texts = STATUS_PRESETS.map((preset) => preset.text);
55 for (const wanted of ["In a meeting", "Commuting", "Out sick", "On vacation", "Focusing"]) assert.ok(texts.includes(wanted), wanted);
56 assert.equal(STATUS_PRESETS.find((preset) => preset.text === "On vacation")!.clear, "never");
57});
58
59test("how someone shows: offline, away, active, and Do Not Disturb over either", () => {
60 const now = NOW.getTime();
61 const entry = (over: Partial<PresenceEntry>): PresenceEntry => ({ user_id: "usr_1", username: "ana", presence: "active", dnd_until: null, status: null, at: 1, ...over });
62 assert.equal(shownAs(null, now), "offline");
63 assert.equal(shownAs(entry({ presence: "offline", dnd_until: new Date(now + MIN).toISOString() }), now), "offline");
64 assert.equal(shownAs(entry({ presence: "away" }), now), "away");
65 assert.equal(shownAs(entry({}), now), "active");
66 assert.equal(shownAs(entry({ dnd_until: new Date(now + MIN).toISOString() }), now), "dnd");
67 assert.equal(shownAs(entry({ dnd_until: new Date(now - MIN).toISOString() }), now), "active");
68});
69
70test("a status past its time is gone, even before the feed says so", () => {
71 const status = { emoji: "🍔", text: "Lunch", clear_at: new Date(NOW.getTime() + MIN).toISOString(), source: "manual" as const, set_at: NOW.toISOString() };
72 assert.equal(liveStatus(status, NOW.getTime()), status);
73 assert.equal(liveStatus(status, NOW.getTime() + 2 * MIN), null);
74 assert.equal(liveStatus({ ...status, clear_at: null }, NOW.getTime() + 999 * MIN)?.text, "Lunch");
75});
76
77test("when something ends, in a few words", () => {
78 const at = (d: Date) => untilLabel(d.toISOString(), NOW, "en-US");
79 assert.equal(at(new Date(2026, 9, 7, 15, 30)), "until 3:30 PM");
80 assert.equal(at(new Date(2026, 9, 8, 0, 0)), "until the end of today");
81 assert.equal(at(new Date(2026, 9, 8, 9, 0)), "until tomorrow 9:00 AM");
82 assert.equal(at(new Date(2026, 9, 12, 0, 0)), "until the end of Sunday");
83 assert.equal(at(new Date(2026, 9, 10, 9, 0)), "until Sat 9:00 AM");
84 assert.equal(at(new Date(2026, 10, 2, 9, 0)), "until Nov 2");
85 const status = { emoji: "🗓️", text: "In a meeting", clear_at: new Date(2026, 9, 7, 15, 0).toISOString(), source: "manual" as const, set_at: "" };
86 assert.equal(statusWords(status, NOW, "en-US"), "🗓️ In a meeting, until 3:00 PM");
87 assert.equal(statusWords({ ...status, clear_at: null }, NOW, "en-US"), "🗓️ In a meeting");
88});
89
90test("a tab is idle after ten minutes without input", () => {
91 assert.ok(!isIdle(0, IDLE_MS - 1));
92 assert.ok(isIdle(0, IDLE_MS));
93});
94
95test("custom emoji are named; ordinary emoji are not", () => {
96 assert.equal(customEmojiName(":party-parrot:"), "party-parrot");
97 assert.equal(customEmojiName("🎉"), null);
98 assert.equal(customEmojiName(null), null);
99});
100
101test("people merge by id, and a later word wins", () => {
102 const a: PresenceEntry = { user_id: "usr_1", username: "ana", presence: "active", dnd_until: null, status: null, at: 10 };
103 const people = mergePeople({}, [a], true);
104 assert.equal(people.usr_1, a);
105 const older = mergePeople(people, [{ ...a, presence: "away", at: 5 }], false);
106 assert.equal(older, people);
107 const newer = mergePeople(people, [{ ...a, presence: "away", at: 20 }], false);
108 assert.equal(newer.usr_1!.presence, "away");
109});