mattrguerra/meridian-ops-boardmainPrivate on GitHub, published here in full

fonts.test.ts

Language TypeScript5.9 KB127 lines

apps/dashboard/test/fonts.test.ts

  1. /**
  2. * The display face has to be able to spell everyone's name.
  3. *
  4. * A face subset to "A-Z 0-9 and the marks" renders "Rosa Galván" in two
  5. * typefaces: the display face and one á in a fallback, in a 26px heading. A
  6. * missing glyph is hard to see in review unless you are looking at that one
  7. * character and know what the face should look like, and from outside it reads
  8. * as an encoding problem while the bytes are right.
  9. *
  10. * What this pins, and what it cannot: it checks names against the DECLARED
  11. * ranges in fonts.ts, not against the font binary: reading a brotli-compressed
  12. * woff2 cmap needs fonttools, and this suite runs offline with no dependency
  13. * the repo does not already carry. So the guard is only as true as the
  14. * declaration is kept in step with the pyftsubset command beside it. That is a
  15. * real limit and it is written down rather than papered over, but it still
  16. * catches the likely case, which is a person arriving with a character nobody
  17. * thought about.
  18. */
  19. import { describe, expect, it } from 'vitest'
  20. import {
  21. FONT_FACES,
  22. DISPLAY_SUBSET_RANGES,
  23. inBodySubset,
  24. inDisplaySubset,
  25. } from '../src/fonts.js'
  26. import { THEME_CSS } from '../src/theme.js'
  27. import { MERIDIAN_PEOPLE } from '../../../config/meridian-people.js'
  28. import { ONBOARDING } from '../../../config/onboarding.js'
  29. /** Everything the display face is actually asked to draw. */
  30. const displayStrings = (): { what: string; text: string }[] => [
  31. ...MERIDIAN_PEOPLE.map((p) => ({ what: `roster name ${p.name}`, text: p.name })),
  32. ...ONBOARDING.flatMap((p) => [
  33. { what: `plan subject ${p.person}`, text: p.person },
  34. { what: `plan role ${p.role}`, text: p.role },
  35. ...(p.trainedBy ? [{ what: `trainer ${p.trainedBy}`, text: p.trainedBy }] : []),
  36. ...p.days.map((d) => ({ what: `day title "${d.title}"`, text: d.title })),
  37. ...p.days.flatMap((d) =>
  38. d.sections.map((sec) => ({ what: `topic "${sec.title}"`, text: sec.title })),
  39. ),
  40. ]),
  41. ]
  42. const hex = (ch: string): string =>
  43. `U+${ch.codePointAt(0)!.toString(16).toUpperCase().padStart(4, '0')}`
  44. /** A letter has to be in the display face itself; punctuation may fall through. */
  45. const isLetter = (ch: string): boolean => /\p{L}|\p{M}/u.test(ch)
  46. describe('every name the display face has to render', () => {
  47. it('has every LETTER in the display face itself, in both cases', () => {
  48. // Both cases: the eyebrow uppercases with CSS, so a lowercase á in the data
  49. // becomes Á on the page and it is the UPPERCASE glyph that has to exist.
  50. // Subsetting the one that appears in the source file is half a fix.
  51. //
  52. // Letters only, because a letter is the one thing that CANNOT fall back
  53. // gracefully: one Georgia á inside twenty display-face capitals is the bug this
  54. // file is named for. Punctuation is handled by the next test.
  55. for (const { what, text } of displayStrings()) {
  56. for (const ch of [...text, ...text.toUpperCase(), ...text.toLowerCase()]) {
  57. if (!isLetter(ch)) continue
  58. expect(
  59. inDisplaySubset(ch),
  60. `${what}: "${ch}" (${hex(ch)}) is not in the display face subset, so it renders in Georgia`,
  61. ).toBe(true)
  62. }
  63. }
  64. })
  65. it('has everything else in the body face, which is what the stack falls through to', () => {
  66. // The display face has no em dash, en dash, ellipsis or arrow: the glyphs are
  67. // absent from the font, so no subset can add them. Every "Name: heading"
  68. // on this board drew its dash in Georgia until the stack gained the body face.
  69. for (const { what, text } of displayStrings()) {
  70. for (const ch of text) {
  71. if (isLetter(ch)) continue
  72. expect(
  73. inDisplaySubset(ch) || inBodySubset(ch),
  74. `${what}: "${ch}" (${hex(ch)}) is in neither face, so it falls to Georgia`,
  75. ).toBe(true)
  76. }
  77. }
  78. })
  79. it('puts the body face ahead of Georgia in the display stack', () => {
  80. // The whole previous test is worthless if the stack does not actually
  81. // reach the body face before the serif.
  82. const stack = THEME_CSS.match(/--font-display:([^;]+);/)![1]!
  83. expect(stack.indexOf('Inter')).toBeGreaterThan(stack.indexOf('Anton'))
  84. expect(stack.indexOf('Georgia')).toBeGreaterThan(stack.indexOf('Inter'))
  85. })
  86. it('covers Galván and Ibáñez specifically, because those are the two on the roster today', () => {
  87. for (const ch of 'áÁñÑ') expect(inDisplaySubset(ch), ch).toBe(true)
  88. })
  89. it('has not been narrowed back to a list of the accents we happen to use', () => {
  90. // The fix was a RANGE, deliberately. A list of the characters currently on
  91. // the roster is the same decision that caused the bug, one hire later: it
  92. // would have carried Galván and still broken on a François or a João.
  93. for (const ch of 'çÇãÃõÕüÜøØåÅßæÆéÉ') expect(inDisplaySubset(ch), ch).toBe(true)
  94. })
  95. it('still excludes what it never needed, so the range is a decision and not a shrug', () => {
  96. // The U+00A0-00BF symbol block is out on purpose: a display heading has no
  97. // use for a section mark, and it is ~2KB of base64 on every page.
  98. for (const ch of '§¶©«»¿¡') expect(inDisplaySubset(ch), ch).toBe(false)
  99. })
  100. })
  101. describe('the embedded faces', () => {
  102. it('ships all three, as data URIs, because nothing else can reach them', () => {
  103. // CSP is default-src 'none' and the page is self-contained; a @font-face
  104. // pointing at a repo-relative file rendered in Georgia in production for
  105. // the entire time the board had been live.
  106. expect(FONT_FACES).toContain('font-family:"Anton"')
  107. expect((FONT_FACES.match(/font-family:"Inter"/g) ?? []).length).toBe(2)
  108. expect((FONT_FACES.match(/data:font\/woff2;base64,/g) ?? []).length).toBe(3)
  109. expect(FONT_FACES).not.toMatch(/url\((?!data:)/)
  110. })
  111. it('declares ranges that are ordered and non-empty', () => {
  112. expect(DISPLAY_SUBSET_RANGES.length).toBeGreaterThan(0)
  113. for (const [lo, hi] of DISPLAY_SUBSET_RANGES) expect(hi).toBeGreaterThanOrEqual(lo)
  114. })
  115. })