@bongos/core 1.24.58 → 1.24.60

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.
@@ -28,7 +28,7 @@ const readReal = (f) => fs.readFileSync(path.join(ROOT, f), 'utf8');
28
28
 
29
29
  test('the detector counts typed-in values and leaves tokens alone', () => {
30
30
  const c = scan.countSheet(`
31
- :root { --ds-x: 14px; --tone: #123456; } /* token definitions: not counted */
31
+ :root { --ds-x: 14px; --tone: #123456; } /* a token sheet's definitions: not counted */
32
32
  .a { font-size: 13px; border-radius: 6px; color: #abc; }
33
33
  .b { font-size: var(--ds-text-sm); border-radius: var(--ds-radius-tile); color: var(--ink); }
34
34
  .c { box-shadow: 0 2px 4px rgba(0, 0, 0, .2); } /* a shadow; the black scrim is allowed */
@@ -36,7 +36,7 @@ test('the detector counts typed-in values and leaves tokens alone', () => {
36
36
  .e { background: color-mix(in srgb, var(--ink) 40%, rgb(12 34 56)); padding: 0; }
37
37
  @media (max-width: 720px) { .f { border-radius: 0; } }
38
38
  @media (prefers-reduced-motion: reduce) { .g { transition: none; } }
39
- `);
39
+ `, { tokens: true });
40
40
  assert.deepEqual(c, { colours: 2, font_sizes: 1, radii: 1, shadows: 1, breakpoints: 1 });
41
41
  });
42
42
 
@@ -99,6 +99,8 @@ const GENERATOR_SOURCES = [
99
99
  'modules/hall-ui/public/controls.css',
100
100
  'modules/hall-ui/public/dom-utils.js',
101
101
  'scripts/bongos/gen-brand.js',
102
+ 'src/brand-mark.js',
103
+ 'src/html-escape.js',
102
104
  'DESIGN.md',
103
105
  ];
104
106
 
@@ -132,3 +134,96 @@ test('the freshness gate passes on a fresh tree and fails when the token sheet c
132
134
  fs.rmSync(dir, { recursive: true, force: true });
133
135
  }
134
136
  });
137
+
138
+ // task 1005133: the logo set joins the gate. Editing the one source (mark.svg) without
139
+ // running gen-brand.js fails it; so does a PNG whose source page moved (the pack's ground)
140
+ // or a PNG swapped by hand — the PNGs are checked by hash, not just their pixel size.
141
+ test('the freshness gate covers the logo set: mark.svg, the pack and both PNGs', () => {
142
+ const dir = copiedTree();
143
+ const brand = path.join(dir, 'modules/ui-design/public/brand');
144
+ const gate = () => checkDesignGeneratorsFresh(dir).filter((x) => x.startsWith('the logo set'));
145
+ try {
146
+ assert.deepEqual(gate(), [], 'the copied logo set should start fresh');
147
+
148
+ const mark = path.join(brand, 'mark.svg');
149
+ const src = fs.readFileSync(mark, 'utf8');
150
+ const moved = src.replace(/(id="mark-dot"[^>]*?\br=")(\d+)/, (_, a, r) => a + (Number(r) + 1));
151
+ assert.notEqual(moved, src, 'mark.svg should carry the moon\'s radius');
152
+ fs.writeFileSync(mark, moved);
153
+ assert.ok(gate().some((x) => /mark-light\.svg/.test(x)), `editing mark.svg must fail the gate: ${gate().join('\n')}`);
154
+ fs.writeFileSync(mark, src);
155
+ assert.deepEqual(gate(), [], 'restoring mark.svg restores the gate');
156
+
157
+ const pngFile = path.join(brand, 'og-card.png');
158
+ const png = fs.readFileSync(pngFile);
159
+ const swapped = Buffer.from(png); swapped[swapped.length - 20] ^= 0xff; // same size, other bytes
160
+ fs.writeFileSync(pngFile, swapped);
161
+ assert.ok(gate().some((x) => /og-card\.png \(the file is not the one --png wrote\)/.test(x)), gate().join('\n'));
162
+ fs.writeFileSync(pngFile, png);
163
+
164
+ const packFile = path.join(dir, 'config/branding.neutral.json');
165
+ const pack = JSON.parse(fs.readFileSync(packFile, 'utf8'));
166
+ pack.theme.ui.colors.bg = '#edeced';
167
+ fs.writeFileSync(packFile, JSON.stringify(pack, null, 2));
168
+ assert.ok(gate().some((x) => /apple-touch-icon\.png \(its source page changed/.test(x)), gate().join('\n'));
169
+ } finally {
170
+ fs.rmSync(dir, { recursive: true, force: true });
171
+ }
172
+ });
173
+
174
+ // task 1005134: two loopholes closed. (1) A sheet that names its colours as custom
175
+ // properties used to count zero, so a new sheet could bypass its zero start; now
176
+ // only the shared token definitions are exempt. (2) A page's inline <style> and the
177
+ // error page's CSS string were never scanned; now they are, with baselines.
178
+ const HALL_TOKENS = /^(?::root|html\[data-theme="dark"\]|html:not\(\[data-theme\]\))$/;
179
+
180
+ test('a colour typed into a custom property counts, outside the shared token definitions', () => {
181
+ assert.equal(scan.countSheet(':root { --mine: #123456; }').colours, 1, ':root{--mine:#123456}');
182
+ assert.equal(scan.countSheet('body { --x: rgb(10, 20, 30); --y: hsl(1 2% 3%); }').colours, 2, 'body{--x:rgb(...)}');
183
+ assert.equal(scan.countSheet('.v { --veil: rgba(0, 0, 0, .4); --tone: var(--ink); }').colours, 0, 'a scrim and a reference are not typed-in colours');
184
+ assert.equal(scan.countSheet(':root { --mine: #123456; } .x { --y: #654321; }', { tokens: true }).colours, 0, 'the shared token sheet is exempt');
185
+ const hall = ':root { --a: #111111; } html[data-theme="dark"] { --a: #222222; } '
186
+ + '@media (prefers-color-scheme: dark) { html:not([data-theme]) { --a: #222222; } } '
187
+ + '.brand { --mark-ink: #333333; } @media (max-width: 1px) { .y { --z: #444444; } }';
188
+ assert.equal(scan.countSheet(hall, { tokens: HALL_TOKENS }).colours, 2, 'the hall token block is exempt; its other rules are not');
189
+ });
190
+
191
+ test('a NEW sheet that names its colours as custom properties fails fitness', () => {
192
+ const NEW = 'modules/x/public/new.css';
193
+ const { sheets } = scan.scanCssLiterals({ files: [NEW], read: () => ':root { --mine: #123456; } body { --x: rgb(1, 2, 3); }' });
194
+ const d = ratchets.compareDesignCounts(sheets, { _comment: 'note' });
195
+ assert.ok(d.violations.some((v) => v.includes(`colours in ${NEW} regressed: 2 > baseline 0`)), d.violations.join('\n'));
196
+ // …while a definition planted in the shared token sheet, or the hall's :root, stays exempt.
197
+ const planted = scan.scanCssLiterals({
198
+ read: (f) => readReal(f) + (f === 'modules/ui-design/public/tokens.css' || f === HALL_SHEET ? '\n:root { --mine: #123456; }\n' : ''),
199
+ });
200
+ assert.deepEqual(ratchets.checkQualityRatchets({ metrics: {}, designCounts: planted.sheets }).violations, []);
201
+ const leak = scan.scanCssLiterals({ read: (f) => readReal(f) + (f === HALL_SHEET ? '\n.planted { --mine: #123456; }\n' : '') });
202
+ const r = ratchets.checkQualityRatchets({ metrics: {}, designCounts: leak.sheets });
203
+ assert.ok(r.violations.some((v) => v.includes(`colours in ${HALL_SHEET} regressed`)), r.violations.join('\n'));
204
+ });
205
+
206
+ test('inline <style> blocks in served pages and the error page CSS are scanned', () => {
207
+ const PAGE = 'modules/public-landing/public/terms.html';
208
+ const ERR = 'src/error-page.js';
209
+ const { sheets } = scan.scanCssLiterals();
210
+ assert.ok(sheets[ERR], `${ERR} is scanned`);
211
+ assert.ok(sheets[PAGE], `${PAGE} is scanned`);
212
+ assert.ok(!Object.keys(sheets).some((f) => /\/styles\/[^/]+\/mock\.html$|\/specimens\/.*\.html$/.test(f)), 'unserved mocks and specimens are not');
213
+ assert.equal(scan.countSheet('').colours, 0);
214
+ const planted = scan.scanCssLiterals({
215
+ read: (f) => {
216
+ const t = readReal(f);
217
+ if (f === PAGE || f === ERR) return t.replace('</style>', '.planted { color: #123456; font-size: 13px; }\n</style>');
218
+ return t;
219
+ },
220
+ });
221
+ const r = ratchets.checkQualityRatchets({ metrics: {}, designCounts: planted.sheets });
222
+ for (const f of [PAGE, ERR]) {
223
+ assert.ok(r.violations.some((v) => v.includes(`colours in ${f} regressed`)), `${f} colours: ${r.violations.join('\n')}`);
224
+ assert.ok(r.violations.some((v) => v.includes(`font_sizes in ${f} regressed`)), `${f} sizes: ${r.violations.join('\n')}`);
225
+ }
226
+ // Text outside a <style> block is not CSS: a hex in a page's prose is not counted.
227
+ const prose = scan.scanCssLiterals({ read: (f) => readReal(f) + (f === PAGE ? '\n<p>color: #123456;</p>\n' : '') });
228
+ assert.deepEqual(prose.sheets[PAGE], sheets[PAGE]);
229
+ });
@@ -88,7 +88,7 @@ test('the shared components, the logo and the naming rule are on the page', () =
88
88
  }
89
89
  assert.equal((PAGE.match(/data-ref="tones"[^>]*>(.*?)<\/div>/) || [])[1].match(/status-pill--/g).length, 6, 'six tones');
90
90
  assert.match(PAGE, /<use href="\/modules\/ui-design\/public\/brand\/mark\.svg#mark"\/>/, 'the mark is drawn from its one source');
91
- assert.match(PAGE, /<span class="ref-lockup__name">cloud bongos<\/span>/, 'the wordmark is lowercase (DESIGN.md Naming)');
91
+ assert.match(PAGE, /<span class="ref-lockup__name">\{\{productName\}\}<\/span>/, 'the wordmark is the pack\'s name, stamped at serve time (task 1005135)');
92
92
  assert.match(PAGE, /Never &quot;CloudBongos&quot;/, 'the naming rule is read from DESIGN.md');
93
93
  for (const f of ['mark-light.svg', 'mark-dark.svg', 'mark-mono.svg', 'favicon.svg']) assert.ok(PAGE.includes(`brand/${f}"`), `lists ${f}`);
94
94
  });
