oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1//go:build browser
2
3// The presenting screens with their script on. The Go tests read the
4// markup; only a browser runs static/present.js against it, and the
5// failure this file was written for (polish audit B3, 2026-09-23) was
6// the script and the markup disagreeing: the presenter view and the
7// remote carried no data-slide, so the page believed it was on slide
8// 0, the stream's first event said slide 1, and the page reloaded —
9// and did the same on every load after, about fifty times a second.
10package slidestest
11
12import (
13 "fmt"
14 "regexp"
15 "sync/atomic"
16 "testing"
17 "time"
18
19 "github.com/chromedp/cdproto/emulation"
20 "github.com/chromedp/cdproto/page"
21 "github.com/chromedp/chromedp"
22 "github.com/chromedp/chromedp/kb"
23)
24
25// TestBrowserProjectorFitsTheViewport keeps the projector a stage rather
26// than a long document: its slide is bounded by the viewport and its
27// controls are overlaid where the presenter can still reach them.
28func TestBrowserProjectorFitsTheViewport(t *testing.T) {
29 rig, mailer := newRig(t)
30 browserSignIn(t, rig, mailer)
31
32 var deck string
33 rig.Run(chromedp.Evaluate(`(document.querySelector('a[href^="/d/"]')?.getAttribute('href') ?? '')`, &deck))
34 if !deckPath.MatchString(deck) {
35 t.Fatalf("the deck list links no deck: %q", deck)
36 }
37
38 var path string
39 rig.Run(
40 chromedp.Navigate(rig.Origin+deck),
41 chromedp.Click(`form[action$="/present"] button`, chromedp.ByQuery),
42 waitFor(`location.pathname.endsWith('/notes')`),
43 chromedp.Evaluate(`location.pathname + location.search`, &path),
44 )
45 m := notesLocation.FindStringSubmatch(path)
46 if m == nil {
47 t.Fatalf("Present landed on %q, want /p/{session}/notes?t={token}", path)
48 }
49
50 for _, viewport := range []struct{ width, height int }{{1920, 1080}, {1280, 720}} {
51 t.Run(fmt.Sprintf("%dx%d", viewport.width, viewport.height), func(t *testing.T) {
52 var geometry struct {
53 ScrollHeight float64 `json:"scrollHeight"`
54 Viewport float64 `json:"viewport"`
55 ScrollY float64 `json:"scrollY"`
56 Top float64 `json:"top"`
57 Bottom float64 `json:"bottom"`
58 }
59 rig.Run(
60 emulation.SetDeviceMetricsOverride(int64(viewport.width), int64(viewport.height), 1, false),
61 chromedp.Navigate(rig.Origin+m[1]+"?t="+m[2]),
62 chromedp.WaitVisible(`[slides-bar]`, chromedp.ByQuery),
63 chromedp.Evaluate(`(() => {
64 window.scrollTo(0, 1);
65 const controls = document.querySelector('[slides-bar]').getBoundingClientRect();
66 return {
67 scrollHeight: document.documentElement.scrollHeight,
68 viewport: window.innerHeight,
69 scrollY: window.scrollY,
70 top: controls.top,
71 bottom: controls.bottom,
72 };
73 })()`, &geometry),
74 )
75 if geometry.ScrollHeight > geometry.Viewport || geometry.ScrollY != 0 {
76 t.Errorf("projector scrolls: document %.0fpx, viewport %.0fpx, scrollY %.0f", geometry.ScrollHeight, geometry.Viewport, geometry.ScrollY)
77 }
78 if geometry.Top < 0 || geometry.Bottom > geometry.Viewport {
79 t.Errorf("controls are outside the viewport: top %.0fpx, bottom %.0fpx, viewport %.0fpx", geometry.Top, geometry.Bottom, geometry.Viewport)
80 }
81 })
82 }
83}
84
85var notesLocation = regexp.MustCompile(`^(/p/[23456789abcdefghjkmnpqrstuvwxyz]{10})/notes\?t=([A-Za-z0-9_-]{43})$`)
86
87// TestBrowserPresentingScreensHoldStill: the presenter view and the
88// phone remote each load once and then stay put while nothing
89// changes, and each reloads exactly once when the slide does move —
90// which is how those two pages follow a talk (present.js reloads them
91// rather than re-rendering two slides and the notes itself).
92func TestBrowserPresentingScreensHoldStill(t *testing.T) {
93 rig, mailer := newRig(t)
94 browserSignIn(t, rig, mailer)
95
96 // Every top-level navigation the tab makes, reloads included.
97 var loads atomic.Int64
98 chromedp.ListenTarget(rig.Context(), func(ev any) {
99 if e, ok := ev.(*page.EventFrameNavigated); ok && e.Frame.ParentID == "" {
100 loads.Add(1)
101 }
102 })
103
104 var deck string
105 rig.Run(chromedp.Evaluate(`(document.querySelector('a[href^="/d/"]')?.getAttribute('href') ?? '')`, &deck))
106 if !deckPath.MatchString(deck) {
107 t.Fatalf("the deck list links no deck: %q", deck)
108 }
109
110 var path string
111 rig.Run(
112 chromedp.Navigate(rig.Origin+deck),
113 chromedp.Click(`form[action$="/present"] button`, chromedp.ByQuery),
114 waitFor(`location.pathname.endsWith('/notes')`),
115 chromedp.Evaluate(`location.pathname + location.search`, &path),
116 )
117 m := notesLocation.FindStringSubmatch(path)
118 if m == nil {
119 t.Fatalf("Present landed on %q, want /p/{session}/notes?t={token}", path)
120 }
121 session, token := m[1], m[2]
122
123 // holdsStill waits for the page to settle and the stream to have
124 // said its piece (one event on connect), then counts loads over a
125 // window long enough for the broken page to have reloaded hundreds
126 // of times.
127 holdsStill := func(name string) {
128 t.Helper()
129 rig.Run(chromedp.Sleep(1500 * time.Millisecond))
130 before := loads.Load()
131 rig.Run(chromedp.Sleep(3 * time.Second))
132 if n := loads.Load() - before; n != 0 {
133 t.Errorf("the %s loaded %d more times in 3s with nothing changing, want 0", name, n)
134 }
135 // And it holds still because it knows where it is, not only
136 // because present.js refuses to reload twice for one slide:
137 // the script's root names the slide the counter shows.
138 var marked, counter string
139 rig.Run(
140 chromedp.Evaluate(`document.querySelector('[data-session]')?.getAttribute('data-slide') ?? 'missing'`, &marked),
141 chromedp.Evaluate(`(document.querySelector('[slides-counter]')?.textContent ?? '').trim().split(' ')[0]`, &counter),
142 )
143 if marked != counter {
144 t.Errorf("the %s's script root says data-slide=%q; the counter shows slide %q", name, marked, counter)
145 }
146 }
147 // movesOnce presses the right arrow — present.js posts `next` — and
148 // the page must reload onto the new slide once, and only once.
149 movesOnce := func(name, counter string) {
150 t.Helper()
151 before := loads.Load()
152 rig.Run(
153 chromedp.KeyEvent(kb.ArrowRight),
154 waitFor(`document.querySelector('[slides-counter]')?.textContent.trim().startsWith('`+counter+` /')`),
155 chromedp.Sleep(3*time.Second),
156 )
157 if n := loads.Load() - before; n != 1 {
158 t.Errorf("the %s loaded %d times after one slide change, want exactly 1", name, n)
159 }
160 }
161
162 holdsStill("presenter view")
163 rig.Screen("body", "presenter view")
164 movesOnce("presenter view", "2")
165
166 rig.Run(chromedp.Navigate(rig.Origin + session + "/remote?t=" + token))
167 holdsStill("remote")
168 rig.Screen("body", "remote")
169 movesOnce("remote", "3")
170}
171