oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1//go:build browser
2
3// A flow slide on a phone reads top to bottom, one region under the
4// next. The container query in slides.css collapses every template to
5// one column; the failure this file was written for (polish audit B4,
6// 2026-09-23) was that collapse losing a specificity fight — the
7// phone's `[slides-region] { grid-area: auto }` (0,1,0) against the
8// template's `[slides-region][data-region="heading"] { grid-area:
9// heading }` (0,2,0) — so every region kept an area name the collapsed
10// grid no longer had, and all of them landed in the same cell: the
11// heading drawn over the body on every flow slide, in the viewer, on
12// share links and in the editor's Phone and Both views.
13package slidestest
14
15import (
16 "encoding/json"
17 "strconv"
18 "testing"
19
20 "amadan.net/rastrillo/rastrillo/harness"
21 "github.com/chromedp/cdproto/runtime"
22 "github.com/chromedp/chromedp"
23)
24
25// stackedRegions is a JS expression that answers, for every flow
26// slide under root, the pairs of visible regions that overlap: an
27// empty list is the pass. Reading order is DOM order after the phone
28// layer's own `order`, so the check is on the boxes and not on which
29// comes first — two regions in one place is the bug whichever is on
30// top.
31func stackedRegions(root string) string {
32 return `JSON.stringify((() => {
33 const out = [];
34 for (const s of document.querySelectorAll('` + root + ` [slides-slide][data-layout="flow"]')) {
35 const rs = [...s.querySelectorAll(':scope > [slides-frame] > [slides-region]')]
36 .map(e => ({name: e.dataset.region, r: e.getBoundingClientRect()}))
37 .filter(x => x.r.width > 0 && x.r.height > 0);
38 if (s.getBoundingClientRect().width > 600) out.push('slide wider than a phone: ' + s.getBoundingClientRect().width);
39 for (let i = 0; i < rs.length; i++) for (let j = i + 1; j < rs.length; j++) {
40 const a = rs[i].r, b = rs[j].r;
41 const overlap = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
42 if (overlap > 0.5) out.push(s.dataset.template + ': ' + rs[i].name + ' [' + a.top.toFixed(1) + '–' + a.bottom.toFixed(1) +
43 '] overlaps ' + rs[j].name + ' [' + b.top.toFixed(1) + '–' + b.bottom.toFixed(1) + ']');
44 }
45 }
46 return out;
47 })())`
48}
49
50func noStackedRegions(t *testing.T, rig *harness.Rig, where, root string) {
51 t.Helper()
52 var raw string
53 var flows int
54 rig.Run(
55 chromedp.Evaluate(`document.querySelectorAll('`+root+` [slides-slide][data-layout="flow"]').length`, &flows),
56 chromedp.Evaluate(stackedRegions(root), &raw),
57 )
58 var bad []string
59 if err := json.Unmarshal([]byte(raw), &bad); err != nil {
60 t.Fatalf("%s: read the browser's answer: %v\n%s", where, err, raw)
61 }
62 for _, b := range bad {
63 t.Errorf("%s: %s", where, b)
64 }
65 if flows == 0 {
66 t.Logf("%s: no flow slide on screen", where)
67 }
68}
69
70// TestBrowserPhoneStacksFlowRegions walks the sample deck in the
71// viewer at 390px — every slide of it, so every template it carries —
72// and then opens the editor's Phone and Both views on a desktop
73// window, and requires that no two regions of a flow slide share any
74// height.
75func TestBrowserPhoneStacksFlowRegions(t *testing.T) {
76 rig, mailer := newRig(t)
77 browserSignIn(t, rig, mailer)
78
79 var deck string
80 rig.Run(chromedp.Evaluate(`(document.querySelector('a[href^="/d/"]')?.getAttribute('href') ?? '')`, &deck))
81 if !deckPath.MatchString(deck) {
82 t.Fatalf("the deck list links no deck: %q", deck)
83 }
84
85 var count, flowsSeen int
86 rig.Run(
87 chromedp.EmulateViewport(390, 844, chromedp.EmulateMobile),
88 chromedp.Navigate(rig.Origin+deck),
89 chromedp.WaitVisible(`[slides-slide]`, chromedp.ByQuery),
90 chromedp.Evaluate(`document.querySelectorAll('[slides-film] a').length`, &count),
91 )
92 if count < 2 {
93 t.Fatalf("the viewer lists %d slides; the sample deck has more", count)
94 }
95 for n := 1; n <= count; n++ {
96 var flow int
97 rig.Run(
98 chromedp.Navigate(rig.Origin+deck+"?slide="+strconv.Itoa(n)),
99 chromedp.WaitVisible(`[slides-slide]`, chromedp.ByQuery),
100 chromedp.Evaluate(`document.fonts.ready.then(() => document.querySelectorAll('[slides-slide][data-layout="flow"]').length)`, &flow,
101 func(p *runtime.EvaluateParams) *runtime.EvaluateParams { return p.WithAwaitPromise(true) }),
102 )
103 flowsSeen += flow
104 noStackedRegions(t, rig, "viewer at 390, slide "+strconv.Itoa(n), "body")
105 if n == 1 {
106 rig.Screen("body", "viewer at 390, slide 1")
107 }
108 }
109 if flowsSeen < 3 {
110 t.Errorf("the walk met only %d flow slides; the check needs real ones to mean anything", flowsSeen)
111 }
112
113 // The editor's Phone and Both views, on a desktop window: the phone
114 // stage is a slide in a phone-sized container, so the same query
115 // fires inside a 1440px page.
116 rig.Run(
117 chromedp.EmulateViewport(1440, 900),
118 chromedp.Navigate(rig.Origin+deck+"/edit"),
119 chromedp.WaitVisible(`[slides-editor]`, chromedp.ByQuery),
120 waitFor(`document.querySelector('[slides-stage] [slides-slide]')`),
121 chromedp.Click(`input[name="slides-view"][value="phone"]`, chromedp.ByQuery),
122 waitFor(`document.querySelector('[slides-stage-pane]').dataset.view === 'phone'`),
123 waitFor(`document.querySelector('[slides-stage] [slides-slide]')?.getBoundingClientRect().width <= 600`),
124 )
125 rig.Screen("[slides-editor]", "editor phone view")
126 noStackedRegions(t, rig, "editor Phone view", "[slides-stage]")
127
128 rig.Run(
129 chromedp.Click(`input[name="slides-view"][value="both"]`, chromedp.ByQuery),
130 waitFor(`document.querySelector('[slides-stage-pane]').dataset.view === 'both'`),
131 waitFor(`document.querySelector('[slides-stage-phone] [slides-slide]')`),
132 )
133 rig.Screen("[slides-editor]", "editor both view")
134 noStackedRegions(t, rig, "editor Both view, phone half", "[slides-stage-phone]")
135}
136