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 "regexp"
8 "strings"
9 "testing"
10 "time"
11
12 "github.com/chromedp/cdproto/emulation"
13 "github.com/chromedp/chromedp"
14
15 "amadan.net/rastrillo/fiesta/internal/harness"
16)
17
18// noShadow: none, or a shadow list whose every colour is fully
19// transparent, which is what a token set flat computes to; an element
20// with such a list paints nothing.
21func noShadow(v string) bool {
22 if v == "none" {
23 return true
24 }
25 for _, c := range regexp.MustCompile(`rgba?\([^)]*\)|color\([^)]*\)`).FindAllString(v, -1) {
26 if !strings.HasSuffix(c, ", 0)") && !strings.HasSuffix(c, "/ 0)") {
27 return false
28 }
29 }
30 return true
31}
32
33// noMove: none, or a translate whose every length is 0px (a 0px drop
34// computes to "0px", not to none).
35func noMove(v string) bool {
36 if v == "none" {
37 return true
38 }
39 for _, f := range strings.Fields(v) {
40 if f != "0px" {
41 return false
42 }
43 }
44 return true
45}
46
47// knobStyle reads one computed property of the first switch's knob,
48// the ::after getComputedStyle takes as its second argument.
49func knobStyle(t *testing.T, ctx context.Context, prop string) string {
50 t.Helper()
51 return knobStyleOf(t, ctx, "#sw [rst-switch-track]", prop)
52}
53
54// knobStyleOf is knobStyle for the track the selector names.
55func knobStyleOf(t *testing.T, ctx context.Context, track, prop string) string {
56 t.Helper()
57 var out string
58 mustRun(t, ctx, chromedp.Evaluate(`getComputedStyle(document.querySelector(`+jsStr(track)+`), "::after").getPropertyValue(`+jsStr(prop)+`)`, &out))
59 return strings.TrimSpace(out)
60}
61
62// systemColour reads what the page resolves one CSS system colour to,
63// so a drive can hold an element to Highlight or HighlightText by
64// value rather than by a guess at the emulated palette.
65func systemColour(t *testing.T, ctx context.Context, name string) string {
66 t.Helper()
67 var out string
68 mustRun(t, ctx, chromedp.Evaluate(`(() => { const e = document.createElement("span"); e.style.backgroundColor = `+jsStr(name)+`; document.body.append(e); const v = getComputedStyle(e).backgroundColor; e.remove(); return v; })()`, &out))
69 return strings.TrimSpace(out)
70}
71
72// emulatedRig opens the depth page under an emulated medium, at a width
73// set before the navigation. A viewport change after the page has
74// loaded turns (hover: hover) off in this Chromium until the next
75// navigation, so a hover cell read after a resize sees no rule inside
76// a hover query and tests nothing; the query is checked here so that
77// cannot happen quietly.
78func emulatedRig(t *testing.T, media *emulation.SetEmulatedMediaParams, width, height int64) (*harness.Rig, context.Context) {
79 t.Helper()
80 rig := sizingRig(t, false, depthDoc(t), harness.WithHover())
81 ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
82 t.Cleanup(cancel)
83 mustRun(t, ctx, media, chromedp.EmulateViewport(width, height), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible("#field", chromedp.ByQuery))
84 still(t, ctx)
85 var hov bool
86 mustRun(t, ctx, chromedp.Evaluate(`matchMedia("(hover: hover)").matches`, &hov))
87 if !hov {
88 t.Fatalf("(hover: hover) is false at %dx%d; the hover cells would read nothing", width, height)
89 }
90 mustRun(t, ctx, chromedp.Evaluate(`document.getElementById("dd").open = true, true`, nil))
91 return rig, ctx
92}
93
94var forcedMedia = emulation.SetEmulatedMedia().WithFeatures([]*emulation.MediaFeature{{Name: "forced-colors", Value: "active"}, {Name: "prefers-color-scheme", Value: "light"}})
95
96// TestForcedColoursKeepBordersAndSelection emulates forced colours:
97// cards, buttons, fields and summaries have a visible border and
98// no painted shadow in every state; the knob is painted and its track
99// bordered; the current item in each of the three shells, the current
100// menu item, tab and chip use the highlight pair and paint no shadow
101// (the tab's lift is a theme shadow the axis never flattened); a
102// pressed button does not move; the focus outline is still there. At
103// the phone width, on a rig born there, the topbar card's current item
104// takes the pair, and the index's current link keeps it hovered and
105// pressed: the index ladder painted its surface over the highlight
106// and left the text HighlightText, white on white.
107func TestForcedColoursKeepBordersAndSelection(t *testing.T) {
108 _, ctx := emulatedRig(t, forcedMedia, 1280, 900)
109 var active bool
110 mustRun(t, ctx, chromedp.Evaluate(`matchMedia("(forced-colors: active)").matches`, &active))
111 if !active {
112 t.Fatal("forced colours are not emulated; the leg would prove nothing")
113 }
114 for _, sel := range []string{"#box", "#card", "#list", "#stats", "#b-plain", "#b-primary", "#b-ghost", "#field", "#dd-sum"} {
115 for _, states := range [][]string{{}, {"hover"}, {"hover", "active"}, {"active"}} {
116 forceState(t, ctx, sel, states...)
117 if w, s := computed(t, ctx, sel, "border-top-width"), computed(t, ctx, sel, "border-top-style"); w == "0px" || s == "none" {
118 t.Errorf("%s %v under forced colours has border %s %s; want a real border", sel, states, w, s)
119 }
120 if sh := computed(t, ctx, sel, "box-shadow"); !noShadow(sh) {
121 t.Errorf("%s %v under forced colours paints box-shadow %q", sel, states, sh)
122 }
123 forceState(t, ctx, sel)
124 }
125 }
126 // The mode paints Canvas under everything, so the knob is held
127 // against its track, not against transparent: ButtonText on Canvas.
128 if knob, track := knobStyle(t, ctx, "background-color"), computed(t, ctx, "#sw [rst-switch-track]", "background-color"); knob == "rgba(0, 0, 0, 0)" || knob == track {
129 t.Errorf("the switch knob under forced colours is %s on a %s track; want ButtonText on Canvas", knob, track)
130 }
131 // Checked, the track is Highlight, and the knob on it must be
132 // HighlightText: the pair a palette guarantees contrasts. ButtonText
133 // on Highlight need not (a white ButtonText on a cyan Highlight is
134 // about 1.25:1), for the ordinary switch and the toggle block's head
135 // alike. Highlight and HighlightText are held apart from
136 // ButtonText first, or the knob could pass by coincidence.
137 hl, hlText, btnText := systemColour(t, ctx, "Highlight"), systemColour(t, ctx, "HighlightText"), systemColour(t, ctx, "ButtonText")
138 if hlText == btnText || hl == hlText {
139 t.Fatalf("the emulated palette has HighlightText %s, ButtonText %s and Highlight %s; the checked knob could not be told from an unchecked one", hlText, btnText, hl)
140 }
141 for _, track := range []string{"#sw-on [rst-switch-track]", "#tb-head [rst-switch-track]"} {
142 if knob, bg := knobStyleOf(t, ctx, track, "background-color"), computed(t, ctx, track, "background-color"); bg != hl || knob != hlText {
143 t.Errorf("the checked switch %s under forced colours paints its knob %s on a %s track; want HighlightText %s on Highlight %s", track, knob, bg, hlText, hl)
144 }
145 }
146 // The hovered knob's dome is a gradient of the theme's knob colours,
147 // not of depth tokens; the mode drops every gradient layer (one none
148 // per layer), and that is the measurement the block relies on when
149 // it sets only the knob's colour.
150 forceState(t, ctx, "#sw", "hover")
151 if img := knobStyle(t, ctx, "background-image"); strings.Trim(strings.ReplaceAll(img, "none", ""), ", ") != "" {
152 t.Errorf("a hovered knob under forced colours still paints background-image %q", img)
153 }
154 forceState(t, ctx, "#sw")
155 // Canvas on CanvasText is what the mode gives every item, so the
156 // current item is held against its sibling: the pair must differ.
157 highlighted := func(ctx context.Context, cur, other string) {
158 t.Helper()
159 bg, fg := computed(t, ctx, cur, "background-color"), computed(t, ctx, cur, "color")
160 if bg == "rgba(0, 0, 0, 0)" || bg == computed(t, ctx, other, "background-color") || fg == computed(t, ctx, other, "color") {
161 t.Errorf("%s under forced colours is %s on %s, the same as %s; want Highlight on HighlightText", cur, fg, bg, other)
162 }
163 if sh := computed(t, ctx, cur, "box-shadow"); !noShadow(sh) {
164 t.Errorf("%s under forced colours paints box-shadow %q", cur, sh)
165 }
166 }
167 for _, pair := range [][2]string{{"#nav-y", "#nav-x"}, {"#cnav-y", "#cnav-x"}, {"#menu-cur", "#menu-a"}, {"#tab-2", "#tab-1"}, {"#page-2", "#page-1"}} {
168 highlighted(ctx, pair[0], pair[1])
169 }
170 forceState(t, ctx, "#b-primary", "hover", "active")
171 if got := computed(t, ctx, "#b-primary", "translate"); !noMove(got) {
172 t.Errorf("a pressed button moved (%s) under forced colours", got)
173 }
174 forceState(t, ctx, "#b-primary", "focus", "focus-visible")
175 if st := computed(t, ctx, "#b-primary", "outline-style"); st == "none" {
176 t.Error("the focus outline is gone under forced colours")
177 }
178 forceState(t, ctx, "#b-primary")
179
180 _, nctx := emulatedRig(t, forcedMedia, 390, 844)
181 highlighted(nctx, "#tnav-y", "#tnav-x")
182 rest := computed(t, nctx, "#inav-y", "background-color")
183 for _, states := range [][]string{{}, {"hover"}, {"hover", "active"}, {"active"}} {
184 forceState(t, nctx, "#inav-y", states...)
185 if bg, fg := computed(t, nctx, "#inav-y", "background-color"), computed(t, nctx, "#inav-y", "color"); bg != rest || bg == fg {
186 t.Errorf("the index's current link %v under forced colours is %s on %s; want the rest highlight %s under HighlightText", states, fg, bg, rest)
187 }
188 forceState(t, nctx, "#inav-y")
189 }
190}
191
192// TestPrintDrawsBordersAndNoDepth emulates print media (C17): no
193// painted shadow, no gradient, no tint, no movement in any state;
194// cards, fields, buttons and summaries have a --rst-line-strong
195// border; a filled button is outlined text in every state (the fills
196// are tokens, and print sets them transparent); the knob has a
197// printable fill and a ring, and the checked track a fill the off
198// track lacks; current items are flat. The check is for borders
199// because "backgrounds disabled" is a print setting that drops
200// backgrounds and leaves borders and outlines, so a boundary drawn as
201// one of those is the one that survives it. The phone width is walked
202// on a rig born there, because the topbar's card and its hover are
203// phone-only rules inside a hover query; and a dark-scheme press on a
204// page opened again, because day's dark press numbers sit in
205// :root[data-theme] blocks that outrank html:root.
206func TestPrintDrawsBordersAndNoDepth(t *testing.T) {
207 printMedia := emulation.SetEmulatedMedia().WithMedia("print")
208 rig, ctx := emulatedRig(t, printMedia, 1280, 900)
209 flat := func(ctx context.Context, sel string, states ...string) {
210 t.Helper()
211 forceState(t, ctx, sel, states...)
212 if sh, img, tr := computed(t, ctx, sel, "box-shadow"), computed(t, ctx, sel, "background-image"), computed(t, ctx, sel, "translate"); !noShadow(sh) || img != "none" || !noMove(tr) {
213 t.Errorf("%s %v in print: box-shadow %q background-image %q translate %s; want nothing painted and nothing moved", sel, states, sh, img, tr)
214 }
215 forceState(t, ctx, sel)
216 }
217 for _, sel := range []string{"#box", "#card", "#list", "#stats", "#b-plain", "#b-primary", "#b-danger", "#b-ghost", "#field", "#dd-sum", "#row-a", "#lra", "#tile", "#nav-x", "#nav-y", "#menu-a", "#opt-active", "#tab-1", "#tab-2", "#page-1", "#page-2"} {
218 for _, states := range [][]string{{}, {"hover"}, {"hover", "active"}, {"active"}} {
219 flat(ctx, sel, states...)
220 }
221 }
222 // day's light --rst-line-strong is #868691 (its comment says why it
223 // is darker than the mockup's).
224 const line = "rgb(134, 134, 145)"
225 for _, sel := range []string{"#box", "#card", "#list", "#stats", "#b-plain", "#b-primary", "#b-ghost", "#field", "#dd-sum", "#page-2"} {
226 if c, w := computed(t, ctx, sel, "border-top-color"), computed(t, ctx, sel, "border-top-width"); c != line || w != "1px" {
227 t.Errorf("%s in print has border %s %s, want 1px --rst-line-strong %s", sel, c, w, line)
228 }
229 }
230 for _, sel := range []string{"#b-primary", "#b-danger", "#b-plain"} {
231 for _, states := range [][]string{{}, {"hover"}, {"hover", "active"}} {
232 forceState(t, ctx, sel, states...)
233 if bg, fg := computed(t, ctx, sel, "background-color"), computed(t, ctx, sel, "color"); bg != "rgba(0, 0, 0, 0)" || fg != "rgb(24, 24, 27)" {
234 t.Errorf("%s %v in print is %s on %s; want outlined text", sel, states, fg, bg)
235 }
236 forceState(t, ctx, sel)
237 }
238 }
239 if knob, ring := knobStyle(t, ctx, "background-color"), knobStyle(t, ctx, "outline-style"); knob != "rgb(24, 24, 27)" || ring != "solid" {
240 t.Errorf("the switch knob prints as %s with outline %s; want the text colour inside a solid ring", knob, ring)
241 }
242 // On and off must differ by more than the knob's end: the checked
243 // track (a switch's and a toggle block's) keeps the strong line.
244 if off, on, head := computed(t, ctx, "#sw [rst-switch-track]", "background-color"), computed(t, ctx, "#sw-on [rst-switch-track]", "background-color"), computed(t, ctx, "#tb [rst-switch-track]", "background-color"); off != "rgba(0, 0, 0, 0)" || on != line || head != line {
245 t.Errorf("tracks print off %s, on %s, toggle-block on %s; want an empty ring and two filled with %s", off, on, head, line)
246 }
247 // Hover tints: the row's, the tile's and the menu item's are tokens
248 // of their own, and print sets each to the surface or to nothing.
249 for _, sel := range []string{"#row-a", "#tile", "#menu-a"} {
250 forceState(t, ctx, sel, "hover")
251 if got := computed(t, ctx, sel, "background-color"); got != "rgba(0, 0, 0, 0)" && got != "rgb(255, 255, 255)" {
252 t.Errorf("hovered %s in print is tinted %s", sel, got)
253 }
254 forceState(t, ctx, sel)
255 }
256
257 _, nctx := emulatedRig(t, printMedia, 390, 844)
258 for _, sel := range []string{"#tnav-x", "#tnav-y", "#nav-y", "#inav-y", "#b-primary", "#card"} {
259 for _, states := range [][]string{{}, {"hover"}, {"hover", "active"}} {
260 flat(nctx, sel, states...)
261 }
262 }
263 // The card's hover is its own token, read only inside a phone-and-
264 // hover query; print sets it to nothing.
265 forceState(t, nctx, "#tnav-x", "hover")
266 if got := computed(t, nctx, "#tnav-x", "background-color"); got != "rgba(0, 0, 0, 0)" {
267 t.Errorf("a hovered topbar phone-card link in print is tinted %s", got)
268 }
269 forceState(t, nctx, "#tnav-x")
270 if got := computed(t, nctx, "#tnav-y", "background-color"); got != "rgba(0, 0, 0, 0)" {
271 t.Errorf("topbar phone-card current item in print is filled %s; want no fill", got)
272 }
273
274 // A fresh navigation on the first rig: open() sets the viewport
275 // before it navigates, so the hover query is on again.
276 open(t, ctx, rig, "/", false)
277 still(t, ctx)
278 var hov bool
279 mustRun(t, ctx, chromedp.Evaluate(`matchMedia("(hover: hover)").matches`, &hov))
280 if !hov {
281 t.Fatal("(hover: hover) is false after the second navigation; the dark press would be the tap rule alone")
282 }
283 mustRun(t, ctx, chromedp.Evaluate(`document.documentElement.dataset.theme = "dark", true`, nil))
284 flat(ctx, "#b-primary", "hover", "active")
285 flat(ctx, "#b-plain", "active")
286}
287