// applyThemeTokens — writing the server-resolved theme onto the document, and // (the part with real logic) taking back exactly what it wrote last time. // // Pure module, exercised against a fake CSSStyleDeclaration: node --test has no // DOM, and the function only ever needs setProperty/removeProperty. import { test } from 'node:test' import assert from 'node:assert/strict' import { applyThemeTokens } from '../src/lib/themeVars.js' // Minimal stand-in for element.style, plus a log of the calls so a test can // assert that a property was *removed* rather than merely absent. function fakeStyle() { const props = new Map() const removed = [] return { props, removed, setProperty: (name, value) => props.set(name, value), removeProperty: (name) => { props.delete(name) removed.push(name) }, get: (name) => props.get(name), } } test('writes each token and reports the keys it applied', () => { const style = fakeStyle() const applied = applyThemeTokens(style, { '--accent': '#c9973f', '--bg': '#1a120b' }) assert.equal(style.get('--accent'), '#c9973f') assert.equal(style.get('--bg'), '#1a120b') assert.deepEqual(applied.sort(), ['--accent', '--bg']) }) // The untouched-instance case: no theme block means the stylesheet's :root // stands and nothing is written at all. test('no theme writes nothing', () => { for (const empty of [null, undefined, {}]) { const style = fakeStyle() const applied = applyThemeTokens(style, empty) assert.equal(style.props.size, 0) assert.deepEqual(applied, []) } }) test('removes a token that is no longer in the theme', () => { const style = fakeStyle() const first = applyThemeTokens(style, { '--accent': '#c9973f', '--bg': '#1a120b' }) const second = applyThemeTokens(style, { '--accent': '#c9973f' }, first) assert.equal(style.get('--accent'), '#c9973f') assert.equal(style.get('--bg'), undefined) assert.deepEqual(style.removed, ['--bg']) assert.deepEqual(second, ['--accent']) }) // "Reset to defaults" — the case that would look broken without the removal // half: the payload stops mentioning the variables, and the inline values have // to come off for :root to show through again. test('resetting to no theme clears everything previously applied', () => { const style = fakeStyle() const first = applyThemeTokens(style, { '--accent': '#c9973f', '--radius-card': '2px' }) const second = applyThemeTokens(style, null, first) assert.equal(style.props.size, 0) assert.deepEqual(style.removed.sort(), ['--accent', '--radius-card']) assert.deepEqual(second, []) }) // Only ever clears its own keys. SiteContext writes --accent itself from // brand.accent, and a future feature may write others; those are not ours. test('never removes a property it did not apply', () => { const style = fakeStyle() style.setProperty('--accent', '#ff0000') // someone else's write applyThemeTokens(style, { '--bg': '#000000' }, []) assert.equal(style.get('--accent'), '#ff0000') assert.deepEqual(style.removed, []) }) test('ignores anything that is not a custom property', () => { const style = fakeStyle() const applied = applyThemeTokens(style, { background: 'url(http://evil.example/x)', '--bg': '#000000' }) assert.equal(style.get('background'), undefined) assert.deepEqual(applied, ['--bg']) }) test('ignores non-string and empty values', () => { const style = fakeStyle() const applied = applyThemeTokens(style, { '--a': 4, '--b': null, '--c': '', '--d': '#fff' }) assert.deepEqual(applied, ['--d']) }) // A stale key list must not survive a call that could not write: the next call // still has to know what is actually on the element. test('a token dropped as invalid is removed if it was applied before', () => { const style = fakeStyle() const first = applyThemeTokens(style, { '--bg': '#000000' }) const second = applyThemeTokens(style, { '--bg': '' }, first) assert.equal(style.get('--bg'), undefined) assert.deepEqual(second, []) })