mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(chrome): focus descends into the shadow root
document.activeElement names the host, not the node focused inside it, so a Compose-for-wasm app that mounts its tree in a shadow root reported focus on div#app forever. confirmFocus could never be satisfied and every InputText step aborted the run after three tries. selectAllScript already descends the boundary; the tree builder did not.
This commit is contained in:
1 parent
0bfd194332
commit
071f9c7f52
3 files changed
+81
-1
No files matched your search
@@ -642,6 +642,13 @@ func (d *Driver) Hierarchy(ctx context.Context) (string, error) {
|
||||
', [onclick]'));
|
||||
const editableSet = new Set(deepQuery(
|
||||
'input, textarea, [contenteditable]').filter(isEditableElement));
|
||||
// Descended once for the whole dump, for the reason selectAllScript above
|
||||
// descends: document.activeElement names the shadow host, so a Compose for
|
||||
// Web app reported focus on its mount element and never on the field.
|
||||
let focusedElement = document.activeElement;
|
||||
while (focusedElement && focusedElement.shadowRoot && focusedElement.shadowRoot.activeElement) {
|
||||
focusedElement = focusedElement.shadowRoot.activeElement;
|
||||
}
|
||||
function buildTree(el, isRoot) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
// Every attribute the markup wrote, keyed as written, which is what attrs
|
||||
@@ -704,7 +711,7 @@ func (d *Driver) Hierarchy(ctx context.Context) (string, error) {
|
||||
children: children,
|
||||
clickable: isClickable || null,
|
||||
enabled: isEnabled(el) || null,
|
||||
focused: document.activeElement === el || null,
|
||||
focused: focusedElement === el || null,
|
||||
// A component keeps what it likes in these two properties, so what is
|
||||
// emitted is the flag the field declares and not the property's value.
|
||||
checked: el.checked === true || null,
|
||||
|
||||
@@ -191,6 +191,62 @@ func TestElementState_ReportsTheOtherDocumentedBooleans(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
// Focus belongs to the node the user is typing into, not to the element the
|
||||
// shadow tree is mounted on.
|
||||
//
|
||||
// document.activeElement stops at a shadow boundary and names the host, so a
|
||||
// Compose for Web app reports focus on its mount element forever. confirmFocus
|
||||
// in internal/runner/runner.go re-reads the dump after a focus tap and refuses
|
||||
// to type when the field it tapped is not the one holding focus, so every
|
||||
// InputText step on such an app failed and the run aborted.
|
||||
func TestElementState_FocusDescendsIntoTheShadowRoot(t *testing.T) {
|
||||
server := httptest.NewServer(http.FileServer(http.Dir("testdata")))
|
||||
defer server.Close()
|
||||
|
||||
d := New()
|
||||
defer d.Terminate(context.Background())
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
if err := d.Launch(ctx, server.URL+"/shadow-focus.html", false, nil); err != nil {
|
||||
t.Fatalf("Launch: %v", err)
|
||||
}
|
||||
|
||||
field := elementInHierarchyDump(ctx, t, d, "id:shadow-field")
|
||||
x, y := field.Bounds.Center()
|
||||
if err := d.Tap(ctx, x, y); err != nil {
|
||||
t.Fatalf("Tap the field: %v", err)
|
||||
}
|
||||
|
||||
if tapped := elementInHierarchyDump(ctx, t, d, "id:shadow-field"); !tapped.Focused {
|
||||
t.Error("the field inside the shadow root reports no focus after being tapped")
|
||||
}
|
||||
if host := elementInHierarchyDump(ctx, t, d, "id:app"); host.Focused {
|
||||
t.Error("the shadow host reports focus, so the text would land there")
|
||||
}
|
||||
}
|
||||
|
||||
func elementInHierarchyDump(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
d *Driver,
|
||||
selector string,
|
||||
) *hierarchy.Element {
|
||||
t.Helper()
|
||||
dump, err := d.Hierarchy(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Hierarchy: %v", err)
|
||||
}
|
||||
tree, err := hierarchy.Parse(dump)
|
||||
if err != nil {
|
||||
t.Fatalf("parse hierarchy: %v", err)
|
||||
}
|
||||
element := tree.Find(selector)
|
||||
if element == nil {
|
||||
t.Fatalf("the hierarchy dump holds no element matching %q", selector)
|
||||
}
|
||||
return element
|
||||
}
|
||||
|
||||
func requireBoolean(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html id="page">
|
||||
<head id="page-head">
|
||||
<meta id="page-charset" charset="utf-8" />
|
||||
<title id="page-title">shadow focus</title>
|
||||
</head>
|
||||
<body id="page-body">
|
||||
<!-- Shaped like Compose for Web: the app mounts its accessibility tree
|
||||
inside a shadow root on this element, so document.activeElement answers
|
||||
with the host for every field the user is typing into. -->
|
||||
<div id="app"></div>
|
||||
<script id="page-script">
|
||||
const root = document.getElementById("app").attachShadow({ mode: "open" });
|
||||
root.innerHTML = `<input id="shadow-field" type="text" />`;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user