Skip to content

LFDOM ​

Global:LFDOM · Enable:// @require dom · Includes:network (automatic)

DOM and page-operation library: readiness, HTML mounting, toasts, Playwright-style locators (auto-wait/retry), semantic snapshots (see → act), card aggregation, form filling, virtual-list collection, screenshots, and network waits.

Page environments only: saved/installed apps and liveDebug (the isolated USER_SCRIPT world), plus run --tab helper scripts (MAIN world by default). Background app branches (// @match background / run --target background) and run without --tab (the editor sandbox) have no target page: with @require dom the library still loads, but it works on a hidden zero-size sandbox document — page selectors match nothing, visibility-gated actions such as click / fill / hover / waitFor (visible) time out with a [no web page in the <env> environment …] hint, and waitForResponse / waitForRequest reject with LF_NETWORK_NO_TAB. Branch on LFRuntime.executionEnvironment and never deliver UI there. @require dom injects network at runtime so waitForResponse / waitForRequest work, but calling LFNetwork.* directly in your source still requires // @require network.

Methods ​

ts
LFDOM.ready(callback): void
LFDOM.ready(options, callback): void
await LFDOM.ready(options?): Promise<void>

Runs the callback once the document leaves loading (DOMContentLoaded); runs it synchronously if the DOM is already parsed. Not idempotent: every call runs its own callback. options.waitFor (a CSS selector, or a predicate that returns truthy when ready, may be async) additionally waits for an SPA body to render; options.timeout defaults to 30000ms. A timeout does not reject — it logs a console warning and runs the callback anyway. Without a callback it returns a Promise

ts
LFDOM.whenReady(options?): Promise<void>

Promise-based readiness, equivalent to await LFDOM.ready(options); same options, plus pierce (default false), interval (polling starts at 50ms) and maxInterval (backoff cap, 500ms)

ts
LFDOM.mountHTML(html, parent?): Element | Element[] | Node | null

Mounts an HTML string as real nodes (parent defaults to document.body): one top-level element returns that element, several sibling elements return an array, plain text returns a Text node, an empty string returns null

ts
LFDOM.createFloatingRoot(options?): HTMLDivElement

Creates a fixed floating root; the same app reuses the same id, and if the id is taken by another app or the page it uses <id>--lf-<appId> instead

ts
LFDOM.toast(message, duration?): HTMLDivElement

Shows an auto-dismissing toast

ts
LFDOM.locator(selector, options?): Locator

Creates a locator: queries + actions (click/fill/hover…) with auto-wait and retry; selector may also be an @N ref from a snapshot

ts
LFDOM.activeElement(): Locator

Locator for the currently focused element

ts
await LFDOM.clickAt(x, y, options?): Promise<Element>

Clicks at viewport coordinates

ts
LFDOM.mountAnchoredHTML(options): AnchoredMountState

Anchored mount next to a page element; mounts when the anchor appears and re-mounts when the anchor is replaced (only while the anchor's direct parent stays the same — see reanchorOnLost below)

ts
LFDOM.unmountAnchored(key): boolean

Unmounts an anchored mount

ts
LFDOM.mountFloatingHTML(options): FloatingMountState

Floating mount that follows an anchor element

ts
LFDOM.unmountFloating(key): boolean

Unmounts a floating mount

ts
LFDOM.mountHoverAction(options): HoverActionState

Mounts a shared hover action for card feeds; see mountHoverAction

ts
LFDOM.waitForElement(selector, options?): Promise<Element | true>

Waits for an element to reach a state (default 'attached' — note this differs from Locator.waitFor, which defaults to 'visible'), default timeout 5000ms; resolves true instead of an element when state is hidden / detached

ts
LFDOM.fillForm(map, options?): Promise<Record<string, true>>

Fills forms from a "CSS selector / @N ref → value" map (label text is not accepted as a key)

ts
LFDOM.findInVirtualList(options): Promise<Array<Item>>

Scroll-collects every item of a virtual list with dedupe

ts
LFDOM.waitForResponse(urlPattern, options?): Promise<NetworkRecord>

Waits for a response matching a URL pattern (LFNetwork-backed; resolves as soon as response headers arrive)

ts
LFDOM.waitForRequest(urlPattern, options?): Promise<NetworkRecord>

Waits for a request matching a URL pattern

ts
LFDOM.intercept(urlPattern, handler): () => void

Monkey-patches fetch/XHR of the current JS world to mock responses (returns an unsubscribe function). Only suitable for ad-hoc probing from run --tab (MAIN world): saved apps and liveDebug run in the isolated world and never see the page's own requests, and a formal Compile rejects any source that contains LFDOM.intercept. For product code use LFRuntime.network and declare @intercept. Handlers that are not unsubscribed are removed automatically when the run process ends or liveDebug hot-reloads; once the last handler is removed, the original fetch/XHR are restored

ts
LFDOM.screenshot(target?, options?): Promise<string>

Serializes the DOM of the page or an element into an SVG foreignObject and returns a data:image/svg+xml string — not a pixel screenshot (no external stylesheets, canvas, video or cross-origin images); use the terminal screenshot command for a real capture

ts
LFDOM.actAsHuman(options?): LFDOM

Returns a proxy: only top-level click / dblclick / hover / fill / type / press called through that proxy get a random delay first; LFDOM itself is unchanged, and Locator actions are not delayed

ts
await LFDOM.snapshot(options?): Promise<{ text, nodes, cards, ... }>

Semantic snapshot text with @N refs — see the page, then act

ts
await LFDOM.cards(options?): Promise<CardRecord[]>

Aggregates repeated card structures (product lists etc.) into records

ts
await LFDOM.layoutAudit(root?, options?): Promise<{ text, issues, count }>

Layout audit: overlap/overflow/contrast issues

ts
await LFDOM.fireReactHandler(selector, eventName?): Promise<boolean>

Fires a React synthetic handler directly (stubborn hover cases)

Parameter details ​

createFloatingRoot(options?) / toast(message, duration?) ​

ParameterTypeDefaultDescription
options.idstringuserscript-lf-rootDOM id of the root; the same app reuses one element per id, and an id already taken by another app/the page becomes <id>--lf-<appId>
options.classNamestring''Added to the root's classList on every call (including when an existing root is reused)
options.zIndexnumber / string2147483000z-index of the new root, leaving headroom for the site's own overlays (not changed when reusing)
message (toast)string—Plain text via textContent; no HTML parsing
duration (toast)number2400Total visible ms; call .remove() on the returned element to dismiss early

A newly created root is styled position: fixed; top: 16px; right: 16px; max-width: 420px. If page scripts remove it, it re-attaches itself to body; only calling the root's own .remove() removes it for good.

Locators ​

LFDOM.locator(selector, { root?, pierce? }) creates a lazy locator: actions auto-wait for attached/visible/stable/enabled (default timeout 5000ms with retry) and pierce open Shadow DOM by default. selector must be native CSS or an @N ref from a snapshot — Playwright-only pseudo-selectors like :has-text() throw at construction. For text matching use the semantic constructors:

ConstructorDescription
getByRole(role, { name?, exact? })Match by accessible role + name (button/link/textbox/checkbox …, incl. implicit HTML roles); the preferred way to target interactive controls
getByText(text, { exact? })Match visible text; resolves to the innermost enclosing element; substring by default (case-insensitive, whitespace-normalized), exact: true for exact
getByLabel(text)Match a labeled form control (aria-label / for= / wrapping label); resolves to the control itself
getByPlaceholder(text) / getByTitle(text) / getByAltText(text) / getByTestId(id)Match by placeholder / title / img alt / data-testid

Chaining and narrowing: locator(child), filter({ hasText, hasNotText, has, hasNot }), first() / last() / nth(i), and(other) / or(other), all(), count(). Semantic queries can be scoped to a parent locator (e.g. row.getByTitle('Delete')) — the recommended way to disambiguate repeated buttons. When a name can be a substring of sibling labels, pass { exact: true } or scope the query.

Actions / readsDescription
click({ modifiers?, position?, force? }) / dblclick / tripleClick / hover / tap / dragTo(target)Full browser event sequence (pointer→mouse→click); modifiers: ['Control'] enables Ctrl+Click multi-select etc.
fill(value) / type(text) / clear()Work on inputs/textareas and contenteditable, firing input/change so React/Vue controlled inputs update; throw on readonly/disabled targets — { force: true } overrides
press(key)Real default behavior (character insertion, Backspace, caret movement, Control+a select-all, Enter submits a form, Tab moves focus); combos use Modifier+Key syntax
selectOption(value)Native selects only; accepts a string (value or visible text), { value } / { label } / { index }, or arrays; custom dropdowns must be driven like a user (click trigger, then option)
check() / uncheck() / setChecked(bool) / setFiles(files)Checkbox/radio/file inputs; hidden-input + styled-label custom controls auto-redirect to the visible label; setFiles returns a receipt { ok, count, names, input }, not the element
focus() / blur() / scrollIntoViewIfNeeded()Focus and scrolling
waitFor({ state, timeout })state: attached / visible (default) / hidden / detached / stable
textContent() / innerText() / innerHTML() / inputValue() / getAttribute(name) / boundingBox()Content and geometry reads
isVisible() / isHidden() / isEnabled() / isDisabled() / isChecked() / isEditable()State checks
elementHandle() / elementHandles() / elementHandleOrNull()Get native elements (the first waits for attachment; the other two return immediately)
evaluate(fn) / evaluateAll(fn)Run custom functions against matched elements
allInVirtualList(options?)Scroll-collect virtual-list items within this Locator's scope; equivalent to findInVirtualList({ ...options, locator: this }). Uses the same options as findInVirtualList below; specify itemSelector. Returns Promise<Array<{ key, text, html, href, dataset }>>
fireReactHandler(eventName?, options?) / dispatchReact(eventName?, options?)Equivalent methods returning Promise<boolean>. eventName is a React event name, default 'onMouseEnter'; supports options.modifiers (e.g. ['Control']) and uses force: true to bypass actionability checks. Tries the current world, then MAIN world; returns false without a handler and times out after 5000ms by default if the element is missing. Fallback when hover() fails

mountAnchored / mountFloating(options) ​

Mount a button/panel next to an existing page element. Both share most options and use LFRuntime.mountHot internally, so liveDebug reinjection reuses the element and calls onUpdate instead of duplicating DOM:

OptionTypeDefaultDescription
keystringrequiredMount slot identifier
anchorstring / Element / LFLocator / () => ElementrequiredAnchor: CSS selector, element, semantic locator, or a function returning an element (anchorSelector is an alias)
position (anchored)'beforebegin' / 'afterbegin' / 'beforeend' / 'afterend''afterend'Insertion position relative to the anchor (participates in native layout); any other value silently falls back to 'afterend'
placement (floating)'left' / 'right' / 'top' / 'bottom' / 'overlay''right'Where the fixed-position float sits relative to the anchor (never enters the anchor parent's child list)
offset (floating)number8Gap from the anchor (replaces margin)
host (floating)Elementdocument.bodyParent the float is appended to; the float is forced to position: fixed and margin: 0, with z-index 2147483646 unless you set one
html / factory / vue—one ofHTML fragment / factory function / Vue config { createApp, h?, component, props?, setup? } (reactive props require passing h too)
onMount / onUpdate / onDisposeFunction—Mount / hot-update / teardown callbacks
persistentbooleantrueKeep the element through liveDebug teardown; pass false or call unmountAnchored(key) for strict removal
reanchorOnLostbooleantrueRe-resolve the anchor when a framework replaces it (only an explicit false disables it; string/Locator/function anchors only). Known limitation: works only while the anchor's direct parent stays the same — when a framework re-renders the parent or a higher ancestor along with it, the anchored element disappears with the old subtree and is not re-mounted, and the float does not follow the new anchor
anchorTimeoutnumber30000Timeout in ms to wait for the anchor
piercebooleanfalseWhether the anchor lookup pierces Shadow DOM (unlike locators, which pierce by default; pass true for anchors inside a shadow root)
suppressInsertionWarning (anchored)booleanfalseSilences the liveDebug warning about beforebegin / afterend insertion into framework-managed containers

Mounting is asynchronous: the returned state exposes state.ready (a Promise) — await state.ready or use onMount before operating the mounted element. Choosing: prefer mountAnchored when the anchor's parent is a stable container; use mountFloating when you must sit next to a Vue/React-managed button without breaking the framework's child diff.

mountHoverAction(options) ​

Use this method for all card-feed hover actions. One persistent fixed root follows the active card through document-level pointer delegation, with delayed hiding and repositioning on scroll / resize.

OptionDescription
keyRequired; slot identifier unique within the bundle and stable across hot updates
cardSelectorRequired; CSS selector for cards
html / factory(state) => ElementChoose one to create the shared action element, or use vue instead
vueVue config { createApp, h?, component, props?, setup? }, replacing html / factory
placementDefaults to 'top-right-inside'; also supports 'top-left-inside' / 'bottom-right-inside' / 'bottom-left-inside'
offsetPosition offset relative to the card
hideDelayDelay before hiding
getIdentity(card)Capture a stable business identifier when shown; passed directly to onActivate without re-reading hover state
onActivate({ event, card, identity, element, state })Click callback
onShow / onHide / onMount / onDisposeShow / hide / mount / teardown callbacks

Returns a HoverActionState with:

Field / methodDescription
root / elementShared fixed root / action element
activeCard / identityCurrent active card / business identifier
visibleWhether the action is visible
show(card)Show on the specified card
hide()Hide the action
reposition()Recalculate position
  • onActivate handles real clicks during the root's capture phase. Button listeners added in onMount can call stopPropagation() / preventDefault() without blocking it; click forwarding is usually unnecessary.
  • Vue buttons must use the vue config: one instance is reused and unmounted automatically. Do not replace it with a custom HoverMount / createFloatingRoot / createApp state machine.
  • Do not repeatedly call mountFloating / unmountFloating from mouseover / mouseout; the button could disappear between mousedown and click.

Example

js
// @require dom

LFDOM.mountHoverAction({
  key: "product-hover-action",
  cardSelector: "[data-product-id]",
  html: '<button type="button">Show product ID</button>',
  getIdentity: (card) => card.dataset.productId,
  onActivate: ({ identity }) => LFDOM.toast(`Product ID: ${identity}`),
});

snapshot(options?) / cards(options?) ​

OptionTypeDefaultDescription
interactiveOnlybooleanfalseKeep only interactive roles (button/link/textbox …); named non-interactive nodes such as headings are dropped too
viewportOnlybooleanfalseKeep only nodes intersecting the viewport
contentOnlybooleanfalseSkip footer/banner/navigation landmarks
groupCardsbooleanfalseCollapse repeated cards into one line each and fill cards[]
cardMinRepeatnumber3Minimum repetition to detect a card group (minimum 2)
cardSelector (cards)string—Deterministic fallback when the heuristic misses: target card elements directly
cardRootElementsame as rootDetect cards only inside this subtree
maxNodes / maxCardsnumber4000 / 200Output caps
maxCardActions / maxCardDescendantsnumber12 / 150Max actions collected per card / max descendants scanned when detecting cards
rootElementdocument.documentElementScope the walk to a subtree; must be an Element (a Document / ShadowRoot is ignored and the default is used)

snapshot returns { text, nodes, cards, count, refCount }: text is an indented accessibility tree with @N refs; each nodes[] entry carries { ref, role, name, depth, url, region, states }. An @N ref works anywhere a selector is accepted — LFDOM.locator('@42'), or top-level shortcuts such as await LFDOM.click('@42') — but only for the most recent snapshot: every snapshot rebuilds the ref table, so snapshot again after the page changes; a stale ref throws immediately. cards returns an array of { index, depth, region, ref, title, url, image, text, actions: [{ ref, role, name }] }, ideal for structured iteration over product grids/feeds.

Other common methods ​

MethodParameters
waitForElement(selector, { timeout?, state? })state defaults to 'attached', timeout to 5000ms; returns the element for attached/visible/stable and true for hidden / detached
fillForm(map, { root?, timeout? })map keys are CSS selectors or @N refs (label text is not supported — to target by label, use getByLabel and act on each control); values pick fill/selectOption/setChecked per control type; the first failing field throws and already-filled fields are not rolled back; root can scope into an iframe document or ShadowRoot
findInVirtualList({ itemSelector, scrollContainer?, maxItems?, idleMs?, dedupeKey?, timeout? })Scroll-collects and dedupes virtual-list items, returns { key, text, html, href, dataset }[]. itemSelector is effectively required (it defaults to '*', which matches page-level elements); scrollContainer may be a selector string, an Element or a Locator — omit it (or pass 'window') for page scrolling; maxItems defaults to 100; idleMs (stop after this long without new items) defaults to 300, minimum 50; dedupeKey defaults to data-id, then href, then text, or pass (element) => string
waitForResponse(urlPattern, { timeout?, networkOptions? }) / waitForRequest(...)urlPattern accepts string (substring)/RegExp/predicate; timeout defaults to 5000ms. The JS listener registers synchronously, but if LFNetwork is not already listening it calls start() asynchronously in the background, and requests sent before the debugger attaches are missed — call await LFNetwork.start() once first (requires // @require network), then set up the wait before triggering: const p = LFDOM.waitForResponse(/api/); await action(); const rec = await p;. waitForResponse resolves when response headers arrive (rec.finished is usually still false); to read the body, wait for finished and then call LFNetwork.getResponseBody(rec.requestId). A monitor it started implicitly is not stopped automatically; networkOptions is ignored when already listening
intercept(urlPattern, handler)MAIN-world ad-hoc probing only (see above). urlPattern accepts string (substring)/RegExp/predicate; the handler receives { type: 'fetch', url, input, init } or { type: 'xhr', method, url, body } and returns { body, status?, headers? } to mock (status defaults to 200) or a falsy value to pass through; XHR mocks only set readyState / status / responseText
screenshot(target?, options?)target is a CSS selector or Element (not a Locator), the whole document when omitted; options only affects waiting for the element; returns data:image/svg+xml;charset=utf-8,...
actAsHuman({ delay? })Returns a proxy; top-level click/dblclick/hover/fill/type/press(selector, …) called through it first wait a random delay–2×delay ms (delay defaults to 80)
clickAt(x, y, { modifiers? })Hit-tests the deepest element at viewport coordinates (piercing open Shadow DOM) and dispatches the full event sequence; throws when nothing is at that point; only for genuine coordinate cases (canvas hot-spots)
layoutAudit(root?, { maxIssues?, contrast?, overlap? })root may be a selector/Element/Locator and falls back to body when omitted or unmatched; maxIssues defaults to 50 (cap 200), contrast / overlap default to true. Returns { text, issues: [{ type, element, detail }], count }, where element is a descriptive string, not an Element; issue types include horizontal overflow, zero-size interactive, covered interactive, sibling overlap, clipped text, low contrast
fireReactHandler(selector, eventName?)Invokes a React synthetic handler on the fiber directly (default onMouseEnter): returns false when the element exists but has no handler, and throws after the wait times out (default 5000ms) when the element does not exist; the fallback when hover() cannot reveal a flyout
frameLocator(selector)Enter a same-origin iframe: LFDOM.frameLocator('#if').getByRole(...); chainable through nested iframes, supports nth(i) / first() / last(); .frame() resolves to Promise<Document>

Top-level shortcuts ​

These are equivalent to LFDOM.locator(selector).xxx(...); selector likewise accepts CSS or an @N ref, with the same auto-wait rules and 5000ms default timeout:

MethodEquivalent to
click(selector, options?) / dblclick / hoverlocator(selector).click(options) etc.
fill(selector, value, options?) / type(selector, text, options?) / press(selector, key, options?) / clear(selector, options?)The matching Locator input actions
text(selector) / value(selector) / attr(selector, name)innerText() / inputValue() / getAttribute(name)
exists(selector) / visible(selector) / count(selector)Immediate existence check (no waiting) / isVisible() / count()
waitFor(selector, options?) / waitForHidden(selector, options?)locator(selector).waitFor(options) (default state 'visible') / state fixed to 'hidden'
getByRole / getByText / getByLabel / getByPlaceholder / getByTitle / getByAltText / getByTestIdPage-wide semantic constructors (see "Locators")
waitForUrl(pattern, options?)Polls until location.href matches (substring/RegExp/predicate), resolves to the current URL
waitForLoadState(state?)Defaults to 'domcontentloaded'; 'networkidle' is only a fixed 500ms wait and does not detect network idleness
waitForFunction(fn, options?)Polls until fn() returns truthy
mountAnchored / mountFloatingSame as mountAnchoredHTML / mountFloatingHTML
timeoutError(message, details?)Builds a timeout error consistent with the library's own

Example

js
// @require dom

await LFDOM.whenReady();

// Playwright-style locators: automatic waiting and retries
await LFDOM.locator("#submit-button").click();
await LFDOM.locator("input[name=q]").fill("keyword");

// Semantic snapshot: page structure as text with @N references (inspect, then act)
const snap = await LFDOM.snapshot({ interactiveOnly: true });
LFRuntime.print(snap.text);

// Wait for an element
const row = await LFDOM.waitForElement(".order-row", { timeout: 8000 });