Live Extension UI Automation
Core principle
Automate and test the development browser extension UI against real accounts and logged-in states without closing or disturbing the user's daily primary browser.
Choose the runtime
- Android phone/tablet, including Firefox, Edge, or Lemur (狐猴): read mobile debugging and hot updates. Use the unified
scripts/mobile-browser.mjs entrypoint for discovery, diagnostics, and development sessions. Diagnose the installed release when that is the user's goal; do not replace it with a dev build merely to obtain execution access.
- Desktop Chromium: use the profile and CDP workflow below. Its build, profile-sync, and launcher steps do not apply to Android.
Desktop Chromium
Chromium blocks --remote-debugging-port on the system default user data directory. To preserve daily browser workflows while enabling CDP automation, tests run in a dedicated dev profile (AllApiHub/dev-browser) that shares accounts and sessions across all Git worktrees.
Shared vs isolated dev profiles
By default every worktree mounts the single shared profile (AllApiHub/dev-browser) on CDP port 9222. This reuses one account login across worktrees but couples their browser instances: a --restart mounts a different worktree's build and closes another session's tabs, and discovery by title can target the wrong extension.
When concurrent worktrees must not interfere (multi-agent runs), mount a per-worktree profile with --isolate. The profile resolves to AllApiHub/dev-browser-<worktree>-<path-hash>, using the canonical checkout path to distinguish same-named worktrees. Its default CDP port is a stable offset from 9222 based on the resolved profile path. Hash collisions remain possible; isolated clients and the launcher verify the listening browser uses the expected profile before reuse, reload or restart. On a collision, choose a free explicit CDP_PORT. Assess existing sessions before seeding an isolated profile:
| Goal | Command |
|---|
| Isolated browser (own profile + port) | pnpm browser:cdp:isolate |
| Offline sync when both browsers are already closed | pnpm browser:sync:isolate -- --cookies |
| Env-var equivalents | AAH_DEV_PROFILE_PER_WORKTREE=1 (or --isolate); explicit AAH_DEV_PROFILE_DIR / CDP_PORT still win. |
Live suites (e2e:cdp:*) compute their default CDP URL from the same resolver, so launching and driving under --isolate need no extra flags. The trade-off: each isolated profile needs its own first login or sync, and each buys an extra resident browser process. The shared-profile default is unchanged.
Available Tooling & Commands
All commands are cross-platform (Windows, macOS, Linux) and runnable directly via pnpm:
| Command | Script | Description |
|---|
pnpm browser:sync | scripts/sync-extension-profile.mjs | Discovers extension storage sources (--list); copies databases only with browsers closed. Website sessions require --cookies. |
pnpm browser:cdp | scripts/launch-cdp-browser.mjs | Launches (or connects to) the dedicated debug browser on port 9222 with the current worktree extension mounted. |
pnpm e2e:cdp | scripts/e2e-cdp-control.mjs | Connects via CDP, detects the current worktree extension, opens its UI, and automates interactions. |
Workflow
1. Build Current Worktree Output
Always ensure the extension is built for the current branch:
pnpm build
2. Launch or Reuse the CDP Browser
pnpm browser:cdp
- Prioritize Dev Mode (Auto-Spawn): By default, the launcher prioritizes the HMR dev mode (
.output/chrome-mv3-dev). If WXT dev server is not running, it automatically and silently launches it in the background, waits for the initial pre-rendering to finish, and mounts the hot dev extension.
- Actively Suppress Duplicate Browser Windows: WXT defaults to opening an unconfigured Chrome instance upon launch. To avoid desktop clutter and preserve the shared profile with real accounts,
wxt.config.ts disables the built-in runner (webExt.disabled = true unless WXT_OPEN_BROWSER=1), and the launcher passes environment guards. Dev server logs stream silently to .scratch/wxt-dev.log.
- Dynamic Port & Multi-Worktree: When multiple worktrees run dev servers concurrently, WXT assigns dynamic ports (
3000, 3001, 3002...). The launcher inspects .output/chrome-mv3-dev/options.html and probes the Vite client to bind cleanly to the current worktree's assigned port over IPv4 (127.0.0.1).
- Live Extension Hot-Reload: If the debug browser on port 9222 is already open, running
pnpm browser:cdp instantly triggers chrome.runtime.reload() over CDP and refreshes open extension tabs without restarting the browser.
- Convenience Flags:
pnpm browser:cdp -- --reload: Instantly trigger chrome.runtime.reload() in the running browser.
pnpm browser:cdp -- --stop-dev: Terminate the background WXT dev server process.
pnpm browser:cdp -- --prod: Force standalone production build mode (.output/chrome-mv3).
pnpm browser:cdp -- --build: Force a full production rebuild before launching.
pnpm browser:cdp -- --restart: Terminate the running debug browser and start a fresh instance.
3. Verify Session Readiness Before Authenticated Flows
Checking readiness is required; synchronizing state is conditional. Use the site's authenticated identity endpoint in the selected dev profile, not a saved extension account, tab title or Cookie count. Reuse a valid target session immediately, checking the expected account ID when known.
If the target is unauthenticated, inspect task-authorized source sessions and select an applicable sync method before asking the user to log in. A target 401 establishes only that the target lacks authentication. A closed-browser requirement applies to database copying, not to all session reuse. Network failures, challenges and identity mismatches require diagnosis rather than a login request.
Read session assessment and reuse for the shared preflight helper, origin-scoped live transfer and offline sync commands. After transfer, re-read the target identity and compare it with the source. Request manual login only after authorized sources and applicable transfer methods have been checked and found unusable; report missing evidence as uninspected, not unavailable. Do not replace an authenticated but mismatched account silently.
4. Automate and Verify the Extension UI
Run the default smoke runner:
pnpm e2e:cdp
5. Authoring Custom Automated E2E/CDP Tests
When implementing a new feature or reproducing UI bugs, author or extend a CDP automation script following this pattern:
import path from "node:path"
import { chromium } from "@playwright/test"
const browser = await chromium.connectOverCDP("http://127.0.0.1:9222")
const context = browser.contexts()[0]
// 1. Locate the extension (CDP Target inspection works even if the service worker is dormant)
const session = await browser.newBrowserCDPSession()
const { targetInfos } = await session.send("Target.getTargets")
const extTarget = targetInfos.find((t) => t.url?.startsWith("chrome-extension://"))
const extensionId = new URL(extTarget.url).hostname
await session.detach()
// 2. Open any extension entrypoint
const page = await context.newPage()
// Stabilize Framer Motion animations and transitions
await page.emulateMedia({ reducedMotion: "reduce" })
await page.goto(`chrome-extension://${extensionId}/options.html#account`)
await page.waitForLoadState("domcontentloaded")
// 3. Interact with UI primitives via testId or role
const root = page.locator('[data-testid="options-app"]')
await root.waitFor({ state: "attached", timeout: 10000 })
// Await explicit visibility for elements rather than static sleeps
const target = page.locator('[data-testid="account-table-row"]').first()
await target.waitFor({ state: "visible", timeout: 8000 })
// Radix dialogs/popovers render into document.body portals; locate directly from page
const dialog = page.locator('[role="dialog"]').first()
// 4. Inspect or mutate extension storage directly via service worker
// NOTE: Plasmo Storage serializes envelopes as JSON strings (e.g. { accounts: [...] })
const sw = context.serviceWorkers().find((w) => w.url().includes(extensionId))
const accounts = await sw.evaluate(() => {
return new Promise((resolve) => {
chrome.storage.local.get("site_accounts", (res) => {
const data = typeof res.site_accounts === "string" ? JSON.parse(res.site_accounts) : res.site_accounts
resolve(data?.accounts || [])
})
})
})
Account onboarding validation
For account integrations, use the authentication and guidance contract in account sites. Drive a fresh add-account form with its normal authentication default through automatic detection, any credential guidance, verification, save, and refresh. Manually preselecting another method or pre-seeding a saved account only validates that alternate path; it does not establish that ordinary onboarding works.
When credentials require user interaction, assert that detection preserves the discovered account context, offers the official acquisition page and a clear return/paste action, and reaches an actionable state. Exercise the persistent UI handoff when starting from a popup. Record the authentication mode and starting state in the reusable runner's evidence. If the credential is unavailable, validate the guide independently and report save/refresh as unverified; do not count a successful Cookie fallback as proof of the preferred token path.
Native resource editor readiness
For native resource editors, a visible modal may still contain only the opening/loading shell while fresh provider data is fetched. Wait for an always-present field from the accepted editor before reading values or asserting secret retention, with a bounded timeout suitable for live provider requests. Modal visibility alone and the default short DOM assertion timeout do not establish that the editor has loaded; avoid replacing this check with a fixed sleep. Once the editor is ready, a missing conditional control may indicate an incomplete detail projection or incorrect visibility state; compare the saved configuration and controlling value before increasing timeouts or forcing the control visible.
Developer image previews
For UI visual regression capture, stabilization, and side-by-side result presentation, follow UI visual evidence and capture guidelines:
- Component focus with context: Target the immediate semantic parent (e.g. table row
tr, card .card, action group, or dialog section) at natural pixel scale instead of full-page viewports (which shrink details to unreadable thumbnails) or micro-cropped icons (which strip spatial context).
- Stabilization & waiting: Never rely on static sleeps. Wait explicitly for element visibility (
waitFor({ state: "visible" })). Freeze Framer Motion transitions (emulateMedia({ reducedMotion: "reduce" }) and animations: "disabled").
- Side-by-side composite stitching: Stitch Before (left, amber/red label) and After (right, emerald/green label) into single composite images (
[BEFORE | AFTER]) using pnpm visual:compare or image compositing, avoiding detached files. For multi-surface updates, provide consolidated Markdown reports or standalone HTML galleries.
For deterministic Playwright workflows, use pnpm visual:capture --help and the generic capture/comparison workflow. Select an existing test via --spec --grep --fixture and add opt-in captureVisualViews checkpoints; optional --view settings control dimensions/themes. No registry entry is needed for new surfaces; --list exposes maintained hotspot shortcuts. This runner handles current-worktree build validation, capture, metadata and optional baseline pairing in temporary browser profiles. It does not replace authenticated site-specific CDP workflows or add automatic image inspection.
For site adaptation or a requested visual handoff, follow visual previews. Persist screenshots at meaningful asserted states in the site-specific runner. Agents prioritize representative final results when visual judgment is needed; inspect baseline/comparison/diff images only for a concrete question, rather than after every small edit. Display representative images directly with brief captions, reusing retained artifacts without another image-tool call. Local developer evidence is raw and unmasked by default, lives outside Git, and uses the shared evidence storage rules. Keep automatic CI/real-site capture settings unchanged; report missing live access or image-rendering limitations explicitly.
Multi-Worktree Conventions
- Shared Profile: All worktrees resolve to the same OS-level data folder (
%LOCALAPPDATA%\AllApiHub\dev-browser on Windows, ~/Library/Application Support/AllApiHub/dev-browser on macOS). Accounts and cookies persist across branch switches.
- Current Branch Targeting:
scripts/e2e-cdp-control.mjs checks process.cwd() against extension manifest titles to ensure it controls the extension belonging to the active worktree when multiple branches are mounted.