| 1 | //go:build browser |
| 2 | |
| 3 | // A flow slide on a phone reads top to bottom, one region under the |
| 4 | // next. The container query in slides.css collapses every template to |
| 5 | // one column; the failure this file was written for (polish audit B4, |
| 6 | // 2026-09-23) was that collapse losing a specificity fight — the |
| 7 | // phone's `[slides-region] { grid-area: auto }` (0,1,0) against the |
| 8 | // template's `[slides-region][data-region="heading"] { grid-area: |
| 9 | // heading }` (0,2,0) — so every region kept an area name the collapsed |
| 10 | // grid no longer had, and all of them landed in the same cell: the |
| 11 | // heading drawn over the body on every flow slide, in the viewer, on |
| 12 | // share links and in the editor's Phone and Both views. |
| 13 | package slidestest |
| 14 | |
| 15 | import ( |
| 16 | "encoding/json" |
| 17 | "strconv" |
| 18 | "testing" |
| 19 | |
| 20 | "amadan.net/rastrillo/rastrillo/harness" |
| 21 | "github.com/chromedp/cdproto/runtime" |
| 22 | "github.com/chromedp/chromedp" |
| 23 | ) |
| 24 | |
| 25 | // stackedRegions is a JS expression that answers, for every flow |
| 26 | // slide under root, the pairs of visible regions that overlap: an |
| 27 | // empty list is the pass. Reading order is DOM order after the phone |
| 28 | // layer's own `order`, so the check is on the boxes and not on which |
| 29 | // comes first — two regions in one place is the bug whichever is on |
| 30 | // top. |
| 31 | func stackedRegions(root string) string { |
| 32 | return `JSON.stringify((() => { |
| 33 | const out = []; |
| 34 | for (const s of document.querySelectorAll('` + root + ` [slides-slide][data-layout="flow"]')) { |
| 35 | const rs = [...s.querySelectorAll(':scope > [slides-frame] > [slides-region]')] |
| 36 | .map(e => ({name: e.dataset.region, r: e.getBoundingClientRect()})) |
| 37 | .filter(x => x.r.width > 0 && x.r.height > 0); |
| 38 | if (s.getBoundingClientRect().width > 600) out.push('slide wider than a phone: ' + s.getBoundingClientRect().width); |
| 39 | for (let i = 0; i < rs.length; i++) for (let j = i + 1; j < rs.length; j++) { |
| 40 | const a = rs[i].r, b = rs[j].r; |
| 41 | const overlap = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top); |
| 42 | if (overlap > 0.5) out.push(s.dataset.template + ': ' + rs[i].name + ' [' + a.top.toFixed(1) + '–' + a.bottom.toFixed(1) + |
| 43 | '] overlaps ' + rs[j].name + ' [' + b.top.toFixed(1) + '–' + b.bottom.toFixed(1) + ']'); |
| 44 | } |
| 45 | } |
| 46 | return out; |
| 47 | })())` |
| 48 | } |
| 49 | |
| 50 | func noStackedRegions(t *testing.T, rig *harness.Rig, where, root string) { |
| 51 | t.Helper() |
| 52 | var raw string |
| 53 | var flows int |
| 54 | rig.Run( |
| 55 | chromedp.Evaluate(`document.querySelectorAll('`+root+` [slides-slide][data-layout="flow"]').length`, &flows), |
| 56 | chromedp.Evaluate(stackedRegions(root), &raw), |
| 57 | ) |
| 58 | var bad []string |
| 59 | if err := json.Unmarshal([]byte(raw), &bad); err != nil { |
| 60 | t.Fatalf("%s: read the browser's answer: %v\n%s", where, err, raw) |
| 61 | } |
| 62 | for _, b := range bad { |
| 63 | t.Errorf("%s: %s", where, b) |
| 64 | } |
| 65 | if flows == 0 { |
| 66 | t.Logf("%s: no flow slide on screen", where) |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | // TestBrowserPhoneStacksFlowRegions walks the sample deck in the |
| 71 | // viewer at 390px — every slide of it, so every template it carries — |
| 72 | // and then opens the editor's Phone and Both views on a desktop |
| 73 | // window, and requires that no two regions of a flow slide share any |
| 74 | // height. |
| 75 | func TestBrowserPhoneStacksFlowRegions(t *testing.T) { |
| 76 | rig, mailer := newRig(t) |
| 77 | browserSignIn(t, rig, mailer) |
| 78 | |
| 79 | var deck string |
| 80 | rig.Run(chromedp.Evaluate(`(document.querySelector('a[href^="/d/"]')?.getAttribute('href') ?? '')`, &deck)) |
| 81 | if !deckPath.MatchString(deck) { |
| 82 | t.Fatalf("the deck list links no deck: %q", deck) |
| 83 | } |
| 84 | |
| 85 | var count, flowsSeen int |
| 86 | rig.Run( |
| 87 | chromedp.EmulateViewport(390, 844, chromedp.EmulateMobile), |
| 88 | chromedp.Navigate(rig.Origin+deck), |
| 89 | chromedp.WaitVisible(`[slides-slide]`, chromedp.ByQuery), |
| 90 | chromedp.Evaluate(`document.querySelectorAll('[slides-film] a').length`, &count), |
| 91 | ) |
| 92 | if count < 2 { |
| 93 | t.Fatalf("the viewer lists %d slides; the sample deck has more", count) |
| 94 | } |
| 95 | for n := 1; n <= count; n++ { |
| 96 | var flow int |
| 97 | rig.Run( |
| 98 | chromedp.Navigate(rig.Origin+deck+"?slide="+strconv.Itoa(n)), |
| 99 | chromedp.WaitVisible(`[slides-slide]`, chromedp.ByQuery), |
| 100 | chromedp.Evaluate(`document.fonts.ready.then(() => document.querySelectorAll('[slides-slide][data-layout="flow"]').length)`, &flow, |
| 101 | func(p *runtime.EvaluateParams) *runtime.EvaluateParams { return p.WithAwaitPromise(true) }), |
| 102 | ) |
| 103 | flowsSeen += flow |
| 104 | noStackedRegions(t, rig, "viewer at 390, slide "+strconv.Itoa(n), "body") |
| 105 | if n == 1 { |
| 106 | rig.Screen("body", "viewer at 390, slide 1") |
| 107 | } |
| 108 | } |
| 109 | if flowsSeen < 3 { |
| 110 | t.Errorf("the walk met only %d flow slides; the check needs real ones to mean anything", flowsSeen) |
| 111 | } |
| 112 | |
| 113 | // The editor's Phone and Both views, on a desktop window: the phone |
| 114 | // stage is a slide in a phone-sized container, so the same query |
| 115 | // fires inside a 1440px page. |
| 116 | rig.Run( |
| 117 | chromedp.EmulateViewport(1440, 900), |
| 118 | chromedp.Navigate(rig.Origin+deck+"/edit"), |
| 119 | chromedp.WaitVisible(`[slides-editor]`, chromedp.ByQuery), |
| 120 | waitFor(`document.querySelector('[slides-stage] [slides-slide]')`), |
| 121 | chromedp.Click(`input[name="slides-view"][value="phone"]`, chromedp.ByQuery), |
| 122 | waitFor(`document.querySelector('[slides-stage-pane]').dataset.view === 'phone'`), |
| 123 | waitFor(`document.querySelector('[slides-stage] [slides-slide]')?.getBoundingClientRect().width <= 600`), |
| 124 | ) |
| 125 | rig.Screen("[slides-editor]", "editor phone view") |
| 126 | noStackedRegions(t, rig, "editor Phone view", "[slides-stage]") |
| 127 | |
| 128 | rig.Run( |
| 129 | chromedp.Click(`input[name="slides-view"][value="both"]`, chromedp.ByQuery), |
| 130 | waitFor(`document.querySelector('[slides-stage-pane]').dataset.view === 'both'`), |
| 131 | waitFor(`document.querySelector('[slides-stage-phone] [slides-slide]')`), |
| 132 | ) |
| 133 | rig.Screen("[slides-editor]", "editor both view") |
| 134 | noStackedRegions(t, rig, "editor Both view, phone half", "[slides-stage-phone]") |
| 135 | } |
| 136 | |