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 { EntryView } from "./Entry";12import { ImportView } from "./Import";13import { VaultView } from "./Vault";14import { changeSelect, flushView, inputText, pressEnter, renderView } from "./view-test-utils";1516const sendMessageMock = vi.fn<(msg: unknown) => Promise<unknown>>();17const triggerRingMock = vi.fn();18const triggerTotpRingMock = 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 triggerTotpRing: () => triggerTotpRingMock(),28 triggerPipPulse: () => triggerPipPulseMock(),29}));3031vi.mock("../../shared/backup", () => ({32 exportEntriesToPeachJson: (...args: unknown[]) => exportEntriesToPeachJsonMock(...args),33}));3435vi.mock("../../shared/import", () => ({36 parseImportSource: (...args: [string, string | undefined]) => parseImportSourceMock(...args),37}));3839vi.mock("../../shared/favicon", () => ({40 faviconUrl: () => null,41}));4243vi.mock("../../crypto/totp", () => ({44 generateTOTP: () => ({ code: "123 456", remaining: 24 }),45 parseOtpauthURI: (value: string) =>46 value.startsWith("otpauth://")47 ? { secret: "NB2W45DFOIZA====", algorithm: "SHA1" as const }48 : null,49}));5051vi.mock("../components/GeneratorDrawer", () => ({52 GeneratorDrawer: () => null,53}));5455vi.mock("./CJKImport", () => ({56 CJKImportView: () => null,57}));5859vi.mock("./CJKExport", () => ({60 CJKExportView: () => null,61}));6263vi.mock("zxcvbn", () => ({64 default: (value: string) => zxcvbnMock(value),65}));6667describe("popup contract tests", () => {68 const disposers: Array<() => void> = [];6970 beforeEach(() => {71 document.body.innerHTML = "";72 sendMessageMock.mockReset();73 triggerRingMock.mockReset();74 triggerTotpRingMock.mockReset();75 triggerPipPulseMock.mockReset();76 exportEntriesToPeachJsonMock.mockReset();77 parseImportSourceMock.mockReset();78 zxcvbnMock.mockReset();79 zxcvbnMock.mockImplementation((value: string) => ({ score: value.length >= 16 ? 3 : 1 }));80 parseImportSourceMock.mockResolvedValue({81 format: "unknown",82 label: "Unknown format",83 entries: [],84 totalItems: 0,85 skippedItems: 0,86 requiresPassword: false,87 });8889 vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {90 return window.setTimeout(() => cb(0), 0) as unknown as number;91 });92 vi.stubGlobal("cancelAnimationFrame", (id: number) => window.clearTimeout(id));93 vi.stubGlobal("URL", {94 createObjectURL: vi.fn(() => "blob:peach-test"),95 revokeObjectURL: vi.fn(),96 });97 vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {});9899 (globalThis as { chrome?: typeof chrome }).chrome = {100 runtime: {101 getManifest: () => ({ version: "1.4.47" }),102 onMessage: { addListener: vi.fn() },103 },104 } as unknown as typeof chrome;105 });106107 afterEach(() => {108 while (disposers.length > 0) {109 disposers.pop()?.();110 }111 vi.unstubAllGlobals();112 });113114 function mount(view: JSX.Element) {115 const mounted = renderView(view);116 disposers.push(mounted.dispose);117 return mounted.container;118 }119120 function sampleEntry(overrides: Partial<VaultEntry> = {}): VaultEntry {121 return {122 id: "entry-1",123 type: "login",124 title: "GitHub",125 username: "june",126 password: "secret123",127 url: "https://github.com",128 totp: "JBSWY3DPEHPK3PXP",129 canary: {130 webhookUrl: "https://example.com/hook",131 },132 createdAt: 1,133 modifiedAt: 1,134 ...overrides,135 };136 }137138 function sampleImportResult(139 overrides: Partial<ImportParseResult> = {}140 ): ImportParseResult {141 return {142 format: "csv",143 label: "CSV import",144 entries: [145 {146 type: "login",147 title: "Imported site",148 username: "imported-user",149 password: "imported-pass",150 url: "https://imported.test",151 },152 ],153 totalItems: 1,154 skippedItems: 0,155 requiresPassword: false,156 ...overrides,157 };158 }159160 async function selectImportFile(161 container: HTMLElement,162 contents = '{"export":"test"}'163 ) {164 const input = container.querySelector('input[type="file"]') as HTMLInputElement;165 const file = new File([contents], "import.json", { type: "application/json" });166 Object.defineProperty(input, "files", {167 configurable: true,168 value: [file],169 });170 input.dispatchEvent(new Event("change", { bubbles: true }));171 await flushView();172 await flushView();173 }174175 function findButton(container: HTMLElement, text: string): HTMLButtonElement {176 return Array.from(container.querySelectorAll("button")).find(177 (candidate) => candidate.textContent?.trim() === text178 ) as HTMLButtonElement;179 }180181 it("Lock sends UNLOCK with the entered passphrase", async () => {182 sendMessageMock.mockResolvedValue({ ok: true });183 const onUnlock = vi.fn(async () => undefined);184 const container = mount(<LockView hasVault={true} ready={true} onUnlock={onUnlock} />);185186 const password = container.querySelector("#unlock-password") as HTMLInputElement;187 inputText(password, "correct horse battery staple");188 (container.querySelector(".peach-lock-submit") as HTMLButtonElement).click();189 await flushView();190191 expect(sendMessageMock).toHaveBeenCalledWith({192 type: "UNLOCK",193 payload: { password: "correct horse battery staple" },194 });195 expect(onUnlock).toHaveBeenCalled();196 });197198 it("Lock keeps create-vault disabled for short passwords and weak passphrases", async () => {199 const container = mount(<LockView hasVault={false} ready={true} onUnlock={vi.fn()} />);200 const createButton = container.querySelector(".peach-lock-submit") as HTMLButtonElement;201 const password = container.querySelector("#master-password") as HTMLInputElement;202 const confirm = container.querySelector("#confirm-password") as HTMLInputElement;203204 inputText(password, "short");205 inputText(confirm, "short");206 await flushView();207 expect(createButton.disabled).toBe(true);208209 zxcvbnMock.mockImplementation(() => ({ score: 1 }));210 inputText(password, "weakweakweak");211 inputText(confirm, "weakweakweak");212 await flushView();213 expect(createButton.disabled).toBe(true);214 });215216 it("Lock sends CREATE_VAULT when the onboarding passphrase is strong enough", async () => {217 sendMessageMock.mockResolvedValue({ ok: true });218 zxcvbnMock.mockImplementation(() => ({ score: 3 }));219 const onUnlock = vi.fn(async () => undefined);220 const container = mount(<LockView hasVault={false} ready={true} onUnlock={onUnlock} />);221222 const password = container.querySelector("#master-password") as HTMLInputElement;223 const confirm = container.querySelector("#confirm-password") as HTMLInputElement;224 const createButton = container.querySelector(".peach-lock-submit") as HTMLButtonElement;225226 inputText(password, "correct horse battery staple");227 inputText(confirm, "correct horse battery staple");228 await flushView();229 expect(createButton.disabled).toBe(false);230231 pressEnter(confirm);232 await flushView();233234 expect(sendMessageMock).toHaveBeenCalledWith({235 type: "CREATE_VAULT",236 payload: { password: "correct horse battery staple" },237 });238 expect(onUnlock).toHaveBeenCalled();239 });240241 it("Export keeps encrypted export disabled until the password meets the minimum length", async () => {242 const container = mount(<ExportView onBack={vi.fn()} />);243244 const password = container.querySelector('input[placeholder*="backup password"]') as HTMLInputElement;245 const button = Array.from(container.querySelectorAll("button")).find(246 (candidate) => candidate.textContent?.trim() === "Download backup file"247 ) as HTMLButtonElement;248249 inputText(password, "too-short");250 await flushView();251252 expect(button.disabled).toBe(true);253 expect(exportEntriesToPeachJsonMock).not.toHaveBeenCalled();254 });255256 it("Export requests entries and sends the correct payload to the encrypted backup exporter", async () => {257 sendMessageMock.mockResolvedValue({258 ok: true,259 data: [sampleEntry(), sampleEntry({ id: "deleted", deletedAt: 2 })],260 });261 exportEntriesToPeachJsonMock.mockResolvedValue({262 version: 2,263 type: "peach-export",264 });265 const container = mount(<ExportView onBack={vi.fn()} />);266267 const password = container.querySelector('input[placeholder*="backup password"]') as HTMLInputElement;268 const button = Array.from(container.querySelectorAll("button")).find(269 (candidate) => candidate.textContent?.trim() === "Download backup file"270 ) as HTMLButtonElement;271272 inputText(password, "correct horse battery staple");273 await flushView();274 button.click();275 await flushView();276277 expect(sendMessageMock).toHaveBeenCalledWith({ type: "GET_ENTRIES" });278 expect(exportEntriesToPeachJsonMock).toHaveBeenCalledWith(279 [expect.objectContaining({ id: "entry-1" })],280 "correct horse battery staple"281 );282 });283284 it("Settings requires current, new, and confirm passwords before enabling save", async () => {285 sendMessageMock.mockResolvedValue({ ok: true, data: null });286 const container = mount(287 <SettingsView288 entriesCount={1}289 breachMonitoring={true}290 lockOnClose={true}291 autoLockMinutes={5}292 trashRetentionDays={30}293 theme="dark"294 onBreachMonitoringChange={vi.fn()}295 onLockOnCloseChange={vi.fn()}296 onAutoLockMinutesChange={vi.fn()}297 onTrashRetentionChange={vi.fn()}298 onThemeChange={vi.fn()}299 onBack={vi.fn()}300 onImport={vi.fn()}301 onExport={vi.fn()}302 onBreachCheckDone={vi.fn()}303 />304 );305 await flushView();306307 const changeButton = Array.from(container.querySelectorAll("button")).find(308 (candidate) => candidate.textContent?.trim() === "change"309 ) as HTMLButtonElement;310 changeButton.click();311 await flushView();312313 const saveButton = Array.from(container.querySelectorAll("button")).find(314 (candidate) => candidate.textContent?.trim() === "save new password"315 ) as HTMLButtonElement;316 expect(saveButton.disabled).toBe(true);317318 const inputs = container.querySelectorAll('input[type="password"]');319 inputText(inputs[0] as HTMLInputElement, "current-pass");320 inputText(inputs[1] as HTMLInputElement, "new-pass-12345");321 await flushView();322 expect(saveButton.disabled).toBe(true);323324 inputText(inputs[2] as HTMLInputElement, "new-pass-12345");325 await flushView();326 expect(saveButton.disabled).toBe(false);327 });328329 it("Settings sends CHANGE_PASSWORD and forwards auto-lock/theme changes through callbacks", async () => {330 sendMessageMock.mockResolvedValue({ ok: true, data: null });331 const onAutoLockMinutesChange = vi.fn();332 const onThemeChange = vi.fn();333 const container = mount(334 <SettingsView335 entriesCount={1}336 breachMonitoring={true}337 lockOnClose={true}338 autoLockMinutes={5}339 trashRetentionDays={30}340 theme="dark"341 onBreachMonitoringChange={vi.fn()}342 onLockOnCloseChange={vi.fn()}343 onAutoLockMinutesChange={onAutoLockMinutesChange}344 onTrashRetentionChange={vi.fn()}345 onThemeChange={onThemeChange}346 onBack={vi.fn()}347 onImport={vi.fn()}348 onExport={vi.fn()}349 onBreachCheckDone={vi.fn()}350 />351 );352 await flushView();353 sendMessageMock.mockClear();354355 const selects = container.querySelectorAll("select");356 changeSelect(selects[0] as HTMLSelectElement, "15");357 expect(onAutoLockMinutesChange).toHaveBeenCalledWith(15);358359 const themeButton = Array.from(container.querySelectorAll("button")).find(360 (candidate) => candidate.textContent?.trim() === "Switch to light"361 ) as HTMLButtonElement;362 themeButton.click();363 expect(onThemeChange).toHaveBeenCalledWith("light");364365 const changeButton = Array.from(container.querySelectorAll("button")).find(366 (candidate) => candidate.textContent?.trim() === "change"367 ) as HTMLButtonElement;368 changeButton.click();369 await flushView();370371 const inputs = container.querySelectorAll('input[type="password"]');372 inputText(inputs[0] as HTMLInputElement, "current-pass");373 inputText(inputs[1] as HTMLInputElement, "new-password-123");374 inputText(inputs[2] as HTMLInputElement, "new-password-123");375 await flushView();376377 const saveButton = Array.from(container.querySelectorAll("button")).find(378 (candidate) => candidate.textContent?.trim() === "save new password"379 ) as HTMLButtonElement;380 saveButton.click();381 await flushView();382383 expect(sendMessageMock).toHaveBeenCalledWith({384 type: "CHANGE_PASSWORD",385 payload: {386 currentPassword: "current-pass",387 newPassword: "new-password-123",388 },389 });390 });391392 it("Entry copy actions send COPY and canary ping messages for password and username", async () => {393 sendMessageMock.mockResolvedValue({ ok: true });394 const container = mount(395 <EntryView396 entry={sampleEntry()}397 onDelete={vi.fn()}398 onFieldSaved={vi.fn(async () => undefined)}399 />400 );401402 const passwordButton = container.querySelector('button[aria-label="Copy password"]') as HTMLButtonElement;403 passwordButton.click();404 await flushView();405406 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {407 type: "COPY",408 payload: { text: "secret123", label: "password" },409 });410 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {411 type: "FIRE_CANARY_PING",412 payload: {413 webhookUrl: "https://example.com/hook",414 entryTitle: "GitHub",415 trigger: "password_copied",416 },417 });418419 sendMessageMock.mockClear();420 const usernameButton = container.querySelector('button[aria-label="Copy username"]') as HTMLButtonElement;421 usernameButton.click();422 await flushView();423424 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {425 type: "COPY",426 payload: { text: "june", label: "username" },427 });428 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {429 type: "FIRE_CANARY_PING",430 payload: {431 webhookUrl: "https://example.com/hook",432 entryTitle: "GitHub",433 trigger: "username_copied",434 },435 });436 });437438 it("Entry refuses to send an empty password update and copies TOTP through COPY", async () => {439 sendMessageMock.mockResolvedValue({ ok: true });440 const onFieldSaved = vi.fn(async () => undefined);441 const container = mount(442 <EntryView443 entry={sampleEntry()}444 onDelete={vi.fn()}445 onFieldSaved={onFieldSaved}446 />447 );448449 const passwordValue = container.querySelector(".peach-password-value") as HTMLSpanElement;450 passwordValue.click();451 await flushView();452453 const passwordInput = container.querySelector('input[placeholder="paste or type a password"]') as HTMLInputElement;454 inputText(passwordInput, "");455 await flushView();456457 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;458 saveButton.click();459 await flushView();460461 expect(sendMessageMock).not.toHaveBeenCalledWith(462 expect.objectContaining({ type: "UPDATE_ENTRY" })463 );464 expect(onFieldSaved).not.toHaveBeenCalled();465466 const totpButton = container.querySelector('button[aria-label="Copy TOTP code"]') as HTMLButtonElement;467 totpButton.click();468 await flushView();469470 expect(sendMessageMock).toHaveBeenLastCalledWith({471 type: "COPY",472 payload: { text: "123456", label: "totp" },473 });474 });475476 it("Entry closes an unchanged inline editor when focus leaves it", async () => {477 sendMessageMock.mockResolvedValue({ ok: true });478 const container = mount(479 <EntryView480 entry={sampleEntry()}481 onDelete={vi.fn()}482 onFieldSaved={vi.fn(async () => undefined)}483 />484 );485486 const usernameValue = container.querySelector(".peach-field-value.peach-inline-clickable") as HTMLSpanElement;487 usernameValue.click();488 await flushView();489490 const usernameInput = container.querySelector(".peach-inline-input[autocomplete=\"off\"]") as HTMLInputElement;491 usernameInput.blur();492 await flushView();493494 expect(container.querySelector(".peach-inline-input[autocomplete=\"off\"]")).toBeNull();495 expect(sendMessageMock).not.toHaveBeenCalledWith(496 expect.objectContaining({ type: "UPDATE_ENTRY" })497 );498 });499500 it("Entry lets TOTP secrets be edited inline", async () => {501 sendMessageMock.mockResolvedValue({ ok: true });502 const onFieldSaved = vi.fn(async () => undefined);503 const container = mount(504 <EntryView505 entry={sampleEntry()}506 onDelete={vi.fn()}507 onFieldSaved={onFieldSaved}508 />509 );510511 const totpMain = container.querySelector(".peach-totp-main.peach-inline-clickable") as HTMLDivElement;512 totpMain.click();513 await flushView();514515 const totpInput = container.querySelector('input[placeholder="otpauth://... or base32 secret"]') as HTMLInputElement;516 inputText(totpInput, "otpauth://totp/Example?secret=NB2W45DFOIZA====&issuer=Peach");517 await flushView();518519 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;520 saveButton.click();521 await flushView();522523 expect(sendMessageMock).toHaveBeenCalledWith({524 type: "UPDATE_ENTRY",525 payload: {526 id: "entry-1",527 patch: { totp: "NB2W45DFOIZA====" },528 },529 });530 expect(onFieldSaved).toHaveBeenCalled();531 });532533 it("Vault seeds and focuses search when a search seed is provided", async () => {534 const container = mount(535 <VaultView536 entries={[sampleEntry(), sampleEntry({ id: "entry-2", title: "GitLab" })]}537 sortOrder="url-first"538 searchSeed="g"539 searchSeedNonce={1}540 onSelectEntry={vi.fn()}541 onSortOrderChange={vi.fn()}542 onAdd={vi.fn()}543 onClose={vi.fn()}544 />545 );546547 await flushView();548549 const search = container.querySelector(".peach-list-top .peach-search-input") as HTMLInputElement;550 expect(search.value).toBe("g");551 expect(document.activeElement).toBe(search);552 });553554 it("Import existing-vault add mode parses a file and batches entries into the vault", async () => {555 parseImportSourceMock.mockResolvedValue(556 sampleImportResult({557 entries: [558 {559 type: "login",560 title: "Imported site",561 username: "imported-user",562 password: "imported-pass",563 url: "https://imported.test",564 },565 ],566 })567 );568 sendMessageMock.mockImplementation(async (msg: unknown) => {569 const type = (msg as { type?: string }).type;570 if (type === "GET_ENTRIES") {571 return {572 ok: true,573 data: [574 sampleEntry({575 id: "existing-1",576 title: "Imported site",577 username: "imported-user",578 url: "https://imported.test",579 }),580 ],581 };582 }583 if (type === "BATCH_ADD_ENTRIES") {584 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };585 }586 return { ok: true };587 });588 const onImport = vi.fn(async () => undefined);589 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);590 await flushView();591 sendMessageMock.mockClear();592593 await selectImportFile(container);594 findButton(container, "Import 1").click();595 await flushView();596597 expect(parseImportSourceMock).toHaveBeenCalledWith('{"export":"test"}', undefined);598 expect(sendMessageMock).toHaveBeenCalledWith({599 type: "BATCH_ADD_ENTRIES",600 payload: {601 entries: [602 expect.objectContaining({603 title: "Imported site",604 username: "imported-user",605 password: "imported-pass",606 url: "https://imported.test",607 }),608 ],609 },610 });611 expect(onImport).toHaveBeenCalled();612 expect(container.textContent).toContain("1 entry appears to be duplicates");613 });614615 it("Import replace mode requires confirmation before clearing and replacing the vault", async () => {616 parseImportSourceMock.mockResolvedValue(sampleImportResult());617 sendMessageMock.mockImplementation(async (msg: unknown) => {618 const type = (msg as { type?: string }).type;619 if (type === "GET_ENTRIES") {620 return { ok: true, data: [sampleEntry({ id: "existing-1" })] };621 }622 if (type === "CLEAR_VAULT_ENTRIES") {623 return { ok: true };624 }625 if (type === "BATCH_ADD_ENTRIES") {626 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };627 }628 return { ok: true };629 });630 const onImport = vi.fn(async () => undefined);631 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);632 await flushView();633 sendMessageMock.mockClear();634635 await selectImportFile(container);636 (container.querySelector('input[value="replace"]') as HTMLInputElement).click();637 await flushView();638639 findButton(container, "Replace vault with 1").click();640 await flushView();641642 expect(sendMessageMock).not.toHaveBeenCalledWith(643 expect.objectContaining({ type: "CLEAR_VAULT_ENTRIES" })644 );645 expect(container.textContent).toContain("Replace forever? This cannot be undone.");646647 findButton(container, "Yes, replace forever").click();648 await flushView();649650 expect(sendMessageMock).toHaveBeenCalledWith({ type: "CLEAR_VAULT_ENTRIES" });651 expect(sendMessageMock).toHaveBeenCalledWith({652 type: "BATCH_ADD_ENTRIES",653 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },654 });655 expect(onImport).toHaveBeenCalled();656 });657658 it("Import onboarding mode waits for a matching master password before creating a vault", async () => {659 parseImportSourceMock.mockResolvedValue(sampleImportResult());660 sendMessageMock.mockImplementation(async (msg: unknown) => {661 const type = (msg as { type?: string }).type;662 if (type === "CREATE_VAULT") return { ok: true };663 if (type === "BATCH_ADD_ENTRIES") {664 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };665 }666 return { ok: true };667 });668 const onImport = vi.fn(async () => undefined);669 const container = mount(670 <ImportView671 mode="onboarding"672 replacesExistingVault={true}673 onBack={vi.fn()}674 onImport={onImport}675 />676 );677678 await selectImportFile(container);679680 const createButton = findButton(container, "Create vault + import 1");681 expect(createButton.disabled).toBe(true);682 expect(container.textContent).toContain("Restoring now replaces the locked local vault");683684 const inputs = container.querySelectorAll('input[type="password"]');685 inputText(inputs[0] as HTMLInputElement, "fresh-master-password");686 inputText(inputs[1] as HTMLInputElement, "fresh-master-password");687 await flushView();688689 expect(createButton.disabled).toBe(false);690 createButton.click();691 await flushView();692693 expect(sendMessageMock).toHaveBeenCalledWith({694 type: "CREATE_VAULT",695 payload: { password: "fresh-master-password" },696 });697 expect(sendMessageMock).toHaveBeenCalledWith({698 type: "BATCH_ADD_ENTRIES",699 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },700 });701 expect(onImport).toHaveBeenCalled();702 });703704 it("Import encrypted backups reparse with the backup password before import", async () => {705 parseImportSourceMock706 .mockResolvedValueOnce(707 sampleImportResult({708 format: "peach-json",709 label: "Peach encrypted backup",710 entries: [],711 totalItems: 0,712 requiresPassword: true,713 })714 )715 .mockResolvedValueOnce(716 sampleImportResult({717 format: "peach-json",718 label: "Peach encrypted backup",719 })720 );721 sendMessageMock.mockImplementation(async (msg: unknown) => {722 const type = (msg as { type?: string }).type;723 if (type === "GET_ENTRIES") return { ok: true, data: [] };724 if (type === "BATCH_ADD_ENTRIES") {725 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };726 }727 return { ok: true };728 });729 const onImport = vi.fn(async () => undefined);730 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);731 await flushView();732 sendMessageMock.mockClear();733734 await selectImportFile(container, '{"type":"peach-export"}');735 const password = container.querySelector('input[placeholder="backup password"]') as HTMLInputElement;736 inputText(password, "backup-passphrase");737 pressEnter(password);738 await flushView();739 await flushView();740741 expect(parseImportSourceMock).toHaveBeenNthCalledWith(1, '{"type":"peach-export"}', undefined);742 expect(parseImportSourceMock).toHaveBeenNthCalledWith(2, '{"type":"peach-export"}', "backup-passphrase");743744 findButton(container, "Import 1").click();745 await flushView();746747 expect(sendMessageMock).toHaveBeenCalledWith({748 type: "BATCH_ADD_ENTRIES",749 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },750 });751 expect(onImport).toHaveBeenCalled();752 });753});