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 triggerPipPulseMock = vi.fn();19const exportEntriesToPeachJsonMock = 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/backup", () => ({30 exportEntriesToPeachJson: (...args: unknown[]) => exportEntriesToPeachJsonMock(...args),31}));3233vi.mock("../../shared/import", () => ({34 parseImportSource: (...args: [string, string | undefined]) => parseImportSourceMock(...args),35}));3637vi.mock("../../shared/favicon", () => ({38 faviconUrl: () => null,39}));4041vi.mock("../../crypto/totp", () => ({42 generateTOTP: () => ({ code: "123 456", remaining: 24 }),43 parseOtpauthURI: (value: string) =>44 value.startsWith("otpauth://")45 ? { secret: "NB2W45DFOIZA====", algorithm: "SHA1" as const }46 : null,47}));4849vi.mock("../components/GeneratorDrawer", () => ({50 GeneratorDrawer: () => null,51}));5253vi.mock("./CJKImport", () => ({54 CJKImportView: () => null,55}));5657vi.mock("./CJKExport", () => ({58 CJKExportView: () => null,59}));6061vi.mock("zxcvbn", () => ({62 default: (value: string) => zxcvbnMock(value),63}));6465describe("popup contract tests", () => {66 const disposers: Array<() => void> = [];6768 beforeEach(() => {69 document.body.innerHTML = "";70 sendMessageMock.mockReset();71 triggerRingMock.mockReset();72 triggerPipPulseMock.mockReset();73 exportEntriesToPeachJsonMock.mockReset();74 parseImportSourceMock.mockReset();75 zxcvbnMock.mockReset();76 zxcvbnMock.mockImplementation((value: string) => ({ score: value.length >= 16 ? 3 : 1 }));77 parseImportSourceMock.mockResolvedValue({78 format: "unknown",79 label: "Unknown format",80 entries: [],81 totalItems: 0,82 skippedItems: 0,83 requiresPassword: false,84 });8586 vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {87 return window.setTimeout(() => cb(0), 0) as unknown as number;88 });89 vi.stubGlobal("cancelAnimationFrame", (id: number) => window.clearTimeout(id));90 vi.stubGlobal("URL", {91 createObjectURL: vi.fn(() => "blob:peach-test"),92 revokeObjectURL: vi.fn(),93 });94 vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {});9596 (globalThis as { chrome?: typeof chrome }).chrome = {97 runtime: {98 getManifest: () => ({ version: "1.4.47" }),99 onMessage: { addListener: vi.fn() },100 },101 } as unknown as typeof chrome;102 });103104 afterEach(() => {105 while (disposers.length > 0) {106 disposers.pop()?.();107 }108 vi.unstubAllGlobals();109 });110111 function mount(view: JSX.Element) {112 const mounted = renderView(view);113 disposers.push(mounted.dispose);114 return mounted.container;115 }116117 function sampleEntry(overrides: Partial<VaultEntry> = {}): VaultEntry {118 return {119 id: "entry-1",120 type: "login",121 title: "GitHub",122 username: "june",123 password: "secret123",124 url: "https://github.com",125 totp: "JBSWY3DPEHPK3PXP",126 canary: {127 webhookUrl: "https://example.com/hook",128 },129 createdAt: 1,130 modifiedAt: 1,131 ...overrides,132 };133 }134135 function sampleImportResult(136 overrides: Partial<ImportParseResult> = {}137 ): ImportParseResult {138 return {139 format: "csv",140 label: "CSV import",141 entries: [142 {143 type: "login",144 title: "Imported site",145 username: "imported-user",146 password: "imported-pass",147 url: "https://imported.test",148 },149 ],150 totalItems: 1,151 skippedItems: 0,152 requiresPassword: false,153 ...overrides,154 };155 }156157 async function selectImportFile(158 container: HTMLElement,159 contents = '{"export":"test"}'160 ) {161 const input = container.querySelector('input[type="file"]') as HTMLInputElement;162 const file = new File([contents], "import.json", { type: "application/json" });163 Object.defineProperty(input, "files", {164 configurable: true,165 value: [file],166 });167 input.dispatchEvent(new Event("change", { bubbles: true }));168 await flushView();169 await flushView();170 }171172 function findButton(container: HTMLElement, text: string): HTMLButtonElement {173 return Array.from(container.querySelectorAll("button")).find(174 (candidate) => candidate.textContent?.trim() === text175 ) as HTMLButtonElement;176 }177178 it("Lock sends UNLOCK with the entered passphrase", async () => {179 sendMessageMock.mockResolvedValue({ ok: true });180 const onUnlock = vi.fn(async () => undefined);181 const container = mount(<LockView hasVault={true} ready={true} onUnlock={onUnlock} />);182183 const password = container.querySelector("#unlock-password") as HTMLInputElement;184 inputText(password, "correct horse battery staple");185 (container.querySelector(".peach-lock-submit") as HTMLButtonElement).click();186 await flushView();187188 expect(sendMessageMock).toHaveBeenCalledWith({189 type: "UNLOCK",190 payload: { password: "correct horse battery staple" },191 });192 expect(onUnlock).toHaveBeenCalled();193 });194195 it("Lock keeps create-vault disabled for short passwords and weak passphrases", async () => {196 const container = mount(<LockView hasVault={false} ready={true} onUnlock={vi.fn()} />);197 const createButton = container.querySelector(".peach-lock-submit") as HTMLButtonElement;198 const password = container.querySelector("#master-password") as HTMLInputElement;199 const confirm = container.querySelector("#confirm-password") as HTMLInputElement;200201 inputText(password, "short");202 inputText(confirm, "short");203 await flushView();204 expect(createButton.disabled).toBe(true);205206 zxcvbnMock.mockImplementation(() => ({ score: 1 }));207 inputText(password, "weakweakweak");208 inputText(confirm, "weakweakweak");209 await flushView();210 expect(createButton.disabled).toBe(true);211 });212213 it("Lock sends CREATE_VAULT when the onboarding passphrase is strong enough", async () => {214 sendMessageMock.mockResolvedValue({ ok: true });215 zxcvbnMock.mockImplementation(() => ({ score: 3 }));216 const onUnlock = vi.fn(async () => undefined);217 const container = mount(<LockView hasVault={false} ready={true} onUnlock={onUnlock} />);218219 const password = container.querySelector("#master-password") as HTMLInputElement;220 const confirm = container.querySelector("#confirm-password") as HTMLInputElement;221 const createButton = container.querySelector(".peach-lock-submit") as HTMLButtonElement;222223 inputText(password, "correct horse battery staple");224 inputText(confirm, "correct horse battery staple");225 await flushView();226 expect(createButton.disabled).toBe(false);227228 pressEnter(confirm);229 await flushView();230231 expect(sendMessageMock).toHaveBeenCalledWith({232 type: "CREATE_VAULT",233 payload: { password: "correct horse battery staple" },234 });235 expect(onUnlock).toHaveBeenCalled();236 });237238 it("Export keeps encrypted export disabled until the password meets the minimum length", async () => {239 const container = mount(<ExportView onBack={vi.fn()} />);240241 const password = container.querySelector('input[placeholder*="backup password"]') as HTMLInputElement;242 const button = Array.from(container.querySelectorAll("button")).find(243 (candidate) => candidate.textContent?.trim() === "Download backup file"244 ) as HTMLButtonElement;245246 inputText(password, "too-short");247 await flushView();248249 expect(button.disabled).toBe(true);250 expect(exportEntriesToPeachJsonMock).not.toHaveBeenCalled();251 });252253 it("Export requests entries and sends the correct payload to the encrypted backup exporter", async () => {254 sendMessageMock.mockResolvedValue({255 ok: true,256 data: [sampleEntry(), sampleEntry({ id: "deleted", deletedAt: 2 })],257 });258 exportEntriesToPeachJsonMock.mockResolvedValue({259 version: 2,260 type: "peach-export",261 });262 const container = mount(<ExportView onBack={vi.fn()} />);263264 const password = container.querySelector('input[placeholder*="backup password"]') as HTMLInputElement;265 const button = Array.from(container.querySelectorAll("button")).find(266 (candidate) => candidate.textContent?.trim() === "Download backup file"267 ) as HTMLButtonElement;268269 inputText(password, "correct horse battery staple");270 await flushView();271 button.click();272 await flushView();273274 expect(sendMessageMock).toHaveBeenCalledWith({ type: "GET_ENTRIES" });275 expect(exportEntriesToPeachJsonMock).toHaveBeenCalledWith(276 [277 expect.objectContaining({ id: "entry-1" }),278 expect.objectContaining({ id: "deleted", deletedAt: 2 }),279 ],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 accentColor="peach"295 onBreachMonitoringChange={vi.fn()}296 onLockOnCloseChange={vi.fn()}297 onAutoLockMinutesChange={vi.fn()}298 onTrashRetentionChange={vi.fn()}299 onThemeChange={vi.fn()}300 onAccentChange={vi.fn()}301 onBack={vi.fn()}302 onImport={vi.fn()}303 onExport={vi.fn()}304 onBreachCheckDone={vi.fn()}305 />306 );307 await flushView();308309 const changeButton = Array.from(container.querySelectorAll("button")).find(310 (candidate) => candidate.textContent?.trim() === "change"311 ) as HTMLButtonElement;312 changeButton.click();313 await flushView();314315 const saveButton = Array.from(container.querySelectorAll("button")).find(316 (candidate) => candidate.textContent?.trim() === "save new password"317 ) as HTMLButtonElement;318 expect(saveButton.disabled).toBe(true);319320 const inputs = container.querySelectorAll('input[type="password"]');321 inputText(inputs[0] as HTMLInputElement, "current-pass");322 inputText(inputs[1] as HTMLInputElement, "new-pass-12345");323 await flushView();324 expect(saveButton.disabled).toBe(true);325326 inputText(inputs[2] as HTMLInputElement, "new-pass-12345");327 await flushView();328 expect(saveButton.disabled).toBe(false);329 });330331 it("Settings sends CHANGE_PASSWORD and forwards auto-lock/theme changes through callbacks", async () => {332 sendMessageMock.mockResolvedValue({ ok: true, data: null });333 const onAutoLockMinutesChange = vi.fn();334 const onThemeChange = vi.fn();335 const container = mount(336 <SettingsView337 entriesCount={1}338 breachMonitoring={true}339 lockOnClose={true}340 autoLockMinutes={5}341 trashRetentionDays={30}342 theme="dark"343 accentColor="peach"344 onBreachMonitoringChange={vi.fn()}345 onLockOnCloseChange={vi.fn()}346 onAutoLockMinutesChange={onAutoLockMinutesChange}347 onTrashRetentionChange={vi.fn()}348 onThemeChange={onThemeChange}349 onAccentChange={vi.fn()}350 onBack={vi.fn()}351 onImport={vi.fn()}352 onExport={vi.fn()}353 onBreachCheckDone={vi.fn()}354 />355 );356 await flushView();357 sendMessageMock.mockClear();358359 const selects = container.querySelectorAll("select");360 changeSelect(selects[0] as HTMLSelectElement, "15");361 expect(onAutoLockMinutesChange).toHaveBeenCalledWith(15);362363 const themeButton = Array.from(container.querySelectorAll("button")).find(364 (candidate) => candidate.textContent?.trim() === "Switch to light"365 ) as HTMLButtonElement;366 themeButton.click();367 expect(onThemeChange).toHaveBeenCalledWith("light");368369 const changeButton = Array.from(container.querySelectorAll("button")).find(370 (candidate) => candidate.textContent?.trim() === "change"371 ) as HTMLButtonElement;372 changeButton.click();373 await flushView();374375 const inputs = container.querySelectorAll('input[type="password"]');376 inputText(inputs[0] as HTMLInputElement, "current-pass");377 inputText(inputs[1] as HTMLInputElement, "new-password-123");378 inputText(inputs[2] as HTMLInputElement, "new-password-123");379 await flushView();380381 const saveButton = Array.from(container.querySelectorAll("button")).find(382 (candidate) => candidate.textContent?.trim() === "save new password"383 ) as HTMLButtonElement;384 saveButton.click();385 await flushView();386387 expect(sendMessageMock).toHaveBeenCalledWith({388 type: "CHANGE_PASSWORD",389 payload: {390 currentPassword: "current-pass",391 newPassword: "new-password-123",392 },393 });394 });395396 it("Entry copy actions send COPY and canary ping messages for password and username", async () => {397 sendMessageMock.mockResolvedValue({ ok: true });398 const container = mount(399 <EntryView400 entry={sampleEntry()}401 onDelete={vi.fn()}402 onFieldSaved={vi.fn(async () => undefined)}403 />404 );405406 const passwordButton = container.querySelector('button[aria-label="Copy password"]') as HTMLButtonElement;407 passwordButton.click();408 await flushView();409410 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {411 type: "COPY",412 payload: { text: "secret123", label: "password" },413 });414 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {415 type: "FIRE_CANARY_PING",416 payload: {417 webhookUrl: "https://example.com/hook",418 entryTitle: "GitHub",419 trigger: "password_copied",420 },421 });422423 sendMessageMock.mockClear();424 const usernameButton = container.querySelector('button[aria-label="Copy username"]') as HTMLButtonElement;425 usernameButton.click();426 await flushView();427428 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {429 type: "COPY",430 payload: { text: "june", label: "username" },431 });432 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {433 type: "FIRE_CANARY_PING",434 payload: {435 webhookUrl: "https://example.com/hook",436 entryTitle: "GitHub",437 trigger: "username_copied",438 },439 });440 });441442 it("Entry refuses to send an empty password update and copies TOTP through COPY", async () => {443 sendMessageMock.mockResolvedValue({ ok: true });444 const onFieldSaved = vi.fn(async () => undefined);445 const container = mount(446 <EntryView447 entry={sampleEntry()}448 onDelete={vi.fn()}449 onFieldSaved={onFieldSaved}450 />451 );452453 const passwordValue = container.querySelector(".peach-password-value") as HTMLSpanElement;454 passwordValue.click();455 await flushView();456457 const passwordInput = container.querySelector('input[placeholder="paste or type a password"]') as HTMLInputElement;458 inputText(passwordInput, "");459 await flushView();460461 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;462 saveButton.click();463 await flushView();464465 expect(sendMessageMock).not.toHaveBeenCalledWith(466 expect.objectContaining({ type: "UPDATE_ENTRY" })467 );468 expect(onFieldSaved).not.toHaveBeenCalled();469470 const totpButton = container.querySelector('button[aria-label="Copy TOTP code"]') as HTMLButtonElement;471 totpButton.click();472 await flushView();473474 expect(sendMessageMock).toHaveBeenLastCalledWith({475 type: "COPY",476 payload: { text: "123456", label: "totp" },477 });478 });479480 it("Entry closes an unchanged inline editor when focus leaves it", async () => {481 sendMessageMock.mockResolvedValue({ ok: true });482 const container = mount(483 <EntryView484 entry={sampleEntry()}485 onDelete={vi.fn()}486 onFieldSaved={vi.fn(async () => undefined)}487 />488 );489490 const usernameValue = container.querySelector(".peach-field-value.peach-inline-clickable") as HTMLSpanElement;491 usernameValue.click();492 await flushView();493494 const usernameInput = container.querySelector(".peach-inline-input[autocomplete=\"off\"]") as HTMLInputElement;495 usernameInput.blur();496 await flushView();497498 expect(container.querySelector(".peach-inline-input[autocomplete=\"off\"]")).toBeNull();499 expect(sendMessageMock).not.toHaveBeenCalledWith(500 expect.objectContaining({ type: "UPDATE_ENTRY" })501 );502 });503504 it("Entry lets TOTP secrets be edited inline", async () => {505 sendMessageMock.mockResolvedValue({ ok: true });506 const onFieldSaved = vi.fn(async () => undefined);507 const container = mount(508 <EntryView509 entry={sampleEntry()}510 onDelete={vi.fn()}511 onFieldSaved={onFieldSaved}512 />513 );514515 const totpMain = container.querySelector(".peach-totp-main.peach-inline-clickable") as HTMLDivElement;516 totpMain.click();517 await flushView();518519 const totpInput = container.querySelector('input[placeholder="otpauth://... or base32 secret"]') as HTMLInputElement;520 inputText(totpInput, "otpauth://totp/Example?secret=NB2W45DFOIZA====&issuer=Peach");521 await flushView();522523 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;524 saveButton.click();525 await flushView();526527 expect(sendMessageMock).toHaveBeenCalledWith({528 type: "UPDATE_ENTRY",529 payload: {530 id: "entry-1",531 patch: { totp: "NB2W45DFOIZA====" },532 },533 });534 expect(onFieldSaved).toHaveBeenCalled();535 });536537 it("Vault seeds and focuses search when a search seed is provided", async () => {538 const container = mount(539 <VaultView540 entries={[sampleEntry(), sampleEntry({ id: "entry-2", title: "GitLab" })]}541 sortOrder="url-first"542 searchSeed="g"543 searchSeedNonce={1}544 onSelectEntry={vi.fn()}545 onSortOrderChange={vi.fn()}546 onAdd={vi.fn()}547 onClose={vi.fn()}548 />549 );550551 await flushView();552553 const search = container.querySelector(".peach-list-top .peach-search-input") as HTMLInputElement;554 expect(search.value).toBe("g");555 expect(document.activeElement).toBe(search);556 });557558 it("Import existing-vault add mode parses a file and batches entries into the vault", async () => {559 parseImportSourceMock.mockResolvedValue(560 sampleImportResult({561 entries: [562 {563 type: "login",564 title: "Imported site",565 username: "imported-user",566 password: "imported-pass",567 url: "https://imported.test",568 },569 ],570 })571 );572 sendMessageMock.mockImplementation(async (msg: unknown) => {573 const type = (msg as { type?: string }).type;574 if (type === "GET_ENTRIES") {575 return {576 ok: true,577 data: [578 sampleEntry({579 id: "existing-1",580 title: "Imported site",581 username: "imported-user",582 url: "https://imported.test",583 }),584 ],585 };586 }587 if (type === "BATCH_ADD_ENTRIES") {588 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };589 }590 return { ok: true };591 });592 const onImport = vi.fn(async () => undefined);593 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);594 await flushView();595 sendMessageMock.mockClear();596597 await selectImportFile(container);598 findButton(container, "Import 1").click();599 await flushView();600601 expect(parseImportSourceMock).toHaveBeenCalledWith('{"export":"test"}', undefined);602 expect(sendMessageMock).toHaveBeenCalledWith({603 type: "BATCH_ADD_ENTRIES",604 payload: {605 entries: [606 expect.objectContaining({607 title: "Imported site",608 username: "imported-user",609 password: "imported-pass",610 url: "https://imported.test",611 }),612 ],613 },614 });615 expect(onImport).toHaveBeenCalled();616 expect(container.textContent).toContain("1 entry appears to be duplicates");617 });618619 it("Import replace mode requires confirmation before clearing and replacing the vault", async () => {620 parseImportSourceMock.mockResolvedValue(sampleImportResult());621 sendMessageMock.mockImplementation(async (msg: unknown) => {622 const type = (msg as { type?: string }).type;623 if (type === "GET_ENTRIES") {624 return { ok: true, data: [sampleEntry({ id: "existing-1" })] };625 }626 if (type === "CLEAR_VAULT_ENTRIES") {627 return { ok: true };628 }629 if (type === "BATCH_ADD_ENTRIES") {630 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };631 }632 return { ok: true };633 });634 const onImport = vi.fn(async () => undefined);635 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);636 await flushView();637 sendMessageMock.mockClear();638639 await selectImportFile(container);640 (container.querySelector('input[value="replace"]') as HTMLInputElement).click();641 await flushView();642643 findButton(container, "Replace vault with 1").click();644 await flushView();645646 expect(sendMessageMock).not.toHaveBeenCalledWith(647 expect.objectContaining({ type: "CLEAR_VAULT_ENTRIES" })648 );649 expect(container.textContent).toContain("Replace forever? This cannot be undone.");650651 findButton(container, "Yes, replace forever").click();652 await flushView();653654 expect(sendMessageMock).toHaveBeenCalledWith({ type: "CLEAR_VAULT_ENTRIES" });655 expect(sendMessageMock).toHaveBeenCalledWith({656 type: "BATCH_ADD_ENTRIES",657 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },658 });659 expect(onImport).toHaveBeenCalled();660 });661662 it("Import onboarding mode waits for a matching master password before creating a vault", async () => {663 parseImportSourceMock.mockResolvedValue(sampleImportResult());664 sendMessageMock.mockImplementation(async (msg: unknown) => {665 const type = (msg as { type?: string }).type;666 if (type === "CREATE_VAULT") return { ok: true };667 if (type === "BATCH_ADD_ENTRIES") {668 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };669 }670 return { ok: true };671 });672 const onImport = vi.fn(async () => undefined);673 const container = mount(674 <ImportView675 mode="onboarding"676 replacesExistingVault={true}677 onBack={vi.fn()}678 onImport={onImport}679 />680 );681682 await selectImportFile(container);683684 const createButton = findButton(container, "Create vault + import 1");685 expect(createButton.disabled).toBe(true);686 expect(container.textContent).toContain("Restoring now replaces the locked local vault");687688 const inputs = container.querySelectorAll('input[type="password"]');689 inputText(inputs[0] as HTMLInputElement, "fresh-master-password");690 inputText(inputs[1] as HTMLInputElement, "fresh-master-password");691 await flushView();692693 expect(createButton.disabled).toBe(false);694 createButton.click();695 await flushView();696697 expect(sendMessageMock).toHaveBeenCalledWith({698 type: "CREATE_VAULT",699 payload: { password: "fresh-master-password" },700 });701 expect(sendMessageMock).toHaveBeenCalledWith({702 type: "BATCH_ADD_ENTRIES",703 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },704 });705 expect(onImport).toHaveBeenCalled();706 });707708 it("Import encrypted backups reparse with the backup password before import", async () => {709 parseImportSourceMock710 .mockResolvedValueOnce(711 sampleImportResult({712 format: "peach-json",713 label: "Peach encrypted backup",714 entries: [],715 totalItems: 0,716 requiresPassword: true,717 })718 )719 .mockResolvedValueOnce(720 sampleImportResult({721 format: "peach-json",722 label: "Peach encrypted backup",723 })724 );725 sendMessageMock.mockImplementation(async (msg: unknown) => {726 const type = (msg as { type?: string }).type;727 if (type === "GET_ENTRIES") return { ok: true, data: [] };728 if (type === "BATCH_ADD_ENTRIES") {729 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };730 }731 return { ok: true };732 });733 const onImport = vi.fn(async () => undefined);734 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);735 await flushView();736 sendMessageMock.mockClear();737738 await selectImportFile(container, '{"type":"peach-export"}');739 const password = container.querySelector('input[placeholder="backup password"]') as HTMLInputElement;740 inputText(password, "backup-passphrase");741 pressEnter(password);742 await flushView();743 await flushView();744745 expect(parseImportSourceMock).toHaveBeenNthCalledWith(1, '{"type":"peach-export"}', undefined);746 expect(parseImportSourceMock).toHaveBeenNthCalledWith(2, '{"type":"peach-export"}', "backup-passphrase");747748 findButton(container, "Import 1").click();749 await flushView();750751 expect(sendMessageMock).toHaveBeenCalledWith({752 type: "BATCH_ADD_ENTRIES",753 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },754 });755 expect(onImport).toHaveBeenCalled();756 });757});