@bongos/core 1.19.458 → 1.19.459
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +98 -28
- package/.claude/skills/design/SKILL.md +10 -0
- package/docs/adr/0219-a-look-is-a-branding-pack-the-style-library.md +30 -0
- package/docs/adr/README.md +1 -0
- package/docs/branding-contract.md +1 -1
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/recipes/ui-look-before-you-ship.md +2 -0
- package/modules/ui-design/kit/README.md +7 -5
- package/modules/ui-design/kit/check-mock.js +12 -5
- package/modules/ui-design/kit/contrast.js +13 -6
- package/modules/ui-design/kit/lib.js +39 -2
- package/modules/ui-design/kit/probe.js +4 -1
- package/modules/ui-design/kit/render.js +7 -4
- package/modules/ui-design/kit/serve.js +9 -2
- package/modules/ui-design/styles/README.md +54 -2
- package/modules/ui-design/styles/chrome-world/DESIGN.md +56 -0
- package/modules/ui-design/styles/chrome-world/mock.html +275 -0
- package/modules/ui-design/styles/chrome-world/mock.states.json +11 -0
- package/modules/ui-design/styles/chrome-world/pack.json +33 -0
- package/modules/ui-design/styles/expedition/DESIGN.md +141 -0
- package/modules/ui-design/styles/expedition/mock.html +285 -0
- package/modules/ui-design/styles/expedition/mock.states.json +11 -0
- package/modules/ui-design/styles/expedition/pack.json +33 -0
- package/modules/ui-design/styles/grove/DESIGN.md +148 -0
- package/modules/ui-design/styles/grove/mock.html +294 -0
- package/modules/ui-design/styles/grove/mock.states.json +11 -0
- package/modules/ui-design/styles/grove/pack.json +33 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/cosmos_token_contrast.mjs +9 -2
- package/tests/hall_tokens.mjs +15 -6
- package/tests/ui_design_styles.mjs +218 -0
- package/tests/world_tokens.mjs +9 -2
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// tests/ui_design_styles.mjs — the ui-design module's style library (task 1003323, ADR 0219)
|
|
2
|
+
// is a pinned contract, not a folder of ideas: every style dir carries a pack (exactly the
|
|
3
|
+
// fifteen), a DESIGN.md whose frontmatter holds the same thirteen value for value, and a
|
|
4
|
+
// specimen mock that passes the platform's rules under its own colours; the chrome world
|
|
5
|
+
// entry IS the neutral pack; every pack clears the pack floor; the three served sheets
|
|
6
|
+
// (world.css, cosmos.css, the hall's style.css) clear AA under every alternate in both modes
|
|
7
|
+
// (their own suites run once per pack through UI_DESIGN_PACK, and a bad pack is proven to
|
|
8
|
+
// fail them); the stub serves a page under a style with the look's fifteen spliced in and the
|
|
9
|
+
// instance's identity untouched; the CLIs refuse an unknown style; and the doors (the styles
|
|
10
|
+
// README, the module CLAUDE.md, /design) name every look. DB-free; the one browser-free stub
|
|
11
|
+
// round-trip is the same the kit test already makes.
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { test } from 'node:test';
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { listStyles, resolveStyle, styleBrandingFile, packColors, startStub, STYLES_DIR, EXIT } from '../modules/ui-design/kit/lib.js';
|
|
20
|
+
import { packContrast, PACK_PAIRS } from '../modules/ui-design/kit/contrast.js';
|
|
21
|
+
import { checkMock, readDesignBans } from '../modules/ui-design/kit/check-mock.js';
|
|
22
|
+
|
|
23
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
|
+
const KIT = path.join(ROOT, 'modules', 'ui-design', 'kit');
|
|
25
|
+
const read = (p) => fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n');
|
|
26
|
+
const readJson = (p) => JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
27
|
+
const neutralUi = readJson(path.join(ROOT, 'config', 'branding.neutral.json')).theme.ui;
|
|
28
|
+
const THIRTEEN = Object.keys(neutralUi.colors).sort();
|
|
29
|
+
const camelToKebab = (k) => k.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
|
|
30
|
+
const styles = listStyles().map((name) => resolveStyle(name));
|
|
31
|
+
const alternates = styles.filter((s) => s.name !== 'chrome-world');
|
|
32
|
+
// a child `node --test` must not inherit NODE_TEST_CONTEXT, or it reports to this runner's
|
|
33
|
+
// channel instead of its own stdout (an empty stdout and exit 0: a silent pass)
|
|
34
|
+
const childEnv = (extra = {}) => { const e = { ...process.env, ...extra }; delete e.NODE_TEST_CONTEXT; return e; };
|
|
35
|
+
const run = (file, args, env = {}) => spawnSync(process.execPath, [path.join(KIT, file), ...args], { encoding: 'utf8', env: childEnv(env), timeout: 60000 });
|
|
36
|
+
|
|
37
|
+
// The `colors:` block of a DESIGN.md frontmatter → { 'bg': '#…', … } (quoted scalars only;
|
|
38
|
+
// comments and the nested keys of other blocks are skipped).
|
|
39
|
+
function frontmatterColors(md) {
|
|
40
|
+
const fm = md.match(/^---\n([\s\S]*?)\n---\n/);
|
|
41
|
+
assert.ok(fm, 'DESIGN.md opens with a frontmatter block');
|
|
42
|
+
const lines = fm[1].split('\n');
|
|
43
|
+
const start = lines.findIndex((l) => /^colors:\s*$/.test(l));
|
|
44
|
+
assert.ok(start >= 0, 'the frontmatter carries a colors: block');
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const line of lines.slice(start + 1)) {
|
|
47
|
+
if (/^[a-z]/.test(line)) break;
|
|
48
|
+
const m = line.match(/^\s+([a-z][a-z0-9-]*):\s*"([^"]*)"/);
|
|
49
|
+
if (m) out[m[1]] = m[2];
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── the catalogue ─────────────────────────────────────────────────────────────
|
|
55
|
+
test('the catalogue: the chrome world plus at least two alternates, each dir carrying pack.json, DESIGN.md and mock.html', () => {
|
|
56
|
+
assert.ok(styles.some((s) => s.name === 'chrome-world'), 'the chrome world is the first entry');
|
|
57
|
+
assert.ok(alternates.length >= 2, `at least two alternates (found ${alternates.map((s) => s.name).join(', ') || 'none'})`);
|
|
58
|
+
for (const s of styles) {
|
|
59
|
+
for (const f of ['pack.json', 'DESIGN.md', 'mock.html']) assert.ok(fs.existsSync(path.join(s.dir, f)), `${s.name}: ${f}`);
|
|
60
|
+
assert.equal(s.name, path.basename(s.dir), `${s.name}: pack.json names its own dir`);
|
|
61
|
+
assert.match(s.name, /^[a-z0-9][a-z0-9-]*$/, `${s.name}: kebab-case`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('a pack is exactly the fifteen: the thirteen colour keys of the neutral pack, the two font stacks and a links line, with a title, a summary and an origin', () => {
|
|
66
|
+
for (const s of styles) {
|
|
67
|
+
const ui = s.pack.theme && s.pack.theme.ui;
|
|
68
|
+
assert.ok(ui && ui.colors && ui.fonts, `${s.name}: theme.ui.colors + theme.ui.fonts`);
|
|
69
|
+
assert.deepEqual(Object.keys(ui.colors).sort(), THIRTEEN, `${s.name}: exactly the thirteen`);
|
|
70
|
+
for (const [k, v] of Object.entries(ui.colors)) assert.match(v, /^#[0-9a-f]{6}$/i, `${s.name}: ${k} is a plain hex (the sheets do hex maths on the contract tier)`);
|
|
71
|
+
assert.ok(typeof ui.fonts.display === 'string' && ui.fonts.display && typeof ui.fonts.body === 'string' && ui.fonts.body, `${s.name}: both font stacks`);
|
|
72
|
+
assert.ok(Array.isArray(ui.fonts.links) && ui.fonts.links.length && ui.fonts.links.every((l) => /^https:\/\//.test(l)), `${s.name}: fonts.links carries the face stylesheet(s)`);
|
|
73
|
+
assert.deepEqual(Object.keys(ui).sort(), ['colors', 'fonts'], `${s.name}: a look sets the fifteen and nothing else (favicon and backdrop stay the instance's)`);
|
|
74
|
+
for (const k of ['title', 'summary', 'origin']) assert.ok(typeof s.pack[k] === 'string' && s.pack[k].length > 8, `${s.name}: ${k}`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('the chrome world pack IS the neutral pack, and every alternate is a different look', () => {
|
|
79
|
+
const chrome = styles.find((s) => s.name === 'chrome-world');
|
|
80
|
+
assert.deepEqual(chrome.pack.theme.ui.colors, neutralUi.colors, 'chrome-world colours = config/branding.neutral.json theme.ui.colors');
|
|
81
|
+
assert.equal(chrome.pack.theme.ui.fonts.display, neutralUi.fonts.display);
|
|
82
|
+
assert.equal(chrome.pack.theme.ui.fonts.body, neutralUi.fonts.body);
|
|
83
|
+
assert.deepEqual(chrome.pack.theme.ui.fonts.links, neutralUi.fonts.links);
|
|
84
|
+
for (const s of alternates) {
|
|
85
|
+
assert.notEqual(s.pack.theme.ui.colors.accent.toLowerCase(), neutralUi.colors.accent.toLowerCase(), `${s.name}: a look has its own accent`);
|
|
86
|
+
assert.notEqual(s.pack.theme.ui.fonts.display, neutralUi.fonts.display, `${s.name}: a look has its own display face`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('every DESIGN.md frontmatter carries its pack\'s thirteen, value for value, and its prose names the look\'s adoption path', () => {
|
|
91
|
+
for (const s of styles) {
|
|
92
|
+
const md = read(path.join(s.dir, 'DESIGN.md'));
|
|
93
|
+
const colors = frontmatterColors(md);
|
|
94
|
+
for (const [k, v] of Object.entries(s.pack.theme.ui.colors)) {
|
|
95
|
+
assert.equal((colors[camelToKebab(k)] || '').toLowerCase(), v.toLowerCase(), `${s.name}: DESIGN.md ${camelToKebab(k)} = pack ${k}`);
|
|
96
|
+
}
|
|
97
|
+
assert.match(md, /config\/branding\.json/, `${s.name}: DESIGN.md says how an instance adopts the look`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ── the floors ────────────────────────────────────────────────────────────────
|
|
102
|
+
test('every pack clears the pack floor: every ink on every ground at 4.5:1', () => {
|
|
103
|
+
for (const s of styles) {
|
|
104
|
+
const rows = packContrast(s.pack);
|
|
105
|
+
assert.equal(rows.length, PACK_PAIRS.length);
|
|
106
|
+
for (const r of rows) assert.equal(r.ok, true, `${s.name}: ${r.fg} on ${r.bg} = ${r.ratio} (need ${r.need})`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('every mock passes the platform rules under its own colours and its own DESIGN.md bans, is stamped by the transform, and declares no colour of its own', () => {
|
|
111
|
+
for (const s of styles) {
|
|
112
|
+
const html = read(path.join(s.dir, 'mock.html'));
|
|
113
|
+
const bans = readDesignBans(read(path.join(s.dir, 'DESIGN.md')));
|
|
114
|
+
const results = checkMock(html, { bans, packColors: packColors(s.pack) });
|
|
115
|
+
const fails = results.filter((r) => r[0] === 'FAIL').map((r) => `${r[1]} (${r[2]})`);
|
|
116
|
+
assert.deepEqual(fails, [], `${s.name}: mock.html`);
|
|
117
|
+
assert.ok(results.some((r) => r[1].startsWith('every dark rule written twice') && r[0] === 'PASS'), `${s.name}: the mock carries a dark tier, written twice`);
|
|
118
|
+
assert.ok(html.includes('{{productName}}'), `${s.name}: the mock carries the identity token the transform stamps`);
|
|
119
|
+
assert.ok(bans.has('em-dash') && bans.has('emoji'), `${s.name}: the look's DESIGN.md declares the copy bans`);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('the world, hub and hall sheets clear AA under every alternate in both modes: the three token suites run once per pack', () => {
|
|
124
|
+
// relative, forward-slash paths: `node --test <path>` reads its positionals as globs, and a
|
|
125
|
+
// Windows absolute path matches nothing (zero tests, exit 0 — a silent pass)
|
|
126
|
+
const suites = ['tests/world_tokens.mjs', 'tests/cosmos_token_contrast.mjs', 'tests/hall_tokens.mjs'];
|
|
127
|
+
for (const s of alternates) {
|
|
128
|
+
const r = spawnSync(process.execPath, ['--test', '--test-reporter=tap', ...suites], { cwd: ROOT, encoding: 'utf8', env: childEnv({ UI_DESIGN_PACK: s.file }), timeout: 120000 });
|
|
129
|
+
assert.match(r.stdout, /# tests [1-9]/, `${s.name}: the suites actually ran`);
|
|
130
|
+
const fails = (r.stdout.match(/^not ok .*$/gm) || []).join('\n');
|
|
131
|
+
assert.equal(r.status, 0, `${s.name}: a sheet fails under this pack\n${fails}\n${(r.stdout.split('\n').filter((l) => /^\s+(\+|-|\*|actual|expected|error|message)/.test(l) || /:1 \(need|vs pack| = \d/.test(l)).slice(0, 30).join('\n'))}`);
|
|
132
|
+
assert.match(r.stdout, /# pass \d+/);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('UI_DESIGN_PACK is honoured, not decorative: a pack that thins ink-faint fails the same suites', () => {
|
|
137
|
+
const bad = structuredClone(readJson(path.join(STYLES_DIR, 'expedition', 'pack.json')));
|
|
138
|
+
bad.theme.ui.colors.inkFaint = '#c8c6bd';
|
|
139
|
+
const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'ui-style-bad-')), 'pack.json');
|
|
140
|
+
fs.writeFileSync(file, JSON.stringify(bad));
|
|
141
|
+
const r = spawnSync(process.execPath, ['--test', '--test-reporter=tap', 'tests/world_tokens.mjs'], { cwd: ROOT, encoding: 'utf8', env: childEnv({ UI_DESIGN_PACK: file }), timeout: 60000 });
|
|
142
|
+
assert.match(r.stdout, /# tests [1-9]/, 'the suite actually ran');
|
|
143
|
+
assert.notEqual(r.status, 0, 'a thinned ink-faint must fail the world suite under the env var');
|
|
144
|
+
assert.match(r.stdout, /ink-faint on the page ground/, 'the failing pair is named');
|
|
145
|
+
const drift = spawnSync(process.execPath, ['--test', '--test-reporter=tap', 'tests/world_tokens.mjs'], { cwd: ROOT, encoding: 'utf8', env: childEnv({ UI_DESIGN_PACK: path.join(STYLES_DIR, 'expedition', 'pack.json') }), timeout: 60000 });
|
|
146
|
+
assert.match(drift.stdout, /^ok .*do not drift/m, 'the drift test keeps measuring the sheet against the NEUTRAL pack under an alternate');
|
|
147
|
+
fs.rmSync(path.dirname(file), { recursive: true, force: true });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ── the picker ────────────────────────────────────────────────────────────────
|
|
151
|
+
test('resolveStyle: by name, by pack.json path, and nothing else; styleBrandingFile lays the look over the instance and keeps its identity', () => {
|
|
152
|
+
const byName = resolveStyle('expedition');
|
|
153
|
+
assert.equal(byName.name, 'expedition');
|
|
154
|
+
assert.equal(resolveStyle(byName.file).name, 'expedition', 'a pack.json path resolves too');
|
|
155
|
+
assert.equal(resolveStyle('no-such-look'), null);
|
|
156
|
+
assert.equal(resolveStyle('../kit/lib.js'), null, 'a path that is not a pack.json does not resolve');
|
|
157
|
+
assert.equal(resolveStyle(''), null);
|
|
158
|
+
const merged = readJson(styleBrandingFile(byName));
|
|
159
|
+
assert.equal(merged.theme.ui.colors.accent, '#ffd214', 'the look\'s fifteen');
|
|
160
|
+
assert.equal(merged.theme.ui.fonts.display, byName.pack.theme.ui.fonts.display);
|
|
161
|
+
assert.equal(merged.identity, undefined, 'a neutral checkout has no instance pack: only theme is written, so identity resolves from the neutral starter');
|
|
162
|
+
// a pack.json loaded by path names itself: the temp file the transform reads takes a slug, never the name
|
|
163
|
+
const hostile = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'ui-style-name-')), 'pack.json');
|
|
164
|
+
fs.writeFileSync(hostile, JSON.stringify({ ...byName.pack, name: '../../escape' }));
|
|
165
|
+
const written = styleBrandingFile(resolveStyle(hostile));
|
|
166
|
+
assert.equal(path.dirname(written), path.join(os.tmpdir(), 'ui-design-kit', 'styles'), "the merged pack is written inside the kit's temp dir whatever the pack calls itself");
|
|
167
|
+
assert.match(path.basename(written), /^[A-Za-z0-9_-]+\.branding\.json$/);
|
|
168
|
+
fs.rmSync(path.dirname(hostile), { recursive: true, force: true });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('the stub serves a real surface and a specimen under a style: the spliced :root carries the look\'s fifteen, the instance\'s name is stamped, the feeds still answer', async () => {
|
|
172
|
+
const s = await startStub({ surface: 'public-landing', style: 'grove' });
|
|
173
|
+
try {
|
|
174
|
+
const html = await (await fetch(s.base + '/')).text();
|
|
175
|
+
assert.ok(html.includes('--accent:#e3c39c;'), 'the grove\'s accent is in the spliced :root');
|
|
176
|
+
assert.ok(html.includes('--font-display:"Nunito"'), 'the grove\'s display face');
|
|
177
|
+
assert.ok(html.includes('family=Nunito'), 'the look\'s font link is injected');
|
|
178
|
+
assert.ok(html.includes('window.__BRANDING__'), 'the pack is injected');
|
|
179
|
+
assert.ok(!html.includes('#fb9569'), 'the neutral accent does not leak into the served page');
|
|
180
|
+
assert.equal((await fetch(s.base + '/api/gds/public/progress')).status, 200);
|
|
181
|
+
} finally { s.stop(); }
|
|
182
|
+
const spec = await startStub({ surface: 'modules/ui-design/styles/expedition', style: 'expedition' });
|
|
183
|
+
try {
|
|
184
|
+
const html = await (await fetch(spec.base + '/mock')).text();
|
|
185
|
+
assert.ok(html.includes('--accent:#ffd214;'), 'the specimen is served under its own pack');
|
|
186
|
+
assert.ok(!html.includes('{{productName}}'), 'the identity token is stamped by the real transform');
|
|
187
|
+
assert.match(html, /<title>[^<]*the expedition, on a sheet<\/title>/);
|
|
188
|
+
} finally { spec.stop(); }
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('the CLIs refuse an unknown style with the catalogue named, before any browser; contrast --pack <name> measures a look', () => {
|
|
192
|
+
const bad = run('serve.js', [], { STYLE: 'no-such-look', STUB_PORT: '0' });
|
|
193
|
+
assert.equal(bad.status, EXIT.USAGE); assert.match(bad.stderr, /neither a style/); assert.match(bad.stderr, /chrome-world, expedition, grove/);
|
|
194
|
+
const render = run('render.js', ['--page', '/', '--style', 'no-such-look']);
|
|
195
|
+
assert.equal(render.status, EXIT.USAGE); assert.match(render.stderr, /--style "no-such-look" is not a style/);
|
|
196
|
+
const probe = run('probe.js', ['--page', '/projects', '--style', 'no-such-look']);
|
|
197
|
+
assert.equal(probe.status, EXIT.USAGE); assert.match(probe.stderr, /--style "no-such-look" is not a style/);
|
|
198
|
+
const c = run('contrast.js', ['--pack', 'expedition']);
|
|
199
|
+
assert.equal(c.status, EXIT.CLEAN, c.stdout + c.stderr); assert.match(c.stdout, /the expedition pack/); assert.match(c.stdout, /PACK CLEAN/);
|
|
200
|
+
const cBad = run('contrast.js', ['--pack', 'no-such-look']);
|
|
201
|
+
assert.equal(cBad.status, EXIT.USAGE); assert.match(cBad.stderr, /not a style/);
|
|
202
|
+
const mock = run('check-mock.js', ['--file', path.join(STYLES_DIR, 'grove', 'mock.html'), '--pack', 'grove']);
|
|
203
|
+
assert.equal(mock.status, EXIT.CLEAN, mock.stdout); assert.match(mock.stdout, /ALL RULES PASS/);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ── the doors ─────────────────────────────────────────────────────────────────
|
|
207
|
+
test('the styles README, the module CLAUDE.md and /design name every look', () => {
|
|
208
|
+
const readme = read(path.join(STYLES_DIR, 'README.md'));
|
|
209
|
+
const claude = read(path.join(ROOT, 'modules', 'ui-design', 'CLAUDE.md'));
|
|
210
|
+
const design = read(path.join(ROOT, '.claude', 'skills', 'design', 'SKILL.md'));
|
|
211
|
+
for (const s of styles) {
|
|
212
|
+
assert.ok(readme.includes(`\`${s.name}\``), `styles/README.md rosters ${s.name}`);
|
|
213
|
+
assert.ok(claude.includes(`\`${s.name}\``), `modules/ui-design/CLAUDE.md names ${s.name}`);
|
|
214
|
+
assert.ok(design.split('\n').some((l) => l.includes(`\`${s.name}\``)), `/design names ${s.name} on a line`);
|
|
215
|
+
}
|
|
216
|
+
assert.match(design, /STYLE=/, '/design says how to render under a look');
|
|
217
|
+
assert.match(readme, /GDS_BRANDING_FILE/, 'the README names the knob the picker rides');
|
|
218
|
+
});
|
package/tests/world_tokens.mjs
CHANGED
|
@@ -93,9 +93,15 @@ const darkMedia = declarations(blockAfter(':root:not([data-mode="light"]) {'));
|
|
|
93
93
|
// pack emits the PACK wins and world.css's own value never renders. Measuring
|
|
94
94
|
// world.css alone would measure a page nobody sees. The dark blocks are (0,2,0)
|
|
95
95
|
// and win back.
|
|
96
|
-
|
|
96
|
+
// UI_DESIGN_PACK=<pack.json> measures the sheet under a look from the style library
|
|
97
|
+
// (tests/ui_design_styles.mjs runs this file once per pack, task 1003323); the DRIFT test
|
|
98
|
+
// below always compares the sheet against the neutral pack, which is what it must equal.
|
|
99
|
+
const neutralUi = JSON.parse(
|
|
97
100
|
fs.readFileSync(path.join(ROOT, 'config', 'branding.neutral.json'), 'utf8'),
|
|
98
101
|
).theme.ui;
|
|
102
|
+
const packUi = process.env.UI_DESIGN_PACK
|
|
103
|
+
? JSON.parse(fs.readFileSync(path.resolve(process.env.UI_DESIGN_PACK), 'utf8')).theme.ui
|
|
104
|
+
: neutralUi;
|
|
99
105
|
|
|
100
106
|
// mirrors COLOR_VARS in src/bongos/serve-internal.js
|
|
101
107
|
const camelToVar = (k) => '--' + k.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
|
|
@@ -173,7 +179,8 @@ test('world.css defaults do not drift from the branding pack', () => {
|
|
|
173
179
|
// page reads correctly standalone (opened as a file, with no server) only while
|
|
174
180
|
// the two agree. This is the fifteen: thirteen colours plus the two faces.
|
|
175
181
|
const drift = [];
|
|
176
|
-
const
|
|
182
|
+
const NEUTRAL = Object.fromEntries(Object.entries(neutralUi.colors).map(([k, v]) => [camelToVar(k), v]));
|
|
183
|
+
const expected = { ...NEUTRAL, '--font-display': neutralUi.fonts.display, '--font-body': neutralUi.fonts.body };
|
|
177
184
|
for (const [name, packValue] of Object.entries(expected)) {
|
|
178
185
|
const cssValue = contractTier[name];
|
|
179
186
|
if (cssValue === undefined) {
|