From 84baf18ee92a620c6da7822c7ddac61856b8bb1b Mon Sep 17 00:00:00 2001 From: PJ Date: Tue, 18 Aug 2026 20:44:15 +0530 Subject: [PATCH] fix(spec): keep a selector out of the head subtree the head renders nothing, so the hierarchy dump drops it and so does the enumeration the picker walks, but a selector still resolved into it: a whole-page findAll answered with and here and with neither on the goja host, which is a divergence the moment a state selector asks a question every element has an answer to. --- internal/driver/chrome/selector_parity_test.go | 16 ++++++++++++++++ pkg/spec/src/web-runtime.ts | 12 +++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/internal/driver/chrome/selector_parity_test.go b/internal/driver/chrome/selector_parity_test.go index 76b3729..d31c9e8 100644 --- a/internal/driver/chrome/selector_parity_test.go +++ b/internal/driver/chrome/selector_parity_test.go @@ -149,6 +149,22 @@ func TestSelectors_ResolveTheSameElementsAsTheWebRuntime(t *testing.T) { object: objectSelector("secure", "false"), want: []string{"login_email", "login_note", "login_terms"}, }, + { + // The head subtree renders nothing, so the hierarchy dump drops it + // (buildTree in driver.go) and so does the enumeration the picker + // walks (targetElements in web-runtime.ts). A selector resolving + // into it names an element the goja host cannot see at all. + name: "tag naming the head element", + selector: "tag:head", + object: objectSelector("tag", "head"), + want: nil, + }, + { + name: "tag naming an element inside the head", + selector: "tag:title", + object: objectSelector("tag", "title"), + want: nil, + }, { // The root element answers a selector like any other: the string // form scans from the root down, and the object form used to start diff --git a/pkg/spec/src/web-runtime.ts b/pkg/spec/src/web-runtime.ts index 3966a90..0d99d5b 100644 --- a/pkg/spec/src/web-runtime.ts +++ b/pkg/spec/src/web-runtime.ts @@ -426,12 +426,22 @@ function deepQueryAll(selector: string, root: ParentNode): Element[] { // matchedElements resolves one compiled selector: the document query first, then // the states it named, which no query can express. +// +// The head subtree is dropped here the way targetElements drops it from the +// enumeration and buildTree (internal/driver/chrome/driver.go) drops it from the +// dump: it renders nothing, so a selector reaching into it names an element the +// goja host cannot see at all. document.head is absent only from the small fake +// documents the unit tests install. function matchedElements(root: ParentNode, compiled: CompiledSelector): Element[] { const { css, xpath, match } = compiled; let found: Element[] = []; if (css) found = deepQueryAll(css, root); else if (xpath) found = evaluateXPathAll(xpath, root as Node); - return match === undefined ? found : found.filter(match); + const head: Element | undefined = document.head; + return found.filter( + (element) => + !(head !== undefined && head.contains(element)) && (match === undefined || match(element)), + ); } function queryElement(