oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1package slidestest
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "fmt"
7 "net/http"
8 "os"
9 "path/filepath"
10 "regexp"
11 "strings"
12 "testing"
13
14 "amadan.net/oficina/slides/internal/slides/fonts"
15)
16
17// This file holds slides.css's half of "one font set everywhere" to
18// the Go half. The stylesheet is a static file — nothing templates it
19// — so the twelve @font-face URLs, the fallback stacks and the served
20// bytes are all things that can drift silently. These tests are what
21// notice.
22
23func slidesCSS(t *testing.T) string {
24 t.Helper()
25 b, err := os.ReadFile(filepath.Join("..", "slides", "static", "slides.css"))
26 if err != nil {
27 t.Fatalf("read slides.css: %v", err)
28 }
29 return string(b)
30}
31
32// fontFaceSrc pulls each @font-face's family, style, weight and URL
33// out in the order slides.css writes them.
34var fontFaceRule = regexp.MustCompile(`(?s)@font-face \{(.*?)\}`)
35
36type faceRule struct {
37 family, style, weight, url string
38}
39
40func parseFontFaces(t *testing.T, css string) []faceRule {
41 t.Helper()
42 field := func(body, name string) string {
43 re := regexp.MustCompile(name + `:\s*([^;]+);`)
44 m := re.FindStringSubmatch(body)
45 if m == nil {
46 return ""
47 }
48 return strings.Trim(strings.TrimSpace(m[1]), `"`)
49 }
50 var out []faceRule
51 for _, m := range fontFaceRule.FindAllStringSubmatch(css, -1) {
52 body := m[1]
53 src := field(body, "src")
54 u := regexp.MustCompile(`url\("([^"]+)"\)`).FindStringSubmatch(src)
55 if u == nil {
56 t.Errorf("an @font-face has no url(): %q", src)
57 continue
58 }
59 out = append(out, faceRule{
60 family: field(body, "font-family"),
61 style: field(body, "font-style"),
62 weight: field(body, "font-weight"),
63 url: u[1],
64 })
65 }
66 return out
67}
68
69// Every one of the twelve embedded faces is declared, once, under its
70// own family name and with the weight and slope that select it. A face
71// the engine measures with and the browser cannot fetch is the
72// divergence this whole font set exists to close.
73func TestSlidesCSSDeclaresEveryEmbeddedFace(t *testing.T) {
74 rules := parseFontFaces(t, slidesCSS(t))
75 got := map[string]string{}
76 for _, r := range rules {
77 key := fmt.Sprintf("%s|%s|%s", r.family, r.style, r.weight)
78 if prev, dup := got[key]; dup {
79 t.Errorf("%s is declared twice (%s and %s)", key, prev, r.url)
80 }
81 got[key] = r.url
82 }
83 for _, f := range fonts.All() {
84 style, weight := "normal", "400"
85 if f.Italic {
86 style = "italic"
87 }
88 if f.Bold {
89 weight = "700"
90 }
91 key := fmt.Sprintf("%s|%s|%s", f.Family.CSSName(), style, weight)
92 url, ok := got[key]
93 if !ok {
94 t.Errorf("slides.css declares no @font-face for %s (%s)", f.File(), key)
95 continue
96 }
97 if !strings.HasPrefix(url, "fonts/"+strings.TrimSuffix(f.File(), ".ttf")+".") {
98 t.Errorf("%s is declared as %q, which is not that face's file", key, url)
99 }
100 delete(got, key)
101 }
102 for key, url := range got {
103 t.Errorf("slides.css declares %s (%s), which is not a face this build embeds", key, url)
104 }
105}
106
107// The URLs carry a content hash so rastrillo's asset handler serves
108// them immutable rather than no-cache — a slide is a page load, and
109// refetching a megabyte of fonts on every next-slide link is not a
110// cost worth paying for a set of files that never change.
111//
112// A stale hash is not a broken page (the handler strips it, finds the
113// file and serves it no-cache), so this failing means "slower", not
114// "broken" — the message says what to write instead.
115func TestSlidesCSSFontURLsCarryTheirContentHash(t *testing.T) {
116 for _, r := range parseFontFaces(t, slidesCSS(t)) {
117 name := strings.TrimPrefix(r.url, "fonts/")
118 parts := strings.Split(name, ".")
119 if len(parts) != 3 || parts[2] != "ttf" || len(parts[1]) != 16 {
120 t.Errorf("%q is not <face>.<16 hex>.ttf", r.url)
121 continue
122 }
123 b, err := os.ReadFile(filepath.Join("..", "slides", "static", "fonts", parts[0]+".ttf"))
124 if err != nil {
125 t.Errorf("%s: %v", r.url, err)
126 continue
127 }
128 sum := sha256.Sum256(b)
129 if want := hex.EncodeToString(sum[:])[:16]; parts[1] != want {
130 t.Errorf("%s carries a stale hash; write fonts/%s.%s.ttf", r.url, parts[0], want)
131 }
132 }
133}
134
135// The stylesheet's own fallback stacks — what a section that arrived
136// without inline custom properties renders as — name the same faces
137// the themes do.
138func TestSlidesCSSFallbackStacksAreTheEmbeddedFamilies(t *testing.T) {
139 css := slidesCSS(t)
140 for _, c := range []struct {
141 prop string
142 fam fonts.Family
143 }{
144 {"--slides-font-heading", fonts.Serif},
145 {"--slides-font-body", fonts.Sans},
146 {"--slides-font-code", fonts.Mono},
147 } {
148 want := c.prop + ": " + c.fam.Stack() + ";"
149 if !strings.Contains(css, want) {
150 t.Errorf("slides.css does not set %q", want)
151 }
152 }
153}
154
155// The end of the wire: the app serves the bytes at the URL the
156// stylesheet asks for, with the immutable Cache-Control the hash buys.
157func TestTheAppServesTheDeclaredFontFiles(t *testing.T) {
158 a := newApp(t)
159 for _, r := range parseFontFaces(t, slidesCSS(t)) {
160 rec := get(t, a, "/static/"+r.url)
161 if rec.Code != http.StatusOK {
162 t.Errorf("GET /static/%s: %d", r.url, rec.Code)
163 continue
164 }
165 if got := rec.Header().Get("Cache-Control"); !strings.Contains(got, "immutable") {
166 t.Errorf("GET /static/%s: Cache-Control %q, want an immutable one", r.url, got)
167 }
168 if rec.Body.Len() < 100_000 {
169 t.Errorf("GET /static/%s returned %d bytes", r.url, rec.Body.Len())
170 }
171 }
172}
173
174// The licence travels with the fonts, as the OFL requires of anything
175// redistributing them — and a slide surface is redistribution.
176func TestTheAppServesTheFontLicence(t *testing.T) {
177 a := newApp(t)
178 rec := get(t, a, "/static/fonts/LICENSE-OFL-1.1.txt")
179 if rec.Code != http.StatusOK {
180 t.Fatalf("GET the font licence: %d", rec.Code)
181 }
182 if !strings.Contains(rec.Body.String(), "SIL OPEN FONT LICENSE") {
183 t.Error("the served licence is not the OFL")
184 }
185}
186