123import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";45import type { VaultEntry } from "../../shared/types";6import { CJKExportView } from "./CJKExport";7import { flushView, inputText, renderView } from "./view-test-utils";89const sendMessageMock = vi.fn<(msg: unknown) => Promise<unknown>>();10const exportToCJKMock = vi.fn<(input: unknown) => Promise<unknown>>();1112vi.mock("../App", () => ({13 sendMessage: (msg: unknown) => sendMessageMock(msg),14}));1516vi.mock("../../crypto/cjkExport", () => ({17 ARGON2_DEFAULT: {18 memoryKiB: 65536,19 iterations: 3,20 parallelism: 4,21 },22 exportToCJK: (input: unknown) => exportToCJKMock(input),23}));2425vi.mock("../../crypto/cjkPassphrase", () => ({26 MIN_PASSPHRASE_LENGTH: 16,27 evaluateStrength: () => ({28 tier: "excellent",29 score: 4,30 feedback: "strong",31 }),32 validateForExport: ({33 passphrase,34 confirm,35 }: {36 passphrase: string;37 confirm: string;38 }) =>39 passphrase.length >= 16 && passphrase === confirm40 ? { ok: true }41 : { ok: false, error: "PASSPHRASE_TOO_SHORT" },42 checkPassphraseBreached: vi.fn(async () => ({43 breached: false,44 count: 0,45 apiReachable: true,46 })),47 generateDicewarePassphrase: () => "correct horse battery staple drift waves",48}));4950describe("CJKExportView", () => {51 const disposers: Array<() => void> = [];5253 beforeEach(() => {54 document.body.innerHTML = "";55 sendMessageMock.mockReset();56 exportToCJKMock.mockReset();57 });5859 afterEach(() => {60 while (disposers.length > 0) {61 disposers.pop()?.();62 }63 });6465 function mount() {66 const mounted = renderView(<CJKExportView onBack={vi.fn()} />);67 disposers.push(mounted.dispose);68 return mounted.container;69 }7071 async function unlockExport(container: HTMLElement) {72 const passphrase = container.querySelector(73 'input[placeholder="passphrase"]'74 ) as HTMLInputElement;75 inputText(passphrase, "correct horse battery staple");76 await flushView();7778 const confirm = container.querySelector(79 'input[placeholder="confirm passphrase"]'80 ) as HTMLInputElement;81 inputText(confirm, "correct horse battery staple");82 await flushView();83 }8485 function findButton(container: HTMLElement, text: string): HTMLButtonElement {86 return Array.from(container.querySelectorAll("button")).find(87 (candidate) => candidate.textContent?.trim() === text88 ) as HTMLButtonElement;89 }9091 function sampleEntry(overrides: Partial<VaultEntry> = {}): VaultEntry {92 return {93 id: "entry-1",94 type: "login",95 title: "GitHub",96 username: "june",97 password: "secret123",98 url: "https://github.com",99 createdAt: 1,100 modifiedAt: 1,101 ...overrides,102 };103 }104105 it("routes back to unlock when the background is unavailable during export", async () => {106 const onRequiresUnlock = vi.fn();107 sendMessageMock.mockImplementation(async (msg: unknown) => {108 const type = (msg as { type?: string }).type;109 if (type === "GET_ENTRIES") {110 return { ok: false, error: "SERVICE_WORKER_UNAVAILABLE" };111 }112 return { ok: true, data: {} };113 });114 const mounted = renderView(115 <CJKExportView onBack={vi.fn()} onRequiresUnlock={onRequiresUnlock} />116 );117 disposers.push(mounted.dispose);118 const container = mounted.container;119120 await unlockExport(container);121 findButton(container, "Create backup phrase →").click();122 await flushView();123 await flushView();124125 expect(exportToCJKMock).not.toHaveBeenCalled();126 expect(onRequiresUnlock).toHaveBeenCalled();127 });128129 it("exports all entries including trashed and shows counts", async () => {130 sendMessageMock.mockImplementation(async (msg: unknown) => {131 const type = (msg as { type?: string }).type;132 if (type === "GET_ENTRIES") {133 return {134 ok: true,135 data: [136 sampleEntry(),137 sampleEntry({138 id: "note-1",139 type: "note",140 title: "Recovery codes",141 notes: "a1b2-c3d4",142 password: undefined,143 username: undefined,144 url: undefined,145 }),146 sampleEntry({147 id: "deleted-1",148 title: "Old account",149 deletedAt: 2,150 }),151 ],152 };153 }154 if (type === "GET_SETTINGS") {155 return { ok: true, data: { autoLockMinutes: 30 } };156 }157 return { ok: true };158 });159 exportToCJKMock.mockResolvedValue({160 phrase: "peach:v3:entries=3:zstd:aes256gcm:argon2id\n漢字漢字",161 entryCount: 3,162 cjkCharCount: 4,163 loginCount: 1,164 noteCount: 1,165 passkeyCount: 0,166 trashedCount: 1,167 binary: new Uint8Array([1, 2, 3]),168 });169 const container = mount();170171 await unlockExport(container);172 findButton(container, "Create backup phrase →").click();173 await flushView();174 await flushView();175 await flushView();176177 expect(exportToCJKMock).toHaveBeenCalledWith({178 entries: [179 expect.objectContaining({ id: "entry-1", title: "GitHub" }),180 expect.objectContaining({ id: "note-1", title: "Recovery codes" }),181 expect.objectContaining({ id: "deleted-1", title: "Old account", deletedAt: 2 }),182 ],183 settings: { autoLockMinutes: 30 },184 passphrase: "correct horse battery staple",185 argon2: expect.objectContaining({ memoryKiB: 65536 }),186 });187 expect(container.textContent).toContain("2 entries");188 expect(container.textContent).toContain("1 in trash");189 expect(container.textContent).toContain("1 login");190 expect(container.textContent).toContain("1 note");191 });192});