import { assertEquals } from "@std/assert"; import { withDom } from "@berry/util/dom.ts"; import { announcements, type Notice, notificationsAvailable, notificationsOn, setNotifications, } from "./notifications.ts"; /** What a page's browser shows. */ const shown: FakeNotification[] = []; class FakeNotification { static permission: NotificationPermission = "granted"; static requestPermission = () => Promise.resolve("granted" as const); onclick: (() => void) | null = null; constructor(readonly title: string, readonly options: { body?: string; tag?: string }) { shown.push(this); } close() {} } /** A page whose browser grants notifications, keeping what it shows. */ function withNotifications(test: () => Promise | void) { shown.length = 0; return withDom(() => test(), { globals: { Notification: FakeNotification } }); } function setUp(options: { active?: () => string } = {}) { const heard: Notice[] = []; const update = announcements({ active: options.active ?? (() => ""), name: (tid) => `conversation ${tid}`, speak: (notice) => heard.push(notice), }); return { heard, update }; } function hidePage(hidden: boolean) { Object.defineProperty(document, "visibilityState", { configurable: true, get: () => hidden ? "hidden" : "visible", }); } function focusPage(focused: boolean) { Object.defineProperty(document, "hasFocus", { configurable: true, value: () => focused }); } Deno.test("a conversation that stops answering is announced by name, with its answer", async () => { await withNotifications(async () => { assertEquals(await setNotifications(true), true); const { heard, update } = setUp(); update({ a: { busy: true, unread: false } }); update({ a: { busy: false, unread: true, reply: "the tests pass" } }); assertEquals(heard, [ { tid: "a", title: "conversation a", body: "the tests pass" }, ]); }); }); Deno.test("the conversation in view is not announced while someone is looking at it", async () => { await withNotifications(async () => { hidePage(false); focusPage(true); await setNotifications(true); const { heard, update } = setUp({ active: () => "a" }); update({ a: { busy: true, unread: false } }); update({ a: { busy: false, unread: true, reply: "done" } }); assertEquals(heard, []); // the window is still on screen, but the reader has moved on to another one focusPage(false); update({ a: { busy: true, unread: false } }); update({ a: { busy: false, unread: true, reply: "done" } }); assertEquals(heard.map((notice) => notice.tid), ["a"]); hidePage(true); focusPage(true); update({ a: { busy: true, unread: false } }); update({ a: { busy: false, unread: true, reply: "done" } }); assertEquals(heard.map((notice) => notice.tid), ["a", "a"]); }); }); Deno.test("turns still running, and turns returning control without an answer, are not announced", async () => { await withNotifications(async () => { await setNotifications(true); const { heard, update } = setUp(); update({ a: { busy: true, unread: false } }); update({ a: { busy: true, unread: false } }); // an aborted turn settles with nothing new to read update({ a: { busy: false, unread: false } }); update({ b: { busy: false, unread: true, reply: "already here" } }); assertEquals(heard, []); }); }); Deno.test("nothing is announced until the switch is on", async () => { await withNotifications(() => { const { heard, update } = setUp(); update({ a: { busy: true, unread: false } }); update({ a: { busy: false, unread: true, reply: "done" } }); assertEquals(heard, []); }); }); Deno.test("a page without the API leaves announcements off rather than throwing", async () => { await withDom(async () => { assertEquals(notificationsAvailable(), false); assertEquals(await setNotifications(true), false); assertEquals(notificationsOn(), false); // a flag left on from another page's choice must not reach a missing Notification here localStorage.setItem("berry.notifications", "on"); const update = announcements({ active: () => "", name: (tid) => tid }); update({ a: { busy: true, unread: false } }); update({ a: { busy: false, unread: true, reply: "done" } }); }); }); Deno.test("a clicked notification opens its conversation", async () => { await withNotifications(async () => { await setNotifications(true); const update = announcements({ active: () => "", name: (tid) => tid }); update({ a: { busy: true, unread: false } }); update({ a: { busy: false, unread: true, reply: "done" } }); assertEquals(shown.length, 1); assertEquals(shown[0].options.tag, "a"); shown[0].onclick?.(); assertEquals(location.hash, "#/a"); }); });