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
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
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)
LFDOM.mountHTML(html, parent?): Element | Element[] | Node | nullMounts 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
LFDOM.createFloatingRoot(options?): HTMLDivElementCreates 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
LFDOM.toast(message, duration?): HTMLDivElementShows an auto-dismissing toast
LFDOM.locator(selector, options?): LocatorCreates a locator: queries + actions (click/fill/hover…) with auto-wait and retry; selector may also be an @N ref from a snapshot
LFDOM.activeElement(): LocatorLocator for the currently focused element
await LFDOM.clickAt(x, y, options?): Promise<Element>Clicks at viewport coordinates
LFDOM.mountAnchoredHTML(options): AnchoredMountStateAnchored 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)
LFDOM.unmountAnchored(key): booleanUnmounts an anchored mount
LFDOM.mountFloatingHTML(options): FloatingMountStateFloating mount that follows an anchor element
LFDOM.unmountFloating(key): booleanUnmounts a floating mount
LFDOM.mountHoverAction(options): HoverActionStateMounts a shared hover action for card feeds; see mountHoverAction
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
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)
LFDOM.findInVirtualList(options): Promise<Array<Item>>Scroll-collects every item of a virtual list with dedupe
LFDOM.waitForResponse(urlPattern, options?): Promise<NetworkRecord>Waits for a response matching a URL pattern (LFNetwork-backed; resolves as soon as response headers arrive)
LFDOM.waitForRequest(urlPattern, options?): Promise<NetworkRecord>Waits for a request matching a URL pattern
LFDOM.intercept(urlPattern, handler): () => voidMonkey-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
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
LFDOM.actAsHuman(options?): LFDOMReturns 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
await LFDOM.snapshot(options?): Promise<{ text, nodes, cards, ... }>Semantic snapshot text with @N refs — see the page, then act
await LFDOM.cards(options?): Promise<CardRecord[]>Aggregates repeated card structures (product lists etc.) into records
await LFDOM.layoutAudit(root?, options?): Promise<{ text, issues, count }>Layout audit: overlap/overflow/contrast issues
await LFDOM.fireReactHandler(selector, eventName?): Promise<boolean>Fires a React synthetic handler directly (stubborn hover cases)
Parameter details
createFloatingRoot(options?) / toast(message, duration?)
| Parameter | Type | Default | Description |
|---|---|---|---|
options.id | string | userscript-lf-root | DOM 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.className | string | '' | Added to the root's classList on every call (including when an existing root is reused) |
options.zIndex | number / string | 2147483000 | z-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) | number | 2400 | Total 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:
| Constructor | Description |
|---|---|
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 / reads | Description |
|---|---|
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:
| Option | Type | Default | Description |
|---|---|---|---|
key | string | required | Mount slot identifier |
anchor | string / Element / LFLocator / () => Element | required | Anchor: 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) | number | 8 | Gap from the anchor (replaces margin) |
host (floating) | Element | document.body | Parent 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 of | HTML fragment / factory function / Vue config { createApp, h?, component, props?, setup? } (reactive props require passing h too) |
onMount / onUpdate / onDispose | Function | — | Mount / hot-update / teardown callbacks |
persistent | boolean | true | Keep the element through liveDebug teardown; pass false or call unmountAnchored(key) for strict removal |
reanchorOnLost | boolean | true | Re-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 |
anchorTimeout | number | 30000 | Timeout in ms to wait for the anchor |
pierce | boolean | false | Whether the anchor lookup pierces Shadow DOM (unlike locators, which pierce by default; pass true for anchors inside a shadow root) |
suppressInsertionWarning (anchored) | boolean | false | Silences 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.
| Option | Description |
|---|---|
key | Required; slot identifier unique within the bundle and stable across hot updates |
cardSelector | Required; CSS selector for cards |
html / factory(state) => Element | Choose one to create the shared action element, or use vue instead |
vue | Vue config { createApp, h?, component, props?, setup? }, replacing html / factory |
placement | Defaults to 'top-right-inside'; also supports 'top-left-inside' / 'bottom-right-inside' / 'bottom-left-inside' |
offset | Position offset relative to the card |
hideDelay | Delay 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 / onDispose | Show / hide / mount / teardown callbacks |
Returns a HoverActionState with:
| Field / method | Description |
|---|---|
root / element | Shared fixed root / action element |
activeCard / identity | Current active card / business identifier |
visible | Whether the action is visible |
show(card) | Show on the specified card |
hide() | Hide the action |
reposition() | Recalculate position |
onActivatehandles real clicks during the root's capture phase. Button listeners added inonMountcan callstopPropagation()/preventDefault()without blocking it; click forwarding is usually unnecessary.- Vue buttons must use the
vueconfig: one instance is reused and unmounted automatically. Do not replace it with a custom HoverMount /createFloatingRoot/createAppstate machine. - Do not repeatedly call
mountFloating/unmountFloatingfrommouseover/mouseout; the button could disappear betweenmousedownandclick.
Example
// @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?)
| Option | Type | Default | Description |
|---|---|---|---|
interactiveOnly | boolean | false | Keep only interactive roles (button/link/textbox …); named non-interactive nodes such as headings are dropped too |
viewportOnly | boolean | false | Keep only nodes intersecting the viewport |
contentOnly | boolean | false | Skip footer/banner/navigation landmarks |
groupCards | boolean | false | Collapse repeated cards into one line each and fill cards[] |
cardMinRepeat | number | 3 | Minimum repetition to detect a card group (minimum 2) |
cardSelector (cards) | string | — | Deterministic fallback when the heuristic misses: target card elements directly |
cardRoot | Element | same as root | Detect cards only inside this subtree |
maxNodes / maxCards | number | 4000 / 200 | Output caps |
maxCardActions / maxCardDescendants | number | 12 / 150 | Max actions collected per card / max descendants scanned when detecting cards |
root | Element | document.documentElement | Scope 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
| Method | Parameters |
|---|---|
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:
| Method | Equivalent to |
|---|---|
click(selector, options?) / dblclick / hover | locator(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 / getByTestId | Page-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 / mountFloating | Same as mountAnchoredHTML / mountFloatingHTML |
timeoutError(message, details?) | Builds a timeout error consistent with the library's own |
Example
// @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 });