// @vitest-environment jsdom import type { JSX } from "solid-js"; import { afterEach, describe, expect, it, vi } from "vitest"; import { Onboarding } from "./Onboarding"; import { AdoptVaultView } from "./AdoptVaultView"; import { flushView, inputText, renderView } from "./view-test-utils"; import { encodePairingCode } from "../../shared/pairing-code"; const PAIRING_CODE = encodePairingCode("192.168.4.77", 51555, Uint8Array.of(1, 2, 3, 4)); vi.mock("@zxcvbn-ts/core", () => ({ zxcvbn: (value: string) => ({ score: value.length >= 16 ? 4 : 1 }), zxcvbnOptions: { setOptions: vi.fn() }, })); vi.mock("@zxcvbn-ts/language-common", () => ({ adjacencyGraphs: {}, dictionary: {}, })); vi.mock("@zxcvbn-ts/language-en", () => ({ translations: {}, dictionary: {}, })); function button(container: HTMLElement, text: string): HTMLButtonElement { const found = Array.from(container.querySelectorAll("button")).find((b) => (b.textContent ?? "").includes(text), ); if (!found) throw new Error(`button not found: ${text}`); return found as HTMLButtonElement; } /** The pair phase now needs a real code before it will dial anything. */ async function enterPairingCode(container: HTMLElement) { inputText(container.querySelector("#adopt-pair-code") as HTMLInputElement, PAIRING_CODE); await flushView(); } async function enterStrongPassword(container: HTMLElement) { inputText(container.querySelector("#adopt-password") as HTMLInputElement, "9xQ!vZ2pLm74Tr#Ke8Wd5Bn"); inputText(container.querySelector("#adopt-confirm-password") as HTMLInputElement, "9xQ!vZ2pLm74Tr#Ke8Wd5Bn"); await flushView(); } describe("vault adoption onboarding", () => { const cleanups: Array<() => void> = []; afterEach(() => { vi.useRealTimers(); while (cleanups.length) cleanups.pop()!(); }); function mounted(view: () => JSX.Element) { const rendered = renderView(view); cleanups.push(rendered.dispose); return rendered; } it("T-A21: Onboarding offers Create vault, Restore from backup, and I already have Peach", () => { vi.useFakeTimers(); const onComplete = vi.fn(); const onRestore = vi.fn(); const onAdopt = vi.fn(); const view = mounted(() => ); button(view.container, "next").click(); vi.advanceTimersByTime(220); button(view.container, "next").click(); vi.advanceTimersByTime(220); expect(view.container.textContent).toContain("Create vault"); expect(view.container.textContent).toContain("Restore from backup"); expect(view.container.textContent).toContain("I already have Peach"); button(view.container, "I already have Peach").click(); expect(onAdopt).toHaveBeenCalledTimes(1); }); it("T-A22: password screen uses the same minimum-length gate as create-vault", () => { const view = mounted(() => {}} onDone={() => {}} startAdoption={vi.fn()} />); const continueButton = button(view.container, "Continue"); expect(continueButton.disabled).toBe(true); inputText(view.container.querySelector("#adopt-password") as HTMLInputElement, "short"); inputText(view.container.querySelector("#adopt-confirm-password") as HTMLInputElement, "short"); expect(view.container.textContent).toContain("need 7 more characters"); expect(continueButton.disabled).toBe(true); }); it("T-A26: pairing cannot start until a valid pairing code is entered", async () => { const startAdoption = vi.fn(async () => {}); const view = mounted(() => {}} onDone={() => {}} startAdoption={startAdoption} />); await enterStrongPassword(view.container); button(view.container, "Continue").click(); await flushView(); expect(button(view.container, "Pair a device").disabled).toBe(true); // Long enough to be grouped, but too short to carry a token. inputText(view.container.querySelector("#adopt-pair-code") as HTMLInputElement, "A74H48J3"); await flushView(); expect(button(view.container, "Pair a device").disabled).toBe(true); await enterPairingCode(view.container); expect(button(view.container, "Pair a device").disabled).toBe(false); expect(startAdoption).not.toHaveBeenCalled(); }); it("T-A27: the typed code is handed to the adoption transport", async () => { const startAdoption = vi.fn(async (_args: { code: string }) => {}); const view = mounted(() => {}} onDone={() => {}} startAdoption={startAdoption} />); await enterStrongPassword(view.container); button(view.container, "Continue").click(); await enterPairingCode(view.container); button(view.container, "Pair a device").click(); await flushView(); expect(startAdoption).toHaveBeenCalledTimes(1); expect(startAdoption.mock.calls[0][0]).toMatchObject({ code: PAIRING_CODE }); }); it("T-A23: adoption flow reaches the SAS screen", async () => { const startAdoption = vi.fn(async ({ confirmSAS }: { confirmSAS(sas: string): Promise }) => { await confirmSAS("895245"); }); const view = mounted(() => {}} onDone={() => {}} startAdoption={startAdoption} />); await enterStrongPassword(view.container); button(view.container, "Continue").click(); await enterPairingCode(view.container); button(view.container, "Pair a device").click(); await flushView(); expect(view.container.textContent).toContain("Confirm this code matches"); expect(view.container.textContent).toContain("8 9 5 2 4 5"); }); it("T-A24: success leaves a vault and opens Peach", async () => { const onDone = vi.fn(); const view = mounted( () => ( {}} onDone={onDone} startAdoption={vi.fn(async () => {})} /> ), ); await enterStrongPassword(view.container); button(view.container, "Continue").click(); await enterPairingCode(view.container); button(view.container, "Pair a device").click(); await flushView(); expect(view.container.textContent).toContain("Joined your vault."); button(view.container, "Open Peach").click(); expect(onDone).toHaveBeenCalledTimes(1); }); it("T-A25: SAS cancel keeps receiver vault-less and returns to the password step", async () => { const createVault = vi.fn(); const view = mounted( () => ( {}} onDone={() => {}} startAdoption={vi.fn(async ({ confirmSAS }) => { const ok = await confirmSAS("895245"); if (ok) await createVault(); else throw new Error("sas_rejected"); })} /> ), ); await enterStrongPassword(view.container); button(view.container, "Continue").click(); await enterPairingCode(view.container); button(view.container, "Pair a device").click(); await flushView(); button(view.container, "Cancel").click(); await flushView(); expect(createVault).not.toHaveBeenCalled(); expect(view.container.textContent).toContain("Set a password for this device."); }); });