contracts.test.tsx

25.4 KB · raw

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