//go:build browser
// The layout engine's own drive. internal/slides/layout measures a
// slide the way slides.css would at 1920 by 1080 — it is a reading of
// that stylesheet, done in Go, and everything downstream of it (the
// SVG thumbnail, the PDF) inherits whatever that reading gets wrong.
//
// Nothing in the Go tests can catch a misreading: they check the engine
// against itself. This file is the other half — a real Chromium laying
// out real slide markup at 1920, with the numbers sizes.go and
// regions.go were derived from asserted against what the browser
// actually does.
//
// It caught two mistakes when it was written (slice 3 task 2), and
// they are the reason it exists rather than a comment saying the
// numbers looked right:
//
// - A container query unit is a percent of the container's CONTENT
// box, so a `cqw` on a slide with the section's 120px padding is
// 16.8px and not 19.2. Every type size on six of the seven
// templates was 12.5% too big.
// - A picture or a shape in a flow region is a flex item that
// SHRINKS to the space that is left; the engine was stacking a
// square 1680px picture and running three slides off the bottom.
package slidestest
import (
"encoding/json"
"testing"
"github.com/chromedp/chromedp"
)
// wantGeometry is what slides.css computes at 1920, and every number
// in it has a twin in internal/slides/layout (sizes.go's scale, and
// regions.go's table). When this fails, one of the two has moved: read
// the diff before changing either.
//
// The tolerance is a pixel. Chromium rounds a used font size to a
// 64th, so 121.968 comes back as 121.95, and pinning tighter than that
// would fail on a browser upgrade without anything being wrong.
const tolerance = 1.0
type rect struct{ X, Y, W, H float64 }
type geometry struct {
TwoColumnHeading rect `json:"twoColumnHeading"`
TwoColumnBody rect `json:"twoColumnBody"`
TwoColumnAside rect `json:"twoColumnAside"`
TitleHeading rect `json:"titleHeading"`
TitleBody rect `json:"titleBody"`
ImageFullMedia rect `json:"imageFullMedia"`
BlankRegion rect `json:"blankRegion"`
BlankText rect `json:"blankText"`
BlankImage rect `json:"blankImage"`
PaddedT1 float64 `json:"paddedT1"`
PaddedBodyM float64 `json:"paddedBodyM"`
PaddedBodyL float64 `json:"paddedBodyL"`
FullT1 float64 `json:"fullT1"`
FullT2 float64 `json:"fullT2"`
HeadingLine float64 `json:"headingLine"`
BodyLine float64 `json:"bodyLine"`
WideGap float64 `json:"wideGap"`
NarrowGap float64 `json:"narrowGap"`
}
func TestTheStylesheetMeasuresWhatTheLayoutEngineAssumes(t *testing.T) {
rig, _ := newRig(t)
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"`
// One section per template under test, at the frame's own width.
// Built by hand rather than through render.Slide because the point
// is the STYLESHEET: this is the markup render produces, and a
// change in either that broke the pairing should fail here.
const markup = `
Regions, not coordinates
A flow slide names regions.
Renderer fixture
One section per slide, one element per block.
`
var raw string
rig.Run(
// A 1920 viewport, which is the projector and the export. The
// slide is drawn at 1920 in the page too — and the second copy
// at 780, which is what proves the gap no longer follows the
// window (see narrowGap below).
chromedp.EmulateViewport(1920, 1080),
chromedp.Navigate(rig.Origin+"/signin"),
chromedp.Evaluate(`(() => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/static/slides.css';
document.head.appendChild(link);
document.body.insertAdjacentHTML('beforeend', `+"`"+markup+"`"+`);
return 'ok';
})()`, new(string)),
// Long enough for the stylesheet and the faces it asks for.
chromedp.Sleep(2_000_000_000),
chromedp.Evaluate(`(() => {
const q = id => { const e = document.getElementById(id); const r = e.getBoundingClientRect();
const p = e.closest('[slides-slide]').getBoundingClientRect();
return {X: r.x-p.x, Y: r.y-p.y, W: r.width, H: r.height}; };
const px = (id, prop) => parseFloat(getComputedStyle(document.getElementById(id))[prop]);
return JSON.stringify({
twoColumnHeading: q('TCH'), twoColumnBody: q('TCB'), twoColumnAside: q('TCA'),
titleHeading: q('TH'), titleBody: q('TB'),
imageFullMedia: q('IFM'),
blankRegion: q('BR'), blankText: q('BT'), blankImage: q('BI'),
paddedT1: px('TCH1','fontSize'), paddedBodyM: px('TCT','fontSize'), paddedBodyL: px('TT','fontSize'),
fullT1: px('CVH','fontSize'), fullT2: px('IFH2','fontSize'),
headingLine: px('TCH1','lineHeight'), bodyLine: px('TCT','lineHeight'),
wideGap: q('TCB').Y - (q('TCH').Y + q('TCH').H),
narrowGap: (() => { const h = document.getElementById('NGH').getBoundingClientRect();
const b = document.getElementById('NGB').getBoundingClientRect();
return b.y - (h.y + h.height); })(),
});
})()`, &raw),
)
var got geometry
if err := json.Unmarshal([]byte(raw), &got); err != nil {
t.Fatalf("read the browser's geometry: %v\n%s", err, raw)
}
// The type scale. 16.8 per cqw with the section's padding, 19.2
// without it (image-full and canvas both set `padding: 0`).
for _, c := range []struct {
name string
got, want float64
derivation string
}{
{"--slides-t-1 on a padded template", got.PaddedT1, 110.88, "6.6 × 16.8"},
{"--slides-body-m on a padded template", got.PaddedBodyM, 47.04, "2.8 × 16.8"},
{"--slides-body-l on a padded template", got.PaddedBodyL, 57.12, "3.4 × 16.8"},
{"--slides-t-1 on a canvas slide", got.FullT1, 126.72, "6.6 × 19.2"},
{"--slides-t-2 on image-full", got.FullT2, 96, "5 × 19.2"},
{"a heading's line box", got.HeadingLine, 121.968, "110.88 × 1.1"},
{"a body line box", got.BodyLine, 61.152, "47.04 × 1.3"},
} {
if diff := c.got - c.want; diff > tolerance || diff < -tolerance {
t.Errorf("%s is %v in the browser; layout/sizes.go says %v (%s)",
c.name, c.got, c.want, c.derivation)
}
}
// The region boxes, which are layout/regions.go's table.
for _, c := range []struct {
name string
got rect
want rect
}{
{"two-column heading", got.TwoColumnHeading, rect{120, 120, 1680, 121.968}},
{"two-column body", got.TwoColumnBody, rect{120, 285.648, 805.056, 674.352}},
{"two-column aside", got.TwoColumnAside, rect{994.944, 285.648, 805.056, 674.352}},
// title centres its two auto rows as a pair, so where each one
// lands depends on how tall the other is.
{"title heading", got.TitleHeading, rect{573.19, 420.048, 773.61, 121.968}},
{"title body", got.TitleBody, rect{386.98, 585.696, 1146.02, 74.256}},
{"image-full media", got.ImageFullMedia, rect{0, 0, 1920, 1080}},
{"a blank slide's body region", got.BlankRegion, rect{120, 120, 1680, 840}},
} {
compareRect(t, c.name, c.got, c.want)
}
// A picture in a flow region is a flex item and shrinks to what is
// left: 840 less the text above it and the gap between them.
compareRect(t, "a picture under one line of text",
got.BlankImage, rect{120, 224.832, 1680, 735.168})
compareRect(t, "that line of text", got.BlankText, rect{120, 120, 1680, 61.152})
// The gap between regions, and the reason slice 3 moved the
// template grid onto [slides-frame]. It used to be written on the
// section, which is the query container, so the browser measured
// it against whatever was outside the slide: 49.92px here and
// 20.27px in a 780px window, for the same slide. On the frame it
// is 2.6% of the section's content box, so it is 43.68 at 1920 and
// 17.745 at 780 — the same slide, scaled, which is what the rest
// of the stylesheet already promised and what lets
// layout/sizes.go state one number for it.
//
// If either of these ever equals the other, the gap has stopped
// scaling with the slide and gone back to following the window.
for _, c := range []struct {
name string
got, want float64
derivation string
}{
{"the gap between regions at 1920", got.WideGap, 43.68, "2.6% of 1680"},
{"the same gap on a 780px slide", got.NarrowGap, 17.745, "2.6% of 682.5"},
} {
if diff := c.got - c.want; diff > tolerance || diff < -tolerance {
t.Errorf("%s is %v in the browser; slides.css means %v (%s)",
c.name, c.got, c.want, c.derivation)
}
}
}
func compareRect(t *testing.T, name string, got, want rect) {
t.Helper()
for _, c := range []struct {
axis string
got, want float64
}{{"x", got.X, want.X}, {"y", got.Y, want.Y}, {"width", got.W, want.W}, {"height", got.H, want.H}} {
if diff := c.got - c.want; diff > tolerance || diff < -tolerance {
t.Errorf("%s: the browser puts its %s at %v; internal/slides/layout says %v",
name, c.axis, c.got, c.want)
}
}
}