Skip to content
130 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { decide } from "./prefs.ts";
5import {
6 MAX_STATUS_TEXT,
7 NOTHING_KEPT,
8 applyChange,
9 cleanStatus,
10 cleanWorkspaces,
11 current,
12 dndOn,
13 entryOf,
14 liveEntry,
15 nextExpiry,
16 onlyPeople,
17 presenceOf,
18 readKept,
19 sameEntry,
20} from "./presence.ts";
21
22const NOW = 1_800_000_000_000;
23const iso = (ms: number) => new Date(ms).toISOString();
24const MIN = 60_000;
25
26test("presence comes from the tabs: none is offline, all idle is away, one in use is active", () => {
27 assert.equal(presenceOf([], false), "offline");
28 assert.equal(presenceOf([], true), "offline");
29 assert.equal(presenceOf([{ idle: true }, { idle: true }], false), "away");
30 assert.equal(presenceOf([{ idle: true }, { idle: false }], false), "active");
31 // Away by hand holds while tabs are in use.
32 assert.equal(presenceOf([{ idle: false }], true), "away");
33});
34
35test("a status is an emoji and a few words, trimmed, and clears at a time in the future", () => {
36 const status = cleanStatus({ emoji: " 🗓️ ", text: " In a meeting ", clear_at: iso(NOW + 30 * MIN) }, NOW)!;
37 assert.equal(status.emoji, "🗓️");
38 assert.equal(status.text, "In a meeting");
39 assert.equal(status.clear_at, iso(NOW + 30 * MIN));
40 assert.equal(status.source, "manual");
41 assert.equal(status.set_at, iso(NOW));
42 assert.equal(cleanStatus({ text: "x".repeat(300) }, NOW)!.text.length, MAX_STATUS_TEXT);
43 // Nothing said, or a time already past, is no status.
44 assert.equal(cleanStatus({ emoji: "", text: " " }, NOW), null);
45 assert.equal(cleanStatus({ text: "Lunch", clear_at: iso(NOW - MIN) }, NOW), null);
46 assert.equal(cleanStatus({ text: "Lunch", clear_at: "soon" }, NOW), null);
47 // Without a time it is kept until changed.
48 assert.equal(cleanStatus({ text: "On vacation" }, NOW)!.clear_at, null);
49 // An emoji alone is enough.
50 assert.equal(cleanStatus({ emoji: "🌴", text: "" }, NOW)!.text, "");
51});
52
53test("a status and Do Not Disturb are gone once their time passes", () => {
54 const kept = applyChange(NOTHING_KEPT, { status: { emoji: "🍔", text: "Lunch", clear_at: iso(NOW + 30 * MIN) }, dnd_until: iso(NOW + 60 * MIN) }, NOW);
55 assert.ok(kept.status);
56 assert.ok(dndOn(kept.dnd_until, NOW));
57 assert.equal(nextExpiry(kept, NOW), NOW + 30 * MIN);
58 const later = current(kept, NOW + 45 * MIN);
59 assert.equal(later.status, null);
60 assert.equal(later.dnd_until, kept.dnd_until);
61 assert.equal(nextExpiry(later, NOW + 45 * MIN), NOW + 60 * MIN);
62 const after = current(kept, NOW + 61 * MIN);
63 assert.equal(after.dnd_until, null);
64 assert.equal(nextExpiry(after, NOW + 61 * MIN), null);
65});
66
67test("Do Not Disturb resumes with null, and refuses a time in the past", () => {
68 const on = applyChange(NOTHING_KEPT, { dnd_until: iso(NOW + MIN) }, NOW);
69 assert.ok(dndOn(on.dnd_until, NOW));
70 assert.equal(applyChange(on, { dnd_until: null }, NOW).dnd_until, null);
71 assert.equal(applyChange(NOTHING_KEPT, { dnd_until: iso(NOW - MIN) }, NOW).dnd_until, null);
72 // A change that does not mention it leaves it.
73 assert.equal(applyChange(on, { away: true }, NOW).dnd_until, on.dnd_until);
74});
75
76test("a calendar never replaces a status set by hand, but replaces its own", () => {
77 const byHand = applyChange(NOTHING_KEPT, { status: { emoji: "🤒", text: "Out sick" } }, NOW);
78 const meeting = applyChange(byHand, { status: { emoji: "🗓️", text: "In a meeting", source: "calendar", clear_at: iso(NOW + 30 * MIN) } }, NOW);
79 assert.equal(meeting.status?.text, "Out sick");
80 // Nor clears it.
81 assert.equal(applyChange(byHand, { status: null }, NOW).status, null);
82 assert.equal(applyChange(byHand, { status: { text: "", source: "calendar" } }, NOW).status?.text, "Out sick");
83
84 const fromCalendar = applyChange(NOTHING_KEPT, { status: { emoji: "🗓️", text: "In a meeting", source: "calendar", clear_at: iso(NOW + 30 * MIN) } }, NOW);
85 assert.equal(fromCalendar.status?.source, "calendar");
86 const next = applyChange(fromCalendar, { status: { text: "Focusing", source: "integration" } }, NOW);
87 assert.equal(next.status?.text, "Focusing");
88 // The person always can.
89 assert.equal(applyChange(fromCalendar, { status: { text: "Commuting" } }, NOW).status?.source, "manual");
90});
91
92test("Do Not Disturb silences toasts and pushes; a test still shows", () => {
93 const prefs = { level: "all" as const, workspaces: {} };
94 const notification = { kind: "dm" as const, workspace: "acme" };
95 assert.deepEqual(decide({ prefs, notification, tabs: [], subscriptions: 2, now: NOW, dnd: true }), { toast: false, push: false });
96 assert.deepEqual(decide({ prefs, notification, tabs: [], subscriptions: 2, now: NOW, dnd: true, test: true }), { toast: true, push: true });
97 assert.deepEqual(decide({ prefs, notification, tabs: [], subscriptions: 2, now: NOW }), { toast: true, push: true });
98});
99
100test("entries say how a person shows, and compare without their time", () => {
101 const kept = applyChange(NOTHING_KEPT, { status: { emoji: "🎯", text: "Focusing", clear_at: iso(NOW + MIN) } }, NOW);
102 const a = entryOf({ user_id: "usr_1", username: "ana" }, "active", kept, NOW);
103 const b = entryOf({ user_id: "usr_1", username: "ana" }, "active", kept, NOW + 5);
104 assert.ok(sameEntry(a, b));
105 assert.ok(!sameEntry(a, { ...b, presence: "away" }));
106 assert.ok(!sameEntry(a, null));
107 // Read after its status ran out, a kept entry shows without it.
108 assert.equal(liveEntry(a, NOW + 2 * MIN).status, null);
109 assert.equal(liveEntry(a, NOW), a);
110});
111
112test("what is kept reads back, and anything else reads as nothing", () => {
113 const kept = applyChange(NOTHING_KEPT, { away: true, status: { text: "Commuting" } }, NOW);
114 assert.deepEqual(readKept(JSON.stringify(kept)), kept);
115 assert.deepEqual(readKept("not json"), NOTHING_KEPT);
116 assert.deepEqual(readKept(null), NOTHING_KEPT);
117});
118
119test("a workspace's presence narrows to the people asked about", () => {
120 const entry = (user_id: string) => entryOf({ user_id, username: user_id }, "active", NOTHING_KEPT, NOW);
121 const everyone = [entry("usr_a"), entry("usr_b"), entry("usr_c")];
122 assert.deepEqual(onlyPeople(everyone, ["usr_c", "usr_a", 7]).map((e) => e.user_id), ["usr_a", "usr_c"]);
123 assert.equal(onlyPeople(everyone, null).length, 3);
124 assert.deepEqual(onlyPeople(everyone, []), []);
125});
126
127test("workspaces are slugs, lowercased and distinct", () => {
128 assert.deepEqual(cleanWorkspaces(["Acme", "acme", " web-team ", "-bad", 3, "a".repeat(60)]), ["acme", "web-team"]);
129 assert.deepEqual(cleanWorkspaces("acme"), []);
130});