| new file mode 100644 |
| index 0000000..fcf39fa |
| --- /dev/null |
| +++ b/internal/slidestest/browser_phone_test.go |
| @@ -0,0 +1,135 @@ |
| +//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]") |
| +} |