Summary
For a normal-flow <label>text <input></label> (label text followed by an input, the common form pattern), DOM.getNodeForLocation returns the INPUT only in a thin strip at the top of the input's box and returns the wrapping LABEL for the entire center and bottom of the input — even though the input is the topmost, visible, hit-testable element across its whole box (and Chromium/Playwright's Chromium return the INPUT at every one of those points).
This makes CDP-driven click targeting / hit verification unreliable for form controls: an automation that computes an element's clickable point from DOM.getContentQuads (the box center) and confirms it with DOM.getNodeForLocation sees a miss (the label, an ancestor, not the input or a descendant), so it refuses or mis-targets the click.
moli v1.1.2 (x86_64-unknown-linux-gnu), moli serve --host <ip> --port 9222 --layout, connected via playwright-core@1.55.1 connectOverCDP.
Minimal reproduction
Page (a data: URL, no external deps):
<!doctype html><meta charset=utf-8>
<body><form><p><label>Customer name: <input name=custname></label></p></form>
CDP sequence (Node + playwright-core, connected over CDP to a running moli serve --layout):
import { chromium } from "playwright-core";
const b = await chromium.connectOverCDP("http://<moli-ip>:9222");
const ctx = await b.newContext();
const p = await ctx.newPage();
const cdp = await ctx.newCDPSession(p);
const html = "<!doctype html><meta charset=utf-8><body><form><p><label>Customer name: <input name=custname></label></p></form>";
await p.goto("data:text/html," + encodeURIComponent(html), { waitUntil: "load" });
const { result } = await cdp.send("Runtime.evaluate", { expression: 'document.querySelector("input")' });
const desc = await cdp.send("DOM.describeNode", { objectId: result.objectId });
const inputBackend = desc.node.backendNodeId;
const { quads } = await cdp.send("DOM.getContentQuads", { objectId: result.objectId });
const q = quads[0]; // [x1,y1, x2,y2, x3,y3, x4,y4]
const cx = Math.round((q[0] + q[4]) / 2);
for (const y of [q[1] + 1, Math.round((q[1] + q[5]) / 2), q[5] - 1]) { // top, center, bottom of the input box
const nl = await cdp.send("DOM.getNodeForLocation", { x: cx, y: Math.round(y) });
const r = await cdp.send("DOM.resolveNode", { backendNodeId: nl.backendNodeId });
const t = await cdp.send("Runtime.callFunctionOn",
{ objectId: r.object.objectId, functionDeclaration: "function(){return this.tagName}", returnByValue: true });
console.log(`(${cx},${Math.round(y)}) -> ${t.result.value} ${nl.backendNodeId === inputBackend ? "== INPUT" : "!= INPUT <-- BUG"}`);
}
Observed (moli v1.1.2)
INPUT content quad: [156.03,19, 315.97,19, 315.97,35, 156.03,35] (box y ≈ 19..35)
(236,20) -> INPUT == INPUT # top edge: correct
(236,27) -> LABEL != INPUT <-- BUG # center: returns the wrapping <label>
(236,34) -> LABEL != INPUT <-- BUG # bottom: returns the wrapping <label>
The input's getContentQuads box is y ≈ 19..35, so all three points are inside the input, yet only the top ~1px strip hit-tests to the INPUT; the rest returns the ancestor LABEL.
Expected
DOM.getNodeForLocation at any point inside the input's content box should return the INPUT (the topmost hit-testable element there), matching Chromium — as getContentQuads already reports that box as the input's.
Notes / narrowing
- The trigger is the normal-flow label-wraps-text-then-input layout (the input sits on the text baseline within the label's line box). An input positioned so it is not baseline-aligned inside a text-carrying line box (e.g.
position:absolute) hit-tests correctly at every point — so this looks like a vertical hit-test / line-box (baseline-alignment) issue rather than a general control problem.
- A related symptom appears for below-the-fold controls after a scroll (
Page.getFrameTree/scroll then getNodeForLocation) — likely the same hit-test path; happy to open a separate issue with a repro if useful.
- Impact is limited to hit-test verification (
getNodeForLocation); getFullAXTree, describeNode, and getContentQuads all report the input correctly.
Summary
For a normal-flow
<label>text <input></label>(label text followed by an input, the common form pattern),DOM.getNodeForLocationreturns theINPUTonly in a thin strip at the top of the input's box and returns the wrappingLABELfor the entire center and bottom of the input — even though the input is the topmost, visible, hit-testable element across its whole box (and Chromium/Playwright's Chromium return theINPUTat every one of those points).This makes CDP-driven click targeting / hit verification unreliable for form controls: an automation that computes an element's clickable point from
DOM.getContentQuads(the box center) and confirms it withDOM.getNodeForLocationsees a miss (the label, an ancestor, not the input or a descendant), so it refuses or mis-targets the click.moli v1.1.2 (x86_64-unknown-linux-gnu),
moli serve --host <ip> --port 9222 --layout, connected viaplaywright-core@1.55.1connectOverCDP.Minimal reproduction
Page (a
data:URL, no external deps):CDP sequence (Node +
playwright-core, connected over CDP to a runningmoli serve --layout):Observed (moli v1.1.2)
The input's
getContentQuadsbox isy ≈ 19..35, so all three points are inside the input, yet only the top ~1px strip hit-tests to theINPUT; the rest returns the ancestorLABEL.Expected
DOM.getNodeForLocationat any point inside the input's content box should return theINPUT(the topmost hit-testable element there), matching Chromium — asgetContentQuadsalready reports that box as the input's.Notes / narrowing
position:absolute) hit-tests correctly at every point — so this looks like a vertical hit-test / line-box (baseline-alignment) issue rather than a general control problem.Page.getFrameTree/scroll thengetNodeForLocation) — likely the same hit-test path; happy to open a separate issue with a repro if useful.getNodeForLocation);getFullAXTree,describeNode, andgetContentQuadsall report the input correctly.