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// The snippet example as a reader meets it: a line of code in a box no
4// wider than the line on a desktop and no wider than the screen on a
5// phone, where the line wraps rather than run off the edge, still left
6// to right in a right-to-left page; with no script the Copy button is
7// gone, the code is selected whole by one click, and the status is a
8// live region before it ever speaks.
9package check
10
11import (
12 "context"
13 "fmt"
14 "math"
15 "testing"
16 "time"
17
18 "github.com/chromedp/cdproto/accessibility"
19 "github.com/chromedp/cdproto/cdp"
20 "github.com/chromedp/cdproto/emulation"
21 "github.com/chromedp/chromedp"
22
23 "amadan.net/rastrillo/fiesta"
24 "amadan.net/rastrillo/fiesta/example"
25)
26
27// snippetExampleDoc is the snippet example's one state as a document,
28// its assets flattened beside the page as the sizing mux serves them.
29func snippetExampleDoc(t *testing.T) string {
30 t.Helper()
31 state, ok := manifest(t).Default("snippet")
32 if !ok || state == "" {
33 t.Fatal("examples.json names no default state for snippet")
34 }
35 doc, err := example.Document(fiesta.FS(), "snippet", state)
36 if err != nil {
37 t.Fatal(err)
38 }
39 return parityLinks(doc)
40}
41
42// snippetPages are the example at / and right to left at /rtl.
43func snippetPages(t *testing.T) map[string]string {
44 t.Helper()
45 doc := snippetExampleDoc(t)
46 return map[string]string{"/": doc, "/rtl": inArabic(t, doc)}
47}
48
49// snippetGeometry is what the layout test reads of the box: its width
50// and its container's content width, the code's one-line width (a
51// clone of it set on one line), its height and line height, the
52// button's width with its inline margins when shown, the box's column
53// gap, its paddings and borders, the rect of the code's last character, the document's
54// scroll and client widths, and the code's direction.
55type snippetGeometry struct {
56 Box, BoxLeft, BoxRight, Container float64
57 Line, CodeHeight, LineHeight, Button, Gap float64
58 Chrome float64
59 ButtonShown bool
60 LastLeft, LastRight, ScrollWidth, ClientWidth float64
61 Direction string
62}
63
64const snippetGeometryJS = `(() => {
65 const box = document.querySelector("[rst-snippet]"), code = box.querySelector("code"),
66 b = box.querySelector("[rst-snippet-copy]"), bs = getComputedStyle(box), cs = getComputedStyle(code);
67 const parent = box.parentElement, ps = getComputedStyle(parent);
68 const clone = code.cloneNode(true);
69 clone.style.cssText = "position: absolute; visibility: hidden; white-space: nowrap; inline-size: max-content";
70 box.append(clone);
71 const line = clone.getBoundingClientRect().width;
72 clone.remove();
73 const text = code.firstChild, last = document.createRange();
74 last.setStart(text, text.length - 1);
75 last.setEnd(text, text.length);
76 const lr = last.getBoundingClientRect(), r = box.getBoundingClientRect();
77 const px = (v) => parseFloat(v) || 0;
78 const lh = cs.lineHeight === "normal" ? 1.2 * px(cs.fontSize) : px(cs.lineHeight);
79 return JSON.stringify({
80 Box: r.width, BoxLeft: r.left, BoxRight: r.right,
81 Container: parent.clientWidth - px(ps.paddingLeft) - px(ps.paddingRight),
82 Line: line, CodeHeight: code.getBoundingClientRect().height, LineHeight: lh,
83 ButtonShown: b.getClientRects().length > 0,
84 Button: b.getBoundingClientRect().width + px(getComputedStyle(b).marginLeft) + px(getComputedStyle(b).marginRight),
85 Gap: px(bs.columnGap),
86 Chrome: px(bs.paddingLeft) + px(bs.paddingRight) + px(bs.borderLeftWidth) + px(bs.borderRightWidth),
87 LastLeft: lr.left, LastRight: lr.right,
88 ScrollWidth: document.documentElement.scrollWidth, ClientWidth: document.documentElement.clientWidth,
89 Direction: cs.direction});
90})()`
91
92// TestTheSnippetHugsItsLineAndWrapsOnAPhone: at 1280 the box is as
93// wide as the line of code, the Copy button and the box's own gap,
94// padding and border, with the code on one line, so a short line does
95// not sit in a box the width of the page. At 390 and 320 the box is no
96// wider than its container and the code wraps inside it, its last
97// character still in the box and the page never wider than the
98// screen: the line a reader is told to copy is all on screen. Right to
99// left, the code still reads left to right.
100func TestTheSnippetHugsItsLineAndWrapsOnAPhone(t *testing.T) {
101 rig := sizingRig(t, false, snippetPages(t))
102 ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
103 defer cancel()
104 grantClipboard(t, ctx, rig.Origin)
105 for _, w := range []int64{1280, 390, 320} {
106 for _, path := range []string{"/", "/rtl"} {
107 where := fmt.Sprintf("%s at %d", path, w)
108 mustRun(t, ctx, chromedp.EmulateViewport(w, 800), chromedp.Navigate(rig.Origin+path), chromedp.WaitReady("body", chromedp.ByQuery))
109 settleUntil(t, ctx, `document.readyState === "complete"`)
110 var g snippetGeometry
111 at(t, ctx, snippetGeometryJS, &g)
112 if !g.ButtonShown {
113 t.Fatalf("%s: the Copy button is not shown with a clipboard; the widths below would leave it out", where)
114 }
115 if g.Direction != "ltr" {
116 t.Errorf("%s: the code's direction is %s; code reads left to right in any page", where, g.Direction)
117 }
118 if w == 1280 {
119 want := g.Line + g.Gap + g.Button + g.Chrome
120 if math.Abs(g.Box-want) > 2 {
121 t.Errorf("%s: the box is %.1fpx wide; want %.1fpx, the line (%.1f), the gap (%.1f), the button (%.1f) and the padding and border (%.1f)", where, g.Box, want, g.Line, g.Gap, g.Button, g.Chrome)
122 }
123 if g.CodeHeight > 1.5*g.LineHeight {
124 t.Errorf("%s: the code is %.1fpx tall, its line %.1fpx; at this width it fits on one line", where, g.CodeHeight, g.LineHeight)
125 }
126 continue
127 }
128 if g.Box > g.Container+0.5 {
129 t.Errorf("%s: the box is %.1fpx wide in a container of %.1fpx", where, g.Box, g.Container)
130 }
131 if g.CodeHeight < 1.5*g.LineHeight {
132 t.Errorf("%s: the code is %.1fpx tall, its line %.1fpx; a line this long must wrap on a phone", where, g.CodeHeight, g.LineHeight)
133 }
134 if g.LastLeft < g.BoxLeft-0.5 || g.LastRight > g.BoxRight+0.5 {
135 t.Errorf("%s: the code's last character spans %.1f to %.1f, outside the box's %.1f to %.1f", where, g.LastLeft, g.LastRight, g.BoxLeft, g.BoxRight)
136 }
137 if g.ScrollWidth > g.ClientWidth {
138 t.Errorf("%s: the page is %.0fpx wide in a %.0fpx viewport; the code must wrap, not scroll sideways", where, g.ScrollWidth, g.ClientWidth)
139 }
140 }
141 }
142}
143
144// statusInTheTree says whether the element sel finds is a status node
145// in the accessibility tree: a live region that is not in the tree
146// when its first message arrives may never be announced.
147func statusInTheTree(t *testing.T, ctx context.Context, sel string) bool {
148 t.Helper()
149 var nodes []*cdp.Node
150 mustRun(t, ctx, chromedp.Nodes(sel, &nodes, chromedp.ByQuery))
151 // Read loosely: cdproto's Node refuses an ignored reason it has no
152 // constant for (Chromium's "uninteresting"), and this needs only
153 // the role, the ignored flag and the DOM node.
154 var tree struct {
155 Nodes []struct {
156 Ignored bool `json:"ignored"`
157 Role *struct {
158 Value any `json:"value"`
159 } `json:"role"`
160 BackendDOMNodeID cdp.BackendNodeID `json:"backendDOMNodeId"`
161 } `json:"nodes"`
162 }
163 mustRun(t, ctx, accessibility.Enable(), chromedp.ActionFunc(func(ctx context.Context) error {
164 return cdp.Execute(ctx, accessibility.CommandGetFullAXTree, accessibility.GetFullAXTree(), &tree)
165 }))
166 if len(tree.Nodes) == 0 {
167 t.Fatal("the accessibility tree came back empty; the read is broken, not the status")
168 }
169 for _, n := range tree.Nodes {
170 if n.BackendDOMNodeID == nodes[0].BackendNodeID {
171 return !n.Ignored && n.Role != nil && n.Role.Value == "status"
172 }
173 }
174 return false
175}
176
177// TestTheSnippetWithNoScript: with scripts off there is no Copy
178// button to press and do nothing; the code is selected whole by one
179// click or tap (user-select: all), so it can still be copied by hand;
180// and the empty status takes no room, though it is a status node in
181// the accessibility tree, clipped rather than display: none.
182func TestTheSnippetWithNoScript(t *testing.T) {
183 rig := sizingRig(t, false, snippetPages(t))
184 ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
185 defer cancel()
186 mustRun(t, ctx, emulation.SetScriptExecutionDisabled(true))
187 for _, path := range []string{"/", "/rtl"} {
188 mustRun(t, ctx, chromedp.EmulateViewport(1280, 800), chromedp.Navigate(rig.Origin+path), chromedp.WaitReady("body", chromedp.ByQuery))
189 var r struct {
190 Ran, Shown bool
191 Width, Height float64
192 UserSelect, WebkitSelect string
193 }
194 at(t, ctx, `(() => {
195 const box = document.querySelector("[rst-snippet]"), s = box.querySelector("[rst-snippet-status]").getBoundingClientRect(),
196 cs = getComputedStyle(box.querySelector("code"));
197 return JSON.stringify({Ran: window.fiestaCopy === true, Shown: box.querySelector("[rst-snippet-copy]").getClientRects().length > 0,
198 Width: s.width, Height: s.height, UserSelect: cs.userSelect, WebkitSelect: cs.webkitUserSelect}); })()`, &r)
199 if r.Ran {
200 t.Fatalf("%s: copy.js ran with scripts off; this leg is not the page a reader with no script gets", path)
201 }
202 if r.Shown {
203 t.Errorf("%s: with no script the Copy button is displayed; it could only do nothing", path)
204 }
205 if r.Width > 1 || r.Height > 1 {
206 t.Errorf("%s: the empty status is %.1f by %.1f; while it has nothing to say it takes no room", path, r.Width, r.Height)
207 }
208 if r.UserSelect != "all" && r.WebkitSelect != "all" {
209 t.Errorf("%s: the code's user-select is %q (-webkit- %q); want all, so one click selects the whole line to copy by hand", path, r.UserSelect, r.WebkitSelect)
210 }
211 if !statusInTheTree(t, ctx, "[rst-snippet-status]") {
212 t.Errorf("%s: the empty status is not a status node in the accessibility tree", path)
213 }
214 }
215}
216
217// TestTheSnippetExampleCopies: the gallery's own snippet document takes
218// driveCopy's journey, so what the gallery shows is markup copy.js
219// works on, with the example's own strings.
220func TestTheSnippetExampleCopies(t *testing.T) {
221 rig := sizingRig(t, false, snippetPages(t))
222 ctx, cancel := context.WithTimeout(rig.Context(), 60*time.Second)
223 defer cancel()
224 driveCopy(t, ctx, rig.Origin, "/")
225 rig.Screen("body", "the snippet example after two copies")
226}
227