oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1package fonts_test
2
3import (
4 "bytes"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "amadan.net/oficina/slides/internal/slides/fonts"
10)
11
12func TestEveryFaceIsEmbeddedAndParses(t *testing.T) {
13 all := fonts.All()
14 if len(all) != 12 {
15 t.Fatalf("want 12 faces (three families, four each), got %d", len(all))
16 }
17 seen := map[string]bool{}
18 for _, f := range all {
19 if seen[f.File()] {
20 t.Errorf("%s appears twice in All()", f.File())
21 }
22 seen[f.File()] = true
23 if len(fonts.Data(f)) == 0 {
24 t.Errorf("%s has no bytes", f.File())
25 }
26 if _, err := f.Parsed(); err != nil {
27 t.Errorf("parse %s: %v", f.File(), err)
28 }
29 }
30}
31
32// The measurements the layout engine leans on: an ascent above the
33// baseline, a descent below it, and both a sane fraction of the size.
34func TestMetricsAreASensibleFractionOfTheSize(t *testing.T) {
35 for _, f := range fonts.All() {
36 asc, desc, lh := fonts.Metrics(f, 100)
37 if asc <= 50 || asc >= 120 {
38 t.Errorf("%s ascent at size 100 is %v", f.File(), asc)
39 }
40 if desc <= 5 || desc >= 50 {
41 t.Errorf("%s descent at size 100 is %v", f.File(), desc)
42 }
43 if lh < asc+desc {
44 t.Errorf("%s line height %v is under ascent+descent %v", f.File(), lh, asc+desc)
45 }
46 }
47}
48
49func TestMetricsScaleWithTheSize(t *testing.T) {
50 f := fonts.Get("paper", fonts.Body, false, false)
51 a1, d1, l1 := fonts.Metrics(f, 10)
52 a2, d2, l2 := fonts.Metrics(f, 20)
53 for _, pair := range [][2]float64{{a1, a2}, {d1, d2}, {l1, l2}} {
54 if got, want := pair[1], pair[0]*2; got < want-1e-9 || got > want+1e-9 {
55 t.Errorf("doubling the size gave %v, want %v", got, want)
56 }
57 }
58}
59
60func TestAdvanceMeasuresText(t *testing.T) {
61 f := fonts.Get("paper", fonts.Body, false, false)
62 empty, _ := fonts.Advance(f, 100, "")
63 if empty != 0 {
64 t.Errorf("the empty string is %v wide", empty)
65 }
66 one, missing := fonts.Advance(f, 100, "Hello, slides")
67 if len(missing) != 0 {
68 t.Errorf("plain ASCII reported missing glyphs: %q", missing)
69 }
70 if one <= 0 {
71 t.Fatalf("a string measured %v", one)
72 }
73 two, _ := fonts.Advance(f, 100, "Hello, slidesHello, slides")
74 if got, want := two, one*2; got < want-1e-6 || got > want+1e-6 {
75 t.Errorf("twice the string measured %v, want %v", got, want)
76 }
77 half, _ := fonts.Advance(f, 50, "Hello, slides")
78 if got, want := half, one/2; got < want-1e-6 || got > want+1e-6 {
79 t.Errorf("half the size measured %v, want %v", got, want)
80 }
81}
82
83// Bold is wider than regular in the same face at the same size. It is
84// the cheapest possible check that a Face actually selects a font
85// program rather than always measuring the same one.
86func TestBoldMeasuresWiderThanRegular(t *testing.T) {
87 const s = "The quick brown fox"
88 reg, _ := fonts.Advance(fonts.Face{Family: fonts.Sans}, 100, s)
89 bold, _ := fonts.Advance(fonts.Face{Family: fonts.Sans, Bold: true}, 100, s)
90 if bold <= reg {
91 t.Errorf("bold measured %v, regular %v: bold should be wider", bold, reg)
92 }
93}
94
95// Every glyph in a monospaced face has the same advance, which is a
96// property the code block's layout relies on.
97func TestMonoAdvancesAreUniform(t *testing.T) {
98 f := fonts.Face{Family: fonts.Mono}
99 one, _ := fonts.Advance(f, 100, "i")
100 wide, _ := fonts.Advance(f, 100, "W")
101 if got, want := wide, one; got < want-1e-9 || got > want+1e-9 {
102 t.Errorf("mono W is %v and i is %v", got, want)
103 }
104 ten, _ := fonts.Advance(f, 100, "iWiWiWiWiW")
105 if got, want := ten, one*10; got < want-1e-6 || got > want+1e-6 {
106 t.Errorf("ten mono glyphs measured %v, want %v", got, want)
107 }
108}
109
110// A rune Liberation has no glyph for is measured (as .notdef, which is
111// what a browser draws a box at) and reported, once, in order.
112func TestAdvanceReportsRunesTheFaceCannotDraw(t *testing.T) {
113 f := fonts.Get("paper", fonts.Body, false, false)
114 w, missing := fonts.Advance(f, 100, "a😀b😀漢")
115 if w <= 0 {
116 t.Errorf("a string with missing glyphs measured %v; it should still take room", w)
117 }
118 want := []rune{'😀', '漢'}
119 if len(missing) != len(want) {
120 t.Fatalf("missing = %q, want %q", missing, want)
121 }
122 for i, r := range want {
123 if missing[i] != r {
124 t.Errorf("missing[%d] = %q, want %q", i, missing[i], r)
125 }
126 }
127}
128
129// §2.1's six themes, and the one fact this package knows about them.
130func TestPaperIsTheOnlyThemeWithSerifHeadings(t *testing.T) {
131 for _, theme := range []string{"paper", "ink", "sunset", "forest", "slate", "candy"} {
132 want := fonts.Sans
133 if theme == "paper" {
134 want = fonts.Serif
135 }
136 if got := fonts.Get(theme, fonts.Heading, false, false).Family; got != want {
137 t.Errorf("%s headings are family %v, want %v", theme, got, want)
138 }
139 if got := fonts.Get(theme, fonts.Body, false, false).Family; got != fonts.Sans {
140 t.Errorf("%s body is family %v, want Sans", theme, got)
141 }
142 if got := fonts.Get(theme, fonts.Code, false, false).Family; got != fonts.Mono {
143 t.Errorf("%s code is family %v, want Mono", theme, got)
144 }
145 }
146}
147
148func TestAnUnknownThemeStillGetsAFace(t *testing.T) {
149 f := fonts.Get("a theme from a later build", fonts.Heading, true, true)
150 if f.Family != fonts.Sans || !f.Bold || !f.Italic {
151 t.Errorf("got %+v, want a bold italic Sans", f)
152 }
153}
154
155func TestFamilyForStackReadsTheFirstFamily(t *testing.T) {
156 for _, tc := range []struct {
157 stack string
158 want fonts.Family
159 ok bool
160 }{
161 {"Liberation Sans, system-ui, sans-serif", fonts.Sans, true},
162 {"Liberation Serif, Georgia, serif", fonts.Serif, true},
163 {`"Liberation Mono", monospace`, fonts.Mono, true},
164 {" liberation sans , x", fonts.Sans, true},
165 {"Comic Sans MS, cursive", 0, false},
166 {"", 0, false},
167 } {
168 got, ok := fonts.FamilyForStack(tc.stack)
169 if ok != tc.ok || (ok && got != tc.want) {
170 t.Errorf("FamilyForStack(%q) = %v, %v; want %v, %v", tc.stack, got, ok, tc.want, tc.ok)
171 }
172 }
173}
174
175// Every family's own stack resolves back to it: the CSS the themes
176// write and the measurement this package makes name one face.
177func TestEveryStackResolvesToItsOwnFamily(t *testing.T) {
178 for _, fam := range []fonts.Family{fonts.Sans, fonts.Serif, fonts.Mono} {
179 got, ok := fonts.FamilyForStack(fam.Stack())
180 if !ok || got != fam {
181 t.Errorf("%s's stack %q resolves to %v, %v", fam.CSSName(), fam.Stack(), got, ok)
182 }
183 }
184}
185
186// The two copies of the same twelve files (this package's header says
187// why there are two) must be the same bytes, or the browser renders
188// one font set and the exporter measures another.
189func TestServedFontsMatchTheEmbeddedOnes(t *testing.T) {
190 for _, f := range fonts.All() {
191 served, err := os.ReadFile(filepath.Join("..", "static", "fonts", f.File()))
192 if err != nil {
193 t.Errorf("static/fonts/%s: %v", f.File(), err)
194 continue
195 }
196 if !bytes.Equal(served, fonts.Data(f)) {
197 t.Errorf("static/fonts/%s differs from the embedded copy", f.File())
198 }
199 }
200 licence, err := os.ReadFile(filepath.Join("..", "static", "fonts", "LICENSE-OFL-1.1.txt"))
201 if err != nil {
202 t.Fatalf("the licence has to travel with the served fonts: %v", err)
203 }
204 if string(licence) != fonts.Licence() {
205 t.Error("the served licence differs from the embedded one")
206 }
207}
208
209func TestLicenceIsTheOFL(t *testing.T) {
210 if got := fonts.Licence(); !bytes.Contains([]byte(got), []byte("SIL OPEN FONT LICENSE Version 1.1")) {
211 t.Errorf("the licence text does not name the OFL 1.1 (%d bytes)", len(got))
212 }
213}
214
215func TestPostScriptNamesAreTheUpstreamOnes(t *testing.T) {
216 for _, tc := range []struct {
217 face fonts.Face
218 want string
219 }{
220 {fonts.Face{Family: fonts.Sans}, "LiberationSans-Regular"},
221 {fonts.Face{Family: fonts.Serif, Bold: true}, "LiberationSerif-Bold"},
222 {fonts.Face{Family: fonts.Mono, Italic: true}, "LiberationMono-Italic"},
223 {fonts.Face{Family: fonts.Serif, Bold: true, Italic: true}, "LiberationSerif-BoldItalic"},
224 } {
225 if got := tc.face.PostScriptName(); got != tc.want {
226 t.Errorf("PostScriptName = %q, want %q", got, tc.want)
227 }
228 }
229}
230