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