oficina / sheets Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build browser
2
3// The browser checks: Sheets in a real Chromium. Not part of the plain
4// suite, because it needs a Chromium (on PATH, via RASTRILLO_CHROME, or in
5// a Playwright cache):
6//
7// make test-browser
8//
9// `make test-vet` vets this file with the tag, so it cannot stop
10// compiling unnoticed.
11package sheets
12
13import (
14 "context"
15 "fmt"
16 "net/http"
17 "strings"
18 "testing"
19 "time"
20
21 "amadan.net/rastrillo/rastrillo/harness"
22 "github.com/chromedp/cdproto/emulation"
23 "github.com/chromedp/cdproto/network"
24 "github.com/chromedp/chromedp"
25)
26
27// textControls is every control a tap puts a caret in. A checkbox, a
28// radio, a file picker or a button raises no keyboard, so iOS never
29// zooms for them.
30const textControls = `input:not([type=checkbox]):not([type=radio]):not([type=hidden]):not([type=file]):not([type=submit]):not([type=button]):not([type=color]):not([type=range]), select, textarea`
31
32// TestNoZoomOnTap: iOS Safari zooms the whole page into any text control
33// whose type is under 16px, and rastrillo's base type is 14px. Its
34// tokens.css brings text controls up to 16px where the pointer is coarse
35// ("Touch: the two zooms nobody asked for"); an app on a tokens.css
36// without that block, or with a rule of its own that sets a control
37// smaller, zooms on the first tap of every field.
38//
39// Emulated as pointer: coarse, which is how the CSS decides. The grid's
40// cell editor is measured too: it only exists once a cell is being
41// edited, and it is the field someone taps most.
42func TestNoZoomOnTap(t *testing.T) {
43 app, mail := appWithRecorder(t)
44 cookies := sessionFor(t, app, mail)
45 sheet, _ := newSheet(t, app, cookies, "Zoom")
46
47 rig := harness.New(t, func(string) http.Handler { return app.Router() })
48 // Signed out first: the door is the field every visit starts on.
49 pages := []struct{ path, ready string }{
50 {"/signin", `input[name="address"]`},
51 {"", ""}, // sign in, below
52 {"/sheets", `main`},
53 {fmt.Sprintf("/sheets/%d", sheet.ID), `td[data-x="0"][data-y="0"]`},
54 {fmt.Sprintf("/sheets/%d/share", sheet.ID), `input[type=email], input[name=address]`},
55 {fmt.Sprintf("/sheets/%d/comments", sheet.ID), `textarea, input[type=text]`},
56 {fmt.Sprintf("/sheets/%d/table", sheet.ID), `main`},
57 }
58 run := func(what string, actions ...chromedp.Action) {
59 t.Helper()
60 ctx, cancel := context.WithTimeout(rig.Context(), 20*time.Second)
61 defer cancel()
62 if err := chromedp.Run(ctx, actions...); err != nil {
63 var at string
64 _ = chromedp.Run(rig.Context(), chromedp.Location(&at))
65 t.Fatalf("%s: %v (the browser is at %s)", what, err, at)
66 }
67 }
68 run("touch emulation",
69 // Touch emulation, then a reload so the page re-evaluates its
70 // media queries. SetEmulatedMedia's "pointer" feature is not
71 // emulated by this Chromium; the coarse check below is what
72 // proves the emulation took.
73 emulation.SetTouchEmulationEnabled(true).WithMaxTouchPoints(5),
74 )
75 for _, page := range pages {
76 if page.path == "" {
77 // The session signIn's real magic-link flow minted, handed
78 // to the browser.
79 var set []chromedp.Action
80 for _, c := range cookies {
81 set = append(set, network.SetCookie(c.Name, c.Value).WithURL(rig.Origin+"/").
82 WithHTTPOnly(c.HttpOnly).WithSecure(c.Secure).WithSameSite(network.CookieSameSiteLax))
83 }
84 run("sign in", set...)
85 continue
86 }
87 var coarse bool
88 var small []string
89 run(page.path,
90 chromedp.Navigate(rig.Origin+page.path),
91 chromedp.Reload(),
92 chromedp.WaitVisible(page.ready),
93 chromedp.Evaluate(`matchMedia('(pointer: coarse)').matches`, &coarse),
94 chromedp.Evaluate(smallControls(textControls), &small),
95 )
96 if !coarse {
97 t.Fatalf("%s: the touch-screen emulation did not apply; this would measure desktop styles", page.path)
98 }
99 if len(small) > 0 {
100 t.Errorf("%s: iOS zooms when these are tapped (under 16px on a touch screen): %s", page.path, strings.Join(small, "; "))
101 }
102 }
103
104 // The cell editor, opened the way a double-tap opens it.
105 var small []string
106 run("the cell editor",
107 chromedp.Navigate(fmt.Sprintf("%s/sheets/%d", rig.Origin, sheet.ID)),
108 chromedp.WaitVisible(`td[data-x="0"][data-y="0"]`),
109 chromedp.DoubleClick(`td[data-x="0"][data-y="0"]`),
110 chromedp.WaitVisible(`td[data-x="0"][data-y="0"] input, td[data-x="0"][data-y="0"] textarea, td[data-x="0"][data-y="0"] [contenteditable]`),
111 chromedp.Evaluate(smallControls(`td[data-x="0"][data-y="0"] input, td[data-x="0"][data-y="0"] textarea, td[data-x="0"][data-y="0"] [contenteditable]`), &small),
112 )
113 if len(small) > 0 {
114 t.Errorf("the cell editor zooms when a cell is tapped: %s", strings.Join(small, "; "))
115 }
116}
117
118// TestFormatMenuFitsOnTap: the Format menu's two size rows (label, a
119// number field, Set) are sized to the panel's 15rem. On a touch screen
120// the field is 16px type and the button is padded for a finger, and Set
121// hung 11-21px past the panel's edge, half of it off the menu, at every
122// width. sheets.css narrows the label column under a coarse pointer;
123// this measures the result at 320 and 390 wide: each Set button inside
124// its panel, and its centre landing on the button, not on whatever is
125// behind the menu.
126func TestFormatMenuFitsOnTap(t *testing.T) {
127 app, cookies := signIn(t)
128 sheet, _ := newSheet(t, app, cookies, "Format")
129 rig := harness.New(t, func(string) http.Handler { return app.Router() })
130 var set []chromedp.Action
131 for _, c := range cookies {
132 set = append(set, network.SetCookie(c.Name, c.Value).WithURL(rig.Origin+"/").
133 WithHTTPOnly(c.HttpOnly).WithSecure(c.Secure).WithSameSite(network.CookieSameSiteLax))
134 }
135 set = append(set, emulation.SetTouchEmulationEnabled(true).WithMaxTouchPoints(5))
136 rig.Run(set...)
137 for _, width := range []int64{320, 390} {
138 var coarse bool
139 var problems []string
140 ctx, cancel := context.WithTimeout(rig.Context(), 20*time.Second)
141 err := chromedp.Run(ctx,
142 emulation.SetDeviceMetricsOverride(width, 844, 1, true),
143 chromedp.Navigate(fmt.Sprintf("%s/sheets/%d", rig.Origin, sheet.ID)),
144 chromedp.Reload(),
145 chromedp.WaitVisible(`td[data-x="0"][data-y="0"]`),
146 chromedp.Evaluate(`matchMedia('(pointer: coarse)').matches`, &coarse),
147 chromedp.Evaluate(`[...document.querySelectorAll('[estilo-menubar-title]')].find(e => e.textContent.trim() === 'Format').click()`, nil),
148 chromedp.WaitVisible(`[data-size-form] button`),
149 chromedp.Evaluate(`[...document.querySelectorAll('[data-size-form] button')].flatMap(b => {
150 const r = b.getBoundingClientRect(), m = b.closest('[estilo-menu]').getBoundingClientRect()
151 const out = []
152 if (r.left < m.left || r.right > m.right || r.top < m.top || r.bottom > m.bottom)
153 out.push(b.closest('form').dataset.axis + ': Set spans ' + Math.round(r.left) + '-' + Math.round(r.right) + ', its panel ' + Math.round(m.left) + '-' + Math.round(m.right))
154 const hit = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2)
155 if (!hit || !b.contains(hit)) out.push(b.closest('form').dataset.axis + ': a tap on Set lands on ' + (hit ? hit.tagName.toLowerCase() : 'nothing'))
156 return out
157 })`, &problems),
158 )
159 cancel()
160 if err != nil {
161 t.Fatalf("%dpx: %v", width, err)
162 }
163 if !coarse {
164 t.Fatalf("%dpx: the touch-screen emulation did not apply", width)
165 }
166 for _, p := range problems {
167 t.Errorf("%dpx: %s", width, p)
168 }
169 }
170}
171
172// smallControls is a script listing each visible control matching sel
173// whose computed font-size is under 16px, described well enough to find.
174func smallControls(sel string) string {
175 return fmt.Sprintf(`[...document.querySelectorAll(%q)]
176 .filter(e => e.getClientRects().length > 0)
177 .filter(e => parseFloat(getComputedStyle(e).fontSize) < 16)
178 .map(e => e.tagName.toLowerCase() + (e.name ? '[name=' + e.name + ']' : '') + (e.className ? '.' + String(e.className).trim().split(/\s+/).join('.') : '') + ' ' + getComputedStyle(e).fontSize)`, sel)
179}
180