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 const input = container.querySelector('input[type="file"]') as HTMLInputElement;149 const file = new File([contents], "import.json", { type: "application/json" });150 Object.defineProperty(input, "files", {151 configurable: true,152 value: [file],153 });154 input.dispatchEvent(new Event("change", { bubbles: true }));155 await flushView();156 await flushView();157 }158159 function findButton(container: HTMLElement, text: string): HTMLButtonElement {160 return Array.from(container.querySelectorAll("button")).find(161 (candidate) => candidate.textContent?.trim() === text162 ) as HTMLButtonElement;163 }164165 it("Lock sends UNLOCK with the entered passphrase", async () => {166 sendMessageMock.mockResolvedValue({ ok: true });167 const onUnlock = vi.fn(async () => undefined);168 const container = mount(<LockView hasVault={true} ready={true} onUnlock={onUnlock} />);169170 const password = container.querySelector("#unlock-password") as HTMLInputElement;171 inputText(password, "correct horse battery staple");172 (container.querySelector(".peach-lock-submit") as HTMLButtonElement).click();173 await flushView();174175 expect(sendMessageMock).toHaveBeenCalledWith({176 type: "UNLOCK",177 payload: { password: "correct horse battery staple" },178 });179 expect(onUnlock).toHaveBeenCalled();180 });181182 it("Lock keeps create-vault disabled for short passwords and weak passphrases", async () => {183 const container = mount(<LockView hasVault={false} ready={true} onUnlock={vi.fn()} />);184 const createButton = container.querySelector(".peach-lock-submit") as HTMLButtonElement;185 const password = container.querySelector("#master-password") as HTMLInputElement;186 const confirm = container.querySelector("#confirm-password") as HTMLInputElement;187188 inputText(password, "short");189 inputText(confirm, "short");190 await flushView();191 expect(createButton.disabled).toBe(true);192193 zxcvbnMock.mockImplementation(() => ({ score: 1 }));194 inputText(password, "weakweakweak");195 inputText(confirm, "weakweakweak");196 await flushView();197 expect(createButton.disabled).toBe(true);198 });199200 it("Lock sends CREATE_VAULT when the onboarding passphrase is strong enough", async () => {201 sendMessageMock.mockResolvedValue({ ok: true });202 zxcvbnMock.mockImplementation(() => ({ score: 3 }));203 const onUnlock = vi.fn(async () => undefined);204 const container = mount(<LockView hasVault={false} ready={true} onUnlock={onUnlock} />);205206 const password = container.querySelector("#master-password") as HTMLInputElement;207 const confirm = container.querySelector("#confirm-password") as HTMLInputElement;208 const createButton = container.querySelector(".peach-lock-submit") as HTMLButtonElement;209210 inputText(password, "correct horse battery staple");211 inputText(confirm, "correct horse battery staple");212 await flushView();213 expect(createButton.disabled).toBe(false);214215 pressEnter(confirm);216 await flushView();217218 expect(sendMessageMock).toHaveBeenCalledWith({219 type: "CREATE_VAULT",220 payload: { password: "correct horse battery staple" },221 });222 expect(onUnlock).toHaveBeenCalled();223 });224225 it("Export menu surfaces the three v2.5.0 backup formats", async () => {226 const container = mount(<ExportView onBack={vi.fn()} />);227 await flushView();228229 expect(container.textContent).toContain("PeachScript");230 expect(container.textContent).toContain("print backup");231 expect(container.textContent).toContain("CSV");232 });233234 it("Settings requires current, new, and confirm passwords before enabling save", async () => {235 sendMessageMock.mockResolvedValue({ ok: true, data: null });236 const container = mount(237 <SettingsView238 entriesCount={1}239 breachMonitoring={true}240 lockOnClose={true}241 autoLockMinutes={5}242 trashRetentionDays={30}243 theme="dark"244 accentColor="peach"245 displayFont="cormorant-italic"246 textSize="medium"247 reduceMotion={false}248 onBreachMonitoringChange={vi.fn()}249 onLockOnCloseChange={vi.fn()}250 onAutoLockMinutesChange={vi.fn()}251 onTrashRetentionChange={vi.fn()}252 onThemeChange={vi.fn()}253 onAccentChange={vi.fn()}254 onDisplayFontChange={vi.fn()}255 onTextSizeChange={vi.fn()}256 onReduceMotionChange={vi.fn()}257 shortcuts={DEFAULT_BINDINGS}258 onShortcutsChange={vi.fn()}259 onBack={vi.fn()}260 onImport={vi.fn()}261 onExport={vi.fn()}262 onBreachCheckDone={vi.fn()}263 />264 );265 await flushView();266267 const securityRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(268 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Security"269 ) as HTMLButtonElement;270 securityRow.click();271 await flushView();272273 const changeButton = Array.from(container.querySelectorAll("button")).find(274 (candidate) => candidate.textContent?.trim() === "change"275 ) as HTMLButtonElement;276 changeButton.click();277 await flushView();278279 const saveButton = Array.from(container.querySelectorAll("button")).find(280 (candidate) => candidate.textContent?.trim() === "save new password"281 ) as HTMLButtonElement;282 expect(saveButton.disabled).toBe(true);283284 const inputs = container.querySelectorAll('input[type="password"]');285 inputText(inputs[0] as HTMLInputElement, "current-pass");286 inputText(inputs[1] as HTMLInputElement, "new-pass-12345");287 await flushView();288 expect(saveButton.disabled).toBe(true);289290 inputText(inputs[2] as HTMLInputElement, "new-pass-12345");291 await flushView();292 expect(saveButton.disabled).toBe(false);293 });294295 it("Settings sends CHANGE_PASSWORD and forwards auto-lock/theme changes through callbacks", async () => {296 sendMessageMock.mockResolvedValue({ ok: true, data: null });297 const onAutoLockMinutesChange = vi.fn();298 const onThemeChange = vi.fn();299 const container = mount(300 <SettingsView301 entriesCount={1}302 breachMonitoring={true}303 lockOnClose={true}304 autoLockMinutes={5}305 trashRetentionDays={30}306 theme="dark"307 accentColor="peach"308 displayFont="cormorant-italic"309 textSize="medium"310 reduceMotion={false}311 onBreachMonitoringChange={vi.fn()}312 onLockOnCloseChange={vi.fn()}313 onAutoLockMinutesChange={onAutoLockMinutesChange}314 onTrashRetentionChange={vi.fn()}315 onThemeChange={onThemeChange}316 onAccentChange={vi.fn()}317 onDisplayFontChange={vi.fn()}318 onTextSizeChange={vi.fn()}319 onReduceMotionChange={vi.fn()}320 shortcuts={DEFAULT_BINDINGS}321 onShortcutsChange={vi.fn()}322 onBack={vi.fn()}323 onImport={vi.fn()}324 onExport={vi.fn()}325 onBreachCheckDone={vi.fn()}326 />327 );328 await flushView();329 sendMessageMock.mockClear();330331 const headerBack = container.querySelector(".peach-overlay-header button") as HTMLButtonElement;332333 const securityRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(334 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Security"335 ) as HTMLButtonElement;336 securityRow.click();337 await flushView();338339 const selects = container.querySelectorAll("select");340 changeSelect(selects[0] as HTMLSelectElement, "15");341 expect(onAutoLockMinutesChange).toHaveBeenCalledWith(15);342343 const changeButton = Array.from(container.querySelectorAll("button")).find(344 (candidate) => candidate.textContent?.trim() === "change"345 ) as HTMLButtonElement;346 changeButton.click();347 await flushView();348349 const inputs = container.querySelectorAll('input[type="password"]');350 inputText(inputs[0] as HTMLInputElement, "current-pass");351 inputText(inputs[1] as HTMLInputElement, "new-password-123");352 inputText(inputs[2] as HTMLInputElement, "new-password-123");353 await flushView();354355 const saveButton = Array.from(container.querySelectorAll("button")).find(356 (candidate) => candidate.textContent?.trim() === "save new password"357 ) as HTMLButtonElement;358 saveButton.click();359 await flushView();360361 headerBack.click();362 await flushView();363 const appearanceRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(364 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Appearance"365 ) as HTMLButtonElement;366 appearanceRow.click();367 await flushView();368 const themeButton = Array.from(container.querySelectorAll("button")).find(369 (candidate) => candidate.textContent?.trim() === "Switch to light"370 ) as HTMLButtonElement;371 themeButton.click();372 expect(onThemeChange).toHaveBeenCalledWith("light");373374 expect(sendMessageMock).toHaveBeenCalledWith({375 type: "CHANGE_PASSWORD",376 payload: {377 currentPassword: "current-pass",378 newPassword: "new-password-123",379 },380 });381 });382383 it("Entry copy actions send COPY and canary ping messages for password and username", async () => {384 sendMessageMock.mockResolvedValue({ ok: true });385 const container = mount(386 <EntryView387 entry={sampleEntry()}388 onDelete={vi.fn()}389 onFieldSaved={vi.fn(async () => undefined)}390 />391 );392393 const passwordButton = container.querySelector('button[aria-label="Copy password"]') as HTMLButtonElement;394 passwordButton.click();395 await flushView();396397 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {398 type: "COPY",399 payload: { text: "secret123", label: "password", alreadyWritten: false },400 });401 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {402 type: "FIRE_CANARY_PING",403 payload: {404 webhookUrl: "https://example.com/hook",405 entryTitle: "GitHub",406 trigger: "password_copied",407 },408 });409410 sendMessageMock.mockClear();411 const usernameButton = container.querySelector('button[aria-label="Copy username"]') as HTMLButtonElement;412 usernameButton.click();413 await flushView();414415 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {416 type: "COPY",417 payload: { text: "june", label: "username", alreadyWritten: false },418 });419 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {420 type: "FIRE_CANARY_PING",421 payload: {422 webhookUrl: "https://example.com/hook",423 entryTitle: "GitHub",424 trigger: "username_copied",425 },426 });427 });428429 it("Entry refuses to send an empty password update and copies TOTP through COPY", async () => {430 sendMessageMock.mockResolvedValue({ ok: true });431 const onFieldSaved = vi.fn(async () => undefined);432 const container = mount(433 <EntryView434 entry={sampleEntry()}435 onDelete={vi.fn()}436 onFieldSaved={onFieldSaved}437 />438 );439440 const passwordValue = container.querySelector(".peach-password-value") as HTMLSpanElement;441 passwordValue.click();442 await flushView();443444 const passwordInput = container.querySelector('input[placeholder="paste or type a password"]') as HTMLInputElement;445 inputText(passwordInput, "");446 await flushView();447448 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;449 saveButton.click();450 await flushView();451452 expect(sendMessageMock).not.toHaveBeenCalledWith(453 expect.objectContaining({ type: "UPDATE_ENTRY" })454 );455 expect(onFieldSaved).not.toHaveBeenCalled();456457 const totpMain = container.querySelector(".peach-totp-main") as HTMLDivElement;458 totpMain.click();459 await flushView();460461 expect(sendMessageMock).toHaveBeenLastCalledWith({462 type: "COPY",463 payload: { text: "123456", label: "totp", alreadyWritten: false },464 });465 });466467 it("Entry closes an unchanged inline editor when focus leaves it", async () => {468 sendMessageMock.mockResolvedValue({ ok: true });469 const container = mount(470 <EntryView471 entry={sampleEntry()}472 onDelete={vi.fn()}473 onFieldSaved={vi.fn(async () => undefined)}474 />475 );476477 const usernameValue = container.querySelector(".peach-field-value.peach-inline-clickable") as HTMLSpanElement;478 usernameValue.click();479 await flushView();480481 const usernameInput = container.querySelector(".peach-inline-input[autocomplete=\"off\"]") as HTMLInputElement;482 usernameInput.blur();483 await flushView();484485 expect(container.querySelector(".peach-inline-input[autocomplete=\"off\"]")).toBeNull();486 expect(sendMessageMock).not.toHaveBeenCalledWith(487 expect.objectContaining({ type: "UPDATE_ENTRY" })488 );489 });490491 it("Entry lets TOTP secrets be edited inline", async () => {492 sendMessageMock.mockResolvedValue({ ok: true });493 const onFieldSaved = vi.fn(async () => undefined);494 const container = mount(495 <EntryView496 entry={sampleEntry()}497 onDelete={vi.fn()}498 onFieldSaved={onFieldSaved}499 />500 );501502 const totpMain = container.querySelector(".peach-totp-main") as HTMLDivElement;503 totpMain.dispatchEvent(new MouseEvent("dblclick", { bubbles: true }));504 await flushView();505506 const totpInput = container.querySelector('input[placeholder="otpauth://... or base32 secret"]') as HTMLInputElement;507 inputText(totpInput, "otpauth://totp/Example?secret=NB2W45DFOIZA====&issuer=Peach");508 await flushView();509510 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;511 saveButton.click();512 await flushView();513514 expect(sendMessageMock).toHaveBeenCalledWith({515 type: "UPDATE_ENTRY",516 payload: {517 id: "entry-1",518 patch: { totp: "NB2W45DFOIZA====" },519 },520 });521 expect(onFieldSaved).toHaveBeenCalled();522 });523524 it("Vault seeds and focuses search when a search seed is provided", async () => {525 const container = mount(526 <VaultView527 entries={[sampleEntry(), sampleEntry({ id: "entry-2", title: "GitLab" })]}528 sortOrder="url-first"529 searchSeed="g"530 searchSeedNonce={1}531 onSelectEntry={vi.fn()}532 onSortOrderChange={vi.fn()}533 onAdd={vi.fn()}534 onClose={vi.fn()}535 />536 );537538 await flushView();539540 const search = container.querySelector(".peach-list-top .peach-search-input") as HTMLInputElement;541 expect(search.value).toBe("g");542 expect(document.activeElement).toBe(search);543 });544545 it("Import existing-vault add mode parses a file and batches entries into the vault", async () => {546 parseImportSourceMock.mockResolvedValue(547 sampleImportResult({548 entries: [549 {550 type: "login",551 title: "Imported site",552 username: "imported-user",553 password: "imported-pass",554 url: "https://imported.test",555 },556 ],557 })558 );559 sendMessageMock.mockImplementation(async (msg: unknown) => {560 const type = (msg as { type?: string }).type;561 if (type === "GET_ENTRIES") {562 return {563 ok: true,564 data: [565 sampleEntry({566 id: "existing-1",567 title: "Imported site",568 username: "imported-user",569 url: "https://imported.test",570 }),571 ],572 };573 }574 if (type === "BATCH_ADD_ENTRIES") {575 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };576 }577 return { ok: true };578 });579 const onImport = vi.fn(async () => undefined);580 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);581 await flushView();582 sendMessageMock.mockClear();583584 await selectImportFile(container);585 findButton(container, "Import 1").click();586 await flushView();587588 expect(parseImportSourceMock).toHaveBeenCalledWith('{"export":"test"}', undefined);589 expect(sendMessageMock).toHaveBeenCalledWith({590 type: "BATCH_ADD_ENTRIES",591 payload: {592 entries: [593 expect.objectContaining({594 title: "Imported site",595 username: "imported-user",596 password: "imported-pass",597 url: "https://imported.test",598 }),599 ],600 },601 });602 expect(onImport).toHaveBeenCalled();603 expect(container.textContent).toContain("1 entry appears to be duplicates");604 });605606 it("Import replace mode requires confirmation before clearing and replacing the vault", async () => {607 parseImportSourceMock.mockResolvedValue(sampleImportResult());608 sendMessageMock.mockImplementation(async (msg: unknown) => {609 const type = (msg as { type?: string }).type;610 if (type === "GET_ENTRIES") {611 return { ok: true, data: [sampleEntry({ id: "existing-1" })] };612 }613 if (type === "CLEAR_VAULT_ENTRIES") {614 return { ok: true };615 }616 if (type === "BATCH_ADD_ENTRIES") {617 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };618 }619 return { ok: true };620 });621 const onImport = vi.fn(async () => undefined);622 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);623 await flushView();624 sendMessageMock.mockClear();625626 await selectImportFile(container);627 (container.querySelector('input[value="replace"]') as HTMLInputElement).click();628 await flushView();629630 findButton(container, "Replace vault with 1").click();631 await flushView();632633 expect(sendMessageMock).not.toHaveBeenCalledWith(634 expect.objectContaining({ type: "CLEAR_VAULT_ENTRIES" })635 );636 expect(container.textContent).toContain("Replace forever? This cannot be undone.");637638 findButton(container, "Yes, replace forever").click();639 await flushView();640641 expect(sendMessageMock).toHaveBeenCalledWith({ type: "CLEAR_VAULT_ENTRIES" });642 expect(sendMessageMock).toHaveBeenCalledWith({643 type: "BATCH_ADD_ENTRIES",644 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },645 });646 expect(onImport).toHaveBeenCalled();647 });648649 it("Import onboarding mode waits for a matching master password before creating a vault", async () => {650 parseImportSourceMock.mockResolvedValue(sampleImportResult());651 sendMessageMock.mockImplementation(async (msg: unknown) => {652 const type = (msg as { type?: string }).type;653 if (type === "CREATE_VAULT") return { ok: true };654 if (type === "BATCH_ADD_ENTRIES") {655 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };656 }657 return { ok: true };658 });659 const onImport = vi.fn(async () => undefined);660 const container = mount(661 <ImportView662 mode="onboarding"663 replacesExistingVault={true}664 onBack={vi.fn()}665 onImport={onImport}666 />667 );668669 await selectImportFile(container);670671 const createButton = findButton(container, "Create vault + import 1");672 expect(createButton.disabled).toBe(true);673 expect(container.textContent).toContain("Restoring now replaces the locked local vault");674675 const masterInput = container.querySelector(676 'input[placeholder="new master password"]'677 ) as HTMLInputElement;678 const confirmInput = container.querySelector(679 'input[placeholder="confirm master password"]'680 ) as HTMLInputElement;681 inputText(masterInput, "fresh-master-password");682 inputText(confirmInput, "fresh-master-password");683 await flushView();684685 expect(createButton.disabled).toBe(false);686 createButton.click();687 await flushView();688689 expect(sendMessageMock).toHaveBeenCalledWith({690 type: "CREATE_VAULT",691 payload: { password: "fresh-master-password" },692 });693 expect(sendMessageMock).toHaveBeenCalledWith({694 type: "BATCH_ADD_ENTRIES",695 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },696 });697 expect(onImport).toHaveBeenCalled();698 });699700 it("Import encrypted backups reparse with the backup password before import", async () => {701 parseImportSourceMock702 .mockResolvedValueOnce(703 sampleImportResult({704 format: "peach-json",705 label: "Peach encrypted backup",706 entries: [],707 totalItems: 0,708 requiresPassword: true,709 })710 )711 .mockResolvedValueOnce(712 sampleImportResult({713 format: "peach-json",714 label: "Peach encrypted backup",715 })716 );717 sendMessageMock.mockImplementation(async (msg: unknown) => {718 const type = (msg as { type?: string }).type;719 if (type === "GET_ENTRIES") return { ok: true, data: [] };720 if (type === "BATCH_ADD_ENTRIES") {721 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };722 }723 return { ok: true };724 });725 const onImport = vi.fn(async () => undefined);726 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);727 await flushView();728 sendMessageMock.mockClear();729730 await selectImportFile(container, '{"type":"peach-export"}');731 const password = container.querySelector('input[placeholder="backup password"]') as HTMLInputElement;732 inputText(password, "backup-passphrase");733 pressEnter(password);734 await flushView();735 await flushView();736737 expect(parseImportSourceMock).toHaveBeenNthCalledWith(1, '{"type":"peach-export"}', undefined);738 expect(parseImportSourceMock).toHaveBeenNthCalledWith(2, '{"type":"peach-export"}', "backup-passphrase");739740 findButton(container, "Import 1").click();741 await flushView();742743 expect(sendMessageMock).toHaveBeenCalledWith({744 type: "BATCH_ADD_ENTRIES",745 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },746 });747 expect(onImport).toHaveBeenCalled();748 });749});