mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
fix(web): select the focused field inside a shadow root before typing
This commit is contained in:
1 parent
596f0e0766
commit
871a56d148
2 files changed
+94
-11
No files matched your search
@@ -256,12 +256,7 @@ func (d *Driver) InputText(callerCtx context.Context, text string) error {
|
||||
defer cancel()
|
||||
return chromedp.Run(runCtx,
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
// Select any existing content so InsertText replaces rather than appends.
|
||||
if err := chromedp.Evaluate(`
|
||||
(function() {
|
||||
const el = document.activeElement;
|
||||
if (el && typeof el.select === 'function') el.select();
|
||||
})()`, nil).Do(ctx); err != nil {
|
||||
if err := selectFocusedText(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return input.InsertText(text).Do(ctx)
|
||||
@@ -269,6 +264,28 @@ func (d *Driver) InputText(callerCtx context.Context, text string) error {
|
||||
)
|
||||
}
|
||||
|
||||
// selectAllScript selects everything in the focused field so the InsertText
|
||||
// that follows replaces rather than appends.
|
||||
//
|
||||
// document.activeElement stops at a shadow boundary: it names the HOST, not the
|
||||
// focused node inside. Compose for Web focuses a hidden <input> inside the
|
||||
// shadow root it mounts, so the host answer has no select() and the selection
|
||||
// never happened - every InputText appended to the last one, and a fuzzer that
|
||||
// types into the same field twice built up garbage it could never clear.
|
||||
// Descending activeElement through each shadow root finds the real field.
|
||||
const selectAllScript = `
|
||||
(function() {
|
||||
let el = document.activeElement;
|
||||
while (el && el.shadowRoot && el.shadowRoot.activeElement) {
|
||||
el = el.shadowRoot.activeElement;
|
||||
}
|
||||
if (el && typeof el.select === 'function') el.select();
|
||||
})()`
|
||||
|
||||
func selectFocusedText(ctx context.Context) error {
|
||||
return chromedp.Evaluate(selectAllScript, nil).Do(ctx)
|
||||
}
|
||||
|
||||
// ReplacesTextOnInput reports that InputText replaces existing content via
|
||||
// select-all, so the runner skips its pre-erase.
|
||||
func (d *Driver) ReplacesTextOnInput() bool {
|
||||
@@ -282,11 +299,7 @@ func (d *Driver) EraseText(callerCtx context.Context, _ int) error {
|
||||
defer cancel()
|
||||
return chromedp.Run(runCtx,
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
if err := chromedp.Evaluate(`
|
||||
(function() {
|
||||
const el = document.activeElement;
|
||||
if (el && typeof el.select === 'function') el.select();
|
||||
})()`, nil).Do(ctx); err != nil {
|
||||
if err := selectFocusedText(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return input.InsertText("").Do(ctx)
|
||||
|
||||
@@ -474,3 +474,73 @@ func TestLaunch_KeepsBrowserAliveAfterCallerContextEnds(t *testing.T) {
|
||||
t.Fatalf("second Launch after the first caller context ended: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInputText_ReplacesTextInsideAShadowRoot pins ReplacesTextOnInput's promise
|
||||
// on the shape a canvas app actually has. Compose for Web draws its text fields
|
||||
// on a canvas and routes typing through a hidden <input> INSIDE the shadow root
|
||||
// it mounts, and document.activeElement stops at a shadow boundary: it names the
|
||||
// host. The select-all therefore ran against a <div> with no select(), every
|
||||
// InputText appended to the last, and a fuzzer typing twice into one field built
|
||||
// up text it could never clear (observed on the folio wasm build as
|
||||
// "0.0000001" -> "0.0000001\t-1").
|
||||
func TestInputText_ReplacesTextInsideAShadowRoot(t *testing.T) {
|
||||
const page = `<body><div id="app"></div><script>
|
||||
const root = document.getElementById("app").attachShadow({mode: "open"});
|
||||
root.innerHTML = ` + "`" + `
|
||||
<style>
|
||||
#surface { position: absolute; left: 0; top: 0; }
|
||||
#a11y { position: absolute; left: 0; top: 0; pointer-events: none; }
|
||||
#proxy { position: absolute; left: -9999px; }
|
||||
</style>
|
||||
<canvas id="surface" width="300" height="200"></canvas>
|
||||
<div id="a11y"><div id="field">-</div></div>
|
||||
<input id="proxy" type="text">` + "`" + `;
|
||||
const proxy = root.getElementById("proxy");
|
||||
const field = root.getElementById("field");
|
||||
// The canvas owns the pointer (the a11y overlay is pointer-events: none)
|
||||
// and hands focus to the proxy, exactly as a canvas app does.
|
||||
root.getElementById("surface").addEventListener("click", function () { proxy.focus(); });
|
||||
proxy.addEventListener("input", function () { field.textContent = proxy.value; });
|
||||
</script></body>`
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
_, _ = w.Write([]byte(page))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
d := New()
|
||||
defer d.Terminate(context.Background())
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
if err := d.Launch(ctx, server.URL, false, nil); err != nil {
|
||||
t.Fatalf("Launch: %v", err)
|
||||
}
|
||||
if err := d.Tap(ctx, 40, 40); err != nil {
|
||||
t.Fatalf("Tap: %v", err)
|
||||
}
|
||||
if err := d.InputText(ctx, "alpha"); err != nil {
|
||||
t.Fatalf("InputText: %v", err)
|
||||
}
|
||||
if err := d.InputText(ctx, "beta"); err != nil {
|
||||
t.Fatalf("InputText: %v", err)
|
||||
}
|
||||
|
||||
var shown string
|
||||
script := `document.getElementById("app").shadowRoot.getElementById("field").textContent`
|
||||
if err := chromedp.Run(d.tabCtx, chromedp.Evaluate(script, &shown)); err != nil {
|
||||
t.Fatalf("read field: %v", err)
|
||||
}
|
||||
if shown != "beta" {
|
||||
t.Errorf("field holds %q, want %q; the second InputText appended instead of replacing", shown, "beta")
|
||||
}
|
||||
|
||||
if err := d.EraseText(ctx, len("beta")); err != nil {
|
||||
t.Fatalf("EraseText: %v", err)
|
||||
}
|
||||
if err := chromedp.Run(d.tabCtx, chromedp.Evaluate(script, &shown)); err != nil {
|
||||
t.Fatalf("read field: %v", err)
|
||||
}
|
||||
if shown != "" {
|
||||
t.Errorf("field holds %q after EraseText, want empty", shown)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user