1import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";2import { startMDNSListener } from "./chirp-pair-capacitor";34interface RegisteredListener {5 event: string;6 handler: (data: unknown) => void;7}89function installMockCapacitor(opts: {10 withSyncPlugin?: boolean;11 withPeerDiscoveredEvent?: boolean;12 appActive?: boolean;13} = {}): { listeners: RegisteredListener[]; removeCount: { v: number } } {14 const listeners: RegisteredListener[] = [];15 const removeCount = { v: 0 };16 const syncPlugin = opts.withSyncPlugin === false ? undefined : {17 startPairingServer: vi.fn(),18 stopPairingServer: vi.fn(),19 sendFrame: vi.fn(),20 addListener: opts.withPeerDiscoveredEvent === false21 ? undefined22 : vi.fn((event: string, handler: (data: unknown) => void) => {23 listeners.push({ event, handler });24 return { remove: () => { removeCount.v++; } };25 }),26 };27 const appPlugin = {28 getState: vi.fn(async () => ({ isActive: opts.appActive ?? true })),29 };30 (globalThis as { Capacitor?: unknown }).Capacitor = {31 isNativePlatform: () => true,32 Plugins: { SyncPlugin: syncPlugin, App: appPlugin },33 };34 return { listeners, removeCount };35}3637describe("startMDNSListener", () => {38 let sentMessages: Array<{ type: string; payload?: unknown }>;3940 beforeEach(() => {41 sentMessages = [];42 (globalThis as { chrome?: unknown }).chrome = {43 runtime: {44 sendMessage: vi.fn(async (msg: { type: string; payload?: unknown }) => {45 sentMessages.push(msg);46 }),47 },48 };49 });5051 afterEach(() => {52 delete (globalThis as { Capacitor?: unknown }).Capacitor;53 delete (globalThis as { chrome?: unknown }).chrome;54 });5556 it("is a no-op when the SyncPlugin is unavailable", () => {57 installMockCapacitor({ withSyncPlugin: false });58 const stop = startMDNSListener();59 expect(typeof stop).toBe("function");60 stop();61 });6263 it("registers a peerDiscovered listener when the plugin exposes addListener", () => {64 const { listeners } = installMockCapacitor();65 startMDNSListener();66 expect(listeners.length).toBe(1);67 expect(listeners[0].event).toBe("peerDiscovered");68 });6970 it("fires TRIGGER_SYNC with reason 'mdns' when peerDiscovered arrives in the foreground", async () => {71 const { listeners } = installMockCapacitor({ appActive: true });72 startMDNSListener();73 listeners[0].handler({ deviceId: "pixel-7", ip: "192.168.1.42", port: 8765 });74 75 await new Promise((r) => setTimeout(r, 0));76 expect(sentMessages).toEqual([77 { type: "TRIGGER_SYNC", payload: { reason: "mdns" } },78 ]);79 });8081 it("does NOT fire TRIGGER_SYNC when the app is backgrounded", async () => {82 const { listeners } = installMockCapacitor({ appActive: false });83 startMDNSListener();84 listeners[0].handler({ deviceId: "pixel-7", ip: "192.168.1.42", port: 8765 });85 await new Promise((r) => setTimeout(r, 0));86 expect(sentMessages).toEqual([]);87 });8889 it("ignores malformed events (missing deviceId or ip)", async () => {90 const { listeners } = installMockCapacitor({ appActive: true });91 startMDNSListener();92 listeners[0].handler({ ip: "192.168.1.42" });93 listeners[0].handler({ deviceId: "pixel-7" });94 listeners[0].handler(null);95 await new Promise((r) => setTimeout(r, 0));96 expect(sentMessages).toEqual([]);97 });9899 it("returns a teardown that removes the native listener", () => {100 const { removeCount } = installMockCapacitor();101 const stop = startMDNSListener();102 stop();103 expect(removeCount.v).toBe(1);104 });105});