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
3// A seg-tabs track made of buttons, for a choice that changes the page
4// in place: each button must be the tab a link is, or a page that
5// mixes the two, or moves from one to the other, shows two controls.
6package check
7
8import (
9 "context"
10 "fmt"
11 "net/http"
12 "strings"
13 "testing"
14 "time"
15
16 "github.com/chromedp/cdproto/emulation"
17 "github.com/chromedp/chromedp"
18
19 "amadan.net/rastrillo/fiesta"
20 "amadan.net/rastrillo/fiesta/internal/harness"
21)
22
23// segTabsTrack is one track holding the current link, a plain link,
24// the pressed button, an unpressed one and a disabled one, so each
25// button is read
26// beside the link it must look like, under the same rules.
27const segTabsTrack = `<main rst-page><div rst-seg-tabs role="group" aria-label="View">` +
28 `<a id="current" href="#list" aria-current="page">List</a><a id="link" href="#grid">Grid</a>` +
29 `<button id="pressed" type="button" aria-pressed="true">List</button><button id="unpressed" type="button" aria-pressed="false">Grid</button>` +
30 `<button id="disabled" type="button" aria-pressed="false" disabled>Map</button>` +
31 `</div></main>`
32
33// segTabsPages serves the track at /<theme>/<scheme>/ under tokens.css
34// and that theme, with no script, so nothing but the stylesheets
35// decides what each tab looks like.
36func segTabsPages(t *testing.T) http.Handler {
37 t.Helper()
38 mux := http.NewServeMux()
39 mux.HandleFunc("GET /tokens.css", func(w http.ResponseWriter, _ *http.Request) {
40 w.Header().Set("Content-Type", "text/css")
41 w.Write(fiesta.TokensCSS())
42 })
43 mux.HandleFunc("GET /theme/{name}", func(w http.ResponseWriter, r *http.Request) {
44 css, ok := fiesta.ThemeCSS(strings.TrimSuffix(r.PathValue("name"), ".css"))
45 if !ok {
46 http.NotFound(w, r)
47 return
48 }
49 w.Header().Set("Content-Type", "text/css")
50 w.Write(css)
51 })
52 mux.HandleFunc("GET /{theme}/{scheme}/", func(w http.ResponseWriter, r *http.Request) {
53 w.Header().Set("Content-Type", "text/html; charset=utf-8")
54 fmt.Fprintf(w, `<!doctype html><html lang="en" dir="ltr" data-theme=%q><head><meta charset="utf-8">`+
55 `<meta name="viewport" content="width=device-width"><title>seg-tabs</title>`+
56 `<link rel="stylesheet" href="/tokens.css"><link rel="stylesheet" href="/theme/%s.css">`+
57 `</head><body>%s</body></html>`, r.PathValue("scheme"), r.PathValue("theme"), segTabsTrack)
58 })
59 return mux
60}
61
62// segTabFace is what a reader sees of a tab: its fill, its text and
63// its shape, the box a finger or a pointer has to hit, and the display
64// that centres its label in that box on a phone.
65var segTabFace = []string{"background-color", "color", "font-family", "font-size", "font-weight", "line-height", "padding", "border-radius", "box-shadow", "block-size", "min-block-size", "min-inline-size", "display", "align-items", "justify-content", "cursor"}
66
67// sameFace reports each property of segTabFace in which button differs
68// from link.
69func sameFace(t *testing.T, ctx context.Context, where, button, link string) {
70 t.Helper()
71 for _, prop := range segTabFace {
72 if b, l := computed(t, ctx, button, prop), computed(t, ctx, link, prop); b != l {
73 t.Errorf("%s: %s's %s is %q, %s's is %q; a button in a track must be the tab a link is", where, button, prop, b, link, l)
74 }
75 }
76}
77
78// TestASegTabButtonLooksLikeASegTabLink: in every theme and scheme,
79// with a mouse and on a touch screen, the pressed button is the thumb
80// the current link is, and an unpressed button is a plain tab, with
81// no border and no fill of the browser's own; hovered, it turns the
82// colour a hovered link does. Under forced colours and in print the
83// pressed button keeps the current link's system colours and loses
84// its lift the same way, or the choice made would vanish there.
85func TestASegTabButtonLooksLikeASegTabLink(t *testing.T) {
86 for _, coarse := range []bool{false, true} {
87 // A mouse on a desktop meets the desktop floor, a finger on a
88 // phone the touch floor and its centring: each is a rule the
89 // buttons must share with the links.
90 pointer, width := "mouse at 1280", int64(1280)
91 var opts []harness.Option
92 if coarse {
93 pointer, width = "touch at 390", 390
94 opts = append(opts, harness.WithCoarsePointer())
95 }
96 rig := harness.New(t, func(string) http.Handler { return segTabsPages(t) }, opts...)
97 ctx, cancel := context.WithTimeout(rig.Context(), 90*time.Second)
98 mustRun(t, ctx, chromedp.EmulateViewport(width, 844))
99 for _, theme := range fiesta.ThemeNames() {
100 for _, scheme := range []string{"light", "dark"} {
101 where := fmt.Sprintf("%s %s, %s", theme, scheme, pointer)
102 mustRun(t, ctx, chromedp.Navigate(fmt.Sprintf("%s/%s/%s/", rig.Origin, theme, scheme)), chromedp.WaitVisible("#unpressed", chromedp.ByQuery))
103 requirePointer(t, ctx, coarse)
104 still(t, ctx)
105 sameFace(t, ctx, where, "#pressed", "#current")
106 sameFace(t, ctx, where, "#unpressed", "#link")
107 if w := computed(t, ctx, "#unpressed", "border-width"); w != "0px" {
108 t.Errorf("%s: the unpressed button's border is %s wide; the browser's button edge must go", where, w)
109 }
110 if bg := computed(t, ctx, "#unpressed", "background-color"); bg != "rgba(0, 0, 0, 0)" {
111 t.Errorf("%s: the unpressed button is filled %s; only the pressed one wears the thumb", where, bg)
112 }
113 rest := computed(t, ctx, "#link", "color")
114 forceState(t, ctx, "#link", "hover")
115 forceState(t, ctx, "#unpressed", "hover")
116 hovered := computed(t, ctx, "#link", "color")
117 if hovered == rest {
118 t.Errorf("%s: CONTROL: a hovered link is %s, the colour it rests in; the hover below cannot be told from rest", where, rest)
119 }
120 if b := computed(t, ctx, "#unpressed", "color"); b != hovered {
121 t.Errorf("%s: a hovered unpressed button is %s, a hovered link %s", where, b, hovered)
122 }
123 forceState(t, ctx, "#link")
124 forceState(t, ctx, "#unpressed")
125 disabledLooksIdle(t, ctx, where, rest)
126 }
127 }
128 cancel()
129 }
130
131 // Forced colours and print, in Day light, where the axis is
132 // flattened and the current tab is drawn by rules of its own.
133 rig := harness.New(t, func(string) http.Handler { return segTabsPages(t) })
134 ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
135 defer cancel()
136 for _, medium := range []struct {
137 name, query string
138 emulate *emulation.SetEmulatedMediaParams
139 }{
140 {"forced colours", "(forced-colors: active)", emulation.SetEmulatedMedia().WithFeatures([]*emulation.MediaFeature{{Name: "forced-colors", Value: "active"}})},
141 {"print", "print", emulation.SetEmulatedMedia().WithMedia("print")},
142 } {
143 mustRun(t, ctx, chromedp.EmulateViewport(1280, 800), medium.emulate, chromedp.Navigate(rig.Origin+"/day/light/"), chromedp.WaitVisible("#pressed", chromedp.ByQuery))
144 var on bool
145 at(t, ctx, `JSON.stringify(matchMedia(`+jsStr(medium.query)+`).matches)`, &on)
146 if !on {
147 t.Fatalf("%s: matchMedia(%q) is false; this leg is not the medium it claims", medium.name, medium.query)
148 }
149 still(t, ctx)
150 for _, prop := range []string{"background-color", "color", "box-shadow"} {
151 if b, l := computed(t, ctx, "#pressed", prop), computed(t, ctx, "#current", prop); b != l {
152 t.Errorf("%s: the pressed button's %s is %q, the current link's %q", medium.name, prop, b, l)
153 }
154 }
155 mustRun(t, ctx, emulation.SetEmulatedMedia())
156 }
157}
158
159// disabledLooksIdle: a disabled button says it is not listening, as
160// Fiesta's other disabled controls do: faint text, the default cursor,
161// and no answer to a hover. With the enabled tab's colour, pointer and
162// hover it read as a choice that would work, and a click did nothing.
163// rest is a plain link's resting colour, which the faint text must not
164// be, or the check below could not tell the two apart.
165func disabledLooksIdle(t *testing.T, ctx context.Context, where, rest string) {
166 t.Helper()
167 var faint string
168 at(t, ctx, `(() => { const p = document.createElement("span"); p.style.color = "var(--rst-text-faint)"; document.body.append(p); const c = getComputedStyle(p).color; p.remove(); return JSON.stringify(c); })()`, &faint)
169 faint = canonColours(faint)
170 if faint == rest {
171 t.Errorf("%s: CONTROL: the faint text colour is %s, a plain tab's own; a disabled button cannot be told from an enabled one by colour", where, faint)
172 }
173 if c := computed(t, ctx, "#disabled", "color"); c != faint {
174 t.Errorf("%s: a disabled button's text is %s, want the faint text colour %s", where, c, faint)
175 }
176 if c := computed(t, ctx, "#disabled", "cursor"); c != "default" {
177 t.Errorf("%s: a disabled button's cursor is %q, want \"default\": a pointer promises a click that does nothing", where, c)
178 }
179 forceState(t, ctx, "#disabled", "hover", "active")
180 if c := computed(t, ctx, "#disabled", "color"); c != faint {
181 t.Errorf("%s: a hovered, pressed disabled button turns %s; it must stay %s", where, c, faint)
182 }
183 forceState(t, ctx, "#disabled")
184}
185