1import { beforeEach, describe, expect, it, vi } from "vitest";23456async function freshShim() {7 vi.resetModules();8 delete (globalThis as Record<string, unknown>).Capacitor;9 delete (globalThis as Record<string, unknown>).chrome;10 await import("./chrome-shim");11 return (globalThis as unknown as { chrome: typeof chrome }).chrome;12}1314describe("chrome-shim runtime.sendMessage dispatch", () => {15 beforeEach(() => {16 vi.resetModules();17 });1819 it("queues sendMessage calls until a listener registers, per messages.ts's own comment", async () => {20 const chromeShim = await freshShim();21 const sent = chromeShim.runtime.sendMessage({ type: "GET_VAULT_STATUS" });2223 let resolved = false;24 void sent.then(() => {25 resolved = true;26 });27 await Promise.resolve();28 expect(resolved).toBe(false);2930 chromeShim.runtime.onMessage.addListener((_msg, _sender, sendResponse) => {31 sendResponse({ ok: true, unlocked: false });32 return true;33 });3435 const result = await sent;36 expect(result).toEqual({ ok: true, unlocked: false });37 });3839 it("invokes handleMessage exactly once even though messages.ts registers via both onMessage.addListener and __peachRegisterMessageHandler", async () => {40 const chromeShim = await freshShim();41 let handleMessageCalls = 0;4243 44 45 chromeShim.runtime.onMessage.addListener((msg, _sender, sendResponse) => {46 handleMessageCalls++;47 Promise.resolve({ ok: true, echoed: msg }).then(sendResponse);48 return true;49 });50 (globalThis as unknown as { __peachRegisterMessageHandler: (fn: unknown) => void })51 .__peachRegisterMessageHandler(async (m: unknown) => {52 handleMessageCalls++;53 return { ok: true, echoed: m };54 });5556 const result = await chromeShim.runtime.sendMessage({ type: "GET_VAULT_STATUS" });57 expect(result).toEqual({ ok: true, echoed: { type: "GET_VAULT_STATUS" } });58 expect(handleMessageCalls).toBe(1);59 });6061 it("delivers every message to broadcast-only listeners (App.tsx's VAULT_LOCKED pattern) without them winning the response", async () => {62 const chromeShim = await freshShim();63 const seenByBroadcast: unknown[] = [];6465 chromeShim.runtime.onMessage.addListener((_msg, _sender, sendResponse) => {66 Promise.resolve({ ok: true, from: "primary" }).then(sendResponse);67 return true;68 });69 chromeShim.runtime.onMessage.addListener((msg) => {70 seenByBroadcast.push(msg);71 72 73 });7475 const result = await chromeShim.runtime.sendMessage({ type: "VAULT_LOCKED" });76 expect(result).toEqual({ ok: true, from: "primary" });77 expect(seenByBroadcast).toEqual([{ type: "VAULT_LOCKED" }]);78 });7980 it("resolves undefined immediately when no listener intends to respond (matches Chrome's own semantics)", async () => {81 const chromeShim = await freshShim();82 chromeShim.runtime.onMessage.addListener(() => {83 84 });85 const result = await chromeShim.runtime.sendMessage({ type: "UNKNOWN" });86 expect(result).toBeUndefined();87 });88});8990describe("chrome-shim storage", () => {91 it("storage.session round-trips in-memory and fires onChanged", async () => {92 const chromeShim = await freshShim();93 const changes: unknown[] = [];94 chromeShim.storage.onChanged.addListener((c, area) => changes.push({ c, area }));9596 await chromeShim.storage.session.set({ foo: "bar" });97 const got = await chromeShim.storage.session.get("foo");98 expect(got).toEqual({ foo: "bar" });99 expect(changes).toEqual([{ c: { foo: { oldValue: undefined, newValue: "bar" } }, area: "session" }]);100 });101102 it("storage.local falls back to an in-memory store when PeachNative is unavailable", async () => {103 const chromeShim = await freshShim();104 await chromeShim.storage.local.set({ peach_settings: { autoLockMinutes: 5 } });105 const got = await chromeShim.storage.local.get("peach_settings");106 expect(got).toEqual({ peach_settings: { autoLockMinutes: 5 } });107 });108109 it("storage.local routes through PeachNative.kv* when available, JSON-encoding at the boundary", async () => {110 const chromeShim = await freshShim();111 const kv = new Map<string, string>();112 113 114 115 (globalThis as unknown as { Capacitor: unknown }).Capacitor = {116 isNativePlatform: () => true,117 Plugins: {118 PeachNative: {119 async kvGet(key: string) {120 return kv.has(key) ? kv.get(key)! : null;121 },122 async kvSet(key: string, value: string) {123 kv.set(key, value);124 },125 async kvKeys() {126 return Array.from(kv.keys());127 },128 },129 },130 };131 await chromeShim.storage.local.set({ peach_vault_a: { ci: "abc", nonce: "def" } });132 expect(kv.get("peach_vault_a")).toBe(JSON.stringify({ ci: "abc", nonce: "def" }));133134 const got = await chromeShim.storage.local.get("peach_vault_a");135 expect(got).toEqual({ peach_vault_a: { ci: "abc", nonce: "def" } });136 });137});