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 {78 type: "TRIGGER_SYNC",79 payload: { reason: "mdns", deviceId: "pixel-7", discoveredIp: "192.168.1.42", discoveredPort: 8765 },80 },81 ]);82 });8384 it("does NOT fire TRIGGER_SYNC when the app is backgrounded", async () => {85 const { listeners } = installMockCapacitor({ appActive: false });86 startMDNSListener();87 listeners[0].handler({ deviceId: "pixel-7", ip: "192.168.1.42", port: 8765 });88 await new Promise((r) => setTimeout(r, 0));89 expect(sentMessages).toEqual([]);90 });9192 it("ignores malformed events (missing deviceId or ip)", async () => {93 const { listeners } = installMockCapacitor({ appActive: true });94 startMDNSListener();95 listeners[0].handler({ ip: "192.168.1.42" });96 listeners[0].handler({ deviceId: "pixel-7" });97 listeners[0].handler(null);98 await new Promise((r) => setTimeout(r, 0));99 expect(sentMessages).toEqual([]);100 });101102 it("returns a teardown that removes the native listener", () => {103 const { removeCount } = installMockCapacitor();104 const stop = startMDNSListener();105 stop();106 expect(removeCount.v).toBe(1);107 });108});