// @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"; 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; } 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 separates pairing from backup restore", () => { 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("Already using Peach?"); expect(view.container.textContent).toContain("Pair with another device"); expect(view.container.textContent).toContain("Scan a QR and confirm one 8-digit code."); expect(view.container.textContent).toContain("Restore a backup"); button(view.container, "Pair with another device").click(); expect(onAdopt).toHaveBeenCalledTimes(1); }); it("T-A22: password screen uses the same minimum-length gate as create-vault", () => { const view = mounted(() => {}} onDone={() => {}} />); 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-A28: pair phase shows the pairing-request QR after a strong password", async () => { const view = mounted(() => {}} onDone={() => {}} />); await enterStrongPassword(view.container); button(view.container, "Continue").click(); await flushView(); // The browser is the source of the QR now — the phone scans it. // We assert the heading + copy; the actual QR is rendered by a background // call that's out of scope for a jsdom test (chrome.runtime is undefined). expect(view.container.textContent).toContain("Pair with your other device"); expect(view.container.textContent).toContain("Scan this code on the device that holds your vault"); // The scan button from the old flow is gone — the phone is the scanner now. expect(view.container.textContent).not.toContain("Enter invitation manually"); }); });