chrome-shim.test.ts

5.6 KB · raw

1import { beforeEach, describe, expect, it, vi } from "vitest";23// Fresh shim install per test — the module installs `globalThis.chrome` as a4// side effect at import time, and internal listener/queue state lives in5// module-level closures, so each test needs its own module instance.6async 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    // Mirrors setupMessageHandler()'s exact shape: register a real listener,44    // then also call the (now-inert) mobile hook a second time.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      // Never calls sendResponse and never returns true — matches App.tsx's72      // handleRuntimeMessage exactly.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      // No sendResponse call, no `return true`.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    // freshShim() clears globalThis.Capacitor before importing the shim (so113    // module import order is deterministic across tests) — set it after114    // import, which is fine since nativePlugin() is read fresh on every call.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});