//go:build browser // A flow slide on a phone reads top to bottom, one region under the // next. The container query in slides.css collapses every template to // one column; the failure this file was written for (polish audit B4, // 2026-09-23) was that collapse losing a specificity fight — the // phone's `[slides-region] { grid-area: auto }` (0,1,0) against the // template's `[slides-region][data-region="heading"] { grid-area: // heading }` (0,2,0) — so every region kept an area name the collapsed // grid no longer had, and all of them landed in the same cell: the // heading drawn over the body on every flow slide, in the viewer, on // share links and in the editor's Phone and Both views. package slidestest import ( "encoding/json" "strconv" "testing" "amadan.net/rastrillo/rastrillo/harness" "github.com/chromedp/cdproto/runtime" "github.com/chromedp/chromedp" ) // stackedRegions is a JS expression that answers, for every flow // slide under root, the pairs of visible regions that overlap: an // empty list is the pass. Reading order is DOM order after the phone // layer's own `order`, so the check is on the boxes and not on which // comes first — two regions in one place is the bug whichever is on // top. func stackedRegions(root string) string { return `JSON.stringify((() => { const out = []; for (const s of document.querySelectorAll('` + root + ` [slides-slide][data-layout="flow"]')) { const rs = [...s.querySelectorAll(':scope > [slides-frame] > [slides-region]')] .map(e => ({name: e.dataset.region, r: e.getBoundingClientRect()})) .filter(x => x.r.width > 0 && x.r.height > 0); if (s.getBoundingClientRect().width > 600) out.push('slide wider than a phone: ' + s.getBoundingClientRect().width); for (let i = 0; i < rs.length; i++) for (let j = i + 1; j < rs.length; j++) { const a = rs[i].r, b = rs[j].r; const overlap = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top); if (overlap > 0.5) out.push(s.dataset.template + ': ' + rs[i].name + ' [' + a.top.toFixed(1) + '–' + a.bottom.toFixed(1) + '] overlaps ' + rs[j].name + ' [' + b.top.toFixed(1) + '–' + b.bottom.toFixed(1) + ']'); } } return out; })())` } func noStackedRegions(t *testing.T, rig *harness.Rig, where, root string) { t.Helper() var raw string var flows int rig.Run( chromedp.Evaluate(`document.querySelectorAll('`+root+` [slides-slide][data-layout="flow"]').length`, &flows), chromedp.Evaluate(stackedRegions(root), &raw), ) var bad []string if err := json.Unmarshal([]byte(raw), &bad); err != nil { t.Fatalf("%s: read the browser's answer: %v\n%s", where, err, raw) } for _, b := range bad { t.Errorf("%s: %s", where, b) } if flows == 0 { t.Logf("%s: no flow slide on screen", where) } } // TestBrowserPhoneStacksFlowRegions walks the sample deck in the // viewer at 390px — every slide of it, so every template it carries — // and then opens the editor's Phone and Both views on a desktop // window, and requires that no two regions of a flow slide share any // height. func TestBrowserPhoneStacksFlowRegions(t *testing.T) { rig, mailer := newRig(t) browserSignIn(t, rig, mailer) var deck string rig.Run(chromedp.Evaluate(`(document.querySelector('a[href^="/d/"]')?.getAttribute('href') ?? '')`, &deck)) if !deckPath.MatchString(deck) { t.Fatalf("the deck list links no deck: %q", deck) } var count, flowsSeen int rig.Run( chromedp.EmulateViewport(390, 844, chromedp.EmulateMobile), chromedp.Navigate(rig.Origin+deck), chromedp.WaitVisible(`[slides-slide]`, chromedp.ByQuery), chromedp.Evaluate(`document.querySelectorAll('[slides-film] a').length`, &count), ) if count < 2 { t.Fatalf("the viewer lists %d slides; the sample deck has more", count) } for n := 1; n <= count; n++ { var flow int rig.Run( chromedp.Navigate(rig.Origin+deck+"?slide="+strconv.Itoa(n)), chromedp.WaitVisible(`[slides-slide]`, chromedp.ByQuery), chromedp.Evaluate(`document.fonts.ready.then(() => document.querySelectorAll('[slides-slide][data-layout="flow"]').length)`, &flow, func(p *runtime.EvaluateParams) *runtime.EvaluateParams { return p.WithAwaitPromise(true) }), ) flowsSeen += flow noStackedRegions(t, rig, "viewer at 390, slide "+strconv.Itoa(n), "body") if n == 1 { rig.Screen("body", "viewer at 390, slide 1") } } if flowsSeen < 3 { t.Errorf("the walk met only %d flow slides; the check needs real ones to mean anything", flowsSeen) } // The editor's Phone and Both views, on a desktop window: the phone // stage is a slide in a phone-sized container, so the same query // fires inside a 1440px page. rig.Run( chromedp.EmulateViewport(1440, 900), chromedp.Navigate(rig.Origin+deck+"/edit"), chromedp.WaitVisible(`[slides-editor]`, chromedp.ByQuery), waitFor(`document.querySelector('[slides-stage] [slides-slide]')`), chromedp.Click(`input[name="slides-view"][value="phone"]`, chromedp.ByQuery), waitFor(`document.querySelector('[slides-stage-pane]').dataset.view === 'phone'`), waitFor(`document.querySelector('[slides-stage] [slides-slide]')?.getBoundingClientRect().width <= 600`), ) rig.Screen("[slides-editor]", "editor phone view") noStackedRegions(t, rig, "editor Phone view", "[slides-stage]") rig.Run( chromedp.Click(`input[name="slides-view"][value="both"]`, chromedp.ByQuery), waitFor(`document.querySelector('[slides-stage-pane]').dataset.view === 'both'`), waitFor(`document.querySelector('[slides-stage-phone] [slides-slide]')`), ) rig.Screen("[slides-editor]", "editor both view") noStackedRegions(t, rig, "editor Both view, phone half", "[slides-stage-phone]") }