AdoptVaultView.test.tsx

5.1 KB · raw

1// @vitest-environment jsdom23import type { JSX } from "solid-js";4import { afterEach, describe, expect, it, vi } from "vitest";5import { Onboarding } from "./Onboarding";6import { AdoptVaultView } from "./AdoptVaultView";7import { flushView, inputText, renderView } from "./view-test-utils";89vi.mock("@zxcvbn-ts/core", () => ({10  zxcvbn: (value: string) => ({ score: value.length >= 16 ? 4 : 1 }),11  zxcvbnOptions: { setOptions: vi.fn() },12}));13vi.mock("@zxcvbn-ts/language-common", () => ({14  adjacencyGraphs: {},15  dictionary: {},16}));17vi.mock("@zxcvbn-ts/language-en", () => ({18  translations: {},19  dictionary: {},20}));2122function button(container: HTMLElement, text: string): HTMLButtonElement {23  const found = Array.from(container.querySelectorAll("button")).find((b) =>24    (b.textContent ?? "").includes(text),25  );26  if (!found) throw new Error(`button not found: ${text}`);27  return found as HTMLButtonElement;28}2930async function enterStrongPassword(container: HTMLElement) {31  inputText(container.querySelector("#adopt-password") as HTMLInputElement, "9xQ!vZ2pLm74Tr#Ke8Wd5Bn");32  inputText(container.querySelector("#adopt-confirm-password") as HTMLInputElement, "9xQ!vZ2pLm74Tr#Ke8Wd5Bn");33  await flushView();34}3536describe("vault adoption onboarding", () => {37  const cleanups: Array<() => void> = [];3839  afterEach(() => {40    vi.useRealTimers();41    while (cleanups.length) cleanups.pop()!();42  });4344  function mounted(view: () => JSX.Element) {45    const rendered = renderView(view);46    cleanups.push(rendered.dispose);47    return rendered;48  }4950  it("T-A21: Onboarding offers Create vault, Restore from backup, and I already have Peach", () => {51    vi.useFakeTimers();52    const onComplete = vi.fn();53    const onRestore = vi.fn();54    const onAdopt = vi.fn();55    const view = mounted(() => <Onboarding onComplete={onComplete} onRestore={onRestore} onAdopt={onAdopt} />);5657    button(view.container, "next").click();58    vi.advanceTimersByTime(220);59    button(view.container, "next").click();60    vi.advanceTimersByTime(220);6162    expect(view.container.textContent).toContain("Create vault");63    expect(view.container.textContent).toContain("Restore from backup");64    expect(view.container.textContent).toContain("I already have Peach");65    button(view.container, "I already have Peach").click();66    expect(onAdopt).toHaveBeenCalledTimes(1);67  });6869  it("T-A22: password screen uses the same minimum-length gate as create-vault", () => {70    const view = mounted(() => <AdoptVaultView onCancel={() => {}} onDone={() => {}} startAdoption={vi.fn()} />);71    const continueButton = button(view.container, "Continue");72    expect(continueButton.disabled).toBe(true);73    inputText(view.container.querySelector("#adopt-password") as HTMLInputElement, "short");74    inputText(view.container.querySelector("#adopt-confirm-password") as HTMLInputElement, "short");75    expect(view.container.textContent).toContain("need 7 more characters");76    expect(continueButton.disabled).toBe(true);77  });7879  it("T-A23: adoption flow reaches the SAS screen", async () => {80    const startAdoption = vi.fn(async ({ confirmSAS }: { confirmSAS(sas: string): Promise<boolean> }) => {81      await confirmSAS("895245");82    });83    const view = mounted(() => <AdoptVaultView onCancel={() => {}} onDone={() => {}} startAdoption={startAdoption} />);84    await enterStrongPassword(view.container);85    button(view.container, "Continue").click();86    button(view.container, "Pair a device").click();87    await flushView();8889    expect(view.container.textContent).toContain("Confirm this code matches");90    expect(view.container.textContent).toContain("8 9 5 2 4 5");91  });9293  it("T-A24: success leaves a vault and opens Peach", async () => {94    const onDone = vi.fn();95    const view = mounted(96      () => (97        <AdoptVaultView98          onCancel={() => {}}99          onDone={onDone}100          startAdoption={vi.fn(async () => {})}101        />102      ),103    );104    await enterStrongPassword(view.container);105    button(view.container, "Continue").click();106    button(view.container, "Pair a device").click();107    await flushView();108109    expect(view.container.textContent).toContain("Joined your vault.");110    button(view.container, "Open Peach").click();111    expect(onDone).toHaveBeenCalledTimes(1);112  });113114  it("T-A25: SAS cancel keeps receiver vault-less and returns to the password step", async () => {115    const createVault = vi.fn();116    const view = mounted(117      () => (118        <AdoptVaultView119          onCancel={() => {}}120          onDone={() => {}}121          startAdoption={vi.fn(async ({ confirmSAS }) => {122            const ok = await confirmSAS("895245");123            if (ok) await createVault();124            else throw new Error("sas_rejected");125          })}126        />127      ),128    );129    await enterStrongPassword(view.container);130    button(view.container, "Continue").click();131    button(view.container, "Pair a device").click();132    await flushView();133    button(view.container, "Cancel").click();134    await flushView();135136    expect(createVault).not.toHaveBeenCalled();137    expect(view.container.textContent).toContain("Set a password for this device.");138  });139});