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 gallery
4
5import (
6 "context"
7 "fmt"
8 "net/http"
9 "os"
10 "sort"
11 "strings"
12 "testing"
13 "time"
14
15 "github.com/chromedp/chromedp"
16
17 "amadan.net/rastrillo/fiesta/internal/gallery/galleryrig"
18 "amadan.net/rastrillo/fiesta/internal/harness"
19)
20
21// The heights in heights.go are measurements, and this is where they
22// are measured: every frame on every example page is asked what its
23// document needs and held to the box the renderer gave it, at Desktop
24// and with Mobile chosen, at a desktop window and at the narrowest
25// phone. A frame smaller than its content is a sample a reader has to
26// scroll inside a box the size of a paragraph.
27//
28// heightsEnforced is the switch between reporting and gating. The
29// table is calibrated now the shell, the frame padding, the stage and
30// the demo are all in, so a box short of its content, at rest or with
31// what opens in it open, fails the drive by name. The self-check stays:
32// a box made 100px short of its content by hand has to be reported
33// short, which proves the measurement.
34const heightsEnforced = true
35
36// sweepFull is the opt-in for the whole table: every locale rather than
37// the routine few, and the table printed as Go source.
38func sweepFull() bool {
39 switch v := os.Getenv("FIESTA_SWEEP"); v {
40 case "":
41 return false
42 case "full":
43 return true
44 default:
45 panic(fmt.Sprintf("FIESTA_SWEEP=%q: the only value is \"full\"", v))
46 }
47}
48
49// wideLocales is the desktop drive's languages: English, or all twelve
50// under the sweep.
51func wideLocales() []string {
52 if sweepFull() {
53 return Locales
54 }
55 return []string{"en"}
56}
57
58// narrowLocales is the 320px drive's languages: English, the one
59// right-to-left locale and the two that wrap longest, or all twelve
60// under the sweep.
61func narrowLocales() []string {
62 if sweepFull() {
63 return Locales
64 }
65 return []string{"en", "ar", "ru", "ga"}
66}
67
68// frameKey is the JS that keys a frame as heights.go keys it: the
69// <example>/<state> its src names under frames/, demo for the demo
70// application, or "?" for anything else.
71const frameKey = `const frameKey = f => {
72 const src = f.getAttribute("src") || "";
73 const m = /\/frames\/([^/]+)\/([^/]+)\.html$/.exec(src);
74 if (m) return m[1] + "/" + m[2];
75 return /\/demo\.html$/.test(src) ? "demo" : "?";
76};`
77
78// measureHeights reads every frame on the page, keyed by frameKey: what
79// its document needs in its own pixels, what its box gives it, which is
80// --ds-h or --ds-hm resolved, since the frame takes its height back off
81// the box in virtual pixels, and whether it frames a sidebar shell,
82// whose rail is 100dvh tall. A widget view.js has marked at rest
83// (data-ds-rest) gives its rest height as Box, and is unmarked for a
84// moment to read the height it opens to as Open; any other's Open is
85// its Box.
86const measureHeights = `(() => {
87 ` + frameKey + `
88 const out = {};
89 for (const f of document.querySelectorAll(".ds-view__frame")) {
90 const key = frameKey(f);
91 let d = null;
92 try { d = f.contentDocument; } catch (e) {}
93 const need = d && d.body ? Math.ceil(Math.max(d.body.getBoundingClientRect().height, d.body.scrollHeight)) : -1;
94 const shell = !!(d && d.querySelector("[rst-shell-sidebar]"));
95 const viewport = !!(d && d.querySelector("[rst-shell-sidebar], [rst-shell-topbar], [rst-shell-console], [rst-stage]"));
96 const view = f.closest(".ds-view"), rest = view.hasAttribute("data-ds-rest"), box = Math.round(parseFloat(getComputedStyle(f).height));
97 let open = box;
98 if (rest) {
99 view.removeAttribute("data-ds-rest");
100 open = Math.round(parseFloat(getComputedStyle(f).height));
101 view.setAttribute("data-ds-rest", "");
102 }
103 out[key] = {Need: need, Box: box, Open: open, Rest: rest, Shell: shell, Viewport: viewport};
104 }
105 return JSON.stringify(out);
106})()`
107
108type heightReading struct {
109 Need, Box int
110 // Open is the box while something in the frame is open: Box, but
111 // for a widget with a rest height (previewRestHeights), which Rest
112 // says view.js had marked at rest when Box was read.
113 Open int
114 Rest bool
115 // Shell is a frame holding a sidebar shell, whose rail is 100dvh;
116 // Viewport any document as tall as its frame (a shell, the stage),
117 // which reads its own box when its content fits.
118 Shell, Viewport bool
119}
120
121// readHeights decodes one measurement, failing on what makes it
122// worthless: no frame, or a frame with no document in it.
123func readHeights(t *testing.T, ctx context.Context, where string) map[string]heightReading {
124 t.Helper()
125 var got map[string]heightReading
126 at(t, ctx, measureHeights, &got)
127 if len(got) == 0 {
128 t.Fatalf("%s: no frame on the page", where)
129 }
130 for key, r := range got {
131 if r.Need < 0 {
132 t.Fatalf("%s %s: the frame has no document in it", where, key)
133 }
134 // A rest height the drive never sees in force is a box nobody
135 // measured: the rest readings would be of the open box.
136 if _, rest := previewRestHeights[key]; rest != r.Rest {
137 t.Fatalf("%s %s: view.js has the widget at rest %v, and previewRestHeights says it has a rest height %v; the readings would be of the wrong box", where, key, r.Rest, rest)
138 }
139 if strings.HasPrefix(key, "?") {
140 t.Fatalf("%s: a frame's src names no document under frames/ and is not the demo", where)
141 }
142 }
143 return got
144}
145
146// measureOpen is what a box has to hold for what a reader opens in it:
147// a menu (a dropdown, a row menu, the language and scheme menus), the
148// enhanced select's list, the date and time fields' calendar or clock.
149// Every widget's box is first made 1600px tall, because dvh inside a
150// frame is the frame, and tokens.css caps a menu at
151// min(20rem, max(100dvh - 6rem, 8rem)): measured in its own box, a
152// menu would shrink to fit and always read as fitting. Then each thing
153// that opens is opened in turn, the bottom of everything visible in the
154// document read, the body's own padding added under it, and the thing
155// closed again; the boxes get their heights back at the end. A frame
156// whose document is a whole page (a shell, the stage, a backdrop) is
157// 100dvh tall and is left out: its menus open inside it.
158const measureOpen = `(async (prop) => {
159 ` + frameKey + `
160 const settle = (w) => new Promise(r => w.requestAnimationFrame(() => w.requestAnimationFrame(() => setTimeout(r, 30))));
161 const views = [...document.querySelectorAll(".ds-view")];
162 const kept = views.map(v => v.style.getPropertyValue(prop));
163 views.forEach(v => v.style.setProperty(prop, "1600px"));
164 await settle(window);
165 // What a modal's panel holds: its own height, or, where something in
166 // it scrolls, the height that thing would need unclipped, plus the
167 // overlay's padding round the panel.
168 const panelNeed = (d, w, panel) => {
169 const top = panel.getBoundingClientRect().top;
170 let h = Math.max(panel.getBoundingClientRect().height, panel.scrollHeight);
171 for (const e of panel.querySelectorAll("*")) {
172 if (!/auto|scroll|hidden|clip/.test(w.getComputedStyle(e).overflowY)) continue;
173 const r = e.getBoundingClientRect();
174 h = Math.max(h, r.top - top + e.scrollHeight + (panel.getBoundingClientRect().bottom - r.bottom));
175 }
176 const m = w.getComputedStyle(panel.closest("[rst-modal-overlay]") || d.body);
177 return Math.ceil(h + (parseFloat(m.paddingTop) || 0) + (parseFloat(m.paddingBottom) || 0));
178 };
179 // Whether a modal's panel clips anything at the box it has: the
180 // panel, or anything in it that scrolls, holding more than it shows.
181 const panelClipped = (w, panel) => [panel, ...panel.querySelectorAll("*")].some(e =>
182 (e === panel || /auto|scroll|hidden|clip/.test(w.getComputedStyle(e).overflowY)) && e.scrollHeight > e.clientHeight + 1);
183 const out = {};
184 const modals = [];
185 for (const f of document.querySelectorAll(".ds-view__frame")) {
186 const d = f.contentDocument;
187 if (!d || !d.body) continue;
188 // A modal is laid out on a fixed overlay the size of the frame, so
189 // what it asks of the box is its panel unclipped; the page behind
190 // it is the backdrop's and reads as content at rest.
191 const panel = d.querySelector("[rst-modal-panel]");
192 if (panel) {
193 const w = d.defaultView;
194 await settle(w);
195 const h = Math.ceil(d.body.getBoundingClientRect().height);
196 out[frameKey(f)] = {Need: panelNeed(d, w, panel), What: "modal panel", Closed: h, After: h};
197 modals.push([f, w, panel]);
198 continue;
199 }
200 if (d.querySelector("[rst-shell-sidebar], [rst-shell-topbar], [rst-shell-console], [rst-stage], [rst-backdrop]")) continue;
201 const w = d.defaultView;
202 const opens = [];
203 for (const det of d.querySelectorAll("details[rst-dropdown], details[rst-row-menu]")) {
204 if (!det.parentElement.closest("details")) opens.push(["menu", det]);
205 }
206 for (const input of d.querySelectorAll("[rst-combo] input[role=combobox]")) opens.push(["select", input]);
207 for (const pick of d.querySelectorAll("[rst-dtp] [rst-dtp-pick]")) opens.push(["picker", pick]);
208 if (opens.length === 0) continue;
209 await settle(w);
210 const pad = parseFloat(w.getComputedStyle(d.body).paddingBottom) || 0;
211 let need = 0, what = "";
212 const closed = Math.ceil(d.body.getBoundingClientRect().height);
213 for (const [kind, el] of opens) {
214 if (kind === "menu") el.open = true;
215 else if (kind === "select") { el.focus(); el.click(); }
216 else el.click();
217 await settle(w);
218 // A sheet pinned to the frame's foot (the calendar on a narrow
219 // frame: fixed, and not anchored to anything) is as tall as its
220 // content up to the frame's height, so what it asks of the box is
221 // its own content and its margins, and what lies inside it is not
222 // read again. A menu anchored to its button is fixed too, and is
223 // read where it lies, as is everything else; but what scrolls
224 // inside a menu or a list is read as the box that clips it.
225 const all = [...d.body.querySelectorAll("*")];
226 const sheets = all.filter(e => {
227 const m = w.getComputedStyle(e);
228 return m.position === "fixed" && (!m.positionArea || m.positionArea === "none") && (!e.checkVisibility || e.checkVisibility());
229 });
230 const clips = all.filter(e => /auto|scroll|hidden|clip/.test(w.getComputedStyle(e).overflowY));
231 let bottom = d.body.getBoundingClientRect().bottom - pad, by = "body";
232 for (const e of all) {
233 if (e.checkVisibility && !e.checkVisibility()) continue;
234 if (sheets.some(x => x.contains(e)) || clips.some(c => c !== e && c.contains(e))) continue;
235 const r = e.getBoundingClientRect();
236 if (r.height > 0 && r.bottom > bottom) { bottom = r.bottom; by = e.tagName.toLowerCase() + [...e.attributes].filter(a => a.name.startsWith("rst-")).map(a => "[" + a.name + "]").join(""); }
237 }
238 let n = Math.ceil(bottom + pad + w.scrollY);
239 for (const x of sheets) {
240 const sheet = Math.ceil(x.scrollHeight + 2 * (parseFloat(w.getComputedStyle(x).bottom) || 0));
241 if (sheet > n) { n = sheet; by = "a sheet"; }
242 }
243 if (n > need) { need = n; what = kind + " (" + by + ")"; }
244 if (kind === "menu") el.open = false;
245 else {
246 const input = el.closest("[rst-dtp], [rst-combo]").querySelector("input[role=combobox]");
247 input.dispatchEvent(new KeyboardEvent("keydown", {key: "Escape", bubbles: true, cancelable: true}));
248 input.blur();
249 }
250 await settle(w);
251 }
252 const after = Math.ceil(d.body.getBoundingClientRect().height);
253 out[frameKey(f)] = {Need: need, What: what, Closed: closed, After: after};
254 }
255 views.forEach((v, i) => kept[i] ? v.style.setProperty(prop, kept[i]) : v.style.removeProperty(prop));
256 await settle(window);
257 // Back at the boxes the table gives them, the modal panels are read
258 // for clipping: a panel that scrolls in its own box is a modal a
259 // reader sees cut short, whatever the arithmetic above says.
260 for (const [f, w, panel] of modals) {
261 await settle(w);
262 out[frameKey(f)].Clipped = panelClipped(w, panel);
263 }
264 return JSON.stringify(out);
265})`
266
267type openReading struct {
268 Need int
269 What string
270 Closed, After int
271 // Clipped is set for a modal whose panel, back in the box the
272 // table gives it, holds more than it shows.
273 Clipped bool
274}
275
276// readOpen measures what each frame's box must hold with its menus and
277// panels open, at the view prop names (--ds-h for Desktop, --ds-hm for
278// Mobile), and fails if a frame's document is not back to its closed
279// height afterwards: a panel left open would be read as content by the
280// next measurement.
281func readOpen(t *testing.T, ctx context.Context, where, prop string) map[string]openReading {
282 t.Helper()
283 var got map[string]openReading
284 at(t, ctx, fmt.Sprintf("%s(%q)", measureOpen, prop), &got, awaitPromise)
285 for key, o := range got {
286 if o.After > o.Closed+1 {
287 t.Fatalf("%s %s: %dpx before its %s was opened and %dpx after it was closed; something was left open", where, key, o.Closed, o.What, o.After)
288 }
289 }
290 return got
291}
292
293// shortfall reports one frame's box against its content: an error when
294// the heights are enforced, a line in the log until then. grace is the
295// slack the box is allowed under its content: Rastrillo's 48px for a
296// sidebar shell's 100dvh rail, which no box can fit, and 0 for the rest
297// where a drive grants it to shells alone.
298func shortfall(t *testing.T, where, key string, r heightReading, grace int) bool {
299 t.Helper()
300 if r.Need <= r.Box+grace {
301 if r.Box > r.Need*4 && r.Box-r.Need > 120 {
302 t.Logf("%s %s: %dpx of frame for %dpx of document; deliberate headroom, or a number to bring down", where, key, r.Box, r.Need)
303 }
304 return false
305 }
306 msg := fmt.Sprintf("%s %s: its document needs %dpx and its frame is %dpx; raise its height (previewHeights for Desktop, previewMobileHeights or previewMobileLocaleHeights for Mobile, or its previewRestHeights entry if it has one and nothing is open) to at least %d", where, key, r.Need, r.Box, r.Need+20)
307 if heightsEnforced {
308 t.Error(msg)
309 } else {
310 t.Log("not enforced yet: " + msg)
311 }
312 return true
313}
314
315// openShortfall reports one frame's box against what it must hold open:
316// no grace, since a menu cut short is a menu a reader cannot use; and a
317// modal whose panel is clipped in its box, read directly.
318func openShortfall(t *testing.T, where, key string, open openReading, box int) bool {
319 t.Helper()
320 short := shortfall(t, where+" with its "+open.What+" open", key, heightReading{Need: open.Need, Box: box}, 0)
321 if open.Clipped {
322 msg := fmt.Sprintf("%s %s: the modal's panel is clipped in a frame of %dpx; it needs %dpx to show whole", where, key, box, open.Need)
323 if heightsEnforced {
324 t.Error(msg)
325 } else {
326 t.Log("not enforced yet: " + msg)
327 }
328 short = true
329 }
330 return short
331}
332
333// selfCheck proves the measurement can see a short box: every widget's
334// prop (--ds-h or --ds-hm) is set 100px under what its document needs,
335// and the reading must then report the shortfall.
336func selfCheck(t *testing.T, ctx context.Context, where, prop string, before map[string]heightReading) {
337 t.Helper()
338 for key, r := range before {
339 mustRun(t, ctx, chromedp.Evaluate(fmt.Sprintf(`document.querySelectorAll(".ds-view").forEach(v => v.style.setProperty(%q, "%dpx")); true`, prop, r.Need-100), nil))
340 after := readHeights(t, ctx, where+", self-check")[key]
341 if after.Need <= after.Box {
342 t.Fatalf("%s: with %s set to %dpx under its content, %s reads need %d in a box of %d; the measurement cannot see a short box", where, prop, 100, key, after.Need, after.Box)
343 }
344 t.Logf("%s: self-check, %s %dpx short reads need %d in a box of %d", where, prop, 100, after.Need, after.Box)
345 return
346 }
347}
348
349// heightPages is every page that frames something: the Overview, for
350// the demo application, then every example's page in manifest order.
351func heightPages(t *testing.T) []string {
352 t.Helper()
353 out := []string{"index.html"}
354 for _, e := range manifest(t).Examples {
355 out = append(out, e.Name+".html")
356 }
357 return out
358}
359
360// printTable writes the readings out as Go source: one Desktop height
361// per key, the tallest over the locales read plus slack, and the
362// Mobile entries where the derived 1.25x does not cover a reading.
363//
364// It is written so that a second sweep after pasting the first changes
365// nothing. A document as tall as the frame it is in (a shell, the
366// stage) measures its own box when its content fits, so slack on that
367// reading would grow the box on every sweep; such a key keeps the
368// reservation the table already gives it, unless a reading found its
369// content taller than its box (fit says which keys fitted every time).
370// Mobile entries the table already has are kept unless a reading needs
371// more: the 320px drive raises them and prints no table, so the wide
372// printout carrying them is what keeps a paste from dropping them.
373// previewReserved is never printed: it is a floor, not a measurement.
374func printTable(t *testing.T, desk, mob map[string]map[string]int, deskFit, mobFit map[string]bool) {
375 t.Helper()
376 keys := map[string]bool{}
377 for _, m := range desk {
378 for k := range m {
379 keys[k] = true
380 }
381 }
382 var sorted []string
383 for k := range keys {
384 sorted = append(sorted, k)
385 }
386 sort.Strings(sorted)
387 // Slack, then rounded up to the next ten: twelve languages and three
388 // type families do not lay out to the same pixel.
389 up := func(n int) int { return (n + 20 + 9) / 10 * 10 }
390 tallest := func(m map[string]map[string]int, k string) int {
391 n := 0
392 for _, byKey := range m {
393 n = max(n, byKey[k])
394 }
395 return n
396 }
397 var b strings.Builder
398 b.WriteString("var previewHeights = map[string]int{\n")
399 h := map[string]int{}
400 for _, k := range sorted {
401 cur, ok := previewHeights[k]
402 if !ok {
403 cur = previewHeight
404 }
405 h[k] = up(tallest(desk, k))
406 if deskFit[k] {
407 h[k] = cur
408 }
409 fmt.Fprintf(&b, "\t%q: %d,\n", k, h[k])
410 }
411 b.WriteString("}\nvar previewMobileHeights = map[string]int{\n")
412 mh := map[string]int{}
413 for _, k := range sorted {
414 m := 0
415 if n := mob["en"][k]; n > h[k]*5/4 && !mobFit[k] {
416 m = up(n)
417 }
418 m = max(m, previewMobileHeights[k])
419 if m > h[k]*5/4 {
420 mh[k] = m
421 fmt.Fprintf(&b, "\t%q: %d,\n", k, m)
422 }
423 }
424 b.WriteString("}\nvar previewMobileLocaleHeights = map[string]map[string]int{\n")
425 locales := map[string]bool{}
426 for l := range mob {
427 if l != "en" {
428 locales[l] = true
429 }
430 }
431 for l := range previewMobileLocaleHeights {
432 locales[l] = true
433 }
434 var sortedLocales []string
435 for l := range locales {
436 sortedLocales = append(sortedLocales, l)
437 }
438 sort.Strings(sortedLocales)
439 for _, l := range sortedLocales {
440 var lines []string
441 for _, k := range sorted {
442 base := max(h[k]*5/4, mh[k])
443 n := previewMobileLocaleHeights[l][k]
444 if r := mob[l][k]; r > base && !mobFit[k] {
445 n = max(n, up(r))
446 }
447 if n > base {
448 lines = append(lines, fmt.Sprintf("\t\t%q: %d,", k, n))
449 }
450 }
451 if len(lines) > 0 {
452 fmt.Fprintf(&b, "\t%q: {\n%s\n\t},\n", l, strings.Join(lines, "\n"))
453 }
454 }
455 b.WriteString("}\n")
456 t.Logf("the table as measured:\n%s", b.String())
457}
458
459// The two drives are the gallery's longest tests, so each is dealt into
460// parts, one top-level test apiece, which the suite's slices can put on
461// shards of their own. A drive's work is its (locale, page) pairs, in
462// locale order and heightPages order within each, and the i-th pair
463// goes to part i mod n: the parts are a partition of the drive, each
464// pair read in exactly one of them. The checks the drive made once over
465// everything it read are made where what they check was read: the
466// self-check with en/button.html, the demo with en/index.html, each
467// sidebar-shell example on every page it is read; and that something
468// was measured and opened, in every part, which is only stricter.
469//
470// Under FIESTA_SWEEP=full a drive is one part again, its first, so the
471// table it prints is the whole table; the other parts skip and say so.
472// make heights-sweep runs both drives that way.
473const (
474 wideHeightParts = 3
475 narrowHeightParts = 12
476)
477
478// heightsUnit is one page of one locale, a drive's unit of work.
479type heightsUnit struct{ locale, file string }
480
481// heightsPart is part k of n of a drive over locales: its pairs, dealt
482// as above.
483func heightsPart(t *testing.T, locales []string, k, n int) []heightsUnit {
484 t.Helper()
485 if sweepFull() {
486 if k > 1 {
487 t.Skipf("FIESTA_SWEEP=full reads the whole drive in part 1 of %d, so the table it prints is whole", n)
488 }
489 n = 1
490 }
491 var out []heightsUnit
492 i := 0
493 pages := heightPages(t)
494 for _, locale := range locales {
495 for _, file := range pages {
496 if i%n == k-1 {
497 out = append(out, heightsUnit{locale, file})
498 }
499 i++
500 }
501 }
502 if len(out) == 0 {
503 t.Fatalf("part %d of %d of %d pages in %d locale(s) was dealt nothing", k, n, len(pages), len(locales))
504 }
505 return out
506}
507
508// sidebarShellPages are the examples whose frames hold a sidebar shell:
509// each, wherever a narrow part reads it, must read as one, which is
510// what proves the 48px of grace goes where it should. A name here the
511// manifest no longer has fails, so the list cannot go quietly stale.
512var sidebarShellPages = []string{"layout-sidebar.html", "layout-sidebar-page.html", "shell-sidebar.html"}
513
514// TestPreviewFrameHeightsFitTheirContent1of3 to 3of3: on the Overview
515// and every example page of day/en (every locale under
516// FIESTA_SWEEP=full), at a 1500px window, every frame's document
517// against its box at Desktop and, with Mobile chosen, against --ds-hm;
518// then, at each, what the box must hold with each of its menus and
519// panels open. A stage frame counts like any other: its document is
520// 100dvh tall, so one that fits reads equal to its box. The wide drive
521// grants every frame Rastrillo's 48px for content at rest, and none for
522// what opens. Preservation: the one wide drive, dealt into parts.
523func TestPreviewFrameHeightsFitTheirContent1of3(t *testing.T) {
524 wideHeights(t, 1, wideHeightParts)
525}
526
527func TestPreviewFrameHeightsFitTheirContent2of3(t *testing.T) {
528 wideHeights(t, 2, wideHeightParts)
529}
530
531func TestPreviewFrameHeightsFitTheirContent3of3(t *testing.T) {
532 wideHeights(t, 3, wideHeightParts)
533}
534
535// wideHeights is part k of n of the wide drive.
536func wideHeights(t *testing.T, k, n int) {
537 units := heightsPart(t, wideLocales(), k, n)
538 rig := harness.New(t, func(string) http.Handler { return treeHandler(t) })
539 deadline := 12 * time.Minute
540 if sweepFull() {
541 deadline = 28 * time.Minute
542 }
543 ctx, cancel := context.WithTimeout(rig.Context(), deadline)
544 defer cancel()
545 desk, mob := map[string]map[string]int{}, map[string]map[string]int{}
546 // What a widget with a rest height needs at rest, over the locales
547 // read: its previewRestHeights entry, which the table below, the
548 // open heights, does not carry.
549 restDesk, restMob := map[string]int{}, map[string]int{}
550 measured, opened, short := 0, 0, 0
551 readDemo := false
552 keep := func(m map[string]int, key string, n int) {
553 if n > m[key] {
554 m[key] = n
555 }
556 }
557 // fit says, per key, whether every reading of it was a viewport-sized
558 // document whose content fitted its box: printTable keeps those at
559 // the reservation they have.
560 deskFit, mobFit := map[string]bool{}, map[string]bool{}
561 fits := func(fit map[string]bool, key string, r heightReading) {
562 ok := r.Viewport && r.Need <= r.Box
563 if was, read := fit[key]; read {
564 ok = ok && was
565 }
566 fit[key] = ok
567 }
568 for _, u := range units {
569 locale, file := u.locale, u.file
570 if desk[locale] == nil {
571 desk[locale], mob[locale] = map[string]int{}, map[string]int{}
572 }
573 where := fmt.Sprintf("%s/%s at 1500px", locale, file)
574 openPage(t, ctx, where, pageURL(rig, locale, file), 1500, 1000)
575 eagerly(t, ctx, where)
576 desktop := readHeights(t, ctx, where+" Desktop")
577 for key, r := range desktop {
578 measured++
579 keep(desk[locale], key, r.Need)
580 if r.Rest {
581 keep(restDesk, key, r.Need)
582 }
583 fits(deskFit, key, r)
584 if shortfall(t, where+" Desktop", key, r, 48) {
585 short++
586 }
587 }
588 for key, o := range readOpen(t, ctx, where+" Desktop", "--ds-h") {
589 opened++
590 keep(desk[locale], key, o.Need)
591 deskFit[key] = false
592 if openShortfall(t, where+" Desktop", key, o, desktop[key].Open) {
593 short++
594 }
595 }
596 clickAll(t, ctx, where, clickedMobile, "Mobile")
597 mobileSettle(t, ctx, where)
598 mobile := readHeights(t, ctx, where+" Mobile")
599 for key, r := range mobile {
600 keep(mob[locale], key, r.Need)
601 if r.Rest {
602 keep(restMob, key, r.Need)
603 }
604 fits(mobFit, key, r)
605 if shortfall(t, where+" Mobile", key, r, 48) {
606 short++
607 }
608 }
609 for key, o := range readOpen(t, ctx, where+" Mobile", "--ds-hm") {
610 keep(mob[locale], key, o.Need)
611 mobFit[key] = false
612 if openShortfall(t, where+" Mobile", key, o, mobile[key].Open) {
613 short++
614 }
615 }
616 if file == "button.html" && locale == "en" {
617 selfCheck(t, ctx, where+" Mobile", "--ds-hm", mobile)
618 clickAll(t, ctx, where, clickedDesktop, "Desktop")
619 selfCheck(t, ctx, where+" Desktop", "--ds-h", desktop)
620 }
621 if file == "index.html" && locale == "en" {
622 if _, ok := desk["en"]["demo"]; !ok {
623 t.Fatal("the Overview's demo was not measured")
624 }
625 readDemo = true
626 }
627 }
628 if measured == 0 || opened == 0 {
629 t.Fatalf("%d frames measured and %d opened; the sweep read too little", measured, opened)
630 }
631 t.Logf("part %d: %d pages, %d frames measured and %d opened over %d locale(s); %d short (enforced: %v; the Overview's demo read here: %v)", k, len(units), measured, opened, len(desk), short, heightsEnforced, readDemo)
632 if sweepFull() {
633 printTable(t, desk, mob, deskFit, mobFit)
634 for key, n := range restDesk {
635 t.Logf("previewRestHeights as measured at 1500px (the 320px drive may raise Mobile): %q: {Desktop: %d, Mobile: %d},", key, (n+20+9)/10*10, (restMob[key]+20+9)/10*10)
636 }
637 }
638}
639
640// TestPreviewFrameHeightsFitAtThePhonesNarrowest1of12 to 12of12: the same
641// with Mobile chosen at a 320px viewport, where a Mobile frame lays out
642// at the stage's width and text that fits a line at 390px wraps, over
643// en, ar, ru and ga (all twelve under the sweep), closed and open. Only
644// a frame holding a sidebar shell gets the 48px of grace, as
645// Rastrillo's narrow drive grants it: the rail is 100dvh tall and no
646// box can fit it; every other frame gets none. The control: every frame
647// is laid out under 390px, at its stage's width. Preservation: the one
648// narrow drive, dealt into parts.
649func TestPreviewFrameHeightsFitAtThePhonesNarrowest1of12(t *testing.T) {
650 narrowHeights(t, 1, narrowHeightParts)
651}
652
653func TestPreviewFrameHeightsFitAtThePhonesNarrowest2of12(t *testing.T) {
654 narrowHeights(t, 2, narrowHeightParts)
655}
656
657func TestPreviewFrameHeightsFitAtThePhonesNarrowest3of12(t *testing.T) {
658 narrowHeights(t, 3, narrowHeightParts)
659}
660
661func TestPreviewFrameHeightsFitAtThePhonesNarrowest4of12(t *testing.T) {
662 narrowHeights(t, 4, narrowHeightParts)
663}
664
665func TestPreviewFrameHeightsFitAtThePhonesNarrowest5of12(t *testing.T) {
666 narrowHeights(t, 5, narrowHeightParts)
667}
668
669func TestPreviewFrameHeightsFitAtThePhonesNarrowest6of12(t *testing.T) {
670 narrowHeights(t, 6, narrowHeightParts)
671}
672
673func TestPreviewFrameHeightsFitAtThePhonesNarrowest7of12(t *testing.T) {
674 narrowHeights(t, 7, narrowHeightParts)
675}
676
677func TestPreviewFrameHeightsFitAtThePhonesNarrowest8of12(t *testing.T) {
678 narrowHeights(t, 8, narrowHeightParts)
679}
680
681func TestPreviewFrameHeightsFitAtThePhonesNarrowest9of12(t *testing.T) {
682 narrowHeights(t, 9, narrowHeightParts)
683}
684
685func TestPreviewFrameHeightsFitAtThePhonesNarrowest10of12(t *testing.T) {
686 narrowHeights(t, 10, narrowHeightParts)
687}
688
689func TestPreviewFrameHeightsFitAtThePhonesNarrowest11of12(t *testing.T) {
690 narrowHeights(t, 11, narrowHeightParts)
691}
692
693func TestPreviewFrameHeightsFitAtThePhonesNarrowest12of12(t *testing.T) {
694 narrowHeights(t, 12, narrowHeightParts)
695}
696
697// narrowHeights is part k of n of the narrow drive.
698func narrowHeights(t *testing.T, k, n int) {
699 units := heightsPart(t, narrowLocales(), k, n)
700 pages := map[string]bool{}
701 for _, file := range heightPages(t) {
702 pages[file] = true
703 }
704 shellPage := map[string]bool{}
705 for _, file := range sidebarShellPages {
706 if !pages[file] {
707 t.Fatalf("%s is listed as a sidebar shell's page, but the manifest has no such example", file)
708 }
709 shellPage[file] = true
710 }
711 rig := harness.New(t, func(string) http.Handler { return treeHandler(t) })
712 deadline := 15 * time.Minute
713 if sweepFull() {
714 deadline = 28 * time.Minute
715 }
716 ctx, cancel := context.WithTimeout(rig.Context(), deadline)
717 defer cancel()
718 measured, opened, short, shells := 0, 0, 0, 0
719 locales := map[string]bool{}
720 var raise []string
721 for _, u := range units {
722 locale, file := u.locale, u.file
723 locales[locale] = true
724 where := fmt.Sprintf("%s/%s at 320px", locale, file)
725 openPage(t, ctx, where, pageURL(rig, locale, file), 320, 844)
726 eagerly(t, ctx, where)
727 clickAll(t, ctx, where, clickedMobile, "Mobile")
728 mobileSettle(t, ctx, where)
729 var wide []string
730 mustRun(t, ctx, chromedp.Evaluate(`[...document.querySelectorAll(".ds-view")].filter(v => {
731 const f = v.querySelector(".ds-view__frame"); return !(f.offsetWidth < 390 && Math.abs(f.offsetWidth - v.clientWidth) <= 1); }).map(v => v.querySelector(".ds-view__frame").title)`, &wide))
732 if len(wide) > 0 {
733 t.Fatalf("%s: frames %v are not at the stage's narrow width; the readings would be of the 390px frame", where, wide)
734 }
735 got := readHeights(t, ctx, where)
736 need, openNeed := map[string]int{}, map[string]int{}
737 pageShells := 0
738 for key, r := range got {
739 measured++
740 grace := 0
741 if r.Shell {
742 grace, shells, pageShells = 48, shells+1, pageShells+1
743 }
744 need[key] = r.Need
745 if shortfall(t, where, key, r, grace) {
746 short++
747 }
748 }
749 if shellPage[file] && pageShells == 0 {
750 t.Errorf("%s: no frame read as a sidebar shell, so the drive cannot say the grace went where it should", where)
751 }
752 for key, o := range readOpen(t, ctx, where, "--ds-hm") {
753 opened++
754 openNeed[key] = o.Need
755 if openShortfall(t, where, key, o, got[key].Open) {
756 short++
757 }
758 }
759 for key, n := range need {
760 grace := 0
761 if got[key].Shell {
762 grace = 48
763 }
764 // A widget without a rest height holds both in one box.
765 rest, open := n, max(n, openNeed[key])
766 if got[key].Rest {
767 open = openNeed[key]
768 if rest > got[key].Box+grace {
769 raise = append(raise, fmt.Sprintf("%s %q: rest Mobile %d (previewRestHeights),", locale, key, (rest+20+9)/10*10))
770 }
771 }
772 if open > got[key].Open+grace {
773 raise = append(raise, fmt.Sprintf("%s %q: %d,", locale, key, (open+20+9)/10*10))
774 }
775 }
776 if file == "button.html" && locale == "en" {
777 selfCheck(t, ctx, where, "--ds-hm", got)
778 }
779 }
780 if measured == 0 || opened == 0 {
781 t.Fatalf("%d frames measured and %d opened; the sweep read too little", measured, opened)
782 }
783 sort.Strings(raise)
784 t.Logf("part %d: %d pages, %d frames measured and %d opened at 320px over %d locale(s), %d sidebar shells given grace; %d short (enforced: %v)", k, len(units), measured, opened, len(locales), shells, short, heightsEnforced)
785 if len(raise) > 0 {
786 t.Logf("Mobile entries to raise, as locale key: height:\n\t%s", strings.Join(raise, "\n\t"))
787 }
788}
789
790// columnFrame reads the column page's one frame: its height in the
791// frame's own pixels, which is the box's --ds-h or --ds-hm resolved,
792// and whether its language and scheme menus are open, after two frames
793// and a beat, so a resize view.js makes on a toggle has landed. act,
794// run in the framed document first, is "" or a click to make.
795func columnFrame(t *testing.T, ctx context.Context, where, act string) (height float64, open []bool) {
796 t.Helper()
797 var got struct {
798 Height float64
799 Open []bool
800 }
801 at(t, ctx, `(async () => {
802 const f = document.querySelector(".ds-view__frame"), d = f.contentDocument, w = d.defaultView;
803 `+act+`
804 for (const win of [window, w]) await new Promise(r => win.requestAnimationFrame(() => win.requestAnimationFrame(() => setTimeout(r, 50))));
805 return JSON.stringify({Height: parseFloat(getComputedStyle(f).height), Open: [...d.querySelectorAll("details[rst-dropdown]")].map(m => m.open)});
806 })()`, &got, awaitPromise)
807 if len(got.Open) != 2 {
808 t.Fatalf("%s: the framed column has %d menus, want its footer's two", where, len(got.Open))
809 }
810 return got.Height, got.Open
811}
812
813// TestTheColumnPreviewGrowsOnlyWhileAMenuIsOpen: the column's box is
814// its page's height at rest and the open height while a footer menu
815// is open, on Desktop and on Mobile. Opening the scheme menu while the
816// language menu is open closes the language menu (the two share a
817// name), and the box stays open; closing the last one brings it back.
818// With scripts off nothing could grow the box, so it keeps the open
819// height, and a menu opened there is still all in view.
820func TestTheColumnPreviewGrowsOnlyWhileAMenuIsOpen(t *testing.T) {
821 key := "layout-column/" + mustDefault(t, "layout-column")
822 rest, ok := previewRestHeights[key]
823 if !ok {
824 t.Fatalf("%s has no rest height to switch from", key)
825 }
826 openDesktop, openMobile := previewHeightsFor("en", key)
827 rig := harness.New(t, func(string) http.Handler { return treeHandler(t) })
828 ctx, cancel := context.WithTimeout(rig.Context(), 120*time.Second)
829 defer cancel()
830 const language, scheme = `d.querySelector("[rst-locale] > summary").click();`, `d.querySelector("[rst-scheme] > summary").click();`
831 want := func(where string, got, wantH float64, open []bool, wantOpen ...bool) {
832 t.Helper()
833 if fmt.Sprint(open) != fmt.Sprint(wantOpen) {
834 t.Fatalf("%s: the menus' open states are %v, want %v; the step did not do what it says", where, open, wantOpen)
835 }
836 if got < wantH-1 || got > wantH+1 {
837 t.Errorf("%s: the frame is %.0fpx tall, want %.0f", where, got, wantH)
838 }
839 }
840 openPage(t, ctx, "en/layout-column.html", pageURL(rig, "en", "layout-column.html"), 1500, 1000)
841 eagerly(t, ctx, "en/layout-column.html")
842 for _, mode := range []struct {
843 name string
844 rest, open int
845 }{{"Desktop", rest.Desktop, openDesktop}, {"Mobile", rest.Mobile, openMobile}} {
846 if mode.name == "Mobile" {
847 clickAll(t, ctx, "en/layout-column.html", clickedMobile, "Mobile")
848 mobileSettle(t, ctx, "en/layout-column.html")
849 }
850 where := "en/layout-column.html " + mode.name
851 h, open := columnFrame(t, ctx, where, "")
852 want(where+" at rest", h, float64(mode.rest), open, false, false)
853 h, open = columnFrame(t, ctx, where, language)
854 want(where+" with Language open", h, float64(mode.open), open, true, false)
855 h, open = columnFrame(t, ctx, where, scheme)
856 want(where+" with Colour scheme opened over Language", h, float64(mode.open), open, false, true)
857 h, open = columnFrame(t, ctx, where, scheme)
858 want(where+" with both closed again", h, float64(mode.rest), open, false, false)
859 }
860
861 off := galleryrig.NoScripts(t, ctx)
862 mustRun(t, off, chromedp.EmulateViewport(1500, 1000), chromedp.Navigate(pageURL(rig, "en", "layout-column.html")), chromedp.WaitReady(".ds-view__frame", chromedp.ByQuery))
863 galleryrig.RequireScriptsOff(t, off, "en/layout-column.html, scripts off")
864 var h float64
865 until(t, off, "en/layout-column.html, scripts off", `!!document.querySelector(".ds-view__frame").contentDocument?.querySelector("[rst-page-foot]")`)
866 at(t, off, `JSON.stringify(parseFloat(getComputedStyle(document.querySelector(".ds-view__frame")).height))`, &h)
867 if h < float64(openDesktop)-1 || h > float64(openDesktop)+1 {
868 t.Errorf("en/layout-column.html, scripts off: the frame is %.0fpx tall, want the open height %d, since nothing can grow it when a menu opens", h, openDesktop)
869 }
870}
871