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("zxcvbn", () => ({50 default: (value: string) => zxcvbnMock(value),51}));5253describe("popup contract tests", () => {54 const disposers: Array<() => void> = [];5556 beforeEach(() => {57 document.body.innerHTML = "";58 sendMessageMock.mockReset();59 triggerRingMock.mockReset();60 triggerPipPulseMock.mockReset();61 parseImportSourceMock.mockReset();62 zxcvbnMock.mockReset();63 zxcvbnMock.mockImplementation((value: string) => ({ score: value.length >= 16 ? 3 : 1 }));64 parseImportSourceMock.mockResolvedValue({65 format: "unknown",66 label: "Unknown format",67 entries: [],68 totalItems: 0,69 skippedItems: 0,70 requiresPassword: false,71 });7273 vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {74 return window.setTimeout(() => cb(0), 0) as unknown as number;75 });76 vi.stubGlobal("cancelAnimationFrame", (id: number) => window.clearTimeout(id));77 vi.stubGlobal("URL", {78 createObjectURL: vi.fn(() => "blob:peach-test"),79 revokeObjectURL: vi.fn(),80 });81 vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {});8283 (globalThis as { chrome?: typeof chrome }).chrome = {84 runtime: {85 getManifest: () => ({ version: "1.4.47" }),86 onMessage: { addListener: vi.fn() },87 },88 } as unknown as typeof chrome;89 });9091 afterEach(() => {92 while (disposers.length > 0) {93 disposers.pop()?.();94 }95 vi.unstubAllGlobals();96 });9798 function mount(view: JSX.Element) {99 const mounted = renderView(view);100 disposers.push(mounted.dispose);101 return mounted.container;102 }103104 function sampleEntry(overrides: Partial<VaultEntry> = {}): VaultEntry {105 return {106 id: "entry-1",107 type: "login",108 title: "GitHub",109 username: "june",110 password: "secret123",111 url: "https://github.com",112 totp: "JBSWY3DPEHPK3PXP",113 canary: {114 webhookUrl: "https://example.com/hook",115 },116 createdAt: 1,117 modifiedAt: 1,118 ...overrides,119 };120 }121122 function sampleImportResult(123 overrides: Partial<ImportParseResult> = {}124 ): ImportParseResult {125 return {126 format: "csv",127 label: "CSV import",128 entries: [129 {130 type: "login",131 title: "Imported site",132 username: "imported-user",133 password: "imported-pass",134 url: "https://imported.test",135 },136 ],137 totalItems: 1,138 skippedItems: 0,139 requiresPassword: false,140 ...overrides,141 };142 }143144 async function selectImportFile(145 container: HTMLElement,146 contents = '{"export":"test"}'147 ) {148 149 150 151 const fileCard = findButton(container, "Choose file →");152 if (fileCard) {153 fileCard.click();154 await flushView();155 }156 const input = container.querySelector('input[type="file"]') as HTMLInputElement;157 const file = new File([contents], "import.json", { type: "application/json" });158 Object.defineProperty(input, "files", {159 configurable: true,160 value: [file],161 });162 input.dispatchEvent(new Event("change", { bubbles: true }));163 await flushView();164 await flushView();165 }166167 function findButton(container: HTMLElement, text: string): HTMLButtonElement {168 return Array.from(container.querySelectorAll("button")).find(169 (candidate) => candidate.textContent?.trim() === text170 ) as HTMLButtonElement;171 }172173 it("Lock sends UNLOCK with the entered passphrase", async () => {174 sendMessageMock.mockResolvedValue({ ok: true });175 const onUnlock = vi.fn(async () => undefined);176 const container = mount(<LockView hasVault={true} ready={true} onUnlock={onUnlock} />);177178 const password = container.querySelector("#unlock-password") as HTMLInputElement;179 inputText(password, "correct horse battery staple");180 (container.querySelector(".peach-lock-submit") as HTMLButtonElement).click();181 await flushView();182183 expect(sendMessageMock).toHaveBeenCalledWith({184 type: "UNLOCK",185 payload: { password: "correct horse battery staple" },186 });187 expect(onUnlock).toHaveBeenCalled();188 });189190 it("Lock keeps create-vault disabled for short passwords and weak passphrases", async () => {191 const container = mount(<LockView hasVault={false} ready={true} onUnlock={vi.fn()} />);192 const createButton = container.querySelector(".peach-lock-submit") as HTMLButtonElement;193 const password = container.querySelector("#master-password") as HTMLInputElement;194 const confirm = container.querySelector("#confirm-password") as HTMLInputElement;195196 inputText(password, "short");197 inputText(confirm, "short");198 await flushView();199 expect(createButton.disabled).toBe(true);200201 zxcvbnMock.mockImplementation(() => ({ score: 1 }));202 inputText(password, "weakweakweak");203 inputText(confirm, "weakweakweak");204 await flushView();205 expect(createButton.disabled).toBe(true);206 });207208 it("Lock sends CREATE_VAULT when the onboarding passphrase is strong enough", async () => {209 sendMessageMock.mockResolvedValue({ ok: true });210 zxcvbnMock.mockImplementation(() => ({ score: 3 }));211 const onUnlock = vi.fn(async () => undefined);212 const container = mount(<LockView hasVault={false} ready={true} onUnlock={onUnlock} />);213214 const password = container.querySelector("#master-password") as HTMLInputElement;215 const confirm = container.querySelector("#confirm-password") as HTMLInputElement;216 const createButton = container.querySelector(".peach-lock-submit") as HTMLButtonElement;217218 inputText(password, "correct horse battery staple");219 inputText(confirm, "correct horse battery staple");220 await flushView();221 expect(createButton.disabled).toBe(false);222223 pressEnter(confirm);224 await flushView();225226 expect(sendMessageMock).toHaveBeenCalledWith({227 type: "CREATE_VAULT",228 payload: { password: "correct horse battery staple" },229 });230 expect(onUnlock).toHaveBeenCalled();231 });232233 it("Export menu surfaces the three v2.5.0 backup formats", async () => {234 const container = mount(<ExportView onBack={vi.fn()} />);235 await flushView();236237 expect(container.textContent).toContain("PeachScript");238 expect(container.textContent).toContain("print backup");239 expect(container.textContent).toContain("CSV");240 });241242 it("Settings requires current, new, and confirm passwords before enabling save", async () => {243 sendMessageMock.mockResolvedValue({ ok: true, data: null });244 const container = mount(245 <SettingsView246 entriesCount={1}247 breachMonitoring={true}248 lockOnClose={true}249 autoLockMinutes={5}250 trashRetentionDays={30}251 theme="dark"252 accentColor="peach"253 displayFont="cormorant-italic"254 textSize="medium"255 reduceMotion={false}256 onBreachMonitoringChange={vi.fn()}257 onLockOnCloseChange={vi.fn()}258 onAutoLockMinutesChange={vi.fn()}259 onTrashRetentionChange={vi.fn()}260 onThemeChange={vi.fn()}261 onAccentChange={vi.fn()}262 onDisplayFontChange={vi.fn()}263 onTextSizeChange={vi.fn()}264 onReduceMotionChange={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 displayFont="cormorant-italic"317 textSize="medium"318 reduceMotion={false}319 onBreachMonitoringChange={vi.fn()}320 onLockOnCloseChange={vi.fn()}321 onAutoLockMinutesChange={onAutoLockMinutesChange}322 onTrashRetentionChange={vi.fn()}323 onThemeChange={onThemeChange}324 onAccentChange={vi.fn()}325 onDisplayFontChange={vi.fn()}326 onTextSizeChange={vi.fn()}327 onReduceMotionChange={vi.fn()}328 shortcuts={DEFAULT_BINDINGS}329 onShortcutsChange={vi.fn()}330 onBack={vi.fn()}331 onImport={vi.fn()}332 onExport={vi.fn()}333 onBreachCheckDone={vi.fn()}334 />335 );336 await flushView();337 sendMessageMock.mockClear();338339 const headerBack = container.querySelector(".peach-overlay-header button") as HTMLButtonElement;340341 const securityRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(342 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Security"343 ) as HTMLButtonElement;344 securityRow.click();345 await flushView();346347 const selects = container.querySelectorAll("select");348 changeSelect(selects[0] as HTMLSelectElement, "15");349 expect(onAutoLockMinutesChange).toHaveBeenCalledWith(15);350351 const changeButton = Array.from(container.querySelectorAll("button")).find(352 (candidate) => candidate.textContent?.trim() === "change"353 ) as HTMLButtonElement;354 changeButton.click();355 await flushView();356357 const inputs = container.querySelectorAll('input[type="password"]');358 inputText(inputs[0] as HTMLInputElement, "current-pass");359 inputText(inputs[1] as HTMLInputElement, "new-password-123");360 inputText(inputs[2] as HTMLInputElement, "new-password-123");361 await flushView();362363 const saveButton = Array.from(container.querySelectorAll("button")).find(364 (candidate) => candidate.textContent?.trim() === "save new password"365 ) as HTMLButtonElement;366 saveButton.click();367 await flushView();368369 headerBack.click();370 await flushView();371 const appearanceRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(372 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Appearance"373 ) as HTMLButtonElement;374 appearanceRow.click();375 await flushView();376 const themeButton = Array.from(container.querySelectorAll("button")).find(377 (candidate) => candidate.textContent?.trim() === "Switch to light"378 ) as HTMLButtonElement;379 themeButton.click();380 expect(onThemeChange).toHaveBeenCalledWith("light");381382 expect(sendMessageMock).toHaveBeenCalledWith({383 type: "CHANGE_PASSWORD",384 payload: {385 currentPassword: "current-pass",386 newPassword: "new-password-123",387 },388 });389 });390391 it("Entry copy actions send COPY and canary ping messages for password and username", async () => {392 sendMessageMock.mockResolvedValue({ ok: true });393 const container = mount(394 <EntryView395 entry={sampleEntry()}396 onDelete={vi.fn()}397 onFieldSaved={vi.fn(async () => undefined)}398 />399 );400401 const passwordButton = container.querySelector('button[aria-label="Copy password"]') as HTMLButtonElement;402 passwordButton.click();403 await flushView();404405 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {406 type: "COPY",407 payload: { text: "secret123", label: "password", alreadyWritten: false },408 });409 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {410 type: "FIRE_CANARY_PING",411 payload: {412 webhookUrl: "https://example.com/hook",413 entryTitle: "GitHub",414 trigger: "password_copied",415 },416 });417418 sendMessageMock.mockClear();419 const usernameButton = container.querySelector('button[aria-label="Copy username"]') as HTMLButtonElement;420 usernameButton.click();421 await flushView();422423 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {424 type: "COPY",425 payload: { text: "june", label: "username", alreadyWritten: false },426 });427 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {428 type: "FIRE_CANARY_PING",429 payload: {430 webhookUrl: "https://example.com/hook",431 entryTitle: "GitHub",432 trigger: "username_copied",433 },434 });435 });436437 it("Entry refuses to send an empty password update and copies TOTP through COPY", async () => {438 sendMessageMock.mockResolvedValue({ ok: true });439 const onFieldSaved = vi.fn(async () => undefined);440 const container = mount(441 <EntryView442 entry={sampleEntry()}443 onDelete={vi.fn()}444 onFieldSaved={onFieldSaved}445 />446 );447448 const passwordValue = container.querySelector(".peach-password-value") as HTMLSpanElement;449 passwordValue.click();450 await flushView();451452 const passwordInput = container.querySelector('input[placeholder="paste or type a password"]') as HTMLInputElement;453 inputText(passwordInput, "");454 await flushView();455456 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;457 saveButton.click();458 await flushView();459460 expect(sendMessageMock).not.toHaveBeenCalledWith(461 expect.objectContaining({ type: "UPDATE_ENTRY" })462 );463 expect(onFieldSaved).not.toHaveBeenCalled();464465 const totpMain = container.querySelector(".peach-totp-main") as HTMLDivElement;466 totpMain.click();467 await flushView();468469 expect(sendMessageMock).toHaveBeenLastCalledWith({470 type: "COPY",471 payload: { text: "123456", label: "totp", alreadyWritten: false },472 });473 });474475 it("Entry closes an unchanged inline editor when focus leaves it", async () => {476 sendMessageMock.mockResolvedValue({ ok: true });477 const container = mount(478 <EntryView479 entry={sampleEntry()}480 onDelete={vi.fn()}481 onFieldSaved={vi.fn(async () => undefined)}482 />483 );484485 const usernameValue = container.querySelector(".peach-field-value.peach-inline-clickable") as HTMLSpanElement;486 usernameValue.click();487 await flushView();488489 const usernameInput = container.querySelector(".peach-inline-input[autocomplete=\"off\"]") as HTMLInputElement;490 usernameInput.blur();491 await flushView();492493 expect(container.querySelector(".peach-inline-input[autocomplete=\"off\"]")).toBeNull();494 expect(sendMessageMock).not.toHaveBeenCalledWith(495 expect.objectContaining({ type: "UPDATE_ENTRY" })496 );497 });498499 it("Entry lets TOTP secrets be edited inline", async () => {500 sendMessageMock.mockResolvedValue({ ok: true });501 const onFieldSaved = vi.fn(async () => undefined);502 const container = mount(503 <EntryView504 entry={sampleEntry()}505 onDelete={vi.fn()}506 onFieldSaved={onFieldSaved}507 />508 );509510 const totpMain = container.querySelector(".peach-totp-main") as HTMLDivElement;511 totpMain.dispatchEvent(new MouseEvent("dblclick", { bubbles: true }));512 await flushView();513514 const totpInput = container.querySelector('input[placeholder="otpauth://... or base32 secret"]') as HTMLInputElement;515 inputText(totpInput, "otpauth://totp/Example?secret=NB2W45DFOIZA====&issuer=Peach");516 await flushView();517518 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;519 saveButton.click();520 await flushView();521522 expect(sendMessageMock).toHaveBeenCalledWith({523 type: "UPDATE_ENTRY",524 payload: {525 id: "entry-1",526 patch: { totp: "NB2W45DFOIZA====" },527 },528 });529 expect(onFieldSaved).toHaveBeenCalled();530 });531532 it("Vault seeds and focuses search when a search seed is provided", async () => {533 const container = mount(534 <VaultView535 entries={[sampleEntry(), sampleEntry({ id: "entry-2", title: "GitLab" })]}536 sortOrder="url-first"537 searchSeed="g"538 searchSeedNonce={1}539 onSelectEntry={vi.fn()}540 onSortOrderChange={vi.fn()}541 onAdd={vi.fn()}542 onClose={vi.fn()}543 />544 );545546 await flushView();547548 const search = container.querySelector(".peach-list-top .peach-search-input") as HTMLInputElement;549 expect(search.value).toBe("g");550 expect(document.activeElement).toBe(search);551 });552553 it("Import existing-vault add mode parses a file and batches entries into the vault", async () => {554 parseImportSourceMock.mockResolvedValue(555 sampleImportResult({556 entries: [557 {558 type: "login",559 title: "Imported site",560 username: "imported-user",561 password: "imported-pass",562 url: "https://imported.test",563 },564 ],565 })566 );567 sendMessageMock.mockImplementation(async (msg: unknown) => {568 const type = (msg as { type?: string }).type;569 if (type === "GET_ENTRIES") {570 return {571 ok: true,572 data: [573 sampleEntry({574 id: "existing-1",575 title: "Imported site",576 username: "imported-user",577 url: "https://imported.test",578 }),579 ],580 };581 }582 if (type === "BATCH_ADD_ENTRIES") {583 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };584 }585 return { ok: true };586 });587 const onImport = vi.fn(async () => undefined);588 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);589 await flushView();590 sendMessageMock.mockClear();591592 await selectImportFile(container);593 findButton(container, "Import 1").click();594 await flushView();595596 expect(parseImportSourceMock).toHaveBeenCalledWith('{"export":"test"}', undefined);597 expect(sendMessageMock).toHaveBeenCalledWith({598 type: "BATCH_ADD_ENTRIES",599 payload: {600 entries: [601 expect.objectContaining({602 title: "Imported site",603 username: "imported-user",604 password: "imported-pass",605 url: "https://imported.test",606 }),607 ],608 },609 });610 expect(onImport).toHaveBeenCalled();611 expect(container.textContent).toContain("1 entry appears to be duplicates");612 });613614 it("Import replace mode requires confirmation before clearing and replacing the vault", async () => {615 parseImportSourceMock.mockResolvedValue(sampleImportResult());616 sendMessageMock.mockImplementation(async (msg: unknown) => {617 const type = (msg as { type?: string }).type;618 if (type === "GET_ENTRIES") {619 return { ok: true, data: [sampleEntry({ id: "existing-1" })] };620 }621 if (type === "CLEAR_VAULT_ENTRIES") {622 return { ok: true };623 }624 if (type === "BATCH_ADD_ENTRIES") {625 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };626 }627 return { ok: true };628 });629 const onImport = vi.fn(async () => undefined);630 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);631 await flushView();632 sendMessageMock.mockClear();633634 await selectImportFile(container);635 (container.querySelector('input[value="replace"]') as HTMLInputElement).click();636 await flushView();637638 findButton(container, "Replace vault with 1").click();639 await flushView();640641 expect(sendMessageMock).not.toHaveBeenCalledWith(642 expect.objectContaining({ type: "CLEAR_VAULT_ENTRIES" })643 );644 expect(container.textContent).toContain("Replace forever? This cannot be undone.");645646 findButton(container, "Yes, replace forever").click();647 await flushView();648649 expect(sendMessageMock).toHaveBeenCalledWith({ type: "CLEAR_VAULT_ENTRIES" });650 expect(sendMessageMock).toHaveBeenCalledWith({651 type: "BATCH_ADD_ENTRIES",652 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },653 });654 expect(onImport).toHaveBeenCalled();655 });656657 it("Import onboarding mode waits for a matching master password before creating a vault", async () => {658 parseImportSourceMock.mockResolvedValue(sampleImportResult());659 sendMessageMock.mockImplementation(async (msg: unknown) => {660 const type = (msg as { type?: string }).type;661 if (type === "CREATE_VAULT") return { ok: true };662 if (type === "BATCH_ADD_ENTRIES") {663 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };664 }665 return { ok: true };666 });667 const onImport = vi.fn(async () => undefined);668 const container = mount(669 <ImportView670 mode="onboarding"671 replacesExistingVault={true}672 onBack={vi.fn()}673 onImport={onImport}674 />675 );676677 await selectImportFile(container);678679 const createButton = findButton(container, "Create vault + import 1");680 expect(createButton.disabled).toBe(true);681 expect(container.textContent).toContain("Restoring now replaces the locked local vault");682683 const masterInput = container.querySelector(684 'input[placeholder="new master password"]'685 ) as HTMLInputElement;686 const confirmInput = container.querySelector(687 'input[placeholder="confirm master password"]'688 ) as HTMLInputElement;689 inputText(masterInput, "fresh-master-password");690 inputText(confirmInput, "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});