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 { EntryView } from "./Entry";12import { ImportView } from "./Import";13import { VaultView } from "./Vault";14import { changeSelect, flushView, inputText, pressEnter, renderView } from "./view-test-utils";1516const sendMessageMock = vi.fn<(msg: unknown) => Promise<unknown>>();17const triggerRingMock = vi.fn();18const triggerPipPulseMock = vi.fn();19const exportEntriesToPeachJsonMock = 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/backup", () => ({30 exportEntriesToPeachJson: (...args: unknown[]) => exportEntriesToPeachJsonMock(...args),31}));3233vi.mock("../../shared/import", () => ({34 parseImportSource: (...args: [string, string | undefined]) => parseImportSourceMock(...args),35}));3637vi.mock("../../shared/favicon", () => ({38 faviconUrl: () => null,39}));4041vi.mock("../../crypto/totp", () => ({42 generateTOTP: () => ({ code: "123 456", remaining: 24 }),43 parseOtpauthURI: (value: string) =>44 value.startsWith("otpauth://")45 ? { secret: "NB2W45DFOIZA====", algorithm: "SHA1" as const }46 : null,47}));4849vi.mock("../components/GeneratorDrawer", () => ({50 GeneratorDrawer: () => null,51}));5253vi.mock("./CJKImport", () => ({54 CJKImportView: () => null,55}));5657vi.mock("./CJKExport", () => ({58 CJKExportView: () => null,59}));6061vi.mock("zxcvbn", () => ({62 default: (value: string) => zxcvbnMock(value),63}));6465describe("popup contract tests", () => {66 const disposers: Array<() => void> = [];6768 beforeEach(() => {69 document.body.innerHTML = "";70 sendMessageMock.mockReset();71 triggerRingMock.mockReset();72 triggerPipPulseMock.mockReset();73 exportEntriesToPeachJsonMock.mockReset();74 parseImportSourceMock.mockReset();75 zxcvbnMock.mockReset();76 zxcvbnMock.mockImplementation((value: string) => ({ score: value.length >= 16 ? 3 : 1 }));77 parseImportSourceMock.mockResolvedValue({78 format: "unknown",79 label: "Unknown format",80 entries: [],81 totalItems: 0,82 skippedItems: 0,83 requiresPassword: false,84 });8586 vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {87 return window.setTimeout(() => cb(0), 0) as unknown as number;88 });89 vi.stubGlobal("cancelAnimationFrame", (id: number) => window.clearTimeout(id));90 vi.stubGlobal("URL", {91 createObjectURL: vi.fn(() => "blob:peach-test"),92 revokeObjectURL: vi.fn(),93 });94 vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {});9596 (globalThis as { chrome?: typeof chrome }).chrome = {97 runtime: {98 getManifest: () => ({ version: "1.4.47" }),99 onMessage: { addListener: vi.fn() },100 },101 } as unknown as typeof chrome;102 });103104 afterEach(() => {105 while (disposers.length > 0) {106 disposers.pop()?.();107 }108 vi.unstubAllGlobals();109 });110111 function mount(view: JSX.Element) {112 const mounted = renderView(view);113 disposers.push(mounted.dispose);114 return mounted.container;115 }116117 function sampleEntry(overrides: Partial<VaultEntry> = {}): VaultEntry {118 return {119 id: "entry-1",120 type: "login",121 title: "GitHub",122 username: "june",123 password: "secret123",124 url: "https://github.com",125 totp: "JBSWY3DPEHPK3PXP",126 canary: {127 webhookUrl: "https://example.com/hook",128 },129 createdAt: 1,130 modifiedAt: 1,131 ...overrides,132 };133 }134135 function sampleImportResult(136 overrides: Partial<ImportParseResult> = {}137 ): ImportParseResult {138 return {139 format: "csv",140 label: "CSV import",141 entries: [142 {143 type: "login",144 title: "Imported site",145 username: "imported-user",146 password: "imported-pass",147 url: "https://imported.test",148 },149 ],150 totalItems: 1,151 skippedItems: 0,152 requiresPassword: false,153 ...overrides,154 };155 }156157 async function selectImportFile(158 container: HTMLElement,159 contents = '{"export":"test"}'160 ) {161 const input = container.querySelector('input[type="file"]') as HTMLInputElement;162 const file = new File([contents], "import.json", { type: "application/json" });163 Object.defineProperty(input, "files", {164 configurable: true,165 value: [file],166 });167 input.dispatchEvent(new Event("change", { bubbles: true }));168 await flushView();169 await flushView();170 }171172 function findButton(container: HTMLElement, text: string): HTMLButtonElement {173 return Array.from(container.querySelectorAll("button")).find(174 (candidate) => candidate.textContent?.trim() === text175 ) as HTMLButtonElement;176 }177178 it("Lock sends UNLOCK with the entered passphrase", async () => {179 sendMessageMock.mockResolvedValue({ ok: true });180 const onUnlock = vi.fn(async () => undefined);181 const container = mount(<LockView hasVault={true} ready={true} onUnlock={onUnlock} />);182183 const password = container.querySelector("#unlock-password") as HTMLInputElement;184 inputText(password, "correct horse battery staple");185 (container.querySelector(".peach-lock-submit") as HTMLButtonElement).click();186 await flushView();187188 expect(sendMessageMock).toHaveBeenCalledWith({189 type: "UNLOCK",190 payload: { password: "correct horse battery staple" },191 });192 expect(onUnlock).toHaveBeenCalled();193 });194195 it("Lock keeps create-vault disabled for short passwords and weak passphrases", async () => {196 const container = mount(<LockView hasVault={false} ready={true} onUnlock={vi.fn()} />);197 const createButton = container.querySelector(".peach-lock-submit") as HTMLButtonElement;198 const password = container.querySelector("#master-password") as HTMLInputElement;199 const confirm = container.querySelector("#confirm-password") as HTMLInputElement;200201 inputText(password, "short");202 inputText(confirm, "short");203 await flushView();204 expect(createButton.disabled).toBe(true);205206 zxcvbnMock.mockImplementation(() => ({ score: 1 }));207 inputText(password, "weakweakweak");208 inputText(confirm, "weakweakweak");209 await flushView();210 expect(createButton.disabled).toBe(true);211 });212213 it("Lock sends CREATE_VAULT when the onboarding passphrase is strong enough", async () => {214 sendMessageMock.mockResolvedValue({ ok: true });215 zxcvbnMock.mockImplementation(() => ({ score: 3 }));216 const onUnlock = vi.fn(async () => undefined);217 const container = mount(<LockView hasVault={false} ready={true} onUnlock={onUnlock} />);218219 const password = container.querySelector("#master-password") as HTMLInputElement;220 const confirm = container.querySelector("#confirm-password") as HTMLInputElement;221 const createButton = container.querySelector(".peach-lock-submit") as HTMLButtonElement;222223 inputText(password, "correct horse battery staple");224 inputText(confirm, "correct horse battery staple");225 await flushView();226 expect(createButton.disabled).toBe(false);227228 pressEnter(confirm);229 await flushView();230231 expect(sendMessageMock).toHaveBeenCalledWith({232 type: "CREATE_VAULT",233 payload: { password: "correct horse battery staple" },234 });235 expect(onUnlock).toHaveBeenCalled();236 });237238 it("Export keeps encrypted export disabled until the password meets the minimum length", async () => {239 const container = mount(<ExportView onBack={vi.fn()} />);240241 const password = container.querySelector('input[placeholder*="backup password"]') as HTMLInputElement;242 const confirm = container.querySelector("#export-confirm-password") as HTMLInputElement;243 const button = Array.from(container.querySelectorAll("button")).find(244 (candidate) => candidate.textContent?.trim() === "Download backup file"245 ) as HTMLButtonElement;246247 inputText(password, "too-short");248 inputText(confirm, "too-short");249 await flushView();250251 expect(button.disabled).toBe(true);252 expect(exportEntriesToPeachJsonMock).not.toHaveBeenCalled();253 });254255 it("Export keeps encrypted export disabled when passwords do not match", async () => {256 const container = mount(<ExportView onBack={vi.fn()} />);257258 const password = container.querySelector('input[placeholder*="backup password"]') as HTMLInputElement;259 const confirm = container.querySelector("#export-confirm-password") as HTMLInputElement;260 const button = Array.from(container.querySelectorAll("button")).find(261 (candidate) => candidate.textContent?.trim() === "Download backup file"262 ) as HTMLButtonElement;263264 inputText(password, "correct horse battery staple");265 inputText(confirm, "different horse battery staple");266 await flushView();267268 expect(button.disabled).toBe(true);269 expect(exportEntriesToPeachJsonMock).not.toHaveBeenCalled();270 });271272 it("Export requests entries and sends the correct payload to the encrypted backup exporter", async () => {273 sendMessageMock.mockResolvedValue({274 ok: true,275 data: [sampleEntry(), sampleEntry({ id: "deleted", deletedAt: 2 })],276 });277 exportEntriesToPeachJsonMock.mockResolvedValue({278 version: 2,279 type: "peach-export",280 });281 const container = mount(<ExportView onBack={vi.fn()} />);282283 const password = container.querySelector('input[placeholder*="backup password"]') as HTMLInputElement;284 const confirm = container.querySelector("#export-confirm-password") as HTMLInputElement;285 const button = Array.from(container.querySelectorAll("button")).find(286 (candidate) => candidate.textContent?.trim() === "Download backup file"287 ) as HTMLButtonElement;288289 inputText(password, "correct horse battery staple");290 inputText(confirm, "correct horse battery staple");291 await flushView();292 button.click();293 await flushView();294295 expect(sendMessageMock).toHaveBeenCalledWith({ type: "GET_ENTRIES" });296 expect(exportEntriesToPeachJsonMock).toHaveBeenCalledWith(297 [298 expect.objectContaining({ id: "entry-1" }),299 expect.objectContaining({ id: "deleted", deletedAt: 2 }),300 ],301 "correct horse battery staple"302 );303 });304305 it("Settings requires current, new, and confirm passwords before enabling save", async () => {306 sendMessageMock.mockResolvedValue({ ok: true, data: null });307 const container = mount(308 <SettingsView309 entriesCount={1}310 breachMonitoring={true}311 lockOnClose={true}312 autoLockMinutes={5}313 trashRetentionDays={30}314 theme="dark"315 accentColor="peach"316 onBreachMonitoringChange={vi.fn()}317 onLockOnCloseChange={vi.fn()}318 onAutoLockMinutesChange={vi.fn()}319 onTrashRetentionChange={vi.fn()}320 onThemeChange={vi.fn()}321 onAccentChange={vi.fn()}322 onBack={vi.fn()}323 onImport={vi.fn()}324 onExport={vi.fn()}325 onBreachCheckDone={vi.fn()}326 />327 );328 await flushView();329330 const changeButton = Array.from(container.querySelectorAll("button")).find(331 (candidate) => candidate.textContent?.trim() === "change"332 ) as HTMLButtonElement;333 changeButton.click();334 await flushView();335336 const saveButton = Array.from(container.querySelectorAll("button")).find(337 (candidate) => candidate.textContent?.trim() === "save new password"338 ) as HTMLButtonElement;339 expect(saveButton.disabled).toBe(true);340341 const inputs = container.querySelectorAll('input[type="password"]');342 inputText(inputs[0] as HTMLInputElement, "current-pass");343 inputText(inputs[1] as HTMLInputElement, "new-pass-12345");344 await flushView();345 expect(saveButton.disabled).toBe(true);346347 inputText(inputs[2] as HTMLInputElement, "new-pass-12345");348 await flushView();349 expect(saveButton.disabled).toBe(false);350 });351352 it("Settings sends CHANGE_PASSWORD and forwards auto-lock/theme changes through callbacks", async () => {353 sendMessageMock.mockResolvedValue({ ok: true, data: null });354 const onAutoLockMinutesChange = vi.fn();355 const onThemeChange = vi.fn();356 const container = mount(357 <SettingsView358 entriesCount={1}359 breachMonitoring={true}360 lockOnClose={true}361 autoLockMinutes={5}362 trashRetentionDays={30}363 theme="dark"364 accentColor="peach"365 onBreachMonitoringChange={vi.fn()}366 onLockOnCloseChange={vi.fn()}367 onAutoLockMinutesChange={onAutoLockMinutesChange}368 onTrashRetentionChange={vi.fn()}369 onThemeChange={onThemeChange}370 onAccentChange={vi.fn()}371 onBack={vi.fn()}372 onImport={vi.fn()}373 onExport={vi.fn()}374 onBreachCheckDone={vi.fn()}375 />376 );377 await flushView();378 sendMessageMock.mockClear();379380 const selects = container.querySelectorAll("select");381 changeSelect(selects[0] as HTMLSelectElement, "15");382 expect(onAutoLockMinutesChange).toHaveBeenCalledWith(15);383384 const themeButton = Array.from(container.querySelectorAll("button")).find(385 (candidate) => candidate.textContent?.trim() === "Switch to light"386 ) as HTMLButtonElement;387 themeButton.click();388 expect(onThemeChange).toHaveBeenCalledWith("light");389390 const changeButton = Array.from(container.querySelectorAll("button")).find(391 (candidate) => candidate.textContent?.trim() === "change"392 ) as HTMLButtonElement;393 changeButton.click();394 await flushView();395396 const inputs = container.querySelectorAll('input[type="password"]');397 inputText(inputs[0] as HTMLInputElement, "current-pass");398 inputText(inputs[1] as HTMLInputElement, "new-password-123");399 inputText(inputs[2] as HTMLInputElement, "new-password-123");400 await flushView();401402 const saveButton = Array.from(container.querySelectorAll("button")).find(403 (candidate) => candidate.textContent?.trim() === "save new password"404 ) as HTMLButtonElement;405 saveButton.click();406 await flushView();407408 expect(sendMessageMock).toHaveBeenCalledWith({409 type: "CHANGE_PASSWORD",410 payload: {411 currentPassword: "current-pass",412 newPassword: "new-password-123",413 },414 });415 });416417 it("Entry copy actions send COPY and canary ping messages for password and username", async () => {418 sendMessageMock.mockResolvedValue({ ok: true });419 const container = mount(420 <EntryView421 entry={sampleEntry()}422 onDelete={vi.fn()}423 onFieldSaved={vi.fn(async () => undefined)}424 />425 );426427 const passwordButton = container.querySelector('button[aria-label="Copy password"]') as HTMLButtonElement;428 passwordButton.click();429 await flushView();430431 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {432 type: "COPY",433 payload: { text: "secret123", label: "password" },434 });435 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {436 type: "FIRE_CANARY_PING",437 payload: {438 webhookUrl: "https://example.com/hook",439 entryTitle: "GitHub",440 trigger: "password_copied",441 },442 });443444 sendMessageMock.mockClear();445 const usernameButton = container.querySelector('button[aria-label="Copy username"]') as HTMLButtonElement;446 usernameButton.click();447 await flushView();448449 expect(sendMessageMock).toHaveBeenNthCalledWith(1, {450 type: "COPY",451 payload: { text: "june", label: "username" },452 });453 expect(sendMessageMock).toHaveBeenNthCalledWith(2, {454 type: "FIRE_CANARY_PING",455 payload: {456 webhookUrl: "https://example.com/hook",457 entryTitle: "GitHub",458 trigger: "username_copied",459 },460 });461 });462463 it("Entry refuses to send an empty password update and copies TOTP through COPY", async () => {464 sendMessageMock.mockResolvedValue({ ok: true });465 const onFieldSaved = vi.fn(async () => undefined);466 const container = mount(467 <EntryView468 entry={sampleEntry()}469 onDelete={vi.fn()}470 onFieldSaved={onFieldSaved}471 />472 );473474 const passwordValue = container.querySelector(".peach-password-value") as HTMLSpanElement;475 passwordValue.click();476 await flushView();477478 const passwordInput = container.querySelector('input[placeholder="paste or type a password"]') as HTMLInputElement;479 inputText(passwordInput, "");480 await flushView();481482 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;483 saveButton.click();484 await flushView();485486 expect(sendMessageMock).not.toHaveBeenCalledWith(487 expect.objectContaining({ type: "UPDATE_ENTRY" })488 );489 expect(onFieldSaved).not.toHaveBeenCalled();490491 const totpButton = container.querySelector('button[aria-label="Copy TOTP code"]') as HTMLButtonElement;492 totpButton.click();493 await flushView();494495 expect(sendMessageMock).toHaveBeenLastCalledWith({496 type: "COPY",497 payload: { text: "123456", label: "totp" },498 });499 });500501 it("Entry closes an unchanged inline editor when focus leaves it", async () => {502 sendMessageMock.mockResolvedValue({ ok: true });503 const container = mount(504 <EntryView505 entry={sampleEntry()}506 onDelete={vi.fn()}507 onFieldSaved={vi.fn(async () => undefined)}508 />509 );510511 const usernameValue = container.querySelector(".peach-field-value.peach-inline-clickable") as HTMLSpanElement;512 usernameValue.click();513 await flushView();514515 const usernameInput = container.querySelector(".peach-inline-input[autocomplete=\"off\"]") as HTMLInputElement;516 usernameInput.blur();517 await flushView();518519 expect(container.querySelector(".peach-inline-input[autocomplete=\"off\"]")).toBeNull();520 expect(sendMessageMock).not.toHaveBeenCalledWith(521 expect.objectContaining({ type: "UPDATE_ENTRY" })522 );523 });524525 it("Entry lets TOTP secrets be edited inline", async () => {526 sendMessageMock.mockResolvedValue({ ok: true });527 const onFieldSaved = vi.fn(async () => undefined);528 const container = mount(529 <EntryView530 entry={sampleEntry()}531 onDelete={vi.fn()}532 onFieldSaved={onFieldSaved}533 />534 );535536 const totpMain = container.querySelector(".peach-totp-main.peach-inline-clickable") as HTMLDivElement;537 totpMain.click();538 await flushView();539540 const totpInput = container.querySelector('input[placeholder="otpauth://... or base32 secret"]') as HTMLInputElement;541 inputText(totpInput, "otpauth://totp/Example?secret=NB2W45DFOIZA====&issuer=Peach");542 await flushView();543544 const saveButton = container.querySelector('button[aria-label="Save"]') as HTMLButtonElement;545 saveButton.click();546 await flushView();547548 expect(sendMessageMock).toHaveBeenCalledWith({549 type: "UPDATE_ENTRY",550 payload: {551 id: "entry-1",552 patch: { totp: "NB2W45DFOIZA====" },553 },554 });555 expect(onFieldSaved).toHaveBeenCalled();556 });557558 it("Vault seeds and focuses search when a search seed is provided", async () => {559 const container = mount(560 <VaultView561 entries={[sampleEntry(), sampleEntry({ id: "entry-2", title: "GitLab" })]}562 sortOrder="url-first"563 searchSeed="g"564 searchSeedNonce={1}565 onSelectEntry={vi.fn()}566 onSortOrderChange={vi.fn()}567 onAdd={vi.fn()}568 onClose={vi.fn()}569 />570 );571572 await flushView();573574 const search = container.querySelector(".peach-list-top .peach-search-input") as HTMLInputElement;575 expect(search.value).toBe("g");576 expect(document.activeElement).toBe(search);577 });578579 it("Import existing-vault add mode parses a file and batches entries into the vault", async () => {580 parseImportSourceMock.mockResolvedValue(581 sampleImportResult({582 entries: [583 {584 type: "login",585 title: "Imported site",586 username: "imported-user",587 password: "imported-pass",588 url: "https://imported.test",589 },590 ],591 })592 );593 sendMessageMock.mockImplementation(async (msg: unknown) => {594 const type = (msg as { type?: string }).type;595 if (type === "GET_ENTRIES") {596 return {597 ok: true,598 data: [599 sampleEntry({600 id: "existing-1",601 title: "Imported site",602 username: "imported-user",603 url: "https://imported.test",604 }),605 ],606 };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 findButton(container, "Import 1").click();620 await flushView();621622 expect(parseImportSourceMock).toHaveBeenCalledWith('{"export":"test"}', undefined);623 expect(sendMessageMock).toHaveBeenCalledWith({624 type: "BATCH_ADD_ENTRIES",625 payload: {626 entries: [627 expect.objectContaining({628 title: "Imported site",629 username: "imported-user",630 password: "imported-pass",631 url: "https://imported.test",632 }),633 ],634 },635 });636 expect(onImport).toHaveBeenCalled();637 expect(container.textContent).toContain("1 entry appears to be duplicates");638 });639640 it("Import replace mode requires confirmation before clearing and replacing the vault", async () => {641 parseImportSourceMock.mockResolvedValue(sampleImportResult());642 sendMessageMock.mockImplementation(async (msg: unknown) => {643 const type = (msg as { type?: string }).type;644 if (type === "GET_ENTRIES") {645 return { ok: true, data: [sampleEntry({ id: "existing-1" })] };646 }647 if (type === "CLEAR_VAULT_ENTRIES") {648 return { ok: true };649 }650 if (type === "BATCH_ADD_ENTRIES") {651 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };652 }653 return { ok: true };654 });655 const onImport = vi.fn(async () => undefined);656 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);657 await flushView();658 sendMessageMock.mockClear();659660 await selectImportFile(container);661 (container.querySelector('input[value="replace"]') as HTMLInputElement).click();662 await flushView();663664 findButton(container, "Replace vault with 1").click();665 await flushView();666667 expect(sendMessageMock).not.toHaveBeenCalledWith(668 expect.objectContaining({ type: "CLEAR_VAULT_ENTRIES" })669 );670 expect(container.textContent).toContain("Replace forever? This cannot be undone.");671672 findButton(container, "Yes, replace forever").click();673 await flushView();674675 expect(sendMessageMock).toHaveBeenCalledWith({ type: "CLEAR_VAULT_ENTRIES" });676 expect(sendMessageMock).toHaveBeenCalledWith({677 type: "BATCH_ADD_ENTRIES",678 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },679 });680 expect(onImport).toHaveBeenCalled();681 });682683 it("Import onboarding mode waits for a matching master password before creating a vault", async () => {684 parseImportSourceMock.mockResolvedValue(sampleImportResult());685 sendMessageMock.mockImplementation(async (msg: unknown) => {686 const type = (msg as { type?: string }).type;687 if (type === "CREATE_VAULT") return { ok: true };688 if (type === "BATCH_ADD_ENTRIES") {689 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };690 }691 return { ok: true };692 });693 const onImport = vi.fn(async () => undefined);694 const container = mount(695 <ImportView696 mode="onboarding"697 replacesExistingVault={true}698 onBack={vi.fn()}699 onImport={onImport}700 />701 );702703 await selectImportFile(container);704705 const createButton = findButton(container, "Create vault + import 1");706 expect(createButton.disabled).toBe(true);707 expect(container.textContent).toContain("Restoring now replaces the locked local vault");708709 const inputs = container.querySelectorAll('input[type="password"]');710 inputText(inputs[0] as HTMLInputElement, "fresh-master-password");711 inputText(inputs[1] as HTMLInputElement, "fresh-master-password");712 await flushView();713714 expect(createButton.disabled).toBe(false);715 createButton.click();716 await flushView();717718 expect(sendMessageMock).toHaveBeenCalledWith({719 type: "CREATE_VAULT",720 payload: { password: "fresh-master-password" },721 });722 expect(sendMessageMock).toHaveBeenCalledWith({723 type: "BATCH_ADD_ENTRIES",724 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },725 });726 expect(onImport).toHaveBeenCalled();727 });728729 it("Import encrypted backups reparse with the backup password before import", async () => {730 parseImportSourceMock731 .mockResolvedValueOnce(732 sampleImportResult({733 format: "peach-json",734 label: "Peach encrypted backup",735 entries: [],736 totalItems: 0,737 requiresPassword: true,738 })739 )740 .mockResolvedValueOnce(741 sampleImportResult({742 format: "peach-json",743 label: "Peach encrypted backup",744 })745 );746 sendMessageMock.mockImplementation(async (msg: unknown) => {747 const type = (msg as { type?: string }).type;748 if (type === "GET_ENTRIES") return { ok: true, data: [] };749 if (type === "BATCH_ADD_ENTRIES") {750 return { ok: true, data: [sampleEntry({ id: "imported-1" })] };751 }752 return { ok: true };753 });754 const onImport = vi.fn(async () => undefined);755 const container = mount(<ImportView onBack={vi.fn()} onImport={onImport} />);756 await flushView();757 sendMessageMock.mockClear();758759 await selectImportFile(container, '{"type":"peach-export"}');760 const password = container.querySelector('input[placeholder="backup password"]') as HTMLInputElement;761 inputText(password, "backup-passphrase");762 pressEnter(password);763 await flushView();764 await flushView();765766 expect(parseImportSourceMock).toHaveBeenNthCalledWith(1, '{"type":"peach-export"}', undefined);767 expect(parseImportSourceMock).toHaveBeenNthCalledWith(2, '{"type":"peach-export"}', "backup-passphrase");768769 findButton(container, "Import 1").click();770 await flushView();771772 expect(sendMessageMock).toHaveBeenCalledWith({773 type: "BATCH_ADD_ENTRIES",774 payload: { entries: [expect.objectContaining({ title: "Imported site" })] },775 });776 expect(onImport).toHaveBeenCalled();777 });778});