rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1//go:build browser
2
3package check
4
5import (
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// topbarPage is the real topbar layout as rastrillo's drive rendered it, in
21// one direction: a nav whose current item is Posts (#nav-posts,
22// #nav-drafts, #nav-settings), an account menu (#acct-profile), a
23// language menu (#bar-locale), and a link in the page (#main-link) far
24// enough down that the card never covers it.
25func topbarPage(t *testing.T, dir string) string {
26 t.Helper()
27 return fixture(t, "card_browser_test.go", "topbarPage", dir)
28}
29
30// cardPage is one of the other pages this file's drives read, as rastrillo
31// rendered it, by the drive's name and the page's case.
32func cardPage(t *testing.T, test, kase string) string {
33 t.Helper()
34 return fixture(t, "card_browser_test.go", test, kase)
35}
36
37// cardRig serves pages with the assets the layouts link, on a touch
38// screen. scripts=false disables script execution in the engine, which
39// is the no-JavaScript half of every claim below.
40func cardRig(t *testing.T, pages map[string]string, scripts bool) (*harness.Rig, context.Context, context.CancelFunc) {
41 t.Helper()
42 pages["/go/"] = sizingDoc("landed", `<p id="landed">landed</p>`)
43 rig := harness.New(t, func(string) http.Handler { return sizingMux(t, pages) }, harness.WithCoarsePointer())
44 ctx, cancel := context.WithTimeout(rig.Context(), 180*time.Second)
45 if !scripts {
46 if err := chromedp.Run(ctx, emulation.SetScriptExecutionDisabled(true)); err != nil {
47 cancel()
48 t.Fatal(err)
49 }
50 }
51 return rig, ctx, cancel
52}
53
54// cardJS is one reading of the bar: the card's box, main's box, what is
55// open, where focus is, and the current item's marks.
56const cardJS = `(() => {
57 const q = s => document.querySelector(s);
58 const menu = q("[rst-shell-menu], .rst-shell__menu"), tail = q("[rst-shell-tail], .rst-shell__tail");
59 const acct = q("[rst-shell-account], .rst-shell__account"), main = q("main");
60 const cur = q("#nav-posts"), cs = getComputedStyle(cur), t = tail.getBoundingClientRect(), m = main.getBoundingClientRect();
61 const a = document.activeElement;
62 return JSON.stringify({MenuOpen: menu.open, AcctOpen: acct.open, TailDisplay: getComputedStyle(tail).display,
63 CardLeft: t.left, CardRight: t.right, CardTop: t.top,
64 RootLeft: document.documentElement.getBoundingClientRect().left, RootRight: document.documentElement.getBoundingClientRect().right,
65 MainBox: [m.left, m.top, m.width, m.height].map(Math.round).join(","),
66 Focus: a ? (a.id || (a.closest("[rst-shell-menu], .rst-shell__menu") ? "menu-summary" : a.closest("[rst-shell-account], .rst-shell__account") ? "account-summary" : a.tagName)) : "none",
67 FocusShown: !!a && a.getClientRects().length > 0,
68 CurBg: cs.backgroundColor, CurShadow: cs.boxShadow, CurStart: cs.borderInlineStartWidth, CurEnd: cs.borderBlockEndWidth,
69 AccentSoft: getComputedStyle(document.documentElement).getPropertyValue("--rst-accent-soft").trim(),
70 AcctPanel: getComputedStyle(acct.querySelector("[rst-dropdown-menu], .rst-dropdown__menu")).position,
71 Path: location.pathname});
72})()`
73
74type cardReading struct {
75 MenuOpen, AcctOpen, FocusShown bool
76 TailDisplay, MainBox, Focus, Path string
77 CardLeft, CardRight, CardTop float64
78 RootLeft, RootRight float64
79 CurBg, CurShadow, CurStart, CurEnd string
80 AccentSoft, AcctPanel string
81}
82
83func readCard(t *testing.T, ctx context.Context) cardReading {
84 t.Helper()
85 var c cardReading
86 at(t, ctx, cardJS, &c)
87 return c
88}
89
90// resolvedColor turns a colour custom property into the colour a
91// computed style reports, so the current item's fill can be compared.
92func resolvedColor(t *testing.T, ctx context.Context, prop string) string {
93 t.Helper()
94 var s string
95 mustRun(t, ctx, chromedp.Evaluate(`(() => { const e = document.createElement("span"); e.style.backgroundColor = "var(`+prop+`)"; document.body.appendChild(e); const c = getComputedStyle(e).backgroundColor; e.remove(); return c; })()`, &s))
96 if s == "" || s == "rgba(0, 0, 0, 0)" {
97 t.Fatalf("%s resolved to %q; a colour compared with it would be compared with nothing", prop, s)
98 }
99 return s
100}
101
102// TestTheTopbarMenuIsACardOverThePage checks the topbar's card with
103// scripts off and on, LTR and RTL, at 390 with a coarse pointer.
104func TestTheTopbarMenuIsACardOverThePage(t *testing.T) {
105 for _, dir := range []string{"ltr", "rtl"} {
106 for _, scripts := range []bool{false, true} {
107 name := dir + map[bool]string{false: ", no script", true: ", scripts on"}[scripts]
108 t.Run(name, func(t *testing.T) {
109 rig, ctx, cancel := cardRig(t, map[string]string{"/": topbarPage(t, dir)}, scripts)
110 defer cancel()
111 mustRun(t, ctx, chromedp.EmulateViewport(390, 844), chromedp.Navigate(rig.Origin+"/"),
112 chromedp.WaitVisible("[rst-shell-menu] > summary", chromedp.ByQuery))
113 requirePointer(t, ctx, true)
114 closed := readCard(t, ctx)
115 mustRun(t, ctx, chromedp.Click("[rst-shell-menu] > summary", chromedp.ByQuery))
116 open := readCard(t, ctx)
117 if !open.MenuOpen {
118 t.Fatal("the Menu did not open")
119 }
120 if open.MainBox != closed.MainBox {
121 t.Errorf("opening the card moved the page: main %s -> %s", closed.MainBox, open.MainBox)
122 }
123 // Against the root box, not clientWidth: this rig's touch
124 // Chromium reports clientWidth as the whole 390 while
125 // tokens.css's scrollbar-gutter: stable still reserves a
126 // 15px classic gutter, so the page's own inline end is
127 // 375 and clientWidth would count the gutter as gap.
128 gap := open.RootRight - open.CardRight
129 if dir == "rtl" {
130 gap = open.CardLeft - open.RootLeft
131 }
132 if gap < 0 || gap > 12+1 {
133 t.Errorf("the card's inline-end edge is %.1fpx from the viewport's, want within 0.75rem + 1px", gap)
134 }
135 // The current item wears the nav thumb: the theme's current
136 // fill and the thumb's shadow layers (transparent in a flat
137 // theme), never the rail's border marks.
138 if fill := resolvedColor(t, ctx, "--rst-nav-current-bg, var(--rst-accent-soft)"); open.CurBg != fill || open.CurStart != "0px" || open.CurEnd != "0px" {
139 t.Errorf("the current item is bg %s border-start %s border-end %s; want %s, 0, 0", open.CurBg, open.CurStart, open.CurEnd, fill)
140 }
141 // The premise of the no-script leg: fiesta.js would close
142 // the card on Escape, so a card that survives one proves no
143 // script is running, and the close below is the CSS layer's.
144 if !scripts {
145 mustRun(t, ctx, chromedp.KeyEvent(kb.Escape))
146 if !readCard(t, ctx).MenuOpen {
147 t.Fatal("Escape closed the card with scripts disabled; something is still running script, and this leg would not be the no-JavaScript case")
148 }
149 }
150 // An outside tap over a link in the page closes the card
151 // and does not follow the link. A page with no Menu also
152 // ends the wait: that is the link followed, which
153 // clickAndStay then reports as the navigation it was.
154 clickAndStay(t, ctx, probeScripted(t, ctx, scripts, "#main-link", 0.5, 0.5, 0, 0), "", `(() => { const m = document.querySelector("[rst-shell-menu]"); return !m || !m.open; })()`)
155 })
156 }
157 }
158}
159
160// TestEscapeClosesTheWholeCard: from a plain nav link in the card, and
161// from inside the account menu in it, one Escape closes everything and
162// focus lands on the Menu summary; opening the account menu keeps the
163// card open. Both spellings of the whole topbar.
164func TestEscapeClosesTheWholeCard(t *testing.T) {
165 for _, spelling := range []string{"attribute", "class"} {
166 t.Run(spelling, func(t *testing.T) {
167 page := topbarPage(t, "ltr")
168 menu, acct := "[rst-shell-menu] > summary", "[rst-shell-account] > summary"
169 if spelling == "class" {
170 page, menu, acct = classSpelling(t, page), ".rst-shell__menu > summary", ".rst-shell__account > summary"
171 if !strings.Contains(page, "rst-shell__tail") {
172 t.Fatal("the class spelling of the topbar has no rst-shell__tail; the leg would test the attribute spelling twice")
173 }
174 }
175 rig, ctx, cancel := cardRig(t, map[string]string{"/": page}, true)
176 defer cancel()
177 open := func() {
178 mustRun(t, ctx, chromedp.EmulateViewport(390, 844), chromedp.Navigate(rig.Origin+"/"),
179 chromedp.WaitVisible(menu, chromedp.ByQuery), chromedp.Click(menu, chromedp.ByQuery))
180 }
181 open()
182 mustRun(t, ctx, chromedp.Focus("#nav-drafts", chromedp.ByQuery), chromedp.KeyEvent(kb.Escape))
183 if c := readCard(t, ctx); c.MenuOpen || c.Focus != "menu-summary" {
184 t.Errorf("Escape from a nav link: open=%v focus=%s, want closed and the Menu summary", c.MenuOpen, c.Focus)
185 }
186 open()
187 mustRun(t, ctx, chromedp.Click(acct, chromedp.ByQuery))
188 if c := readCard(t, ctx); !c.MenuOpen || !c.AcctOpen {
189 t.Errorf("opening the account menu inside the card: card open=%v, account open=%v; want both", c.MenuOpen, c.AcctOpen)
190 }
191 mustRun(t, ctx, chromedp.Focus("#acct-profile", chromedp.ByQuery), chromedp.KeyEvent(kb.Escape))
192 if c := readCard(t, ctx); c.MenuOpen || c.AcctOpen || c.Focus != "menu-summary" {
193 t.Errorf("Escape from inside the account menu: card %v account %v focus %s; want both closed by one Escape and focus on the Menu summary", c.MenuOpen, c.AcctOpen, c.Focus)
194 }
195 })
196 }
197}
198
199// TestTheCardSurvivesAResize: open the card at 390 and widen to 1280,
200// and the bar is today's (the tail inline, the current item underlined)
201// and a click on a link in the page follows it while the <details> is
202// still open (no leftover layer); open the account menu in the card,
203// widen, and it is today's positioned panel, and Escape focuses its
204// visible summary; narrow again and the card is closed, because that
205// Escape closed every menu, the hidden one included.
206func TestTheCardSurvivesAResize(t *testing.T) {
207 rig, ctx, cancel := cardRig(t, map[string]string{"/": topbarPage(t, "ltr")}, true)
208 defer cancel()
209 load := func() {
210 mustRun(t, ctx, chromedp.EmulateViewport(390, 844), chromedp.Navigate(rig.Origin+"/"),
211 chromedp.WaitVisible("[rst-shell-menu] > summary", chromedp.ByQuery),
212 chromedp.Click("[rst-shell-menu] > summary", chromedp.ByQuery))
213 }
214 load()
215 mustRun(t, ctx, chromedp.EmulateViewport(1280, 900))
216 wide := readCard(t, ctx)
217 if !wide.MenuOpen {
218 t.Fatal("the <details> closed on the resize; the leftover-layer case this leg is for has not arisen")
219 }
220 if wide.TailDisplay != "contents" {
221 t.Errorf("widened with the card open: the tail is display %s, want today's contents", wide.TailDisplay)
222 }
223 if wide.CurEnd != "2px" {
224 t.Errorf("widened: the current item's underline is %s, want today's 2px", wide.CurEnd)
225 }
226 // A leftover layer would swallow this click; landing is the proof
227 // there is none.
228 clickAndLand(t, ctx, probe(t, ctx, "#main-link", 0.5, 0.5, 0, 0), "/go/main")
229
230 load()
231 mustRun(t, ctx, chromedp.Click("[rst-shell-account] > summary", chromedp.ByQuery), chromedp.EmulateViewport(1280, 900))
232 if c := readCard(t, ctx); !c.AcctOpen || c.AcctPanel == "static" {
233 t.Errorf("widened with the account menu open: open %v, panel position %s; want today's positioned panel", c.AcctOpen, c.AcctPanel)
234 }
235 mustRun(t, ctx, chromedp.Focus("#acct-profile", chromedp.ByQuery), chromedp.KeyEvent(kb.Escape))
236 if c := readCard(t, ctx); c.Focus != "account-summary" || !c.FocusShown {
237 t.Errorf("Escape after widening focused %q (shown %v); want the account summary, which is visible", c.Focus, c.FocusShown)
238 }
239 mustRun(t, ctx, chromedp.EmulateViewport(390, 844))
240 if c := readCard(t, ctx); c.MenuOpen {
241 t.Error("narrowed again after Escape: the card is open, but Escape closes every menu, the hidden one included")
242 }
243}
244
245// TestTheCardsControlsAreTaps: the Menu summary, the nav rows and the
246// account, language and colour-scheme summaries in the open card, at
247// 44px.
248func TestTheCardsControlsAreTaps(t *testing.T) {
249 rig, ctx, cancel := cardRig(t, map[string]string{"/": topbarPage(t, "ltr")}, true)
250 defer cancel()
251 mustRun(t, ctx, chromedp.EmulateViewport(390, 844), chromedp.Navigate(rig.Origin+"/"),
252 chromedp.WaitVisible("[rst-shell-menu] > summary", chromedp.ByQuery), chromedp.Click("[rst-shell-menu] > summary", chromedp.ByQuery))
253 requirePointer(t, ctx, true)
254 // The Menu and the card, not the whole bar: while the card is open
255 // the brand is under the outside-tap layer on purpose, so a tap there
256 // closes the card instead of following it.
257 got := readTargets(t, ctx, `(() => { `+measureFn+`; return JSON.stringify([...measure(document.querySelector("[rst-shell-menu]")), ...measure(document.querySelector("[rst-shell-tail]"))]); })()`)
258 if len(got) != 7 {
259 t.Fatalf("measured %d controls in the open card, want exactly 7: the Menu summary, three nav rows, the account, language and colour-scheme summaries", len(got))
260 }
261 assertTargets(t, "390 touch, the open card", got)
262}
263
264// TestAModalCoversTheTopbarOnAPhone: the bar is lifted above in-page
265// menu panels only while its card is open. Lifted for good, it painted
266// over a modal's scrim (fixed, z-index 10, inside main) on a phone: the
267// bar stayed undimmed and live, and its Menu could open a card over the
268// modal. Read at the brand and at the Menu summary, the two things a tap
269// on a phone's bar reaches. The sidebar's and console's content pages
270// have the same exposure through their sticky back strip: above the
271// scrim, Back stayed live under an open modal.
272//
273// The pages are the shell examples (layout-topbar, layout-sidebar-page
274// and layout-console-page) with the modal example in the slot: the
275// drive reads the brand, the Menu summary and the back control, which
276// every one of them carries, and the modal's overlay.
277func TestAModalCoversTheTopbarOnAPhone(t *testing.T) {
278 modal := defaultMarkup(t, "modal")
279 hits := func(t *testing.T, ctx context.Context, sels ...string) {
280 t.Helper()
281 for _, sel := range sels {
282 var hit string
283 mustRun(t, ctx, chromedp.Evaluate(`(() => { const r = document.querySelector(`+"`"+sel+"`"+`).getBoundingClientRect();
284 const h = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);
285 return !h ? "nothing" : h.closest("[rst-modal-overlay]") ? "overlay" : h.tagName + (h.getAttributeNames().filter(n => n.startsWith("rst-")).map(n => "[" + n + "]").join("")); })()`, &hit))
286 if hit != "overlay" {
287 t.Errorf("a tap on %s with a modal open lands on %s, want the modal's overlay: it paints above the scrim", sel, hit)
288 }
289 }
290 }
291 t.Run("topbar", func(t *testing.T) {
292 rig, ctx, cancel := cardRig(t, map[string]string{"/": shellPage(t, "topbar", modal)}, true)
293 defer cancel()
294 mustRun(t, ctx, chromedp.EmulateViewport(390, 844), chromedp.Navigate(rig.Origin+"/"),
295 chromedp.WaitVisible("[rst-modal-overlay]", chromedp.ByQuery))
296 requirePointer(t, ctx, true)
297 hits(t, ctx, "[rst-shell-brand]", "[rst-shell-menu] > summary")
298 })
299 for _, name := range []string{"sidebar", "console"} {
300 t.Run(name+" content page", func(t *testing.T) {
301 pages := map[string]string{"/": shellPage(t, name+"-page", modal)}
302 rig := harness.New(t, func(string) http.Handler { return shellAssets(t, pages) }, harness.WithCoarsePointer())
303 ctx, cancel := context.WithTimeout(rig.Context(), 90*time.Second)
304 defer cancel()
305 mustRun(t, ctx, chromedp.EmulateViewport(390, 844), chromedp.Navigate(rig.Origin+"/"),
306 chromedp.WaitVisible("[rst-modal-overlay]", chromedp.ByQuery), chromedp.WaitVisible("[rst-shell-back] a", chromedp.ByQuery))
307 requirePointer(t, ctx, true)
308 hits(t, ctx, "[rst-shell-back] a")
309 })
310 }
311}
312
313// TestAClickOnTheDocumentItselfDoesNotThrow: a click whose target is the
314// document (a script's synthetic dispatch) has no closest(), and light
315// dismiss runs in a capture listener on every click, so a throw there
316// is an error on every such dispatch and leaves any open menu open.
317func TestAClickOnTheDocumentItselfDoesNotThrow(t *testing.T) {
318 rig, ctx, cancel := cardRig(t, map[string]string{"/": topbarPage(t, "ltr")}, true)
319 defer cancel()
320 mustRun(t, ctx, chromedp.EmulateViewport(390, 844), chromedp.Navigate(rig.Origin+"/"),
321 chromedp.WaitVisible("[rst-shell-menu] > summary", chromedp.ByQuery))
322 // The shim is deferred; the drive starts once it has bound, which
323 // the account menu closing on an outside click shows.
324 settleUntil(t, ctx, `(() => { const a = document.querySelector("[rst-shell-account]"); a.open = true; document.body.click(); return !a.open; })()`)
325 var got string
326 mustRun(t, ctx, chromedp.Evaluate(`(() => { let err = "none"; addEventListener("error", e => { err = e.message; });
327 const a = document.querySelector("[rst-shell-account]"); a.open = true;
328 document.dispatchEvent(new MouseEvent("click", {bubbles: true}));
329 return err + (a.open ? " (menu left open)" : ""); })()`, &got))
330 if got != "none" {
331 t.Errorf("a click dispatched on the document: %s; want no error and the open menu closed", got)
332 }
333}
334
335// TestTheCardFitsTheSmallestPhone: at 320, with this rig's classic
336// scrollbar gutter reserved, the card stays inside the page at its
337// inline start too. Sized from the viewport (100vw) it counted the
338// gutter as room and overhung the start edge by 3px.
339func TestTheCardFitsTheSmallestPhone(t *testing.T) {
340 for _, dir := range []string{"ltr", "rtl"} {
341 t.Run(dir, func(t *testing.T) {
342 rig, ctx, cancel := cardRig(t, map[string]string{"/": topbarPage(t, dir)}, true)
343 defer cancel()
344 mustRun(t, ctx, chromedp.EmulateViewport(320, 640), chromedp.Navigate(rig.Origin+"/"),
345 chromedp.WaitVisible("[rst-shell-menu] > summary", chromedp.ByQuery),
346 chromedp.Click("[rst-shell-menu] > summary", chromedp.ByQuery))
347 c := readCard(t, ctx)
348 if !c.MenuOpen {
349 t.Fatal("the Menu did not open")
350 }
351 if c.CardLeft < c.RootLeft-0.5 || c.CardRight > c.RootRight+0.5 {
352 t.Errorf("at 320 the card spans %.1f to %.1f and the page %.1f to %.1f; it overhangs", c.CardLeft, c.CardRight, c.RootLeft, c.RootRight)
353 }
354 })
355 }
356}
357
358// TestTheConsoleBarIsTheSameCard: the console's tail folds into the same
359// card as the topbar's: the page does not move, the card sits at the
360// inline end, an outside tap closes it with and without scripts, and
361// one Escape from inside its account menu closes both.
362func TestTheConsoleBarIsTheSameCard(t *testing.T) {
363 for _, c := range []struct {
364 dir string
365 scripts bool
366 }{{"ltr", false}, {"ltr", true}, {"rtl", false}, {"rtl", true}} {
367 scripts := c.scripts
368 t.Run(fmt.Sprintf("%s scripts %v", c.dir, scripts), func(t *testing.T) {
369 // The console with one account link (#acct-profile) and a
370 // link in the page (#main-link) 450px down.
371 pages := map[string]string{"/": cardPage(t, "TestTheConsoleBarIsTheSameCard", c.dir)}
372 rig := harness.New(t, func(string) http.Handler { return shellAssets(t, pages) }, harness.WithCoarsePointer())
373 ctx, cancel := context.WithTimeout(rig.Context(), 90*time.Second)
374 defer cancel()
375 acts := []chromedp.Action{chromedp.EmulateViewport(390, 844)}
376 if !scripts {
377 acts = append(acts, emulation.SetScriptExecutionDisabled(true))
378 }
379 acts = append(acts, chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible("[rst-shell-menu] > summary", chromedp.ByQuery))
380 mustRun(t, ctx, acts...)
381 var before, open struct {
382 Main, Basis, Inner string
383 Gap float64
384 }
385 // Gap is the card's distance from the page's inline end, which is
386 // the left edge in RTL, measured against the root element's box:
387 // tokens.css reserves a stable scrollbar gutter, so the viewport's
388 // edge is not the page's. Basis and Inner are the card's own box
389 // rules: the console's base rule for its open tail (flex-basis 100%
390 // and a gap) comes earlier in the file than the card's, at the same
391 // weight, so these prove the card's rules won.
392 read := `(() => { const m = document.querySelector("main").getBoundingClientRect(), tail = document.querySelector("[rst-shell-tail]"), t = tail.getBoundingClientRect(), cs = getComputedStyle(tail);
393 const rtl = document.documentElement.dir === "rtl", h = document.documentElement.getBoundingClientRect();
394 return JSON.stringify({Main: [m.left, m.top, m.width].map(Math.round).join(","), Gap: rtl ? t.left - h.left : h.right - t.right, Basis: cs.flexBasis, Inner: cs.rowGap}); })()`
395 at(t, ctx, read, &before)
396 mustRun(t, ctx, chromedp.Click("[rst-shell-menu] > summary", chromedp.ByQuery), chromedp.WaitVisible("[rst-shell-tail] [rst-shell-account] > summary", chromedp.ByQuery))
397 at(t, ctx, read, &open)
398 if open.Main != before.Main || open.Gap < 0 || open.Gap > 13 {
399 t.Errorf("main %s -> %s, card %.1fpx from the edge; want a card over a page that did not move", before.Main, open.Main, open.Gap)
400 }
401 if open.Basis != "auto" || open.Inner != "0px" {
402 t.Errorf("the console's open tail has flex-basis %s and gap %s; the card's auto and 0 lost to the console's own rule", open.Basis, open.Inner)
403 }
404 clickAndStay(t, ctx, probeScripted(t, ctx, scripts, "#main-link", 0.5, 0.5, 0, 0), "", `!document.querySelector("[rst-shell-menu]").open`)
405 if scripts {
406 mustRun(t, ctx, chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible("[rst-shell-menu] > summary", chromedp.ByQuery),
407 chromedp.Click("[rst-shell-menu] > summary", chromedp.ByQuery), chromedp.WaitVisible("[rst-shell-tail] [rst-shell-account] > summary", chromedp.ByQuery),
408 chromedp.Click("[rst-shell-account] > summary", chromedp.ByQuery), chromedp.WaitVisible("#acct-profile", chromedp.ByQuery),
409 chromedp.Focus("#acct-profile", chromedp.ByQuery), chromedp.KeyEvent(kb.Escape))
410 var s struct {
411 Menu, Acct bool
412 Focus string
413 }
414 at(t, ctx, `JSON.stringify({Menu: document.querySelector("[rst-shell-menu]").open, Acct: document.querySelector("[rst-shell-account]").open, Focus: document.activeElement.closest("[rst-shell-menu]") ? "menu" : document.activeElement.tagName})`, &s)
415 if s.Menu || s.Acct || s.Focus != "menu" {
416 t.Errorf("Escape from the account menu in the console's card: %+v; want both closed and focus on the Menu summary", s)
417 }
418 }
419 })
420 }
421}
422