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 210 sendMessageMock.mockImplementation(211 async (msg: unknown) => {212 const m = msg as { type: string; payload?: { password?: string } };213 if (m.type === "CREATE_VAULT") return { ok: true };214 if (m.type === "GET_RECOVERY_WORDS") {215 216 return {217 ok: true,218 data: [219 "abandon", "ability", "able", "about", "above", "absent",220 "absorb", "abstract", "absurd", "abuse", "access", "accident",221 ],222 };223 }224 return { ok: true };225 }226 );227 zxcvbnMock.mockImplementation(() => ({ score: 3 }));228 const onUnlock = vi.fn(async () => undefined);229 const container = mount(<LockView hasVault={false} ready={true} onUnlock={onUnlock} />);230231 const password = container.querySelector("#master-password") as HTMLInputElement;232 const confirm = container.querySelector("#confirm-password") as HTMLInputElement;233 const createButton = container.querySelector(".peach-lock-submit") as HTMLButtonElement;234235 inputText(password, "correct horse battery staple");236 inputText(confirm, "correct horse battery staple");237 await flushView();238 expect(createButton.disabled).toBe(false);239240 pressEnter(confirm);241 await flushView();242243 expect(sendMessageMock).toHaveBeenCalledWith({244 type: "CREATE_VAULT",245 payload: { password: "correct horse battery staple" },246 });247 248 249 expect(onUnlock).not.toHaveBeenCalled();250 });251252 it("Export menu surfaces both backup formats", async () => {253 const container = mount(<ExportView onBack={vi.fn()} />);254 await flushView();255256 expect(container.textContent).toContain("PeachScript");257 expect(container.textContent).toContain("CSV");258 });259260 it("Settings requires current, new, and confirm passwords before enabling save", async () => {261 sendMessageMock.mockResolvedValue({ ok: true, data: null });262 const container = mount(263 <SettingsView264 entriesCount={1}265 breachMonitoring={true}266 lockOnClose={true}267 autoLockMinutes={5}268 trashRetentionDays={30}269 theme="dark"270 accentColor="peach"271 displayFont="cormorant-italic"272 textSize="medium"273 reduceMotion={false}274 onBreachMonitoringChange={vi.fn()}275 onLockOnCloseChange={vi.fn()}276 onAutoLockMinutesChange={vi.fn()}277 onTrashRetentionChange={vi.fn()}278 onThemeChange={vi.fn()}279 onAccentChange={vi.fn()}280 onDisplayFontChange={vi.fn()}281 onTextSizeChange={vi.fn()}282 onReduceMotionChange={vi.fn()}283 shortcuts={DEFAULT_BINDINGS}284 onShortcutsChange={vi.fn()}285 onBack={vi.fn()}286 onImport={vi.fn()}287 onExport={vi.fn()}288 onBreachCheckDone={vi.fn()}289 />290 );291 await flushView();292293 const securityRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(294 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Security"295 ) as HTMLButtonElement;296 securityRow.click();297 await flushView();298299 const changeButton = Array.from(container.querySelectorAll("button")).find(300 (candidate) => candidate.textContent?.trim() === "change"301 ) as HTMLButtonElement;302 changeButton.click();303 await flushView();304305 const saveButton = Array.from(container.querySelectorAll("button")).find(306 (candidate) => candidate.textContent?.trim() === "save new password"307 ) as HTMLButtonElement;308 expect(saveButton.disabled).toBe(true);309310 const inputs = container.querySelectorAll('input[type="password"]');311 inputText(inputs[0] as HTMLInputElement, "current-pass");312 inputText(inputs[1] as HTMLInputElement, "new-pass-12345");313 await flushView();314 expect(saveButton.disabled).toBe(true);315316 inputText(inputs[2] as HTMLInputElement, "new-pass-12345");317 await flushView();318 expect(saveButton.disabled).toBe(false);319 });320321 it("Settings sends CHANGE_PASSWORD and forwards auto-lock/theme changes through callbacks", async () => {322 sendMessageMock.mockImplementation(async (msg: unknown) => {323 const m = msg as { type: string };324 if (m.type === "GET_RECOVERY_WORDS") {325 return {326 ok: true,327 data: [328 "abandon", "ability", "able", "about", "above", "absent",329 "absorb", "abstract", "absurd", "abuse", "access", "accident",330 ],331 };332 }333 return { ok: true, data: null };334 });335 const onAutoLockMinutesChange = vi.fn();336 const onThemeChange = vi.fn();337 const container = mount(338 <SettingsView339 entriesCount={1}340 breachMonitoring={true}341 lockOnClose={true}342 autoLockMinutes={5}343 trashRetentionDays={30}344 theme="dark"345 accentColor="peach"346 displayFont="cormorant-italic"347 textSize="medium"348 reduceMotion={false}349 onBreachMonitoringChange={vi.fn()}350 onLockOnCloseChange={vi.fn()}351 onAutoLockMinutesChange={onAutoLockMinutesChange}352 onTrashRetentionChange={vi.fn()}353 onThemeChange={onThemeChange}354 onAccentChange={vi.fn()}355 onDisplayFontChange={vi.fn()}356 onTextSizeChange={vi.fn()}357 onReduceMotionChange={vi.fn()}358 shortcuts={DEFAULT_BINDINGS}359 onShortcutsChange={vi.fn()}360 onBack={vi.fn()}361 onImport={vi.fn()}362 onExport={vi.fn()}363 onBreachCheckDone={vi.fn()}364 />365 );366 await flushView();367 sendMessageMock.mockClear();368369 function headerBack() {370 return container.querySelector(".peach-overlay-header button") as HTMLButtonElement;371 }372373 374 const securityRow = () =>375 Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(376 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Security"377 ) as HTMLButtonElement;378379 securityRow().click();380 await flushView();381382 const selects = container.querySelectorAll("select");383 changeSelect(selects[0] as HTMLSelectElement, "15");384 expect(onAutoLockMinutesChange).toHaveBeenCalledWith(15);385386 headerBack().click();387 await flushView();388389 const appearanceRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(390 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Appearance"391 ) as HTMLButtonElement;392 appearanceRow.click();393 await flushView();394 const themeButton = Array.from(container.querySelectorAll("button")).find(395 (candidate) => candidate.textContent?.trim() === "Switch to light"396 ) as HTMLButtonElement;397 themeButton.click();398 expect(onThemeChange).toHaveBeenCalledWith("light");399400 401 headerBack().click();402 await flushView();403404 securityRow().click();405 await flushView();406407 const changeButton = Array.from(container.querySelectorAll("button")).find(408 (candidate) => candidate.textContent?.trim() === "change"409 ) as HTMLButtonElement;410 changeButton.click();411 await flushView();412413 const inputs = container.querySelectorAll('input[type="password"]');414 inputText(inputs[0] as HTMLInputElement, "current-pass");415 inputText(inputs[1] as HTMLInputElement, "new-password-123");416 inputText(inputs[2] as HTMLInputElement, "new-password-123");417 await flushView();418419 const saveButton = Array.from(container.querySelectorAll("button")).find(420 (candidate) => candidate.textContent?.trim() === "save new password"421 ) as HTMLButtonElement;422 saveButton.click();423 await flushView();424425 expect(sendMessageMock).toHaveBeenCalledWith({426 type: "CHANGE_PASSWORD",427 payload: {428 currentPassword: "current-pass",429 newPassword: "new-password-123",430 },431 });432433 434 expect(container.textContent).toContain("save your recovery words");435 expect(sendMessageMock).toHaveBeenCalledWith({ type: "GET_RECOVERY_WORDS" });436 });437438 it("Entry copy actions send COPY and canary ping messages for password and username", async () => {439 sendMessageMock.mockResolvedValue({ ok: true });440 const container = mount(441 <EntryView442 entry={sampleEntry()}443 onDelete={vi.fn()}444 onFieldSaved={vi.fn(async () => undefined)}445 />446 );447448 const passwordButton = container.querySelector('button[aria-label="Copy password"]') as HTMLButtonElement;449 passwordButton.click();450 await flushView();451452 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {453 type: "COPY",454 payload: { text: "secret123", label: "password", alreadyWritten: false },455 });456 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {457 type: "FIRE_CANARY_PING",458 payload: {459 webhookUrl: "https://example.com/hook",460 entryTitle: "GitHub",461 trigger: "password_copied",462 },463 });464465 sendMessageMock.mockClear();466 const usernameButton = container.querySelector('button[aria-label="Copy username"]') as HTMLButtonElement;467 usernameButton.click();468 await flushView();469470 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {471 type: "COPY",472 payload: { text: "june", label: "username", alreadyWritten: false },473 });474 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {475 type: "FIRE_CANARY_PING",476 payload: {477 webhookUrl: "https://example.com/hook",478 entryTitle: "GitHub",479 trigger: "username_copied",480 },481 });482 });483484 it("Entry refuses to send an empty password update and copies TOTP through COPY", async () => {485 sendMessageMock.mockResolvedValue({ ok: true });486 const onFieldSaved = vi.fn(async () => undefined);487 const container = mount(488 <EntryView489 entry={sampleEntry()}490 onDelete={vi.fn()}491 onFieldSaved={onFieldSaved}492 />493 );494495 const passwordValue = container.querySelector(".peach-password-value") as HTMLSpanElement;496 passwordValue.click();497 await flushView();498499 const passwordInput = container.querySelector('input[placeholder="paste or type a password"]') as HTMLInputElement;500 inputText(passwordInput, "");501 await flushView();502503 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;504 saveButton.click();505 await flushView();506507 expect(sendMessageMock).not.toHaveBeenCalledWith(508 expect.objectContaining({ type: "UPDATE_ENTRY" })509 );510 expect(onFieldSaved).not.toHaveBeenCalled();511512 const totpMain = container.querySelector(".peach-totp-main") as HTMLDivElement;513 totpMain.click();514 await flushView();515516 expect(sendMessageMock).toHaveBeenLastCalledWith({517 type: "COPY",518 payload: { text: "123456", label: "totp", alreadyWritten: false },519 });520 });521522 it("Entry closes an unchanged inline editor when focus leaves it", async () => {523 sendMessageMock.mockResolvedValue({ ok: true });524 const container = mount(525 <EntryView526 entry={sampleEntry()}527 onDelete={vi.fn()}528 onFieldSaved={vi.fn(async () => undefined)}529 />530 );531532 const usernameValue = container.querySelector(".peach-field-value.peach-inline-clickable") as HTMLSpanElement;533 usernameValue.click();534 await flushView();535536 const usernameInput = container.querySelector(".peach-inline-input[autocomplete=\"off\"]") as HTMLInputElement;537 usernameInput.blur();538 await flushView();539540 expect(container.querySelector(".peach-inline-input[autocomplete=\"off\"]")).toBeNull();541 expect(sendMessageMock).not.toHaveBeenCalledWith(542 expect.objectContaining({ type: "UPDATE_ENTRY" })543 );544 });545546 it("Entry lets TOTP secrets be edited inline", async () => {547 sendMessageMock.mockResolvedValue({ ok: true });548 const onFieldSaved = vi.fn(async () => undefined);549 const container = mount(550 <EntryView551 entry={sampleEntry()}552 onDelete={vi.fn()}553 onFieldSaved={onFieldSaved}554 />555 );556557 const totpMain = container.querySelector(".peach-totp-main") as HTMLDivElement;558 totpMain.dispatchEvent(new MouseEvent("dblclick", { bubbles: true }));559 await flushView();560561 const totpInput = container.querySelector('input[placeholder="otpauth://... or base32 secret"]') as HTMLInputElement;562 inputText(totpInput, "otpauth://totp/Example?secret=NB2W45DFOIZA====&issuer=Peach");563 await flushView();564565 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;566 saveButton.click();567 await flushView();568569 expect(sendMessageMock).toHaveBeenCalledWith({570 type: "UPDATE_ENTRY",571 payload: {572 id: "entry-1",573 patch: { totp: "NB2W45DFOIZA====" },574 },575 });576 expect(onFieldSaved).toHaveBeenCalled();577 });578579 it("Vault seeds and focuses search when a search seed is provided", async () => {580 const container = mount(581 <VaultView582 entries={[sampleEntry(), sampleEntry({ id: "entry-2", title: "GitLab" })]}583 sortOrder="url-first"584 searchSeed="g"585 searchSeedNonce={1}586 onSelectEntry={vi.fn()}587 onSortOrderChange={vi.fn()}588 onAdd={vi.fn()}589 onClose={vi.fn()}590 />591 );592593 await flushView();594595 const search = container.querySelector(".peach-list-top .peach-search-input") as HTMLInputElement;596 expect(search.value).toBe("g");597 expect(document.activeElement).toBe(search);598 });599600 it("Import existing-vault add mode parses a file and batches entries into the vault", async () => {601 parseImportSourceMock.mockResolvedValue(602 sampleImportResult({603 entries: [604 {605 type: "login",606 title: "Imported site",607 username: "imported-user",608 password: "imported-pass",609 url: "https://imported.test",610 },611 ],612 })613 );614 sendMessageMock.mockImplementation(async (msg: unknown) => {615 const type = (msg as { type?: string }).type;616 if (type === "GET_ENTRIES") {617 return {618 ok: true,619 data: [620 sampleEntry({621 id: "existing-1",622 title: "Imported site",623 username: "imported-user",624 url: "https://imported.test",625 }),626 ],627 };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 findButton(container, "Import 1").click();641 await flushView();642643 expect(parseImportSourceMock).toHaveBeenCalledWith('{"export":"test"}', undefined);644 expect(sendMessageMock).toHaveBeenCalledWith({645 type: "BATCH_ADD_ENTRIES",646 payload: {647 entries: [648 expect.objectContaining({649 title: "Imported site",650 username: "imported-user",651 password: "imported-pass",652 url: "https://imported.test",653 }),654 ],655 },656 });657 expect(onImport).toHaveBeenCalled();658 expect(container.textContent).toContain("1 entry appears to be duplicates");659 });660661 it("Import replace mode requires confirmation before clearing and replacing the vault", async () => {662 parseImportSourceMock.mockResolvedValue(sampleImportResult());663 sendMessageMock.mockImplementation(async (msg: unknown) => {664 const type = (msg as { type?: string }).type;665 if (type === "GET_ENTRIES") {666 return { ok: true, data: [sampleEntry({ id: "existing-1" })] };667 }668 if (type === "CLEAR_VAULT_ENTRIES") {669 return { ok: true };670 }671 if (type === "BATCH_ADD_ENTRIES") {672 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };673 }674 return { ok: true };675 });676 const onImport = vi.fn(async () => undefined);677 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);678 await flushView();679 sendMessageMock.mockClear();680681 await selectImportFile(container);682 (container.querySelector('input[value="replace"]') as HTMLInputElement).click();683 await flushView();684685 findButton(container, "Replace vault with 1").click();686 await flushView();687688 expect(sendMessageMock).not.toHaveBeenCalledWith(689 expect.objectContaining({ type: "CLEAR_VAULT_ENTRIES" })690 );691 expect(container.textContent).toContain("Replace forever? This cannot be undone.");692693 findButton(container, "Yes, replace forever").click();694 await flushView();695696 expect(sendMessageMock).toHaveBeenCalledWith({ type: "CLEAR_VAULT_ENTRIES" });697 expect(sendMessageMock).toHaveBeenCalledWith({698 type: "BATCH_ADD_ENTRIES",699 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },700 });701 expect(onImport).toHaveBeenCalled();702 });703704 it("Import onboarding mode waits for a matching master password before creating a vault", async () => {705 parseImportSourceMock.mockResolvedValue(sampleImportResult());706 sendMessageMock.mockImplementation(async (msg: unknown) => {707 const type = (msg as { type?: string }).type;708 if (type === "CREATE_VAULT") return { ok: true };709 if (type === "BATCH_ADD_ENTRIES") {710 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };711 }712 return { ok: true };713 });714 const onImport = vi.fn(async () => undefined);715 const container = mount(716 <ImportView717 mode="onboarding"718 replacesExistingVault={true}719 onBack={vi.fn()}720 onImport={onImport}721 />722 );723724 await selectImportFile(container);725726 const createButton = findButton(container, "Create vault + import 1");727 expect(createButton.disabled).toBe(true);728 expect(container.textContent).toContain("Restoring now replaces the locked local vault");729730 const masterInput = container.querySelector(731 'input[placeholder="new master password"]'732 ) as HTMLInputElement;733 const confirmInput = container.querySelector(734 'input[placeholder="confirm master password"]'735 ) as HTMLInputElement;736 inputText(masterInput, "fresh-master-password");737 inputText(confirmInput, "fresh-master-password");738 await flushView();739740 expect(createButton.disabled).toBe(false);741 createButton.click();742 await flushView();743744 expect(sendMessageMock).toHaveBeenCalledWith({745 type: "CREATE_VAULT",746 payload: { password: "fresh-master-password" },747 });748 expect(sendMessageMock).toHaveBeenCalledWith({749 type: "BATCH_ADD_ENTRIES",750 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },751 });752 expect(onImport).toHaveBeenCalled();753 });754755 it("Import encrypted backups reparse with the backup password before import", async () => {756 parseImportSourceMock757 .mockResolvedValueOnce(758 sampleImportResult({759 format: "peach-json",760 label: "Peach encrypted backup",761 entries: [],762 totalItems: 0,763 requiresPassword: true,764 })765 )766 .mockResolvedValueOnce(767 sampleImportResult({768 format: "peach-json",769 label: "Peach encrypted backup",770 })771 );772 sendMessageMock.mockImplementation(async (msg: unknown) => {773 const type = (msg as { type?: string }).type;774 if (type === "GET_ENTRIES") return { ok: true, data: [] };775 if (type === "BATCH_ADD_ENTRIES") {776 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };777 }778 return { ok: true };779 });780 const onImport = vi.fn(async () => undefined);781 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);782 await flushView();783 sendMessageMock.mockClear();784785 await selectImportFile(container, '{"type":"peach-export"}');786 const password = container.querySelector('input[placeholder="backup password"]') as HTMLInputElement;787 inputText(password, "backup-passphrase");788 pressEnter(password);789 await flushView();790 await flushView();791792 expect(parseImportSourceMock).toHaveBeenNthCalledWith(1, '{"type":"peach-export"}', undefined);793 expect(parseImportSourceMock).toHaveBeenNthCalledWith(2, '{"type":"peach-export"}', "backup-passphrase");794795 findButton(container, "Import 1").click();796 await flushView();797798 expect(sendMessageMock).toHaveBeenCalledWith({799 type: "BATCH_ADD_ENTRIES",800 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },801 });802 expect(onImport).toHaveBeenCalled();803 });804});