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// The layout engine's own drive. internal/slides/layout measures a
4// slide the way slides.css would at 1920 by 1080 — it is a reading of
5// that stylesheet, done in Go, and everything downstream of it (the
6// SVG thumbnail, the PDF) inherits whatever that reading gets wrong.
7//
8// Nothing in the Go tests can catch a misreading: they check the engine
9// against itself. This file is the other half — a real Chromium laying
10// out real slide markup at 1920, with the numbers sizes.go and
11// regions.go were derived from asserted against what the browser
12// actually does.
13//
14// It caught two mistakes when it was written (slice 3 task 2), and
15// they are the reason it exists rather than a comment saying the
16// numbers looked right:
17//
18// - A container query unit is a percent of the container's CONTENT
19// box, so a `cqw` on a slide with the section's 120px padding is
20// 16.8px and not 19.2. Every type size on six of the seven
21// templates was 12.5% too big.
22// - A picture or a shape in a flow region is a flex item that
23// SHRINKS to the space that is left; the engine was stacking a
24// square 1680px picture and running three slides off the bottom.
25package slidestest
26
27import (
28 "encoding/json"
29 "testing"
30
31 "github.com/chromedp/chromedp"
32)
33
34// wantGeometry is what slides.css computes at 1920, and every number
35// in it has a twin in internal/slides/layout (sizes.go's scale, and
36// regions.go's table). When this fails, one of the two has moved: read
37// the diff before changing either.
38//
39// The tolerance is a pixel. Chromium rounds a used font size to a
40// 64th, so 121.968 comes back as 121.95, and pinning tighter than that
41// would fail on a browser upgrade without anything being wrong.
42const tolerance = 1.0
43
44type rect struct{ X, Y, W, H float64 }
45
46type geometry struct {
47 TwoColumnHeading rect `json:"twoColumnHeading"`
48 TwoColumnBody rect `json:"twoColumnBody"`
49 TwoColumnAside rect `json:"twoColumnAside"`
50 TitleHeading rect `json:"titleHeading"`
51 TitleBody rect `json:"titleBody"`
52 ImageFullMedia rect `json:"imageFullMedia"`
53 BlankRegion rect `json:"blankRegion"`
54 BlankText rect `json:"blankText"`
55 BlankImage rect `json:"blankImage"`
56 PaddedT1 float64 `json:"paddedT1"`
57 PaddedBodyM float64 `json:"paddedBodyM"`
58 PaddedBodyL float64 `json:"paddedBodyL"`
59 FullT1 float64 `json:"fullT1"`
60 FullT2 float64 `json:"fullT2"`
61 HeadingLine float64 `json:"headingLine"`
62 BodyLine float64 `json:"bodyLine"`
63 WideGap float64 `json:"wideGap"`
64 NarrowGap float64 `json:"narrowGap"`
65}
66
67func TestTheStylesheetMeasuresWhatTheLayoutEngineAssumes(t *testing.T) {
68 rig, _ := newRig(t)
69
70 const vars = `style="--slides-bg:#faf7f0;--slides-fg:#1c1a17;--slides-accent:#8a5410;--slides-accent-2:#46617a;--slides-font-heading:Liberation Serif, Georgia, serif;--slides-font-body:Liberation Sans, system-ui, sans-serif"`
71 // One section per template under test, at the frame's own width.
72 // Built by hand rather than through render.Slide because the point
73 // is the STYLESHEET: this is the markup render produces, and a
74 // change in either that broke the pairing should fail here.
75 const markup = `
76<div style="width:1920px;position:absolute;left:0;top:0">
77<section slides-slide data-layout="flow" data-template="two-column" data-theme="paper" ` + vars + `>
78<div slides-frame>
79<div slides-region data-region="heading" id="TCH"><h1 slides-block data-block="heading" data-level="1" id="TCH1">Regions, not coordinates</h1></div>
80<div slides-region data-region="body" id="TCB"><div slides-block data-block="text" data-size="m" id="TCT"><p>A flow slide names regions.</p></div></div>
81<div slides-region data-region="aside" id="TCA"><div slides-block data-block="text" data-size="m"><p>The aside.</p></div></div>
82</div>
83</section>
84<section slides-slide data-layout="flow" data-template="image-full" data-theme="paper" ` + vars + `>
85<div slides-frame>
86<div slides-region data-region="heading"><h2 slides-block data-block="heading" data-level="2" id="IFH2">A picture would go here</h2></div>
87<div slides-region data-region="media" id="IFM"><figure slides-block data-block="image" data-fit="cover"><img src="/static/fonts/nothing.png" alt="x" width="1600" height="900"></figure></div>
88</div>
89</section>
90<section slides-slide data-layout="canvas" data-template="blank" data-theme="paper" ` + vars + `>
91<h1 slides-block data-block="heading" data-level="1" data-z="1" id="CVH" style="left:6%;top:8%;width:88%;height:16%;z-index:1">Canvas</h1>
92</section>
93<section slides-slide data-layout="flow" data-template="title" data-theme="paper" ` + vars + `>
94<div slides-frame>
95<div slides-region data-region="heading" id="TH"><h1 slides-block data-block="heading" data-level="1">Renderer fixture</h1></div>
96<div slides-region data-region="body" id="TB"><div slides-block data-block="text" data-size="l" id="TT"><p>One section per slide, one element per block.</p></div></div>
97</div>
98</section>
99<section slides-slide data-layout="flow" data-template="blank" data-theme="paper" ` + vars + `>
100<div slides-frame>
101<div slides-region data-region="body" id="BR">
102<div slides-block data-block="text" data-size="m" id="BT"><p>One line.</p></div>
103<figure slides-block data-block="image" data-fit="contain" id="BI"><img src="/static/fonts/nothing.png" alt="x" width="64" height="64"></figure>
104</div>
105</div></section>
106</div>
107<div style="width:780px;position:absolute;left:0;top:2000px">
108<section slides-slide data-layout="flow" data-template="title-body" data-theme="paper" ` + vars + `>
109<div slides-frame>
110<div slides-region data-region="heading" id="NGH"><h1 slides-block data-block="heading" data-level="1">Narrow</h1></div>
111<div slides-region data-region="body" id="NGB"><div slides-block data-block="text" data-size="m"><p>Same slide, smaller.</p></div></div>
112</div>
113</section>
114</div>`
115
116 var raw string
117 rig.Run(
118 // A 1920 viewport, which is the projector and the export. The
119 // slide is drawn at 1920 in the page too — and the second copy
120 // at 780, which is what proves the gap no longer follows the
121 // window (see narrowGap below).
122 chromedp.EmulateViewport(1920, 1080),
123 chromedp.Navigate(rig.Origin+"/signin"),
124 chromedp.Evaluate(`(() => {
125 const link = document.createElement('link');
126 link.rel = 'stylesheet';
127 link.href = '/static/slides.css';
128 document.head.appendChild(link);
129 document.body.insertAdjacentHTML('beforeend', `+"`"+markup+"`"+`);
130 return 'ok';
131 })()`, new(string)),
132 // Long enough for the stylesheet and the faces it asks for.
133 chromedp.Sleep(2_000_000_000),
134 chromedp.Evaluate(`(() => {
135 const q = id => { const e = document.getElementById(id); const r = e.getBoundingClientRect();
136 const p = e.closest('[slides-slide]').getBoundingClientRect();
137 return {X: r.x-p.x, Y: r.y-p.y, W: r.width, H: r.height}; };
138 const px = (id, prop) => parseFloat(getComputedStyle(document.getElementById(id))[prop]);
139 return JSON.stringify({
140 twoColumnHeading: q('TCH'), twoColumnBody: q('TCB'), twoColumnAside: q('TCA'),
141 titleHeading: q('TH'), titleBody: q('TB'),
142 imageFullMedia: q('IFM'),
143 blankRegion: q('BR'), blankText: q('BT'), blankImage: q('BI'),
144 paddedT1: px('TCH1','fontSize'), paddedBodyM: px('TCT','fontSize'), paddedBodyL: px('TT','fontSize'),
145 fullT1: px('CVH','fontSize'), fullT2: px('IFH2','fontSize'),
146 headingLine: px('TCH1','lineHeight'), bodyLine: px('TCT','lineHeight'),
147 wideGap: q('TCB').Y - (q('TCH').Y + q('TCH').H),
148 narrowGap: (() => { const h = document.getElementById('NGH').getBoundingClientRect();
149 const b = document.getElementById('NGB').getBoundingClientRect();
150 return b.y - (h.y + h.height); })(),
151 });
152 })()`, &raw),
153 )
154
155 var got geometry
156 if err := json.Unmarshal([]byte(raw), &got); err != nil {
157 t.Fatalf("read the browser's geometry: %v\n%s", err, raw)
158 }
159
160 // The type scale. 16.8 per cqw with the section's padding, 19.2
161 // without it (image-full and canvas both set `padding: 0`).
162 for _, c := range []struct {
163 name string
164 got, want float64
165 derivation string
166 }{
167 {"--slides-t-1 on a padded template", got.PaddedT1, 110.88, "6.6 × 16.8"},
168 {"--slides-body-m on a padded template", got.PaddedBodyM, 47.04, "2.8 × 16.8"},
169 {"--slides-body-l on a padded template", got.PaddedBodyL, 57.12, "3.4 × 16.8"},
170 {"--slides-t-1 on a canvas slide", got.FullT1, 126.72, "6.6 × 19.2"},
171 {"--slides-t-2 on image-full", got.FullT2, 96, "5 × 19.2"},
172 {"a heading's line box", got.HeadingLine, 121.968, "110.88 × 1.1"},
173 {"a body line box", got.BodyLine, 61.152, "47.04 × 1.3"},
174 } {
175 if diff := c.got - c.want; diff > tolerance || diff < -tolerance {
176 t.Errorf("%s is %v in the browser; layout/sizes.go says %v (%s)",
177 c.name, c.got, c.want, c.derivation)
178 }
179 }
180
181 // The region boxes, which are layout/regions.go's table.
182 for _, c := range []struct {
183 name string
184 got rect
185 want rect
186 }{
187 {"two-column heading", got.TwoColumnHeading, rect{120, 120, 1680, 121.968}},
188 {"two-column body", got.TwoColumnBody, rect{120, 285.648, 805.056, 674.352}},
189 {"two-column aside", got.TwoColumnAside, rect{994.944, 285.648, 805.056, 674.352}},
190 // title centres its two auto rows as a pair, so where each one
191 // lands depends on how tall the other is.
192 {"title heading", got.TitleHeading, rect{573.19, 420.048, 773.61, 121.968}},
193 {"title body", got.TitleBody, rect{386.98, 585.696, 1146.02, 74.256}},
194 {"image-full media", got.ImageFullMedia, rect{0, 0, 1920, 1080}},
195 {"a blank slide's body region", got.BlankRegion, rect{120, 120, 1680, 840}},
196 } {
197 compareRect(t, c.name, c.got, c.want)
198 }
199
200 // A picture in a flow region is a flex item and shrinks to what is
201 // left: 840 less the text above it and the gap between them.
202 compareRect(t, "a picture under one line of text",
203 got.BlankImage, rect{120, 224.832, 1680, 735.168})
204 compareRect(t, "that line of text", got.BlankText, rect{120, 120, 1680, 61.152})
205
206 // The gap between regions, and the reason slice 3 moved the
207 // template grid onto [slides-frame]. It used to be written on the
208 // section, which is the query container, so the browser measured
209 // it against whatever was outside the slide: 49.92px here and
210 // 20.27px in a 780px window, for the same slide. On the frame it
211 // is 2.6% of the section's content box, so it is 43.68 at 1920 and
212 // 17.745 at 780 — the same slide, scaled, which is what the rest
213 // of the stylesheet already promised and what lets
214 // layout/sizes.go state one number for it.
215 //
216 // If either of these ever equals the other, the gap has stopped
217 // scaling with the slide and gone back to following the window.
218 for _, c := range []struct {
219 name string
220 got, want float64
221 derivation string
222 }{
223 {"the gap between regions at 1920", got.WideGap, 43.68, "2.6% of 1680"},
224 {"the same gap on a 780px slide", got.NarrowGap, 17.745, "2.6% of 682.5"},
225 } {
226 if diff := c.got - c.want; diff > tolerance || diff < -tolerance {
227 t.Errorf("%s is %v in the browser; slides.css means %v (%s)",
228 c.name, c.got, c.want, c.derivation)
229 }
230 }
231}
232
233func compareRect(t *testing.T, name string, got, want rect) {
234 t.Helper()
235 for _, c := range []struct {
236 axis string
237 got, want float64
238 }{{"x", got.X, want.X}, {"y", got.Y, want.Y}, {"width", got.W, want.W}, {"height", got.H, want.H}} {
239 if diff := c.got - c.want; diff > tolerance || diff < -tolerance {
240 t.Errorf("%s: the browser puts its %s at %v; internal/slides/layout says %v",
241 name, c.axis, c.got, c.want)
242 }
243 }
244}
245