From d177726c4e3b69eb4e16e9d730e00433b508eff7 Mon Sep 17 00:00:00 2001 From: PJ Date: Tue, 18 Aug 2026 20:43:20 +0530 Subject: [PATCH] fix(spec): keep a tag selector valid beside another key a multi-key object selector concatenates its parts into one compound, and a type selector is valid only at the head of one, so {id, tag} built '[id="amount"]input' and querySelectorAll threw. whether a spec got an exception or an element depended on the order its author wrote the keys in. --- pkg/spec/src/web-runtime.ts | 8 +++++++- pkg/spec/test/web-runtime.test.ts | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pkg/spec/src/web-runtime.ts b/pkg/spec/src/web-runtime.ts index 1cddb0a..3966a90 100644 --- a/pkg/spec/src/web-runtime.ts +++ b/pkg/spec/src/web-runtime.ts @@ -199,9 +199,15 @@ const TAG_NAME = /^[a-zA-Z][a-zA-Z0-9-]*$/; // pseudo-class like `*:hover`, a comma, whitespace) would inject CSS into the // surrounding selector. Returning a never-matching selector rather than // throwing keeps the spec running while making the typo visible in logs. +// +// Wrapped in `:is()` because a multi-key selector concatenates the parts into +// one compound, where a type selector is valid only at the head: `{id, tag}` +// built `[id="amount"]input`, which is a parse error, so querySelectorAll threw +// rather than answering with nothing, and which of the two a spec got depended +// on the order its author wrote the keys in. function tagSelector(value: string): string { if (!TAG_NAME.test(value)) return ":not(*)"; - return value; + return `:is(${value})`; } // SELECTOR_KEYS is every key an object selector may use, held identical to the diff --git a/pkg/spec/test/web-runtime.test.ts b/pkg/spec/test/web-runtime.test.ts index 7b9140d..f93a7e8 100644 --- a/pkg/spec/test/web-runtime.test.ts +++ b/pkg/spec/test/web-runtime.test.ts @@ -523,6 +523,17 @@ test("selectorFromObject composes testTag with a second key", () => { }); }); +// A type selector is valid only at the HEAD of a compound, so `tag` written +// after any other key built `[id="amount"]input` and querySelectorAll threw a +// SyntaxError: what the spec sees is an exception out of the extractor rather +// than an element, and which of the two it gets depends on the order the author +// happened to write the keys in. +test("selectorFromObject composes tag with a second key", () => { + assert.deepEqual(selectorFromObject({ id: "amount", tag: "input" }), { + css: `[id="amount"]:is(input)`, + }); +}); + // A list whose rows are named _ is only reachable by the role // half. internal/driver/chrome/translate.go builds the same CSS for the same // selector, and internal/hierarchy resolves it against the dump of this page.