123import 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";8import { encodePairingCode } from "../../shared/pairing-code";910const PAIRING_CODE = encodePairingCode("192.168.4.77", 51555, Uint8Array.of(1, 2, 3, 4));1112vi.mock("@zxcvbn-ts/core", () => ({13 zxcvbn: (value: string) => ({ score: value.length >= 16 ? 4 : 1 }),14 zxcvbnOptions: { setOptions: vi.fn() },15}));16vi.mock("@zxcvbn-ts/language-common", () => ({17 adjacencyGraphs: {},18 dictionary: {},19}));20vi.mock("@zxcvbn-ts/language-en", () => ({21 translations: {},22 dictionary: {},23}));2425function button(container: HTMLElement, text: string): HTMLButtonElement {26 const found = Array.from(container.querySelectorAll("button")).find((b) =>27 (b.textContent ?? "").includes(text),28 );29 if (!found) throw new Error(`button not found: ${text}`);30 return found as HTMLButtonElement;31}323334async function enterPairingCode(container: HTMLElement) {35 inputText(container.querySelector("#adopt-pair-code") as HTMLInputElement, PAIRING_CODE);36 await flushView();37}3839async function enterStrongPassword(container: HTMLElement) {40 inputText(container.querySelector("#adopt-password") as HTMLInputElement, "9xQ!vZ2pLm74Tr#Ke8Wd5Bn");41 inputText(container.querySelector("#adopt-confirm-password") as HTMLInputElement, "9xQ!vZ2pLm74Tr#Ke8Wd5Bn");42 await flushView();43}4445describe("vault adoption onboarding", () => {46 const cleanups: Array<() => void> = [];4748 afterEach(() => {49 vi.useRealTimers();50 while (cleanups.length) cleanups.pop()!();51 });5253 function mounted(view: () => JSX.Element) {54 const rendered = renderView(view);55 cleanups.push(rendered.dispose);56 return rendered;57 }5859 it("T-A21: Onboarding offers Create vault, Restore from backup, and I already have Peach", () => {60 vi.useFakeTimers();61 const onComplete = vi.fn();62 const onRestore = vi.fn();63 const onAdopt = vi.fn();64 const view = mounted(() => <Onboarding onComplete={onComplete} onRestore={onRestore} onAdopt={onAdopt} />);6566 button(view.container, "next").click();67 vi.advanceTimersByTime(220);68 button(view.container, "next").click();69 vi.advanceTimersByTime(220);7071 expect(view.container.textContent).toContain("Create vault");72 expect(view.container.textContent).toContain("Restore from backup");73 expect(view.container.textContent).toContain("I already have Peach");74 button(view.container, "I already have Peach").click();75 expect(onAdopt).toHaveBeenCalledTimes(1);76 });7778 it("T-A22: password screen uses the same minimum-length gate as create-vault", () => {79 const view = mounted(() => <AdoptVaultView onCancel={() => {}} onDone={() => {}} startAdoption={vi.fn()} />);80 const continueButton = button(view.container, "Continue");81 expect(continueButton.disabled).toBe(true);82 inputText(view.container.querySelector("#adopt-password") as HTMLInputElement, "short");83 inputText(view.container.querySelector("#adopt-confirm-password") as HTMLInputElement, "short");84 expect(view.container.textContent).toContain("need 7 more characters");85 expect(continueButton.disabled).toBe(true);86 });8788 it("T-A26: pairing cannot start until a valid pairing code is entered", async () => {89 const startAdoption = vi.fn(async () => {});90 const view = mounted(() => <AdoptVaultView onCancel={() => {}} onDone={() => {}} startAdoption={startAdoption} />);91 await enterStrongPassword(view.container);92 button(view.container, "Continue").click();93 await flushView();9495 expect(button(view.container, "Pair a device").disabled).toBe(true);96 97 inputText(view.container.querySelector("#adopt-pair-code") as HTMLInputElement, "A74H48J3");98 await flushView();99 expect(button(view.container, "Pair a device").disabled).toBe(true);100101 await enterPairingCode(view.container);102 expect(button(view.container, "Pair a device").disabled).toBe(false);103 expect(startAdoption).not.toHaveBeenCalled();104 });105106 it("T-A27: the typed code is handed to the adoption transport", async () => {107 const startAdoption = vi.fn(async (_args: { code: string }) => {});108 const view = mounted(() => <AdoptVaultView onCancel={() => {}} onDone={() => {}} startAdoption={startAdoption} />);109 await enterStrongPassword(view.container);110 button(view.container, "Continue").click();111 await enterPairingCode(view.container);112 button(view.container, "Pair a device").click();113 await flushView();114115 expect(startAdoption).toHaveBeenCalledTimes(1);116 expect(startAdoption.mock.calls[0][0]).toMatchObject({ code: PAIRING_CODE });117 });118119 it("T-A23: adoption flow reaches the SAS screen", async () => {120 const startAdoption = vi.fn(async ({ confirmSAS }: { confirmSAS(sas: string): Promise<boolean> }) => {121 await confirmSAS("895245");122 });123 const view = mounted(() => <AdoptVaultView onCancel={() => {}} onDone={() => {}} startAdoption={startAdoption} />);124 await enterStrongPassword(view.container);125 button(view.container, "Continue").click();126 await enterPairingCode(view.container);127 button(view.container, "Pair a device").click();128 await flushView();129130 expect(view.container.textContent).toContain("Confirm this code matches");131 expect(view.container.textContent).toContain("8 9 5 2 4 5");132 });133134 it("T-A24: success leaves a vault and opens Peach", async () => {135 const onDone = vi.fn();136 const view = mounted(137 () => (138 <AdoptVaultView139 onCancel={() => {}}140 onDone={onDone}141 startAdoption={vi.fn(async () => {})}142 />143 ),144 );145 await enterStrongPassword(view.container);146 button(view.container, "Continue").click();147 await enterPairingCode(view.container);148 button(view.container, "Pair a device").click();149 await flushView();150151 expect(view.container.textContent).toContain("Joined your vault.");152 button(view.container, "Open Peach").click();153 expect(onDone).toHaveBeenCalledTimes(1);154 });155156 it("T-A25: SAS cancel keeps receiver vault-less and returns to the password step", async () => {157 const createVault = vi.fn();158 const view = mounted(159 () => (160 <AdoptVaultView161 onCancel={() => {}}162 onDone={() => {}}163 startAdoption={vi.fn(async ({ confirmSAS }) => {164 const ok = await confirmSAS("895245");165 if (ok) await createVault();166 else throw new Error("sas_rejected");167 })}168 />169 ),170 );171 await enterStrongPassword(view.container);172 button(view.container, "Continue").click();173 await enterPairingCode(view.container);174 button(view.container, "Pair a device").click();175 await flushView();176 button(view.container, "Cancel").click();177 await flushView();178179 expect(createVault).not.toHaveBeenCalled();180 expect(view.container.textContent).toContain("Set a password for this device.");181 });182});