@bongos/core 1.24.41 → 1.24.43
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 +117 -92
- package/docs/copy-inventory.md +98 -137
- package/docs/copy-registry.json +244 -644
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/primer.md +1 -1
- package/docs/page-inventory.json +3 -3
- package/docs/page-readings.json +494 -535
- package/modules/hall-ui/public/ai-studio.html +4 -4
- package/modules/hall-ui/public/atlas.html +4 -4
- package/modules/hall-ui/public/blockers.html +4 -4
- package/modules/hall-ui/public/board-room.html +4 -4
- package/modules/hall-ui/public/builders.js +35 -763
- package/modules/hall-ui/public/city.html +4 -4
- package/modules/hall-ui/public/collab.html +4 -4
- package/modules/hall-ui/public/commands.html +4 -4
- package/modules/hall-ui/public/copy-desk.html +4 -4
- package/modules/hall-ui/public/craft-mode.js +1 -1
- package/modules/hall-ui/public/deploy.html +4 -4
- package/modules/hall-ui/public/dom-utils.js +2 -1
- package/modules/hall-ui/public/goal-map.html +4 -4
- package/modules/hall-ui/public/goals.html +4 -4
- package/modules/hall-ui/public/government.html +4 -4
- package/modules/hall-ui/public/hall-render.js +25 -771
- package/modules/hall-ui/public/hall-widgets.js +14 -123
- package/modules/hall-ui/public/idea.html +4 -4
- package/modules/hall-ui/public/ideas.html +4 -4
- package/modules/hall-ui/public/index.html +8 -22
- package/modules/hall-ui/public/index.states.json +0 -33
- package/modules/hall-ui/public/market-watch-item.html +4 -4
- package/modules/hall-ui/public/market-watch-rules.html +4 -4
- package/modules/hall-ui/public/market-watch.html +4 -4
- package/modules/hall-ui/public/modules.html +4 -4
- package/modules/hall-ui/public/money.html +4 -4
- package/modules/hall-ui/public/my-agents.html +4 -4
- package/modules/hall-ui/public/palette.js +7 -6
- package/modules/hall-ui/public/primer.html +4 -4
- package/modules/hall-ui/public/profile-own.js +639 -0
- package/modules/hall-ui/public/profile.css +2 -1
- package/modules/hall-ui/public/profile.html +48 -5
- package/modules/hall-ui/public/profile.js +27 -2
- package/modules/hall-ui/public/profile.states.json +18 -0
- package/modules/hall-ui/public/project-settings.html +4 -4
- package/modules/hall-ui/public/roadmap.html +4 -4
- package/modules/hall-ui/public/roster.html +4 -4
- package/modules/hall-ui/public/settings.html +4 -4
- package/modules/hall-ui/public/setup-gate.js +156 -0
- package/modules/hall-ui/public/shell-trail.js +2 -1
- package/modules/hall-ui/public/shell.js +44 -26
- package/modules/hall-ui/public/street.html +4 -4
- package/modules/hall-ui/public/studio.html +4 -4
- package/modules/hall-ui/public/style.css +85 -117
- package/modules/hall-ui/public/task.html +4 -4
- package/modules/hall-ui/public/thinking.html +4 -4
- package/modules/hall-ui/public/tweak-editor.html +4 -4
- package/modules/hall-ui/public/watch.html +4 -4
- package/modules/hall-ui/public/work.html +4 -4
- package/modules/hall-ui/records/setup-gate.md +43 -0
- package/modules/lifecycle/routes/tasks.js +2 -2
- package/modules/onboarding/onboarding-state.js +41 -20
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/routes/me.js +2 -2
- package/src/module-api.js +1 -1
- package/tests/curve_controls_delegation.mjs +9 -8
- package/tests/curve_kind_overlay.mjs +5 -5
- package/tests/hall_acclaim_vote_control.mjs +5 -3
- package/tests/hall_audit.mjs +10 -6
- package/tests/hall_board_world.mjs +41 -27
- package/tests/hall_builders_page_pass.mjs +14 -5
- package/tests/hall_home_ships_bounded.mjs +10 -21
- package/tests/hall_kit_charts.mjs +3 -2
- package/tests/hall_landing_boot.mjs +2 -3
- package/tests/hall_loading.mjs +2 -1
- package/tests/hall_nav.mjs +36 -37
- package/tests/hall_numbers_page.mjs +100 -0
- package/tests/hall_onboarding_bongos_setup.mjs +14 -14
- package/tests/hall_palette.mjs +19 -24
- package/tests/hall_profile_world.mjs +7 -2
- package/tests/hall_reading_room.mjs +3 -2
- package/tests/helpers.mjs +8 -2
- package/tests/leaderboard_roles.mjs +1 -1
- package/tests/module_contributions.mjs +10 -6
- package/tests/nav_permission_atoms.mjs +4 -2
- package/tests/onboarding_state.mjs +33 -0
- package/tests/page_tweak_artist.mjs +2 -1
- package/tests/rank_tier_single_source.mjs +3 -0
- package/tests/setup_flow.mjs +242 -0
- package/tests/task_visuals.mjs +3 -1
- package/tests/welcome_boot.mjs +3 -2
|
@@ -19,7 +19,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
19
19
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
20
|
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
21
21
|
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8').replace(/\r\n/g, '\n');
|
|
22
|
-
|
|
22
|
+
// task 1005050: the peer-recognition feed moved with Accomplishments to the
|
|
23
|
+
// Profile page's "Just you" screen (profile-own.js; its markup is profile.html's).
|
|
24
|
+
const builders = read('profile-own.js');
|
|
23
25
|
|
|
24
26
|
// A small DOM: enough for the control's cast() and event dispatch.
|
|
25
27
|
function el(attrs = {}) {
|
|
@@ -71,7 +73,7 @@ function bootControl(fetchImpl) {
|
|
|
71
73
|
// The feed's real handler, sliced from builders.js and run over a held list.
|
|
72
74
|
function feedHandler(tasks) {
|
|
73
75
|
const start = builders.indexOf('function onAcclaimVote(');
|
|
74
|
-
assert.ok(start > 0, '
|
|
76
|
+
assert.ok(start > 0, 'profile-own.js defines onAcclaimVote');
|
|
75
77
|
const end = builders.indexOf('\n }\n', start) + 4;
|
|
76
78
|
const ctx = { acclaimState: { tasks } };
|
|
77
79
|
vm.createContext(ctx);
|
|
@@ -109,5 +111,5 @@ test('a vote through the real control lands in the held, paged rows', async () =
|
|
|
109
111
|
test('the old vote markup, handler and CSS are gone', () => {
|
|
110
112
|
assert.doesNotMatch(builders, /acclaim__btn|acclaim__vote|data-vote-task|onAcclaimClick|data-acclaim-net/);
|
|
111
113
|
assert.doesNotMatch(read('style.css'), /\.acclaim__(btn|vote|readonly|net)/);
|
|
112
|
-
assert.match(read('
|
|
114
|
+
assert.match(read('profile.html'), /<th>Work<\/th><th>By<\/th><th>Votes<\/th><\/tr>/);
|
|
113
115
|
});
|
package/tests/hall_audit.mjs
CHANGED
|
@@ -320,16 +320,19 @@ test('the three bar copies are gone, and their class families with them', () =>
|
|
|
320
320
|
for (const dead of ['curve__kind-', 'curve__chart', 'watch-trend', 'pmix']) {
|
|
321
321
|
assert.ok(!css.includes(dead), `${dead} is the kit's .kit-bars/.kit-chart now`);
|
|
322
322
|
}
|
|
323
|
-
|
|
323
|
+
// task 1005050: the Curve moved with Accomplishments to profile-own.js.
|
|
324
|
+
assert.match(read('profile-own.js'), /OTBKit\.barSeriesHtml\(/, 'the Curve\'s kinds draw through the kit');
|
|
324
325
|
assert.match(read('watch.js'), /OTBKit\.barSeriesHtml\(/, 'Watch\'s trend draws through the kit');
|
|
325
326
|
assert.match(read('profile-charts.js'), /OTBKit\.barSeriesHtml\(/, 'the craft mix draws through the kit');
|
|
326
327
|
assert.match(read('profile-charts.js'), /OTBKit\.calendarGridHtml\(/, 'the contribution calendar draws through the kit (task 1003572)');
|
|
327
|
-
assert.match(read('
|
|
328
|
+
assert.match(read('profile-own.js'), /OTBKit\.sparklineHtml\(/, 'and the Curve\'s line does too');
|
|
328
329
|
});
|
|
329
330
|
|
|
330
331
|
test('the peer-recognition feed is paged through OTBKit, not by hand', () => {
|
|
331
|
-
|
|
332
|
-
|
|
332
|
+
// task 1005050: the feed moved with Accomplishments to the profile's "Just you"
|
|
333
|
+
// screen; profile-own.js stamps the kit's bar and pages it.
|
|
334
|
+
const js = read('profile-own.js');
|
|
335
|
+
assert.match(js, /pagerBarHtml\('acclaim'/, 'the acclaim card stamps the kit pager bar');
|
|
333
336
|
assert.match(js, /makePager\('acclaim'/, 'acclaim has a kit pager');
|
|
334
337
|
assert.match(js, /paginate\(acclaimState\.tasks, acclaimPager\)/, 'rows are sliced by the kit');
|
|
335
338
|
assert.match(js, /wirePager\(acclaimPager/, 'prev/next/per-page are wired');
|
|
@@ -486,8 +489,9 @@ test('the colour-literal guard CATCHES a reintroduction, and tolerates a task re
|
|
|
486
489
|
'a token is exactly what this rule wants');
|
|
487
490
|
});
|
|
488
491
|
|
|
489
|
-
test("
|
|
490
|
-
|
|
492
|
+
test("the Reviews grade column renders its three verdicts through the vocabulary", () => {
|
|
493
|
+
// task 1005050: Accomplishments' Reviews table is the profile's "Just you" screen's.
|
|
494
|
+
const js = read('profile-own.js');
|
|
491
495
|
const start = js.indexOf('function renderRecentGrades');
|
|
492
496
|
assert.ok(start > 0, 'renderRecentGrades present');
|
|
493
497
|
const body = js.slice(start, js.indexOf('\n }', start));
|
|
@@ -153,11 +153,14 @@ test('work.html has exactly one h1', () => {
|
|
|
153
153
|
});
|
|
154
154
|
|
|
155
155
|
test('both re-skinned pages have a states file, so the floors are reproducible', () => {
|
|
156
|
-
|
|
156
|
+
// task 1005050: Home's two Accomplishments states left with that view (it
|
|
157
|
+
// folded into the Profile page, whose states file measures the "Just you" screen).
|
|
158
|
+
for (const [f, min] of [['work.states.json', 4], ['index.states.json', 2]]) {
|
|
157
159
|
const json = JSON.parse(read(f));
|
|
158
160
|
assert.equal(json.surface, 'hall-ui', `${f} names its surface`);
|
|
159
|
-
assert.ok(Object.keys(json.states).length >=
|
|
161
|
+
assert.ok(Object.keys(json.states).length >= min, `${f} renders more than the default state`);
|
|
160
162
|
}
|
|
163
|
+
assert.ok(JSON.parse(read('profile.states.json')).states.yours, 'the "Just you" screen is measured');
|
|
161
164
|
});
|
|
162
165
|
|
|
163
166
|
// ---- 7. the way INTO a record is the title ----------------------------------
|
|
@@ -350,7 +353,7 @@ function bootHallRender({ me, progress = null, leaderboard = [], ctxExtra = {} }
|
|
|
350
353
|
curLabel: (caps) => (caps ? 'Credits' : 'credits'), emptyRow: () => '', fmtDate: (d) => String(d).slice(0, 10),
|
|
351
354
|
mins: (m) => `${m}m`, normalizeRank: (r) => String(r || '').toLowerCase(), pct: (n) => `${n}%`, postJSON: async () => ({}),
|
|
352
355
|
rankLabel: (r) => r, rankWeight: () => 0, signClasses: () => '', toast() {}, usd: (n) => `$${n}`,
|
|
353
|
-
|
|
356
|
+
newcomerGated: false,
|
|
354
357
|
get lastMeState() { return me; }, get lastCostState() { return me.cost || null; }, get lastKarmaState() { return me.karma || null; },
|
|
355
358
|
get lastNeedsState() { return me.needs || null; }, get lastRebaseState() { return me.needs_rebase_tasks || null; },
|
|
356
359
|
get lastProgressState() { return progress; },
|
|
@@ -397,46 +400,57 @@ test('RUN: the Standing card carries the three facts, the place as "2nd of 4", a
|
|
|
397
400
|
assert.match(withPrefs.el('home-standing-body').innerHTML, /<dt>Leaderboard place<\/dt><dd>—<\/dd>/, 'no roll yet: a dash, not a wrong place');
|
|
398
401
|
});
|
|
399
402
|
|
|
400
|
-
// task 1005046: "Where you stand" left the retired Ranks page for Accomplishments
|
|
401
|
-
//
|
|
402
|
-
// the viewer's rank, the rank
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
};
|
|
403
|
+
// task 1005046: "Where you stand" left the retired Ranks page for Accomplishments,
|
|
404
|
+
// and task 1005050 moved it on, with Accomplishments, to your own profile's
|
|
405
|
+
// "Just you" screen (profile-own.js). The card names the viewer's rank, the rank
|
|
406
|
+
// above it read off the ladder, and how to reach it.
|
|
407
|
+
function bootOwnRank(builder) {
|
|
408
|
+
const nodes = {};
|
|
409
|
+
const el = (id) => (nodes[id] = nodes[id] || { id, hidden: true, innerHTML: '', textContent: '', style: {}, addEventListener() {}, querySelector: () => null });
|
|
410
|
+
const document = { getElementById: el, querySelector: (sel) => el(String(sel).replace(/^#/, '')), querySelectorAll: () => [] };
|
|
411
|
+
const window = {
|
|
412
|
+
OTB: { escapeHtml: (x) => String(x ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])), $: document.querySelector, fmtNum: String, fmtDate: String,
|
|
413
|
+
rankLabel: (r) => (r ? r.charAt(0).toUpperCase() + r.slice(1) : '') },
|
|
414
|
+
OTBKit: { makePager: () => ({ page: 1 }), paginate: (x) => x, wirePager() {}, pagerBarHtml: () => '' },
|
|
415
|
+
};
|
|
416
|
+
// A quiet console: the feeds this stub refuses log their own failure.
|
|
417
|
+
const sandbox = { window, document, console: { log() {}, warn() {}, error() {} }, setTimeout, Promise };
|
|
418
|
+
vm.createContext(sandbox);
|
|
419
|
+
vm.runInContext(read('profile-own.js'), sandbox, { filename: 'profile-own.js' });
|
|
420
|
+
const api = { request: () => Promise.resolve({ status: 404, ok: false, data: null }) };
|
|
421
|
+
window.OTBProfileOwn.render({ api, API: '/api/bongos', me: { builder } });
|
|
422
|
+
return { el };
|
|
423
|
+
}
|
|
407
424
|
|
|
408
425
|
test('RUN: Where you stand names the viewer\'s rank, the one above it, and how to reach it', () => {
|
|
409
|
-
const {
|
|
410
|
-
api.renderProfile({ ...ME.builder, rank: 'thetes' });
|
|
426
|
+
const { el } = bootOwnRank({ ...ME.builder, rank: 'thetes' });
|
|
411
427
|
const html = el('rank-standing-body').innerHTML;
|
|
428
|
+
assert.equal(el('rank-scroll').hidden, false, 'the card shows on your own profile');
|
|
412
429
|
assert.match(html, /<div class="ranks-standing__badge"><span class="rank-badge rank-badge--thetes"/);
|
|
413
430
|
assert.match(html, /<p class="ranks-standing__title">The proven builder<\/p>/);
|
|
414
|
-
assert.match(html, /The rank above yours is <span class="rank-badge rank-badge--metic"/, 'the next rung is read off the
|
|
431
|
+
assert.match(html, /The rank above yours is <span class="rank-badge rank-badge--metic"/, 'the next rung is read off the ladder');
|
|
415
432
|
assert.match(html, /Promotion to <strong>Metic<\/strong>/, 'and the climb is the one from THIS rank');
|
|
416
433
|
assert.match(html, /<a href="\/primer#the-rank-ladder">/, 'the full ladder is one chapter of the Primer');
|
|
417
434
|
assert.doesNotMatch(html, /Loading…/, 'the skeleton is replaced');
|
|
418
435
|
});
|
|
419
436
|
|
|
420
437
|
test('RUN: Where you stand says so at the top, and coerces a stray rank to the entry rung', () => {
|
|
421
|
-
const
|
|
422
|
-
top.api.renderProfile({ ...ME.builder, rank: 'archon' });
|
|
423
|
-
const atTop = top.el('rank-standing-body').innerHTML;
|
|
438
|
+
const atTop = bootOwnRank({ ...ME.builder, rank: 'archon' }).el('rank-standing-body').innerHTML;
|
|
424
439
|
assert.equal((atTop.match(/You hold the highest rank/g) || []).length, 1, 'said once, not twice');
|
|
425
440
|
assert.doesNotMatch(atTop, /ranks-standing__next/, 'no next rung to name');
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
const html = stray.el('rank-standing-body').innerHTML;
|
|
429
|
-
assert.match(html, /<p class="ranks-standing__title">The newcomer<\/p>/, 'a reserved rank reads as xenos, as the Standing band does');
|
|
441
|
+
const html = bootOwnRank({ ...ME.builder, rank: 'strategos' }).el('rank-standing-body').innerHTML;
|
|
442
|
+
assert.match(html, /<p class="ranks-standing__title">The newcomer<\/p>/, 'a reserved rank reads as xenos');
|
|
430
443
|
assert.match(html, /The rank above yours is <span class="rank-badge rank-badge--thetes"/);
|
|
431
444
|
});
|
|
432
445
|
|
|
433
|
-
test('the Where you stand card is
|
|
434
|
-
const
|
|
435
|
-
const at =
|
|
436
|
-
assert.ok(at > -1, '
|
|
437
|
-
const
|
|
438
|
-
assert.match(
|
|
439
|
-
assert.match(
|
|
446
|
+
test('the Where you stand card is on the profile\'s "Just you" screen, hidden until drawn, with a Loading… line', () => {
|
|
447
|
+
const html = read('profile.html');
|
|
448
|
+
const at = html.indexOf('id="rank-scroll"');
|
|
449
|
+
assert.ok(at > -1, 'profile.html carries the card');
|
|
450
|
+
const sec = html.slice(html.lastIndexOf('<section', at), html.indexOf('</section>', at));
|
|
451
|
+
assert.match(sec, /data-group="yours" hidden/);
|
|
452
|
+
assert.match(sec, /<div id="rank-standing-body"><div class="hall-loading" role="status">Loading…<\/div><\/div>/);
|
|
453
|
+
assert.ok(!/w\('rank-scroll'/.test(read('hall-widgets.js')), 'Home no longer registers it');
|
|
440
454
|
});
|
|
441
455
|
|
|
442
456
|
test('RUN: the three notices are inbox lines with a kind, a line and — where one exists — a door', () => {
|
|
@@ -229,10 +229,19 @@ test('B: the id wears the same mono/faint voice as the login', () => {
|
|
|
229
229
|
// PART C — grade and cost on the peer-acclaim feed (task 1002835)
|
|
230
230
|
// =========================================================================
|
|
231
231
|
|
|
232
|
-
// acclaimEvidenceHtml
|
|
233
|
-
// (
|
|
234
|
-
//
|
|
235
|
-
const acclaimEvidence = () =>
|
|
232
|
+
// acclaimEvidenceHtml moved with the peer-recognition feed to profile-own.js
|
|
233
|
+
// (task 1005050: Accomplishments folded into the Profile page). It is private to
|
|
234
|
+
// that file's closure, so the shipped function's own source is run here.
|
|
235
|
+
const acclaimEvidence = () => {
|
|
236
|
+
const src = read('profile-own.js');
|
|
237
|
+
const start = src.indexOf(' function acclaimEvidenceHtml(t) {');
|
|
238
|
+
assert.ok(start > 0, 'profile-own.js defines acclaimEvidenceHtml');
|
|
239
|
+
const end = src.indexOf('\n }\n', start) + 4;
|
|
240
|
+
const ctx = {};
|
|
241
|
+
vm.createContext(ctx);
|
|
242
|
+
vm.runInContext(`${src.slice(start, end)}\nthis.fn = acclaimEvidenceHtml;`, ctx);
|
|
243
|
+
return ctx.fn;
|
|
244
|
+
};
|
|
236
245
|
test('C: a graded, costed ship carries both numbers', () => {
|
|
237
246
|
const html = acclaimEvidence()({ grade_score: '8.5', grade_passed: true, cost_usd: '12.3456' });
|
|
238
247
|
assert.match(html, /ledger__verdict--ok">8\.5 \/ 10</, 'a passing grade reads as passing');
|
|
@@ -264,7 +273,7 @@ test('C: absence renders as absence, never as zero', () => {
|
|
|
264
273
|
});
|
|
265
274
|
|
|
266
275
|
test('C: the evidence line hangs under the work, in the feed row', () => {
|
|
267
|
-
const src = read('
|
|
276
|
+
const src = read('profile-own.js');
|
|
268
277
|
assert.match(
|
|
269
278
|
src,
|
|
270
279
|
/<td>\$\{title\}\$\{acclaimEvidenceHtml\(t\)\}<\/td>/,
|
|
@@ -134,28 +134,17 @@ test('Home: the Standing chip\'s count comes from the small page, with no ledger
|
|
|
134
134
|
assert.equal(el('recent-ships tbody').innerHTML, '', 'the ledger is untouched until its view opens');
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
// task 1005050: the full ledger was the Accomplishments view's, and that view
|
|
138
|
+
// folded into the Profile page (whose Works ledger reads its own record). Home
|
|
139
|
+
// now NEVER asks for the whole history — not on a view change, not on a refresh.
|
|
140
|
+
test('Home never downloads the whole history, whatever view event or refresh arrives', async () => {
|
|
141
|
+
const { r, el, fetched, showView } = loadRender({ view: 'standing' });
|
|
139
142
|
r.setRecentShips(homePage(3000));
|
|
140
143
|
showView('standing');
|
|
141
|
-
await r.loadShipLedger();
|
|
142
|
-
assert.deepEqual(fetched, ['/api/bongos/me/recent-ships?limit=all']);
|
|
143
|
-
assert.match(el('ships-count').textContent, /3000 works shipped/);
|
|
144
|
-
showView('home');
|
|
145
|
-
showView('standing');
|
|
146
|
-
await r.loadShipLedger();
|
|
147
|
-
assert.equal(fetched.length, 1, 'reopening the view with nothing new does not refetch');
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test('a live refresh refetches the open ledger only when the newest ships or the count moved', async () => {
|
|
151
|
-
const { r, fetched } = loadRender({ view: 'standing' });
|
|
152
|
-
r.setRecentShips(homePage(3000));
|
|
153
|
-
await r.loadShipLedger();
|
|
154
|
-
assert.equal(fetched.length, 1, 'opening straight onto Accomplishments loads the ledger');
|
|
155
|
-
r.setRecentShips(homePage(3000)); // a nudge from someone else's write
|
|
156
|
-
await r.loadShipLedger();
|
|
157
|
-
assert.equal(fetched.length, 1, 'an unrelated write does not re-download the history');
|
|
158
144
|
r.setRecentShips(homePage(3001, 3001)); // this builder shipped
|
|
159
|
-
await
|
|
160
|
-
assert.
|
|
145
|
+
await new Promise((done) => setTimeout(done, 0));
|
|
146
|
+
assert.deepEqual(fetched, [], 'no ?limit=all read from Home');
|
|
147
|
+
assert.equal(el('standing-shipped').textContent, '3001', 'the chip follows the newest total');
|
|
148
|
+
assert.equal(r.loadShipLedger, undefined, 'the ledger loader went with the view');
|
|
161
149
|
});
|
|
150
|
+
|
|
@@ -338,7 +338,8 @@ test('Watch\'s trend takes the neutral tone — on an oversight page the accent
|
|
|
338
338
|
});
|
|
339
339
|
|
|
340
340
|
test('the Curve keeps its own line colour, which is not the kit default', () => {
|
|
341
|
-
|
|
341
|
+
// task 1005050: the Curve moved with Accomplishments to the profile's "Just you" screen.
|
|
342
|
+
const src = read('profile-own.js');
|
|
342
343
|
assert.match(src, /sparklineHtml\(values, \{ color: 'var\(--accent-deep\)'/,
|
|
343
344
|
'the Curve\'s first chart is accent-deep; the kit\'s fallback is --accent');
|
|
344
345
|
assert.match(src, /OTBKit\.barSeriesHtml\(/, 'and its kinds row draws through the kit');
|
|
@@ -348,7 +349,7 @@ test('the kit is loaded before every page that draws through it', () => {
|
|
|
348
349
|
// A page script reading window.OTBKit at render time with the kit loaded
|
|
349
350
|
// after it is a blank chart in production and a green unit suite.
|
|
350
351
|
for (const [page, script] of [
|
|
351
|
-
['
|
|
352
|
+
['profile.html', 'profile-own.js'],
|
|
352
353
|
['watch.html', 'watch.js'],
|
|
353
354
|
['profile.html', 'profile-charts.js'],
|
|
354
355
|
// task 1003447: the roadmap's own series module reads the kit, and the page
|
|
@@ -92,15 +92,14 @@ async function boot({ meStatus, settle = false }) {
|
|
|
92
92
|
},
|
|
93
93
|
OTBHallRender: () => ({
|
|
94
94
|
analyticsState: {}, lbState: {},
|
|
95
|
-
shipsPager: { page: 1 },
|
|
95
|
+
shipsPager: { page: 1 },
|
|
96
96
|
// task 1004854: Home asks for this many ships; the ledger's full read lives in hall-render.js.
|
|
97
97
|
HOME_SHIPS_LIMIT: 5, setRecentShips: spy('setRecentShips'), populateShipVersionFilter: spy('populateShipVersionFilter'),
|
|
98
98
|
mountHallWidgets: spy('mountHallWidgets'),
|
|
99
99
|
renderLanding: spy('renderLanding'),
|
|
100
100
|
renderCurveBody: spy('renderCurveBody'), renderGreeting: spy('renderGreeting'),
|
|
101
|
-
renderNav: spy('renderNav'),
|
|
101
|
+
renderNav: spy('renderNav'),
|
|
102
102
|
renderProfile: spy('renderProfile'), renderRecentShips: spy('renderRecentShips'),
|
|
103
|
-
renderWelcomeCore: spy('renderWelcomeCore'),
|
|
104
103
|
// task 1003459 — wireEvents() calls this unconditionally now (the Home
|
|
105
104
|
// Next-up Claim button), so an incomplete stub throws mid-wire.
|
|
106
105
|
wireNextUpClaim: spy('wireNextUpClaim'),
|
package/tests/hall_loading.mjs
CHANGED
|
@@ -135,7 +135,8 @@ test('no hall page waits on a bare line of text any more', () => {
|
|
|
135
135
|
assert.deepEqual(misses, [], 'every waiting placeholder wears the loader');
|
|
136
136
|
// The table placeholder rows keep their cell, with the loader inside it.
|
|
137
137
|
assert.match(read('work.html'), /<td colspan="6" class="ledger__empty"><span class="hall-loading" role="status">Loading…<\/span><\/td>/);
|
|
138
|
-
|
|
138
|
+
// task 1005050: the karma feed's table moved with Accomplishments into profile.html.
|
|
139
|
+
assert.match(read('profile.html'), /<td colspan="4" class="ledger__empty"><span class="hall-loading" role="status">Loading…<\/span><\/td>/);
|
|
139
140
|
});
|
|
140
141
|
|
|
141
142
|
test('the placeholders the page scripts write wear it too, and a failure stops it', () => {
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
// - every nav href resolves to a page that exists ("all links land"),
|
|
11
11
|
// - a group's head is never more permissive than its most permissive item
|
|
12
12
|
// (otherwise the head renders over an empty group),
|
|
13
|
-
// - Achievements has no nav entry,
|
|
14
|
-
//
|
|
13
|
+
// - Achievements has no nav entry, and since task 1005050 neither has
|
|
14
|
+
// Accomplishments: both folded into the Profile page, and every old
|
|
15
|
+
// #achievements / #accomplishments / #standing link redirects there.
|
|
15
16
|
//
|
|
16
17
|
// Pure vm + file-read (no DB/network) — runs in the DB-free unit gate.
|
|
17
18
|
import assert from 'node:assert/strict';
|
|
@@ -296,7 +297,8 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
296
297
|
// task 1004685 (the living-city prototype, round-3 board 6): 'my-agents' joins You —
|
|
297
298
|
// how your own Claude works for you, the personal half of "the city steers
|
|
298
299
|
// everyone, My agents steers yours". Its subject is the viewer's own agents.
|
|
299
|
-
|
|
300
|
+
// task 1005050: 'standing' (Accomplishments) left — your numbers are the one Profile page.
|
|
301
|
+
assert.deepEqual(nav[3].items.map((i) => i.id), ['profile', 'my-agents'], 'You = your own numbers and your own agents');
|
|
300
302
|
// task 1003444: 'people' LEFT the group — the owner deleted the hall's
|
|
301
303
|
// cross-project directory (ADR 0337 D5); its home is the hub, and /people
|
|
302
304
|
// redirects to /roster. Builders is the hall's directory now.
|
|
@@ -556,16 +558,13 @@ test('#leaderboard REDIRECTS to /roster from the index, and only from the index'
|
|
|
556
558
|
assert.deepEqual(unknown.replaced, [], 'an unknown hash is not a redirect');
|
|
557
559
|
});
|
|
558
560
|
|
|
559
|
-
test('Achievements has no nav entry,
|
|
561
|
+
test('Achievements has no nav entry, and #achievements lands on your own profile', () => {
|
|
560
562
|
const ids = parseNav(boot().sidebarHtml).flatMap((g) => g.items.map((i) => i.id));
|
|
561
563
|
assert.ok(!ids.includes('achievements'), 'the nav entry retired with task 2175');
|
|
562
|
-
|
|
563
|
-
//
|
|
564
|
-
// bookmarks and the Standing stat tiles still link #achievements.
|
|
564
|
+
// task 1005050: the Standing view it routed to folded into Profile, whose
|
|
565
|
+
// overview carries your achievements.
|
|
565
566
|
const idx = boot({ page: 'home', pathname: '/', hash: '#achievements' });
|
|
566
|
-
assert.
|
|
567
|
-
assert.equal(idx.viewHidden('home'), true, 'and hides Home');
|
|
568
|
-
assert.equal(idx.shell.currentView(), 'standing');
|
|
567
|
+
assert.deepEqual(idx.replaced, ['/profile'], '#achievements redirects to your own profile');
|
|
569
568
|
});
|
|
570
569
|
|
|
571
570
|
test('collapsible groups carry a stable storage key (collapse persistence)', () => {
|
|
@@ -871,35 +870,35 @@ test('My agents draws its own glyph and lands on a real page', () => {
|
|
|
871
870
|
assert.ok(hrefResolves(item), 'my-agents.html exists');
|
|
872
871
|
});
|
|
873
872
|
|
|
874
|
-
// task
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
|
|
873
|
+
// task 1005050 (owner decision 2026-11-03, idea 1001315): Accomplishments
|
|
874
|
+
// folded into the Profile page — one page for your numbers. Its nav item is gone
|
|
875
|
+
// and every old link, by either of its names, lands on your OWN profile: the
|
|
876
|
+
// section it named, or the "Just you" screen that holds its private half.
|
|
877
|
+
test('Accomplishments is gone from the sidebar, and every old link lands on your own profile', () => {
|
|
878
878
|
const sidebar = boot().sidebarHtml;
|
|
879
|
-
const
|
|
880
|
-
assert.ok(
|
|
881
|
-
assert.
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
879
|
+
const ids = parseNav(sidebar).flatMap((g) => g.items).map((i) => i.id);
|
|
880
|
+
assert.ok(!ids.includes('standing'), 'no Accomplishments item');
|
|
881
|
+
assert.ok(!/>Accomplishments</.test(sidebar), 'nothing in the nav is called Accomplishments');
|
|
882
|
+
const LANDS = {
|
|
883
|
+
'#accomplishments': '/profile#yours',
|
|
884
|
+
'#accomplishments-karma': '/profile#yours',
|
|
885
|
+
'#accomplishments-progress': '/profile#yours',
|
|
886
|
+
'#accomplishments-work': '/profile#works',
|
|
887
|
+
'#accomplishments-achievements': '/profile',
|
|
888
|
+
'#standing': '/profile#yours',
|
|
889
|
+
'#standing-karma': '/profile#yours',
|
|
890
|
+
'#standing-work': '/profile#works',
|
|
891
|
+
};
|
|
892
|
+
for (const [hash, to] of Object.entries(LANDS)) {
|
|
887
893
|
const idx = boot({ page: 'home', pathname: '/', hash });
|
|
888
|
-
assert.
|
|
889
|
-
assert.equal(idx.viewHidden('home'), true, `${hash} hides Home`);
|
|
890
|
-
}
|
|
891
|
-
const html = fs.readFileSync(path.join(HALL, 'index.html'), 'utf8');
|
|
892
|
-
assert.match(html, /id="view-standing" data-view-title="Accomplishments"/, 'the top bar names it Accomplishments');
|
|
893
|
-
assert.ok(!/href="#standing/.test(fs.readFileSync(path.join(HALL, 'hall-widgets.js'), 'utf8')), 'Home links use the new name');
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
test('the Accomplishments band carries no notices — Home\'s attention cards own them (task 1004826)', () => {
|
|
897
|
-
const src = fs.readFileSync(path.join(HALL, 'hall-render.js'), 'utf8');
|
|
898
|
-
const fn = src.slice(src.indexOf(' function renderProfile(builder) {'), src.indexOf(' function renderSystemNotices('));
|
|
899
|
-
const band = fn.slice(fn.indexOf("$('#profile-body').innerHTML"), fn.indexOf("const welcomeLine"));
|
|
900
|
-
assert.ok(band.length > 100, 'sanity: found the band template');
|
|
901
|
-
for (const cls of ['profile__rebase', 'profile__needs', 'profile__budget']) {
|
|
902
|
-
assert.ok(!band.includes(cls), `${cls} no longer renders on the band`);
|
|
894
|
+
assert.deepEqual(idx.replaced, [to], `${hash} lands on ${to}`);
|
|
903
895
|
}
|
|
896
|
+
// No view is left for them to open on the index.
|
|
897
|
+
assert.ok(!/id="view-standing"/.test(fs.readFileSync(path.join(HALL, 'index.html'), 'utf8')), 'the view is gone');
|
|
898
|
+
// Home's Standing card still names the shipped count and links to the profile.
|
|
899
|
+
const render = fs.readFileSync(path.join(HALL, 'hall-render.js'), 'utf8');
|
|
900
|
+
assert.match(render, /View full profile →/);
|
|
901
|
+
assert.ok(!/\$\('#profile-body'\)/.test(render), 'the Accomplishments band is not drawn any more');
|
|
902
|
+
const fn = render.slice(render.indexOf(' function renderProfile(builder) {'), render.indexOf(' function renderSystemNotices('));
|
|
904
903
|
assert.match(fn, /renderSystemNotices\(\{ rebaseTasks, need, actionNeeds, cost \}\)/, 'Home still gets every notice');
|
|
905
904
|
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// tests/hall_numbers_page.mjs — your numbers on one page (task 1005050, owner
|
|
2
|
+
// decision 2026-11-03, idea 1001315 item 2).
|
|
3
|
+
//
|
|
4
|
+
// Accomplishments (Home's 'standing' view) and Profile both showed your shipped
|
|
5
|
+
// work, karma and achievements. Profile is the one page now:
|
|
6
|
+
//
|
|
7
|
+
// * ONE PAGE. The Accomplishments view, its nav item and its widgets are gone;
|
|
8
|
+
// its private half — where you stand, reviews, the karma feed, peer
|
|
9
|
+
// recognition and the progress curve — is Profile's "Just you" screen.
|
|
10
|
+
// * NOTHING PRIVATE ON SOMEONE ELSE'S PROFILE. profile-own.js runs only when the
|
|
11
|
+
// profile ASKED FOR is the viewer's own; on anyone else's it reads nothing and
|
|
12
|
+
// draws nothing, whatever the hash says.
|
|
13
|
+
// * OLD LINKS LAND ON YOUR OWN PROFILE (shell.js VIEW_REDIRECTS; held, with the
|
|
14
|
+
// sidebar, in tests/hall_nav.mjs).
|
|
15
|
+
//
|
|
16
|
+
// These run the real profile.js and profile-own.js against the shared profile
|
|
17
|
+
// harness (tests/helpers.mjs bootProfilePage).
|
|
18
|
+
//
|
|
19
|
+
// Run: node tests/hall_numbers_page.mjs
|
|
20
|
+
|
|
21
|
+
import { test } from 'node:test';
|
|
22
|
+
import assert from 'node:assert/strict';
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
import { bootProfilePage } from './helpers.mjs';
|
|
27
|
+
|
|
28
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
30
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
31
|
+
const settle = async () => { for (let i = 0; i < 30; i++) await new Promise((r) => setImmediate(r)); };
|
|
32
|
+
|
|
33
|
+
// The reads only the "Just you" screen makes: the peer-recognition feed and the
|
|
34
|
+
// viewer's own progress curve.
|
|
35
|
+
const OWN_READS = /\/tasks\/peer-votes\/feed|\/analytics\/builder\//;
|
|
36
|
+
|
|
37
|
+
test('your own profile (no ?id) draws the "Just you" screen', async () => {
|
|
38
|
+
const p = bootProfilePage({ withOwn: true });
|
|
39
|
+
await settle();
|
|
40
|
+
assert.match(p.el('rank-standing-body').innerHTML, /ranks-standing__title/, 'where you stand is drawn');
|
|
41
|
+
assert.equal(p.el('rank-scroll').hidden, false);
|
|
42
|
+
assert.equal(p.el('grades-scroll').hidden, false, 'the Reviews section shows');
|
|
43
|
+
// The two feeds with reads of their own wait until the screen is opened: a
|
|
44
|
+
// visit for your Works does not pay for them.
|
|
45
|
+
assert.equal(p.requests.filter((u) => OWN_READS.test(u)).length, 0, 'no feed is read before the screen opens');
|
|
46
|
+
p.window.OTBProfileOwn.open();
|
|
47
|
+
await settle();
|
|
48
|
+
assert.ok(p.requests.some((u) => /\/tasks\/peer-votes\/feed/.test(u)), 'the peer-recognition feed is read');
|
|
49
|
+
assert.ok(p.requests.some((u) => /\/analytics\/builder\/95\?/.test(u)), 'your own curve is read');
|
|
50
|
+
const n = p.requests.length;
|
|
51
|
+
p.window.OTBProfileOwn.open();
|
|
52
|
+
await settle();
|
|
53
|
+
assert.equal(p.requests.length, n, 'opening it again reads nothing more');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('?id= naming yourself is still your own profile', async () => {
|
|
57
|
+
const p = bootProfilePage({ withOwn: true, search: '?id=95' });
|
|
58
|
+
await settle();
|
|
59
|
+
assert.match(p.el('rank-standing-body').innerHTML, /ranks-standing__title/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('someone else\'s profile reads and draws nothing private', async () => {
|
|
63
|
+
// The harness answers every profile with builder 95's payload, so this is the
|
|
64
|
+
// case that matters: the data alone would say "yours" — the REQUEST says not.
|
|
65
|
+
const p = bootProfilePage({ withOwn: true, search: '?id=2001' });
|
|
66
|
+
await settle();
|
|
67
|
+
p.window.OTBProfileOwn.open(); // even a forced open has no viewer to read for
|
|
68
|
+
await settle();
|
|
69
|
+
assert.equal(p.requests.filter((u) => OWN_READS.test(u)).length, 0, 'no private read is made');
|
|
70
|
+
assert.equal(p.el('rank-standing-body').innerHTML, '', 'where you stand is not drawn');
|
|
71
|
+
assert.equal(p.el('recent-grades tbody').innerHTML, '', 'no reviews are drawn');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('a signed-out visitor gets no "Just you" screen', async () => {
|
|
75
|
+
const p = bootProfilePage({ withOwn: true, signedIn: false, search: '?id=95' });
|
|
76
|
+
await settle();
|
|
77
|
+
assert.equal(p.requests.filter((u) => OWN_READS.test(u)).length, 0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('the screen starts hidden, rail key and all, and only profile.js opens it', () => {
|
|
81
|
+
const html = read('profile.html');
|
|
82
|
+
for (const id of ['rank-scroll', 'grades-scroll', 'karma-scroll', 'acclaim-scroll', 'analytics-scroll']) {
|
|
83
|
+
assert.match(html, new RegExp(`id="${id}"[^>]*data-group="yours" hidden>`), `#${id} starts hidden`);
|
|
84
|
+
}
|
|
85
|
+
const js = read('profile.js');
|
|
86
|
+
assert.match(js, /if \(group === 'yours' && window\.OTBProfileOwn && window\.OTBProfileOwn\.open\) window\.OTBProfileOwn\.open\(\);/, 'the feeds load when the screen first opens');
|
|
87
|
+
assert.match(js, /\(i\.id !== 'yours' \|\| ownShown\)/, 'the rail offers the key only once the screen is drawn');
|
|
88
|
+
assert.match(js, /if \(ownRequest && selfId != null && selfMe && window\.OTBProfileOwn\)/, 'drawn only for your own request');
|
|
89
|
+
const i = (s) => html.indexOf(s);
|
|
90
|
+
assert.ok(i('/builders/profile-own.js') > 0 && i('/builders/profile-own.js') < i('/builders/profile.js?'), 'loaded before profile.js');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('Home carries no Accomplishments view, and no renderer of what moved', () => {
|
|
94
|
+
assert.ok(!/id="view-standing"/.test(read('index.html')));
|
|
95
|
+
assert.ok(!/view: 'standing'/.test(read('hall-widgets.js')), 'no widget mounts into it');
|
|
96
|
+
for (const fn of ['renderRecentGrades', 'renderKarmaFeed', 'renderAcclaimFeed', 'loadAnalytics', 'renderCurveBody', 'renderRankStanding', 'acclaimEvidenceHtml']) {
|
|
97
|
+
assert.ok(!new RegExp(`function ${fn}\\b`).test(read('builders.js') + read('hall-render.js')), `${fn} moved, not copied`);
|
|
98
|
+
assert.match(read('profile-own.js'), new RegExp(`function ${fn}\\b`), `${fn} lives in profile-own.js`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
@@ -14,20 +14,20 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
14
14
|
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
|
|
15
15
|
const OLD = /\/builder-setup|setup-builder\.(sh|js)/;
|
|
16
16
|
|
|
17
|
-
// The
|
|
18
|
-
function
|
|
19
|
-
const src = read('modules/hall-ui/public/
|
|
20
|
-
const start = src.indexOf(
|
|
21
|
-
const end = src.indexOf(
|
|
22
|
-
assert.ok(start > 0 && end > start, '
|
|
17
|
+
// The setup screen's machine-linking step (task 1005076 replaced the Home welcome panel).
|
|
18
|
+
function setupStepMarkup() {
|
|
19
|
+
const src = read('modules/hall-ui/public/setup-gate.js');
|
|
20
|
+
const start = src.indexOf("if (step.id === 'cli_setup') {");
|
|
21
|
+
const end = src.indexOf("if (step.id === 'pick_disciplines')", start);
|
|
22
|
+
assert.ok(start > 0 && end > start, 'the setup screen\'s cli_setup step located');
|
|
23
23
|
return src.slice(start, end).split('\n').filter((l) => !/^\s*\/\//.test(l)).join('\n');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
test('
|
|
27
|
-
const w =
|
|
28
|
-
assert.match(w, /
|
|
29
|
-
assert.match(w, /\\nnpm install\\nnpx bongos setup
|
|
30
|
-
assert.doesNotMatch(w, OLD, 'no legacy setup command on the
|
|
26
|
+
test('setup screen: the steps and the clone block say npx bongos setup', () => {
|
|
27
|
+
const w = setupStepMarkup();
|
|
28
|
+
assert.match(w, /run <code>npx bongos setup<\/code>/);
|
|
29
|
+
assert.match(w, /\\nnpm install\\nnpx bongos setup<\/pre>/, 'clone block ends npm install + npx bongos setup');
|
|
30
|
+
assert.doesNotMatch(w, OLD, 'no legacy setup command on the setup screen');
|
|
31
31
|
assert.doesNotMatch(w, /On Windows, run/, 'one command on every OS — no Windows-only line');
|
|
32
32
|
assert.match(w, /nodejs\.org/, 'the tooling list names Node, which npm install needs');
|
|
33
33
|
});
|
|
@@ -51,7 +51,7 @@ test('primer: both routes sign in with npx bongos setup', () => {
|
|
|
51
51
|
|
|
52
52
|
test('the get-the-code block keeps one command per line (screen and copy)', () => {
|
|
53
53
|
const css = read('modules/hall-ui/public/style.css');
|
|
54
|
-
const m = /\.
|
|
54
|
+
const m = /\.setup-gate__code\s*\{([^}]*)\}/.exec(css);
|
|
55
55
|
assert.ok(m, 'scoped rule present');
|
|
56
56
|
assert.match(m[1], /white-space:\s*pre/, 'copy-on-click reads innerText, so pre is what keeps the line breaks');
|
|
57
57
|
assert.match(m[1], /overflow-y:\s*hidden/, 'a one-axis strip names both axes');
|
|
@@ -66,8 +66,8 @@ test('bin/bongos.js is committed executable, so npx leaves a fresh clone clean',
|
|
|
66
66
|
assert.match(entry, /^100755 /, `expected mode 100755, got: ${entry.trim()}`);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
test('
|
|
70
|
-
const src = read('modules/hall-ui/public/
|
|
69
|
+
test('the setup screen and checklist lead ask only for what a builder needs', () => {
|
|
70
|
+
const src = read('modules/hall-ui/public/setup-gate.js');
|
|
71
71
|
assert.doesNotMatch(src, /run the platform locally/, 'a builder does not run the platform');
|
|
72
72
|
assert.match(src, /Clone the project to your machine and run <code>npm install<\/code> in it\./);
|
|
73
73
|
const st = read('modules/onboarding/onboarding-state.js');
|