also looking at this
LSU's own coaching surface, and what auditing the site turned up
#12
86 files
+3943
-1190
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -0,0 +1,123 @@ | ||
| 1 | import { describe, expect, it } from 'bun:test' | |
| 2 | import { resolve } from 'node:path' | |
| 3 | import { processDirectives } from '@stacksjs/stx' | |
| 4 | ||
| 5 | /** | |
| 6 | * The results pages state figures that are derived from the race database in | |
| 7 | * resources/data/results-<year>.json. The README's one content rule is that | |
| 8 | * race facts are not replaced with sample content; this is what checks it. | |
| 9 | * | |
| 10 | * Everything here is recomputed from the JSON and then looked for in the | |
| 11 | * rendered page, so a page that drifts from its own source fails, and so | |
| 12 | * does a page whose numbers were edited by hand. | |
| 13 | */ | |
| 14 | ||
| 15 | interface Row { name: string, yards: number, status: string } | |
| 16 | ||
| 17 | async function season(year: number) { | |
| 18 | const data = await Bun.file(`resources/data/results-${year}.json`).json() | |
| 19 | const rows: Row[] = data.results | |
| 20 | const ordered = [...rows].sort((a, b) => b.yards - a.yards) | |
| 21 | const loopMetres: number = data.event.loop_metres | |
| 22 | const winner = ordered[0]! | |
| 23 | ||
| 24 | return { | |
| 25 | date: data.event.start_date as string, | |
| 26 | started: data.totals.started as number, | |
| 27 | loopMetres, | |
| 28 | winner, | |
| 29 | assist: ordered[1]!, | |
| 30 | third: ordered[2]!, | |
| 31 | kilometres: Math.round(winner.yards * loopMetres / 100) / 10, | |
| 32 | /** A yard an hour, so the whole days the race ran for. */ | |
| 33 | days: Math.ceil(winner.yards / 24), | |
| 34 | } | |
| 35 | } | |
| 36 | ||
| 37 | const page = await (async () => { | |
| 38 | const path = resolve('resources/views/results.stx') | |
| 39 | return processDirectives(await Bun.file(path).text(), {}, path, { | |
| 40 | enabled: true, | |
| 41 | cache: false, | |
| 42 | componentsDir: resolve('resources/components'), | |
| 43 | layoutsDir: resolve('resources/layouts'), | |
| 44 | partialsDir: resolve('resources/partials'), | |
| 45 | }, new Set()) | |
| 46 | })() | |
| 47 | ||
| 48 | /** | |
| 49 | * One season's own section, so a figure from the other year cannot satisfy | |
| 50 | * an assertion about this one. Each is `<section id="<year>" ...>`. | |
| 51 | */ | |
| 52 | function sectionFor(year: number): string { | |
| 53 | const start = page.indexOf(`<section id="${year}"`) | |
| 54 | expect(start).toBeGreaterThan(-1) | |
| 55 | const next = page.indexOf('<section id="', start + 1) | |
| 56 | return page.slice(start, next === -1 ? undefined : next) | |
| 57 | } | |
| 58 | ||
| 59 | /** | |
| 60 | * The page's stat blocks are a value element followed by its label. The | |
| 61 | * source has them on separate lines and the build minifies that away, so the | |
| 62 | * gap between them is optional. | |
| 63 | */ | |
| 64 | function stat(section: string, label: string): string | undefined { | |
| 65 | return new RegExp(`<div class="stat-v">([^<]+)</div>\\s*<div class="stat-l">${label}</div>`).exec(section)?.[1] | |
| 66 | } | |
| 67 | ||
| 68 | describe.each([2025, 2026])('the %i results', (year) => { | |
| 69 | it('names the winner the database names, at the yards it records', async () => { | |
| 70 | const { winner } = await season(year) | |
| 71 | const section = sectionFor(year) | |
| 72 | ||
| 73 | expect(winner.status).toBe('1st') | |
| 74 | expect(section).toContain(winner.name) | |
| 75 | expect(stat(section, 'winning yards')).toBe(String(winner.yards)) | |
| 76 | }) | |
| 77 | ||
| 78 | it('counts the field the database counted', async () => { | |
| 79 | const { started } = await season(year) | |
| 80 | const section = sectionFor(year) | |
| 81 | ||
| 82 | expect(section).toContain(`${started} starters`) | |
| 83 | expect(stat(section, 'athletes on the start line')).toBe(String(started)) | |
| 84 | }) | |
| 85 | ||
| 86 | it('converts yards to kilometres with the loop the database records', async () => { | |
| 87 | const { kilometres, loopMetres } = await season(year) | |
| 88 | const section = sectionFor(year) | |
| 89 | ||
| 90 | expect(stat(section, 'kilometres covered')).toBe(String(kilometres)) | |
| 91 | expect(stat(section, 'km per hourly loop')).toBe(String(loopMetres / 1000)) | |
| 92 | }) | |
| 93 | ||
| 94 | it('counts the days the race actually ran', async () => { | |
| 95 | const { days } = await season(year) | |
| 96 | expect(stat(sectionFor(year), 'days of racing')).toBe(String(days)) | |
| 97 | }) | |
| 98 | ||
| 99 | it('names the two behind the winner, in the order they finished', async () => { | |
| 100 | const { winner, assist, third } = await season(year) | |
| 101 | const section = sectionFor(year) | |
| 102 | ||
| 103 | expect(assist.yards).toBeGreaterThanOrEqual(third.yards) | |
| 104 | expect(section.indexOf(winner.name)).toBeLessThan(section.indexOf(assist.name)) | |
| 105 | expect(section.indexOf(assist.name)).toBeLessThan(section.indexOf(third.name)) | |
| 106 | }) | |
| 107 | }) | |
| 108 | ||
| 109 | describe('the edition comparison', () => { | |
| 110 | it('states differences that are the difference between the two seasons', async () => { | |
| 111 | const a = await season(2025) | |
| 112 | const b = await season(2026) | |
| 113 | ||
| 114 | expect(page).toContain(`+${b.winner.yards - a.winner.yards}`) | |
| 115 | expect(page).toContain(`+${b.started - a.started}`) | |
| 116 | }) | |
| 117 | ||
| 118 | it('agrees with the loop length the rest of the site uses', async () => { | |
| 119 | const { race } = await import('../resources/data/race') | |
| 120 | expect((await season(2026)).loopMetres).toBe(race.loopMetres) | |
| 121 | expect((await season(2025)).loopMetres).toBe(race.loopMetres) | |
| 122 | }) | |
| 123 | }) | |