test(client): unit-test the pure-logic modules + wire coverage into CI/Sonar
All checks were successful
PR Checks / bot-install (pull_request) Successful in 15s
PR Checks / client-build (pull_request) Successful in 44s
PR Checks / server-tests (pull_request) Successful in 9m36s

Stand up a client test suite on Node's built-in runner (no vitest/jsdom — the
targeted modules are plain ESM with no browser/DOM deps) and cover the
meaningful client logic, not presentational components:

- api/client.js: the fetch wrapper — always sends the session cookie, maps a
  non-2xx response to a thrown ApiError (body.message → statusText fallback),
  resolves an empty body to null, sets Content-Type for JSON but NOT for raw
  FormData uploads, and builds/encodes query strings + path params.
- lib/shardEvents.js: describe() across event kinds (payload vs live frame,
  actor name→acct→"Someone" fallback, sale pluralization, champ.update
  branches) and the categoryOf table-consistency check.
- data/regionBuckets.js: the presence roll-up, incl. the first-match-wins
  ordering and the "buckets always reconcile to the total" invariant.
- lib/heroLayout.js: parseLayout's version/shape guard and heroBackground's
  default-vs-custom branch.
- lib/format.js: the date/label formatters + relative-time buckets.

Wiring so these actually count:
- client/package.json gains a `test` script (node --test);
- pr-checks.yml runs the client tests as a PR gate;
- sonarqube.yml generates a client LCOV report and sonar-project.properties
  feeds it alongside the server report (SF paths resolve to client/src/...).

43 client tests; coverage on the tested modules: format/regionBuckets/
heroLayout 100%, api client 86%, shardEvents 80%.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-21 00:38:53 -05:00
parent 35e5269ec5
commit 886a504152
9 changed files with 440 additions and 11 deletions

View File

@@ -0,0 +1,55 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { longDate, shortDate, dateTime, monthTile, ago, categoryLabel } from '../src/lib/format.js'
// Unit-test the shared date/label formatters. Date strings are given with an
// explicit local time (no trailing Z) so getMonth/getDate read the same value in
// any timezone the test runs in — otherwise a date-only UTC string could shift a
// day. The point is to lock the human-facing formats the whole site renders.
test('longDate renders "Month D, YYYY"', () => {
assert.equal(longDate('2026-06-24T12:00:00'), 'June 24, 2026')
})
test('shortDate renders "Mon D"', () => {
assert.equal(shortDate('2026-06-24T12:00:00'), 'Jun 24')
})
test('dateTime renders "Mon D HH:MM" with zero-padded time', () => {
assert.equal(dateTime('2026-06-24T08:05:00'), 'Jun 24 08:05')
})
test('monthTile returns the uppercased 3-letter month and 2-digit year', () => {
assert.deepEqual(monthTile('2026-06-24T12:00:00'), { mon: 'JUN', num: "'26" })
})
test('every formatter returns an empty/placeholder value for a missing or invalid date', () => {
for (const bad of [null, undefined, '', 'not-a-date']) {
assert.equal(longDate(bad), '')
assert.equal(shortDate(bad), '')
assert.equal(dateTime(bad), '')
assert.equal(ago(bad), '')
assert.deepEqual(monthTile(bad), { mon: '—', num: '' })
}
})
// ── ago(): relative-time buckets ────────────────────────────────────────
test('ago picks the right unit as the gap widens', () => {
const now = Date.now()
assert.equal(ago(new Date(now - 5 * 1000)), '5s ago')
assert.equal(ago(new Date(now - 5 * 60 * 1000)), '5m ago')
assert.equal(ago(new Date(now - 3 * 60 * 60 * 1000)), '3h ago')
assert.equal(ago(new Date(now - 2 * 24 * 60 * 60 * 1000)), '2d ago')
})
test('ago floors to at least 1s (never "0s ago" or a negative)', () => {
assert.equal(ago(new Date(Date.now())), '1s ago')
assert.equal(ago(new Date(Date.now() + 5000)), '1s ago') // a slightly-future timestamp
})
// ── categoryLabel(): known map + passthrough ────────────────────────────
test('categoryLabel maps known db categories and passes unknown ones through', () => {
assert.equal(categoryLabel('five_on_friday'), 'Five on Friday')
assert.equal(categoryLabel('newsletter'), 'Newsletter')
assert.equal(categoryLabel('mystery_category'), 'mystery_category') // unknown → itself
})