@@ -0,0 +1,111 @@
1
+ // tests/wordmark_and_faces.mjs — the logo wordmark is the pack's name in lowercase, and
2
+ // no served page loads a retired face (task 1005135, goal 1000138).
3
+ //
4
+ // DESIGN.md "Naming": beside the mark the name is lowercase ("cloud bongos"), and
5
+ // platform code never hardcodes either spelling; it reads the pack. The UI check found
6
+ // the status page, the 404/500 pages and the API docs showing "Cloud Bongos" beside the
7
+ // mark, and /brand baking the neutral pack's name into its generated HTML. The sign-in
8
+ // error and confirming-access pages still loaded Cinzel and EB Garamond. This pins:
9
+ // 1. every wordmark beside the mark is the {{productName}} hole (stamped from the pack
10
+ // at serve time), never a typed-in name, and the rule that styles it lowercases it;
11
+ // 2. no served page source (the auth pages, the sign-in shell, the error page, the API
12
+ // docs, the status page, the hall, the landing) names Cinzel or Garamond, and the
13
+ // rendered auth pages carry the pack's own font links instead;
14
+ // 3. the error page declares no colour of its own: it reads the hall sheet's tier.
15
+ import { test } from 'node:test';
16
+ import assert from 'node:assert/strict';
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { createRequire } from 'node:module';
21
+
22
+ const require = createRequire(import.meta.url);
23
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
24
+ const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
25
+ const stripComments = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/<!--[\s\S]*?-->/g, '').replace(/(^|[^:'"`\\])\/\/[^\n]*/g, '$1');
26
+
27
+ const { errorPageTemplate } = require('../src/error-page.js');
28
+ const { clientBranding } = require('../src/branding.js');
29
+ const authRoutes = require('../src/bongos/routes/auth.js');
30
+ const signIn = require('../src/bongos/sign-in-pages.js');
31
+
32
+ // The CSS rule for a selector, from a sheet or an inline <style>. Throws when missing.
33
+ function ruleOf(css, selector) {
34
+ const esc = selector.replace(/[.*+?^${}()|[\]\\#]/g, '\\$&');
35
+ const m = stripComments(css).match(new RegExp(`(?:^|[}\\s,])${esc}\\s*\\{([^}]*)\\}`));
36
+ assert.ok(m, `a rule for ${selector}`);
37
+ return m[1];
38
+ }
39
+ const lowercased = (body) => /text-transform\s*:\s*lowercase/.test(body);
40
+
41
+ // Each wordmark beside the mark: [where, the markup that must carry the pack's hole, the
42
+ // sheet that styles it, the selector of its rule].
43
+ const WORDMARKS = [
44
+ ['the status page', read('modules/status-ui/public/index.html'), /<span class="topbar__name">\{\{productName\}\}<\/span>/, read('modules/status-ui/public/style.css'), '.topbar__name'],
45
+ ['the 404 page', errorPageTemplate(404), /<a class="name" href="\/">\{\{productName\}\}<\/a>/, errorPageTemplate(404), '.name'],
46
+ ['the 500 page', errorPageTemplate(500), /<a class="name" href="\/">\{\{productName\}\}<\/a>/, errorPageTemplate(500), '.name'],
47
+ ['the API docs', read('public-docs/index.html'), /<span class="wordmark">\{\{productName\}\}<\/span>/, read('public-docs/index.html'), '#topbar .wordmark'],
48
+ ['/brand (bar)', read('modules/ui-design/public/brand.html'), /<a class="ref-head__brand"[^>]*>.*<span>\{\{productName\}\}<\/span><\/a>/, read('modules/ui-design/public/brand.css'), '.ref-head__brand'],
49
+ ['/brand (lockup)', read('modules/ui-design/public/brand.html'), /<span class="ref-lockup__name">\{\{productName\}\}<\/span>/, read('modules/ui-design/public/brand.css'), '.ref-lockup'],
50
+ ['the hall', read('modules/hall-ui/public/style.css'), /\.brand-mark\s*\{/, read('modules/hall-ui/public/style.css'), '.brand-mark'],
51
+ ];
52
+
53
+ test('every wordmark beside the mark is the pack\'s name, lowercased by its rule', () => {
54
+ for (const [where, markup, hole, css, selector] of WORDMARKS) {
55
+ assert.match(markup, hole, `${where}: the wordmark is the {{productName}} hole`);
56
+ assert.ok(lowercased(ruleOf(css, selector)), `${where}: ${selector} sets text-transform: lowercase`);
57
+ }
58
+ });
59
+
60
+ test('no typed-in product name beside the mark', () => {
61
+ // A capitalised (or any) literal in a wordmark element is exactly the regression.
62
+ const literal = /class="(?:topbar__name|name|wordmark|ref-lockup__name)"[^>]*>(?!\{\{)[^<]+</;
63
+ for (const [where, markup] of WORDMARKS) assert.doesNotMatch(markup, literal, `${where}: no literal name in the wordmark`);
64
+ assert.doesNotMatch(read('modules/ui-design/public/brand.html'), /ref-head__brand[^>]*>.*<span>(?!\{\{)[^<]+<\/span><\/a>/, '/brand bar: no literal name');
65
+ // the generator writes the hole, never the pack's name it happened to read
66
+ assert.doesNotMatch(read('modules/ui-design/scripts/gen-design-reference.js'), /productName\)?\.toLowerCase\(\)|product\.toLowerCase\(\)/);
67
+ });
68
+
69
+ const RETIRED = /Cinzel|Garamond/i;
70
+ const SERVED = [
71
+ 'src/bongos/routes/auth.js', 'src/bongos/sign-in-pages.js', 'src/bongos/routes/auth-google.js', 'src/error-page.js',
72
+ 'public-docs/index.html', 'modules/status-ui/public/index.html', 'modules/status-ui/public/style.css',
73
+ 'modules/ui-design/public/brand.html', 'modules/ui-design/public/brand.css',
74
+ 'modules/hall-ui/public/style.css', 'modules/hall-ui/public/controls.css',
75
+ ];
76
+
77
+ test('no served page source loads a retired face', () => {
78
+ for (const rel of SERVED) {
79
+ if (!fs.existsSync(path.join(ROOT, rel))) continue;
80
+ // read raw: a comment stripper over JS eats template strings (a `/*` in a URL glob),
81
+ // so these files name the retired faces nowhere at all, comments included
82
+ assert.doesNotMatch(read(rel), RETIRED, `${rel} names a retired face`);
83
+ }
84
+ });
85
+
86
+ test('the auth pages carry the pack\'s faces and the shared sheet, and nothing else', () => {
87
+ const links = (clientBranding().theme && clientBranding().theme.fonts && clientBranding().theme.fonts.links) || [];
88
+ const pages = [
89
+ ['sign-in error', authRoutes.renderAuthErrorPage('Sign-in failed', 'The sign-in did not complete.')],
90
+ ['confirming access', authRoutes.renderAccessPendingPage('someone', '/api/bongos/auth/web/start')],
91
+ ['sign-in chooser', signIn.renderSignInChooser({ githubHref: '/g', googleHref: '/o' })],
92
+ ];
93
+ for (const [name, html] of pages) {
94
+ assert.ok(html.includes('<link rel="stylesheet" href="/builders/style.css">'), `${name}: the hall sheet`);
95
+ for (const href of links) assert.ok(html.includes(href.replace(/&/g, '&amp;')), `${name}: the pack's font link ${href}`);
96
+ const own = html.match(/https:\/\/fonts\.googleapis\.com\/css2\?[^"]*/g) || [];
97
+ for (const href of own) assert.ok(links.some((l) => l.replace(/&/g, '&amp;') === href || l === href), `${name}: ${href} is not the pack's`);
98
+ }
99
+ });
100
+
101
+ test('the error page declares no colour of its own', () => {
102
+ for (const status of [404, 500]) {
103
+ const html = errorPageTemplate(status);
104
+ const style = (html.match(/<style>([\s\S]*?)<\/style>/) || [])[1] || '';
105
+ assert.ok(style, 'the page has its layout rules');
106
+ const declarations = style.replace(/--[\w-]+\s*:[^;}]*/g, '');
107
+ assert.doesNotMatch(style, /--(bg|ink|accent|rule)[\w-]*\s*:/, 'no colour token is defined on the page');
108
+ assert.doesNotMatch(declarations, /#[0-9a-f]{3,8}\b|rgba?\(/i, 'no typed-in colour');
109
+ assert.ok(html.includes('<link rel="stylesheet" href="/modules/hall-ui/public/style.css">'), 'the colours come from the shared sheet');
110
+ }
111
+ });