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 parseImportSourceMock = vi.fn<(raw: string, password?: string) => Promise<ImportParseResult>>();21const zxcvbnMock = vi.fn((value: string) => ({ score: value.length >= 16 ? 3 : 1 }));2223vi.mock("../App", () => ({24 sendMessage: (msg: unknown) => sendMessageMock(msg),25 triggerRing: () => triggerRingMock(),26 triggerPipPulse: () => triggerPipPulseMock(),27}));2829vi.mock("../../shared/import", () => ({30 parseImportSource: (...args: [string, string | undefined]) => parseImportSourceMock(...args),31}));3233vi.mock("../../shared/favicon", () => ({34 faviconUrl: () => null,35}));3637vi.mock("../../crypto/totp", () => ({38 generateTOTP: () => ({ code: "123 456", remaining: 24 }),39 parseOtpauthURI: (value: string) =>40 value.startsWith("otpauth://")41 ? { secret: "NB2W45DFOIZA====", algorithm: "SHA1" as const }42 : null,43}));4445vi.mock("../components/GeneratorDrawer", () => ({46 GeneratorDrawer: () => null,47}));4849vi.mock("./CJKImport", () => ({50 CJKImportView: () => null,51}));5253vi.mock("zxcvbn", () => ({54 default: (value: string) => zxcvbnMock(value),55}));5657describe("popup contract tests", () => {58 const disposers: Array<() => void> = [];5960 beforeEach(() => {61 document.body.innerHTML = "";62 sendMessageMock.mockReset();63 triggerRingMock.mockReset();64 triggerPipPulseMock.mockReset();65 parseImportSourceMock.mockReset();66 zxcvbnMock.mockReset();67 zxcvbnMock.mockImplementation((value: string) => ({ score: value.length >= 16 ? 3 : 1 }));68 parseImportSourceMock.mockResolvedValue({69 format: "unknown",70 label: "Unknown format",71 entries: [],72 totalItems: 0,73 skippedItems: 0,74 requiresPassword: false,75 });7677 vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {78 return window.setTimeout(() => cb(0), 0) as unknown as number;79 });80 vi.stubGlobal("cancelAnimationFrame", (id: number) => window.clearTimeout(id));81 vi.stubGlobal("URL", {82 createObjectURL: vi.fn(() => "blob:peach-test"),83 revokeObjectURL: vi.fn(),84 });85 vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {});8687 (globalThis as { chrome?: typeof chrome }).chrome = {88 runtime: {89 getManifest: () => ({ version: "1.4.47" }),90 onMessage: { addListener: vi.fn() },91 },92 } as unknown as typeof chrome;93 });9495 afterEach(() => {96 while (disposers.length > 0) {97 disposers.pop()?.();98 }99 vi.unstubAllGlobals();100 });101102 function mount(view: JSX.Element) {103 const mounted = renderView(view);104 disposers.push(mounted.dispose);105 return mounted.container;106 }107108 function sampleEntry(overrides: Partial<VaultEntry> = {}): VaultEntry {109 return {110 id: "entry-1",111 type: "login",112 title: "GitHub",113 username: "june",114 password: "secret123",115 url: "https://github.com",116 totp: "JBSWY3DPEHPK3PXP",117 canary: {118 webhookUrl: "https://example.com/hook",119 },120 createdAt: 1,121 modifiedAt: 1,122 ...overrides,123 };124 }125126 function sampleImportResult(127 overrides: Partial<ImportParseResult> = {}128 ): ImportParseResult {129 return {130 format: "csv",131 label: "CSV import",132 entries: [133 {134 type: "login",135 title: "Imported site",136 username: "imported-user",137 password: "imported-pass",138 url: "https://imported.test",139 },140 ],141 totalItems: 1,142 skippedItems: 0,143 requiresPassword: false,144 ...overrides,145 };146 }147148 async function selectImportFile(149 container: HTMLElement,150 contents = '{"export":"test"}'151 ) {152 const input = container.querySelector('input[type="file"]') as HTMLInputElement;153 const file = new File([contents], "import.json", { type: "application/json" });154 Object.defineProperty(input, "files", {155 configurable: true,156 value: [file],157 });158 input.dispatchEvent(new Event("change", { bubbles: true }));159 await flushView();160 await flushView();161 }162163 function findButton(container: HTMLElement, text: string): HTMLButtonElement {164 return Array.from(container.querySelectorAll("button")).find(165 (candidate) => candidate.textContent?.trim() === text166 ) as HTMLButtonElement;167 }168169 it("Lock sends UNLOCK with the entered passphrase", async () => {170 sendMessageMock.mockResolvedValue({ ok: true });171 const onUnlock = vi.fn(async () => undefined);172 const container = mount(<LockView hasVault={true} ready={true} onUnlock={onUnlock} />);173174 const password = container.querySelector("#unlock-password") as HTMLInputElement;175 inputText(password, "correct horse battery staple");176 (container.querySelector(".peach-lock-submit") as HTMLButtonElement).click();177 await flushView();178179 expect(sendMessageMock).toHaveBeenCalledWith({180 type: "UNLOCK",181 payload: { password: "correct horse battery staple" },182 });183 expect(onUnlock).toHaveBeenCalled();184 });185186 it("Lock keeps create-vault disabled for short passwords and weak passphrases", async () => {187 const container = mount(<LockView hasVault={false} ready={true} onUnlock={vi.fn()} />);188 const createButton = container.querySelector(".peach-lock-submit") as HTMLButtonElement;189 const password = container.querySelector("#master-password") as HTMLInputElement;190 const confirm = container.querySelector("#confirm-password") as HTMLInputElement;191192 inputText(password, "short");193 inputText(confirm, "short");194 await flushView();195 expect(createButton.disabled).toBe(true);196197 zxcvbnMock.mockImplementation(() => ({ score: 1 }));198 inputText(password, "weakweakweak");199 inputText(confirm, "weakweakweak");200 await flushView();201 expect(createButton.disabled).toBe(true);202 });203204 it("Lock sends CREATE_VAULT when the onboarding passphrase is strong enough", async () => {205 sendMessageMock.mockResolvedValue({ ok: true });206 zxcvbnMock.mockImplementation(() => ({ score: 3 }));207 const onUnlock = vi.fn(async () => undefined);208 const container = mount(<LockView hasVault={false} ready={true} onUnlock={onUnlock} />);209210 const password = container.querySelector("#master-password") as HTMLInputElement;211 const confirm = container.querySelector("#confirm-password") as HTMLInputElement;212 const createButton = container.querySelector(".peach-lock-submit") as HTMLButtonElement;213214 inputText(password, "correct horse battery staple");215 inputText(confirm, "correct horse battery staple");216 await flushView();217 expect(createButton.disabled).toBe(false);218219 pressEnter(confirm);220 await flushView();221222 expect(sendMessageMock).toHaveBeenCalledWith({223 type: "CREATE_VAULT",224 payload: { password: "correct horse battery staple" },225 });226 expect(onUnlock).toHaveBeenCalled();227 });228229 it("Export menu surfaces the three v2.5.0 backup formats", async () => {230 const container = mount(<ExportView onBack={vi.fn()} />);231 await flushView();232233 expect(container.textContent).toContain("PeachScript");234 expect(container.textContent).toContain("print backup");235 expect(container.textContent).toContain("CSV");236 });237238 it("Settings requires current, new, and confirm passwords before enabling save", async () => {239 sendMessageMock.mockResolvedValue({ ok: true, data: null });240 const container = mount(241 <SettingsView242 entriesCount={1}243 breachMonitoring={true}244 lockOnClose={true}245 autoLockMinutes={5}246 trashRetentionDays={30}247 theme="dark"248 accentColor="peach"249 displayFont="cormorant-italic"250 textSize="medium"251 reduceMotion={false}252 onBreachMonitoringChange={vi.fn()}253 onLockOnCloseChange={vi.fn()}254 onAutoLockMinutesChange={vi.fn()}255 onTrashRetentionChange={vi.fn()}256 onThemeChange={vi.fn()}257 onAccentChange={vi.fn()}258 onDisplayFontChange={vi.fn()}259 onTextSizeChange={vi.fn()}260 onReduceMotionChange={vi.fn()}261 shortcuts={DEFAULT_BINDINGS}262 onShortcutsChange={vi.fn()}263 onBack={vi.fn()}264 onImport={vi.fn()}265 onExport={vi.fn()}266 onBreachCheckDone={vi.fn()}267 />268 );269 await flushView();270271 const securityRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(272 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Security"273 ) as HTMLButtonElement;274 securityRow.click();275 await flushView();276277 const changeButton = Array.from(container.querySelectorAll("button")).find(278 (candidate) => candidate.textContent?.trim() === "change"279 ) as HTMLButtonElement;280 changeButton.click();281 await flushView();282283 const saveButton = Array.from(container.querySelectorAll("button")).find(284 (candidate) => candidate.textContent?.trim() === "save new password"285 ) as HTMLButtonElement;286 expect(saveButton.disabled).toBe(true);287288 const inputs = container.querySelectorAll('input[type="password"]');289 inputText(inputs[0] as HTMLInputElement, "current-pass");290 inputText(inputs[1] as HTMLInputElement, "new-pass-12345");291 await flushView();292 expect(saveButton.disabled).toBe(true);293294 inputText(inputs[2] as HTMLInputElement, "new-pass-12345");295 await flushView();296 expect(saveButton.disabled).toBe(false);297 });298299 it("Settings sends CHANGE_PASSWORD and forwards auto-lock/theme changes through callbacks", async () => {300 sendMessageMock.mockResolvedValue({ ok: true, data: null });301 const onAutoLockMinutesChange = vi.fn();302 const onThemeChange = vi.fn();303 const container = mount(304 <SettingsView305 entriesCount={1}306 breachMonitoring={true}307 lockOnClose={true}308 autoLockMinutes={5}309 trashRetentionDays={30}310 theme="dark"311 accentColor="peach"312 displayFont="cormorant-italic"313 textSize="medium"314 reduceMotion={false}315 onBreachMonitoringChange={vi.fn()}316 onLockOnCloseChange={vi.fn()}317 onAutoLockMinutesChange={onAutoLockMinutesChange}318 onTrashRetentionChange={vi.fn()}319 onThemeChange={onThemeChange}320 onAccentChange={vi.fn()}321 onDisplayFontChange={vi.fn()}322 onTextSizeChange={vi.fn()}323 onReduceMotionChange={vi.fn()}324 shortcuts={DEFAULT_BINDINGS}325 onShortcutsChange={vi.fn()}326 onBack={vi.fn()}327 onImport={vi.fn()}328 onExport={vi.fn()}329 onBreachCheckDone={vi.fn()}330 />331 );332 await flushView();333 sendMessageMock.mockClear();334335 const headerBack = container.querySelector(".peach-overlay-header button") as HTMLButtonElement;336337 const securityRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(338 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Security"339 ) as HTMLButtonElement;340 securityRow.click();341 await flushView();342343 const selects = container.querySelectorAll("select");344 changeSelect(selects[0] as HTMLSelectElement, "15");345 expect(onAutoLockMinutesChange).toHaveBeenCalledWith(15);346347 const changeButton = Array.from(container.querySelectorAll("button")).find(348 (candidate) => candidate.textContent?.trim() === "change"349 ) as HTMLButtonElement;350 changeButton.click();351 await flushView();352353 const inputs = container.querySelectorAll('input[type="password"]');354 inputText(inputs[0] as HTMLInputElement, "current-pass");355 inputText(inputs[1] as HTMLInputElement, "new-password-123");356 inputText(inputs[2] as HTMLInputElement, "new-password-123");357 await flushView();358359 const saveButton = Array.from(container.querySelectorAll("button")).find(360 (candidate) => candidate.textContent?.trim() === "save new password"361 ) as HTMLButtonElement;362 saveButton.click();363 await flushView();364365 headerBack.click();366 await flushView();367 const appearanceRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(368 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Appearance"369 ) as HTMLButtonElement;370 appearanceRow.click();371 await flushView();372 const themeButton = Array.from(container.querySelectorAll("button")).find(373 (candidate) => candidate.textContent?.trim() === "Switch to light"374 ) as HTMLButtonElement;375 themeButton.click();376 expect(onThemeChange).toHaveBeenCalledWith("light");377378 expect(sendMessageMock).toHaveBeenCalledWith({379 type: "CHANGE_PASSWORD",380 payload: {381 currentPassword: "current-pass",382 newPassword: "new-password-123",383 },384 });385 });386387 it("Entry copy actions send COPY and canary ping messages for password and username", async () => {388 sendMessageMock.mockResolvedValue({ ok: true });389 const container = mount(390 <EntryView391 entry={sampleEntry()}392 onDelete={vi.fn()}393 onFieldSaved={vi.fn(async () => undefined)}394 />395 );396397 const passwordButton = container.querySelector('button[aria-label="Copy password"]') as HTMLButtonElement;398 passwordButton.click();399 await flushView();400401 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {402 type: "COPY",403 payload: { text: "secret123", label: "password", alreadyWritten: false },404 });405 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {406 type: "FIRE_CANARY_PING",407 payload: {408 webhookUrl: "https://example.com/hook",409 entryTitle: "GitHub",410 trigger: "password_copied",411 },412 });413414 sendMessageMock.mockClear();415 const usernameButton = container.querySelector('button[aria-label="Copy username"]') as HTMLButtonElement;416 usernameButton.click();417 await flushView();418419 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {420 type: "COPY",421 payload: { text: "june", label: "username", alreadyWritten: false },422 });423 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {424 type: "FIRE_CANARY_PING",425 payload: {426 webhookUrl: "https://example.com/hook",427 entryTitle: "GitHub",428 trigger: "username_copied",429 },430 });431 });432433 it("Entry refuses to send an empty password update and copies TOTP through COPY", async () => {434 sendMessageMock.mockResolvedValue({ ok: true });435 const onFieldSaved = vi.fn(async () => undefined);436 const container = mount(437 <EntryView438 entry={sampleEntry()}439 onDelete={vi.fn()}440 onFieldSaved={onFieldSaved}441 />442 );443444 const passwordValue = container.querySelector(".peach-password-value") as HTMLSpanElement;445 passwordValue.click();446 await flushView();447448 const passwordInput = container.querySelector('input[placeholder="paste or type a password"]') as HTMLInputElement;449 inputText(passwordInput, "");450 await flushView();451452 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;453 saveButton.click();454 await flushView();455456 expect(sendMessageMock).not.toHaveBeenCalledWith(457 expect.objectContaining({ type: "UPDATE_ENTRY" })458 );459 expect(onFieldSaved).not.toHaveBeenCalled();460461 const totpMain = container.querySelector(".peach-totp-main") as HTMLDivElement;462 totpMain.click();463 await flushView();464465 expect(sendMessageMock).toHaveBeenLastCalledWith({466 type: "COPY",467 payload: { text: "123456", label: "totp", alreadyWritten: false },468 });469 });470471 it("Entry closes an unchanged inline editor when focus leaves it", async () => {472 sendMessageMock.mockResolvedValue({ ok: true });473 const container = mount(474 <EntryView475 entry={sampleEntry()}476 onDelete={vi.fn()}477 onFieldSaved={vi.fn(async () => undefined)}478 />479 );480481 const usernameValue = container.querySelector(".peach-field-value.peach-inline-clickable") as HTMLSpanElement;482 usernameValue.click();483 await flushView();484485 const usernameInput = container.querySelector(".peach-inline-input[autocomplete=\"off\"]") as HTMLInputElement;486 usernameInput.blur();487 await flushView();488489 expect(container.querySelector(".peach-inline-input[autocomplete=\"off\"]")).toBeNull();490 expect(sendMessageMock).not.toHaveBeenCalledWith(491 expect.objectContaining({ type: "UPDATE_ENTRY" })492 );493 });494495 it("Entry lets TOTP secrets be edited inline", async () => {496 sendMessageMock.mockResolvedValue({ ok: true });497 const onFieldSaved = vi.fn(async () => undefined);498 const container = mount(499 <EntryView500 entry={sampleEntry()}501 onDelete={vi.fn()}502 onFieldSaved={onFieldSaved}503 />504 );505506 const totpMain = container.querySelector(".peach-totp-main") as HTMLDivElement;507 totpMain.dispatchEvent(new MouseEvent("dblclick", { bubbles: true }));508 await flushView();509510 const totpInput = container.querySelector('input[placeholder="otpauth://... or base32 secret"]') as HTMLInputElement;511 inputText(totpInput, "otpauth://totp/Example?secret=NB2W45DFOIZA====&issuer=Peach");512 await flushView();513514 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;515 saveButton.click();516 await flushView();517518 expect(sendMessageMock).toHaveBeenCalledWith({519 type: "UPDATE_ENTRY",520 payload: {521 id: "entry-1",522 patch: { totp: "NB2W45DFOIZA====" },523 },524 });525 expect(onFieldSaved).toHaveBeenCalled();526 });527528 it("Vault seeds and focuses search when a search seed is provided", async () => {529 const container = mount(530 <VaultView531 entries={[sampleEntry(), sampleEntry({ id: "entry-2", title: "GitLab" })]}532 sortOrder="url-first"533 searchSeed="g"534 searchSeedNonce={1}535 onSelectEntry={vi.fn()}536 onSortOrderChange={vi.fn()}537 onAdd={vi.fn()}538 onClose={vi.fn()}539 />540 );541542 await flushView();543544 const search = container.querySelector(".peach-list-top .peach-search-input") as HTMLInputElement;545 expect(search.value).toBe("g");546 expect(document.activeElement).toBe(search);547 });548549 it("Import existing-vault add mode parses a file and batches entries into the vault", async () => {550 parseImportSourceMock.mockResolvedValue(551 sampleImportResult({552 entries: [553 {554 type: "login",555 title: "Imported site",556 username: "imported-user",557 password: "imported-pass",558 url: "https://imported.test",559 },560 ],561 })562 );563 sendMessageMock.mockImplementation(async (msg: unknown) => {564 const type = (msg as { type?: string }).type;565 if (type === "GET_ENTRIES") {566 return {567 ok: true,568 data: [569 sampleEntry({570 id: "existing-1",571 title: "Imported site",572 username: "imported-user",573 url: "https://imported.test",574 }),575 ],576 };577 }578 if (type === "BATCH_ADD_ENTRIES") {579 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };580 }581 return { ok: true };582 });583 const onImport = vi.fn(async () => undefined);584 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);585 await flushView();586 sendMessageMock.mockClear();587588 await selectImportFile(container);589 findButton(container, "Import 1").click();590 await flushView();591592 expect(parseImportSourceMock).toHaveBeenCalledWith('{"export":"test"}', undefined);593 expect(sendMessageMock).toHaveBeenCalledWith({594 type: "BATCH_ADD_ENTRIES",595 payload: {596 entries: [597 expect.objectContaining({598 title: "Imported site",599 username: "imported-user",600 password: "imported-pass",601 url: "https://imported.test",602 }),603 ],604 },605 });606 expect(onImport).toHaveBeenCalled();607 expect(container.textContent).toContain("1 entry appears to be duplicates");608 });609610 it("Import replace mode requires confirmation before clearing and replacing the vault", async () => {611 parseImportSourceMock.mockResolvedValue(sampleImportResult());612 sendMessageMock.mockImplementation(async (msg: unknown) => {613 const type = (msg as { type?: string }).type;614 if (type === "GET_ENTRIES") {615 return { ok: true, data: [sampleEntry({ id: "existing-1" })] };616 }617 if (type === "CLEAR_VAULT_ENTRIES") {618 return { ok: true };619 }620 if (type === "BATCH_ADD_ENTRIES") {621 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };622 }623 return { ok: true };624 });625 const onImport = vi.fn(async () => undefined);626 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);627 await flushView();628 sendMessageMock.mockClear();629630 await selectImportFile(container);631 (container.querySelector('input[value="replace"]') as HTMLInputElement).click();632 await flushView();633634 findButton(container, "Replace vault with 1").click();635 await flushView();636637 expect(sendMessageMock).not.toHaveBeenCalledWith(638 expect.objectContaining({ type: "CLEAR_VAULT_ENTRIES" })639 );640 expect(container.textContent).toContain("Replace forever? This cannot be undone.");641642 findButton(container, "Yes, replace forever").click();643 await flushView();644645 expect(sendMessageMock).toHaveBeenCalledWith({ type: "CLEAR_VAULT_ENTRIES" });646 expect(sendMessageMock).toHaveBeenCalledWith({647 type: "BATCH_ADD_ENTRIES",648 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },649 });650 expect(onImport).toHaveBeenCalled();651 });652653 it("Import onboarding mode waits for a matching master password before creating a vault", async () => {654 parseImportSourceMock.mockResolvedValue(sampleImportResult());655 sendMessageMock.mockImplementation(async (msg: unknown) => {656 const type = (msg as { type?: string }).type;657 if (type === "CREATE_VAULT") return { ok: true };658 if (type === "BATCH_ADD_ENTRIES") {659 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };660 }661 return { ok: true };662 });663 const onImport = vi.fn(async () => undefined);664 const container = mount(665 <ImportView666 mode="onboarding"667 replacesExistingVault={true}668 onBack={vi.fn()}669 onImport={onImport}670 />671 );672673 await selectImportFile(container);674675 const createButton = findButton(container, "Create vault + import 1");676 expect(createButton.disabled).toBe(true);677 expect(container.textContent).toContain("Restoring now replaces the locked local vault");678679 const masterInput = container.querySelector(680 'input[placeholder="new master password"]'681 ) as HTMLInputElement;682 const confirmInput = container.querySelector(683 'input[placeholder="confirm master password"]'684 ) as HTMLInputElement;685 inputText(masterInput, "fresh-master-password");686 inputText(confirmInput, "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});