oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1//go:build browser
2
3// The editor's round-2 polish, held in a real Chromium: the strip does
4// not draw over its own words, a reload is not somebody else arriving,
5// and a modal dialog owns the keyboard. Each of these was a defect a
6// person could see (polish audit, slides.md), and none of them is
7// visible to a markup test — they are geometry, a sessionStorage and a
8// key event's route through the page.
9package slidestest
10
11import (
12 "fmt"
13 "testing"
14 "time"
15
16 "amadan.net/rastrillo/rastrillo/harness"
17 "github.com/chromedp/cdproto/input"
18 "github.com/chromedp/chromedp"
19)
20
21const editorBound = `document.querySelector('[slides-stage] [data-block="heading"]')?.getAttribute('contenteditable') === 'true'`
22
23// openEditor signs in and opens the sample deck's editor, bound.
24func openEditor(t *testing.T) (*harness.Rig, *fakeMailer, string) {
25 t.Helper()
26 rig, mailer := newRig(t)
27 browserSignIn(t, rig, mailer)
28 var deck string
29 rig.Run(chromedp.Evaluate(`(document.querySelector('a[href^="/d/"]')?.getAttribute('href') ?? '')`, &deck))
30 if !deckPath.MatchString(deck) {
31 t.Fatalf("the deck list links no deck: %q", deck)
32 }
33 rig.Run(
34 chromedp.Navigate(rig.Origin+deck+"/edit"),
35 chromedp.WaitVisible(`[slides-editor]`, chromedp.ByQuery),
36 waitFor(editorBound),
37 )
38 return rig, mailer, deck
39}
40
41// stripGeometry measures every strip item: its label, its avatars and
42// its menu trigger, and reports each overlap, each trigger under the
43// 24px target minimum, and each avatar box outside its thumbnail. An
44// empty answer is the pass. Items outside the strip's scrollport are
45// measured too — getBoundingClientRect does not care.
46const stripGeometry = `(() => {
47 const r = el => el && el.getBoundingClientRect()
48 const hit = (a, b) => a && b && a.width > 0 && b.width > 0 &&
49 a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom
50 const inside = (a, b) => a.left >= b.left - 0.5 && a.right <= b.right + 0.5 && a.top >= b.top - 0.5 && a.bottom <= b.bottom + 0.5
51 const out = []
52 let avatars = 0
53 for (const li of document.querySelectorAll('[slides-strip-item]')) {
54 const id = li.dataset.slideId
55 const label = r(li.querySelector('[slides-strip-label]'))
56 const thumb = r(li.querySelector('[slides-thumb]'))
57 const av = r(li.querySelector('[slides-presence-avatars]'))
58 const menu = r(li.querySelector('[slides-strip-menu] > summary'))
59 if (av) avatars++
60 if (hit(label, av)) out.push(id + ': the label is under the avatars')
61 if (hit(label, menu)) out.push(id + ': the label is under the menu trigger')
62 if (av && !inside(av, thumb)) out.push(id + ': the avatars are outside the thumbnail')
63 if (menu && (menu.width < 24 || menu.height < 24)) out.push(id + ': the menu trigger is ' + menu.width + 'x' + menu.height + ', under 24x24')
64 if (menu && (menu.width > 32 || menu.height > 32)) out.push(id + ': the menu trigger is ' + menu.width + 'x' + menu.height + ', not a compact icon button')
65 if (menu && !inside(menu, thumb)) out.push(id + ': the menu trigger is outside the thumbnail')
66 }
67 if (avatars === 0) out.push('no strip item carries an avatar — nothing was measured')
68 return out.join('\n')
69})()`
70
71// TestBrowserStripDoesNotOverlapItself (polish audit, "PA RRENT
72// SLIDE"): with another tab's avatar on the current slide, the item's
73// label is clear of the avatars and of the menu trigger, the trigger is
74// a compact 24px-or-more target, and the words "Current slide" are for
75// a screen reader only — at 1440, where the strip is a column, and at
76// 800, where it is a row.
77func TestBrowserStripDoesNotOverlapItself(t *testing.T) {
78 rig, _, deck := openEditor(t)
79 var slide string
80 rig.Run(chromedp.Evaluate(`document.querySelector('[slides-editor]').dataset.slide`, &slide))
81
82 // A real second tab, so the avatar is presence-view.mjs's own and
83 // sits wherever the bundle puts it.
84 tab2, cancel := chromedp.NewContext(rig.Context())
85 defer cancel()
86 runOn(t, tab2,
87 chromedp.Navigate(rig.Origin+deck+"/edit"),
88 chromedp.WaitVisible(`[slides-editor]`, chromedp.ByQuery),
89 waitFor(editorBound),
90 )
91 rig.Run(waitFor(`document.querySelector('[slides-strip-item][data-slide-id="` + slide + `"] [slides-presence-avatar]') !== null`))
92
93 for _, width := range []int64{1440, 800} {
94 rig.Run(
95 chromedp.EmulateViewport(width, 900),
96 // Park the pointer away from the strip: hover is one of the
97 // states that shows a menu trigger, and this is the resting
98 // look being measured.
99 chromedp.MouseEvent(input.MouseMoved, float64(width)-5, 895),
100 chromedp.Sleep(300*time.Millisecond),
101 )
102 var problems string
103 rig.Run(chromedp.Evaluate(stripGeometry, &problems))
104 if problems != "" {
105 t.Errorf("at %dpx the strip overlaps itself:\n%s", width, problems)
106 }
107
108 // The resting menu trigger of an item that is neither current
109 // nor hovered nor focused is not drawn over its thumbnail.
110 var restingOpacity string
111 rig.Run(chromedp.Evaluate(`getComputedStyle(document.querySelector('[slides-strip-item]:not([data-current="true"]) [slides-strip-menu] > summary')).opacity`, &restingOpacity))
112 if restingOpacity != "0" {
113 t.Errorf("at %dpx a resting item's menu trigger has opacity %s, want 0 (shown on hover, focus or current only)", width, restingOpacity)
114 }
115 var currentOpacity string
116 rig.Run(chromedp.Evaluate(`getComputedStyle(document.querySelector('[slides-strip-item][data-current="true"] [slides-strip-menu] > summary')).opacity`, &currentOpacity))
117 if currentOpacity != "1" {
118 t.Errorf("at %dpx the current item's menu trigger has opacity %s, want 1", width, currentOpacity)
119 }
120
121 // "Current slide" is in the accessible name and not on screen.
122 var current struct {
123 Text string `json:"text"`
124 Width float64 `json:"width"`
125 Height float64 `json:"height"`
126 Upper string `json:"upper"`
127 }
128 rig.Run(chromedp.Evaluate(`(() => {
129 const el = document.querySelector('[slides-strip-item][data-current="true"] [slides-strip-current]')
130 const b = el.getBoundingClientRect()
131 return { text: el.textContent, width: b.width, height: b.height, upper: getComputedStyle(el).textTransform }
132 })()`, &current))
133 if current.Text == "" {
134 t.Errorf("at %dpx the current item lost its 'Current slide' text — a screen reader still needs it", width)
135 }
136 if current.Width > 1 || current.Height > 1 {
137 t.Errorf("at %dpx 'Current slide' is visible (%gx%g), want it for screen readers only", width, current.Width, current.Height)
138 }
139 if current.Upper == "uppercase" {
140 t.Errorf("at %dpx 'Current slide' is still uppercase", width)
141 }
142
143 // Hover and keyboard focus each show a resting item's trigger.
144 var at struct {
145 X float64 `json:"x"`
146 Y float64 `json:"y"`
147 }
148 rig.Run(chromedp.Evaluate(`(() => {
149 const li = document.querySelector('[slides-strip-item]:not([data-current="true"])')
150 li.scrollIntoView({ block: 'nearest', inline: 'nearest' })
151 const b = li.getBoundingClientRect()
152 return { x: b.left + b.width / 2, y: b.top + b.height / 2 }
153 })()`, &at))
154 var hoverOpacity string
155 rig.Run(
156 chromedp.MouseEvent(input.MouseMoved, at.X, at.Y),
157 chromedp.Sleep(300*time.Millisecond),
158 chromedp.Evaluate(`[...document.querySelectorAll('[slides-strip-item]:not([data-current="true"])')].filter(li => li.matches(':hover')).map(li => getComputedStyle(li.querySelector('[slides-strip-menu] > summary')).opacity).join(',')`, &hoverOpacity),
159 )
160 if hoverOpacity != "1" {
161 t.Errorf("at %dpx a hovered item's menu trigger has opacity %q, want 1", width, hoverOpacity)
162 }
163 var focusOpacity string
164 rig.Run(
165 chromedp.MouseEvent(input.MouseMoved, float64(width)-5, 895),
166 chromedp.Evaluate(`document.querySelectorAll('[slides-strip-item]:not([data-current="true"]) [slides-strip-link]')[1].focus()`, nil),
167 chromedp.Sleep(300*time.Millisecond),
168 chromedp.Evaluate(`getComputedStyle(document.activeElement.closest('[slides-strip-item]').querySelector('[slides-strip-menu] > summary')).opacity`, &focusOpacity),
169 )
170 if focusOpacity != "1" {
171 t.Errorf("at %dpx a focused item's menu trigger has opacity %q, want 1", width, focusOpacity)
172 }
173 rig.Screen(`[slides-strip-nav]`, fmt.Sprintf("the strip at %dpx", width))
174 }
175}
176