@bongos/core 1.19.474 → 1.19.476
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 +56 -36
- package/docs/adr/0229-the-title-is-the-door-and-the-record-exits-to-where-it-was-opened-from.md +38 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +255 -258
- package/docs/copy-registry.json +376 -414
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/builders.js +13 -64
- package/modules/hall-ui/public/dom-utils.js +2 -1
- package/modules/hall-ui/public/goals-page.js +4 -3
- package/modules/hall-ui/public/goals-render.js +5 -3
- package/modules/hall-ui/public/hall-render.js +10 -142
- package/modules/hall-ui/public/hall-widgets.js +6 -16
- package/modules/hall-ui/public/index.html +0 -3
- package/modules/hall-ui/public/index.states.json +3 -2
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/roster.html +63 -0
- package/modules/hall-ui/public/roster.js +227 -0
- package/modules/hall-ui/public/roster.states.json +100 -0
- package/modules/hall-ui/public/shell.js +28 -4
- package/modules/hall-ui/public/style.css +20 -18
- package/modules/hall-ui/public/task-detail.js +16 -4
- package/modules/hall-ui/public/task.html +5 -3
- package/modules/hall-ui/public/task.js +58 -4
- package/modules/hall-ui/public/task.states.json +1 -0
- package/modules/hall-ui/public/work.js +10 -11
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/hall_board_world.mjs +69 -0
- package/tests/hall_nav.mjs +36 -1
- package/tests/hall_page_gate_map.mjs +8 -0
- package/tests/hall_record_world.mjs +83 -4
- package/tests/rank_tier_single_source.mjs +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.476",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -34,7 +34,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
34
34
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
35
35
|
// the entry to that file. Look for a version's history there, not here.
|
|
36
36
|
// ---------------------------------------------------------------------------
|
|
37
|
-
const CORE_VERSION = '1.19.
|
|
37
|
+
const CORE_VERSION = '1.19.476'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
38
38
|
|
|
39
39
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
40
40
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -34,6 +34,7 @@ import { test } from 'node:test';
|
|
|
34
34
|
import assert from 'node:assert/strict';
|
|
35
35
|
import fs from 'node:fs';
|
|
36
36
|
import path from 'node:path';
|
|
37
|
+
import vm from 'node:vm';
|
|
37
38
|
import { fileURLToPath } from 'node:url';
|
|
38
39
|
|
|
39
40
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -151,3 +152,71 @@ test('both re-skinned pages have a states file, so the floors are reproducible',
|
|
|
151
152
|
assert.ok(Object.keys(json.states).length >= 4, `${f} renders more than the default state`);
|
|
152
153
|
}
|
|
153
154
|
});
|
|
155
|
+
|
|
156
|
+
// ---- 7. the way INTO a record is the title ----------------------------------
|
|
157
|
+
//
|
|
158
|
+
// task 1003468. Since 1759/1869 the card body toggles selection (bulk claim is
|
|
159
|
+
// the primary card interaction) and the only door into the record was a 26px ⤢
|
|
160
|
+
// glyph in --ink-faint — one the owner never found. The title is now a real
|
|
161
|
+
// link on both surfaces that render the card, the board and the goal pane, and
|
|
162
|
+
// the glyph is gone. A title that becomes a control owes the 24px floor on both
|
|
163
|
+
// axes, and the card's line must not re-space to pay for it. The link also
|
|
164
|
+
// carries WHERE it was opened from, which is the record's way back (§10 of
|
|
165
|
+
// tests/hall_record_world.mjs).
|
|
166
|
+
|
|
167
|
+
const cardWorkJs = read('work.js');
|
|
168
|
+
const cardGoalsRenderJs = read('goals-render.js');
|
|
169
|
+
const cardGoalsPageJs = read('goals-page.js');
|
|
170
|
+
const cardTaskDetailJs = read('task-detail.js');
|
|
171
|
+
|
|
172
|
+
test('the task title is a link on the board and on the goal pane, and the ⤢ glyph is gone', () => {
|
|
173
|
+
for (const [name, src] of [['work.js', cardWorkJs], ['goals-render.js', cardGoalsRenderJs]]) {
|
|
174
|
+
assert.match(src, /<p class="task__title"><a class="task__link" href="\$\{escapeHtml\(taskHref\(/,
|
|
175
|
+
`${name} renders the title as the door`);
|
|
176
|
+
assert.doesNotMatch(src, /task__open/, `${name} still renders the glyph`);
|
|
177
|
+
}
|
|
178
|
+
assert.doesNotMatch(cardGoalsPageJs, /task__open/, 'goals-page.js still listens for the glyph');
|
|
179
|
+
assert.doesNotMatch(styleCss.replace(/\/\*[\s\S]*?\*\//g, ''), /\.task__open/, 'style.css still styles a glyph nothing renders');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('both click handlers let the title link through instead of toggling selection under it', () => {
|
|
183
|
+
for (const [name, src] of [['work.js', cardWorkJs], ['goals-page.js', cardGoalsPageJs]]) {
|
|
184
|
+
assert.match(src, /if \(e\.target\.closest\('\.task__link'\)\) return;/, `${name} returns before the card-body toggle`);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('.task__link buys the 24px floor on both axes and hands the room back', () => {
|
|
189
|
+
const link = rule(styleCss, '.task__link');
|
|
190
|
+
assert.ok(link, '.task__link has a rule');
|
|
191
|
+
assert.match(link, /display:\s*inline-block/, 'inline-block, so min-height applies to a run of text');
|
|
192
|
+
assert.match(link, /min-width:\s*24px/, 'width is half the floor');
|
|
193
|
+
assert.match(link, /min-height:\s*24px/, 'height is the other half');
|
|
194
|
+
const pad = /padding:\s*(\d+)px\s+(\d+)px/.exec(link);
|
|
195
|
+
const mar = /margin:\s*-(\d+)px\s+-(\d+)px/.exec(link);
|
|
196
|
+
assert.ok(pad && mar, 'padding and a negative margin are both declared');
|
|
197
|
+
assert.deepEqual([mar[1], mar[2]], [pad[1], pad[2]], 'the negative margin equals the padding, so the card does not re-space');
|
|
198
|
+
assert.match(link, /color:\s*inherit/, 'ink at rest — sixty accent titles would take the board over');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('the link a list writes carries WHERE it was opened from — path, query and hash', () => {
|
|
202
|
+
const sandbox = {
|
|
203
|
+
window: { BongosClient: { createClient: () => ({}) }, addEventListener() {}, OTB: {} },
|
|
204
|
+
document: { readyState: 'complete', addEventListener() {}, getElementById() { return null; }, body: {} },
|
|
205
|
+
location: { hostname: 'cloudbongos.com', pathname: '/builders/goals', search: '', hash: '#/goal/1000081', href: '' },
|
|
206
|
+
sessionStorage: { getItem() { return null; }, setItem() {}, removeItem() {} },
|
|
207
|
+
console, setTimeout, clearTimeout, Date,
|
|
208
|
+
};
|
|
209
|
+
sandbox.globalThis = sandbox;
|
|
210
|
+
sandbox.window.location = sandbox.location;
|
|
211
|
+
vm.createContext(sandbox);
|
|
212
|
+
vm.runInContext(cardTaskDetailJs, sandbox, { filename: 'task-detail.js' });
|
|
213
|
+
const api = sandbox.window.OTBTaskDetail;
|
|
214
|
+
assert.equal(api.href('1003468'), '/builders/task/1003468?from=%2Fbuilders%2Fgoals%23%2Fgoal%2F1000081',
|
|
215
|
+
'the goal\'s #/goal/N rides the query, where a reload and a copied link keep it');
|
|
216
|
+
api.open('1003468');
|
|
217
|
+
assert.equal(sandbox.location.href, api.href('1003468'), 'open() navigates to the same link the title carries');
|
|
218
|
+
sandbox.location.hostname = 'builders.cloudbongos.com';
|
|
219
|
+
sandbox.location.pathname = '/work';
|
|
220
|
+
sandbox.location.hash = '#backlog';
|
|
221
|
+
assert.equal(api.href('7'), '/task/7?from=%2Fwork%23backlog', 'on the builders host the hall is mounted at /');
|
|
222
|
+
});
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -42,11 +42,17 @@ function boot({ page = 'settings', hash = '', pathname = '/settings' } = {}) {
|
|
|
42
42
|
return el;
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
// replace() is recorded, not ignored: task 1003441 retired the #leaderboard
|
|
46
|
+
// VIEW and route() answers that hash with location.replace('/roster'). A stub
|
|
47
|
+
// without it throws the moment a boot carries that hash, which is precisely
|
|
48
|
+
// why the redirect shipped untested the first time.
|
|
49
|
+
const replaced = [];
|
|
45
50
|
const location = {
|
|
46
51
|
hostname: 'builders.cloudbongos.com',
|
|
47
52
|
origin: 'https://builders.cloudbongos.com',
|
|
48
53
|
pathname, hash,
|
|
49
54
|
assign() {},
|
|
55
|
+
replace(target) { replaced.push(String(target)); },
|
|
50
56
|
};
|
|
51
57
|
|
|
52
58
|
const documentObj = {
|
|
@@ -90,6 +96,7 @@ function boot({ page = 'settings', hash = '', pathname = '/settings' } = {}) {
|
|
|
90
96
|
sidebarHtml: documentObj.getElementById('app-sidebar').innerHTML,
|
|
91
97
|
viewHidden: (v) => documentObj.getElementById('view-' + v).hidden,
|
|
92
98
|
shell: windowObj.OTBShell,
|
|
99
|
+
replaced,
|
|
93
100
|
};
|
|
94
101
|
}
|
|
95
102
|
|
|
@@ -155,7 +162,11 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
155
162
|
// First within the group because it is the one page here you ACT on; the
|
|
156
163
|
// leaderboard and the directory are browsing surfaces. Updating this oracle is
|
|
157
164
|
// the reviewed decision the pin exists to force.
|
|
158
|
-
|
|
165
|
+
// task 1003441: `leaderboard` → `roster`. The id changed because the THING
|
|
166
|
+
// changed: it was a hash view on index.html (href /#leaderboard, view:
|
|
167
|
+
// 'leaderboard') and is now a real page at /roster labelled Builders. That is
|
|
168
|
+
// what made this group read as one real page plus a link into the home page.
|
|
169
|
+
assert.deepEqual(nav[2].items.map((i) => i.id), ['collab', 'roster', 'people'], 'Community = everyone else');
|
|
159
170
|
assert.deepEqual(nav[3].items.map((i) => i.id),
|
|
160
171
|
['primer', 'ranks', 'economy', 'diagrams', 'atlas', 'modules', 'apidocs'], 'Docs = reference material');
|
|
161
172
|
// 'government' (the Permissions tab, BV1.R113) is the group's one archon-gated item.
|
|
@@ -194,6 +205,30 @@ test('group-gate check actually CATCHES a head gated wider than its items', () =
|
|
|
194
205
|
assert.equal(headOutOfRange({ label: 'X', headGate: 'archon', items: [{ gate: 'archon' }] }), false);
|
|
195
206
|
});
|
|
196
207
|
|
|
208
|
+
test('#leaderboard REDIRECTS to /roster from the index, and only from the index', () => {
|
|
209
|
+
// task 1003441: the Roll became a page, so its hash has no host view to
|
|
210
|
+
// alias onto. Without the redirect viewFromHash returns nothing and route()
|
|
211
|
+
// leaves the reader on Home — a dead link that looks like it worked, which
|
|
212
|
+
// is the failure this pins. The nav item points at the page, not the hash.
|
|
213
|
+
const ids = parseNav(boot().sidebarHtml).flatMap((g) => g.items.map((i) => i.id));
|
|
214
|
+
assert.ok(!ids.includes('leaderboard'), 'the hash-view nav entry retired with the view');
|
|
215
|
+
assert.ok(ids.includes('roster'), 'the page took its place');
|
|
216
|
+
|
|
217
|
+
const onIndex = boot({ page: 'home', hash: '#leaderboard', pathname: '/' });
|
|
218
|
+
assert.deepEqual(onIndex.replaced, ['/roster'],
|
|
219
|
+
'the index answers the retired hash with a replace() to the page');
|
|
220
|
+
|
|
221
|
+
// The guard matters: the hash is not this shell’s to interpret on a page
|
|
222
|
+
// that has no views, and hijacking it there would break an in-page anchor
|
|
223
|
+
// that happens to be spelled the same.
|
|
224
|
+
const offIndex = boot({ page: 'settings', hash: '#leaderboard', pathname: '/settings' });
|
|
225
|
+
assert.deepEqual(offIndex.replaced, [], 'a non-index page redirects nothing');
|
|
226
|
+
|
|
227
|
+
// And a hash the map does not name is still left alone entirely.
|
|
228
|
+
const unknown = boot({ page: 'home', hash: '#nope', pathname: '/' });
|
|
229
|
+
assert.deepEqual(unknown.replaced, [], 'an unknown hash is not a redirect');
|
|
230
|
+
});
|
|
231
|
+
|
|
197
232
|
test('Achievements has no nav entry, but #achievements still routes to Standing', () => {
|
|
198
233
|
const ids = parseNav(boot().sidebarHtml).flatMap((g) => g.items.map((i) => i.id));
|
|
199
234
|
assert.ok(!ids.includes('achievements'), 'the nav entry retired with task 2175');
|
|
@@ -79,6 +79,14 @@ const ORACLE = {
|
|
|
79
79
|
|
|
80
80
|
// Deliberately UNGATED — the landing card, the rank ladder, the reward
|
|
81
81
|
// schedule and a builder's public portfolio all render for a stranger.
|
|
82
|
+
//
|
|
83
|
+
// /roster joins them (task 1003441): it is this project’s own ledger, and its
|
|
84
|
+
// one data source — GET /leaderboard — is deliberately unauthenticated,
|
|
85
|
+
// mirroring the public status surface. The page must not be gated tighter than
|
|
86
|
+
// the API it fronts, which is the same rule that puts /people at
|
|
87
|
+
// project.curate. It was reachable to a stranger as the #leaderboard view on
|
|
88
|
+
// the ungated index, so this preserves the audience rather than widening it.
|
|
89
|
+
'/builders/roster': null,
|
|
82
90
|
'/builders/': null,
|
|
83
91
|
'/builders/ranks': null,
|
|
84
92
|
'/builders/drachmae': null,
|
|
@@ -259,7 +259,7 @@ test('the preview harness can reach both record pages', () => {
|
|
|
259
259
|
// re-implementation of it.
|
|
260
260
|
const TASK_SRC = read('task.js');
|
|
261
261
|
|
|
262
|
-
function renderTask(task, { lastClaim = null, viewer = null, goal = null, status = 200 } = {}) {
|
|
262
|
+
function renderTask(task, { lastClaim = null, viewer = null, goal = null, status = 200, search = '', referrer = '' } = {}) {
|
|
263
263
|
const els = new Map();
|
|
264
264
|
const makeEl = () => ({
|
|
265
265
|
dataset: {}, style: {}, textContent: '', hidden: false, disabled: false, _html: '',
|
|
@@ -273,6 +273,7 @@ function renderTask(task, { lastClaim = null, viewer = null, goal = null, status
|
|
|
273
273
|
let onReady = null;
|
|
274
274
|
const documentObj = {
|
|
275
275
|
title: 'Task — Cloud Bongos',
|
|
276
|
+
referrer,
|
|
276
277
|
body: { dataset: { page: 'task' } },
|
|
277
278
|
documentElement: { dataset: {}, style: {}, classList: { add() {}, remove() {} } },
|
|
278
279
|
getElementById(id) { if (!els.has(id)) els.set(id, makeEl()); return els.get(id); },
|
|
@@ -302,17 +303,22 @@ function renderTask(task, { lastClaim = null, viewer = null, goal = null, status
|
|
|
302
303
|
categoryChip: () => '',
|
|
303
304
|
toast() {},
|
|
304
305
|
},
|
|
305
|
-
location: { hostname: 'builders.cloudbongos.com', pathname: `/task/${task && task.id ? task.id : '1'}`, origin: 'https://builders.cloudbongos.com', assign() {} },
|
|
306
|
+
location: { hostname: 'builders.cloudbongos.com', pathname: `/task/${task && task.id ? task.id : '1'}`, search, origin: 'https://builders.cloudbongos.com', assign() {} },
|
|
306
307
|
addEventListener() {},
|
|
307
308
|
confirm: () => true,
|
|
308
309
|
navigator: { clipboard: { writeText: async () => {} } },
|
|
309
310
|
};
|
|
310
|
-
const sandbox = { window: windowObj, document: documentObj, location: windowObj.location, console, setTimeout, clearTimeout, Date };
|
|
311
|
+
const sandbox = { window: windowObj, document: documentObj, location: windowObj.location, console, setTimeout, clearTimeout, Date, URL, URLSearchParams };
|
|
311
312
|
sandbox.globalThis = sandbox;
|
|
312
313
|
vm.createContext(sandbox);
|
|
313
314
|
vm.runInContext(TASK_SRC, sandbox, { filename: 'task.js' });
|
|
314
315
|
assert.ok(onReady, 'task.js registers its DOMContentLoaded boot');
|
|
315
|
-
return {
|
|
316
|
+
return {
|
|
317
|
+
boot: onReady,
|
|
318
|
+
html: () => documentObj.getElementById('task-body').innerHTML,
|
|
319
|
+
sub: () => documentObj.getElementById('task-sub').textContent,
|
|
320
|
+
el: (id) => documentObj.getElementById(id),
|
|
321
|
+
};
|
|
316
322
|
}
|
|
317
323
|
|
|
318
324
|
const settle = () => new Promise((r) => setTimeout(r, 0));
|
|
@@ -410,3 +416,76 @@ test('the idea grade badge IS the archetype pill, not a private copy of it', ()
|
|
|
410
416
|
assert.doesNotMatch(decomment(ideaCss), /\.ig-grade(?![\w-])/,
|
|
411
417
|
'idea.css carried a near-verbatim copy of .rec-pill down to the token triples — a hundred lines waiting to drift');
|
|
412
418
|
});
|
|
419
|
+
|
|
420
|
+
// ---- 10. the way out — the record exits to where it was opened from ----------
|
|
421
|
+
//
|
|
422
|
+
// task 1003468. task.js hardcoded "Open the work board" in three places and
|
|
423
|
+
// task.html's crumb a fourth, so a builder who walked goal → task had only the
|
|
424
|
+
// browser's own Back, and the two failure renders sent them somewhere they had
|
|
425
|
+
// never been. The list that opens a record now writes `?from=` on the link
|
|
426
|
+
// (task-detail.js's href()) and the record offers THAT as its exit — in the
|
|
427
|
+
// crumb before the fetch lands, and in the actions row of every render
|
|
428
|
+
// including the two failures. A same-origin referrer is the fallback for a link
|
|
429
|
+
// written without it; the board is the fallback for a reader with no history.
|
|
430
|
+
// Every value is honoured only as a path on this host.
|
|
431
|
+
|
|
432
|
+
const FROM_GOAL = '?from=' + encodeURIComponent('/goals#/goal/1000081');
|
|
433
|
+
|
|
434
|
+
test('opened from a goal, the record exits to that goal — crumb and actions row alike', async () => {
|
|
435
|
+
const r = renderTask(BASE_TASK, { search: FROM_GOAL });
|
|
436
|
+
await r.boot(); await settle();
|
|
437
|
+
assert.match(r.html(), /<a class="td-btn" href="\/goals#\/goal\/1000081">Back to goal 1000081<\/a>/);
|
|
438
|
+
assert.doesNotMatch(r.html(), /Open the work board/, 'the hardcoded exit is gone from the happy path');
|
|
439
|
+
assert.equal(r.el('task-back').href, '/goals#/goal/1000081', 'the crumb points at the goal');
|
|
440
|
+
assert.equal(r.el('task-back-label').textContent, 'Back to goal 1000081');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test('opened from the board, the exit is the board — on the tab it was opened from', async () => {
|
|
444
|
+
const r = renderTask(BASE_TASK, { search: '?from=' + encodeURIComponent('/work#backlog') });
|
|
445
|
+
await r.boot(); await settle();
|
|
446
|
+
assert.match(r.html(), /href="\/work#backlog">Back to the work board</);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test('without ?from=, a same-origin referrer is the way back', async () => {
|
|
450
|
+
const r = renderTask(BASE_TASK, { referrer: 'https://builders.cloudbongos.com/sessions?page=2' });
|
|
451
|
+
await r.boot(); await settle();
|
|
452
|
+
assert.match(r.html(), /href="\/sessions\?page=2">Back to sessions</);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test('a cross-origin referrer, a broken one, or none, falls back to the work board', async () => {
|
|
456
|
+
for (const referrer of ['', 'https://example.org/elsewhere', 'not a url']) {
|
|
457
|
+
const r = renderTask(BASE_TASK, { referrer });
|
|
458
|
+
await r.boot(); await settle();
|
|
459
|
+
assert.match(r.html(), /href="\/work">Back to the work board</, `referrer ${JSON.stringify(referrer)}`);
|
|
460
|
+
assert.equal(r.el('task-back').href, '/work');
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test('a hostile or useless ?from= is refused, never followed', async () => {
|
|
465
|
+
const bad = [
|
|
466
|
+
'//evil.example', '/\\evil.example', 'javascript:alert(1)', 'https://evil.example/x',
|
|
467
|
+
'/api/gds/auth/web/callback', '/lib/bongos-client.js',
|
|
468
|
+
'/task/1003305', // this same record — a loop, not a way back
|
|
469
|
+
'work', '/ work', '',
|
|
470
|
+
];
|
|
471
|
+
for (const from of bad) {
|
|
472
|
+
const r = renderTask(BASE_TASK, { search: '?from=' + encodeURIComponent(from) });
|
|
473
|
+
await r.boot(); await settle();
|
|
474
|
+
assert.match(r.html(), /href="\/work">Back to the work board</, `from=${JSON.stringify(from)}`);
|
|
475
|
+
assert.equal(r.el('task-back').href, '/work', `the crumb refuses from=${JSON.stringify(from)} too`);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test('the two honest failures exit the same way the record does', async () => {
|
|
480
|
+
for (const status of [404, 500]) {
|
|
481
|
+
const r = renderTask(BASE_TASK, { status, search: FROM_GOAL });
|
|
482
|
+
await r.boot(); await settle();
|
|
483
|
+
assert.match(r.html(), /href="\/goals#\/goal\/1000081">Back to goal 1000081</, `status ${status}`);
|
|
484
|
+
assert.doesNotMatch(r.html(), /Open the work board/, `status ${status} still hardcodes the board`);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test('the crumb is an element task.js can point, with its label apart from the icon', () => {
|
|
489
|
+
assert.match(taskHtml, /<a class="rec-back" id="task-back" href="\/work">/, 'the static href is the fallback the page ships with');
|
|
490
|
+
assert.match(taskHtml, /<span id="task-back-label">Back to tasks<\/span>/, 'the label is its own node, so the icon survives a relabel');
|
|
491
|
+
});
|
|
@@ -127,7 +127,12 @@ test('static ladder copies carry the exact canonical ladder', () => {
|
|
|
127
127
|
// task 1003206 carved the hall page scripts up; the rank tier maps travelled with
|
|
128
128
|
// the renderers that read them, so the pin and the allowlist follow the code.
|
|
129
129
|
const HALL_SCRIPTS = ['modules/hall-ui/public/builders.js', 'modules/hall-ui/public/goals-page.js',
|
|
130
|
-
'modules/hall-ui/public/hall-render.js', 'modules/hall-ui/public/goals-render.js'
|
|
130
|
+
'modules/hall-ui/public/hall-render.js', 'modules/hall-ui/public/goals-render.js',
|
|
131
|
+
// task 1003441: the Builders page ranks the roster and draws a rank badge, so
|
|
132
|
+
// it needs both the tier order (its `rank` sort lens) and the known-rank test
|
|
133
|
+
// (the badge variant class). Browser code cannot require db-kernel, so it takes
|
|
134
|
+
// the pinned-copy shape builders.js already takes.
|
|
135
|
+
'modules/hall-ui/public/roster.js'];
|
|
131
136
|
// Filtered INSIDE the test, not at module scope: extractRankMaps is declared
|
|
132
137
|
// further down, so calling it up here is a temporal-dead-zone crash.
|
|
133
138
|
const staticMapFiles = () => HALL_SCRIPTS.filter(
|
|
@@ -160,6 +165,8 @@ const MAP_ALLOWLIST = new Set([
|
|
|
160
165
|
// into goals-render.js — so goals-page.js is pruned and hall-render.js is never
|
|
161
166
|
// added. The allowlist fails on a stale entry, which is the check working.
|
|
162
167
|
'modules/hall-ui/public/goals-render.js',
|
|
168
|
+
// task 1003441 — see the HALL_SCRIPTS note above: pinned, not unchecked.
|
|
169
|
+
'modules/hall-ui/public/roster.js',
|
|
163
170
|
]);
|
|
164
171
|
// Where a rank-ladder array literal is allowed to exist.
|
|
165
172
|
const LADDER_ALLOWLIST = new Set([
|