// @vitest-environment jsdom import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { VaultEntry } from "../../shared/types"; import { CJKExportView } from "./CJKExport"; import { flushView, inputText, renderView } from "./view-test-utils"; const sendMessageMock = vi.fn<(msg: unknown) => Promise>(); const exportToCJKMock = vi.fn<(input: unknown) => Promise>(); vi.mock("../App", () => ({ sendMessage: (msg: unknown) => sendMessageMock(msg), })); vi.mock("../../crypto/cjkExport", () => ({ ARGON2_DEFAULT: { memoryKiB: 65536, iterations: 3, parallelism: 4, }, exportToCJK: (input: unknown) => exportToCJKMock(input), })); vi.mock("../../crypto/cjkPassphrase", () => ({ MIN_PASSPHRASE_LENGTH: 16, evaluateStrength: () => ({ tier: "excellent", score: 4, feedback: "strong", }), validateForExport: ({ passphrase, confirm, }: { passphrase: string; confirm: string; }) => passphrase.length >= 16 && passphrase === confirm ? { ok: true } : { ok: false, error: "PASSPHRASE_TOO_SHORT" }, checkPassphraseBreached: vi.fn(async () => ({ breached: false, count: 0, apiReachable: true, })), generateDicewarePassphrase: () => "correct horse battery staple drift waves", })); describe("CJKExportView", () => { const disposers: Array<() => void> = []; beforeEach(() => { document.body.innerHTML = ""; sendMessageMock.mockReset(); exportToCJKMock.mockReset(); }); afterEach(() => { while (disposers.length > 0) { disposers.pop()?.(); } }); function mount() { const mounted = renderView(); disposers.push(mounted.dispose); return mounted.container; } async function unlockExport(container: HTMLElement) { const passphrase = container.querySelector( 'input[placeholder="passphrase"]' ) as HTMLInputElement; inputText(passphrase, "correct horse battery staple"); await flushView(); const confirm = container.querySelector( 'input[placeholder="confirm passphrase"]' ) as HTMLInputElement; inputText(confirm, "correct horse battery staple"); await flushView(); } function findButton(container: HTMLElement, text: string): HTMLButtonElement { return Array.from(container.querySelectorAll("button")).find( (candidate) => candidate.textContent?.trim() === text ) as HTMLButtonElement; } function sampleEntry(overrides: Partial = {}): VaultEntry { return { id: "entry-1", type: "login", title: "GitHub", username: "june", password: "secret123", url: "https://github.com", createdAt: 1, modifiedAt: 1, ...overrides, }; } it("routes back to unlock when the background is unavailable during export", async () => { const onRequiresUnlock = vi.fn(); sendMessageMock.mockImplementation(async (msg: unknown) => { const type = (msg as { type?: string }).type; if (type === "GET_ENTRIES") { return { ok: false, error: "SERVICE_WORKER_UNAVAILABLE" }; } return { ok: true, data: {} }; }); const mounted = renderView( ); disposers.push(mounted.dispose); const container = mounted.container; await unlockExport(container); findButton(container, "Create backup phrase →").click(); await flushView(); await flushView(); expect(exportToCJKMock).not.toHaveBeenCalled(); expect(onRequiresUnlock).toHaveBeenCalled(); }); it("exports all entries including trashed and shows counts", async () => { sendMessageMock.mockImplementation(async (msg: unknown) => { const type = (msg as { type?: string }).type; if (type === "GET_ENTRIES") { return { ok: true, data: [ sampleEntry(), sampleEntry({ id: "note-1", type: "note", title: "Recovery codes", notes: "a1b2-c3d4", password: undefined, username: undefined, url: undefined, }), sampleEntry({ id: "deleted-1", title: "Old account", deletedAt: 2, }), ], }; } if (type === "GET_SETTINGS") { return { ok: true, data: { autoLockMinutes: 30 } }; } return { ok: true }; }); exportToCJKMock.mockResolvedValue({ phrase: "peach:v3:entries=3:zstd:aes256gcm:argon2id\n漢字漢字", entryCount: 3, cjkCharCount: 4, loginCount: 1, noteCount: 1, passkeyCount: 0, trashedCount: 1, binary: new Uint8Array([1, 2, 3]), }); const container = mount(); await unlockExport(container); findButton(container, "Create backup phrase →").click(); await flushView(); await flushView(); await flushView(); expect(exportToCJKMock).toHaveBeenCalledWith({ entries: [ expect.objectContaining({ id: "entry-1", title: "GitHub" }), expect.objectContaining({ id: "note-1", title: "Recovery codes" }), expect.objectContaining({ id: "deleted-1", title: "Old account", deletedAt: 2 }), ], settings: { autoLockMinutes: 30 }, passphrase: "correct horse battery staple", argon2: expect.objectContaining({ memoryKiB: 65536 }), }); expect(container.textContent).toContain("2 entries"); expect(container.textContent).toContain("1 in trash"); expect(container.textContent).toContain("1 login"); expect(container.textContent).toContain("1 note"); }); });