@bongos/core 1.19.780 → 1.19.782

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.
@@ -167,6 +167,125 @@ test('context links to the task or goal, and to neither when absent', () => {
167
167
  assert.equal(L.contextOf({}), null, 'no context means no element, not an empty one');
168
168
  });
169
169
 
170
+ // ── A ROOM OF PEOPLE (task 1003942, owner report) ───────────────────────────
171
+ //
172
+ // "Way too text heavy, and doesn't feel collaborative at all." The page now
173
+ // leads with faces and figures; the rules that decide WHOSE face and WHAT number
174
+ // are pure in collab-lib.js, so they run here rather than being grepped.
175
+
176
+ test('an incoming row leads with the ASKER, an outgoing one with who you asked', () => {
177
+ const L = lib();
178
+ const inc = L.faceOf({ requested_by: 92, requested_by_name: 'Preview Gamma', requested_by_avatar_url: 'https://x/a.png' }, 'in');
179
+ assert.equal(inc.kind, 'person');
180
+ assert.equal(inc.id, 92);
181
+ assert.equal(inc.name, 'Preview Gamma');
182
+ assert.equal(inc.avatar, 'https://x/a.png');
183
+ const out = L.faceOf({ needs_builder_id: 90, needs_builder_name: 'Preview Alpha', needs_builder_avatar_url: 'https://x/b.png' }, 'out');
184
+ assert.equal(out.id, 90);
185
+ assert.equal(out.name, 'Preview Alpha');
186
+ assert.equal(out.avatar, 'https://x/b.png');
187
+ });
188
+
189
+ test('a craft ask wears the craft\'s letter, never a person\'s face', () => {
190
+ const L = lib();
191
+ const f = L.faceOf({ needs_craft: 'artist' }, 'out');
192
+ assert.equal(f.kind, 'craft');
193
+ assert.equal(f.initial, 'A');
194
+ assert.equal(f.avatar, null);
195
+ assert.equal(f.name, 'artists');
196
+ const css = read('modules', 'hall-ui', 'public', 'collab.css');
197
+ assert.match(css, /\.cb-face--craft\s*\{[^}]*font-family: var\(--label-face\)/, 'drawn in the label face, so it cannot pass for a person');
198
+ });
199
+
200
+ test('no picture means the INITIAL — and a src the page did not vouch for is dropped', () => {
201
+ const L = lib();
202
+ assert.equal(L.initialOf('preview-gamma'), 'P');
203
+ assert.equal(L.initialOf('@lyra'), 'L', 'a leading @ is not a letter');
204
+ assert.equal(L.initialOf(''), '?', 'never empty');
205
+ assert.equal(L.httpUrl('javascript:alert(1)'), null, 'roster.js\'s allowlist rule, kept');
206
+ assert.equal(L.httpUrl('https://avatars.githubusercontent.com/u/1?v=4'), 'https://avatars.githubusercontent.com/u/1?v=4');
207
+ const f = L.faceOf({ requested_by: 5, requested_by_login: 'gamma', requested_by_avatar_url: 'data:image/png;base64,AAAA' }, 'in');
208
+ assert.equal(f.avatar, null);
209
+ assert.equal(f.initial, 'G');
210
+ });
211
+
212
+ test('the face stack shows each PERSON once, however many times they asked', () => {
213
+ const L = lib();
214
+ const rows = [
215
+ { requested_by: 92, requested_by_name: 'Gamma' },
216
+ { requested_by: 90, requested_by_name: 'Alpha' },
217
+ { requested_by: 92, requested_by_name: 'Gamma' },
218
+ ];
219
+ assert.deepEqual(L.askers(rows).map((f) => f.name), ['Gamma', 'Alpha']);
220
+ });
221
+
222
+ test('THREE BLOCKS: the newest three show, the rest wait behind one door', () => {
223
+ // The owner's direction: asked of you left, your asks right, every open ask
224
+ // below, each condensed to the three most recent and expandable. The split
225
+ // and the order are pure, so the button's number and the rows it reveals
226
+ // come from one place.
227
+ const L = lib();
228
+ const rows = [{ id: 1, created_at: daysAgo(9) }, { id: 2, created_at: daysAgo(0) }, { id: 3, created_at: daysAgo(4) }, { id: 4, created_at: daysAgo(1) }];
229
+ const newest = rows.slice().sort(L.byNewestFirst).map((r) => r.id);
230
+ assert.deepEqual(newest, [2, 4, 3, 1], 'newest first is the display order');
231
+ const { shown, hidden } = L.splitShown(rows.slice().sort(L.byNewestFirst), 3);
232
+ assert.deepEqual(shown.map((r) => r.id), [2, 4, 3]);
233
+ assert.deepEqual(hidden.map((r) => r.id), [1], 'the fourth waits behind "show all"');
234
+ assert.equal(L.splitShown(rows.slice(0, 2), 3).hidden.length, 0, 'two rows need no door');
235
+ // The ageing band still marks every row whatever the order — the criterion
236
+ // is met by the pill, not by the sort.
237
+ assert.equal(L.ageBand(rows[0].created_at, NOW).id, 'stale');
238
+ });
239
+
240
+ test('the board of everyone\'s asks speaks in the third person', () => {
241
+ const L = lib();
242
+ assert.equal(L.boardLine({ requested_by_name: 'Alpha', needs_builder_id: 9, needs_builder_name: 'Beta' }), 'Alpha asked Beta by name');
243
+ assert.equal(L.boardLine({ requested_by_name: 'Gamma', needs_craft: 'builder' }), 'Gamma asked builders');
244
+ assert.equal(L.boardLine({ requested_by: 5, needs_builder_id: 9 }), 'builder 5 asked one person', 'an older server still names both ends');
245
+ assert.ok(!/\byou\b/i.test(L.boardLine({ requested_by_name: 'Alpha', needs_builder_id: 9, needs_builder_name: 'Beta' })), 'never "you" on the board');
246
+ });
247
+
248
+ test('the page is the three blocks, led by faces, with the ledes gone', () => {
249
+ const js = read('modules', 'hall-ui', 'public', 'collab.js');
250
+ const html = read('modules', 'hall-ui', 'public', 'collab.html');
251
+ assert.match(js, /L\.faceOf\(req, 'in'\)/, 'the incoming row asks the lib whose face');
252
+ assert.match(js, /L\.faceOf\(req, 'out'\)/, 'and so does the outgoing one');
253
+ assert.match(js, /paintBlock\('for-me'/); assert.match(js, /paintBlock\('mine'/); assert.match(js, /paintBlock\('all'/);
254
+ assert.match(js, /L\.splitShown\(list, SHOWN\)/, 'the condensing is the lib\'s split');
255
+ assert.match(js, /const SHOWN = 3;/, 'three, as asked');
256
+ assert.match(js, /api\.request\('GET', '\/help-requests'\)/, 'the third block reads the whole board');
257
+ assert.match(html, /class="view-zone view-zone--band cb-band"/, 'the top two ride the shell\'s two-up');
258
+ assert.match(html, /id="cb-all"/, 'and the board is the wide block under them');
259
+ for (const k of ['for-me', 'mine', 'all']) assert.match(html, new RegExp(`id="cb-${k}-more"[^>]*aria-expanded="false"`), `${k} has its door`);
260
+ // Prose budget: no block opens with a lede except the social one, which
261
+ // keeps ONE line; the rules sentence is said once, at the foot.
262
+ assert.equal((html.match(/class="scroll__lede"/g) || []).length, 1, 'one lede on the whole page');
263
+ assert.equal((html.match(/declining costs nothing/g) || []).length, 1, 'the rule is said once');
264
+ assert.match(html, /class="cb-foot"/, 'and it is the foot, not the opener');
265
+ });
266
+
267
+ test('the by-name ask is the one LIT thing — the accent goes on the personal call', () => {
268
+ const js = read('modules', 'hall-ui', 'public', 'collab.js');
269
+ const css = read('modules', 'hall-ui', 'public', 'collab.css');
270
+ assert.match(js, /req\.needs_builder_id != null \? ' cb-row--live' : ''/, 'only a by-name ask takes the class');
271
+ assert.match(css, /\.cb-row--live \.ov-row__facts > \.ov-fact \{ color: var\(--accent-ink\); \}/);
272
+ assert.ok(!/var\(--accent\)/.test(css), 'nothing in the sheet spends the accent FILL');
273
+ });
274
+
275
+ test('every colour in the sheet is a token — no literal, both modes for free', () => {
276
+ const css = read('modules', 'hall-ui', 'public', 'collab.css').replace(/\/\*[\s\S]*?\*\//g, '');
277
+ assert.deepEqual(css.match(/#[0-9a-f]{3,8}\b|rgba?\(/gi) || [], [], 'the Fifteen Rule');
278
+ });
279
+
280
+ test('the SIDEBAR badge: the Collab item declares the help_requests count', () => {
281
+ const shell = read('modules', 'hall-ui', 'public', 'shell.js');
282
+ assert.match(shell, /id: 'collab',[^}]*count: 'help_requests'/, 'the same field the Board Room item carries');
283
+ const needsSrc = read('modules', 'builder-settings', 'builder-needs.js');
284
+ const fn = needsSrc.slice(needsSrc.indexOf('function helpRequestsNeed'), needsSrc.indexOf('function taskRecommendationsNeed'));
285
+ assert.match(fn, /count: n,/, 'and the need states its number for the badge to read');
286
+ assert.match(fn, /href: '\/collab'/, 'pointing at the page, not the retired hash view');
287
+ });
288
+
170
289
  // ---- THE ANSWERABLE CLAUSE ------------------------------------------------
171
290
  test('the asker can settle in place, with both outcomes', () => {
172
291
  const js = read('modules', 'hall-ui', 'public', 'collab.js');
@@ -66,7 +66,8 @@ test('every list is the family\'s row, and the card-in-a-card is gone', () => {
66
66
  for (const old of ['cd-card', 'cd-stat', 'cd-tag', 'cd-filters__select', 'cd-browse', 'cd-empty']) {
67
67
  assert.ok(gone(read('copy-desk.js'), old) && gone(read('copy-desk.css'), `\\.${old}`), `copy desk no longer emits ${old}`);
68
68
  }
69
- for (const p of PAGES) assert.match(read(`${p}.html`), /class="empty-state"/, `${p}.html empties with the kit's own state`);
69
+ // The kit's class, whole or with a page modifier beside it (collab's .cb-empty).
70
+ for (const p of PAGES) assert.match(read(`${p}.html`), /class="empty-state( [a-z-]+)*"/, `${p}.html empties with the kit's own state`);
70
71
  });
71
72
 
72
73
  // ---- collab: the facts the audit found thrown away ---------------------------
@@ -135,9 +136,12 @@ test('"asked of you" reads in runs — by name first, then each craft', () => {
135
136
  // task 1003274: the naming moved to directionLine, which also says WHICH WAY
136
137
  // the ask points — the half the owner's report was actually about.
137
138
  assert.match(L.directionLine({ needs_builder_id: 7, needs_builder_name: 'Preview Alpha' }, 'out'), /Preview Alpha by name/, 'a named addressee is named');
139
+ // task 1003942 (owner direction): the block is CONDENSED to the newest three,
140
+ // so the runs and their group heads are gone from the page; the personal ask
141
+ // is still told apart, as the lit row rather than a run of its own.
138
142
  const js = read('collab.js');
139
- assert.match(js, /ov-group__head/, 'each run has the family\'s group head');
140
- assert.match(js, /Number\(b\.id === 'you'\) - Number\(a\.id === 'you'\)/, 'and the personal run sorts first');
143
+ assert.match(js, /req\.needs_builder_id != null \? ' cb-row--live' : ''/, 'a by-name ask is the lit row');
144
+ assert.match(js, /sort\(L\.byNewestFirst\)/, 'the block reads newest first');
141
145
  });
142
146
 
143
147
  test('the age is a state pill in the row\'s right column, and every band has its own colour', () => {
@@ -562,6 +562,53 @@ test('a big count is capped rather than allowed to stretch the rail', () => {
562
562
  assert.equal(b.navBadge('board-room').text, '99+');
563
563
  });
564
564
 
565
+ // ── the SECOND consumer: open help requests on the Collab item (task 1003942) ──
566
+ //
567
+ // The owner's ask: a notification badge on the Collab link like the Board Room's.
568
+ // Exactly the adoption the mechanism above promised — one field on the nav item,
569
+ // one field on the need — so these pin that it landed and inherits every rule.
570
+
571
+ const helpNeeds = (count, extra = {}) => ({
572
+ items: [{
573
+ id: 'help_requests', state: 'action_needed', count,
574
+ title: 'People asked for your help', ...extra,
575
+ }],
576
+ });
577
+ const signedIn = { authed: true, rank: 'xenos', permissions: {} };
578
+
579
+ test('COLLAB: open help requests put a count on the Collab item', () => {
580
+ const b = boot();
581
+ b.shell.applyAccess({ ...signedIn, needs: helpNeeds(4) });
582
+ assert.deepEqual(b.navBadge('collab'), { text: '4', label: 'People asked for your help' });
583
+ });
584
+
585
+ test('COLLAB: zero, no count, or a covered need is silent', () => {
586
+ for (const needs of [helpNeeds(0), { items: [{ id: 'help_requests', state: 'action_needed' }] }, helpNeeds(3, { state: 'covered' })]) {
587
+ const b = boot();
588
+ b.shell.applyAccess({ ...signedIn, needs });
589
+ assert.equal(b.navBadge('collab'), null);
590
+ }
591
+ });
592
+
593
+ test('COLLAB: the badge never out-runs the gate — signed out, no count', () => {
594
+ const b = boot();
595
+ b.shell.applyAccess({ ...signedIn, needs: helpNeeds(2) });
596
+ assert.ok(b.navBadge('collab'), 'precondition: it showed');
597
+ b.shell.applyAccess({ authed: false, needs: helpNeeds(2) });
598
+ assert.equal(b.navHidden('collab'), true, 'precondition: Collab needs a session');
599
+ assert.equal(b.navBadge('collab'), null, 'a hidden item carries no count');
600
+ });
601
+
602
+ test('COLLAB and the Board Room each read their OWN need — the counts do not bleed', () => {
603
+ const b = boot();
604
+ b.shell.applyAccess({
605
+ authed: true, rank: 'xenos', permissions: { 'board.vote.cast': true },
606
+ needs: { items: [...helpNeeds(4).items, ...boardNeeds(2).items] },
607
+ });
608
+ assert.equal(b.navBadge('collab').text, '4');
609
+ assert.equal(b.navBadge('board-room').text, '2');
610
+ });
611
+
565
612
  test('the count reaches a screen reader as WORDS, not a floating numeral', () => {
566
613
  // The numeral is aria-hidden decoration; the need's own sentence rides beside
567
614
  // it in an sr-only span. "3" alone inside a link named Board Room says nothing.
@@ -231,6 +231,37 @@ test('both reads are capped', () => {
231
231
  }
232
232
  });
233
233
 
234
+ // ── the BOARD read (task 1003942, owner direction) ─────────────────────────
235
+ //
236
+ // The Collab page's third block is every open ask in the project. A fourth
237
+ // read beside the addressed one and the asker's own, and the one that could
238
+ // reopen C01's privacy hole if it took a craft or a builder from the caller —
239
+ // so these pin that it takes nothing, stays open-only, and is capped like the
240
+ // other two.
241
+ test('the board read lists every OPEN ask, newest first, and takes nothing from the caller', async () => {
242
+ const calls = [];
243
+ const pool = { query: async (sql, params) => { calls.push({ sql, params }); return { rows: [{ id: 1 }] }; } };
244
+ const rows = await help.listOpenAll({}, { pool });
245
+ assert.deepEqual(rows, [{ id: 1 }]);
246
+ assert.equal(calls.length, 1);
247
+ assert.match(calls[0].sql, /WHERE h\.status = 'open'/, 'open only — a settled ask leaves the board');
248
+ assert.match(calls[0].sql, /ORDER BY h\.created_at DESC/, 'newest first is the board\'s order');
249
+ assert.ok(!/needs_craft\s*=|requested_by\s*=|needs_builder_id\s*=/.test(calls[0].sql), 'no craft or builder narrows it — nothing for a ?craft= hole to widen');
250
+ assert.deepEqual(calls[0].params, [help.MAX_ROWS], 'capped, and the cap is the only parameter');
251
+ assert.match(calls[0].sql, /requested_by_avatar_url/, 'it carries the faces the page leads with');
252
+ });
253
+
254
+ test('the board route is signed-in only and reads no query', () => {
255
+ const routes = read('modules', 'lifecycle', 'routes', 'help-requests.js');
256
+ const idx = routes.indexOf("'GET /help-requests'");
257
+ assert.ok(idx > 0, 'the route exists');
258
+ const line = routes.slice(routes.lastIndexOf('router.get', idx), idx);
259
+ assert.match(line, /api\.requireBuilder/, 'any builder, no stranger');
260
+ const body = routes.slice(idx, routes.indexOf('errorCode', idx));
261
+ assert.ok(!/req\.query|req\.body/.test(body), 'nothing from the caller reaches the read');
262
+ assert.match(body, /help\.listOpenAll\(\)/);
263
+ });
264
+
234
265
  test('core reaches the count through the PORT, never by importing lifecycle', () => {
235
266
  const me = read('src', 'bongos', 'routes', 'me.js');
236
267
  assert.match(me, /openHelpRequestsFor/);
@@ -120,7 +120,9 @@ test('HOME changes: an introduction becomes a need in the roster', () => {
120
120
  const row = roster.find((n) => n.id === 'mingle_introductions');
121
121
  assert.ok(row, 'the need is produced');
122
122
  assert.equal(row.state, 'action_needed');
123
- assert.match(row.action.href, /\/#\/collab/, 'it points at an EXISTING surface, not a new one');
123
+ // Collab has been its own page since task 1003172; the '/#/collab' hash form
124
+ // this used to pin pointed at a view the index no longer routes (task 1003942).
125
+ assert.match(row.action.href, /^\/collab$/, 'it points at an EXISTING surface, not a new one');
124
126
  });
125
127
 
126
128
  test('no introductions means no row — the roster is not padded', () => {