PairDevice.test.tsx

4.4 KB · raw

1// @vitest-environment jsdom2//3// PairDeviceView is relay-only: the extension always DISPLAYS an invitation for4// another device to scan. LAN pairing codes went with LAN sync5// (docs/sync-lan-retirement.md), so there is no code-entry mode to test.6//7// chrome messaging is mocked the way the popup uses it:8//   - shared/bridge `sendMessage` is mocked (popup → bg)9//   - chrome.runtime.onMessage.{add,remove}Listener is faked so the test can10//     fire ADOPTION_SOURCE_* pushes (bg → popup)1112import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";1314// --- mock the background bridge (popup → bg) -------------------------------15const { 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";2324// --- fake chrome.runtime push channel (bg → popup) ------------------------25type 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    // The invitation is carried by the QR alone — the manual textarea and its86    // Copy button duplicated the pairing-code path and were removed.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});