| 1 | //go:build browser |
| 2 | |
| 3 | package check |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "fmt" |
| 8 | "net/http" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | "time" |
| 12 | |
| 13 | "github.com/chromedp/cdproto/emulation" |
| 14 | "github.com/chromedp/chromedp" |
| 15 | "github.com/chromedp/chromedp/kb" |
| 16 | |
| 17 | "amadan.net/rastrillo/fiesta/internal/harness" |
| 18 | ) |
| 19 | |
| 20 | // profilePage is one page of this file's drives as rastrillo rendered it, |
| 21 | // by the drive's name and the page's case: a layout with the drives' |
| 22 | // standard nav (Invoices, Orders, Team) and language menu |
| 23 | // (#rail-locale, English and Gaeilge), and the blocks rastrillo's |
| 24 | // profileDefs filled for the profile menu: the app's name as the |
| 25 | // brand, Grace Hopper as the person, the account's links (#acct-profile |
| 26 | // and #acct-out), and a heading with a link in the page (#main-link). |
| 27 | func profilePage(t *testing.T, test, kase string) string { |
| 28 | t.Helper() |
| 29 | return fixture(t, "profile_browser_test.go", test, kase) |
| 30 | } |
| 31 | |
| 32 | // profileShell is where each shell keeps the profile menu: the sidebar's |
| 33 | // own <details rst-shell-profile> in the rail's foot, and the console's |
| 34 | // Menu, whose summary shows the avatar on the index and whose card is |
| 35 | // the tail beside it. The two cards read the same: the name and email, |
| 36 | // the Language menu, then the account's links laid out flat. |
| 37 | type profileShell struct { |
| 38 | trigger, card, account string |
| 39 | } |
| 40 | |
| 41 | var profileShells = map[string]profileShell{ |
| 42 | "sidebar": {"[rst-shell-profile] > summary", "[rst-shell-profile] > [rst-dropdown-menu]", ""}, |
| 43 | "console": {"[rst-shell-menu] > summary", "[rst-shell-tail]", ""}, |
| 44 | } |
| 45 | |
| 46 | // profileJS reads the index's header row and the card: the heading's |
| 47 | // text, size and height, where the avatar sits against the root's box |
| 48 | // (tokens.css reserves a stable scrollbar gutter, which RTL puts on the |
| 49 | // left, so the viewport's edge is not the page's), and for each element |
| 50 | // the card should show, whether it is on screen and the topmost thing |
| 51 | // at its centre: in the card and above the page. |
| 52 | const profileJS = `((trigger, card) => { |
| 53 | const q = s => document.querySelector(s), h = document.documentElement.getBoundingClientRect(); |
| 54 | const rtl = document.documentElement.dir === "rtl", sum = q(trigger), r = sum.getBoundingClientRect(); |
| 55 | const title = q("[rst-shell-title]"), t = title.getBoundingClientRect(), foot = q("[rst-shell-rail-foot]"); |
| 56 | const nav = [...document.querySelectorAll("[rst-shell-nav] > a")].pop().getBoundingClientRect(); |
| 57 | const seen = sel => [...document.querySelectorAll(sel)].filter(el => el.checkVisibility()).map(el => { |
| 58 | const b = el.getBoundingClientRect(), hit = document.elementFromPoint(b.left + b.width / 2, b.top + b.height / 2); |
| 59 | return {Sel: sel, Text: el.textContent.trim(), On: b.width > 0 && b.top >= 0 && b.bottom <= innerHeight && b.left >= h.left && b.right <= h.right, Top: !!hit && (hit === el || el.contains(hit))}; |
| 60 | }); |
| 61 | const c = q(card), cb = c.getBoundingClientRect(); |
| 62 | return JSON.stringify({ |
| 63 | Open: sum.parentElement.open, Title: title.innerText.trim(), TitleH: Math.round(t.height), TitleSize: parseFloat(getComputedStyle(title).fontSize), |
| 64 | TitleOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth, |
| 65 | AvTop: Math.round(r.top), AvEnd: Math.round(rtl ? r.left - h.left : h.right - r.right), AvW: Math.round(r.width), AvH: Math.round(r.height), |
| 66 | FootBelowNav: !!foot && foot.getBoundingClientRect().top > nav.bottom, |
| 67 | CardShown: c.checkVisibility(), CardTop: Math.round(cb.top), AvBottom: Math.round(r.bottom), |
| 68 | Items: [].concat(seen("[rst-shell-who] [rst-person-name]"), seen("[rst-shell-who] [rst-person-email]"), |
| 69 | seen("#acct-profile"), seen("#acct-out"), seen("#rail-locale [rst-dropdown-menu] a")), |
| 70 | Focus: document.activeElement === sum, |
| 71 | }); |
| 72 | })(%q, %q)` |
| 73 | |
| 74 | type profileItem struct { |
| 75 | Sel, Text string |
| 76 | On, Top bool |
| 77 | } |
| 78 | |
| 79 | type profileReading struct { |
| 80 | Open, CardShown, FootBelowNav, Focus bool |
| 81 | Title string |
| 82 | TitleH, TitleOverflow int |
| 83 | TitleSize float64 |
| 84 | AvTop, AvEnd, AvW, AvH int |
| 85 | CardTop, AvBottom int |
| 86 | Items []profileItem |
| 87 | } |
| 88 | |
| 89 | // requireCardItems holds an open card's readings to what it must show: |
| 90 | // the name and the email, both account links and both languages, each |
| 91 | // present with its own text, on screen and the topmost thing at its |
| 92 | // centre. Asserting only the ones that were found would pass a card |
| 93 | // that had lost an item. |
| 94 | func requireCardItems(t *testing.T, where string, items []profileItem) { |
| 95 | t.Helper() |
| 96 | want := map[string]string{"[rst-shell-who] [rst-person-name]": "Grace Hopper", "[rst-shell-who] [rst-person-email]": "grace@example.com", "#acct-profile": "Profile", "#acct-out": "Sign out"} |
| 97 | got := map[string]bool{} |
| 98 | locales := map[string]bool{} |
| 99 | for _, it := range items { |
| 100 | if !it.On || !it.Top { |
| 101 | t.Errorf("%s: %s (%q) in the open card: on screen %v, topmost %v; want it on screen and above the page", where, it.Sel, it.Text, it.On, it.Top) |
| 102 | } |
| 103 | if w, ok := want[it.Sel]; ok && it.Text != w { |
| 104 | t.Errorf("%s: %s reads %q, want %q", where, it.Sel, it.Text, w) |
| 105 | } |
| 106 | got[it.Sel] = true |
| 107 | if it.Sel == "#rail-locale [rst-dropdown-menu] a" { |
| 108 | locales[it.Text] = true |
| 109 | } |
| 110 | } |
| 111 | for sel := range want { |
| 112 | if !got[sel] { |
| 113 | t.Errorf("%s: the open card never showed %s", where, sel) |
| 114 | } |
| 115 | } |
| 116 | if !locales["English"] || !locales["Gaeilge"] { |
| 117 | t.Errorf("%s: the card showed the languages %v, want English and Gaeilge", where, locales) |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | func readProfile(t *testing.T, ctx context.Context, p profileShell) profileReading { |
| 122 | t.Helper() |
| 123 | var r profileReading |
| 124 | at(t, ctx, fmt.Sprintf(profileJS, p.trigger, p.card), &r) |
| 125 | return r |
| 126 | } |
| 127 | |
| 128 | // TestTheProfileMenuOnThePhoneIndex is the phone index of both shells, |
| 129 | // LTR and RTL, scripts on and off, at 390 on a coarse pointer. The |
| 130 | // heading is the app's name, about 21px, on one line however long the |
| 131 | // name; the avatar is a 44px button at the top inline-end, named with |
| 132 | // the person's name; nothing is left at the foot. Opened, the card |
| 133 | // shows the name, the email, the account's links and every language, |
| 134 | // each on screen and above the page. An outside tap closes it without |
| 135 | // reaching the page, with and without scripts; with scripts Escape |
| 136 | // closes it and hands focus back to the avatar. |
| 137 | func TestTheProfileMenuOnThePhoneIndex(t *testing.T) { |
| 138 | const long = "Harbour Freight Forwarding and Customs Brokerage" |
| 139 | for _, shell := range []string{"sidebar", "console"} { |
| 140 | for _, dir := range []string{"ltr", "rtl"} { |
| 141 | for _, scripts := range []bool{true, false} { |
| 142 | shell, dir, scripts := shell, dir, scripts |
| 143 | t.Run(fmt.Sprintf("%s %s scripts %v", shell, dir, scripts), func(t *testing.T) { |
| 144 | p := profileShells[shell] |
| 145 | // Both indexes: the brand Harbour, and the long brand. |
| 146 | pages := map[string]string{ |
| 147 | "/": profilePage(t, "TestTheProfileMenuOnThePhoneIndex", shell+"-"+dir+"-index"), |
| 148 | "/long": profilePage(t, "TestTheProfileMenuOnThePhoneIndex", shell+"-"+dir+"-long"), |
| 149 | "/go/": sizingDoc("landed", `<p id="landed">landed</p>`), |
| 150 | } |
| 151 | rig := harness.New(t, func(string) http.Handler { return shellAssets(t, pages) }, harness.WithCoarsePointer()) |
| 152 | ctx, cancel := context.WithTimeout(rig.Context(), 120*time.Second) |
| 153 | defer cancel() |
| 154 | acts := []chromedp.Action{chromedp.EmulateViewport(390, 844)} |
| 155 | if !scripts { |
| 156 | acts = append(acts, emulation.SetScriptExecutionDisabled(true)) |
| 157 | } |
| 158 | mustRun(t, ctx, append(acts, chromedp.Navigate(rig.Origin+"/long"), chromedp.WaitVisible(p.trigger, chromedp.ByQuery))...) |
| 159 | requirePointer(t, ctx, true) |
| 160 | lr := readProfile(t, ctx, p) |
| 161 | if lr.Title != long || lr.TitleH > 56 || lr.TitleOverflow > 0 { |
| 162 | t.Errorf("a long brand: heading %q, %dpx tall, the page %dpx wider than the screen; want the whole name, on one line, eliding inside the screen", lr.Title, lr.TitleH, lr.TitleOverflow) |
| 163 | } |
| 164 | |
| 165 | mustRun(t, ctx, chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible(p.trigger, chromedp.ByQuery)) |
| 166 | settled(t, ctx) |
| 167 | closed := readProfile(t, ctx, p) |
| 168 | if closed.Title != "Harbour" || closed.TitleH > 56 || closed.TitleSize < 19 || closed.TitleSize > 23 { |
| 169 | t.Errorf("the heading is %q at %.1fpx, %dpx tall; want the brand, Harbour, at about 21px on one line in the 56px header row", closed.Title, closed.TitleSize, closed.TitleH) |
| 170 | } |
| 171 | if closed.AvTop > 32 || closed.AvEnd > 24 || closed.AvW < 44 || closed.AvH < 44 { |
| 172 | t.Errorf("the avatar is %dx%dpx, %dpx from the top and %dpx from the inline end; want a 44px tap at the top inline-end", closed.AvW, closed.AvH, closed.AvTop, closed.AvEnd) |
| 173 | } |
| 174 | if closed.FootBelowNav { |
| 175 | t.Error("the rail's foot is still under the nav on the phone index") |
| 176 | } |
| 177 | if closed.Open || closed.CardShown { |
| 178 | t.Error("the card is open before anyone opened it") |
| 179 | } |
| 180 | if name := axName(ctx, t, p.trigger); name != "Grace Hopper" { |
| 181 | t.Errorf("the avatar's accessible name is %q, want the person's name", name) |
| 182 | } |
| 183 | |
| 184 | // Opened, both shells' cards read in one order with the |
| 185 | // language closed: the name, the email, the Language menu, |
| 186 | // then the account's links, flat, with no Account menu to |
| 187 | // open first. |
| 188 | mustRun(t, ctx, chromedp.Click(p.trigger, chromedp.ByQuery), chromedp.WaitVisible(p.card+" [rst-shell-who]", chromedp.ByQuery)) |
| 189 | var order []string |
| 190 | at(t, ctx, `JSON.stringify([...document.querySelectorAll("[rst-shell-who] [rst-person-name], [rst-shell-who] [rst-person-email], #rail-locale > summary, #acct-profile, #acct-out")] |
| 191 | .filter(e => e.checkVisibility()).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top).map(e => e.id || e.getAttribute("rst-person-name") !== null && "name" || e.getAttribute("rst-person-email") !== null && "email" || "language"))`, &order) |
| 192 | if got := strings.Join(order, " "); got != "name email language acct-profile acct-out" { |
| 193 | t.Errorf("the open card reads %q, top to bottom; want the name, the email, the language, then the account's links, flat", got) |
| 194 | } |
| 195 | var items []profileItem |
| 196 | if p.account != "" { |
| 197 | mustRun(t, ctx, chromedp.Click(p.account, chromedp.ByQuery), chromedp.WaitVisible("#acct-out", chromedp.ByQuery)) |
| 198 | items = append(items, readProfile(t, ctx, p).Items...) |
| 199 | } |
| 200 | mustRun(t, ctx, chromedp.Click("#rail-locale > summary", chromedp.ByQuery), chromedp.WaitVisible("#rail-locale [rst-dropdown-menu] a", chromedp.ByQuery)) |
| 201 | open := readProfile(t, ctx, p) |
| 202 | items = append(items, open.Items...) |
| 203 | if !open.Open || !open.CardShown || open.CardTop < open.AvBottom { |
| 204 | t.Errorf("opened: open %v, card shown %v, card top %d under an avatar ending at %d; want a card dropping from the avatar", open.Open, open.CardShown, open.CardTop, open.AvBottom) |
| 205 | } |
| 206 | if open.AvW < 44 || open.AvH < 44 { |
| 207 | t.Errorf("the open avatar is %dx%dpx; the open state must stay a 44px tap", open.AvW, open.AvH) |
| 208 | } |
| 209 | requireCardItems(t, "the phone index", items) |
| 210 | |
| 211 | // An outside tap: on a nav row, clear of the card at the |
| 212 | // row's inline start. It closes the card and the page |
| 213 | // under it never sees it. |
| 214 | fx := 0.03 |
| 215 | if dir == "rtl" { |
| 216 | fx = 0.97 |
| 217 | } |
| 218 | clickAndStay(t, ctx, probeScripted(t, ctx, scripts, "#nav-team", fx, 0.5, 0, 0), "", |
| 219 | fmt.Sprintf(`!document.querySelector(%q).parentElement.open`, p.trigger)) |
| 220 | |
| 221 | if scripts { |
| 222 | mustRun(t, ctx, chromedp.Click(p.trigger, chromedp.ByQuery), chromedp.WaitVisible(p.card+" [rst-shell-who]", chromedp.ByQuery)) |
| 223 | focus := "#acct-profile" |
| 224 | if p.account != "" { |
| 225 | mustRun(t, ctx, chromedp.Click(p.account, chromedp.ByQuery), chromedp.WaitVisible(focus, chromedp.ByQuery)) |
| 226 | } |
| 227 | mustRun(t, ctx, chromedp.Focus(focus, chromedp.ByQuery), chromedp.KeyEvent(kb.Escape)) |
| 228 | if after := readProfile(t, ctx, p); after.Open || !after.Focus { |
| 229 | t.Errorf("Escape from a link in the card: open %v, focus on the avatar %v; want it closed and focus handed back", after.Open, after.Focus) |
| 230 | } |
| 231 | } else { |
| 232 | // Scripts off, the avatar's own toggle is all there is. |
| 233 | mustRun(t, ctx, chromedp.Click(p.trigger, chromedp.ByQuery), chromedp.WaitVisible(p.card+" [rst-shell-who]", chromedp.ByQuery), |
| 234 | chromedp.Click(p.trigger, chromedp.ByQuery)) |
| 235 | settleUntil(t, ctx, fmt.Sprintf(`!document.querySelector(%q).parentElement.open`, p.trigger)) |
| 236 | } |
| 237 | }) |
| 238 | } |
| 239 | } |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | // TestTheProfileMenuIsADropUpAtTheRailsFoot is the sidebar on a |
| 244 | // desktop, where the profile menu is the rail's foot: an avatar at the |
| 245 | // bottom of the rail at its inline start, opening a card UPWARD that is |
| 246 | // anchored at the avatar's inline start and grows across the page. It |
| 247 | // has to stay on screen in a 1280x720 window and a 1024x600 one, with |
| 248 | // the language menu open inside it; close on an outside tap, scripts on |
| 249 | // or off, and on Escape; and mirror in RTL, which the inline-start |
| 250 | // readings cover by being measured from the inline start of each |
| 251 | // direction. |
| 252 | func TestTheProfileMenuIsADropUpAtTheRailsFoot(t *testing.T) { |
| 253 | p := profileShells["sidebar"] |
| 254 | // 1280x420 is a laptop with devtools open: the shortest window the |
| 255 | // rail still sits beside the page in. Anchored, the card cannot |
| 256 | // flip downward at the rail's foot: the foot is at the bottom of a |
| 257 | // rail the window's height, so there is never more room below than |
| 258 | // above, and the card's height cap shrinks with the window. What the |
| 259 | // short window has to prove is that the card still fits above. |
| 260 | for _, vp := range [][2]int64{{1280, 720}, {1024, 600}, {1280, 420}} { |
| 261 | for _, dir := range []string{"ltr", "rtl"} { |
| 262 | for _, scripts := range []bool{true, false} { |
| 263 | for _, anchored := range []bool{true, false} { |
| 264 | vp, dir, scripts, anchored := vp, dir, scripts, anchored |
| 265 | t.Run(fmt.Sprintf("%dx%d %s scripts %v anchored %v", vp[0], vp[1], dir, scripts, anchored), func(t *testing.T) { |
| 266 | pages := map[string]string{"/": profilePage(t, "TestTheProfileMenuIsADropUpAtTheRailsFoot", dir), "/go/": sizingDoc("landed", `<p id="landed">landed</p>`)} |
| 267 | rig := harness.New(t, func(string) http.Handler { return shellAssets(t, pages) }) |
| 268 | ctx, cancel := context.WithTimeout(rig.Context(), 90*time.Second) |
| 269 | defer cancel() |
| 270 | acts := []chromedp.Action{chromedp.EmulateViewport(vp[0], vp[1])} |
| 271 | if !scripts { |
| 272 | acts = append(acts, emulation.SetScriptExecutionDisabled(true)) |
| 273 | } |
| 274 | mustRun(t, ctx, append(acts, chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible(p.trigger, chromedp.ByQuery))...) |
| 275 | settled(t, ctx) |
| 276 | // Without anchor positioning, the path every engine |
| 277 | // without it takes: the card absolute in the rail, |
| 278 | // opened upward by the hand-written insets. |
| 279 | if !anchored { |
| 280 | mustRun(t, ctx, chromedp.Evaluate(WithoutAnchorPositioning, nil, awaitPromise)) |
| 281 | } |
| 282 | RequireAnchorPositioning(t, ctx, "the drop-up", "[rst-shell-profile] > [rst-dropdown-menu]", anchored) |
| 283 | const geom = `(() => { |
| 284 | const q = s => document.querySelector(s), rtl = document.documentElement.dir === "rtl"; |
| 285 | const rail = q("[rst-shell-rail]").getBoundingClientRect(), s = q("[rst-shell-profile] > summary").getBoundingClientRect(); |
| 286 | const c = q("[rst-shell-profile] > [rst-dropdown-menu]"), cb = c.getBoundingClientRect(); |
| 287 | const start = b => rtl ? rail.right - b.right : b.left - rail.left; |
| 288 | return JSON.stringify({FootGap: Math.round(rail.bottom - s.bottom), AvStart: Math.round(start(s)), |
| 289 | Shown: c.checkVisibility(), CardStart: Math.round(start(cb)), CardBottom: Math.round(cb.bottom), AvTop: Math.round(s.top), |
| 290 | OnScreen: cb.top >= 0 && cb.bottom <= innerHeight && cb.left >= 0 && cb.right <= document.documentElement.clientWidth}); |
| 291 | })()` |
| 292 | var g struct { |
| 293 | FootGap, AvStart, CardStart, CardBottom, AvTop int |
| 294 | Shown, OnScreen bool |
| 295 | } |
| 296 | at(t, ctx, geom, &g) |
| 297 | if g.FootGap > 40 || g.AvStart > 24 || g.Shown { |
| 298 | t.Errorf("closed: the avatar is %dpx above the rail's foot and %dpx from its inline start, card shown %v; want it at the foot's inline start, closed", g.FootGap, g.AvStart, g.Shown) |
| 299 | } |
| 300 | if name := axName(ctx, t, p.trigger); name != "Grace Hopper" { |
| 301 | t.Errorf("the avatar's accessible name is %q, want the person's name", name) |
| 302 | } |
| 303 | mustRun(t, ctx, chromedp.Click(p.trigger, chromedp.ByQuery), chromedp.WaitVisible(p.card+" [rst-shell-who]", chromedp.ByQuery), |
| 304 | chromedp.Click("#rail-locale > summary", chromedp.ByQuery), chromedp.WaitVisible("#rail-locale [rst-dropdown-menu] a", chromedp.ByQuery)) |
| 305 | at(t, ctx, geom, &g) |
| 306 | if !g.Shown || g.CardBottom > g.AvTop || !g.OnScreen { |
| 307 | t.Errorf("open: card shown %v, its bottom at %d over an avatar whose top is %d, on screen %v; want a drop-up wholly in the window", g.Shown, g.CardBottom, g.AvTop, g.OnScreen) |
| 308 | } |
| 309 | if d := g.CardStart - g.AvStart; d < -2 || d > 2 { |
| 310 | t.Errorf("the card starts %dpx from the rail's inline start and the avatar %dpx; want it anchored at the avatar's inline start", g.CardStart, g.AvStart) |
| 311 | } |
| 312 | requireCardItems(t, "the drop-up", readProfile(t, ctx, p).Items) |
| 313 | clickAndStay(t, ctx, probeScripted(t, ctx, scripts, "#main-link", 0.5, 0.5, 0, 0), "", |
| 314 | `!document.querySelector("[rst-shell-profile]").open`) |
| 315 | if scripts { |
| 316 | mustRun(t, ctx, chromedp.Click(p.trigger, chromedp.ByQuery), chromedp.WaitVisible("#acct-profile", chromedp.ByQuery), |
| 317 | chromedp.Focus("#acct-profile", chromedp.ByQuery), chromedp.KeyEvent(kb.Escape)) |
| 318 | if after := readProfile(t, ctx, p); after.Open || !after.Focus { |
| 319 | t.Errorf("Escape from a link in the card: open %v, focus on the avatar %v", after.Open, after.Focus) |
| 320 | } |
| 321 | } |
| 322 | }) |
| 323 | } |
| 324 | } |
| 325 | } |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | // TestThePersonNeverVanishesFromTheRail: an app with a profile and |
| 330 | // nothing else, no language and no account links, still shows the |
| 331 | // signed-in person's avatar, on a desktop at the rail's foot and on the |
| 332 | // phone index at the top; the card then holds the name and email. Only |
| 333 | // the shell's placeholder, an app with no profile at all and nothing to |
| 334 | // choose, leaves the foot out — and "nothing to choose" now means the |
| 335 | // app emptied the scheme block too, since the shipped shell always |
| 336 | // offers the colour scheme and so always has a card worth opening. |
| 337 | func TestThePersonNeverVanishesFromTheRail(t *testing.T) { |
| 338 | // Three sidebar pages: the person with no language menu (a content |
| 339 | // page, then the index), and the placeholder with no language menu |
| 340 | // and no scheme menu either. |
| 341 | pages := map[string]string{ |
| 342 | "/person": profilePage(t, "TestThePersonNeverVanishesFromTheRail", "person"), |
| 343 | "/person-idx": profilePage(t, "TestThePersonNeverVanishesFromTheRail", "person-idx"), |
| 344 | "/placeholder": profilePage(t, "TestThePersonNeverVanishesFromTheRail", "placeholder"), |
| 345 | } |
| 346 | rig := harness.New(t, func(string) http.Handler { return shellAssets(t, pages) }) |
| 347 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 348 | defer cancel() |
| 349 | shown := `(() => { const s = document.querySelector("[rst-shell-profile] > summary"); return JSON.stringify({Avatar: !!s && s.checkVisibility()}); })()` |
| 350 | for _, c := range []struct { |
| 351 | path string |
| 352 | w, h int64 |
| 353 | want bool |
| 354 | }{{"/person", 1280, 720, true}, {"/person-idx", 390, 844, true}, {"/placeholder", 1280, 720, false}} { |
| 355 | mustRun(t, ctx, chromedp.EmulateViewport(c.w, c.h), chromedp.Navigate(rig.Origin+c.path), chromedp.WaitReady("body")) |
| 356 | var got struct{ Avatar bool } |
| 357 | at(t, ctx, shown, &got) |
| 358 | if got.Avatar != c.want { |
| 359 | t.Errorf("%s at %d: avatar shown %v, want %v", c.path, c.w, got.Avatar, c.want) |
| 360 | } |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | // TestABlankBrandFallsBackToTheTitle: a brand drawn from data that has |
| 365 | // none renders only whitespace, and the phone index's heading is then |
| 366 | // the page's title rather than nothing. |
| 367 | func TestABlankBrandFallsBackToTheTitle(t *testing.T) { |
| 368 | // The sidebar index with the title Harbour Ledger and a brand block |
| 369 | // that renders only whitespace. |
| 370 | pages := map[string]string{"/": profilePage(t, "TestABlankBrandFallsBackToTheTitle", "")} |
| 371 | rig := harness.New(t, func(string) http.Handler { return shellAssets(t, pages) }) |
| 372 | ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second) |
| 373 | defer cancel() |
| 374 | mustRun(t, ctx, chromedp.EmulateViewport(390, 844), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitReady("body")) |
| 375 | var v viewReading |
| 376 | at(t, ctx, viewJS, &v) |
| 377 | if v.Title != "Harbour Ledger" || v.H1s != 1 { |
| 378 | t.Errorf("a whitespace brand: the index heading reads %q (%d h1s); want the title, Harbour Ledger", v.Title, v.H1s) |
| 379 | } |
| 380 | } |
| 381 | |