@bongos/core 1.19.482 → 1.19.484

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.
@@ -220,3 +220,202 @@ test('the link a list writes carries WHERE it was opened from — path, query an
220
220
  sandbox.location.hash = '#backlog';
221
221
  assert.equal(api.href('7'), '/task/7?from=%2Fwork%23backlog', 'on the builders host the hall is mounted at /');
222
222
  });
223
+
224
+ // ---- 8. Home matches its board mock (task 1003464) ---------------------------
225
+ //
226
+ // The owner compared live Home against docs/design/mocks/hall/board/v3.html and
227
+ // found four gaps the floors cannot see: the greeting was never built (the page
228
+ // opened cold on a row of numbers, its h1 sr-only), the band ended ragged
229
+ // (`align-items: start` let Standing shrink under Goals), the Standing card had
230
+ // no fact ledger, and the system banners stacked inside it against the
231
+ // placement rule. A floor measures overflow and contrast; none compares a page
232
+ // to the mock it was built from — so these pins do, statically and then by
233
+ // RUNNING hall-render.js against a stub DOM.
234
+
235
+ const indexHtml = read('index.html');
236
+ const hallRenderJs = read('hall-render.js');
237
+ const buildersJs = read('builders.js');
238
+ const goalInboxJs = read('goal-inbox.js');
239
+
240
+ test('the greeting IS the page\'s one h1, in the board\'s label voice, with a sub-line beside it', () => {
241
+ const h1s = indexHtml.match(/<h1\b/g) || [];
242
+ assert.equal(h1s.length, 1, 'one-header-main-h1: the greeting must BECOME the h1, never add a second');
243
+ assert.match(indexHtml, /<div class="page-head page-head--board page-head--hall">\s*<h1 id="hall-h1" class="page-head__h">Builders' Hall<\/h1>\s*<p id="hall-sub" class="page-head__sub" hidden><\/p>/,
244
+ 'the h1 is visible (no sr-only), keeps the id brand-apply.js writes into, and the sub-line ships hidden');
245
+ assert.doesNotMatch(indexHtml, /id="hall-h1" class="sr-only"/, 'the sr-only h1 is gone');
246
+ });
247
+
248
+ test('the band ends level: no align-items on the grid, so the two cards stretch', () => {
249
+ const band = rule(styleCss, '.view-zone--band');
250
+ assert.ok(band, '.view-zone--band has a rule');
251
+ assert.doesNotMatch(band, /align-items/, 'the mock\'s .band declares none — stretch is what makes Standing end where Goals ends');
252
+ assert.match(styleCss.replace(/\/\*[\s\S]*?\*\//g, ''), /@media \(max-width:\s*768px\)[\s\S]*?\.view-zone--band \{ grid-template-columns: 1fr; \}/,
253
+ 'and the single-column fallback still exists below 768px (the task text said 860; the sheet says 768)');
254
+ });
255
+
256
+ test('the Standing card is the mock\'s: an identity line and a three-row fact ledger, no banner', () => {
257
+ const block = hallRenderJs.match(/const homeStanding = document\.getElementById\('home-standing-body'\);[\s\S]*?renderSystemNotices\(/);
258
+ assert.ok(block, 'the Home card render is where it was');
259
+ assert.match(block[0], /class="home-who"/, 'the identity line');
260
+ assert.match(block[0], /<dt>\$\{escapeHtml\(curLabel\(true\)\)\}<\/dt>/,
261
+ 'the currency row takes its label from the branding contract at render time — a data-currency-label attribute is rebranded once at load, before this row exists');
262
+ for (const label of ['On the board', 'Preferred work']) {
263
+ assert.match(block[0], new RegExp(`<dt>${label}</dt>`), `the ledger row: ${label}`);
264
+ }
265
+ assert.match(block[0], /: PREFS_EMPTY\}/, 'the empty-preference copy is the one string both Standing surfaces share');
266
+ assert.doesNotMatch(block[0], /Banner\}/, 'no banner is stacked into the card any more');
267
+ for (const sel of ['.home-who', '.home-facts', '.home-facts dt', '.home-facts dd']) {
268
+ assert.ok(rule(styleCss, sel), `style.css styles ${sel}`);
269
+ }
270
+ assert.match(rule(styleCss, '.home-who__avatar') || '', /var\(--skeleton\)/, 'the avatar placeholder is the skeleton tone, not --bg-deep (pure black in dark)');
271
+ });
272
+
273
+ test('the system notices are inbox LINES, first in the card, and counted by its refresh()', () => {
274
+ const mount = goalInboxJs.match(/host\.innerHTML =([\s\S]*?);/);
275
+ assert.ok(mount, 'the inbox mounts its slices');
276
+ const sys = mount[1].indexOf('id="attention-system"');
277
+ const goals = mount[1].indexOf('id="attention-goals"');
278
+ assert.ok(sys > -1 && sys < goals, '#attention-system is a slice, ahead of the goals slice');
279
+ assert.match(hallRenderJs, /function renderSystemNotices\(/, 'hall-render.js renders them from the /me state it already holds');
280
+ const fn = hallRenderJs.match(/function renderSystemNotices\([\s\S]*?\n \}\n/);
281
+ assert.ok(fn, 'renderSystemNotices is a whole function');
282
+ assert.match(fn[0], /class="ginbox__item ginbox__item--/, 'rows carry .ginbox__item, so OTBAttention.refresh() counts them');
283
+ assert.match(fn[0], /OTBAttention[\s\S]*refresh\(\)/, 'and the renderer asks the card to re-decide its visibility');
284
+ assert.doesNotMatch(fn[0], /covered/, 'a "we cover it" note is information, not an action, and does not land in the inbox');
285
+ // the rebase-owed line carries task refs as links in prose; a 13.5px link
286
+ // with no compensation sits under the 24px floor, and the kit stub renders
287
+ // no notices, so this is pinned in the sheet rather than left to a render
288
+ for (const sel of ['.ginbox__line a', '.ginbox__goal, .triage-inbox__link, .next-up__title']) {
289
+ const r = rule(styleCss, sel) || '';
290
+ assert.match(r, /min-height:\s*24px/, `${sel} clears the floor`);
291
+ assert.match(r, /padding:\s*3px 0/, `${sel} buys it with the row recipe`);
292
+ }
293
+ });
294
+
295
+ test('the greeting reads /me, the public progress and the claimable feed, and builders.js feeds all three', () => {
296
+ assert.match(buildersJs, /getJSON\(`\$\{API\}\/public\/progress`\)/, 'Home fetches the public progress once');
297
+ assert.match(buildersJs, /lastClaimableCount = resp && Array\.isArray\(resp\.claimable\) \? resp\.claimable\.length : null;/,
298
+ 'the claimable feed Home already fetches for Next-up answers the count');
299
+ assert.match(buildersJs, /get lastProgressState\(\)/, 'the renderer reads progress through a getter, so it cannot freeze at boot');
300
+ assert.match(buildersJs, /get lastClaimableCount\(\)/, 'and the count the same way');
301
+ assert.equal((buildersJs.match(/renderGreeting\(\)/g) || []).length, 3, 'rendered after /me, after progress and after the claimable feed — each half as it lands');
302
+ });
303
+
304
+ // Boot hall-render.js against a stub DOM and a ctx of getters (the way
305
+ // builders.js builds it) and read what the greeting and the card actually say.
306
+ function bootHallRender({ me, progress = null, claimable = null, leaderboard = [] }) {
307
+ const els = new Map();
308
+ const makeEl = () => ({
309
+ dataset: {}, style: {}, textContent: '', hidden: false, _html: '', innerHTML: '',
310
+ classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } },
311
+ setAttribute() {}, getAttribute() { return null; }, addEventListener() {},
312
+ querySelector() { return makeEl(); }, querySelectorAll() { return []; }, appendChild() {}, remove() {},
313
+ });
314
+ const documentObj = {
315
+ getElementById(id) { if (!els.has(id)) els.set(id, makeEl()); return els.get(id); },
316
+ querySelector(sel) { return this.getElementById(String(sel).replace(/^#/, '')); },
317
+ querySelectorAll() { return []; },
318
+ createElement() { return makeEl(); },
319
+ addEventListener() {},
320
+ body: { dataset: {} }, documentElement: { dataset: {} },
321
+ };
322
+ const escapeHtml = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
323
+ const windowObj = {
324
+ OTB: { escapeHtml, $: (sel) => documentObj.querySelector(sel), fmtNum: (n) => Number(n || 0).toLocaleString('en-US'), moduleOn: () => false },
325
+ // the factory builds the Standing ships pager + rail at creation
326
+ OTBKit: { makePager: () => ({ page: 1, perPage: 10 }), makeSectionRail: () => ({ render() {}, refresh() {} }), paginate: (rows) => rows, shipVisualThumbHtml: () => '' },
327
+ OTBTaskDetail: { href: (id) => `/task/${id}?from=%2F` },
328
+ location: { hostname: 'builders.example.com', pathname: '/', hash: '', href: 'https://builders.example.com/' },
329
+ localStorage: { getItem() { return null; }, setItem() {}, removeItem() {} },
330
+ addEventListener() {},
331
+ };
332
+ const sandbox = { window: windowObj, document: documentObj, location: windowObj.location, localStorage: windowObj.localStorage, console, setTimeout, clearTimeout, Date, URL };
333
+ sandbox.globalThis = sandbox;
334
+ vm.createContext(sandbox);
335
+ vm.runInContext(read('hall-render.js'), sandbox, { filename: 'hall-render.js' });
336
+ const ctx = {
337
+ API: '/api/gds', RANK_ORDER: ['xenos', 'thetes', 'metic', 'archon'], api: {}, brandWorld: () => 'Cloud Bongos',
338
+ curLabel: (caps) => (caps ? 'Credits' : 'credits'), emptyRow: () => '', fmtDate: (d) => String(d).slice(0, 10),
339
+ mins: (m) => `${m}m`, normalizeRank: (r) => String(r || '').toLowerCase(), pct: (n) => `${n}%`, postJSON: async () => ({}),
340
+ rankLabel: (r) => r, rankWeight: () => 0, signClasses: () => '', toast() {}, usd: (n) => `$${n}`,
341
+ instanceRepo: null, newcomerGated: false,
342
+ get lastMeState() { return me; }, get lastCostState() { return me.cost || null; }, get lastKarmaState() { return me.karma || null; },
343
+ get lastNeedsState() { return me.needs || null; }, get lastRebaseState() { return me.needs_rebase_tasks || null; },
344
+ get lastProgressState() { return progress; }, get lastClaimableCount() { return claimable; },
345
+ };
346
+ const api = windowObj.OTBHallRender(ctx);
347
+ api.lbState.rows = leaderboard;
348
+ return { api, el: (id) => documentObj.getElementById(id) };
349
+ }
350
+
351
+ const ME = {
352
+ builder: { id: 7, display_name: 'Percy Jackson', github_login: 'percy', rank: 'metic', total_credits: 14441, karma: 96, preferred_disciplines: [], created_at: '2026-01-01T00:00:00Z' },
353
+ needs: { items: [{ state: 'action_needed', title: 'Image-generation key', message: 'Your free art coverage has ended.', action: { href: '/settings#art', label: 'Add your key' } }] },
354
+ cost: { level: 'warn', spend_mtd_usd: 80, budget_mtd_usd: 100, pct_used: 80 },
355
+ needs_rebase_tasks: [{ id: 1003460 }],
356
+ };
357
+ const LEADERBOARD = [{ github_login: 'a', total_credits: 20000 }, { github_login: 'percy', total_credits: 14441 }, { github_login: 'b', total_credits: 3 }, { github_login: 'c', total_credits: 1 }];
358
+ const PROGRESS = { progress: [{ version_id: 'BONGOS-V1', status: 'building', total_count: 605, percent_complete: 75 }, { version_id: 'V2', status: 'planning', total_count: 9000, percent_complete: 0 }] };
359
+
360
+ test('RUN: the greeting names the builder, and the sub-line says how far the version is and what is claimable', () => {
361
+ const { api, el } = bootHallRender({ me: ME, progress: PROGRESS, claimable: 5 });
362
+ api.renderGreeting();
363
+ assert.match(el('hall-h1').textContent, /^Good (morning|afternoon|evening), Percy Jackson$/);
364
+ assert.equal(el('hall-sub').textContent, 'BONGOS-V1 is 75 percent shipped. Five tasks are claimable right now.');
365
+ assert.equal(el('hall-sub').hidden, false);
366
+ });
367
+
368
+ test('RUN: each half of the sentence renders as its data lands, and the line hides with neither', () => {
369
+ const none = bootHallRender({ me: ME });
370
+ none.api.renderGreeting();
371
+ assert.equal(none.el('hall-sub').hidden, true, 'nothing to say yet');
372
+ const onlyProgress = bootHallRender({ me: ME, progress: PROGRESS });
373
+ onlyProgress.api.renderGreeting();
374
+ assert.equal(onlyProgress.el('hall-sub').textContent, 'BONGOS-V1 is 75 percent shipped.');
375
+ const zero = bootHallRender({ me: ME, progress: PROGRESS, claimable: 0 });
376
+ zero.api.renderGreeting();
377
+ assert.match(zero.el('hall-sub').textContent, /Nothing is claimable right now\.$/);
378
+ const one = bootHallRender({ me: ME, claimable: 1 });
379
+ one.api.renderGreeting();
380
+ assert.equal(one.el('hall-sub').textContent, 'One task is claimable right now.');
381
+ const many = bootHallRender({ me: ME, claimable: 63 });
382
+ many.api.renderGreeting();
383
+ assert.equal(many.el('hall-sub').textContent, '63 tasks are claimable right now.');
384
+ });
385
+
386
+ test('RUN: the Standing card carries the three facts, the place as "2nd of 4", and no banner', () => {
387
+ const { api, el } = bootHallRender({ me: ME, leaderboard: LEADERBOARD });
388
+ api.renderProfile(ME.builder);
389
+ const html = el('home-standing-body').innerHTML;
390
+ assert.match(html, /<p class="home-who__name">Percy Jackson<\/p>/);
391
+ assert.match(html, /<p class="home-who__handle">@percy<\/p>/);
392
+ // written from curLabel(true), not a data-currency-label attribute: brand-apply
393
+ // rebrands those once at load, and this row is stamped after /me answers
394
+ assert.match(html, /<dt>Credits<\/dt><dd>14,441<\/dd>/);
395
+ assert.match(html, /<dt>On the board<\/dt><dd>2nd of 4<\/dd>/);
396
+ assert.match(html, /<dt>Preferred work<\/dt><dd>Not set yet<\/dd>/, 'the mock\'s copy for an empty preference');
397
+ assert.doesNotMatch(html, /profile__(needs|budget|rebase)/, 'the banners are not in the card');
398
+ const withPrefs = bootHallRender({ me: ME, leaderboard: [] });
399
+ withPrefs.api.renderProfile({ ...ME.builder, preferred_disciplines: ['engineer', 'ui'] });
400
+ assert.match(withPrefs.el('home-standing-body').innerHTML, /<dd>engineer, ui<\/dd>/);
401
+ assert.match(withPrefs.el('home-standing-body').innerHTML, /<dt>On the board<\/dt><dd>—<\/dd>/, 'no roll yet: a dash, not a wrong place');
402
+ });
403
+
404
+ test('RUN: the three notices are inbox lines with a kind, a line and — where one exists — a door', () => {
405
+ const { api, el } = bootHallRender({ me: ME });
406
+ api.renderProfile(ME.builder);
407
+ const html = el('attention-system').innerHTML;
408
+ assert.match(html, /<h3 class="ginbox__sec-h">From the system<\/h3>/);
409
+ assert.equal((html.match(/class="ginbox__item /g) || []).length, 3, 'rebase, the needed input, the budget');
410
+ assert.match(html, /ginbox__item--danger"><div class="ginbox__body"><span class="ginbox__kind">Rebase owed<\/span>/);
411
+ assert.match(html, /<a href="\/task\/1003460\?from=%2F">task 1003460<\/a>/, 'a task ref is a door through OTBTaskDetail.href, never a bare #N');
412
+ assert.match(html, /ginbox__item--warn"><div class="ginbox__body"><span class="ginbox__kind">Image-generation key<\/span>/);
413
+ assert.match(html, /<a class="btn-ghost" href="\/settings#art">Add your key<\/a>/);
414
+ assert.match(html, /<span class="ginbox__kind">Approaching monthly budget<\/span>/);
415
+ const quiet = bootHallRender({ me: { builder: ME.builder } });
416
+ quiet.api.renderProfile(ME.builder);
417
+ assert.equal(quiet.el('attention-system').innerHTML, '', 'nothing owed, nothing rendered');
418
+ const covered = bootHallRender({ me: { builder: ME.builder, needs: { items: [{ state: 'covered', title: 'Art', message: 'We cover it.' }] } } });
419
+ covered.api.renderProfile(ME.builder);
420
+ assert.equal(covered.el('attention-system').innerHTML, '', 'a covered need is not an action');
421
+ });