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 the three v2.5.0 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("print backup");258 expect(container.textContent).toContain("CSV");259 });260261 it("Settings requires current, new, and confirm passwords before enabling save", async () => {262 sendMessageMock.mockResolvedValue({ ok: true, data: null });263 const container = mount(264 <SettingsView265 entriesCount={1}266 breachMonitoring={true}267 lockOnClose={true}268 autoLockMinutes={5}269 trashRetentionDays={30}270 theme="dark"271 accentColor="peach"272 displayFont="cormorant-italic"273 textSize="medium"274 reduceMotion={false}275 onBreachMonitoringChange={vi.fn()}276 onLockOnCloseChange={vi.fn()}277 onAutoLockMinutesChange={vi.fn()}278 onTrashRetentionChange={vi.fn()}279 onThemeChange={vi.fn()}280 onAccentChange={vi.fn()}281 onDisplayFontChange={vi.fn()}282 onTextSizeChange={vi.fn()}283 onReduceMotionChange={vi.fn()}284 shortcuts={DEFAULT_BINDINGS}285 onShortcutsChange={vi.fn()}286 onBack={vi.fn()}287 onImport={vi.fn()}288 onExport={vi.fn()}289 onBreachCheckDone={vi.fn()}290 />291 );292 await flushView();293294 const securityRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(295 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Security"296 ) as HTMLButtonElement;297 securityRow.click();298 await flushView();299300 const changeButton = Array.from(container.querySelectorAll("button")).find(301 (candidate) => candidate.textContent?.trim() === "change"302 ) as HTMLButtonElement;303 changeButton.click();304 await flushView();305306 const saveButton = Array.from(container.querySelectorAll("button")).find(307 (candidate) => candidate.textContent?.trim() === "save new password"308 ) as HTMLButtonElement;309 expect(saveButton.disabled).toBe(true);310311 const inputs = container.querySelectorAll('input[type="password"]');312 inputText(inputs[0] as HTMLInputElement, "current-pass");313 inputText(inputs[1] as HTMLInputElement, "new-pass-12345");314 await flushView();315 expect(saveButton.disabled).toBe(true);316317 inputText(inputs[2] as HTMLInputElement, "new-pass-12345");318 await flushView();319 expect(saveButton.disabled).toBe(false);320 });321322 it("Settings sends CHANGE_PASSWORD and forwards auto-lock/theme changes through callbacks", async () => {323 sendMessageMock.mockImplementation(async (msg: unknown) => {324 const m = msg as { type: string };325 if (m.type === "GET_RECOVERY_WORDS") {326 return {327 ok: true,328 data: [329 "abandon", "ability", "able", "about", "above", "absent",330 "absorb", "abstract", "absurd", "abuse", "access", "accident",331 ],332 };333 }334 return { ok: true, data: null };335 });336 const onAutoLockMinutesChange = vi.fn();337 const onThemeChange = vi.fn();338 const container = mount(339 <SettingsView340 entriesCount={1}341 breachMonitoring={true}342 lockOnClose={true}343 autoLockMinutes={5}344 trashRetentionDays={30}345 theme="dark"346 accentColor="peach"347 displayFont="cormorant-italic"348 textSize="medium"349 reduceMotion={false}350 onBreachMonitoringChange={vi.fn()}351 onLockOnCloseChange={vi.fn()}352 onAutoLockMinutesChange={onAutoLockMinutesChange}353 onTrashRetentionChange={vi.fn()}354 onThemeChange={onThemeChange}355 onAccentChange={vi.fn()}356 onDisplayFontChange={vi.fn()}357 onTextSizeChange={vi.fn()}358 onReduceMotionChange={vi.fn()}359 shortcuts={DEFAULT_BINDINGS}360 onShortcutsChange={vi.fn()}361 onBack={vi.fn()}362 onImport={vi.fn()}363 onExport={vi.fn()}364 onBreachCheckDone={vi.fn()}365 />366 );367 await flushView();368 sendMessageMock.mockClear();369370 function headerBack() {371 return container.querySelector(".peach-overlay-header button") as HTMLButtonElement;372 }373374 375 const securityRow = () =>376 Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(377 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Security"378 ) as HTMLButtonElement;379380 securityRow().click();381 await flushView();382383 const selects = container.querySelectorAll("select");384 changeSelect(selects[0] as HTMLSelectElement, "15");385 expect(onAutoLockMinutesChange).toHaveBeenCalledWith(15);386387 headerBack().click();388 await flushView();389390 const appearanceRow = Array.from(container.querySelectorAll("button.peach-settings-cat-row")).find(391 (b) => b.querySelector(".peach-settings-cat-name")?.textContent?.trim() === "Appearance"392 ) as HTMLButtonElement;393 appearanceRow.click();394 await flushView();395 const themeButton = Array.from(container.querySelectorAll("button")).find(396 (candidate) => candidate.textContent?.trim() === "Switch to light"397 ) as HTMLButtonElement;398 themeButton.click();399 expect(onThemeChange).toHaveBeenCalledWith("light");400401 402 headerBack().click();403 await flushView();404405 securityRow().click();406 await flushView();407408 const changeButton = Array.from(container.querySelectorAll("button")).find(409 (candidate) => candidate.textContent?.trim() === "change"410 ) as HTMLButtonElement;411 changeButton.click();412 await flushView();413414 const inputs = container.querySelectorAll('input[type="password"]');415 inputText(inputs[0] as HTMLInputElement, "current-pass");416 inputText(inputs[1] as HTMLInputElement, "new-password-123");417 inputText(inputs[2] as HTMLInputElement, "new-password-123");418 await flushView();419420 const saveButton = Array.from(container.querySelectorAll("button")).find(421 (candidate) => candidate.textContent?.trim() === "save new password"422 ) as HTMLButtonElement;423 saveButton.click();424 await flushView();425426 expect(sendMessageMock).toHaveBeenCalledWith({427 type: "CHANGE_PASSWORD",428 payload: {429 currentPassword: "current-pass",430 newPassword: "new-password-123",431 },432 });433434 435 expect(container.textContent).toContain("save your recovery words");436 expect(sendMessageMock).toHaveBeenCalledWith({ type: "GET_RECOVERY_WORDS" });437 });438439 it("Entry copy actions send COPY and canary ping messages for password and username", async () => {440 sendMessageMock.mockResolvedValue({ ok: true });441 const container = mount(442 <EntryView443 entry={sampleEntry()}444 onDelete={vi.fn()}445 onFieldSaved={vi.fn(async () => undefined)}446 />447 );448449 const passwordButton = container.querySelector('button[aria-label="Copy password"]') as HTMLButtonElement;450 passwordButton.click();451 await flushView();452453 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {454 type: "COPY",455 payload: { text: "secret123", label: "password", alreadyWritten: false },456 });457 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {458 type: "FIRE_CANARY_PING",459 payload: {460 webhookUrl: "https://example.com/hook",461 entryTitle: "GitHub",462 trigger: "password_copied",463 },464 });465466 sendMessageMock.mockClear();467 const usernameButton = container.querySelector('button[aria-label="Copy username"]') as HTMLButtonElement;468 usernameButton.click();469 await flushView();470471 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {472 type: "COPY",473 payload: { text: "june", label: "username", alreadyWritten: false },474 });475 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {476 type: "FIRE_CANARY_PING",477 payload: {478 webhookUrl: "https://example.com/hook",479 entryTitle: "GitHub",480 trigger: "username_copied",481 },482 });483 });484485 it("Entry refuses to send an empty password update and copies TOTP through COPY", async () => {486 sendMessageMock.mockResolvedValue({ ok: true });487 const onFieldSaved = vi.fn(async () => undefined);488 const container = mount(489 <EntryView490 entry={sampleEntry()}491 onDelete={vi.fn()}492 onFieldSaved={onFieldSaved}493 />494 );495496 const passwordValue = container.querySelector(".peach-password-value") as HTMLSpanElement;497 passwordValue.click();498 await flushView();499500 const passwordInput = container.querySelector('input[placeholder="paste or type a password"]') as HTMLInputElement;501 inputText(passwordInput, "");502 await flushView();503504 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;505 saveButton.click();506 await flushView();507508 expect(sendMessageMock).not.toHaveBeenCalledWith(509 expect.objectContaining({ type: "UPDATE_ENTRY" })510 );511 expect(onFieldSaved).not.toHaveBeenCalled();512513 const totpMain = container.querySelector(".peach-totp-main") as HTMLDivElement;514 totpMain.click();515 await flushView();516517 expect(sendMessageMock).toHaveBeenLastCalledWith({518 type: "COPY",519 payload: { text: "123456", label: "totp", alreadyWritten: false },520 });521 });522523 it("Entry closes an unchanged inline editor when focus leaves it", async () => {524 sendMessageMock.mockResolvedValue({ ok: true });525 const container = mount(526 <EntryView527 entry={sampleEntry()}528 onDelete={vi.fn()}529 onFieldSaved={vi.fn(async () => undefined)}530 />531 );532533 const usernameValue = container.querySelector(".peach-field-value.peach-inline-clickable") as HTMLSpanElement;534 usernameValue.click();535 await flushView();536537 const usernameInput = container.querySelector(".peach-inline-input[autocomplete=\"off\"]") as HTMLInputElement;538 usernameInput.blur();539 await flushView();540541 expect(container.querySelector(".peach-inline-input[autocomplete=\"off\"]")).toBeNull();542 expect(sendMessageMock).not.toHaveBeenCalledWith(543 expect.objectContaining({ type: "UPDATE_ENTRY" })544 );545 });546547 it("Entry lets TOTP secrets be edited inline", async () => {548 sendMessageMock.mockResolvedValue({ ok: true });549 const onFieldSaved = vi.fn(async () => undefined);550 const container = mount(551 <EntryView552 entry={sampleEntry()}553 onDelete={vi.fn()}554 onFieldSaved={onFieldSaved}555 />556 );557558 const totpMain = container.querySelector(".peach-totp-main") as HTMLDivElement;559 totpMain.dispatchEvent(new MouseEvent("dblclick", { bubbles: true }));560 await flushView();561562 const totpInput = container.querySelector('input[placeholder="otpauth://... or base32 secret"]') as HTMLInputElement;563 inputText(totpInput, "otpauth://totp/Example?secret=NB2W45DFOIZA====&issuer=Peach");564 await flushView();565566 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;567 saveButton.click();568 await flushView();569570 expect(sendMessageMock).toHaveBeenCalledWith({571 type: "UPDATE_ENTRY",572 payload: {573 id: "entry-1",574 patch: { totp: "NB2W45DFOIZA====" },575 },576 });577 expect(onFieldSaved).toHaveBeenCalled();578 });579580 it("Vault seeds and focuses search when a search seed is provided", async () => {581 const container = mount(582 <VaultView583 entries={[sampleEntry(), sampleEntry({ id: "entry-2", title: "GitLab" })]}584 sortOrder="url-first"585 searchSeed="g"586 searchSeedNonce={1}587 onSelectEntry={vi.fn()}588 onSortOrderChange={vi.fn()}589 onAdd={vi.fn()}590 onClose={vi.fn()}591 />592 );593594 await flushView();595596 const search = container.querySelector(".peach-list-top .peach-search-input") as HTMLInputElement;597 expect(search.value).toBe("g");598 expect(document.activeElement).toBe(search);599 });600601 it("Import existing-vault add mode parses a file and batches entries into the vault", async () => {602 parseImportSourceMock.mockResolvedValue(603 sampleImportResult({604 entries: [605 {606 type: "login",607 title: "Imported site",608 username: "imported-user",609 password: "imported-pass",610 url: "https://imported.test",611 },612 ],613 })614 );615 sendMessageMock.mockImplementation(async (msg: unknown) => {616 const type = (msg as { type?: string }).type;617 if (type === "GET_ENTRIES") {618 return {619 ok: true,620 data: [621 sampleEntry({622 id: "existing-1",623 title: "Imported site",624 username: "imported-user",625 url: "https://imported.test",626 }),627 ],628 };629 }630 if (type === "BATCH_ADD_ENTRIES") {631 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };632 }633 return { ok: true };634 });635 const onImport = vi.fn(async () => undefined);636 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);637 await flushView();638 sendMessageMock.mockClear();639640 await selectImportFile(container);641 findButton(container, "Import 1").click();642 await flushView();643644 expect(parseImportSourceMock).toHaveBeenCalledWith('{"export":"test"}', undefined);645 expect(sendMessageMock).toHaveBeenCalledWith({646 type: "BATCH_ADD_ENTRIES",647 payload: {648 entries: [649 expect.objectContaining({650 title: "Imported site",651 username: "imported-user",652 password: "imported-pass",653 url: "https://imported.test",654 }),655 ],656 },657 });658 expect(onImport).toHaveBeenCalled();659 expect(container.textContent).toContain("1 entry appears to be duplicates");660 });661662 it("Import replace mode requires confirmation before clearing and replacing the vault", async () => {663 parseImportSourceMock.mockResolvedValue(sampleImportResult());664 sendMessageMock.mockImplementation(async (msg: unknown) => {665 const type = (msg as { type?: string }).type;666 if (type === "GET_ENTRIES") {667 return { ok: true, data: [sampleEntry({ id: "existing-1" })] };668 }669 if (type === "CLEAR_VAULT_ENTRIES") {670 return { ok: true };671 }672 if (type === "BATCH_ADD_ENTRIES") {673 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };674 }675 return { ok: true };676 });677 const onImport = vi.fn(async () => undefined);678 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);679 await flushView();680 sendMessageMock.mockClear();681682 await selectImportFile(container);683 (container.querySelector('input[value="replace"]') as HTMLInputElement).click();684 await flushView();685686 findButton(container, "Replace vault with 1").click();687 await flushView();688689 expect(sendMessageMock).not.toHaveBeenCalledWith(690 expect.objectContaining({ type: "CLEAR_VAULT_ENTRIES" })691 );692 expect(container.textContent).toContain("Replace forever? This cannot be undone.");693694 findButton(container, "Yes, replace forever").click();695 await flushView();696697 expect(sendMessageMock).toHaveBeenCalledWith({ type: "CLEAR_VAULT_ENTRIES" });698 expect(sendMessageMock).toHaveBeenCalledWith({699 type: "BATCH_ADD_ENTRIES",700 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },701 });702 expect(onImport).toHaveBeenCalled();703 });704705 it("Import onboarding mode waits for a matching master password before creating a vault", async () => {706 parseImportSourceMock.mockResolvedValue(sampleImportResult());707 sendMessageMock.mockImplementation(async (msg: unknown) => {708 const type = (msg as { type?: string }).type;709 if (type === "CREATE_VAULT") return { ok: true };710 if (type === "BATCH_ADD_ENTRIES") {711 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };712 }713 return { ok: true };714 });715 const onImport = vi.fn(async () => undefined);716 const container = mount(717 <ImportView718 mode="onboarding"719 replacesExistingVault={true}720 onBack={vi.fn()}721 onImport={onImport}722 />723 );724725 await selectImportFile(container);726727 const createButton = findButton(container, "Create vault + import 1");728 expect(createButton.disabled).toBe(true);729 expect(container.textContent).toContain("Restoring now replaces the locked local vault");730731 const masterInput = container.querySelector(732 'input[placeholder="new master password"]'733 ) as HTMLInputElement;734 const confirmInput = container.querySelector(735 'input[placeholder="confirm master password"]'736 ) as HTMLInputElement;737 inputText(masterInput, "fresh-master-password");738 inputText(confirmInput, "fresh-master-password");739 await flushView();740741 expect(createButton.disabled).toBe(false);742 createButton.click();743 await flushView();744745 expect(sendMessageMock).toHaveBeenCalledWith({746 type: "CREATE_VAULT",747 payload: { password: "fresh-master-password" },748 });749 expect(sendMessageMock).toHaveBeenCalledWith({750 type: "BATCH_ADD_ENTRIES",751 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },752 });753 expect(onImport).toHaveBeenCalled();754 });755756 it("Import encrypted backups reparse with the backup password before import", async () => {757 parseImportSourceMock758 .mockResolvedValueOnce(759 sampleImportResult({760 format: "peach-json",761 label: "Peach encrypted backup",762 entries: [],763 totalItems: 0,764 requiresPassword: true,765 })766 )767 .mockResolvedValueOnce(768 sampleImportResult({769 format: "peach-json",770 label: "Peach encrypted backup",771 })772 );773 sendMessageMock.mockImplementation(async (msg: unknown) => {774 const type = (msg as { type?: string }).type;775 if (type === "GET_ENTRIES") return { ok: true, data: [] };776 if (type === "BATCH_ADD_ENTRIES") {777 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };778 }779 return { ok: true };780 });781 const onImport = vi.fn(async () => undefined);782 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);783 await flushView();784 sendMessageMock.mockClear();785786 await selectImportFile(container, '{"type":"peach-export"}');787 const password = container.querySelector('input[placeholder="backup password"]') as HTMLInputElement;788 inputText(password, "backup-passphrase");789 pressEnter(password);790 await flushView();791 await flushView();792793 expect(parseImportSourceMock).toHaveBeenNthCalledWith(1, '{"type":"peach-export"}', undefined);794 expect(parseImportSourceMock).toHaveBeenNthCalledWith(2, '{"type":"peach-export"}', "backup-passphrase");795796 findButton(container, "Import 1").click();797 await flushView();798799 expect(sendMessageMock).toHaveBeenCalledWith({800 type: "BATCH_ADD_ENTRIES",801 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },802 });803 expect(onImport).toHaveBeenCalled();804 });805});