Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Refresh the web surface with Everforest, readable type, and clearer navigation

796ac8c0ce77461174cf520d5fad4dea37744662 · Keenan Nemetz · 12d ago · parent ac00929f

server/ui_test.go +215−11

@@ -86,6 +86,130 @@ func TestStaticAssetsAreServedFromTheBinary(t *testing.T) {
86 86 }
87 87 }
88 88
89 // Everforest is the permanent theme: the shell starts on dark/medium, the
90 // account page owns the picker, and every variant is scoped to the attribute
91 // theme.js sets. If the shell stops naming either file the picker does nothing.
92 func TestEverforestThemesAndAccountPicker(t *testing.T) {
93 t.Parallel()
94 f := newFixture(t)
95
96 code, shell := f.get("/")
97 if code != 200 {
98 t.Fatalf("landing page = %d", code)
99 }
100 for _, want := range []string{
101 `src="/static/theme.js"`,
102 `href="/static/everforest.css"`,
103 `data-theme="everforest-dark-medium"`,
104 `data-font-scale="medium"`,
105 } {
106 if !strings.Contains(shell, want) {
107 t.Errorf("the shell does not carry %s", want)
108 }
109 }
110 if strings.Contains(shell, "data-theme-select") {
111 t.Error("the theme picker belongs on the account page, not in the global bar")
112 }
113 if script, sheet := strings.Index(shell, "/static/theme.js"), strings.Index(shell, "/static/vole.css"); script < 0 || sheet < 0 || script > sheet {
114 t.Error("theme.js must run before the stylesheet so a stored choice does not flash")
115 }
116
117 _, cookie := f.browserSession(t, f.ownerID)
118 resp, account := f.browserGet(t, "/account", cookie)
119 if resp.StatusCode != 200 {
120 t.Fatalf("/account = %d", resp.StatusCode)
121 }
122 if !strings.Contains(account, "data-theme-select") || !strings.Contains(account, `value="system"`) {
123 t.Fatal("the account page does not carry the theme picker")
124 }
125 if !strings.Contains(account, "data-font-scale-select") {
126 t.Fatal("the account page does not carry the text-size picker")
127 }
128 for _, size := range []string{"small", "medium", "large"} {
129 if !strings.Contains(account, `value="`+size+`"`) {
130 t.Errorf("the text-size picker does not offer %s", size)
131 }
132 }
133 for _, variant := range []string{
134 "everforest-dark-hard",
135 "everforest-dark-medium",
136 "everforest-dark-soft",
137 "everforest-light-hard",
138 "everforest-light-medium",
139 "everforest-light-soft",
140 } {
141 if !strings.Contains(account, `value="`+variant+`"`) {
142 t.Errorf("the theme picker does not offer %s", variant)
143 }
144 }
145
146 code, css := f.get("/static/everforest.css")
147 if code != 200 {
148 t.Fatalf("/static/everforest.css = %d", code)
149 }
150 for _, variant := range []string{
151 "everforest-dark-hard",
152 "everforest-dark-medium",
153 "everforest-dark-soft",
154 "everforest-light-hard",
155 "everforest-light-medium",
156 "everforest-light-soft",
157 } {
158 if !strings.Contains(css, `data-theme="`+variant+`"`) {
159 t.Errorf("the Everforest sheet does not define %s", variant)
160 }
161 }
162 if !strings.Contains(css, `--ef-green: #a7c080`) {
163 t.Fatal("the Everforest sheet does not carry the dark palette")
164 }
165 if strings.Contains(css, "\nbody {") {
166 t.Fatal("the Everforest sheet has an unscoped body rule and would restyle the fallback")
167 }
168
169 code, base := f.get("/static/vole.css")
170 if code != 200 {
171 t.Fatalf("/static/vole.css = %d", code)
172 }
173 for _, want := range []string{`--font-scale: 0.9`, `--font-scale: 1.1`, `font-size: calc(13.2px * var(--font-scale))`} {
174 if !strings.Contains(base, want) {
175 t.Errorf("the base sheet does not carry the text-size scaling %s", want)
176 }
177 }
178
179 code, script := f.get("/static/theme.js")
180 if code != 200 {
181 t.Fatalf("/static/theme.js = %d", code)
182 }
183 for _, want := range []string{"localStorage", "dataset.theme", "prefers-color-scheme", "everforest-light-medium", "dataset.fontScale", "vole-font-scale"} {
184 if !strings.Contains(script, want) {
185 t.Errorf("the theme script does not carry %s", want)
186 }
187 }
188 }
189
190 // The account page used to render its tables bare, so they had no border or
191 // rounded corners. Every data table on a page surface belongs in a panel.
192 func TestAccountTablesUseThePanelSurface(t *testing.T) {
193 t.Parallel()
194 f := newFixture(t)
195 _, cookie := f.browserSession(t, f.ownerID)
196 resp, body := f.browserGet(t, "/account", cookie)
197 if resp.StatusCode != 200 {
198 t.Fatalf("/account = %d", resp.StatusCode)
199 }
200 for _, heading := range []string{"SSH keys", "Sessions"} {
201 at := strings.Index(body, "<h2>"+heading+"</h2>")
202 if at < 0 {
203 t.Fatalf("account page has no %s section", heading)
204 }
205 section := body[at:]
206 panel, table := strings.Index(section, `<div class="panel">`), strings.Index(section, "<table")
207 if panel < 0 || table < 0 || panel > table {
208 t.Errorf("%s table is not inside a panel", heading)
209 }
210 }
211 }
212
89 213 // HTMX adds its indicator styles with a <style> element unless told not to.
90 214 // style-src 'self' blocks that injection, so the shell must carry the config
91 215 // that turns it off and the stylesheet must carry the rules it would have added.
@@ -109,14 +233,14 @@ func TestHTMXDoesNotInjectInlineStyles(t *testing.T) {
109 233 }
110 234 }
111 235
112 // TestTheEmblemShipsAndTheShellNamesIt: the mark and the favicon are one file,
113 // so a page that points at a name nobody ships is a broken tab icon on every
114 // page. This is the test that would have caught the rename.
236 // TestTheEmblemShipsAndTheShellNamesIt: the header mask and the favicon are
237 // separate assets now, so a page that points at a name nobody ships is still a
238 // broken mark or tab icon. This is the test that would have caught the rename.
115 239 func TestTheEmblemShipsAndTheShellNamesIt(t *testing.T) {
116 240 t.Parallel()
117 241 f := newFixture(t)
118 242
119 for _, name := range []string{"logo.png", "favicon-32.png", "favicon-16.png"} {
243 for _, name := range []string{"logo.png", "vole-head.png", "favicon-32.png", "favicon-32-dark.png", "favicon-16.png", "favicon-16-dark.png"} {
120 244 resp, err := http.Get(f.http.URL + "/static/" + name)
121 245 if err != nil {
122 246 t.Fatal(err)
@@ -144,15 +268,95 @@ func TestTheEmblemShipsAndTheShellNamesIt(t *testing.T) {
144 268 if code != http.StatusOK {
145 269 t.Fatalf("landing page = %d", code)
146 270 }
147 if !strings.Contains(shell, `href="/static/favicon-32.png"`) {
148 t.Error("the shell does not name the emblem as its icon")
271 if !strings.Contains(shell, `href="/static/favicon-32.png"`) ||
272 !strings.Contains(shell, `href="/static/favicon-32-dark.png"`) ||
273 !strings.Contains(shell, `href="/static/favicon.svg"`) ||
274 !strings.Contains(shell, `media="(prefers-color-scheme: dark)"`) {
275 t.Error("the shell does not name the light/dark fallback and SVG favicons")
276 }
277 if !strings.Contains(shell, `<span class="mark"`) {
278 t.Error("the header does not use the head mark")
149 279 }
150 if !strings.Contains(shell, `src="/static/logo.png"`) {
151 t.Error("the header does not use the emblem")
280 code, css := f.get("/static/vole.css")
281 if code != http.StatusOK {
282 t.Fatalf("stylesheet = %d", code)
283 }
284 if !strings.Contains(css, `url("/static/vole-head.png")`) || !strings.Contains(css, "background-color: currentColor") {
285 t.Error("the head mark is not a theme-coloured mask")
286 }
287
288 code, svg := f.get("/static/favicon.svg")
289 if code != http.StatusOK || !strings.Contains(svg, "<svg") {
290 t.Fatalf("/static/favicon.svg = %d and does not look like an SVG", code)
291 }
292 if !strings.Contains(svg, "prefers-color-scheme") || !strings.Contains(svg, "#000") || !strings.Contains(svg, "#fff") {
293 t.Error("the SVG favicon does not switch between black and white with the browser preference")
294 }
295 if _, script := f.get("/static/theme.js"); strings.Contains(script, "favicon") {
296 t.Error("the app theme must not choose the favicon; the browser window preference does")
297 }
298 if strings.Contains(shell, "logo.svg") {
299 t.Error("the shell still points at the removed logo.svg")
300 }
301 }
302
303 // Lexend and JetBrains Mono are embedded, not fetched from a CDN. The WOFF2
304 // magic and the OFL files beside them are the two things a deployer cannot
305 // discover from the page shell alone.
306 func TestSelfHostedFontsAreEmbedded(t *testing.T) {
307 t.Parallel()
308 f := newFixture(t)
309
310 for _, name := range []string{
311 "lexend-latin.woff2",
312 "lexend-latin-ext.woff2",
313 "jetbrains-mono-latin.woff2",
314 "jetbrains-mono-latin-ext.woff2",
315 "jetbrains-mono-italic-latin.woff2",
316 "jetbrains-mono-italic-latin-ext.woff2",
317 } {
318 resp, err := http.Get(f.http.URL + "/static/fonts/" + name)
319 if err != nil {
320 t.Fatal(err)
321 }
322 if resp.StatusCode != http.StatusOK {
323 resp.Body.Close()
324 t.Fatalf("/static/fonts/%s = %d", name, resp.StatusCode)
325 }
326 if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "font/woff2") {
327 resp.Body.Close()
328 t.Fatalf("%s Content-Type = %q", name, ct)
329 }
330 head := make([]byte, 4)
331 if _, err := io.ReadFull(resp.Body, head); err != nil {
332 resp.Body.Close()
333 t.Fatal(err)
334 }
335 resp.Body.Close()
336 if string(head) != "wOF2" {
337 t.Errorf("%s is not a WOFF2 file: %x", name, head)
338 }
339 }
340
341 code, css := f.get("/static/vole.css")
342 if code != http.StatusOK {
343 t.Fatalf("/static/vole.css = %d", code)
344 }
345 for _, want := range []string{
346 `@font-face`,
347 `font-family: "Lexend"`,
348 `font-family: "JetBrains Mono"`,
349 `/static/fonts/lexend-latin.woff2`,
350 `/static/fonts/jetbrains-mono-latin.woff2`,
351 } {
352 if !strings.Contains(css, want) {
353 t.Errorf("the stylesheet does not carry %s", want)
354 }
152 355 }
153 for _, gone := range []string{"logo.svg", "favicon.svg"} {
154 if strings.Contains(shell, gone) {
155 t.Errorf("the shell still points at the removed %s", gone)
356 for _, name := range []string{"OFL-Lexend.txt", "OFL-JetBrainsMono.txt"} {
357 code, body := f.get("/static/fonts/" + name)
358 if code != http.StatusOK || !strings.Contains(body, "SIL OPEN FONT LICENSE") {
359 t.Errorf("/static/fonts/%s is missing or does not look like the OFL text", name)
156 360 }
157 361 }
158 362 }