| 1 | package fonts_test |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "testing" |
| 8 | |
| 9 | "amadan.net/oficina/slides/internal/slides/fonts" |
| 10 | ) |
| 11 | |
| 12 | func 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. |
| 34 | func 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 | |
| 49 | func 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 | |
| 60 | func 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. |
| 86 | func 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. |
| 97 | func 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. |
| 112 | func 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. |
| 130 | func 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 | |
| 148 | func 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 | |
| 155 | func 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. |
| 177 | func 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. |
| 189 | func 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 | |
| 209 | func 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 | |
| 215 | func 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 | |