Back to Lobehub

Electron (LobeHub Desktop) UI Testing

.agents/skills/agent-testing/ui/electron.md

2.2.109.4 KB
Original Source

Electron (LobeHub Desktop) UI Testing

Default surface for verifying pure frontend changes (components, store logic, styles, interactions) in the primary product shape. Drives the Electron renderer over CDP with agent-browser — see ../references/agent-browser.md for the full command reference.

Auth: electron-dev.sh persists the login across runs — stop snapshots it to ~/.lobehub/agent-testing/electron-login, start seeds each new instance from there (login-status inspects it, save-login <id> captures a live instance). Sign in once, not once per run — and if an instance still comes up signed out, drive the sign-in yourself; never ask the user to click it. Run ../scripts/setup-auth.sh status before testing (see ../references/auth.md).

Linux / headless (cloud): Electron itself runs on Linux, but it has no true headless mode — it needs a display server. In a headless environment wrap the launch with xvfb-run (virtual framebuffer). Everything CDP-based keeps working under Xvfb: the agent-browser --cdp 9222 connection, snapshots, eval, and agent-browser screenshot (captured from the renderer via CDP, not the OS screen). What does NOT work on Linux: capture-app-window.sh (macOS screencapture), osascript, and the ffmpeg recording scripts in their current form.

Setup / Teardown

Use the electron-dev.sh script to manage the Electron dev environment. It handles process lifecycle, waits for SPA readiness, and reliably kills all child processes (main + helpers + vite).

bash
SCRIPT=".agents/skills/agent-testing/scripts/electron-dev.sh"

# Start Electron dev with CDP (idempotent — skips if already running)
$SCRIPT start

# Check if Electron is running and CDP is reachable
$SCRIPT status

# Kill all Electron-related processes (main + helper + vite)
$SCRIPT stop

# Force fresh restart
$SCRIPT restart

After start succeeds, connect with: agent-browser --cdp 9222 snapshot -i

Always run $SCRIPT stop when done testingpkill -f "Electron" alone won't catch all helper processes.

Concurrent instances (N worktrees / parallel verification)? electron-dev.sh drives a pool: start <id> gives each instance its own CDP port, userData dir (with copied login state), and — via env — its own Vite port + IPC id, so multiple isolated dev instances run at once. Drive each with a distinct agent-browser --session s<port> --cdp <port> (the daemon otherwise reuses one session across ports). Full design, the collision matrix, and the login-copy recipe: ../references/multi-instance.md.

Environment Variables

VariableDefaultDescription
CDP_PORT9222Chrome DevTools Protocol port
ELECTRON_LOG/tmp/electron-dev.logElectron process log
ELECTRON_WAIT_S60Max seconds to wait for Electron process
RENDERER_WAIT_S60Max seconds to wait for SPA to load
LOBE_LOGIN_STATE_DIR~/.lobehub/agent-testing/electron-loginPersistent login snapshot (survives /tmp wipes)
KEEP_DATA01 = stop <id> keeps the instance's userData
SKIP_LOGIN_SAVE01 = stop <id> does not snapshot the login

LobeHub Probes & Quick Navigation

scripts/app-probe.sh is the standard fast path into app state — use it instead of hand-rolling __LOBE_STORES eval snippets for these common needs:

bash
PROBE=".agents/skills/agent-testing/scripts/app-probe.sh"

$PROBE auth           # login check (Step 0.3) → { isSignedIn, userId }
$PROBE route          # current SPA route
$PROBE ops            # running chat operations (type / startTime)
$PROBE goto /settings # jump the SPA straight to a route (full reload)
$PROBE errors-install # install console.error interceptor
$PROBE errors         # dump captured errors

goto lets a test enter the state under test directly instead of clicking through the UI. Common desktop routes:

RouteWhere it lands
/Home (has a chat input)
/agent/<agentId>Agent conversation (latest topic)
/agent/<agentId>/<topicId>Specific topic in a conversation
/task · /task/<taskId>Task list / task detail
/pageDocuments (文稿)
/settingsSettings
/communityDiscover / community

Targets default to Electron (--cdp 9222); set AB_TARGET="--session <name>" for web sessions. For deeper or one-off state inspection, fall back to raw eval below.

