feedBack/tests/js/v3_theme_read_api.test.js
ChrisBeWithYou d20b33348b
feat(v3): host theme read surface — window.feedBack.theme + always-present --fbv-* tokens (#646)
First slice of the host theme contract (#644): give plugins a host-owned way to
read the active theme + its device affordances, so a feature renders correctly
under any theme instead of binding to whichever one the dev saw.

theme-core.js previously only APPLIED themes and emitted --fbv-* vars only while
a theme was equipped (nothing to read in the default state), with no read API.
Now, all additive + feature-detected:

- Always-present default `fb` palette as `--fbv-*` on :root (the un-themed look
  is unchanged — fb-* utilities still use their compiled defaults; this only
  hands plugins a stable host token to read + derive surfaces from). Adds two
  keystone ROLES the palette lacked: `on-accent` (legible fg on the accent fill)
  and `focus-ring`.
- window.feedBack.theme.get() -> {id, isThemed, tokens}; .capabilities() ->
  {glow, gradients, motion} (the device-affordance signal; recolor-only themes
  report defaults, a theme may opt out via `capabilities` in its payload, motion
  is reduced-motion-gated); .prefersReducedMotion().
- Normalized `theme:changed` event from the single apply() chokepoint.

The apply side stays on window.v3Theme; the read surface is attached defensively
so it survives the feedBack bus being (re)built by capabilities.js regardless of
load order. Verified via a headless render (apply/unequip intact, defaults
present + restored, capability opt-out honored, event payload correct) +
tests/js/v3_theme_read_api.test.js. See docs/host-theme-contract.md.


Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:57:58 +02:00

58 lines
2.5 KiB
JavaScript

// Guard for the host theme READ surface added to the cosmetics applier
// (static/v3/theme-core.js): always-present `--fbv-*` defaults on :root (so a
// plugin can read host tokens un-themed), the two keystone roles the palette
// lacked (on-accent / focus-ring), and the window.feedBack.theme read API +
// theme:changed event. Source-level guards on the contract surface; runtime
// behaviour (apply/unequip/defaults-restore/capabilities) is verified
// separately by a headless render. First slice of the host theme contract
// (got-feedback/feedBack#644).
'use strict';
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const TC = fs.readFileSync(
path.join(__dirname, '..', '..', 'static', 'v3', 'theme-core.js'), 'utf8');
test('always-present --fbv-* defaults are injected on :root', () => {
assert.match(TC, /const DEFAULTS = \{/);
assert.match(TC, /function _injectDefaults\(\)/);
assert.match(TC, /id = 'fb-theme-defaults'/);
assert.match(TC, /':root \{/);
// injected before the first profile read so var(--fbv-*) resolves immediately
assert.match(TC, /_injectDefaults\(\);\s*\n\s*refresh\(\);/);
});
test('two keystone roles the palette lacked are added as defaults', () => {
assert.match(TC, /'on-accent':/);
assert.match(TC, /'focus-ring':/);
});
test('window.feedBack.theme read API is exposed (get/capabilities/prefersReducedMotion)', () => {
assert.match(TC, /window\.feedBack = window\.feedBack \|\| \{\};/);
assert.match(TC, /window\.feedBack\.theme = \{ get, capabilities, prefersReducedMotion \};/);
assert.match(TC, /function get\(\)/);
assert.match(TC, /function capabilities\(\)/);
assert.match(TC, /function prefersReducedMotion\(\)/);
});
test('capabilities reports device affordances and gates motion on reduced-motion', () => {
assert.match(TC, /glow:/);
assert.match(TC, /gradients:/);
assert.match(TC, /motion: allowMotion && !prefersReducedMotion\(\)/);
});
test('apply() tracks theme id + declared capabilities and emits theme:changed', () => {
assert.match(TC, /function apply\(payload, meta\)/);
assert.match(TC, /_themeId =/);
assert.match(TC, /_themeCaps =/);
assert.match(TC, /_emitThemeChanged\(\)/);
assert.match(TC, /emit\('theme:changed'/);
});
test('the apply side stays on window.v3Theme (read surface is additive)', () => {
assert.match(TC, /window\.v3Theme = \{/);
});