123456789101112import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";131415const { mockSendMessage } = vi.hoisted(() => ({ mockSendMessage: vi.fn() }));16vi.mock("../../shared/bridge", () => ({ sendMessage: mockSendMessage }));1718const SOURCE_INVITATION =19 "peach-pair-v1:eyJyZWxheSI6IndzczovL3JlbGF5LnBlYWNocGFzc3dvcmRzLmNvbSIsInRva2VuIjoiQUFFQ0F3UUZCZ2NJQ1FvTERBME9EdyJ9";2021import { PairDeviceView } from "./PairDevice";22import { renderView, inputText } from "./view-test-utils";232425type RuntimeListener = (msg: { type?: string; payload?: unknown }) => void;26let listeners: RuntimeListener[] = [];2728function pushFromBackground(type: string, payload: unknown): void {29 for (const l of [...listeners]) l({ type, payload });30}3132beforeEach(() => {33 listeners = [];34 mockSendMessage.mockReset();35 mockSendMessage.mockImplementation((msg: { type?: string }) => {36 if (msg?.type === "START_ADOPTION_SOURCE") {37 return Promise.resolve({38 ok: true,39 data: { invitation: SOURCE_INVITATION },40 });41 }42 if (msg?.type === "ADOPTION_SOURCE_CONFIRM") {43 return Promise.resolve({ ok: true, data: { valid: true } });44 }45 return Promise.resolve({ ok: true });46 });47 (globalThis as unknown as { chrome: unknown }).chrome = {48 runtime: {49 onMessage: {50 addListener: (fn: RuntimeListener) => { listeners.push(fn); },51 removeListener: (fn: RuntimeListener) => { listeners = listeners.filter((l) => l !== fn); },52 },53 },54 };55});5657describe("PairDevice — relay adoption source", () => {58 const cleanups: Array<() => void> = [];59 afterEach(() => {60 while (cleanups.length) cleanups.pop()!();61 });6263 function mount() {64 const rendered = renderView(65 <PairDeviceView66 isPro={true}67 onBack={() => {}}68 onLearnPro={() => {}}69 />,70 );71 cleanups.push(rendered.dispose);72 return rendered;73 }7475 it("opens the relay room before rendering its QR, and offers no manual copy", async () => {76 const { container } = mount();77 await vi.waitFor(() => {78 expect(container.textContent).toContain("Scan this in Peach");79 });8081 expect(mockSendMessage).toHaveBeenCalledWith({82 type: "START_ADOPTION_SOURCE",83 });84 expect(container.querySelector("svg.peach-pair-qr")).toBeTruthy();85 86 87 expect(container.querySelector("#pairing-invitation-manual")).toBeNull();88 expect(container.textContent).not.toContain("or copy the invitation");89 expect(container.textContent).toContain("single-use token");90 });9192 it("requires the matching SAS and master password before sharing", async () => {93 const { container } = mount();94 await vi.waitFor(() => {95 expect(container.textContent).toContain("Scan this in Peach");96 });9798 pushFromBackground("ADOPTION_SOURCE_SAS", {99 sas: "48219371",100 receiverDeviceName: "June's phone",101 });102 expect(container.textContent).toContain("June's phone");103 expect(container.textContent).toContain("48219371");104105 inputText(106 container.querySelector("#adoption-consent-password") as HTMLInputElement,107 "correct horse battery staple",108 );109 const approve = Array.from(container.querySelectorAll("button")).find(110 (button) => button.textContent?.includes("Share vault"),111 ) as HTMLButtonElement;112 approve.click();113 await vi.waitFor(() => {114 expect(mockSendMessage).toHaveBeenCalledWith({115 type: "ADOPTION_SOURCE_CONFIRM",116 payload: { password: "correct horse battery staple" },117 });118 });119 await vi.waitFor(() => {120 expect(container.textContent).toContain("Waiting for the new device");121 });122 });123});