rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1//go:build browser
2
3// The API token list on a phone and on a desktop, from rastrillo's
4// ui/apitokens_browser_test.go at the seed commit, its page read
5// through fixture.
6package check
7
8import (
9 "context"
10 "encoding/json"
11 "math"
12 "testing"
13 "time"
14
15 "github.com/chromedp/chromedp"
16)
17
18// tokensFactHeadings are the four facts a token row shows beside its
19// identity, in the row's order, each the column heading the seeded
20// page's head row carries (rastrillo read them from its catalog under
21// api_tokens_access, api_tokens_col_created, api_tokens_col_last_used
22// and api_tokens_expires).
23func tokensFactHeadings() []string {
24 return []string{"Access", "Created", "Last used", "Expires"}
25}
26
27// tokensRows is how many token rows the seeded page lists: the
28// four-token fixture, as rastrillo's tokensPage() built it.
29const tokensRows = 4
30
31// tokensPageDoc is the token page with the four-token fixture in a
32// page, the way an app renders it.
33func tokensPageDoc(t *testing.T) string {
34 t.Helper()
35 return sizingDoc("api-tokens", fixture(t, "apitokens_browser_test.go", "tokensPageDoc", ""))
36}
37
38// tokensFact is one fact cell as measured: its box, its label's box and
39// words, its font size, and the value's words.
40type tokensFact struct {
41 Box box
42 Label *box
43 Heading string
44 Value string
45 FontSize float64
46}
47
48// tokensRow is one token row as measured: the row, its identity cell,
49// its facts, and its kebab when it has one.
50type tokensRow struct {
51 ID string
52 Row box
53 Name box
54 Facts []tokensFact
55 Kebab *box
56}
57
58const tokensListJS = `(() => {
59 const r = el => { if (!el) return null; const b = el.getBoundingClientRect(); return {L: b.left, T: b.top, R: b.right, B: b.bottom}; };
60 const vis = el => { if (!el) return null; const b = el.getBoundingClientRect(); return b.width > 0 && b.height > 0 ? r(el) : null; };
61 const rows = [];
62 document.querySelectorAll('[rst-tokens-list] [rst-lrow]:not([rst-lrow~="head"])').forEach(row => {
63 const facts = [];
64 row.querySelectorAll("[rst-tokens-fact]").forEach(f => {
65 const label = f.querySelector("[rst-tokens-fact-label]");
66 facts.push({Box: r(f), Label: vis(label), Heading: label ? label.textContent : "",
67 Value: (f.textContent || "").replace(label ? label.textContent : "", "").trim(),
68 FontSize: parseFloat(getComputedStyle(f).fontSize)});
69 });
70 rows.push({ID: row.querySelector("code").textContent, Row: r(row), Name: r(row.querySelector(".rst-nm")),
71 Facts: facts, Kebab: vis(row.querySelector("[rst-row-menu] > summary"))});
72 });
73 const heads = [...document.querySelectorAll('[rst-tokens-list] [rst-lrow~="head"] > .rst-m-hide')].filter(h => vis(h)).length;
74 const floor = parseFloat(getComputedStyle(document.documentElement).getPropertyValue("--rst-target")) * parseFloat(getComputedStyle(document.documentElement).fontSize);
75 return JSON.stringify({Rows: rows, Heads: heads, Floor: floor, Width: innerWidth});
76})()`
77
78type tokensReading struct {
79 Rows []tokensRow
80 Heads int // the head row's fact headings that are visible
81 Floor float64
82 Width int
83}
84
85func readTokens(t *testing.T, ctx context.Context) tokensReading {
86 t.Helper()
87 var raw string
88 if err := chromedp.Run(ctx, chromedp.Evaluate(tokensListJS, &raw)); err != nil {
89 t.Fatalf("reading the token list: %v", err)
90 }
91 var got tokensReading
92 if err := json.Unmarshal([]byte(raw), &got); err != nil {
93 t.Fatalf("decoding %q: %v", raw, err)
94 }
95 if len(got.Rows) != tokensRows {
96 t.Fatalf("read %d rows, want %d; the selectors no longer match the partial", len(got.Rows), tokensRows)
97 }
98 return got
99}
100
101// TestAPITokensFactsStayVisibleOnAPhone is the list's claim on a phone:
102// a person checks a token's access, when it was made, when it was last
103// used and when it expires before revoking it, so the four facts are
104// not dropped at a phone's width. They stack under the identity, each
105// labelled with the column heading the desktop list shows in its head
106// row, at the phone's 16px type; the kebab stays at the 44px floor.
107// On a desktop the facts sit in the row's own line under the head row's
108// headings, and the labels, which would repeat those, are not shown.
109func TestAPITokensFactsStayVisibleOnAPhone(t *testing.T) {
110 headings := tokensFactHeadings()
111 for _, leg := range []struct {
112 name string
113 w, h int64
114 coarse bool
115 floor float64
116 phone bool
117 }{
118 {"1024 mouse", 1024, 768, false, 32, false},
119 {"390 touch", 390, 844, true, 44, true},
120 {"320 touch", 320, 568, true, 44, true},
121 } {
122 t.Run(leg.name, func(t *testing.T) {
123 rig := mediaRig(t, leg.coarse, sizingMux(t, map[string]string{"/": tokensPageDoc(t)}))
124 ctx, cancel := context.WithTimeout(rig.Context(), 120*time.Second)
125 defer cancel()
126 if err := chromedp.Run(ctx, chromedp.EmulateViewport(leg.w, leg.h), chromedp.Navigate(rig.Origin+"/"), chromedp.WaitVisible("[rst-tokens-list]", chromedp.ByQuery)); err != nil {
127 t.Fatal(err)
128 }
129 requirePointer(t, ctx, leg.coarse)
130 got := readTokens(t, ctx)
131 if got.Floor != leg.floor {
132 t.Errorf("--rst-target is %vpx, want %v", got.Floor, leg.floor)
133 }
134 if leg.phone && got.Heads != 0 {
135 t.Errorf("the head row shows %d fact headings on a phone; the facts carry their own labels there", got.Heads)
136 }
137 if !leg.phone && got.Heads != len(headings) {
138 t.Errorf("the head row shows %d fact headings on a desktop, want %d", got.Heads, len(headings))
139 }
140 for _, row := range got.Rows {
141 where := leg.name + ": " + row.ID
142 if len(row.Facts) != len(headings) {
143 t.Errorf("%s: the row shows %d facts, want its access, created, last used and expires", where, len(row.Facts))
144 continue
145 }
146 for i, f := range row.Facts {
147 if f.Value == "" && !leg.phone {
148 // A token never used has nothing in its cell on a
149 // desktop, and an empty cell has no height; on a
150 // phone its label still shows it.
151 continue
152 }
153 if f.Box.w() <= 0 || f.Box.B-f.Box.T <= 0 {
154 t.Errorf("%s: fact %q is not visible", where, headings[i])
155 continue
156 }
157 if !insideBox(f.Box, row.Row) {
158 t.Errorf("%s: fact %q at %+v is outside its row %+v", where, headings[i], f.Box, row.Row)
159 }
160 if f.Heading != headings[i] {
161 t.Errorf("%s: fact %d is labelled %q, want %q", where, i, f.Heading, headings[i])
162 }
163 if leg.phone {
164 if f.Label == nil {
165 t.Errorf("%s: fact %q has no visible label on a phone", where, headings[i])
166 }
167 if f.Box.T < row.Name.B-1 {
168 t.Errorf("%s: fact %q at %+v is not stacked under the identity %+v", where, headings[i], f.Box, row.Name)
169 }
170 if f.FontSize < 16 {
171 t.Errorf("%s: fact %q is %.1fpx type on a phone, want 16", where, headings[i], f.FontSize)
172 }
173 } else {
174 if f.Label != nil {
175 t.Errorf("%s: fact %q repeats its heading %q under the head row on a desktop", where, headings[i], f.Heading)
176 }
177 if f.Box.T >= row.Name.B-1 {
178 t.Errorf("%s: fact %q at %+v is below the identity %+v on a desktop, want the same line", where, headings[i], f.Box, row.Name)
179 }
180 }
181 }
182 if row.Kebab != nil {
183 if w, h := row.Kebab.w(), row.Kebab.B-row.Kebab.T; math.Min(w, h) < leg.floor-0.5 {
184 t.Errorf("%s: the kebab is %.0f×%.0f, under the %.0fpx floor", where, w, h, leg.floor)
185 }
186 if row.Kebab.T < row.Row.T-1 || row.Kebab.T >= row.Name.B {
187 t.Errorf("%s: the kebab at %+v is not on the identity's line %+v", where, *row.Kebab, row.Name)
188 }
189 }
190 }
191 })
192 }
193}
194