| 1 | //go:build browser |
| 2 | |
| 3 | package check |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "encoding/json" |
| 8 | "testing" |
| 9 | "time" |
| 10 | |
| 11 | "github.com/chromedp/cdproto/emulation" |
| 12 | "github.com/chromedp/cdproto/input" |
| 13 | "github.com/chromedp/chromedp" |
| 14 | "github.com/chromedp/chromedp/kb" |
| 15 | ) |
| 16 | |
| 17 | // selection reads the colour box's value, its selection and how many |
| 18 | // rows its list shows. |
| 19 | const selectionJS = `(() => { const i = document.getElementById("colour-combo"), l = document.getElementById(i.getAttribute("aria-controls")); |
| 20 | return JSON.stringify({Value: i.value, Start: i.selectionStart, End: i.selectionEnd, Focused: document.activeElement === i, |
| 21 | Rows: l ? [...l.querySelectorAll("[role=option]")].filter(o => o.checkVisibility()).length : -1}); })()` |
| 22 | |
| 23 | type comboSelection struct { |
| 24 | Value string |
| 25 | Start, End, Rows int |
| 26 | Focused bool |
| 27 | } |
| 28 | |
| 29 | // TestSelectFocusSelectsThePick: focusing a combobox that shows its pick |
| 30 | // selects the whole pick, so typing or Delete replaces it, by a click as |
| 31 | // much as by Tab. A click put the caret where it landed, and clearing |
| 32 | // "Red" took a double-click or a run of backspaces first. Only the |
| 33 | // focusing click does it: a second click in the focused box places the |
| 34 | // caret, so a partial edit still works, and a drag across the pick as it |
| 35 | // is focused keeps what the drag selected. Real pointer and key events, |
| 36 | // because what is fenced is the order the browser runs mousedown, focus, |
| 37 | // mouseup and click in, which a scripted focus() never goes through. In |
| 38 | // both directions of text. |
| 39 | func TestSelectFocusSelectsThePick(t *testing.T) { |
| 40 | for _, dir := range []string{"ltr", "rtl"} { |
| 41 | t.Run(dir, func(t *testing.T) { |
| 42 | rig := sizingRig(t, false, selectFilterPages(t)) |
| 43 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 44 | defer cancel() |
| 45 | mustRun(t, ctx, emulation.SetFocusEmulationEnabled(true), chromedp.EmulateViewport(1000, 900), chromedp.Navigate(rig.Origin+"/"), |
| 46 | chromedp.WaitVisible(`#colour-combo`, chromedp.ByQuery), |
| 47 | chromedp.Evaluate(`document.documentElement.dir = "`+dir+`"; document.getElementById("colour-combo").scrollIntoView({block: "center"}); true`, nil)) |
| 48 | var box struct{ L, R, Y, PadL, PadR, Gr float64 } |
| 49 | at(t, ctx, `(() => { const i = document.getElementById("colour-combo"), r = i.getBoundingClientRect(), s = getComputedStyle(i); |
| 50 | return JSON.stringify({L: r.left, R: r.right, Y: r.top + r.height / 2, PadL: parseFloat(s.paddingLeft) + parseFloat(s.borderLeftWidth), PadR: parseFloat(s.paddingRight) + parseFloat(s.borderRightWidth), |
| 51 | Gr: Object.assign(document.createElement("canvas").getContext("2d"), {font: s.font}).measureText("Gr").width}); })()`, &box) |
| 52 | // After a frame: the focusing click selects in the next one. |
| 53 | read := func() comboSelection { |
| 54 | var raw string |
| 55 | mustRun(t, ctx, chromedp.Evaluate(afterFrame, nil, awaitPromise), chromedp.Evaluate(selectionJS, &raw)) |
| 56 | var s comboSelection |
| 57 | if err := json.Unmarshal([]byte(raw), &s); err != nil { |
| 58 | t.Fatal(err) |
| 59 | } |
| 60 | return s |
| 61 | } |
| 62 | // Inside the box, clear of the lead and the chevron: a third |
| 63 | // of the way in from either end. |
| 64 | x1, x2 := box.L+(box.R-box.L)/3, box.L+2*(box.R-box.L)/3 |
| 65 | blur := func() { |
| 66 | mustRun(t, ctx, chromedp.Evaluate(`document.activeElement.blur(); document.querySelectorAll("[rst-combo] [role=listbox]").forEach(l => l.hidden = true); true`, nil)) |
| 67 | } |
| 68 | |
| 69 | mustRun(t, ctx, chromedp.MouseClickXY(x1, box.Y)) |
| 70 | if s := read(); !s.Focused || s.Value != "Red" || s.Start != 0 || s.End != len(s.Value) { |
| 71 | t.Errorf("a click into the box showing %q left the selection at %d–%d (focused %v); want the whole pick, 0–%d", s.Value, s.Start, s.End, s.Focused, len(s.Value)) |
| 72 | } |
| 73 | mustRun(t, ctx, chromedp.MouseClickXY(x2, box.Y)) |
| 74 | if s := read(); s.Start != s.End { |
| 75 | t.Errorf("a second click in the focused box left %d–%d selected; it should place the caret", s.Start, s.End) |
| 76 | } |
| 77 | |
| 78 | blur() |
| 79 | mustRun(t, ctx, chromedp.Focus(`#dept-combo`, chromedp.ByQuery), chromedp.KeyEvent(kb.Tab)) |
| 80 | if s := read(); !s.Focused || s.Start != 0 || s.End != len(s.Value) { |
| 81 | t.Errorf("Tab into the box left %d–%d selected (focused %v); want the whole pick", s.Start, s.End, s.Focused) |
| 82 | } |
| 83 | mustRun(t, ctx, chromedp.KeyEvent("gr")) |
| 84 | if s := read(); s.Value != "gr" || s.Rows != 1 { |
| 85 | t.Errorf("typing over the selected pick left %q with %d rows showing; want \"gr\", filtered to Green", s.Value, s.Rows) |
| 86 | } |
| 87 | |
| 88 | // A drag across "Green" as it focuses the box: from just past |
| 89 | // its "Gr" to well past its end, which selects "een". |
| 90 | // Whatever the drag selected stays. |
| 91 | blur() |
| 92 | mustRun(t, ctx, chromedp.Evaluate(`document.getElementById("colour").value = "green"; document.getElementById("colour").dispatchEvent(new Event("change")); true`, nil)) |
| 93 | from, to := box.L+box.PadL+box.Gr+1, x2 |
| 94 | if dir == "rtl" { |
| 95 | from, to = box.R-box.PadR-box.Gr-1, x1 |
| 96 | } |
| 97 | mustRun(t, ctx, chromedp.ActionFunc(func(ctx context.Context) error { |
| 98 | if err := input.DispatchMouseEvent(input.MousePressed, from, box.Y).WithButton(input.Left).WithClickCount(1).Do(ctx); err != nil { |
| 99 | return err |
| 100 | } |
| 101 | if err := input.DispatchMouseEvent(input.MouseMoved, to, box.Y).WithButton(input.Left).Do(ctx); err != nil { |
| 102 | return err |
| 103 | } |
| 104 | return input.DispatchMouseEvent(input.MouseReleased, to, box.Y).WithButton(input.Left).WithClickCount(1).Do(ctx) |
| 105 | })) |
| 106 | if s := read(); !s.Focused || s.Start == 0 && s.End == len(s.Value) { |
| 107 | t.Errorf("a drag as the box was focused ended with %d–%d of %q selected (focused %v); the drag's own selection should stay", s.Start, s.End, s.Value, s.Focused) |
| 108 | } |
| 109 | |
| 110 | // A tap selects the pick too: a tap selects on focus and a |
| 111 | // mouse on its click, so each path is read. Not a second tap: |
| 112 | // with the protocol's raw touch events Chromium keeps a |
| 113 | // selection under any later tap, with or without this file's |
| 114 | // handlers, so it would fence the emulation, not select.js. |
| 115 | blur() |
| 116 | tap := func(x float64) { |
| 117 | mustRun(t, ctx, chromedp.ActionFunc(func(ctx context.Context) error { |
| 118 | pt := []*input.TouchPoint{{X: x, Y: box.Y}} |
| 119 | if err := input.DispatchTouchEvent(input.TouchStart, pt).Do(ctx); err != nil { |
| 120 | return err |
| 121 | } |
| 122 | return input.DispatchTouchEvent(input.TouchEnd, []*input.TouchPoint{}).Do(ctx) |
| 123 | }), chromedp.Evaluate(afterFrame, nil, awaitPromise)) |
| 124 | } |
| 125 | mustRun(t, ctx, emulation.SetTouchEmulationEnabled(true)) |
| 126 | tap(x1) |
| 127 | if s := read(); !s.Focused || s.Start != 0 || s.End != len(s.Value) { |
| 128 | t.Errorf("a tap into the box showing %q left %d–%d selected (focused %v); want the whole pick", s.Value, s.Start, s.End, s.Focused) |
| 129 | } |
| 130 | }) |
| 131 | } |
| 132 | } |
| 133 | |