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') })