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("shows a worker error instead of generating a fake 0-entry phrase", async () => {106 sendMessageMock.mockImplementation(async (msg: unknown) => {107 const type = (msg as { type?: string }).type;108 if (type === "GET_ENTRIES") {109 return { ok: false, error: "SERVICE_WORKER_UNAVAILABLE" };110 }111 return { ok: true, data: {} };112 });113 const container = mount();114115 await unlockExport(container);116 findButton(container, "Create backup phrase →").click();117 await flushView();118 await flushView();119120 expect(exportToCJKMock).not.toHaveBeenCalled();121 expect(container.textContent).toContain(122 "Peach couldn't reach its background worker. Reopen the popup and try again."123 );124 expect(container.textContent).not.toContain("0 entries");125 });126127 it("exports only live entries and shows the returned counts", async () => {128 sendMessageMock.mockImplementation(async (msg: unknown) => {129 const type = (msg as { type?: string }).type;130 if (type === "GET_ENTRIES") {131 return {132 ok: true,133 data: [134 sampleEntry(),135 sampleEntry({136 id: "note-1",137 type: "note",138 title: "Recovery codes",139 notes: "a1b2-c3d4",140 password: undefined,141 username: undefined,142 url: undefined,143 }),144 sampleEntry({145 id: "deleted-1",146 title: "Old account",147 deletedAt: 2,148 }),149 ],150 };151 }152 if (type === "GET_SETTINGS") {153 return { ok: true, data: { autoLockMinutes: 30 } };154 }155 return { ok: true };156 });157 exportToCJKMock.mockResolvedValue({158 phrase: "peach:v3:entries=2:zstd:aes256gcm:argon2id\n漢字漢字",159 entryCount: 2,160 cjkCharCount: 4,161 loginCount: 1,162 noteCount: 1,163 passkeyCount: 0,164 binary: new Uint8Array([1, 2, 3]),165 });166 const container = mount();167168 await unlockExport(container);169 findButton(container, "Create backup phrase →").click();170 await flushView();171 await flushView();172 await flushView();173174 expect(exportToCJKMock).toHaveBeenCalledWith({175 entries: [176 expect.objectContaining({ id: "entry-1", title: "GitHub" }),177 expect.objectContaining({ id: "note-1", title: "Recovery codes" }),178 ],179 settings: { autoLockMinutes: 30 },180 passphrase: "correct horse battery staple",181 argon2: expect.objectContaining({ memoryKiB: 65536 }),182 });183 expect(container.textContent).toContain("2 entries");184 expect(container.textContent).toContain("1 login");185 expect(container.textContent).toContain("1 note");186 });187});