// @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");
});
});