| 1 | //go:build browser |
| 2 | |
| 3 | // The row's lift, the tint on menus, tiles and options, the shell |
| 4 | // tail's flat summaries and the toggle-block head, from rastrillo's |
| 5 | // ui/depth_rows_browser_test.go at the seed commit, on the rows |
| 6 | // fixture and the depth page rig_browser_test.go serves. |
| 7 | package check |
| 8 | |
| 9 | import ( |
| 10 | "context" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | "time" |
| 14 | |
| 15 | "github.com/chromedp/cdproto/input" |
| 16 | "github.com/chromedp/chromedp" |
| 17 | |
| 18 | "amadan.net/rastrillo/fiesta/internal/harness" |
| 19 | ) |
| 20 | |
| 21 | // The rows fixture's real ids (rows_browser_test.go): #row-a holds |
| 22 | // #link-a and the kebab #kebab-a inside its <details>; #row-n in #inert |
| 23 | // is the row with nothing to open; #panel-a is row A's open menu. |
| 24 | const ( |
| 25 | rowA, linkA, rowInert = "#row-a", "#link-a", "#row-n" |
| 26 | ) |
| 27 | |
| 28 | // TestARowLiftsOnHoverAndSinksOnPress (spec §3), with a mouse: |
| 29 | // an actionable row's hover changes its background to the hover tint |
| 30 | // with only inset shadows and no stacking context; pressed, or with |
| 31 | // its overlay link :active, it takes the press tint; an inert row |
| 32 | // changes nothing; a menu item takes the same tint; the media tile and |
| 33 | // the list-row-action row do too (the depth fixture). |
| 34 | func TestARowLiftsOnHoverAndSinksOnPress(t *testing.T) { |
| 35 | rig := sizingRig(t, false, rowsPages(t), harness.WithHover()) |
| 36 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 37 | defer cancel() |
| 38 | mustRun(t, ctx, chromedp.EmulateViewport(1280, 844)) |
| 39 | home(t, ctx, rig.Origin) |
| 40 | still(t, ctx) |
| 41 | rest := computed(t, ctx, rowA, "background-color") |
| 42 | forceState(t, ctx, rowA, "hover") |
| 43 | if got := computed(t, ctx, rowA, "background-color"); got != "rgb(245, 247, 253)" { |
| 44 | t.Errorf("hovered row background %s, want the hover tint rgb(245, 247, 253)", got) |
| 45 | } |
| 46 | if sh := computed(t, ctx, rowA, "box-shadow"); !strings.Contains(sh, "inset") || strings.Count(sh, "inset") != strings.Count(sh, "rgb") { |
| 47 | t.Errorf("hovered row box-shadow %q, want inset layers only", sh) |
| 48 | } |
| 49 | if pos, z := computed(t, ctx, rowA, "position"), computed(t, ctx, rowA, "z-index"); pos != "relative" || z != "auto" { |
| 50 | t.Errorf("hovered row position %s z-index %s; the lift must add no stacking context", pos, z) |
| 51 | } |
| 52 | forceState(t, ctx, rowA) |
| 53 | forceState(t, ctx, linkA, "active") |
| 54 | if got := computed(t, ctx, rowA, "background-color"); got != "rgb(236, 241, 252)" { |
| 55 | t.Errorf("row with its link :active has background %s, want the press tint rgb(236, 241, 252)", got) |
| 56 | } |
| 57 | forceState(t, ctx, linkA) |
| 58 | forceState(t, ctx, rowInert, "hover", "active") |
| 59 | if got := computed(t, ctx, rowInert, "background-color"); got != rest { |
| 60 | t.Errorf("an inert row changed on hover/press: %s -> %s", rest, got) |
| 61 | } |
| 62 | forceState(t, ctx, rowInert) |
| 63 | mustRun(t, ctx, chromedp.Evaluate(`document.getElementById("kebab-a").click(), true`, nil)) |
| 64 | forceState(t, ctx, "#panel-a a", "hover") |
| 65 | if got := computed(t, ctx, "#panel-a a", "background-color"); got != "rgb(245, 247, 253)" { |
| 66 | t.Errorf("hovered menu item background %s, want the hover tint", got) |
| 67 | } |
| 68 | forceState(t, ctx, "#panel-a a") |
| 69 | } |
| 70 | |
| 71 | // TestMenusTilesAndOptionsTakeTheTint: the depth fixture's menu link, |
| 72 | // menu button, keyboard-active option, media tile and list-row-action |
| 73 | // row take the hover tint (the option through .is-active with no |
| 74 | // pointer at all), the current menu item keeps the accent tint, and a |
| 75 | // dropdown nested in a menu keeps no button face in any state. |
| 76 | // A tile or a row is pressed through its overlay link: the press |
| 77 | // selectors read the link's :active with the container hovered, so the |
| 78 | // hover is held on the container and the active forced on the link. |
| 79 | func TestMenusTilesAndOptionsTakeTheTint(t *testing.T) { |
| 80 | rig := sizingRig(t, false, depthDoc(t), harness.WithHover()) |
| 81 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 82 | defer cancel() |
| 83 | open(t, ctx, rig, "/", false) |
| 84 | still(t, ctx) |
| 85 | mustRun(t, ctx, chromedp.Evaluate(`document.getElementById("dd").open = true, true`, nil)) |
| 86 | for _, c := range []struct{ sel, press string }{{"#menu-a", "#menu-a"}, {"#menu-btn", "#menu-btn"}, {"#tile", "#tile-link"}, {"#row-c", "#link-c"}} { |
| 87 | forceState(t, ctx, c.sel, "hover") |
| 88 | if got := computed(t, ctx, c.sel, "background-color"); got != "rgb(245, 247, 253)" { |
| 89 | t.Errorf("%s hovered is %s, want the hover tint", c.sel, got) |
| 90 | } |
| 91 | if c.press == c.sel { |
| 92 | forceState(t, ctx, c.sel, "hover", "active") |
| 93 | } else { |
| 94 | forceState(t, ctx, c.press, "active") |
| 95 | } |
| 96 | if got := computed(t, ctx, c.sel, "background-color"); got != "rgb(236, 241, 252)" { |
| 97 | t.Errorf("%s hovered and pressed is %s, want the press tint", c.sel, got) |
| 98 | } |
| 99 | forceState(t, ctx, c.sel) |
| 100 | forceState(t, ctx, c.press) |
| 101 | } |
| 102 | for _, sel := range []string{"#menu-a", "#menu-btn"} { |
| 103 | forceState(t, ctx, sel, "active") |
| 104 | if got := computed(t, ctx, sel, "background-color"); got != "rgb(236, 241, 252)" { |
| 105 | t.Errorf("%s tapped (active, no hover) is %s, want the tap tint", sel, got) |
| 106 | } |
| 107 | forceState(t, ctx, sel) |
| 108 | } |
| 109 | if got := computed(t, ctx, "#opt-active", "background-color"); got != "rgb(245, 247, 253)" { |
| 110 | t.Errorf("the keyboard-active option is %s, want the hover tint with no pointer", got) |
| 111 | } |
| 112 | if got := computed(t, ctx, "#menu-cur", "background-color"); got != "rgb(235, 241, 254)" { |
| 113 | t.Errorf("the current menu item is %s, want --rst-accent-soft", got) |
| 114 | } |
| 115 | if bg, bw, sh := computed(t, ctx, "#dd-sum", "background-color"), computed(t, ctx, "#dd-sum", "border-top-width"), computed(t, ctx, "#dd-sum", "box-shadow"); bg != "rgb(255, 255, 255)" || bw != "0px" || !strings.HasPrefix(sh, "rgb(208, 212, 220) 0px 0px 0px 1px") { |
| 116 | t.Errorf("the list-bar summary is %s, border-width %s, shadow %q; want the summary face, no border, and the edge as a 1px ring", bg, bw, sh) |
| 117 | } |
| 118 | // Day rounds the face through --rst-summary-radius; a flat theme |
| 119 | // keeps 0, and the matrix's focused #dd-sum cell holds that side. |
| 120 | if got := computed(t, ctx, "#dd-sum", "border-top-left-radius"); got != "4px" { |
| 121 | t.Errorf("the list-bar summary's corner is %s, want day's --rst-radius-sm (4px)", got) |
| 122 | } |
| 123 | for _, states := range [][]string{{}, {"hover"}, {"hover", "active"}, {"active"}} { |
| 124 | forceState(t, ctx, "#dd-nested-sum", states...) |
| 125 | if bg, bw, sh := computed(t, ctx, "#dd-nested-sum", "background-color"), computed(t, ctx, "#dd-nested-sum", "border-top-width"), computed(t, ctx, "#dd-nested-sum", "box-shadow"); bg != "rgba(0, 0, 0, 0)" || bw != "0px" || sh != "none" { |
| 126 | t.Errorf("a dropdown nested in a menu, state %v: %s, border %s, shadow %q; it is a row of the menu, not a button", states, bg, bw, sh) |
| 127 | } |
| 128 | forceState(t, ctx, "#dd-nested-sum") |
| 129 | } |
| 130 | forceState(t, ctx, "#lra", "hover") |
| 131 | if got := computed(t, ctx, "#lra", "background-color"); got != "rgb(245, 247, 253)" { |
| 132 | t.Errorf("a hovered list-row-action row is %s, want --rst-row-tint-hover", got) |
| 133 | } |
| 134 | forceState(t, ctx, "#lra") |
| 135 | for _, sel := range []string{"#link-a", "#lra-link", "#tile-link", "#dd-sum", "#menu-a", "#menu-btn", "#opt-a", "#sw"} { |
| 136 | if got := computed(t, ctx, sel, "-webkit-tap-highlight-color"); got != "rgba(0, 0, 0, 0)" { |
| 137 | t.Errorf("%s tap highlight is %s, want transparent", sel, got) |
| 138 | } |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | // TestATouchedRowShowsThePressNotAHover: on a coarse pointer there is |
| 143 | // no hover styling (the hover rules sit in (hover: hover)); a row whose |
| 144 | // link is active takes the press tint, and the kebab's tap highlight is |
| 145 | // off. |
| 146 | func TestATouchedRowShowsThePressNotAHover(t *testing.T) { |
| 147 | rig := sizingRig(t, true, rowsPages(t)) |
| 148 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 149 | defer cancel() |
| 150 | mustRun(t, ctx, chromedp.EmulateViewport(390, 844)) |
| 151 | home(t, ctx, rig.Origin) |
| 152 | requirePointer(t, ctx, true) |
| 153 | still(t, ctx) |
| 154 | rest := computed(t, ctx, rowA, "background-color") |
| 155 | forceState(t, ctx, rowA, "hover") |
| 156 | if got := computed(t, ctx, rowA, "background-color"); got != rest { |
| 157 | t.Errorf("a coarse pointer's sticky hover tinted the row %s", got) |
| 158 | } |
| 159 | forceState(t, ctx, rowA) |
| 160 | forceState(t, ctx, linkA, "active") |
| 161 | if got := computed(t, ctx, rowA, "background-color"); got != "rgb(236, 241, 252)" { |
| 162 | t.Errorf("touched row is %s, want the press tint", got) |
| 163 | } |
| 164 | forceState(t, ctx, linkA) |
| 165 | if got := computed(t, ctx, "#kebab-a", "-webkit-tap-highlight-color"); got != "rgba(0, 0, 0, 0)" { |
| 166 | t.Errorf("kebab tap highlight %s, want transparent", got) |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | // TestARowsLiftNeverMovesItsOpenMenu (review focus 1): a row menu |
| 171 | // opened on a lifted row stays where it opened while the lift eases |
| 172 | // away. The lift is asserted first, so this fails before the lift |
| 173 | // exists and is not a check of behaviour that already held. Not |
| 174 | // frozen: the easing is the thing watched, so only the inspector is |
| 175 | // enabled. |
| 176 | func TestARowsLiftNeverMovesItsOpenMenu(t *testing.T) { |
| 177 | rig := sizingRig(t, false, rowsPages(t), harness.WithHover()) |
| 178 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 179 | defer cancel() |
| 180 | mustRun(t, ctx, chromedp.EmulateViewport(1280, 844)) |
| 181 | home(t, ctx, rig.Origin) |
| 182 | inspect(t, ctx) |
| 183 | forceState(t, ctx, rowA, "hover") |
| 184 | time.Sleep(300 * time.Millisecond) |
| 185 | if got := computed(t, ctx, rowA, "background-color"); got != "rgb(245, 247, 253)" { |
| 186 | t.Fatalf("row A hovered is %s; the lift this test guards is missing", got) |
| 187 | } |
| 188 | mustRun(t, ctx, chromedp.Evaluate(`document.getElementById("kebab-a").click(), true`, nil)) |
| 189 | var before, after struct{ X, Y float64 } |
| 190 | at(t, ctx, `(() => { const r = document.getElementById("panel-a").getBoundingClientRect(); return JSON.stringify({X: r.left, Y: r.top}); })()`, &before) |
| 191 | forceState(t, ctx, rowA) |
| 192 | time.Sleep(300 * time.Millisecond) |
| 193 | at(t, ctx, `(() => { const r = document.getElementById("panel-a").getBoundingClientRect(); return JSON.stringify({X: r.left, Y: r.top}); })()`, &after) |
| 194 | if before != after { |
| 195 | t.Errorf("the open panel moved from %+v to %+v while the row's lift eased away", before, after) |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | // shellTailBody is a topbar in both spellings with its phone card open: |
| 200 | // the tail holds the nav, the account menu and the locale and scheme |
| 201 | // menus, as layouts/topbar.html renders them, and the page holds a |
| 202 | // locale menu of its own, which is the control (a dropdown standing |
| 203 | // alone keeps the face). |
| 204 | // markup-spelling: old-spelling begin — the shell tail is drawn in both spellings, so the face exclusion is held in each. |
| 205 | const shellTailBody = `<div rst-shell-topbar><header rst-shell-bar><a rst-shell-brand href="/">Home</a> |
| 206 | <details rst-shell-menu name="rst-shell-menu" open><summary>Menu</summary></details> |
| 207 | <div rst-shell-tail><nav rst-shell-nav><a href="#o">Overview</a></nav> |
| 208 | <details rst-dropdown rst-shell-account name="rst-menus"><summary>Account</summary><div rst-dropdown-menu><a href="#s">Sign out</a></div></details> |
| 209 | <details rst-dropdown rst-locale name="rst-menus"><summary id="tail-loc">Language</summary><div rst-dropdown-menu><form><button type="button" aria-current="true">English</button></form></div></details> |
| 210 | <details rst-dropdown rst-scheme name="rst-menus"><summary id="tail-sch">Theme</summary><div rst-dropdown-menu><form><button type="button">Dark</button></form></div></details> |
| 211 | </div></header> |
| 212 | <main rst-page id="main"><details rst-dropdown rst-locale name="rst-menus"><summary id="page-loc">Language</summary><div rst-dropdown-menu><form><button type="button">English</button></form></div></details></main></div> |
| 213 | <div class="rst-shell-topbar"><header class="rst-shell__bar"> |
| 214 | <details class="rst-shell__menu" name="rst-shell-menu-c" open><summary>Menu</summary></details> |
| 215 | <div class="rst-shell__tail"><details class="rst-dropdown rst-scheme" name="rst-menus"><summary id="tail-sch-c">Theme</summary><div class="rst-dropdown__menu"><form><button type="button">Dark</button></form></div></details></div> |
| 216 | </header></div>` |
| 217 | |
| 218 | // markup-spelling: old-spelling end |
| 219 | |
| 220 | // TestTheShellTailsMenusWearNoFace: in day, the |
| 221 | // locale and scheme summaries in a shell's tail stay flat, as their |
| 222 | // neighbours do, in the desktop bar at 1280 and as rows of the phone |
| 223 | // card at 390, in every state; the same menu on the page wears the |
| 224 | // summary face, so the check can see a face when there is one. Each |
| 225 | // width has its own rig, opened at that width: a second |
| 226 | // EmulateViewport loses the mouse (TestEveryNavSurfaceHasItsStates). |
| 227 | func TestTheShellTailsMenusWearNoFace(t *testing.T) { |
| 228 | for _, width := range []int64{1280, 390} { |
| 229 | rig := sizingRig(t, false, map[string]string{"/": sizingDoc("tail", shellTailBody)}, harness.WithHover()) |
| 230 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 231 | mustRun(t, ctx, chromedp.EmulateViewport(width, 844), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible("#page-loc", chromedp.ByQuery)) |
| 232 | still(t, ctx) |
| 233 | var hov bool |
| 234 | at(t, ctx, `JSON.stringify(matchMedia("(hover: hover)").matches)`, &hov) |
| 235 | if !hov { |
| 236 | t.Fatalf("%d: (hover: hover) is false; the face's hover rules cannot be read here", width) |
| 237 | } |
| 238 | if bg := computed(t, ctx, "#page-loc", "background-color"); bg != "rgb(255, 255, 255)" { |
| 239 | t.Errorf("%d: CONTROL: the page's locale summary is %s, want the summary face; the check below cannot see a face", width, bg) |
| 240 | } |
| 241 | for _, sel := range []string{"#tail-loc", "#tail-sch", "#tail-sch-c"} { |
| 242 | for _, states := range [][]string{{}, {"hover"}, {"hover", "active"}, {"active"}} { |
| 243 | forceState(t, ctx, sel, states...) |
| 244 | if bg, sh := computed(t, ctx, sel, "background-color"), computed(t, ctx, sel, "box-shadow"); bg != "rgba(0, 0, 0, 0)" || sh != "none" { |
| 245 | t.Errorf("%d: shell-tail summary %s %v is %s with box-shadow %q; want no face (rgba(0, 0, 0, 0), none)", width, sel, states, bg, sh) |
| 246 | } |
| 247 | forceState(t, ctx, sel) |
| 248 | } |
| 249 | } |
| 250 | cancel() |
| 251 | } |
| 252 | } |
| 253 | |
| 254 | // TestATBlockHeadTapsWithoutAFlash: a tap on the head's title (not the |
| 255 | // track) toggles the block, which is native <label> behaviour and |
| 256 | // already held; the new claims are that the head is the press surface |
| 257 | // (no browser tap highlight) and that the pressed head takes no tint |
| 258 | // of its own, because its switch is the feedback. The highlight check |
| 259 | // passed before the head had a hover rule: the tap-highlight rule |
| 260 | // already named the head; the background check is what fails if the |
| 261 | // head's hover tint leaves the hover query. |
| 262 | func TestATBlockHeadTapsWithoutAFlash(t *testing.T) { |
| 263 | body := `<div rst-page><div rst-tblock id="tb"><label rst-tblock-head id="head"><input type="checkbox"><span rst-switch-track aria-hidden="true"></span><span rst-tblock-title id="title">Block</span></label><div rst-tblock-body>body</div></div></div>` |
| 264 | rig := sizingRig(t, true, map[string]string{"/": sizingDoc("tblock", body)}) |
| 265 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 266 | defer cancel() |
| 267 | mustRun(t, ctx, chromedp.EmulateViewport(390, 844), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible("#head", chromedp.ByQuery)) |
| 268 | requirePointer(t, ctx, true) |
| 269 | x, y, w, h := rect(t, ctx, "#title") |
| 270 | mustRun(t, ctx, input.DispatchTouchEvent(input.TouchStart, []*input.TouchPoint{{X: x + w/2, Y: y + h/2}}), input.DispatchTouchEvent(input.TouchEnd, nil)) |
| 271 | var checked bool |
| 272 | mustRun(t, ctx, chromedp.Evaluate(`document.querySelector("#head input").checked`, &checked)) |
| 273 | if !checked { |
| 274 | t.Fatal("a tap on the title did not toggle the block") |
| 275 | } |
| 276 | if got := computed(t, ctx, "#head", "-webkit-tap-highlight-color"); got != "rgba(0, 0, 0, 0)" { |
| 277 | t.Errorf("head tap highlight %s, want transparent", got) |
| 278 | } |
| 279 | // A tap leaves the head :hover on touch, in Chromium and WebKit, so a |
| 280 | // hover tint outside (hover: hover) stuck on the tapped head. |
| 281 | if got := computed(t, ctx, "#head", "background-color"); got != "rgba(0, 0, 0, 0)" { |
| 282 | t.Errorf("head background after a tap %s, want transparent: the hover tint stuck", got) |
| 283 | } |
| 284 | } |
| 285 | |