Settings: the test notification shows in the tab you're in #51

Merged
jcoffey-dev merged 1 commits from fix/test-notification into main 2026-10-09 23:34:03 +00:00
3 changed files with 52 additions and 4 deletions

No files matched your search

@@ -0,0 +1,46 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { showNotification } from "@/lib/notify/notify";
/**
* The Settings test button runs in a visible, focused tab, which is exactly
* when a new-mail notification is held back. It has to show anyway (#48).
*/
describe("showNotification", () => {
const shown = vi.fn();
beforeEach(() => {
shown.mockClear();
class FakeNotification {
static permission = "granted";
onclick: (() => void) | null = null;
constructor(title: string) {
shown(title);
}
close() {}
}
vi.stubGlobal("Notification", FakeNotification);
vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible");
vi.spyOn(document, "hasFocus").mockReturnValue(true);
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it("holds back new mail in a tab you are looking at", () => {
showNotification("New mail");
expect(shown).not.toHaveBeenCalled();
});
it("shows a forced one in that same tab", () => {
showNotification("ihasmail test", { force: true });
expect(shown).toHaveBeenCalledWith("ihasmail test");
});
it("still shows nothing without permission, forced or not", () => {
(Notification as unknown as { permission: string }).permission = "denied";
showNotification("ihasmail test", { force: true });
expect(shown).not.toHaveBeenCalled();
});
});
+5 -3
View File
@@ -91,10 +91,12 @@ export async function requestNotificationPermission(): Promise<NotificationPermi
* for the same message (`ihasmail-<id>`), so if both ever show it, the second
* replaces the first instead of stacking beside it.
*/
export function showNotification(title: string, opts: NotificationOptions & { onClick?: () => void } = {}): void {
export function showNotification(title: string, opts: NotificationOptions & { onClick?: () => void; force?: boolean } = {}): void {
if (!("Notification" in window) || Notification.permission !== "granted") return;
if (document.visibilityState === "visible" && document.hasFocus()) return;
const { onClick, ...options } = opts;
// New mail in a tab you're looking at needs no notification. A test asked
// for from Settings is always in such a tab, so it says to show anyway (#48).
if (!opts.force && document.visibilityState === "visible" && document.hasFocus()) return;
const { onClick, force: _force, ...options } = opts;
const full = { icon: withBase("/img/icon-192.png"), badge: withBase("/img/favicon-64.png"), ...options };
const viaWorker = navigator.serviceWorker?.controller ? navigator.serviceWorker.ready : null;
if (viaWorker) {
@@ -81,7 +81,7 @@ export function NotificationsSettings() {
/>
<Switch locked={isEnforced("notificationSound")} checked={s.notificationSound} onChange={(v) => update({ notificationSound: v })} label={t("Play a sound for new mail")} />
<div className="row mt-16">
<button className="btn" onClick={() => { showNotification(t("{app} test", { app: appName }), { body: t("This is what a new-mail notification looks like.") }); playNewMailSound(); }}>{t("Test notification")}</button>
<button className="btn" onClick={() => { showNotification(t("{app} test", { app: appName }), { body: t("This is what a new-mail notification looks like."), force: true }); playNewMailSound(); }}>{t("Test notification")}</button>
</div>
<p className="hint mt-8">{t("The tab title and favicon always show your unread Inbox count.")}</p>
</div>