Files
openclaw/ui/src/app/native-notifications.test.ts
T
Peter Steinberger 42133d0c3c feat(mac): show native notification permission in dashboard Notifications settings (#110646)
* feat(mac): show native notification permission in dashboard Notifications settings

The Mac app's embedded Control UI showed the web-push section as
Unsupported/Not subscribed even though the app delivers notifications
natively. The dashboard now installs an openclawNotifications WebKit
bridge (status / request-permission / send-test) and the Notifications
settings page renders native permission state with Enable, Open System
Settings, and Send test actions when hosted in the Mac app. Browsers
keep the existing web-push UI. The notifications section renderer moved
out of the oversized config view module.

* fix(ui): break config view import cycle and satisfy lint/deadcode/i18n gates

Move the notifications section props to a leaf contract (no view.ts
type import), unexport the status-event constant, use bracket access
for the injected dunder global, default-case the status switch, and
refresh the native i18n inventory line offset.
2026-07-18 14:13:06 +01:00

141 lines
4.0 KiB
TypeScript

/* @vitest-environment jsdom */
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createNativeNotificationsCapability,
type NativeNotificationsCapability,
} from "./native-notifications.ts";
// Wire contract shared with the Mac app bridge; asserted literally on purpose.
const NATIVE_NOTIFICATIONS_STATUS_EVENT = "openclaw:native-notifications-status";
type NativeNotificationsMessage = {
type: "status" | "request-permission" | "send-test";
};
type NativeNotificationsTestWindow = Window & {
__OPENCLAW_NATIVE_NOTIFICATIONS__?: unknown;
};
let capability: NativeNotificationsCapability | null = null;
afterEach(() => {
capability?.dispose();
capability = null;
Reflect.deleteProperty(
window as NativeNotificationsTestWindow,
"__OPENCLAW_NATIVE_NOTIFICATIONS__",
);
vi.unstubAllGlobals();
});
function installBridge() {
const postMessage = vi.fn<(message: NativeNotificationsMessage) => void>();
vi.stubGlobal("webkit", {
messageHandlers: { openclawNotifications: { postMessage } },
});
return postMessage;
}
describe("native notifications", () => {
it("returns null without the WebKit bridge", () => {
expect(createNativeNotificationsCapability()).toBeNull();
});
it("posts status on create", () => {
const postMessage = installBridge();
capability = createNativeNotificationsCapability();
expect(capability?.snapshot).toEqual({ permission: "unknown" });
expect(postMessage).toHaveBeenCalledWith({ type: "status" });
});
it("seeds status from the native snapshot", () => {
installBridge();
(window as NativeNotificationsTestWindow)["__OPENCLAW_NATIVE_NOTIFICATIONS__"] = {
permission: "granted",
};
capability = createNativeNotificationsCapability();
expect(capability?.snapshot).toEqual({ permission: "granted" });
});
it("publishes valid status events", () => {
installBridge();
capability = createNativeNotificationsCapability();
const listener = vi.fn();
capability?.subscribe(listener);
window.dispatchEvent(
new CustomEvent(NATIVE_NOTIFICATIONS_STATUS_EVENT, {
detail: { permission: "denied" },
}),
);
expect(capability?.snapshot).toEqual({ permission: "denied" });
expect(listener).toHaveBeenCalledWith({ permission: "denied" });
});
it("ignores invalid status event details", () => {
installBridge();
capability = createNativeNotificationsCapability();
const listener = vi.fn();
capability?.subscribe(listener);
window.dispatchEvent(
new CustomEvent(NATIVE_NOTIFICATIONS_STATUS_EVENT, {
detail: { permission: "authorized" },
}),
);
expect(capability?.snapshot).toEqual({ permission: "unknown" });
expect(listener).not.toHaveBeenCalled();
});
it("reposts status when the window focuses", () => {
const postMessage = installBridge();
capability = createNativeNotificationsCapability();
postMessage.mockClear();
window.dispatchEvent(new Event("focus"));
expect(postMessage).toHaveBeenCalledWith({ type: "status" });
});
it("posts permission and test actions", () => {
const postMessage = installBridge();
capability = createNativeNotificationsCapability();
postMessage.mockClear();
capability?.requestPermission();
capability?.sendTest();
expect(postMessage.mock.calls).toEqual([
[{ type: "request-permission" }],
[{ type: "send-test" }],
]);
});
it("removes listeners on dispose", () => {
const postMessage = installBridge();
capability = createNativeNotificationsCapability();
const listener = vi.fn();
capability?.subscribe(listener);
capability?.dispose();
capability = null;
postMessage.mockClear();
window.dispatchEvent(new Event("focus"));
window.dispatchEvent(
new CustomEvent(NATIVE_NOTIFICATIONS_STATUS_EVENT, {
detail: { permission: "granted" },
}),
);
expect(postMessage).not.toHaveBeenCalled();
expect(listener).not.toHaveBeenCalled();
});
});