Skip to content

Debugging methodology: worlds / verification / SPA selectors ​

The three most common traps when debugging injected apps: misjudging which world a script runs in, "verifying" interactions with unreliable methods, and relying on unstable selectors in SPAs. This section gives a reusable methodology.

World semantics: where does the script actually run ​

Execution pathWorldVisibility
Installed app / liveDebug onUSER_SCRIPT (isolated)Shares the page DOM but not JS globals: page-framework window variables are invisible to you, and yours to the page
run --tab <target> (default)MAIN (page world)Same environment as page JS; cannot see the liveDebug app's globals living in the USER_SCRIPT world
run --tab <target> --world user-scriptUSER_SCRIPT (same world as liveDebug)Can directly probe the liveDebug app's globals and listener state
LFRuntime.runInMainWorld(fn)USER_SCRIPT → MAINRead page-framework data (e.g. React/Vue internals) from inside the app

The most common misdiagnosis

Probing "did my app mount?" with plain run --tab, seeing window.__myPanel === undefined, and concluding the injection failed — while the script is running fine in the USER_SCRIPT world where you cannot see it from MAIN. Probing liveDebug app state requires --world user-script.

Both worlds share the same DOM: DOM nodes and DOM events (including dispatchEvent) cross world boundaries. Only JS globals, prototype patches, and closures stay isolated.

Verification methodology: observe → act → assert ​

  1. Observe: snapshot before touching anything

    Run snapshotText --tab <t> to get an accessibility snapshot with @N refs; confirm the target element exists with the expected role and text. Never write selectors from memory.

  2. Act: trusted input, not synthetic events

    input hover/click/fill --ref @N dispatches trusted input via CDP: it triggers CSS :hover, framework-native event delegation, and browser default behavior. dispatchEvent(new MouseEvent(...)) is synthetic (isTrusted: false), does not trigger :hover styles, and some frameworks ignore it. Verifying hover-revealed buttons requires input hover.

  3. Assert: check output, not "no error"

    After acting, run snapshotText again (or use input ... --expect ...) to confirm the UI reached the expected state. "Command succeeded" does not mean "the page changed as expected".

Hypothesis triage: when behavior is off, run one minimal probe to distinguish "not injected" / "injected but not mounted" / "mounted but selector missed" / "matched but event never fired" before editing code. Verify one hypothesis per round.

SPA selector guidance ​

  • Class names are usually build-time hashes (e.g. feed-active-video-xK3f) and break on every release; prefer semantic attributes (role / aria-* / data-*), stable text, or snapshotText @N refs
  • The same list may use different classes per state (e.g. active feed-active-video vs plain feed-video); snapshot each state and compare before writing a selector
  • SPA route changes (history.pushState) keep the same document: liveDebug does not reinject, so your MutationObserver / event delegation must cover newly rendered nodes; use LFDOM.waitForElement / LFDOM.locator (auto-wait and retry) instead of a one-shot querySelector
  • Virtual lists only render visible rows — querySelectorAll count ≠ total data; collect with LFDOM.findInVirtualList
  • Hover-revealed widgets (menus, tooltips) may be absent from the snapshot: input hover --ref first, then take a second snapshot