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>
75 lines
3.8 KiB
JavaScript
75 lines
3.8 KiB
JavaScript
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { describe, categoryOf, kindLabel, CATEGORIES } from '../src/lib/shardEvents.js'
|
||
|
||
// Unit-test the shared shard-event formatter — the single place that decides how
|
||
// each event kind reads and which filter category it belongs to. These strings
|
||
// are user-facing on the public Shard page, the Activity feed, and the admin
|
||
// live feed, so a regression here is visible everywhere at once.
|
||
|
||
// ── describe(): works on both stored (.payload) and live (top-level) frames ──
|
||
test('describe reads fields from .payload when present, else the top level', () => {
|
||
const stored = { kind: 'quest.complete', payload: { who: { name: 'Ada' }, quest: 'The Cavern' } }
|
||
const live = { kind: 'quest.complete', who: { name: 'Ada' }, quest: 'The Cavern' }
|
||
assert.equal(describe(stored), 'Ada completed “The Cavern”')
|
||
assert.equal(describe(live), 'Ada completed “The Cavern”')
|
||
})
|
||
|
||
test('describe resolves an actor from name → acct → "Someone"', () => {
|
||
assert.equal(describe({ kind: 'mob.login', who: { name: 'Bob' } }), 'Bob entered the world')
|
||
assert.equal(describe({ kind: 'mob.login', who: { acct: 'acct7' } }), 'acct7 entered the world')
|
||
assert.equal(describe({ kind: 'mob.login', who: null }), 'Someone entered the world')
|
||
assert.equal(describe({ kind: 'mob.login', who: 'RawString' }), 'RawString entered the world')
|
||
})
|
||
|
||
test('describe pluralizes a vendor sale only when amount > 1 and formats the price', () => {
|
||
assert.equal(describe({ kind: 'vendor.sale', itemType: 'Katana', amount: 1, price: 1200 }), 'Katana sold for 1,200gp')
|
||
assert.equal(describe({ kind: 'vendor.sale', itemType: 'Arrow', amount: 40, price: 80 }), 'Arrow ×40 sold for 80gp')
|
||
})
|
||
|
||
test('describe includes the killer only when present (optional clause)', () => {
|
||
assert.equal(describe({ kind: 'player.death', who: { name: 'Ada' } }), 'Ada was slain')
|
||
assert.equal(
|
||
describe({ kind: 'player.death', who: { name: 'Ada' }, killer: { name: 'Orc' } }),
|
||
'Ada was slain by Orc',
|
||
)
|
||
})
|
||
|
||
test('describe champ.update branches on status and boss state', () => {
|
||
assert.equal(describe({ kind: 'champ.update', name: 'Rikktor', status: 'active', bossUp: true }), 'Rikktor: boss is up')
|
||
assert.equal(
|
||
describe({ kind: 'champ.update', name: 'Rikktor', status: 'active', level: 3 }),
|
||
'Rikktor is active — level 3',
|
||
)
|
||
assert.equal(describe({ kind: 'champ.update', name: 'Rikktor', status: 'cooldown' }), 'Rikktor is on cooldown')
|
||
})
|
||
|
||
test('describe falls back to the raw kind for an unknown event', () => {
|
||
assert.equal(describe({ kind: 'some.future.kind' }), 'some.future.kind')
|
||
})
|
||
|
||
// ── categoryOf(): membership + catch-all ────────────────────────────────
|
||
test('categoryOf groups kinds per the CATEGORIES table, and unknowns are "other"', () => {
|
||
assert.equal(categoryOf('player.death'), 'pvp')
|
||
assert.equal(categoryOf('skill.gain'), 'progress')
|
||
assert.equal(categoryOf('house.decay'), 'world')
|
||
assert.equal(categoryOf('vendor.sale'), 'other') // deliberately not a public category
|
||
assert.equal(categoryOf('totally.unknown'), 'other')
|
||
})
|
||
|
||
test('every kind listed in CATEGORIES maps back to that category (table stays consistent)', () => {
|
||
for (const cat of CATEGORIES) {
|
||
if (!cat.kinds) continue
|
||
for (const kind of cat.kinds) {
|
||
assert.equal(categoryOf(kind), cat.id, `${kind} should be in ${cat.id}`)
|
||
}
|
||
}
|
||
})
|
||
|
||
// ── kindLabel(): badge text ─────────────────────────────────────────────
|
||
test('kindLabel turns dots/underscores into spaces and tolerates empty input', () => {
|
||
assert.equal(kindLabel('player.death'), 'player death')
|
||
assert.equal(kindLabel('account.login.attempt'), 'account login attempt')
|
||
assert.equal(kindLabel(null), '')
|
||
})
|