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 "math"
9 "strconv"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/chromedp/cdproto/emulation"
15 "github.com/chromedp/cdproto/input"
16 "github.com/chromedp/chromedp"
17
18 "amadan.net/rastrillo/fiesta"
19 "amadan.net/rastrillo/fiesta/internal/harness"
20)
21
22// The day switch (the switch rework, 2026-10-24): its size comes from
23// theme tokens with a fine-pointer set and a touch set; the track sits
24// in and the knob stands on a drop shadow that deepens on hover; the
25// on colour follows the knob, dragged or clicked; a released knob
26// settles at the nearer end with a firm curve. plain and signal keep
27// today's switch.
28
29// swapTheme replaces the page's theme stylesheet with the named
30// theme's CSS, inline, so one page can be read under every theme.
31func swapTheme(t *testing.T, ctx context.Context, name string) {
32 t.Helper()
33 css, ok := fiesta.ThemeCSS(name)
34 if !ok {
35 t.Fatalf("no theme %q", name)
36 }
37 mustRun(t, ctx, chromedp.Evaluate(`(() => { const l = document.querySelector('link[href="/theme.css"]'); if (l) l.remove(); for (const s of document.querySelectorAll("style[data-swap]")) s.remove(); const s = document.createElement("style"); s.dataset.swap = "1"; s.textContent = `+jsStr(string(css))+`; document.head.append(s); return true; })()`, nil))
38}
39
40// daySwitchBody is a page with an on switch, an off switch and a
41// toggle block with an on head, and the counters switchJS reads.
42const daySwitchBody = `<div rst-page><p><label rst-switch id="on"><input type="checkbox" checked><span rst-switch-track aria-hidden="true"></span> On</label></p>` +
43 `<p><label rst-switch id="off"><input type="checkbox"><span rst-switch-track aria-hidden="true"></span> Off</label></p>` +
44 `<p><label rst-switch id="dis"><input type="checkbox" checked disabled><span rst-switch-track aria-hidden="true"></span> Locked</label></p>` +
45 `<div rst-tblock id="tb"><label rst-tblock-head id="head"><input type="checkbox" checked><span rst-switch-track aria-hidden="true"></span><span rst-tblock-title>Block</span></label><div rst-tblock-body>body</div></div></div>` +
46 `<script>window.changes = {}; window.inputs = {}; for (const [k, m] of [["change", window.changes], ["input", window.inputs]]) document.addEventListener(k, e => { const l = e.target.closest("[rst-switch],[rst-tblock-head]"); if (l) m[l.id] = (m[l.id] || 0) + 1; });</script>`
47
48// TestTheSwitchTakesItsSizeFromTheTheme: the track's width and height
49// and the knob's size are theme tokens in two sets, one for a fine
50// pointer and one for a coarse one, and the knob's travel follows from
51// them. day is 40x23 with a 19px knob under a mouse and 51x31 with a
52// 27px knob on a touch screen, the toggle block's head the same; plain
53// and signal are 34x19 with a 15px knob under both. The label's
54// minimum block size, the target floor, is not the switch's to change.
55func TestTheSwitchTakesItsSizeFromTheTheme(t *testing.T) {
56 type size struct{ W, H, K, Run float64 }
57 want := map[bool]map[string]size{
58 false: {"day": {40, 23, 19, 17}, "plain": {34, 19, 15, 15}, "signal": {34, 19, 15, 15}},
59 true: {"day": {51, 31, 27, 20}, "plain": {34, 19, 15, 15}, "signal": {34, 19, 15, 15}},
60 }
61 for _, coarse := range []bool{false, true} {
62 name := "fine"
63 if coarse {
64 name = "coarse"
65 }
66 t.Run(name, func(t *testing.T) {
67 rig := sizingRig(t, coarse, map[string]string{"/": sizingDoc("switch size", daySwitchBody)})
68 ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
69 defer cancel()
70 mustRun(t, ctx, chromedp.EmulateViewport(800, 600), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible("#tb", chromedp.ByQuery))
71 requirePointer(t, ctx, coarse)
72 still(t, ctx)
73 floor := computed(t, ctx, "#on", "min-block-size")
74 for _, theme := range []string{"day", "plain", "signal"} {
75 swapTheme(t, ctx, theme)
76 w := want[coarse][theme]
77 for _, id := range []string{"on", "off", "head"} {
78 s := read(t, ctx, id)
79 if s.W != w.W || s.H != w.H || s.K != w.K {
80 t.Errorf("%s, %s pointer: #%s is %gx%g with a %gpx knob; want %gx%g with %gpx", theme, name, id, s.W, s.H, s.K, w.W, w.H, w.K)
81 }
82 if on := id != "off"; on && math.Abs(s.X-w.Run) > 0.5 || !on && math.Abs(s.X) > 0.5 {
83 t.Errorf("%s, %s pointer: #%s's knob is moved %.1fpx; want %g on and 0 off, the track's width less its height", theme, name, id, s.X, w.Run)
84 }
85 }
86 if got := computed(t, ctx, "#on", "min-block-size"); got != floor {
87 t.Errorf("%s, %s pointer: the label's min-block-size is %s, was %s under day; the target floor is not the switch's to change", theme, name, got, floor)
88 }
89 }
90 })
91 }
92}
93
94// shadowBlur reads one box-shadow layer's blur radius, the third
95// length after its colour ("rgba(…) 0px 1px 2px 0px inset" is 2).
96func shadowBlur(t *testing.T, layer string) float64 {
97 t.Helper()
98 i := strings.LastIndex(layer, ")")
99 f := strings.Fields(layer[i+1:])
100 if len(f) < 4 {
101 t.Fatalf("box-shadow layer %q: want a colour and four lengths", layer)
102 }
103 v, err := strconv.ParseFloat(strings.TrimSuffix(f[2], "px"), 64)
104 if err != nil {
105 t.Fatal(err)
106 }
107 return v
108}
109
110// painted is the layers of a box-shadow that paint: the ones whose
111// colour is not transparent, in order. A transparent layer's geometry
112// is nothing on screen, so two shadows with the same painted layers
113// are the same pixels.
114func painted(v string) []string {
115 var out []string
116 for _, l := range layers(v) {
117 if !strings.HasPrefix(l, "rgba(0, 0, 0, 0)") {
118 out = append(out, l)
119 }
120 }
121 return out
122}
123
124// paintsNothing is true when no layer of a box-shadow paints.
125func paintsNothing(v string) bool { return painted(v) == nil }
126
127// TestTheTrackSitsInAndTheKnobLiftsOnHover (the switch rework, depth
128// like the buttons'): in day the track is sunk by two inset layers,
129// the contact shadow and an ambient hairline rim; the knob stands on
130// the theme's knob shadow, two layers in day (a drop and a hairline
131// ring), over its own contact shadow and under its highlight; and
132// hovered it lifts: a longer contact shadow and the theme's hover knob
133// shadow, deeper than the rest one. The toggle block's head lifts the
134// same. A disabled switch hovered keeps its rest shadow. plain and
135// signal declare no depth: their track paints no shadow, and their
136// knob paints its own theme shadow, the same layers hovered as at
137// rest. Frozen first, for forceState.
138func TestTheTrackSitsInAndTheKnobLiftsOnHover(t *testing.T) {
139 rig := sizingRig(t, false, map[string]string{"/": sizingDoc("switch depth", daySwitchBody)}, harness.WithHover())
140 ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
141 defer cancel()
142 mustRun(t, ctx, chromedp.EmulateViewport(800, 600), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible("#tb", chromedp.ByQuery))
143 still(t, ctx)
144 knob := func(sel string) string { return knobStyleOf(t, ctx, sel+" [rst-switch-track]", "box-shadow") }
145 track := func(sel string) string { return computed(t, ctx, sel+" [rst-switch-track]", "box-shadow") }
146 hovered := func(sel string, read func(string) string) string {
147 forceState(t, ctx, sel, "hover")
148 v := read(sel)
149 forceState(t, ctx, sel)
150 return v
151 }
152 for _, theme := range []string{"day", "plain", "signal"} {
153 swapTheme(t, ctx, theme)
154 if theme != "day" {
155 for _, sel := range []string{"#off", "#on", "#head"} {
156 if tr, k, hk := track(sel), painted(knob(sel)), painted(hovered(sel, knob)); !paintsNothing(tr) || len(k) == 0 || strings.Join(k, ", ") != strings.Join(hk, ", ") {
157 t.Errorf("%s: %s paints track %q, knob %q, hovered knob %q; a flat theme's track paints nothing and its knob paints its own shadow, hovered or not", theme, sel, tr, k, hk)
158 }
159 }
160 continue
161 }
162 if tr := layers(track("#off")); len(tr) != 2 || !strings.HasSuffix(tr[0], "0px 1px 2px 0px inset") || !strings.HasSuffix(tr[1], "0px 0px 0px 0.5px inset") || paintsNothing(track("#off")) {
163 t.Errorf("day: the track's box-shadow is %q; want two painted inset layers, a 1px 2px contact shadow and a 0.5px rim", track("#off"))
164 }
165 for _, sel := range []string{"#off", "#on", "#head"} {
166 rest, lift := layers(knob(sel)), layers(hovered(sel, knob))
167 if len(rest) < 4 || !strings.HasSuffix(rest[0], "inset") || paintsNothing(knob(sel)) {
168 t.Errorf("day: %s's knob at rest has box-shadow %q; want the highlight inset first, its contact shadow, and the theme's two-layer knob shadow", sel, knob(sel))
169 continue
170 }
171 if len(lift) != len(rest) || shadowBlur(t, lift[1]) <= shadowBlur(t, rest[1]) || shadowBlur(t, lift[2]) <= shadowBlur(t, rest[2]) {
172 t.Errorf("day: %s hovered has knob box-shadow %q, at rest %q; the knob should lift, with a longer contact shadow and a deeper knob shadow", sel, strings.Join(lift, ", "), strings.Join(rest, ", "))
173 }
174 }
175 if rest, lift := knob("#dis"), hovered("#dis", knob); lift != rest {
176 t.Errorf("day: a disabled switch hovered has knob box-shadow %q, at rest %q; want no lift", lift, rest)
177 }
178 }
179}
180
181// rgbOf parses an rgb() triple (canonColours' output) into channels.
182func rgbOf(t *testing.T, v string) (r, g, b float64) {
183 t.Helper()
184 if _, err := fmt.Sscanf(canonColours(v), "rgb(%f, %f, %f)", &r, &g, &b); err != nil {
185 t.Fatalf("%q is not an rgb() colour", v)
186 }
187 return
188}
189
190// mixed is color-mix(in srgb, on share, off): each channel the share
191// of the way from off to on, which is what the track must paint when
192// its knob is that share of the way across.
193func mixed(t *testing.T, off, on string, share float64) string {
194 t.Helper()
195 r1, g1, b1 := rgbOf(t, off)
196 r2, g2, b2 := rgbOf(t, on)
197 return fmt.Sprintf("rgb(%.0f, %.0f, %.0f)", r1+(r2-r1)*share, g1+(g2-g1)*share, b1+(b2-b1)*share)
198}
199
200// near is true when two rgb() colours are within tol per channel.
201func near(t *testing.T, a, b string, tol float64) bool {
202 t.Helper()
203 r1, g1, b1 := rgbOf(t, a)
204 r2, g2, b2 := rgbOf(t, b)
205 return math.Abs(r1-r2) <= tol && math.Abs(g1-g2) <= tol && math.Abs(b1-b2) <= tol
206}
207
208// TestTheTrackColourFollowsTheKnob (the switch rework): the on colour
209// fades in exactly as far as the knob has travelled. Two registered
210// numbers on the track, --rst-switch-p (the knob's share of its
211// travel, which the knob's translate reads) and --rst-switch-on (the
212// on colour's share of the track, which a color-mix() reads), set
213// side by side by every rule and by the one script write, and eased
214// together by one transition. Mid-drag, with a finger: the track is
215// the share of the way from the off colour to the on colour that the
216// knob is across; released past the middle, it is the on colour. Mid-
217// animation after a click, the transitions paused part way: the same,
218// for a switch going on and for a toggle block's head going off. At
219// rest the numbers are 0 and 0% off, 1 and 100% on. In LTR and RTL.
220func TestTheTrackColourFollowsTheKnob(t *testing.T) {
221 doc := sizingDoc("switch colour", daySwitchBody)
222 pages := map[string]string{"/": doc, "/rtl": strings.Replace(doc, `dir="ltr"`, `dir="rtl"`, 1)}
223 for _, path := range []string{"/", "/rtl"} {
224 t.Run(path, func(t *testing.T) {
225 rig := sizingRig(t, false, pages)
226 ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
227 defer cancel()
228 mustRun(t, ctx, chromedp.EmulateViewport(800, 600), chromedp.Navigate(rig.Origin+path), chromedp.WaitVisible("#tb", chromedp.ByQuery),
229 chromedp.Evaluate(`new Promise(r => document.readyState === "complete" ? r() : addEventListener("load", r, {once: true}))`, nil, awaitPromise),
230 emulation.SetTouchEmulationEnabled(true))
231 sign := 1.0
232 if path == "/rtl" {
233 sign = -1
234 }
235 off, on := read(t, ctx, "off"), read(t, ctx, "on")
236 offColour, onColour := canonColours(off.Bg), canonColours(on.Bg)
237 if offColour == onColour {
238 t.Fatalf("the off and on tracks are both %s; the drive would prove nothing", offColour)
239 }
240 if off.P != "0" || off.On != "0%" || on.P != "1" || on.On != "100%" {
241 t.Errorf("at rest the off track's numbers are %q and %q, the on track's %q and %q; want 0 and 0%% off, 1 and 100%% on", off.P, off.On, on.P, on.On)
242 }
243 if d := computed(t, ctx, "#off [rst-switch-track]", "transition"); !strings.Contains(d, "--rst-switch-p 0.24s cubic-bezier(0.22, 1, 0.36, 1)") || !strings.Contains(d, "--rst-switch-on 0.24s cubic-bezier(0.22, 1, 0.36, 1)") {
244 t.Errorf("the track's transition is %q; want both numbers eased by the same 0.24s cubic-bezier(0.22, 1, 0.36, 1), so the colour cannot drift from the knob", d)
245 }
246
247 // A finger drags the off knob 60% of the way across in
248 // steps (the shim needs 4px before it takes the drag) and
249 // holds it there.
250 x := thumbAt(off, sign)
251 mustRun(t, ctx, input.DispatchTouchEvent(input.TouchStart, []*input.TouchPoint{{X: x, Y: off.Y}}))
252 for _, o := range []float64{0.2, 0.4, 0.6} {
253 mustRun(t, ctx, input.DispatchTouchEvent(input.TouchMove, []*input.TouchPoint{{X: x + sign*o*off.Run, Y: off.Y}}))
254 }
255 mustRun(t, ctx, chromedp.Sleep(100*time.Millisecond))
256 mid := read(t, ctx, "off")
257 if !mid.Dragging {
258 t.Fatal("the drag did not take")
259 }
260 share := math.Abs(mid.X) / mid.Run
261 if share < 0.5 || share > 0.7 {
262 t.Errorf("mid-drag the knob is %.2f of the way across; want about 0.6", share)
263 }
264 if want := mixed(t, offColour, onColour, share); !near(t, mid.Bg, want, 2) {
265 t.Errorf("mid-drag, knob %.2f across, the track is %s; want %s, that share of the way from %s to %s", share, canonColours(mid.Bg), want, offColour, onColour)
266 }
267 mustRun(t, ctx, input.DispatchTouchEvent(input.TouchEnd, nil), chromedp.Sleep(400*time.Millisecond))
268 if s := read(t, ctx, "off"); !s.Checked || canonColours(s.Bg) != onColour || math.Abs(s.X-sign*s.Run) > 0.5 {
269 t.Errorf("released at 60%%: checked %v, track %s, knob at %.1f; want on, %s, %.1f", s.Checked, canonColours(s.Bg), s.X, onColour, sign*s.Run)
270 }
271
272 // A click, and the transitions paused 100ms in: the knob and
273 // the colour have come the same share of the way. #on goes
274 // off; #head, the toggle block's, goes off too.
275 var pausedProps string
276 for _, id := range []string{"on", "head"} {
277 before := read(t, ctx, id)
278 mustRun(t, ctx, chromedp.Evaluate(`(() => { const l = document.getElementById(`+jsStr(id)+`), tr = l.querySelector("[rst-switch-track]"); l.querySelector("input").click();
279 const as = document.getAnimations().filter(a => a.effect && a.effect.target === tr); as.forEach(a => { a.pause(); a.currentTime = 100; }); return as.map(a => a.transitionProperty).sort().join(" "); })()`, &pausedProps))
280 if pausedProps != "--rst-switch-on --rst-switch-p" {
281 t.Errorf("#%s clicked: the track's running transitions are %q; want --rst-switch-on and --rst-switch-p", id, pausedProps)
282 }
283 s := read(t, ctx, id)
284 share := math.Abs(s.X) / s.Run
285 if s.Checked == before.Checked || share < 0.05 || share > 0.95 {
286 t.Errorf("#%s clicked and paused 100ms in: checked %v (was %v), knob %.2f across; want toggled and mid-way", id, s.Checked, before.Checked, share)
287 }
288 if want := mixed(t, offColour, onColour, share); !near(t, s.Bg, want, 2) {
289 t.Errorf("#%s paused 100ms into its toggle, knob %.2f across, the track is %s; want %s", id, share, canonColours(s.Bg), want)
290 }
291 mustRun(t, ctx, chromedp.Evaluate(`document.getAnimations().forEach(a => a.finish()), true`, nil))
292 }
293 })
294 }
295}
296
297// TestAReleasedKnobSettlesWithAFirmCurve (the switch rework): a drag
298// follows the pointer 1:1 across the whole track, with no transition
299// on the track while it lasts; released, the knob goes to the nearer
300// end, on from half way or further (0.5 is on), with the firm 0.18s
301// cubic-bezier(0.3, 0.9, 0.4, 1) rather than a toggle's 0.24s: the
302// shim marks the label data-rst-settle for the release and clears it
303// after, and both numbers ride the one transition, so the colour
304// follows the knob home. The checkbox is clicked only when the end is
305// the other state. Under reduced motion the settle has no transition.
306// In LTR and RTL.
307func TestAReleasedKnobSettlesWithAFirmCurve(t *testing.T) {
308 doc := sizingDoc("switch release", daySwitchBody)
309 pages := map[string]string{"/": doc, "/rtl": strings.Replace(doc, `dir="ltr"`, `dir="rtl"`, 1)}
310 const firm = "0.18s cubic-bezier(0.3, 0.9, 0.4, 1)"
311 for _, leg := range []struct {
312 name, path string
313 reduce bool
314 }{{"ltr", "/", false}, {"rtl", "/rtl", false}, {"ltr reduced motion", "/", true}} {
315 t.Run(leg.name, func(t *testing.T) {
316 rig := sizingRig(t, false, pages)
317 ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
318 defer cancel()
319 if leg.reduce {
320 mustRun(t, ctx, emulation.SetEmulatedMedia().WithFeatures([]*emulation.MediaFeature{{Name: "prefers-reduced-motion", Value: "reduce"}}))
321 }
322 mustRun(t, ctx, chromedp.EmulateViewport(800, 600), chromedp.Navigate(rig.Origin+leg.path), chromedp.WaitVisible("#tb", chromedp.ByQuery),
323 chromedp.Evaluate(`new Promise(r => document.readyState === "complete" ? r() : addEventListener("load", r, {once: true}))`, nil, awaitPromise))
324 sign := 1.0
325 if leg.path == "/rtl" {
326 sign = -1
327 }
328 mouse := func(typ input.MouseType, x, y float64) chromedp.Action {
329 return input.DispatchMouseEvent(typ, x, y).WithButton(input.Left).WithClickCount(1)
330 }
331 // drag presses the knob where it rests, moves it through the
332 // shares of the travel given (toward on), reads the switch at
333 // the last, releases, and reads it again at once.
334 drag := func(id string, shares ...float64) (mid, dropped switchReading) {
335 s := read(t, ctx, id)
336 x := thumbAt(s, sign)
337 if s.Checked {
338 x += sign * s.Run
339 }
340 mustRun(t, ctx, mouse(input.MousePressed, x, s.Y))
341 for _, o := range shares {
342 mustRun(t, ctx, mouse(input.MouseMoved, thumbAt(s, sign)+sign*o*s.Run, s.Y))
343 }
344 mid = read(t, ctx, id)
345 mustRun(t, ctx, mouse(input.MouseReleased, thumbAt(s, sign)+sign*shares[len(shares)-1]*s.Run, s.Y))
346 dropped = read(t, ctx, id)
347 return
348 }
349 off, on := read(t, ctx, "off"), read(t, ctx, "on")
350 offColour, onColour := canonColours(off.Bg), canonColours(on.Bg)
351
352 // Short of half way: back to off, nothing fired.
353 mid, dropped := drag("off", 0.2, 0.4)
354 if !mid.Dragging || mid.Duration != "0s" || math.Abs(math.Abs(mid.X)/mid.Run-0.4) > 0.03 {
355 t.Errorf("mid-drag: dragging %v, transition %s, knob %.2f across; want a drag with no transition, the knob 0.4 across under the pointer", mid.Dragging, mid.Duration, math.Abs(mid.X)/mid.Run)
356 }
357 if !dropped.Settling || dropped.Dragging {
358 t.Errorf("just released: settling %v, dragging %v; want the settle mark on and the drag mark off", dropped.Settling, dropped.Dragging)
359 }
360 if d := computed(t, ctx, "#off [rst-switch-track]", "transition"); leg.reduce && d != "none" || !leg.reduce && d != "--rst-switch-p "+firm+", --rst-switch-on "+firm {
361 t.Errorf("just released the track's transition is %q; want both numbers on the firm %s (reduced motion %v: none)", d, firm, leg.reduce)
362 }
363 mustRun(t, ctx, chromedp.Sleep(400*time.Millisecond))
364 if s := read(t, ctx, "off"); s.Checked || s.Changes != 0 || s.Settling || math.Abs(s.X) > 0.5 || canonColours(s.Bg) != offColour {
365 t.Errorf("released at 0.4, settled: checked %v, %d change events, settling %v, knob at %.1f, track %s; want off, none, the mark gone, the knob home and the off colour", s.Checked, s.Changes, s.Settling, s.X, canonColours(s.Bg))
366 }
367 if s := read(t, ctx, "off"); !leg.reduce && !strings.Contains(s.Duration, "0.24s") || leg.reduce && s.Duration != "0s" {
368 t.Errorf("settled, the track's transition is %s; want a toggle's 0.24s back (reduced motion %v: none)", s.Duration, leg.reduce)
369 }
370
371 // Half way exactly: on. Then from on, half way: still on; and
372 // short of it: off.
373 drag("off", 0.2, 0.5)
374 mustRun(t, ctx, chromedp.Sleep(400*time.Millisecond))
375 if s := read(t, ctx, "off"); !s.Checked || s.Changes != 1 || s.Inputs != 1 || math.Abs(s.X-sign*s.Run) > 0.5 || canonColours(s.Bg) != onColour {
376 t.Errorf("released at 0.5: checked %v, %d change and %d input events, knob at %.1f, track %s; want on, one of each, the knob at %.1f and the on colour", s.Checked, s.Changes, s.Inputs, s.X, canonColours(s.Bg), sign*s.Run)
377 }
378 drag("on", 0.8, 0.5)
379 mustRun(t, ctx, chromedp.Sleep(400*time.Millisecond))
380 if s := read(t, ctx, "on"); !s.Checked || s.Changes != 0 {
381 t.Errorf("from on, released at 0.5: checked %v, %d change events; want still on, none", s.Checked, s.Changes)
382 }
383 drag("on", 0.8, 0.45)
384 mustRun(t, ctx, chromedp.Sleep(400*time.Millisecond))
385 if s := read(t, ctx, "on"); s.Checked || s.Changes != 1 || math.Abs(s.X) > 0.5 || canonColours(s.Bg) != offColour {
386 t.Errorf("from on, released at 0.45: checked %v, %d change events, knob at %.1f, track %s; want off, once, the knob home and the off colour", s.Checked, s.Changes, s.X, canonColours(s.Bg))
387 }
388
389 // Mid-settle the colour is still the knob's share: release
390 // #off at 0.3 (it is on now, so this is from on) and pause
391 // the settle 60ms in.
392 if leg.reduce {
393 return
394 }
395 var props string
396 s := read(t, ctx, "off")
397 x := thumbAt(s, sign) + sign*s.Run
398 mustRun(t, ctx, mouse(input.MousePressed, x, s.Y), mouse(input.MouseMoved, thumbAt(s, sign)+sign*0.6*s.Run, s.Y), mouse(input.MouseMoved, thumbAt(s, sign)+sign*0.3*s.Run, s.Y))
399 mustRun(t, ctx, chromedp.Evaluate(`(() => { const tr = document.querySelector("#off [rst-switch-track]"); return new Promise(done => { document.addEventListener("pointerup", () => { requestAnimationFrame(() => { const as = document.getAnimations().filter(a => a.effect && a.effect.target === tr); as.forEach(a => { a.pause(); a.currentTime = 60; }); done(as.map(a => a.transitionProperty).sort().join(" ")); }); }, {once: true}); }); })()`, nil))
400 mustRun(t, ctx, mouse(input.MouseReleased, thumbAt(s, sign)+sign*0.3*s.Run, s.Y))
401 mustRun(t, ctx, chromedp.Evaluate(`(() => { const tr = document.querySelector("#off [rst-switch-track]"); const as = document.getAnimations().filter(a => a.effect && a.effect.target === tr); as.forEach(a => { a.pause(); a.currentTime = 60; }); return as.map(a => a.transitionProperty).sort().join(" "); })()`, &props))
402 if props != "--rst-switch-on --rst-switch-p" {
403 t.Errorf("released at 0.3 from on, the track's running transitions are %q; want --rst-switch-on and --rst-switch-p", props)
404 }
405 if s := read(t, ctx, "off"); s.Checked {
406 t.Errorf("released at 0.3 from on: still checked")
407 } else if share := math.Abs(s.X) / s.Run; share <= 0.02 || share >= 0.3 {
408 t.Errorf("paused 60ms into the settle the knob is %.2f across; want it on its way from 0.3 to 0", share)
409 } else if want := mixed(t, offColour, onColour, share); !near(t, s.Bg, want, 2) {
410 t.Errorf("paused 60ms into the settle, knob %.2f across, the track is %s; want %s", share, canonColours(s.Bg), want)
411 }
412 mustRun(t, ctx, chromedp.Evaluate(`document.getAnimations().forEach(a => a.finish()), true`, nil))
413 })
414 }
415}
416
417// TestAHoveredKnobPrintsNoShadow (switch review fix 1): the knob's
418// hover lift is a box-shadow in a rule that outranks print's reset on
419// the knob, so print has to flatten the two knob-shadow tokens as it
420// flattens the depth tokens; otherwise a page printed with the pointer
421// over a switch printed the lift. Hovered and at rest, the ordinary
422// switch and the toggle block's head, in day, plain and signal, on a
423// hover rig under print media: no painted shadow on the knob or the
424// track.
425func TestAHoveredKnobPrintsNoShadow(t *testing.T) {
426 rig := sizingRig(t, false, map[string]string{"/": sizingDoc("switch print", daySwitchBody)}, harness.WithHover())
427 ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
428 defer cancel()
429 mustRun(t, ctx, emulation.SetEmulatedMedia().WithMedia("print"), chromedp.EmulateViewport(800, 600), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible("#tb", chromedp.ByQuery))
430 still(t, ctx)
431 var hov, print bool
432 mustRun(t, ctx, chromedp.Evaluate(`matchMedia("(hover: hover)").matches`, &hov), chromedp.Evaluate(`matchMedia("print").matches`, &print))
433 if !hov || !print {
434 t.Fatalf("(hover: hover) %v, print %v; the hover rule would not apply and the drive would prove nothing", hov, print)
435 }
436 for _, theme := range []string{"day", "plain", "signal"} {
437 swapTheme(t, ctx, theme)
438 for _, sel := range []string{"#on", "#off", "#head"} {
439 for _, states := range [][]string{{}, {"hover"}} {
440 forceState(t, ctx, sel, states...)
441 if k, tr := knobStyleOf(t, ctx, sel+" [rst-switch-track]", "box-shadow"), computed(t, ctx, sel+" [rst-switch-track]", "box-shadow"); !noShadow(k) || !noShadow(tr) {
442 t.Errorf("%s: %s %v in print paints knob box-shadow %q, track %q; want nothing painted", theme, sel, states, k, tr)
443 }
444 forceState(t, ctx, sel)
445 }
446 }
447 }
448}
449
450// TestATblockHeadDragsLikeASwitch (switch review fix 3): the toggle
451// block's head has the same track and must drag the same. The shim's
452// drag found only [rst-switch] labels, so a head never took a drag;
453// it finds the head too, reading the head's own input (the body is
454// the head's sibling, not inside it), and the drag, sheen and settle
455// rules name the head with #104 item 1's direct-child scoping. From
456// on, the knob dragged 30% across follows the pointer with no
457// transition while the track's colour follows it; released, the head
458// settles with the firm curve, goes off once (the label's own click
459// after a drag is cancelled, or it would toggle straight back), and
460// the body hides; dragged back past the middle it goes on and the body
461// shows; a tap still toggles it. In LTR and RTL.
462func TestATblockHeadDragsLikeASwitch(t *testing.T) {
463 doc := sizingDoc("head drag", daySwitchBody)
464 pages := map[string]string{"/": doc, "/rtl": strings.Replace(doc, `dir="ltr"`, `dir="rtl"`, 1)}
465 for _, path := range []string{"/", "/rtl"} {
466 t.Run(path, func(t *testing.T) {
467 rig := sizingRig(t, false, pages)
468 ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
469 defer cancel()
470 mustRun(t, ctx, chromedp.EmulateViewport(800, 600), chromedp.Navigate(rig.Origin+path), chromedp.WaitVisible("#tb", chromedp.ByQuery),
471 chromedp.Evaluate(`new Promise(r => document.readyState === "complete" ? r() : addEventListener("load", r, {once: true}))`, nil, awaitPromise))
472 sign := 1.0
473 if path == "/rtl" {
474 sign = -1
475 }
476 mouse := func(typ input.MouseType, x, y float64) chromedp.Action {
477 return input.DispatchMouseEvent(typ, x, y).WithButton(input.Left).WithClickCount(1)
478 }
479 offColour, onColour := canonColours(read(t, ctx, "off").Bg), canonColours(read(t, ctx, "on").Bg)
480 body := func() string { return computed(t, ctx, "#tb > [rst-tblock-body]", "display") }
481 // drag presses the head's knob where it rests and moves it to
482 // each share of the travel, reads before the release and at
483 // once after it (with the track's transition then), and
484 // waits for the settle.
485 drag := func(shares ...float64) (mid, dropped switchReading, trans string) {
486 s := read(t, ctx, "head")
487 x := thumbAt(s, sign)
488 if s.Checked {
489 x += sign * s.Run
490 }
491 mustRun(t, ctx, mouse(input.MousePressed, x, s.Y))
492 for _, o := range shares {
493 mustRun(t, ctx, mouse(input.MouseMoved, thumbAt(s, sign)+sign*o*s.Run, s.Y))
494 }
495 mid = read(t, ctx, "head")
496 mustRun(t, ctx, mouse(input.MouseReleased, thumbAt(s, sign)+sign*shares[len(shares)-1]*s.Run, s.Y))
497 dropped = read(t, ctx, "head")
498 trans = computed(t, ctx, "#head [rst-switch-track]", "transition")
499 mustRun(t, ctx, chromedp.Sleep(400*time.Millisecond))
500 return
501 }
502 if s := read(t, ctx, "head"); !s.Checked || body() != "block" {
503 t.Fatalf("the head starts checked %v with its body %s; want on and shown", s.Checked, body())
504 }
505 mid, dropped, trans := drag(0.7, 0.3)
506 if !mid.Dragging || mid.Duration != "0s" || math.Abs(math.Abs(mid.X)/mid.Run-0.3) > 0.03 {
507 t.Errorf("%s: mid-drag the head is dragging %v, transition %s, knob %.2f across; want a drag with no transition and the knob 0.3 across under the pointer", path, mid.Dragging, mid.Duration, math.Abs(mid.X)/mid.Run)
508 }
509 if want := mixed(t, offColour, onColour, math.Abs(mid.X)/mid.Run); mid.Dragging && !near(t, mid.Bg, want, 2) {
510 t.Errorf("%s: mid-drag the head's track is %s; want %s, the knob's share of the way from off to on", path, canonColours(mid.Bg), want)
511 }
512 if !dropped.Settling || dropped.Dragging || !strings.Contains(trans, "0.18s cubic-bezier(0.3, 0.9, 0.4, 1)") {
513 t.Errorf("%s: just released the head is settling %v, dragging %v, transition %q; want the settle mark and the firm curve", path, dropped.Settling, dropped.Dragging, trans)
514 }
515 if s := read(t, ctx, "head"); s.Checked || s.Changes != 1 || s.Inputs != 1 || math.Abs(s.X) > 0.5 || canonColours(s.Bg) != offColour || body() != "none" {
516 t.Errorf("%s: released at 0.3: checked %v, %d change and %d input events, knob at %.1f, track %s, body %s; want off, one of each, the knob home, the off colour and the body hidden", path, s.Checked, s.Changes, s.Inputs, s.X, canonColours(s.Bg), body())
517 }
518 drag(0.3, 0.8)
519 if s := read(t, ctx, "head"); !s.Checked || s.Changes != 2 || math.Abs(s.X-sign*s.Run) > 0.5 || body() != "block" {
520 t.Errorf("%s: dragged back to 0.8: checked %v, %d change events, knob at %.1f, body %s; want on, two, the knob at the end and the body shown", path, s.Checked, s.Changes, s.X, body())
521 }
522 s := read(t, ctx, "head")
523 x := thumbAt(s, sign) + sign*s.Run
524 mustRun(t, ctx, mouse(input.MousePressed, x, s.Y), mouse(input.MouseReleased, x, s.Y), chromedp.Sleep(400*time.Millisecond))
525 if s := read(t, ctx, "head"); s.Checked || s.Changes != 3 {
526 t.Errorf("%s: a tap on the head: checked %v, %d change events; want off, three", path, s.Checked, s.Changes)
527 }
528 })
529 }
530}
531
532// TestAToggleDuringASettleTakesItsOwnCurve (switch review fix 4): the
533// settle mark outlives the release (300ms for a 0.18s transition), so a
534// click or Space inside that window took the firm release curve
535// instead of a toggle's 0.24s. The mark now goes on the next change
536// event inside the switch, bar the release's own checkbox click, which
537// is the settle itself. A release at 0.4 then a click: the mark is
538// gone at once and the toggle rides 0.24s; a release that commits
539// keeps the mark and the firm curve; a release at 0.8 from on (no
540// commit) then Space on the focused checkbox: the mark is gone.
541func TestAToggleDuringASettleTakesItsOwnCurve(t *testing.T) {
542 rig := sizingRig(t, false, map[string]string{"/": sizingDoc("settle leak", daySwitchBody)})
543 ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
544 defer cancel()
545 mustRun(t, ctx, chromedp.EmulateViewport(800, 600), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible("#tb", chromedp.ByQuery),
546 chromedp.Evaluate(`new Promise(r => document.readyState === "complete" ? r() : addEventListener("load", r, {once: true}))`, nil, awaitPromise))
547 const firm, toggle = "0.18s cubic-bezier(0.3, 0.9, 0.4, 1)", "0.24s cubic-bezier(0.22, 1, 0.36, 1)"
548 mouse := func(typ input.MouseType, x, y float64) chromedp.Action {
549 return input.DispatchMouseEvent(typ, x, y).WithButton(input.Left).WithClickCount(1)
550 }
551 trans := func(id string) string { return computed(t, ctx, "#"+id+" [rst-switch-track]", "transition") }
552 // release drags the knob to share and lets go, reading at once.
553 release := func(id string, share float64) switchReading {
554 s := read(t, ctx, id)
555 x := thumbAt(s, 1)
556 if s.Checked {
557 x += s.Run
558 }
559 mustRun(t, ctx, mouse(input.MousePressed, x, s.Y), mouse(input.MouseMoved, thumbAt(s, 1)+0.5*s.Run, s.Y), mouse(input.MouseMoved, thumbAt(s, 1)+share*s.Run, s.Y),
560 mouse(input.MouseReleased, thumbAt(s, 1)+share*s.Run, s.Y))
561 return read(t, ctx, id)
562 }
563
564 if s := release("off", 0.4); !s.Settling || s.Checked || !strings.Contains(trans("off"), firm) {
565 t.Fatalf("released at 0.4: settling %v, checked %v, transition %q; want the mark, off, and the firm curve: the drive's premise", s.Settling, s.Checked, trans("off"))
566 }
567 mustRun(t, ctx, chromedp.Evaluate(`document.querySelector("#off input").click(), true`, nil))
568 if s := read(t, ctx, "off"); s.Settling || !s.Checked || !strings.Contains(trans("off"), toggle) {
569 t.Errorf("clicked while settling: settling %v, checked %v, transition %q; want the mark gone, on, and a toggle's %s", s.Settling, s.Checked, trans("off"), toggle)
570 }
571 mustRun(t, ctx, chromedp.Sleep(400*time.Millisecond))
572 if s := read(t, ctx, "off"); !s.Checked || math.Abs(s.X-s.Run) > 0.5 || s.Changes != 1 {
573 t.Errorf("settled after the click: checked %v, knob at %.1f, %d change events; want on at %.1f, one", s.Checked, s.X, s.Changes, s.Run)
574 }
575
576 // The release's own click is the settle, and keeps the mark.
577 if s := release("on", 0.2); !s.Settling || s.Checked || !strings.Contains(trans("on"), firm) {
578 t.Errorf("released at 0.2 from on, which commits: settling %v, checked %v, transition %q; want the mark kept, off, and the firm curve", s.Settling, s.Checked, trans("on"))
579 }
580 mustRun(t, ctx, chromedp.Sleep(400*time.Millisecond))
581
582 // Space on the focused checkbox during a settle that did not commit.
583 if s := release("off", 0.8); !s.Settling || !s.Checked {
584 t.Fatalf("released at 0.8 from on: settling %v, checked %v; want the mark and still on", s.Settling, s.Checked)
585 }
586 mustRun(t, ctx, chromedp.Evaluate(`document.querySelector("#off input").focus(), true`, nil), chromedp.KeyEvent(" "))
587 if s := read(t, ctx, "off"); s.Settling || s.Checked || !strings.Contains(trans("off"), toggle) {
588 t.Errorf("Space while settling: settling %v, checked %v, transition %q; want the mark gone, off, and a toggle's %s", s.Settling, s.Checked, trans("off"), toggle)
589 }
590}
591