test(client): unit-test the pure-logic modules + wire coverage into CI/Sonar
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:
81
client/test/heroLayout.test.js
Normal file
81
client/test/heroLayout.test.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import {
|
||||
heroBgStack,
|
||||
buildOverlay,
|
||||
heroBackground,
|
||||
parseLayout,
|
||||
defaultLayout,
|
||||
DEFAULT_HERO_IMAGE,
|
||||
} from '../src/lib/heroLayout.js'
|
||||
|
||||
// Unit-test the hero-layout helpers shared by the public portal and the admin
|
||||
// editor. The high-value logic: parseLayout must reject anything malformed or of
|
||||
// the wrong version (a bad stored value must never render as a broken hero), and
|
||||
// heroBackground must take the untouched-default branch only when there is no
|
||||
// custom image.
|
||||
|
||||
// ── parseLayout: the version/shape guard ────────────────────────────────
|
||||
test('parseLayout accepts a well-formed v1 layout', () => {
|
||||
const layout = { version: 1, elements: [] }
|
||||
assert.deepEqual(parseLayout(JSON.stringify(layout)), layout)
|
||||
})
|
||||
|
||||
test('parseLayout returns null for missing, malformed, wrong-version, or wrong-shape input', () => {
|
||||
assert.equal(parseLayout(null), null)
|
||||
assert.equal(parseLayout(''), null)
|
||||
assert.equal(parseLayout('{ not json'), null)
|
||||
assert.equal(parseLayout(JSON.stringify({ version: 2, elements: [] })), null) // wrong version
|
||||
assert.equal(parseLayout(JSON.stringify({ version: 1, elements: 'nope' })), null) // elements not an array
|
||||
assert.equal(parseLayout(JSON.stringify({ version: 1 })), null) // no elements
|
||||
})
|
||||
|
||||
// ── heroBackground: default vs custom branch ────────────────────────────
|
||||
test('heroBackground uses the contained-emblem default stack only when default AND no custom image', () => {
|
||||
const style = heroBackground({ background: {} }, { isDefault: true })
|
||||
assert.match(style.backgroundImage, /runic-emblem\.png/)
|
||||
assert.equal(style.backgroundSize, 'cover, min(74vh, 640px, 86vw)') // the two-layer default size
|
||||
})
|
||||
|
||||
test('heroBackground threads a per-instance defaultImage into the default stack', () => {
|
||||
const style = heroBackground({ background: {} }, { isDefault: true, defaultImage: '/brand/hero.jpg' })
|
||||
assert.match(style.backgroundImage, /\/brand\/hero\.jpg/)
|
||||
})
|
||||
|
||||
test('heroBackground composes overlay + custom image once a custom image is set', () => {
|
||||
const style = heroBackground(
|
||||
{ background: { image_url: '/uploads/hero.png', position_x: 'right', position_y: 'top', size: 'contain' }, overlay: { opacity: 0.5 } },
|
||||
{ isDefault: true }, // still leaves the default branch because a custom image_url is present
|
||||
)
|
||||
assert.match(style.backgroundImage, /\/uploads\/hero\.png/)
|
||||
assert.match(style.backgroundImage, /rgba\(11,15,20,0\.5\)/) // overlay opacity threaded in
|
||||
assert.equal(style.backgroundPosition, 'right top')
|
||||
assert.equal(style.backgroundSize, 'contain')
|
||||
})
|
||||
|
||||
test('heroBackground defaults the overlay opacity to 0.72 when unset', () => {
|
||||
const style = heroBackground({ background: { image_url: '/x.png' } }, {})
|
||||
assert.match(style.backgroundImage, /rgba\(11,15,20,0\.72\)/)
|
||||
})
|
||||
|
||||
// ── smaller helpers ─────────────────────────────────────────────────────
|
||||
test('heroBgStack falls back to the default emblem when no image is given', () => {
|
||||
assert.match(heroBgStack(), new RegExp(DEFAULT_HERO_IMAGE.replace(/[/.]/g, '\\$&')))
|
||||
assert.match(heroBgStack('/custom.png'), /\/custom\.png/)
|
||||
})
|
||||
|
||||
test('buildOverlay scales both gradient stops from the opacity', () => {
|
||||
const overlay = buildOverlay(0.8)
|
||||
assert.match(overlay, /rgba\(11,15,20,0.12\)/) // 0.8 * 0.15 top stop
|
||||
assert.match(overlay, /rgba\(11,15,20,0.8\)/) // bottom stop
|
||||
})
|
||||
|
||||
// ── defaultLayout: the fallback hero ────────────────────────────────────
|
||||
test('defaultLayout builds a valid v1 layout that threads the teaser and shard name', () => {
|
||||
const layout = defaultLayout('Come play with us', 'My Shard')
|
||||
assert.equal(parseLayout(JSON.stringify(layout)) !== null, true, 'the default is itself a valid layout')
|
||||
assert.equal(layout.version, 1)
|
||||
const textLines = layout.elements.find((e) => e.id === 'default-text').props.lines
|
||||
assert.ok(textLines.some((l) => l.text === 'My Shard'), 'shard name rendered as the h1')
|
||||
assert.ok(textLines.some((l) => l.text === 'Come play with us'), 'teaser threaded in')
|
||||
})
|
||||
Reference in New Issue
Block a user