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 onBreachMonitoringChange={vi.fn()}250 onLockOnCloseChange={vi.fn()}251 onAutoLockMinutesChange={vi.fn()}252 onTrashRetentionChange={vi.fn()}253 onThemeChange={vi.fn()}254 onAccentChange={vi.fn()}255 shortcuts={DEFAULT_BINDINGS}256 onShortcutsChange={vi.fn()}257 onBack={vi.fn()}258 onImport={vi.fn()}259 onExport={vi.fn()}260 onBreachCheckDone={vi.fn()}261 />262 );263 await flushView();264265 const securityRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(266 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Security"267 ) as HTMLButtonElement;268 securityRow.click();269 await flushView();270271 const changeButton = Array.from(container.querySelectorAll("button")).find(272 (candidate) => candidate.textContent?.trim() === "change"273 ) as HTMLButtonElement;274 changeButton.click();275 await flushView();276277 const saveButton = Array.from(container.querySelectorAll("button")).find(278 (candidate) => candidate.textContent?.trim() === "save new password"279 ) as HTMLButtonElement;280 expect(saveButton.disabled).toBe(true);281282 const inputs = container.querySelectorAll('input[type="password"]');283 inputText(inputs[0] as HTMLInputElement, "current-pass");284 inputText(inputs[1] as HTMLInputElement, "new-pass-12345");285 await flushView();286 expect(saveButton.disabled).toBe(true);287288 inputText(inputs[2] as HTMLInputElement, "new-pass-12345");289 await flushView();290 expect(saveButton.disabled).toBe(false);291 });292293 it("Settings sends CHANGE_PASSWORD and forwards auto-lock/theme changes through callbacks", async () => {294 sendMessageMock.mockResolvedValue({ ok: true, data: null });295 const onAutoLockMinutesChange = vi.fn();296 const onThemeChange = vi.fn();297 const container = mount(298 <SettingsView299 entriesCount={1}300 breachMonitoring={true}301 lockOnClose={true}302 autoLockMinutes={5}303 trashRetentionDays={30}304 theme="dark"305 accentColor="peach"306 onBreachMonitoringChange={vi.fn()}307 onLockOnCloseChange={vi.fn()}308 onAutoLockMinutesChange={onAutoLockMinutesChange}309 onTrashRetentionChange={vi.fn()}310 onThemeChange={onThemeChange}311 onAccentChange={vi.fn()}312 shortcuts={DEFAULT_BINDINGS}313 onShortcutsChange={vi.fn()}314 onBack={vi.fn()}315 onImport={vi.fn()}316 onExport={vi.fn()}317 onBreachCheckDone={vi.fn()}318 />319 );320 await flushView();321 sendMessageMock.mockClear();322323 const headerBack = container.querySelector(".peach-overlay-header button") as HTMLButtonElement;324325 const securityRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(326 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Security"327 ) as HTMLButtonElement;328 securityRow.click();329 await flushView();330331 const selects = container.querySelectorAll("select");332 changeSelect(selects[0] as HTMLSelectElement, "15");333 expect(onAutoLockMinutesChange).toHaveBeenCalledWith(15);334335 const changeButton = Array.from(container.querySelectorAll("button")).find(336 (candidate) => candidate.textContent?.trim() === "change"337 ) as HTMLButtonElement;338 changeButton.click();339 await flushView();340341 const inputs = container.querySelectorAll('input[type="password"]');342 inputText(inputs[0] as HTMLInputElement, "current-pass");343 inputText(inputs[1] as HTMLInputElement, "new-password-123");344 inputText(inputs[2] as HTMLInputElement, "new-password-123");345 await flushView();346347 const saveButton = Array.from(container.querySelectorAll("button")).find(348 (candidate) => candidate.textContent?.trim() === "save new password"349 ) as HTMLButtonElement;350 saveButton.click();351 await flushView();352353 headerBack.click();354 await flushView();355 const appearanceRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(356 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Appearance"357 ) as HTMLButtonElement;358 appearanceRow.click();359 await flushView();360 const themeButton = Array.from(container.querySelectorAll("button")).find(361 (candidate) => candidate.textContent?.trim() === "Switch to light"362 ) as HTMLButtonElement;363 themeButton.click();364 expect(onThemeChange).toHaveBeenCalledWith("light");365366 expect(sendMessageMock).toHaveBeenCalledWith({367 type: "CHANGE_PASSWORD",368 payload: {369 currentPassword: "current-pass",370 newPassword: "new-password-123",371 },372 });373 });374375 it("Entry copy actions send COPY and canary ping messages for password and username", async () => {376 sendMessageMock.mockResolvedValue({ ok: true });377 const container = mount(378 <EntryView379 entry={sampleEntry()}380 onDelete={vi.fn()}381 onFieldSaved={vi.fn(async () => undefined)}382 />383 );384385 const passwordButton = container.querySelector('button[aria-label="Copy password"]') as HTMLButtonElement;386 passwordButton.click();387 await flushView();388389 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {390 type: "COPY",391 payload: { text: "secret123", label: "password", alreadyWritten: false },392 });393 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {394 type: "FIRE_CANARY_PING",395 payload: {396 webhookUrl: "https://example.com/hook",397 entryTitle: "GitHub",398 trigger: "password_copied",399 },400 });401402 sendMessageMock.mockClear();403 const usernameButton = container.querySelector('button[aria-label="Copy username"]') as HTMLButtonElement;404 usernameButton.click();405 await flushView();406407 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {408 type: "COPY",409 payload: { text: "june", label: "username", alreadyWritten: false },410 });411 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {412 type: "FIRE_CANARY_PING",413 payload: {414 webhookUrl: "https://example.com/hook",415 entryTitle: "GitHub",416 trigger: "username_copied",417 },418 });419 });420421 it("Entry refuses to send an empty password update and copies TOTP through COPY", async () => {422 sendMessageMock.mockResolvedValue({ ok: true });423 const onFieldSaved = vi.fn(async () => undefined);424 const container = mount(425 <EntryView426 entry={sampleEntry()}427 onDelete={vi.fn()}428 onFieldSaved={onFieldSaved}429 />430 );431432 const passwordValue = container.querySelector(".peach-password-value") as HTMLSpanElement;433 passwordValue.click();434 await flushView();435436 const passwordInput = container.querySelector('input[placeholder="paste or type a password"]') as HTMLInputElement;437 inputText(passwordInput, "");438 await flushView();439440 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;441 saveButton.click();442 await flushView();443444 expect(sendMessageMock).not.toHaveBeenCalledWith(445 expect.objectContaining({ type: "UPDATE_ENTRY" })446 );447 expect(onFieldSaved).not.toHaveBeenCalled();448449 const totpMain = container.querySelector(".peach-totp-main") as HTMLDivElement;450 totpMain.click();451 await flushView();452453 expect(sendMessageMock).toHaveBeenLastCalledWith({454 type: "COPY",455 payload: { text: "123456", label: "totp", alreadyWritten: false },456 });457 });458459 it("Entry closes an unchanged inline editor when focus leaves it", async () => {460 sendMessageMock.mockResolvedValue({ ok: true });461 const container = mount(462 <EntryView463 entry={sampleEntry()}464 onDelete={vi.fn()}465 onFieldSaved={vi.fn(async () => undefined)}466 />467 );468469 const usernameValue = container.querySelector(".peach-field-value.peach-inline-clickable") as HTMLSpanElement;470 usernameValue.click();471 await flushView();472473 const usernameInput = container.querySelector(".peach-inline-input[autocomplete=\"off\"]") as HTMLInputElement;474 usernameInput.blur();475 await flushView();476477 expect(container.querySelector(".peach-inline-input[autocomplete=\"off\"]")).toBeNull();478 expect(sendMessageMock).not.toHaveBeenCalledWith(479 expect.objectContaining({ type: "UPDATE_ENTRY" })480 );481 });482483 it("Entry lets TOTP secrets be edited inline", async () => {484 sendMessageMock.mockResolvedValue({ ok: true });485 const onFieldSaved = vi.fn(async () => undefined);486 const container = mount(487 <EntryView488 entry={sampleEntry()}489 onDelete={vi.fn()}490 onFieldSaved={onFieldSaved}491 />492 );493494 const totpMain = container.querySelector(".peach-totp-main") as HTMLDivElement;495 totpMain.dispatchEvent(new MouseEvent("dblclick", { bubbles: true }));496 await flushView();497498 const totpInput = container.querySelector('input[placeholder="otpauth://... or base32 secret"]') as HTMLInputElement;499 inputText(totpInput, "otpauth://totp/Example?secret=NB2W45DFOIZA====&issuer=Peach");500 await flushView();501502 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;503 saveButton.click();504 await flushView();505506 expect(sendMessageMock).toHaveBeenCalledWith({507 type: "UPDATE_ENTRY",508 payload: {509 id: "entry-1",510 patch: { totp: "NB2W45DFOIZA====" },511 },512 });513 expect(onFieldSaved).toHaveBeenCalled();514 });515516 it("Vault seeds and focuses search when a search seed is provided", async () => {517 const container = mount(518 <VaultView519 entries={[sampleEntry(), sampleEntry({ id: "entry-2", title: "GitLab" })]}520 sortOrder="url-first"521 searchSeed="g"522 searchSeedNonce={1}523 onSelectEntry={vi.fn()}524 onSortOrderChange={vi.fn()}525 onAdd={vi.fn()}526 onClose={vi.fn()}527 />528 );529530 await flushView();531532 const search = container.querySelector(".peach-list-top .peach-search-input") as HTMLInputElement;533 expect(search.value).toBe("g");534 expect(document.activeElement).toBe(search);535 });536537 it("Import existing-vault add mode parses a file and batches entries into the vault", async () => {538 parseImportSourceMock.mockResolvedValue(539 sampleImportResult({540 entries: [541 {542 type: "login",543 title: "Imported site",544 username: "imported-user",545 password: "imported-pass",546 url: "https://imported.test",547 },548 ],549 })550 );551 sendMessageMock.mockImplementation(async (msg: unknown) => {552 const type = (msg as { type?: string }).type;553 if (type === "GET_ENTRIES") {554 return {555 ok: true,556 data: [557 sampleEntry({558 id: "existing-1",559 title: "Imported site",560 username: "imported-user",561 url: "https://imported.test",562 }),563 ],564 };565 }566 if (type === "BATCH_ADD_ENTRIES") {567 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };568 }569 return { ok: true };570 });571 const onImport = vi.fn(async () => undefined);572 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);573 await flushView();574 sendMessageMock.mockClear();575576 await selectImportFile(container);577 findButton(container, "Import 1").click();578 await flushView();579580 expect(parseImportSourceMock).toHaveBeenCalledWith('{"export":"test"}', undefined);581 expect(sendMessageMock).toHaveBeenCalledWith({582 type: "BATCH_ADD_ENTRIES",583 payload: {584 entries: [585 expect.objectContaining({586 title: "Imported site",587 username: "imported-user",588 password: "imported-pass",589 url: "https://imported.test",590 }),591 ],592 },593 });594 expect(onImport).toHaveBeenCalled();595 expect(container.textContent).toContain("1 entry appears to be duplicates");596 });597598 it("Import replace mode requires confirmation before clearing and replacing the vault", async () => {599 parseImportSourceMock.mockResolvedValue(sampleImportResult());600 sendMessageMock.mockImplementation(async (msg: unknown) => {601 const type = (msg as { type?: string }).type;602 if (type === "GET_ENTRIES") {603 return { ok: true, data: [sampleEntry({ id: "existing-1" })] };604 }605 if (type === "CLEAR_VAULT_ENTRIES") {606 return { ok: true };607 }608 if (type === "BATCH_ADD_ENTRIES") {609 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };610 }611 return { ok: true };612 });613 const onImport = vi.fn(async () => undefined);614 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);615 await flushView();616 sendMessageMock.mockClear();617618 await selectImportFile(container);619 (container.querySelector('input[value="replace"]') as HTMLInputElement).click();620 await flushView();621622 findButton(container, "Replace vault with 1").click();623 await flushView();624625 expect(sendMessageMock).not.toHaveBeenCalledWith(626 expect.objectContaining({ type: "CLEAR_VAULT_ENTRIES" })627 );628 expect(container.textContent).toContain("Replace forever? This cannot be undone.");629630 findButton(container, "Yes, replace forever").click();631 await flushView();632633 expect(sendMessageMock).toHaveBeenCalledWith({ type: "CLEAR_VAULT_ENTRIES" });634 expect(sendMessageMock).toHaveBeenCalledWith({635 type: "BATCH_ADD_ENTRIES",636 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },637 });638 expect(onImport).toHaveBeenCalled();639 });640641 it("Import onboarding mode waits for a matching master password before creating a vault", async () => {642 parseImportSourceMock.mockResolvedValue(sampleImportResult());643 sendMessageMock.mockImplementation(async (msg: unknown) => {644 const type = (msg as { type?: string }).type;645 if (type === "CREATE_VAULT") return { ok: true };646 if (type === "BATCH_ADD_ENTRIES") {647 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };648 }649 return { ok: true };650 });651 const onImport = vi.fn(async () => undefined);652 const container = mount(653 <ImportView654 mode="onboarding"655 replacesExistingVault={true}656 onBack={vi.fn()}657 onImport={onImport}658 />659 );660661 await selectImportFile(container);662663 const createButton = findButton(container, "Create vault + import 1");664 expect(createButton.disabled).toBe(true);665 expect(container.textContent).toContain("Restoring now replaces the locked local vault");666667 const masterInput = container.querySelector(668 'input[placeholder="new master password"]'669 ) as HTMLInputElement;670 const confirmInput = container.querySelector(671 'input[placeholder="confirm master password"]'672 ) as HTMLInputElement;673 inputText(masterInput, "fresh-master-password");674 inputText(confirmInput, "fresh-master-password");675 await flushView();676677 expect(createButton.disabled).toBe(false);678 createButton.click();679 await flushView();680681 expect(sendMessageMock).toHaveBeenCalledWith({682 type: "CREATE_VAULT",683 payload: { password: "fresh-master-password" },684 });685 expect(sendMessageMock).toHaveBeenCalledWith({686 type: "BATCH_ADD_ENTRIES",687 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },688 });689 expect(onImport).toHaveBeenCalled();690 });691692 it("Import encrypted backups reparse with the backup password before import", async () => {693 parseImportSourceMock694 .mockResolvedValueOnce(695 sampleImportResult({696 format: "peach-json",697 label: "Peach encrypted backup",698 entries: [],699 totalItems: 0,700 requiresPassword: true,701 })702 )703 .mockResolvedValueOnce(704 sampleImportResult({705 format: "peach-json",706 label: "Peach encrypted backup",707 })708 );709 sendMessageMock.mockImplementation(async (msg: unknown) => {710 const type = (msg as { type?: string }).type;711 if (type === "GET_ENTRIES") return { ok: true, data: [] };712 if (type === "BATCH_ADD_ENTRIES") {713 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };714 }715 return { ok: true };716 });717 const onImport = vi.fn(async () => undefined);718 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);719 await flushView();720 sendMessageMock.mockClear();721722 await selectImportFile(container, '{"type":"peach-export"}');723 const password = container.querySelector('input[placeholder="backup password"]') as HTMLInputElement;724 inputText(password, "backup-passphrase");725 pressEnter(password);726 await flushView();727 await flushView();728729 expect(parseImportSourceMock).toHaveBeenNthCalledWith(1, '{"type":"peach-export"}', undefined);730 expect(parseImportSourceMock).toHaveBeenNthCalledWith(2, '{"type":"peach-export"}', "backup-passphrase");731732 findButton(container, "Import 1").click();733 await flushView();734735 expect(sendMessageMock).toHaveBeenCalledWith({736 type: "BATCH_ADD_ENTRIES",737 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },738 });739 expect(onImport).toHaveBeenCalled();740 });741});