diff --git a/tests/frontend/NotificationUtils.test.js b/tests/frontend/NotificationUtils.test.js new file mode 100644 index 0000000..1da598f --- /dev/null +++ b/tests/frontend/NotificationUtils.test.js @@ -0,0 +1,127 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import NotificationUtils from "../../meshchatx/src/frontend/js/NotificationUtils"; + +describe("NotificationUtils", () => { + let originalNotification; + let electronMock; + let androidMock; + + beforeEach(() => { + originalNotification = globalThis.Notification; + electronMock = { showNotification: vi.fn() }; + androidMock = { + getPlatform: vi.fn().mockReturnValue("android"), + showNotification: vi.fn(), + showIncomingCallNotification: vi.fn(), + showMissedCallNotification: vi.fn(), + cancelIncomingCallNotification: vi.fn(), + }; + globalThis.Notification = vi.fn(function (title, opts) { + return { title, opts }; + }); + globalThis.Notification.requestPermission = vi.fn().mockResolvedValue("granted"); + }); + + afterEach(() => { + globalThis.Notification = originalNotification; + delete globalThis.electron; + delete globalThis.MeshChatXAndroid; + vi.restoreAllMocks(); + }); + + describe("Electron", () => { + beforeEach(() => { + globalThis.electron = electronMock; + }); + + it("showNewMessageNotification delegates to electron", () => { + NotificationUtils.showNewMessageNotification("Alice", "hello"); + expect(electronMock.showNotification).toHaveBeenCalledWith("New Message", "Alice: hello"); + }); + + it("showIncomingCallNotification delegates to electron", () => { + NotificationUtils.showIncomingCallNotification("Bob"); + expect(electronMock.showNotification).toHaveBeenCalledWith("Incoming Call", "Bob is calling you."); + }); + + it("showMissedCallNotification delegates to electron", () => { + NotificationUtils.showMissedCallNotification("Charlie"); + expect(electronMock.showNotification).toHaveBeenCalledWith( + "Missed Call", + "You missed a call from Charlie." + ); + }); + + it("showNewVoicemailNotification delegates to electron", () => { + NotificationUtils.showNewVoicemailNotification("Dave"); + expect(electronMock.showNotification).toHaveBeenCalledWith( + "New Voicemail", + "You have a new voicemail from Dave." + ); + }); + }); + + describe("Android", () => { + beforeEach(() => { + globalThis.MeshChatXAndroid = androidMock; + }); + + it("showNewMessageNotification delegates to Android bridge", () => { + NotificationUtils.showNewMessageNotification("Alice", "hello"); + expect(androidMock.showNotification).toHaveBeenCalledWith("New Message", "Alice: hello"); + }); + + it("showIncomingCallNotification delegates to Android bridge", () => { + NotificationUtils.showIncomingCallNotification("Bob"); + expect(androidMock.showIncomingCallNotification).toHaveBeenCalledWith("Bob"); + }); + + it("showMissedCallNotification delegates to Android bridge", () => { + NotificationUtils.showMissedCallNotification("Charlie"); + expect(androidMock.showMissedCallNotification).toHaveBeenCalledWith( + "Missed Call", + "You missed a call from Charlie." + ); + }); + + it("showNewVoicemailNotification delegates to Android bridge", () => { + NotificationUtils.showNewVoicemailNotification("Dave"); + expect(androidMock.showNotification).toHaveBeenCalledWith( + "New Voicemail", + "You have a new voicemail from Dave." + ); + }); + + it("cancelIncomingCallNotification delegates to Android bridge", () => { + NotificationUtils.cancelIncomingCallNotification(); + expect(androidMock.cancelIncomingCallNotification).toHaveBeenCalled(); + }); + }); + + describe("Browser fallback", () => { + it("showNewMessageNotification uses browser Notification API", async () => { + NotificationUtils.showNewMessageNotification("Alice", "hello"); + await new Promise((r) => setTimeout(r, 10)); + expect(globalThis.Notification).toHaveBeenCalledWith( + "New Message", + expect.objectContaining({ body: "Alice: hello" }) + ); + }); + }); + + describe("_isAndroid detection", () => { + it("returns false when MeshChatXAndroid is missing", () => { + expect(NotificationUtils._isAndroid()).toBe(false); + }); + + it("returns false when getPlatform returns non-android", () => { + globalThis.MeshChatXAndroid = { getPlatform: () => "ios" }; + expect(NotificationUtils._isAndroid()).toBe(false); + }); + + it("returns true when getPlatform returns android", () => { + globalThis.MeshChatXAndroid = androidMock; + expect(NotificationUtils._isAndroid()).toBe(true); + }); + }); +}); diff --git a/tests/frontend/Toast.test.js b/tests/frontend/Toast.test.js index 95c2bac..ddc3a4d 100644 --- a/tests/frontend/Toast.test.js +++ b/tests/frontend/Toast.test.js @@ -99,4 +99,12 @@ describe("Toast.vue", () => { const toast = wrapper.find(".pointer-events-auto"); expect(toast.find("button").exists()).toBe(true); }); + + it("positions container with mobile-safe bottom offset", () => { + const container = wrapper.find("[class*='fixed']"); + expect(container.exists()).toBe(true); + const cls = container.classes().join(" "); + expect(cls).toContain("max-sm:bottom-"); + expect(cls).not.toContain("max-sm:bottom-[calc(5.75rem"); + }); });