//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.

The aside.

A picture would go here

x

Canvas

Renderer fixture

One section per slide, one element per block.

One line.

x

Narrow

Same slide, smaller.

` 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) } } }