SyncModeSelector.test.tsx

5.5 KB · raw

1// @vitest-environment jsdom2import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";3import { DEFAULT_RELAY_URL } from "../../shared/rendezvous";4import { flushView, inputText, renderView } from "../views/view-test-utils";56const sendMessage = vi.hoisted(() => vi.fn());7vi.mock("../../shared/bridge", () => ({ sendMessage }));89import { RELAY_DISCLOSURE, SyncModeSelector } from "./SyncModeSelector";1011let settings = { mode: "public" as "public" | "private" | "off", relayUrl: DEFAULT_RELAY_URL };12let disposeView: (() => void) | null = null;1314beforeEach(() => {15  settings = { mode: "public", relayUrl: DEFAULT_RELAY_URL };16  sendMessage.mockReset();17  sendMessage.mockImplementation(async (msg: { type: string; payload?: Record<string, unknown> }) => {18    if (msg.type === "GET_SYNC_SETTINGS") return { ok: true, data: settings };19    if (msg.type === "SET_SYNC_SETTINGS") {20      const mode = msg.payload?.mode as typeof settings.mode | undefined;21      if (mode) {22        settings = { mode, relayUrl: mode === "public" ? DEFAULT_RELAY_URL : settings.relayUrl };23      } else {24        const url = String(msg.payload?.relayUrl ?? "");25        if (!url.startsWith("wss://")) return { ok: false, error: "invalid_relay_url" };26        settings = { mode: url === DEFAULT_RELAY_URL ? "public" : "private", relayUrl: url };27      }28      return { ok: true, data: settings };29    }30    return { ok: false };31  });32});3334afterEach(() => {35  disposeView?.();36  disposeView = null;37});3839async function mount(compact = false) {40  const view = renderView(() => <SyncModeSelector compact={compact} />);41  disposeView = view.dispose;42  await flushView();43  return view.container;44}4546function optionNamed(container: HTMLElement, title: string): HTMLButtonElement {47  const match = [...container.querySelectorAll<HTMLButtonElement>("button.peach-sync-mode")].find(48    (b) => b.querySelector(".peach-sync-mode-title")?.textContent === title,49  );50  if (!match) throw new Error(`no option titled ${title}`);51  return match;52}5354describe("sync mode selector", () => {55  it("offers exactly the three named choices as a radio group", async () => {56    const container = await mount();57    const titles = [...container.querySelectorAll(".peach-sync-mode-title")].map((n) => n.textContent);58    expect(titles).toEqual(["Public relay", "Private relay", "No sync"]);59    expect(container.querySelector("[role='radiogroup']")).not.toBeNull();60    expect(optionNamed(container, "Public relay").getAttribute("aria-checked")).toBe("true");61  });6263  it("shows the disclosure once, whichever state is active", async () => {64    const container = await mount();65    expect(container.querySelectorAll(".peach-sync-mode-disclosure")).toHaveLength(1);66    expect(container.querySelector(".peach-sync-mode-disclosure")?.textContent).toBe(RELAY_DISCLOSURE);67    optionNamed(container, "No sync").click();68    await flushView();69    expect(container.querySelector(".peach-sync-mode-disclosure")?.textContent).toBe(RELAY_DISCLOSURE);70  });7172  it("shows no URL field under public or off", async () => {73    const container = await mount();74    expect(container.querySelector("#sync-relay-url")).toBeNull();75    optionNamed(container, "No sync").click();76    await flushView();77    expect(container.querySelector("#sync-relay-url")).toBeNull();78  });7980  it("flags private with no URL instead of silently using the public relay", async () => {81    const container = await mount();82    optionNamed(container, "Private relay").click();83    await flushView();84    const field = container.querySelector<HTMLInputElement>("#sync-relay-url");85    expect(field).not.toBeNull();86    // Only the required secure scheme is pre-filled, never Peach's public host.87    expect(field!.value).toBe("wss://");88    const warn = container.querySelector(".peach-sync-mode-warn");89    expect(warn?.textContent).toContain("Sync is paused until you enter your relay URL");90  });9192  it("clears the warning once a real relay is saved", async () => {93    const container = await mount();94    optionNamed(container, "Private relay").click();95    await flushView();96    inputText(container.querySelector<HTMLInputElement>("#sync-relay-url")!, "wss://relay.example.com");97    [...container.querySelectorAll<HTMLButtonElement>("button")].find((b) => b.textContent === "save")!.click();98    await flushView();99    expect(container.querySelector(".peach-sync-mode-warn")).toBeNull();100    expect(settings).toEqual({ mode: "private", relayUrl: "wss://relay.example.com" });101  });102});103104describe("compact form, used mid-pairing", () => {105  it("is a one-line summary until asked to change", async () => {106    const container = await mount(true);107    expect(container.querySelector(".peach-sync-mode-summary")?.textContent).toContain(108      "Using Public relay",109    );110    expect(container.querySelector(".peach-sync-mode")).toBeNull();111    container.querySelector<HTMLButtonElement>(".peach-sync-mode-change")!.click();112    await flushView();113    expect(container.querySelectorAll(".peach-sync-mode")).toHaveLength(3);114  });115116  it("reports a committed change so the host can restart pairing", async () => {117    const onChange = vi.fn();118    const view = renderView(() => <SyncModeSelector compact onChange={onChange} />);119    disposeView = view.dispose;120    await flushView();121    view.container.querySelector<HTMLButtonElement>(".peach-sync-mode-change")!.click();122    await flushView();123    optionNamed(view.container, "No sync").click();124    await flushView();125    expect(onChange).toHaveBeenCalledWith({ mode: "off", relayUrl: DEFAULT_RELAY_URL });126  });127});