LobeHub-Specific Patterns

Access Zustand Store State

bash
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
(function() {
  var chat = window.__LOBE_STORES.chat();
  var ops = Object.values(chat.operations);
  return JSON.stringify({
    ops: ops.map(function(o) { return { type: o.type, status: o.status }; }),
    activeAgent: chat.activeAgentId,
    activeTopic: chat.activeTopicId,
  });
})()
EVALEOF

Find and Use the Chat Input

bash
# The chat input is contenteditable — must use -C flag
agent-browser --cdp 9222 snapshot -i -C 2>&1 | grep "editable"

agent-browser --cdp 9222 click @e48
agent-browser --cdp 9222 type @e48 "Hello world"
agent-browser --cdp 9222 press Enter

Wait for Agent to Complete

bash
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
(function() {
  var chat = window.__LOBE_STORES.chat();
  var ops = Object.values(chat.operations);
  var running = ops.filter(function(o) { return o.status === 'running'; });
  return running.length === 0 ? 'done' : 'running: ' + running.length;
})()
EVALEOF

Install Error Interceptor

bash
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
(function() {
  window.__CAPTURED_ERRORS = [];
  var orig = console.error;
  console.error = function() {
    var msg = Array.from(arguments).map(function(a) {
      if (a instanceof Error) return a.message;
      return typeof a === 'object' ? JSON.stringify(a) : String(a);
    }).join(' ');
    window.__CAPTURED_ERRORS.push(msg);
    orig.apply(console, arguments);
  };
  return 'installed';
})()
EVALEOF

# Later, check captured errors:
agent-browser --cdp 9222 eval "JSON.stringify(window.__CAPTURED_ERRORS)"

Electron Gotchas

  • agent-browser screenshot can wedge; prefer raw-CDP capture. agent-browser routes captures through a long-lived daemon. An interrupted or mis-invoked screenshot (a stalled --full on a huge page, a killed/backgrounded command, a bad flag) can leave the daemon's CDP session half-open, after which every later screenshot fails with CDP response channel closed / daemon busy — even though eval / get url still work. This is not a display-sleep or permission problem: raw Page.captureScreenshot is fast (~60ms) and works even when the display is asleep or the window is minimized/occluded (verified). Use the raw-CDP helper for Electron evidence and as a preflight:

    bash
    ./.agents/skills/agent-testing/scripts/cdp-screenshot.sh --check               # preflight: PASS ⇒ capture works
    ./.agents/skills/agent-testing/scripts/cdp-screenshot.sh --out shot.png        # viewport
    ./.agents/skills/agent-testing/scripts/cdp-screenshot.sh --out full.png --full # full page (captureBeyondViewport)
    

    If agent-browser's own screenshot has already wedged, reset it with agent-browser close --all (raw-CDP does not use that daemon and is unaffected).

  • Always use electron-dev.sh stop to clean uppkill -f "Electron" only kills the main process; helper processes (GPU, renderer, network) survive. The script finds and kills all of them via PID matching against the project's electron binary path.

  • npx electron-vite dev must run from apps/desktop/ — running from project root fails silently. The electron-dev.sh script handles this automatically.

  • Dev build auto-opens DevTools, which hijacks the CDP targetagent-browser --cdp 9222 may attach to the DevTools page (devtools://…) instead of the app (app://renderer/). Symptom: get url returns a devtools:// URL. Fix: close the DevTools target and reconnect:

    bash
    DT_ID=$(curl -s http://localhost:9222/json/list | python3 -c "import json,sys; ts=json.load(sys.stdin); print(next(t['id'] for t in ts if t['type']=='page' and t['url'].startswith('devtools://')))")
    curl -s "http://localhost:9222/json/close/$DT_ID" > /dev/null
    agent-browser close --all && agent-browser --cdp 9222 get url # expect app://renderer/
    
  • Don't resize the Electron window after load — resizing triggers full SPA reload

  • Store is at window.__LOBE_STORES not window.__ZUSTAND_STORES__

  • Streaming / ticking UI needs GIF evidence — see scripts/record-gif.sh; a static screenshot cannot prove time-based behavior.