package slidestest import ( "crypto/sha256" "encoding/hex" "fmt" "net/http" "os" "path/filepath" "regexp" "strings" "testing" "amadan.net/oficina/slides/internal/slides/fonts" ) // This file holds slides.css's half of "one font set everywhere" to // the Go half. The stylesheet is a static file — nothing templates it // — so the twelve @font-face URLs, the fallback stacks and the served // bytes are all things that can drift silently. These tests are what // notice. func slidesCSS(t *testing.T) string { t.Helper() b, err := os.ReadFile(filepath.Join("..", "slides", "static", "slides.css")) if err != nil { t.Fatalf("read slides.css: %v", err) } return string(b) } // fontFaceSrc pulls each @font-face's family, style, weight and URL // out in the order slides.css writes them. var fontFaceRule = regexp.MustCompile(`(?s)@font-face \{(.*?)\}`) type faceRule struct { family, style, weight, url string } func parseFontFaces(t *testing.T, css string) []faceRule { t.Helper() field := func(body, name string) string { re := regexp.MustCompile(name + `:\s*([^;]+);`) m := re.FindStringSubmatch(body) if m == nil { return "" } return strings.Trim(strings.TrimSpace(m[1]), `"`) } var out []faceRule for _, m := range fontFaceRule.FindAllStringSubmatch(css, -1) { body := m[1] src := field(body, "src") u := regexp.MustCompile(`url\("([^"]+)"\)`).FindStringSubmatch(src) if u == nil { t.Errorf("an @font-face has no url(): %q", src) continue } out = append(out, faceRule{ family: field(body, "font-family"), style: field(body, "font-style"), weight: field(body, "font-weight"), url: u[1], }) } return out } // Every one of the twelve embedded faces is declared, once, under its // own family name and with the weight and slope that select it. A face // the engine measures with and the browser cannot fetch is the // divergence this whole font set exists to close. func TestSlidesCSSDeclaresEveryEmbeddedFace(t *testing.T) { rules := parseFontFaces(t, slidesCSS(t)) got := map[string]string{} for _, r := range rules { key := fmt.Sprintf("%s|%s|%s", r.family, r.style, r.weight) if prev, dup := got[key]; dup { t.Errorf("%s is declared twice (%s and %s)", key, prev, r.url) } got[key] = r.url } for _, f := range fonts.All() { style, weight := "normal", "400" if f.Italic { style = "italic" } if f.Bold { weight = "700" } key := fmt.Sprintf("%s|%s|%s", f.Family.CSSName(), style, weight) url, ok := got[key] if !ok { t.Errorf("slides.css declares no @font-face for %s (%s)", f.File(), key) continue } if !strings.HasPrefix(url, "fonts/"+strings.TrimSuffix(f.File(), ".ttf")+".") { t.Errorf("%s is declared as %q, which is not that face's file", key, url) } delete(got, key) } for key, url := range got { t.Errorf("slides.css declares %s (%s), which is not a face this build embeds", key, url) } } // The URLs carry a content hash so rastrillo's asset handler serves // them immutable rather than no-cache — a slide is a page load, and // refetching a megabyte of fonts on every next-slide link is not a // cost worth paying for a set of files that never change. // // A stale hash is not a broken page (the handler strips it, finds the // file and serves it no-cache), so this failing means "slower", not // "broken" — the message says what to write instead. func TestSlidesCSSFontURLsCarryTheirContentHash(t *testing.T) { for _, r := range parseFontFaces(t, slidesCSS(t)) { name := strings.TrimPrefix(r.url, "fonts/") parts := strings.Split(name, ".") if len(parts) != 3 || parts[2] != "ttf" || len(parts[1]) != 16 { t.Errorf("%q is not .<16 hex>.ttf", r.url) continue } b, err := os.ReadFile(filepath.Join("..", "slides", "static", "fonts", parts[0]+".ttf")) if err != nil { t.Errorf("%s: %v", r.url, err) continue } sum := sha256.Sum256(b) if want := hex.EncodeToString(sum[:])[:16]; parts[1] != want { t.Errorf("%s carries a stale hash; write fonts/%s.%s.ttf", r.url, parts[0], want) } } } // The stylesheet's own fallback stacks — what a section that arrived // without inline custom properties renders as — name the same faces // the themes do. func TestSlidesCSSFallbackStacksAreTheEmbeddedFamilies(t *testing.T) { css := slidesCSS(t) for _, c := range []struct { prop string fam fonts.Family }{ {"--slides-font-heading", fonts.Serif}, {"--slides-font-body", fonts.Sans}, {"--slides-font-code", fonts.Mono}, } { want := c.prop + ": " + c.fam.Stack() + ";" if !strings.Contains(css, want) { t.Errorf("slides.css does not set %q", want) } } } // The end of the wire: the app serves the bytes at the URL the // stylesheet asks for, with the immutable Cache-Control the hash buys. func TestTheAppServesTheDeclaredFontFiles(t *testing.T) { a := newApp(t) for _, r := range parseFontFaces(t, slidesCSS(t)) { rec := get(t, a, "/static/"+r.url) if rec.Code != http.StatusOK { t.Errorf("GET /static/%s: %d", r.url, rec.Code) continue } if got := rec.Header().Get("Cache-Control"); !strings.Contains(got, "immutable") { t.Errorf("GET /static/%s: Cache-Control %q, want an immutable one", r.url, got) } if rec.Body.Len() < 100_000 { t.Errorf("GET /static/%s returned %d bytes", r.url, rec.Body.Len()) } } } // The licence travels with the fonts, as the OFL requires of anything // redistributing them — and a slide surface is redistribution. func TestTheAppServesTheFontLicence(t *testing.T) { a := newApp(t) rec := get(t, a, "/static/fonts/LICENSE-OFL-1.1.txt") if rec.Code != http.StatusOK { t.Fatalf("GET the font licence: %d", rec.Code) } if !strings.Contains(rec.Body.String(), "SIL OPEN FONT LICENSE") { t.Error("the served licence is not the OFL") } }