diff --git a/pkg/spec/src/web-runtime.ts b/pkg/spec/src/web-runtime.ts
index ae3356a..40e18b8 100644
--- a/pkg/spec/src/web-runtime.ts
+++ b/pkg/spec/src/web-runtime.ts
@@ -514,6 +514,21 @@ function pointOf(element: Element): Candidate {
};
}
+// HEAD_SELECTOR is the one subtree the enumeration leaves out. It never renders,
+// so no verb can reach it, and the hierarchy dump the goja host reads
+// (internal/driver/chrome/driver.go) drops it as well. Enumerating it here would
+// put the two hosts on different element sets for every page that has a
.
+const HEAD_SELECTOR = "head, head *";
+
+// targetElements is the walk the target list is built from: the document in
+// pre-order, minus the head subtree.
+function targetElements(): HTMLElement[] {
+ const inHead = new Set(Array.from(document.querySelectorAll(HEAD_SELECTOR)));
+ return Array.from(document.querySelectorAll("*")).filter(
+ (element) => !inHead.has(element),
+ );
+}
+
// collectTargets walks the document ONCE and reports every element with the facts
// the shared eligibility rule reads. The tappable/editable membership sets are
// resolved by selector first so the DOM's answer to "clickable" and "editable"
@@ -525,7 +540,7 @@ function collectTargets(): TargetElement[] {
isEditableElement,
),
);
- return Array.from(document.querySelectorAll("*")).map((element) => ({
+ return targetElements().map((element) => ({
...pointOf(element),
clickable: clickable.has(element),
enabled: !(element as HTMLButtonElement).disabled,
@@ -577,6 +592,7 @@ export const __testing__ = {
host,
seedBigInt,
collectTargets,
+ targetElements,
TAPPABLE_SELECTOR,
EDITABLE_SELECTOR,
resetTargetCache,