fix(input): mousedown focuses tabindex targets, not only contenteditable

focusEditingHostForMouseDown only walked contenteditable hosts, so a
div[tabindex=0] stayed unfocused after click. Replace it with
focusForMouseDown: still prefer the outermost editing host, then focus
the nearest mouse-focusable element (including tabindex=-1).

Call sites: actions.click, CDP triggerMousePress, WebDriver pointerDown.

Tests cover the MCP selector path, agent Page.click on the HTML fixture,
and a runtime-created child click (ancestor walk, non-focusable must not
steal focus, tabindex=-1 is mouse-focusable).
This commit is contained in:
Ramiro_quai committed 2026-09-07 23:31:09 -06:00
1 parent 46ac5094c5
commit e2d878eee8
6 files changed
+122 -11

No files matched your search

+50
View File
@@ -1441,6 +1441,56 @@ test "agent script runtime: tool errors throw and stop execution" {
);
}
test "agent script runtime: selector click preserves pointer mouse semantics" {
defer testing.test_session.closeAllPages();
var registry = CDPNode.Registry.init(testing.allocator);
defer registry.deinit();
const runtime = try Runtime.init(testing.allocator, testing.test_app, testing.test_session, &registry);
defer runtime.deinit();
try runTestScript(runtime,
\\const page = new Page();
\\await page.goto("http://localhost:9582/src/browser/tests/mcp_actions.html");
\\page.click("#btn");
\\const full = page.evaluate("JSON.stringify(window.seq)");
\\if (full !== '["pointerdown:0:1:mouse:true","mousedown:0:1::true","pointerup:0:0:mouse:true","mouseup:0:0::true","click:0:0:mouse:true"]') throw new Error("wrong full click sequence: " + full);
\\page.click("#btnPreventDefault");
\\const suppressed = page.evaluate("JSON.stringify(window.seqPrevented)");
\\if (suppressed !== '["pointerdown","pointerup","click"]') throw new Error("wrong suppressed sequence: " + suppressed);
\\page.click("#btnDisabled");
\\if (page.evaluate("String(window.disabledMousedowned)") !== "false") throw new Error("disabled button received mousedown");
\\page.click("#focusTarget");
\\if (page.evaluate("document.activeElement.id") !== "focusTarget") throw new Error("mousedown focus default action was lost");
);
}
// Different workflow from the fixture above: the tabindex node is created at
// runtime, the click lands on a child (ancestor walk), a following click on a
// non-focusable node must not steal focus, and tabindex=-1 is still mouse-focusable.
test "agent script runtime: dynamic tabindex child click focuses ancestor" {
defer testing.test_session.closeAllPages();
var registry = CDPNode.Registry.init(testing.allocator);
defer registry.deinit();
const runtime = try Runtime.init(testing.allocator, testing.test_app, testing.test_session, &registry);
defer runtime.deinit();
try runTestScript(runtime,
\\const page = new Page();
\\await page.goto("http://localhost:9582/src/browser/tests/mcp_actions.html");
\\page.evaluate("const p=document.createElement('div');p.id='dynFocus';p.setAttribute('tabindex','0');const s=document.createElement('span');s.id='dynChild';s.textContent='x';p.appendChild(s);document.body.appendChild(p);const n=document.createElement('div');n.id='dynNeg';n.setAttribute('tabindex','-1');n.textContent='neg';document.body.appendChild(n)");
\\page.click("#dynChild");
\\if (page.evaluate("document.activeElement.id") !== "dynFocus") throw new Error("child click did not focus tabindex ancestor: " + page.evaluate("document.activeElement && document.activeElement.id"));
\\page.click("#plain");
\\if (page.evaluate("document.activeElement.id") !== "dynFocus") throw new Error("plain click stole focus: " + page.evaluate("document.activeElement && document.activeElement.id"));
\\page.click("#dynNeg");
\\if (page.evaluate("document.activeElement.id") !== "dynNeg") throw new Error("tabindex=-1 was not mouse-focusable: " + page.evaluate("document.activeElement && document.activeElement.id"));
);
}
test "agent script runtime: builtin argument marshalling (positional + options)" {
defer testing.test_session.closeAllPages();