diff --git a/desktop/src/features/notifications/lib/desktop.test.mjs b/desktop/src/features/notifications/lib/desktop.test.mjs index 7fb8830683..43ba6d5014 100644 --- a/desktop/src/features/notifications/lib/desktop.test.mjs +++ b/desktop/src/features/notifications/lib/desktop.test.mjs @@ -23,7 +23,55 @@ class ThrowingNotification { globalThis.window = { Notification: ThrowingNotification }; -const { sendDesktopNotification } = await import("./desktop.ts"); +const { sendDesktopNotification, getDesktopNotificationPermissionState } = + await import("./desktop.ts"); + +// Stands in for the plugin's injected shim: `permission` is a cached value and +// only `requestPermission()` reaches the backend and rewrites it. +function shimNotification(initialPermission, grantedPermission = "granted") { + class ShimNotification { + static permission = initialPermission; + static requestCount = 0; + + static async requestPermission() { + ShimNotification.requestCount += 1; + ShimNotification.permission = grantedPermission; + return grantedPermission; + } + + close() {} + } + + return ShimNotification; +} + +async function withEnvironment({ platform, isTauri, notification }, callback) { + const originalNavigator = Object.getOwnPropertyDescriptor( + globalThis, + "navigator", + ); + const originalNotification = window.Notification; + const originalIsTauri = globalThis.isTauri; + + Object.defineProperty(globalThis, "navigator", { + configurable: true, + value: { platform, userAgent: "" }, + }); + window.Notification = notification; + globalThis.isTauri = isTauri; + + try { + return await callback(); + } finally { + if (originalNavigator) { + Object.defineProperty(globalThis, "navigator", originalNavigator); + } else { + delete globalThis.navigator; + } + window.Notification = originalNotification; + globalThis.isTauri = originalIsTauri; + } +} test("constructor failure is a delivery miss and does not prevent a later notification", async (t) => { const warnings = []; @@ -50,3 +98,72 @@ test("constructor failure is a delivery miss and does not prevent a later notifi }, ]); }); + +test("Windows Tauri repairs the shim's false denied state on read", async () => { + const Notification = shimNotification("denied"); + + const permission = await withEnvironment( + { platform: "Win32", isTauri: true, notification: Notification }, + () => getDesktopNotificationPermissionState(), + ); + + assert.equal(permission, "granted"); + assert.equal(Notification.requestCount, 1); +}); + +test("the repaired state is cached, so later reads do not request again", async () => { + const Notification = shimNotification("denied"); + + await withEnvironment( + { platform: "Win32", isTauri: true, notification: Notification }, + async () => { + await getDesktopNotificationPermissionState(); + const second = await getDesktopNotificationPermissionState(); + assert.equal(second, "granted"); + }, + ); + + assert.equal(Notification.requestCount, 1); +}); + +test("a denial that survives the request is reported as denied", async () => { + const Notification = shimNotification("denied", "denied"); + + const permission = await withEnvironment( + { platform: "Win32", isTauri: true, notification: Notification }, + () => getDesktopNotificationPermissionState(), + ); + + assert.equal(permission, "denied"); + assert.equal(Notification.requestCount, 1); +}); + +test("denied stays terminal outside the Windows Tauri app", async () => { + for (const environment of [ + { label: "Windows web", platform: "Win32", isTauri: false }, + { label: "Linux Tauri", platform: "Linux x86_64", isTauri: true }, + { label: "Linux web", platform: "Linux x86_64", isTauri: false }, + ]) { + const Notification = shimNotification("denied"); + + const permission = await withEnvironment( + { ...environment, notification: Notification }, + () => getDesktopNotificationPermissionState(), + ); + + assert.equal(permission, "denied", environment.label); + assert.equal(Notification.requestCount, 0, environment.label); + } +}); + +test("granted is returned untouched and never triggers a request", async () => { + const Notification = shimNotification("granted"); + + const permission = await withEnvironment( + { platform: "Win32", isTauri: true, notification: Notification }, + () => getDesktopNotificationPermissionState(), + ); + + assert.equal(permission, "granted"); + assert.equal(Notification.requestCount, 0); +}); diff --git a/desktop/src/features/notifications/lib/desktop.ts b/desktop/src/features/notifications/lib/desktop.ts index 0844c24de5..c0ee61c6b3 100644 --- a/desktop/src/features/notifications/lib/desktop.ts +++ b/desktop/src/features/notifications/lib/desktop.ts @@ -6,7 +6,11 @@ import { onAction, requestPermission, } from "@tauri-apps/plugin-notification"; -import { isLinuxPlatform, isMacPlatform } from "@/shared/lib/platform"; +import { + isLinuxPlatform, + isMacPlatform, + isWindowsPlatform, +} from "@/shared/lib/platform"; // Backend event emitted when a native Linux notification is clicked or a // queued macOS activation becomes available. See src-tauri notification code. @@ -147,6 +151,25 @@ export async function getDesktopNotificationPermissionState(): Promise { + for (const platform of ["Win32", "Win64", "Windows"]) { + withNavigator({ platform, userAgent: "" }, () => { + assert.equal(isWindowsPlatform(), true, platform); + }); + } +}); + +test("Windows detection rejects the other desktop platforms", () => { + for (const platform of ["MacIntel", "Darwin", "Linux x86_64"]) { + withNavigator({ platform, userAgent: "" }, () => { + assert.equal(isWindowsPlatform(), false, platform); + }); + } +}); diff --git a/desktop/src/shared/lib/platform.ts b/desktop/src/shared/lib/platform.ts index 42e7f5b944..b8c2c3ecfc 100644 --- a/desktop/src/shared/lib/platform.ts +++ b/desktop/src/shared/lib/platform.ts @@ -23,6 +23,15 @@ export function isLinuxPlatform(): boolean { ); } +/** Returns true on Windows desktops. */ +export function isWindowsPlatform(): boolean { + if (typeof navigator === "undefined") { + return false; + } + + return /^win/i.test(navigator.platform); +} + /** * The platform's normal application-shortcut modifier: * - macOS: Command (Meta)