bce5897d91f7

redesign Import view to mirror Export menu pattern: card menu (peachscript paste / backup file) routes into focused subviews; PeachScript is the primary import path; preserve onboarding header, replace warning, master-password creation, merge/replace flows; bump 2.7.1

time
2026-04-28 01:14 UTC · 6 months ago
machine
koh2
parent
9d435183c145
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/views/Import.tsx+304 −186src/popup/views/contracts.test.tsx+8 −0
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "2.7.0",4+  "version": "2.7.1",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "2.7.0",4+  "version": "2.7.1",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "2.7.0",3+  "version": "2.7.1",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/popup/views/Import.tsx+304 −186
@@ -16,6 +16,9 @@1616 import { parsePayload } from "../../crypto/peachPayload";1717 import type { VaultEntry } from "../../shared/types";1818 19+type ImportSubview = "menu" | "peachscript" | "file";20+type ImportMergeMode = "add" | "replace";21+1922 interface ImportProps {2023   mode?: "existing" | "onboarding";2124   replacesExistingVault?: boolean;@@ -23,83 +26,148 @@2326   onImport: () => void | Promise<void>;2427 }2528 26-type ImportMergeMode = "add" | "replace";27-2829 export function ImportView(props: ImportProps) {30+  const [view, setView] = createSignal<ImportSubview>("menu");31+  const mode = () => props.mode ?? "existing";32+  const isOnboarding = () => mode() === "onboarding";33+2934   return (30-    <MainImportView31-      mode={props.mode}32-      replacesExistingVault={props.replacesExistingVault}33-      onBack={props.onBack}34-      onImport={props.onImport}35-    />35+    <div class={isOnboarding() ? "peach-lock-view" : "peach-overlay-view ov-enter-right"} style="display:flex">36+      <Show when={view() === "menu"}>37+        <ImportMenu38+          mode={mode()}39+          replacesExistingVault={props.replacesExistingVault}40+          onBack={props.onBack}41+          onPick={(v) => setView(v)}42+        />43+      </Show>44+      <Show when={view() === "peachscript"}>45+        <PeachScriptImport46+          mode={mode()}47+          replacesExistingVault={props.replacesExistingVault}48+          onBack={() => setView("menu")}49+          onImport={props.onImport}50+        />51+      </Show>52+      <Show when={view() === "file"}>53+        <FileImport54+          mode={mode()}55+          replacesExistingVault={props.replacesExistingVault}56+          onBack={() => setView("menu")}57+          onImport={props.onImport}58+        />59+      </Show>60+    </div>3661   );3762 }3863 39-function MainImportView(props: ImportProps) {40-  const mode = () => props.mode ?? "existing";64+// ---------------------------------------------------------------------------65+// Menu66+// ---------------------------------------------------------------------------4167 42-  // PeachScript paste flow68+function ImportMenu(props: {69+  mode: "existing" | "onboarding";70+  replacesExistingVault?: boolean;71+  onBack: () => void;72+  onPick: (view: ImportSubview) => void;73+}) {74+  const isOnboarding = props.mode === "onboarding";75+  return (76+    <>77+      <Show78+        when={isOnboarding}79+        fallback={<OverlayHeader title="import" onBack={props.onBack} />}80+      >81+        <div class="peach-lock-view-header">82+          <PeachWordmark class="peach-wordmark-wrap" />83+          <button class="peach-lock-restore-link" onClick={props.onBack}>84+            ‹ back85+          </button>86+        </div>87+      </Show>88+      <div class="peach-settings-body peach-export-menu">89+        <Show when={isOnboarding && props.replacesExistingVault}>90+          <div class="peach-settings-group">91+            <div class="peach-restore-warning">92+              Restoring now replaces the locked local vault on this device with93+              a fresh one from your backup or export file.94+            </div>95+          </div>96+        </Show>97+98+        <div class="peach-settings-group peach-export-card peach-export-card--primary">99+          <p class="peach-settings-label">peachscript</p>100+          <p class="peach-export-card-copy">101+            Paste a PeachScript backup from another Peach device.102+            {props.mode === "existing" ? " We'll merge it into your vault." : ""}103+          </p>104+          <button105+            class="peach-btn peach-btn--subtle peach-btn--sm"106+            onClick={() => props.onPick("peachscript")}107+          >108+            Paste PeachScript →109+          </button>110+        </div>111+112+        <div class="peach-settings-group peach-export-card">113+          <p class="peach-settings-label">backup file</p>114+          <p class="peach-export-card-copy">115+            A Peach JSON export, or a file from Bitwarden, Proton Pass,116+            1Password, or a browser CSV (Chrome, Safari, Edge).117+          </p>118+          <button119+            class="peach-btn peach-btn--subtle peach-btn--sm"120+            onClick={() => props.onPick("file")}121+          >122+            Choose file →123+          </button>124+        </div>125+      </div>126+    </>127+  );128+}129+130+// ---------------------------------------------------------------------------131+// PeachScript paste flow132+// ---------------------------------------------------------------------------133+134+function PeachScriptImport(props: {135+  mode: "existing" | "onboarding";136+  replacesExistingVault?: boolean;137+  onBack: () => void;138+  onImport: () => void | Promise<void>;139+}) {43140   const [scriptInput, setScriptInput] = createSignal("");44141   const [scriptPass, setScriptPass] = createSignal("");45-  const [scriptWorking, setScriptWorking] = createSignal(false);46-  const [scriptError, setScriptError] = createSignal<string | null>(null);47-  const [scriptResult, setScriptResult] = createSignal<{ entryCount: number } | null>(null);48-49-  // File flow (existing format detection)50-  const [preview, setPreview] = createSignal<ImportParseResult | null>(null);51-  const [rawImport, setRawImport] = createSignal<string | null>(null);52-  const [backupPassword, setBackupPassword] = createSignal("");53142   const [masterPassword, setMasterPassword] = createSignal("");54143   const [masterConfirm, setMasterConfirm] = createSignal("");55-  const [importing, setImporting] = createSignal(false);144+  const [working, setWorking] = createSignal(false);56145   const [error, setError] = createSignal<string | null>(null);57-  const [mergeMode, setMergeMode] = createSignal<ImportMergeMode>("add");58-  const [awaitingReplaceConfirm, setAwaitingReplaceConfirm] = createSignal(false);59-  const [importProgress, setImportProgress] = createSignal<{ current: number; total: number } | null>(null);60-  const [duplicateWarning, setDuplicateWarning] = createSignal<number>(0);61-  const [existingCount, setExistingCount] = createSignal<number>(0);146+  const [result, setResult] = createSignal<{ entryCount: number } | null>(null);62147 63-  const needsBackupPassword = createMemo(() => preview()?.requiresPassword ?? false);64-  const needsMasterPassword = createMemo(() => mode() === "onboarding");148+  const scriptLooksValid = createMemo(() => isPeachScript(scriptInput()));149+  const isOnboarding = props.mode === "onboarding";65150   const masterPasswordReady = createMemo(66-    () =>67-      masterPassword().length >= 8 && masterPassword() === masterConfirm()151+    () => masterPassword().length >= 8 && masterPassword() === masterConfirm()68152   );69153 70-  const scriptLooksValid = createMemo(() => isPeachScript(scriptInput()));71-72-  async function refreshExistingCount() {73-    if (mode() !== "existing") return;74-    const res = await sendMessage({ type: "GET_ENTRIES" });75-    const list = (res as { ok: boolean; data?: VaultEntry[] }).data ?? [];76-    setExistingCount(list.length);77-  }78-79-  void refreshExistingCount();80-81-  // ---------------------------------------------------------------------82-  // PeachScript flow83-  // ---------------------------------------------------------------------84-85-  async function handlePeachScriptImport() {86-    setScriptError(null);87-    setScriptResult(null);154+  async function handleImport() {155+    setError(null);156+    setResult(null);88157     if (!scriptLooksValid()) {89-      setScriptError("Paste your PeachScript first.");158+      setError("Paste your PeachScript first.");90159       return;91160     }92161     if (!scriptPass().trim()) {93-      setScriptError("Enter the backup password.");162+      setError("Enter the backup password.");94163       return;95164     }96-    setScriptWorking(true);165+    setWorking(true);97166     try {98167       const bytes = decodePeachScript(normalisePeachScript(scriptInput()));99168       const payload = await parsePayload(bytes, scriptPass());100169 101-      // Onboarding: create a fresh vault first.102-      if (mode() === "onboarding") {170+      if (isOnboarding) {103171         if (!masterPasswordReady()) {104172           throw new Error("MASTER_PASSWORD_REQUIRED");105173         }@@ -127,20 +195,135 @@127195         }).catch(() => {});128196       }129197 130-      setScriptResult({ entryCount: payload.entries.length });198+      setResult({ entryCount: payload.entries.length });131199       await props.onImport();132200     } catch (err) {133201       const code = err instanceof Error ? err.message : String(err);134-      setScriptError(getImportErrorCopy(code));202+      setError(getImportErrorCopy(code));135203     } finally {136-      setScriptWorking(false);204+      setWorking(false);137205     }138206   }139207 140-  // ---------------------------------------------------------------------141-  // File flow (Bitwarden / 1Password / etc.)142-  // ---------------------------------------------------------------------208+  return (209+    <>210+      <SubviewHeader title="peachscript" onBack={props.onBack} mode={props.mode} />211+      <div class="peach-settings-body">212+        <Show when={isOnboarding && props.replacesExistingVault}>213+          <div class="peach-settings-group">214+            <div class="peach-restore-warning">215+              Restoring now replaces the locked local vault on this device with216+              a fresh one from your backup.217+            </div>218+          </div>219+        </Show>220+        <div class="peach-settings-group peach-export-card peach-export-card--primary">221+          <p class="peach-settings-label">paste peachscript</p>222+          <p class="peach-export-card-copy">223+            {isOnboarding224+              ? "Paste a PeachScript backup from another Peach device to restore your vault."225+              : "We'll merge the entries into your existing vault."}226+          </p>227+          <textarea228+            class="peach-script-input"229+            placeholder="paste PeachScript here…"230+            value={scriptInput()}231+            onInput={(e) => setScriptInput((e.target as HTMLTextAreaElement).value)}232+          />233+          <input234+            class="peach-restore-input"235+            type="password"236+            placeholder="PeachScript password"237+            value={scriptPass()}238+            onInput={(e) => setScriptPass((e.target as HTMLInputElement).value)}239+          />240+          <Show when={isOnboarding}>241+            <div class="peach-auth-grid">242+              <input243+                class="peach-restore-input"244+                type="password"245+                placeholder="new master password"246+                value={masterPassword()}247+                onInput={(e) => setMasterPassword((e.target as HTMLInputElement).value)}248+              />249+              <input250+                class="peach-restore-input"251+                type="password"252+                placeholder="confirm master password"253+                value={masterConfirm()}254+                onInput={(e) => setMasterConfirm((e.target as HTMLInputElement).value)}255+              />256+            </div>257+          </Show>258+          <button259+            class="peach-btn peach-btn--subtle peach-btn--sm"260+            onClick={() => void handleImport()}261+            disabled={262+              working() ||263+              !scriptLooksValid() ||264+              !scriptPass().trim() ||265+              (isOnboarding && !masterPasswordReady())266+            }267+          >268+            {working() ? "decoding…" : "Restore from PeachScript"}269+          </button>270+          <Show when={error()}>271+            <div class="peach-restore-error">{error()}</div>272+          </Show>273+          <Show when={result()}>274+            <div class="peach-export-success">275+              <span class="peach-export-success-check">✓</span>276+              <span>277+                Restored {result()!.entryCount} entr278+                {result()!.entryCount === 1 ? "y" : "ies"}.279+              </span>280+            </div>281+          </Show>282+        </div>283+      </div>284+    </>285+  );286+}143287 288+// ---------------------------------------------------------------------------289+// File flow (Bitwarden / 1Password / Peach JSON / etc.)290+// ---------------------------------------------------------------------------291+292+function FileImport(props: {293+  mode: "existing" | "onboarding";294+  replacesExistingVault?: boolean;295+  onBack: () => void;296+  onImport: () => void | Promise<void>;297+}) {298+  const isOnboarding = props.mode === "onboarding";299+  const needsMasterPassword = () => isOnboarding;300+301+  const [preview, setPreview] = createSignal<ImportParseResult | null>(null);302+  const [rawImport, setRawImport] = createSignal<string | null>(null);303+  const [backupPassword, setBackupPassword] = createSignal("");304+  const [masterPassword, setMasterPassword] = createSignal("");305+  const [masterConfirm, setMasterConfirm] = createSignal("");306+  const [importing, setImporting] = createSignal(false);307+  const [error, setError] = createSignal<string | null>(null);308+  const [mergeMode, setMergeMode] = createSignal<ImportMergeMode>("add");309+  const [awaitingReplaceConfirm, setAwaitingReplaceConfirm] = createSignal(false);310+  const [importProgress, setImportProgress] = createSignal<{ current: number; total: number } | null>(null);311+  const [duplicateWarning, setDuplicateWarning] = createSignal<number>(0);312+  const [existingCount, setExistingCount] = createSignal<number>(0);313+314+  const needsBackupPassword = createMemo(() => preview()?.requiresPassword ?? false);315+  const masterPasswordReady = createMemo(316+    () => masterPassword().length >= 8 && masterPassword() === masterConfirm()317+  );318+319+  async function refreshExistingCount() {320+    if (isOnboarding) return;321+    const res = await sendMessage({ type: "GET_ENTRIES" });322+    const list = (res as { ok: boolean; data?: VaultEntry[] }).data ?? [];323+    setExistingCount(list.length);324+  }325+  void refreshExistingCount();326+144327   async function parseSource(text: string, password?: string) {145328     try {146329       const result = await parseImportSource(text, password);@@ -217,7 +400,7 @@217400       }218401     }219402 220-    if (mode() === "existing" && mergeMode() === "replace" && !awaitingReplaceConfirm()) {403+    if (!isOnboarding && mergeMode() === "replace" && !awaitingReplaceConfirm()) {221404       setAwaitingReplaceConfirm(true);222405       return;223406     }@@ -242,7 +425,7 @@242425         }243426       }244427 245-      if (mode() === "existing" && mergeMode() === "replace") {428+      if (!isOnboarding && mergeMode() === "replace") {246429         const clearRes = await sendMessage({ type: "CLEAR_VAULT_ENTRIES" });247430         if (!(clearRes as { ok: boolean }).ok) {248431           throw new Error("CLEAR_VAULT_FAILED");@@ -250,7 +433,7 @@250433       }251434 252435       let dupes = 0;253-      if (mode() === "existing" && mergeMode() === "add") {436+      if (!isOnboarding && mergeMode() === "add") {254437         dupes = await countDuplicates(currentPreview.entries);255438       }256439 @@ -299,120 +482,20 @@299482   }300483 301484   return (302-    <div class={mode() === "onboarding" ? "peach-lock-view" : "peach-overlay-view ov-enter-right"}>303-      <Show304-        when={mode() === "onboarding"}305-        fallback={306-          <div class="peach-overlay-header">307-            <button308-              class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"309-              onClick={props.onBack}310-              aria-label="Back"311-            >312-              <svg313-                width="7" height="12" viewBox="0 0 7 12"314-                fill="none" stroke="currentColor"315-                stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"316-              >317-                <path d="M6 1L1 6L6 11" />318-              </svg>319-            </button>320-            <span class="peach-overlay-title">import</span>321-            <div class="peach-overlay-spacer"></div>322-          </div>323-        }324-      >325-        <div class="peach-lock-view-header">326-          <PeachWordmark class="peach-wordmark-wrap" />327-          <button328-            class="peach-lock-restore-link"329-            onClick={props.onBack}330-            disabled={importing() || scriptWorking()}331-          >332-            ‹ back333-          </button>334-        </div>335-      </Show>336-485+    <>486+      <SubviewHeader title="backup file" onBack={props.onBack} mode={props.mode} />337487       <div class="peach-settings-body">338-        <Show when={mode() === "onboarding" && props.replacesExistingVault}>488+        <Show when={isOnboarding && props.replacesExistingVault}>339489           <div class="peach-settings-group">340490             <div class="peach-restore-warning">341491               Restoring now replaces the locked local vault on this device with342-              a fresh one from your backup or export file.492+              a fresh one from your backup.343493             </div>344494           </div>345495         </Show>346-347-        {/* PeachScript paste — primary path */}348-        <div class="peach-settings-group peach-restore-group">349-          <p class="peach-settings-label">paste peachscript</p>350-          <p class="peach-restore-copy">351-            {mode() === "onboarding"352-              ? "Paste a PeachScript backup from another Peach device to restore your vault."353-              : "Paste a PeachScript backup from another Peach device. We'll merge it into your vault."}354-          </p>355-          <textarea356-            class="peach-script-input"357-            placeholder="paste PeachScript here…"358-            value={scriptInput()}359-            onInput={(e) => setScriptInput((e.target as HTMLTextAreaElement).value)}360-          />361-          <input362-            class="peach-restore-input"363-            type="password"364-            placeholder="PeachScript password"365-            value={scriptPass()}366-            onInput={(e) => setScriptPass((e.target as HTMLInputElement).value)}367-            style="margin-top:8px"368-          />369-          <Show when={mode() === "onboarding"}>370-            <div class="peach-auth-grid" style="margin-top:8px">371-              <input372-                class="peach-restore-input"373-                type="password"374-                placeholder="new master password"375-                value={masterPassword()}376-                onInput={(e) => setMasterPassword((e.target as HTMLInputElement).value)}377-              />378-              <input379-                class="peach-restore-input"380-                type="password"381-                placeholder="confirm master password"382-                value={masterConfirm()}383-                onInput={(e) => setMasterConfirm((e.target as HTMLInputElement).value)}384-              />385-            </div>386-          </Show>387-          <div class="peach-settings-actions" style="margin-top:8px">388-            <button389-              class="peach-btn peach-btn--subtle peach-btn--sm"390-              onClick={() => void handlePeachScriptImport()}391-              disabled={392-                scriptWorking() ||393-                !scriptLooksValid() ||394-                !scriptPass().trim() ||395-                (mode() === "onboarding" && !masterPasswordReady())396-              }397-            >398-              {scriptWorking() ? "decoding…" : "Restore from PeachScript"}399-            </button>400-          </div>401-          <Show when={scriptError()}>402-            <div class="peach-restore-error" style="margin-top:8px">{scriptError()}</div>403-          </Show>404-          <Show when={scriptResult()}>405-            <div class="peach-export-success" style="margin-top:8px">406-              <span class="peach-export-success-check">✓</span>407-              <span>Restored {scriptResult()!.entryCount} entr{scriptResult()!.entryCount === 1 ? "y" : "ies"}.</span>408-            </div>409-          </Show>410-        </div>411-412-        {/* File import — for migrating from other managers */}413-        <div class="peach-settings-group peach-restore-group">414-          <p class="peach-settings-label">import a file</p>415-          <p class="peach-restore-copy">496+        <div class="peach-settings-group peach-export-card peach-export-card--primary">497+          <p class="peach-settings-label">choose a file</p>498+          <p class="peach-export-card-copy">416499             Bitwarden, 1Password, Proton Pass, Chrome / Edge / Safari CSV, FIDO417500             credential-exchange passkeys, or a Peach JSON backup.418501           </p>@@ -420,10 +503,10 @@420503           <div class="peach-provider-chips">421504             <span class="peach-provider-chip">Peach JSON</span>422505             <span class="peach-provider-chip">FIDO passkeys</span>423-            <span class="peach-provider-chip">Proton Pass CSV/JSON</span>424-            <span class="peach-provider-chip">Bitwarden CSV/JSON</span>425-            <span class="peach-provider-chip">1Password JSON</span>426-            <span class="peach-provider-chip">Chrome / Edge / Safari CSV</span>506+            <span class="peach-provider-chip">Proton Pass</span>507+            <span class="peach-provider-chip">Bitwarden</span>508+            <span class="peach-provider-chip">1Password</span>509+            <span class="peach-provider-chip">Browser CSV</span>427510           </div>428511 429512           <input@@ -431,14 +514,13 @@431514             type="file"432515             accept=".csv,.json"433516             onChange={(event) => void handleFileSelect(event)}434-            style="margin-top:8px"435517           />436518         </div>437519 438520         <Show when={needsBackupPassword()}>439521           <div class="peach-settings-group">440522             <p class="peach-settings-label">unlock backup</p>441-            <p class="peach-restore-copy">523+            <p class="peach-export-card-copy">442524               Enter the password used when you created this encrypted Peach JSON443525               backup.444526             </p>@@ -503,7 +585,7 @@503585           </div>504586         </Show>505587 506-        <Show when={mode() === "existing" && preview() && !needsBackupPassword() && preview()!.format !== "unknown" && preview()!.entries.length > 0}>588+        <Show when={!isOnboarding && preview() && !needsBackupPassword() && preview()!.format !== "unknown" && preview()!.entries.length > 0}>507589           <div class="peach-settings-group">508590             <p class="peach-settings-label">merge or replace</p>509591             <label class="peach-import-choice">@@ -547,7 +629,7 @@547629         <Show when={needsMasterPassword() && preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>548630           <div class="peach-settings-group">549631             <p class="peach-settings-label">new master password</p>550-            <p class="peach-restore-copy">632+            <p class="peach-export-card-copy">551633               This password unlocks the Peach vault created on this device after552634               import.553635             </p>@@ -590,7 +672,7 @@590672 591673         <Show when={duplicateWarning() > 0 && !importing()}>592674           <div class="peach-settings-group">593-            <div class="peach-restore-copy">675+            <div class="peach-export-card-copy">594676               {duplicateWarning()} {duplicateWarning() === 1 ? "entry appears" : "entries appear"} to be duplicates of existing ones. They were still imported — you can review and delete them in your vault.595677             </div>596678           </div>@@ -610,7 +692,7 @@610692               >611693                 {importing()612694                   ? "importing..."613-                  : mode() === "onboarding"695+                  : isOnboarding614696                     ? `Create vault + import ${preview()!.entries.length}`615697                     : mergeMode() === "replace"616698                       ? awaitingReplaceConfirm()@@ -618,19 +700,55 @@618700                         : `Replace vault with ${preview()!.entries.length}`619701                       : `Import ${preview()!.entries.length}`}620702               </button>621-              <button622-                class="peach-btn peach-btn--ghost peach-btn--sm"623-                onClick={props.onBack}624-                disabled={importing()}625-              >626-                Cancel627-              </button>628703             </div>629704           </div>630705         </Show>631-632706       </div>707+    </>708+  );709+}710+711+// ---------------------------------------------------------------------------712+// Shared headers713+// ---------------------------------------------------------------------------714+715+function OverlayHeader(props: { title: string; onBack: () => void }) {716+  return (717+    <div class="peach-overlay-header">718+      <button719+        class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"720+        onClick={props.onBack}721+        aria-label="Back"722+      >723+        <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">724+          <path d="M6 1L1 6L6 11" />725+        </svg>726+      </button>727+      <span class="peach-overlay-title">{props.title}</span>728+      <div class="peach-overlay-spacer"></div>633729     </div>730+  );731+}732+733+function SubviewHeader(props: {734+  title: string;735+  onBack: () => void;736+  mode: "existing" | "onboarding";737+}) {738+  // Onboarding sub-flows reuse the lock-screen header; existing-vault flows739+  // use the standard overlay back-arrow header.740+  return (741+    <Show742+      when={props.mode === "onboarding"}743+      fallback={<OverlayHeader title={props.title} onBack={props.onBack} />}744+    >745+      <div class="peach-lock-view-header">746+        <PeachWordmark class="peach-wordmark-wrap" />747+        <button class="peach-lock-restore-link" onClick={props.onBack}>748+          ‹ back749+        </button>750+      </div>751+    </Show>634752   );635753 }636754 

view file

src/popup/views/contracts.test.tsx+8 −0
@@ -145,6 +145,14 @@145145     container: HTMLElement,146146     contents = '{"export":"test"}'147147   ) {148+    // The Import view now opens onto a menu — click into the file subview149+    // before locating the file input. Idempotent: a second click won't re-fire150+    // because the menu unmounts once the subview renders.151+    const fileCard = findButton(container, "Choose file →");152+    if (fileCard) {153+      fileCard.click();154+      await flushView();155+    }148156     const input = container.querySelector('input[type="file"]') as HTMLInputElement;149157     const file = new File([contents], "import.json", { type: "application/json" });150158     Object.defineProperty(input, "files", {

view file