fix(selectors): tag names the whole tag, not a substring of it

matchSelectorKind had no case for tag, so it fell through to the raw
attribute path and matched by substring. web-runtime.ts compiles tag to a
CSS type selector, so tag:li resolved to <todo-list> on the Go side and to
nothing on the web side.
This commit is contained in:
pj committed 2026-08-17 23:24:06 +05:30
1 parent 81fe5f66f2
commit 9012ffa304
3 files changed
+74 -1

No files matched your search

+8
View File
@@ -10,6 +10,7 @@
// text:<value> - substring on text attribute, innermost match only
// desc:<value> - substring on content-desc / accessibilityText
// descPrefix:<prefix> - starts-with on content-desc / accessibilityText
// tag:<value> - exact match on the element's tag name (web)
//
// Object selectors (multi-attribute AND, element-scoped or global):
// { attr: value, ... } - all key/value pairs must match, each key resolved by
@@ -394,6 +395,13 @@ func matchSelectorKind(element *Element, kind, value string) (bool, bool) {
strings.HasPrefix(element.Description, value+", "), true
case "descPrefix":
return strings.HasPrefix(element.Description, value), true
case "tag":
// Both DOM resolvers compile this to a CSS type selector, which is the
// whole tag name. A substring rule here made {tag: "li"} name
// <todo-list> and {tag: "a"} name <todo-app>, so a selector meant for a
// row resolved to the container holding it.
tag, ok := element.Attributes["tag"]
return ok && tag == value, true
default:
return false, false
}
+64
View File
@@ -1551,3 +1551,67 @@ func TestSelectorFormsResolveTheSameElement(t *testing.T) {
})
}
}
// customElementDump is the shape a page built from custom elements reports: a
// container's tag name contains the tag name of what it holds, so "todo-list"
// carries "li" and "todo-app" carries "a".
const customElementDump = `{
"attributes": {"tag": "todo-app", "resource-id": "app", "bounds": "[0,0,800,600]"},
"children": [
{
"attributes": {"tag": "todo-list", "resource-id": "list", "bounds": "[0,0,800,400]"},
"children": [
{"attributes": {"tag": "li", "resource-id": "todo_1", "bounds": "[0,0,800,50]"}, "children": []},
{"attributes": {"tag": "li", "resource-id": "todo_2", "bounds": "[0,50,800,100]"}, "children": []}
]
},
{
"attributes": {"tag": "a", "resource-id": "filter_all", "bounds": "[0,400,800,450]"},
"children": []
}
]
}`
func TestTagNamesTheWholeTagNotASubstringOfIt(t *testing.T) {
tree, _ := Parse(customElementDump)
for _, test := range []struct {
value string
want []string
}{
{"li", []string{"todo_1", "todo_2"}},
{"a", []string{"filter_all"}},
} {
t.Run(test.value, func(t *testing.T) {
sel := Selector{Filters: []AttrFilter{{Attr: "tag", Value: test.value}}}
if got := resourceIDsOf(tree.FindAllBySelector(sel)); !slices.Equal(got, test.want) {
t.Errorf("{tag: %q} matched %v, want %v", test.value, got, test.want)
}
stringForm := "tag:" + test.value
if got := resourceIDsOf(tree.FindAllNodes(stringForm)); !slices.Equal(got, test.want) {
t.Errorf("%q matched %v, want %v", stringForm, got, test.want)
}
})
}
}
func TestTagMatchesTheElementItNames(t *testing.T) {
tree, _ := Parse(customElementDump)
for _, test := range []struct {
value string
want []string
}{
{"todo-app", []string{"app"}},
{"todo-list", []string{"list"}},
} {
t.Run(test.value, func(t *testing.T) {
sel := Selector{Filters: []AttrFilter{{Attr: "tag", Value: test.value}}}
if got := resourceIDsOf(tree.FindAllBySelector(sel)); !slices.Equal(got, test.want) {
t.Errorf("{tag: %q} matched %v, want %v", test.value, got, test.want)
}
stringForm := "tag:" + test.value
if got := resourceIDsOf(tree.FindAllNodes(stringForm)); !slices.Equal(got, test.want) {
t.Errorf("%q matched %v, want %v", stringForm, got, test.want)
}
})
}
}