123import type { JSX } from "solid-js";4import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";56import type { ImportParseResult } from "../../shared/import";7import type { VaultEntry } from "../../shared/types";8import { LockView } from "./Lock";9import { ExportView } from "./Export";10import { SettingsView } from "./Settings";11import { DEFAULT_BINDINGS } from "../../shared/shortcuts";12import { EntryView } from "./Entry";13import { ImportView } from "./Import";14import { VaultView } from "./Vault";15import { changeSelect, flushView, inputText, pressEnter, renderView } from "./view-test-utils";1617const sendMessageMock = vi.fn<(msg: unknown) => Promise<unknown>>();18const triggerRingMock = vi.fn();19const triggerPipPulseMock = vi.fn();20const exportEntriesToPeachJsonMock = vi.fn();21const parseImportSourceMock = vi.fn<(raw: string, password?: string) => Promise<ImportParseResult>>();22const zxcvbnMock = vi.fn((value: string) => ({ score: value.length >= 16 ? 3 : 1 }));2324vi.mock("../App", () => ({25 sendMessage: (msg: unknown) => sendMessageMock(msg),26 triggerRing: () => triggerRingMock(),27 triggerPipPulse: () => triggerPipPulseMock(),28}));2930vi.mock("../../shared/backup", () => ({31 exportEntriesToPeachJson: (...args: unknown[]) => exportEntriesToPeachJsonMock(...args),32}));3334vi.mock("../../shared/import", () => ({35 parseImportSource: (...args: [string, string | undefined]) => parseImportSourceMock(...args),36}));3738vi.mock("../../shared/favicon", () => ({39 faviconUrl: () => null,40}));4142vi.mock("../../crypto/totp", () => ({43 generateTOTP: () => ({ code: "123 456", remaining: 24 }),44 parseOtpauthURI: (value: string) =>45 value.startsWith("otpauth://")46 ? { secret: "NB2W45DFOIZA====", algorithm: "SHA1" as const }47 : null,48}));4950vi.mock("../components/GeneratorDrawer", () => ({51 GeneratorDrawer: () => null,52}));5354vi.mock("./CJKImport", () => ({55 CJKImportView: () => null,56}));5758vi.mock("./CJKExport", () => ({59 CJKExportView: () => null,60}));6162vi.mock("zxcvbn", () => ({63 default: (value: string) => zxcvbnMock(value),64}));6566describe("popup contract tests", () => {67 const disposers: Array<() => void> = [];6869 beforeEach(() => {70 document.body.innerHTML = "";71 sendMessageMock.mockReset();72 triggerRingMock.mockReset();73 triggerPipPulseMock.mockReset();74 exportEntriesToPeachJsonMock.mockReset();75 parseImportSourceMock.mockReset();76 zxcvbnMock.mockReset();77 zxcvbnMock.mockImplementation((value: string) => ({ score: value.length >= 16 ? 3 : 1 }));78 parseImportSourceMock.mockResolvedValue({79 format: "unknown",80 label: "Unknown format",81 entries: [],82 totalItems: 0,83 skippedItems: 0,84 requiresPassword: false,85 });8687 vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {88 return window.setTimeout(() => cb(0), 0) as unknown as number;89 });90 vi.stubGlobal("cancelAnimationFrame", (id: number) => window.clearTimeout(id));91 vi.stubGlobal("URL", {92 createObjectURL: vi.fn(() => "blob:peach-test"),93 revokeObjectURL: vi.fn(),94 });95 vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {});9697 (globalThis as { chrome?: typeof chrome }).chrome = {98 runtime: {99 getManifest: () => ({ version: "1.4.47" }),100 onMessage: { addListener: vi.fn() },101 },102 } as unknown as typeof chrome;103 });104105 afterEach(() => {106 while (disposers.length > 0) {107 disposers.pop()?.();108 }109 vi.unstubAllGlobals();110 });111112 function mount(view: JSX.Element) {113 const mounted = renderView(view);114 disposers.push(mounted.dispose);115 return mounted.container;116 }117118 function sampleEntry(overrides: Partial<VaultEntry> = {}): VaultEntry {119 return {120 id: "entry-1",121 type: "login",122 title: "GitHub",123 username: "june",124 password: "secret123",125 url: "https://github.com",126 totp: "JBSWY3DPEHPK3PXP",127 canary: {128 webhookUrl: "https://example.com/hook",129 },130 createdAt: 1,131 modifiedAt: 1,132 ...overrides,133 };134 }135136 function sampleImportResult(137 overrides: Partial<ImportParseResult> = {}138 ): ImportParseResult {139 return {140 format: "csv",141 label: "CSV import",142 entries: [143 {144 type: "login",145 title: "Imported site",146 username: "imported-user",147 password: "imported-pass",148 url: "https://imported.test",149 },150 ],151 totalItems: 1,152 skippedItems: 0,153 requiresPassword: false,154 ...overrides,155 };156 }157158 async function selectImportFile(159 container: HTMLElement,160 contents = '{"export":"test"}'161 ) {162 const input = container.querySelector('input[type="file"]') as HTMLInputElement;163 const file = new File([contents], "import.json", { type: "application/json" });164 Object.defineProperty(input, "files", {165 configurable: true,166 value: [file],167 });168 input.dispatchEvent(new Event("change", { bubbles: true }));169 await flushView();170 await flushView();171 }172173 function findButton(container: HTMLElement, text: string): HTMLButtonElement {174 return Array.from(container.querySelectorAll("button")).find(175 (candidate) => candidate.textContent?.trim() === text176 ) as HTMLButtonElement;177 }178179 it("Lock sends UNLOCK with the entered passphrase", async () => {180 sendMessageMock.mockResolvedValue({ ok: true });181 const onUnlock = vi.fn(async () => undefined);182 const container = mount(<LockView hasVault={true} ready={true} onUnlock={onUnlock} />);183184 const password = container.querySelector("#unlock-password") as HTMLInputElement;185 inputText(password, "correct horse battery staple");186 (container.querySelector(".peach-lock-submit") as HTMLButtonElement).click();187 await flushView();188189 expect(sendMessageMock).toHaveBeenCalledWith({190 type: "UNLOCK",191 payload: { password: "correct horse battery staple" },192 });193 expect(onUnlock).toHaveBeenCalled();194 });195196 it("Lock keeps create-vault disabled for short passwords and weak passphrases", async () => {197 const container = mount(<LockView hasVault={false} ready={true} onUnlock={vi.fn()} />);198 const createButton = container.querySelector(".peach-lock-submit") as HTMLButtonElement;199 const password = container.querySelector("#master-password") as HTMLInputElement;200 const confirm = container.querySelector("#confirm-password") as HTMLInputElement;201202 inputText(password, "short");203 inputText(confirm, "short");204 await flushView();205 expect(createButton.disabled).toBe(true);206207 zxcvbnMock.mockImplementation(() => ({ score: 1 }));208 inputText(password, "weakweakweak");209 inputText(confirm, "weakweakweak");210 await flushView();211 expect(createButton.disabled).toBe(true);212 });213214 it("Lock sends CREATE_VAULT when the onboarding passphrase is strong enough", async () => {215 sendMessageMock.mockResolvedValue({ ok: true });216 zxcvbnMock.mockImplementation(() => ({ score: 3 }));217 const onUnlock = vi.fn(async () => undefined);218 const container = mount(<LockView hasVault={false} ready={true} onUnlock={onUnlock} />);219220 const password = container.querySelector("#master-password") as HTMLInputElement;221 const confirm = container.querySelector("#confirm-password") as HTMLInputElement;222 const createButton = container.querySelector(".peach-lock-submit") as HTMLButtonElement;223224 inputText(password, "correct horse battery staple");225 inputText(confirm, "correct horse battery staple");226 await flushView();227 expect(createButton.disabled).toBe(false);228229 pressEnter(confirm);230 await flushView();231232 expect(sendMessageMock).toHaveBeenCalledWith({233 type: "CREATE_VAULT",234 payload: { password: "correct horse battery staple" },235 });236 expect(onUnlock).toHaveBeenCalled();237 });238239 it("Export menu surfaces the three v2.5.0 backup formats", async () => {240 const container = mount(<ExportView onBack={vi.fn()} />);241 await flushView();242243 expect(container.textContent).toContain("PeachScript");244 expect(container.textContent).toContain("print backup");245 expect(container.textContent).toContain("CSV");246 });247248 it("Settings requires current, new, and confirm passwords before enabling save", async () => {249 sendMessageMock.mockResolvedValue({ ok: true, data: null });250 const container = mount(251 <SettingsView252 entriesCount={1}253 breachMonitoring={true}254 lockOnClose={true}255 autoLockMinutes={5}256 trashRetentionDays={30}257 theme="dark"258 accentColor="peach"259 onBreachMonitoringChange={vi.fn()}260 onLockOnCloseChange={vi.fn()}261 onAutoLockMinutesChange={vi.fn()}262 onTrashRetentionChange={vi.fn()}263 onThemeChange={vi.fn()}264 onAccentChange={vi.fn()}265 shortcuts={DEFAULT_BINDINGS}266 onShortcutsChange={vi.fn()}267 onBack={vi.fn()}268 onImport={vi.fn()}269 onExport={vi.fn()}270 onBreachCheckDone={vi.fn()}271 />272 );273 await flushView();274275 const securityRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(276 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Security"277 ) as HTMLButtonElement;278 securityRow.click();279 await flushView();280281 const changeButton = Array.from(container.querySelectorAll("button")).find(282 (candidate) => candidate.textContent?.trim() === "change"283 ) as HTMLButtonElement;284 changeButton.click();285 await flushView();286287 const saveButton = Array.from(container.querySelectorAll("button")).find(288 (candidate) => candidate.textContent?.trim() === "save new password"289 ) as HTMLButtonElement;290 expect(saveButton.disabled).toBe(true);291292 const inputs = container.querySelectorAll('input[type="password"]');293 inputText(inputs[0] as HTMLInputElement, "current-pass");294 inputText(inputs[1] as HTMLInputElement, "new-pass-12345");295 await flushView();296 expect(saveButton.disabled).toBe(true);297298 inputText(inputs[2] as HTMLInputElement, "new-pass-12345");299 await flushView();300 expect(saveButton.disabled).toBe(false);301 });302303 it("Settings sends CHANGE_PASSWORD and forwards auto-lock/theme changes through callbacks", async () => {304 sendMessageMock.mockResolvedValue({ ok: true, data: null });305 const onAutoLockMinutesChange = vi.fn();306 const onThemeChange = vi.fn();307 const container = mount(308 <SettingsView309 entriesCount={1}310 breachMonitoring={true}311 lockOnClose={true}312 autoLockMinutes={5}313 trashRetentionDays={30}314 theme="dark"315 accentColor="peach"316 onBreachMonitoringChange={vi.fn()}317 onLockOnCloseChange={vi.fn()}318 onAutoLockMinutesChange={onAutoLockMinutesChange}319 onTrashRetentionChange={vi.fn()}320 onThemeChange={onThemeChange}321 onAccentChange={vi.fn()}322 shortcuts={DEFAULT_BINDINGS}323 onShortcutsChange={vi.fn()}324 onBack={vi.fn()}325 onImport={vi.fn()}326 onExport={vi.fn()}327 onBreachCheckDone={vi.fn()}328 />329 );330 await flushView();331 sendMessageMock.mockClear();332333 const headerBack = container.querySelector(".peach-overlay-header button") as HTMLButtonElement;334335 const securityRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(336 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Security"337 ) as HTMLButtonElement;338 securityRow.click();339 await flushView();340341 const selects = container.querySelectorAll("select");342 changeSelect(selects[0] as HTMLSelectElement, "15");343 expect(onAutoLockMinutesChange).toHaveBeenCalledWith(15);344345 const changeButton = Array.from(container.querySelectorAll("button")).find(346 (candidate) => candidate.textContent?.trim() === "change"347 ) as HTMLButtonElement;348 changeButton.click();349 await flushView();350351 const inputs = container.querySelectorAll('input[type="password"]');352 inputText(inputs[0] as HTMLInputElement, "current-pass");353 inputText(inputs[1] as HTMLInputElement, "new-password-123");354 inputText(inputs[2] as HTMLInputElement, "new-password-123");355 await flushView();356357 const saveButton = Array.from(container.querySelectorAll("button")).find(358 (candidate) => candidate.textContent?.trim() === "save new password"359 ) as HTMLButtonElement;360 saveButton.click();361 await flushView();362363 headerBack.click();364 await flushView();365 const appearanceRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(366 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Appearance"367 ) as HTMLButtonElement;368 appearanceRow.click();369 await flushView();370 const themeButton = Array.from(container.querySelectorAll("button")).find(371 (candidate) => candidate.textContent?.trim() === "Switch to light"372 ) as HTMLButtonElement;373 themeButton.click();374 expect(onThemeChange).toHaveBeenCalledWith("light");375376 expect(sendMessageMock).toHaveBeenCalledWith({377 type: "CHANGE_PASSWORD",378 payload: {379 currentPassword: "current-pass",380 newPassword: "new-password-123",381 },382 });383 });384385 it("Entry copy actions send COPY and canary ping messages for password and username", async () => {386 sendMessageMock.mockResolvedValue({ ok: true });387 const container = mount(388 <EntryView389 entry={sampleEntry()}390 onDelete={vi.fn()}391 onFieldSaved={vi.fn(async () => undefined)}392 />393 );394395 const passwordButton = container.querySelector('button[aria-label="Copy password"]') as HTMLButtonElement;396 passwordButton.click();397 await flushView();398399 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {400 type: "COPY",401 payload: { text: "secret123", label: "password", alreadyWritten: false },402 });403 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {404 type: "FIRE_CANARY_PING",405 payload: {406 webhookUrl: "https://example.com/hook",407 entryTitle: "GitHub",408 trigger: "password_copied",409 },410 });411412 sendMessageMock.mockClear();413 const usernameButton = container.querySelector('button[aria-label="Copy username"]') as HTMLButtonElement;414 usernameButton.click();415 await flushView();416417 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {418 type: "COPY",419 payload: { text: "june", label: "username", alreadyWritten: false },420 });421 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {422 type: "FIRE_CANARY_PING",423 payload: {424 webhookUrl: "https://example.com/hook",425 entryTitle: "GitHub",426 trigger: "username_copied",427 },428 });429 });430431 it("Entry refuses to send an empty password update and copies TOTP through COPY", async () => {432 sendMessageMock.mockResolvedValue({ ok: true });433 const onFieldSaved = vi.fn(async () => undefined);434 const container = mount(435 <EntryView436 entry={sampleEntry()}437 onDelete={vi.fn()}438 onFieldSaved={onFieldSaved}439 />440 );441442 const passwordValue = container.querySelector(".peach-password-value") as HTMLSpanElement;443 passwordValue.click();444 await flushView();445446 const passwordInput = container.querySelector('input[placeholder="paste or type a password"]') as HTMLInputElement;447 inputText(passwordInput, "");448 await flushView();449450 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;451 saveButton.click();452 await flushView();453454 expect(sendMessageMock).not.toHaveBeenCalledWith(455 expect.objectContaining({ type: "UPDATE_ENTRY" })456 );457 expect(onFieldSaved).not.toHaveBeenCalled();458459 const totpMain = container.querySelector(".peach-totp-main") as HTMLDivElement;460 totpMain.click();461 await flushView();462463 expect(sendMessageMock).toHaveBeenLastCalledWith({464 type: "COPY",465 payload: { text: "123456", label: "totp", alreadyWritten: false },466 });467 });468469 it("Entry closes an unchanged inline editor when focus leaves it", async () => {470 sendMessageMock.mockResolvedValue({ ok: true });471 const container = mount(472 <EntryView473 entry={sampleEntry()}474 onDelete={vi.fn()}475 onFieldSaved={vi.fn(async () => undefined)}476 />477 );478479 const usernameValue = container.querySelector(".peach-field-value.peach-inline-clickable") as HTMLSpanElement;480 usernameValue.click();481 await flushView();482483 const usernameInput = container.querySelector(".peach-inline-input[autocomplete=\"off\"]") as HTMLInputElement;484 usernameInput.blur();485 await flushView();486487 expect(container.querySelector(".peach-inline-input[autocomplete=\"off\"]")).toBeNull();488 expect(sendMessageMock).not.toHaveBeenCalledWith(489 expect.objectContaining({ type: "UPDATE_ENTRY" })490 );491 });492493 it("Entry lets TOTP secrets be edited inline", async () => {494 sendMessageMock.mockResolvedValue({ ok: true });495 const onFieldSaved = vi.fn(async () => undefined);496 const container = mount(497 <EntryView498 entry={sampleEntry()}499 onDelete={vi.fn()}500 onFieldSaved={onFieldSaved}501 />502 );503504 const totpMain = container.querySelector(".peach-totp-main") as HTMLDivElement;505 totpMain.dispatchEvent(new MouseEvent("dblclick", { bubbles: true }));506 await flushView();507508 const totpInput = container.querySelector('input[placeholder="otpauth://... or base32 secret"]') as HTMLInputElement;509 inputText(totpInput, "otpauth://totp/Example?secret=NB2W45DFOIZA====&issuer=Peach");510 await flushView();511512 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;513 saveButton.click();514 await flushView();515516 expect(sendMessageMock).toHaveBeenCalledWith({517 type: "UPDATE_ENTRY",518 payload: {519 id: "entry-1",520 patch: { totp: "NB2W45DFOIZA====" },521 },522 });523 expect(onFieldSaved).toHaveBeenCalled();524 });525526 it("Vault seeds and focuses search when a search seed is provided", async () => {527 const container = mount(528 <VaultView529 entries={[sampleEntry(), sampleEntry({ id: "entry-2", title: "GitLab" })]}530 sortOrder="url-first"531 searchSeed="g"532 searchSeedNonce={1}533 onSelectEntry={vi.fn()}534 onSortOrderChange={vi.fn()}535 onAdd={vi.fn()}536 onClose={vi.fn()}537 />538 );539540 await flushView();541542 const search = container.querySelector(".peach-list-top .peach-search-input") as HTMLInputElement;543 expect(search.value).toBe("g");544 expect(document.activeElement).toBe(search);545 });546547 it("Import existing-vault add mode parses a file and batches entries into the vault", async () => {548 parseImportSourceMock.mockResolvedValue(549 sampleImportResult({550 entries: [551 {552 type: "login",553 title: "Imported site",554 username: "imported-user",555 password: "imported-pass",556 url: "https://imported.test",557 },558 ],559 })560 );561 sendMessageMock.mockImplementation(async (msg: unknown) => {562 const type = (msg as { type?: string }).type;563 if (type === "GET_ENTRIES") {564 return {565 ok: true,566 data: [567 sampleEntry({568 id: "existing-1",569 title: "Imported site",570 username: "imported-user",571 url: "https://imported.test",572 }),573 ],574 };575 }576 if (type === "BATCH_ADD_ENTRIES") {577 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };578 }579 return { ok: true };580 });581 const onImport = vi.fn(async () => undefined);582 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);583 await flushView();584 sendMessageMock.mockClear();585586 await selectImportFile(container);587 findButton(container, "Import 1").click();588 await flushView();589590 expect(parseImportSourceMock).toHaveBeenCalledWith('{"export":"test"}', undefined);591 expect(sendMessageMock).toHaveBeenCalledWith({592 type: "BATCH_ADD_ENTRIES",593 payload: {594 entries: [595 expect.objectContaining({596 title: "Imported site",597 username: "imported-user",598 password: "imported-pass",599 url: "https://imported.test",600 }),601 ],602 },603 });604 expect(onImport).toHaveBeenCalled();605 expect(container.textContent).toContain("1 entry appears to be duplicates");606 });607608 it("Import replace mode requires confirmation before clearing and replacing the vault", async () => {609 parseImportSourceMock.mockResolvedValue(sampleImportResult());610 sendMessageMock.mockImplementation(async (msg: unknown) => {611 const type = (msg as { type?: string }).type;612 if (type === "GET_ENTRIES") {613 return { ok: true, data: [sampleEntry({ id: "existing-1" })] };614 }615 if (type === "CLEAR_VAULT_ENTRIES") {616 return { ok: true };617 }618 if (type === "BATCH_ADD_ENTRIES") {619 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };620 }621 return { ok: true };622 });623 const onImport = vi.fn(async () => undefined);624 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);625 await flushView();626 sendMessageMock.mockClear();627628 await selectImportFile(container);629 (container.querySelector('input[value="replace"]') as HTMLInputElement).click();630 await flushView();631632 findButton(container, "Replace vault with 1").click();633 await flushView();634635 expect(sendMessageMock).not.toHaveBeenCalledWith(636 expect.objectContaining({ type: "CLEAR_VAULT_ENTRIES" })637 );638 expect(container.textContent).toContain("Replace forever? This cannot be undone.");639640 findButton(container, "Yes, replace forever").click();641 await flushView();642643 expect(sendMessageMock).toHaveBeenCalledWith({ type: "CLEAR_VAULT_ENTRIES" });644 expect(sendMessageMock).toHaveBeenCalledWith({645 type: "BATCH_ADD_ENTRIES",646 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },647 });648 expect(onImport).toHaveBeenCalled();649 });650651 it("Import onboarding mode waits for a matching master password before creating a vault", async () => {652 parseImportSourceMock.mockResolvedValue(sampleImportResult());653 sendMessageMock.mockImplementation(async (msg: unknown) => {654 const type = (msg as { type?: string }).type;655 if (type === "CREATE_VAULT") return { ok: true };656 if (type === "BATCH_ADD_ENTRIES") {657 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };658 }659 return { ok: true };660 });661 const onImport = vi.fn(async () => undefined);662 const container = mount(663 <ImportView664 mode="onboarding"665 replacesExistingVault={true}666 onBack={vi.fn()}667 onImport={onImport}668 />669 );670671 await selectImportFile(container);672673 const createButton = findButton(container, "Create vault + import 1");674 expect(createButton.disabled).toBe(true);675 expect(container.textContent).toContain("Restoring now replaces the locked local vault");676677 const masterInput = container.querySelector(678 'input[placeholder="new master password"]'679 ) as HTMLInputElement;680 const confirmInput = container.querySelector(681 'input[placeholder="confirm master password"]'682 ) as HTMLInputElement;683 inputText(masterInput, "fresh-master-password");684 inputText(confirmInput, "fresh-master-password");685 await flushView();686687 expect(createButton.disabled).toBe(false);688 createButton.click();689 await flushView();690691 expect(sendMessageMock).toHaveBeenCalledWith({692 type: "CREATE_VAULT",693 payload: { password: "fresh-master-password" },694 });695 expect(sendMessageMock).toHaveBeenCalledWith({696 type: "BATCH_ADD_ENTRIES",697 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },698 });699 expect(onImport).toHaveBeenCalled();700 });701702 it("Import encrypted backups reparse with the backup password before import", async () => {703 parseImportSourceMock704 .mockResolvedValueOnce(705 sampleImportResult({706 format: "peach-json",707 label: "Peach encrypted backup",708 entries: [],709 totalItems: 0,710 requiresPassword: true,711 })712 )713 .mockResolvedValueOnce(714 sampleImportResult({715 format: "peach-json",716 label: "Peach encrypted backup",717 })718 );719 sendMessageMock.mockImplementation(async (msg: unknown) => {720 const type = (msg as { type?: string }).type;721 if (type === "GET_ENTRIES") return { ok: true, data: [] };722 if (type === "BATCH_ADD_ENTRIES") {723 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };724 }725 return { ok: true };726 });727 const onImport = vi.fn(async () => undefined);728 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);729 await flushView();730 sendMessageMock.mockClear();731732 await selectImportFile(container, '{"type":"peach-export"}');733 const password = container.querySelector('input[placeholder="backup password"]') as HTMLInputElement;734 inputText(password, "backup-passphrase");735 pressEnter(password);736 await flushView();737 await flushView();738739 expect(parseImportSourceMock).toHaveBeenNthCalledWith(1, '{"type":"peach-export"}', undefined);740 expect(parseImportSourceMock).toHaveBeenNthCalledWith(2, '{"type":"peach-export"}', "backup-passphrase");741742 findButton(container, "Import 1").click();743 await flushView();744745 expect(sendMessageMock).toHaveBeenCalledWith({746 type: "BATCH_ADD_ENTRIES",747 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },748 });749 expect(onImport).toHaveBeenCalled();750 });751});