@bongos/core 1.23.11 → 1.23.12
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 +38 -18
- package/docs/copy-inventory.md +70 -70
- package/docs/copy-registry.json +83 -83
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +7 -1
- package/docs/page-readings.json +63 -57
- package/modules/public-landing/public/community-owner-two.states.json +16 -0
- package/modules/public-landing/public/community-owner.states.json +16 -0
- package/modules/public-landing/public/community.html +122 -10
- package/modules/ui-design/kit/fixtures/my-projects-owner-two.json +1 -0
- package/modules/ui-design/kit/fixtures/my-projects-owner.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/hub_community_page.mjs +128 -6
|
@@ -78,6 +78,7 @@ test('no top block: one visually hidden h1, then one section per sub-page, only
|
|
|
78
78
|
assert.match(MAIN, /<ol class="commRows" id="boardList" data-unit="\{\{currency\}\}" aria-labelledby="boardH"><\/ol>/, 'the unit is the instance’s own word, stamped by the server');
|
|
79
79
|
assert.match(MAIN, /<p class="projMsg err" id="boardFailed" role="alert" hidden><\/p>/);
|
|
80
80
|
assert.match(MAIN, /<form class="mineJoinRow commFind" id="findForm" role="search"/);
|
|
81
|
+
for (const id of ['boardFor', 'findFor']) assert.match(MAIN, new RegExp(`<div class="commFor" id="${id}" hidden></div>`), `${id}: the Recruit door’s line waits for GET /my-projects (task 1002292)`);
|
|
81
82
|
assert.match(MAIN, /<h2 id="recruitH">Open to recruiters<\/h2>/, 'the hide flag is "hidden", never "private" (ADR 0337)');
|
|
82
83
|
assert.match(MAIN, /<a class="btn" id="recruitSignin" href="\/api\/bongos\/auth\/web\/start\?return=\/community\/recruiters&repos=1">/);
|
|
83
84
|
for (const id of ['recruitDoor', 'recruitClosed', 'recruitFailed', 'recruitEmpty']) assert.match(MAIN, new RegExp(` id="${id}"[^>]*hidden`), `${id} waits for the route’s answer`);
|
|
@@ -100,11 +101,12 @@ test('the window dropdown and the query limits are the routes’ own (boundary-f
|
|
|
100
101
|
assert.match(MAIN, new RegExp(`<input id="findQ" type="search"[^>]* maxlength="${max}"`));
|
|
101
102
|
});
|
|
102
103
|
|
|
103
|
-
test('the script builds no markup from strings, reads only the
|
|
104
|
+
test('the script builds no markup from strings, reads only the four routes, and never re-ranks or reads the ranked tier', () => {
|
|
104
105
|
assert.doesNotMatch(SCRIPT, /innerHTML|insertAdjacentHTML|outerHTML|document\.write/);
|
|
105
|
-
// every path literal in the code: the API base, the
|
|
106
|
+
// every path literal in the code: the API base, the four reads, the three sub-pages, the
|
|
107
|
+
// sign-in door and the hall's invite box the Recruit door opens (task 1002292)
|
|
106
108
|
const paths = [...new Set([...CODE.matchAll(/'(\/[^'?]*)/g)].map((m) => m[1]))];
|
|
107
|
-
assert.deepEqual(paths.sort(), ['/api/bongos', '/api/bongos/auth/web/start', '/community', '/community/find', '/community/leaderboard', '/community/recruiters', '/community/search', '/scouting']);
|
|
109
|
+
assert.deepEqual(paths.sort(), ['/api/bongos', '/api/bongos/auth/web/start', '/builders/watch', '/community', '/community/find', '/community/leaderboard', '/community/recruiters', '/community/search', '/my-projects', '/scouting']);
|
|
108
110
|
assert.doesNotMatch(CODE, /method:/, 'it only reads');
|
|
109
111
|
assert.doesNotMatch(CODE, /'\/me\b/, 'who is signed in is the bar’s question');
|
|
110
112
|
assert.doesNotMatch(CODE, /\.sort\(|\.count\b|_count\b/, 'the server ranks; every count is a list’s length');
|
|
@@ -173,7 +175,9 @@ async function boot({ pathname = '/community', me = null, reads = READS() } = {}
|
|
|
173
175
|
requests.push({ url, credentials: init.credentials });
|
|
174
176
|
const a = reads[url.replace(/^\/api\/bongos/, '')];
|
|
175
177
|
if (!a || a.reject) return Promise.reject(new Error('offline'));
|
|
176
|
-
|
|
178
|
+
const res = { ok: a.status >= 200 && a.status < 300, status: a.status, json: () => Promise.resolve(a.body) };
|
|
179
|
+
// `slow`: answers on a timer, after every microtask-settled read (the order a slow network gives)
|
|
180
|
+
return a.slow ? new Promise((resolve) => setTimeout(() => resolve(res), 5)) : Promise.resolve(res);
|
|
177
181
|
};
|
|
178
182
|
const document = {
|
|
179
183
|
title: 'Cloud Bongos — Community',
|
|
@@ -225,7 +229,8 @@ test('the leaderboard, signed out: the all-time board in the server’s order wi
|
|
|
225
229
|
assert.equal(find(bo, 'mineRow__hint').textContent, '1 work shipped · 0 karma');
|
|
226
230
|
assert.equal(ui.$('boardNote').textContent, 'Credits earned across every project, since each builder started.');
|
|
227
231
|
const signedIn = await boot({ me: { github_login: 'someone' } });
|
|
228
|
-
assert.deepEqual(signedIn.gets(), ['/api/bongos/community/leaderboard?window=all'], 'signed in, the same
|
|
232
|
+
assert.deepEqual(signedIn.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/my-projects'], 'signed in, the same board read: viewing is ungated, and the one more read is the Recruit door’s');
|
|
233
|
+
assert.equal(signedIn.$('boardList').textContent, ui.$('boardList').textContent, 'the same rows for every viewer');
|
|
229
234
|
});
|
|
230
235
|
|
|
231
236
|
test('the window dropdown and a craft are each one read; the pressed craft follows the pick', async () => {
|
|
@@ -320,6 +325,114 @@ test('recruiters: a door signed out, a refusal on a 403, rows only from a 200 as
|
|
|
320
325
|
assert.equal(failed.$('recruitFailed').textContent, 'This list could not be read. Reload the page to try again.');
|
|
321
326
|
});
|
|
322
327
|
|
|
328
|
+
// ── 2b. the Recruit door (task 1002292; spec D6, owner 2026-10-27) ───────────
|
|
329
|
+
// A project OWNER gets a Recruit link on each row: the hub cannot read a project's
|
|
330
|
+
// `project.recruit` (ADR 0335 D1), so GET /my-projects' `owned` stands in. The link is the
|
|
331
|
+
// project's own hall invite box with the GitHub login filled in (ADR 0353), and the
|
|
332
|
+
// project decides.
|
|
333
|
+
|
|
334
|
+
const OWNED = (...ps) => ({ status: 200, body: {
|
|
335
|
+
owned: ps.map(([client_id, origin, name]) => ({ client_id, name, origin, hall_origin: null, membership_kind: 'owner' })),
|
|
336
|
+
building: [{ client_id: 'zed', name: 'Zed', origin: 'https://zed.example', hall_origin: null, membership_kind: 'member' }],
|
|
337
|
+
applied: [],
|
|
338
|
+
} });
|
|
339
|
+
const recruitOf = (row) => find(row, 'commRecruit');
|
|
340
|
+
|
|
341
|
+
test('recruit: an owner gets a Recruit link on every row but their own, opening their project’s hall invite box with the GitHub login filled in', async () => {
|
|
342
|
+
const reads = READS();
|
|
343
|
+
reads['/my-projects'] = OWNED(['mercury', 'https://mercury.example/x?y=1', 'Mercury']);
|
|
344
|
+
const ui = await boot({ me: { github_login: 'BO' }, reads });
|
|
345
|
+
assert.deepEqual(ui.gets(), ['/api/bongos/community/leaderboard?window=all', '/api/bongos/my-projects']);
|
|
346
|
+
const [ana, bo] = kids(ui.$('boardList'));
|
|
347
|
+
const a = recruitOf(ana);
|
|
348
|
+
assert.equal(a.tag, 'a');
|
|
349
|
+
assert.equal(a.textContent, 'Recruit');
|
|
350
|
+
assert.equal(a.attrs.href, 'https://mercury.example/builders/watch?invite=ana', 'the project’s origin and nothing else of the address');
|
|
351
|
+
assert.equal(a.attrs.target, '_blank', 'the board stays open behind it');
|
|
352
|
+
assert.equal(a.attrs.rel, 'noopener');
|
|
353
|
+
assert.equal(a.attrs['aria-label'], 'Recruit ANA to Mercury (opens in a new tab)', 'the new tab is said, not sprung');
|
|
354
|
+
assert.equal(recruitOf(bo).tag, 'span', 'your own row: an invisible twin keeps the credits in line');
|
|
355
|
+
assert.equal(recruitOf(bo).attrs['aria-hidden'], 'true');
|
|
356
|
+
assert.match(ui.$('boardList').className, /\bcanRecruit\b/);
|
|
357
|
+
assert.equal(ui.$('boardFor').hidden, false);
|
|
358
|
+
assert.equal(ui.$('boardFor').textContent, 'Recruit opens Mercury’s hall with their name filled in, and you send the invite there.');
|
|
359
|
+
await ui.pick('30d');
|
|
360
|
+
assert.equal(recruitOf(kids(ui.$('boardList'))[0]).attrs.href, 'https://mercury.example/builders/watch?invite=cy', 'a new window’s rows carry it too');
|
|
361
|
+
reads['/community/leaderboard?window=all'].slow = true;
|
|
362
|
+
const late = await boot({ me: { github_login: 'BO' }, reads });
|
|
363
|
+
assert.equal(kids(late.$('boardList')).length, 0, 'the board has not answered yet');
|
|
364
|
+
assert.equal(late.$('boardFor').hidden, false, 'the owner’s line is up before the board');
|
|
365
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
366
|
+
await settle();
|
|
367
|
+
assert.equal(recruitOf(kids(late.$('boardList'))[0]).attrs.href, 'https://mercury.example/builders/watch?invite=ana', 'a board that answers after GET /my-projects draws its doors as it paints');
|
|
368
|
+
delete reads['/community/leaderboard?window=all'].slow;
|
|
369
|
+
reads['/community/leaderboard?window=all'].body.results[0].github_login = 'a b&c';
|
|
370
|
+
const odd = await boot({ me: { github_login: 'x' }, reads });
|
|
371
|
+
assert.equal(recruitOf(kids(odd.$('boardList'))[0]).attrs.href, 'https://mercury.example/builders/watch?invite=a%20b%26c', 'the login is encoded into the query');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test('recruit: none for a builder who owns no project, signed out, on a failed read, or for a project with no web address; the rows stay the same', async () => {
|
|
375
|
+
const reads = READS();
|
|
376
|
+
reads['/my-projects'] = OWNED();
|
|
377
|
+
const member = await boot({ me: { github_login: 'someone' }, reads });
|
|
378
|
+
assert.equal(member.gets().pop(), '/api/bongos/my-projects');
|
|
379
|
+
assert.ok(kids(member.$('boardList')).every((r) => !recruitOf(r)), 'building on a project is not owning one');
|
|
380
|
+
assert.equal(member.$('boardFor').hidden, true);
|
|
381
|
+
assert.doesNotMatch(member.$('boardList').className, /canRecruit/);
|
|
382
|
+
const out = await boot({ reads });
|
|
383
|
+
assert.deepEqual(out.gets(), ['/api/bongos/community/leaderboard?window=all'], 'signed out, nobody is asked about projects');
|
|
384
|
+
for (const answer of [{ status: 500, body: {} }, { status: 401, body: {} }, { reject: true }, OWNED(['evil', 'javascript:alert(1)', 'Evil'], ['none', null, 'None'])]) {
|
|
385
|
+
reads['/my-projects'] = answer;
|
|
386
|
+
const ui = await boot({ me: { github_login: 'someone' }, reads });
|
|
387
|
+
assert.ok(kids(ui.$('boardList')).every((r) => !recruitOf(r)), 'no door, and the board is drawn all the same');
|
|
388
|
+
assert.equal(kids(ui.$('boardList')).length, 2);
|
|
389
|
+
assert.equal(ui.$('boardFailed').hidden, true, 'a failed projects read is never the board’s failure');
|
|
390
|
+
assert.equal(ui.$('boardFor').hidden, true);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test('recruit: several projects are one “Recruit to” choice, and picking one re-points every row', async () => {
|
|
395
|
+
const reads = READS();
|
|
396
|
+
reads['/my-projects'] = OWNED(['mercury', 'https://mercury.example', 'Mercury'], ['venus', 'http://venus.example:8080', 'Venus']);
|
|
397
|
+
const ui = await boot({ me: { github_login: 'nobody' }, reads });
|
|
398
|
+
const pick = find(ui.$('boardFor'), 'commForPick');
|
|
399
|
+
assert.equal(pick.tag, 'select');
|
|
400
|
+
assert.equal(pick.id, 'boardForPick');
|
|
401
|
+
assert.deepEqual(kids(pick).map((o) => [o.value, o.textContent]), [['0', 'Mercury'], ['1', 'Venus']]);
|
|
402
|
+
assert.equal(kids(ui.$('boardFor'))[0].attrs.for, 'boardForPick', 'the choice is labelled');
|
|
403
|
+
assert.match(ui.$('boardFor').textContent, /^Recruit to/);
|
|
404
|
+
assert.equal(recruitOf(kids(ui.$('boardList'))[0]).attrs.href, 'https://mercury.example/builders/watch?invite=ana', 'the first project, as the server listed them');
|
|
405
|
+
pick.value = '1';
|
|
406
|
+
for (const f of pick._on.change || []) f({});
|
|
407
|
+
await settle();
|
|
408
|
+
const [ana, bo] = kids(ui.$('boardList'));
|
|
409
|
+
assert.equal(recruitOf(ana).attrs.href, 'http://venus.example:8080/builders/watch?invite=ana');
|
|
410
|
+
assert.equal(recruitOf(bo).attrs['aria-label'], 'Recruit bo to Venus (opens in a new tab)');
|
|
411
|
+
assert.equal(find(ui.$('boardFor'), 'commForPick'), pick, 'the choice is not redrawn, so it keeps the focus');
|
|
412
|
+
pick.value = '7';
|
|
413
|
+
for (const f of pick._on.change || []) f({});
|
|
414
|
+
assert.equal(recruitOf(kids(ui.$('boardList'))[0]).attrs.href, 'http://venus.example:8080/builders/watch?invite=ana', 'a value it never offered changes nothing');
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('recruit: found builders carry it too; the recruiter list never asks', async () => {
|
|
418
|
+
const reads = READS();
|
|
419
|
+
reads['/my-projects'] = OWNED(['mercury', 'https://mercury.example', 'Mercury']);
|
|
420
|
+
const ui = await boot({ pathname: '/community/find', me: { github_login: 'ph' }, reads });
|
|
421
|
+
assert.deepEqual(ui.gets(), ['/api/bongos/my-projects'], 'nothing searched until asked');
|
|
422
|
+
assert.equal(ui.$('findFor').hidden, false);
|
|
423
|
+
assert.equal(ui.$('findFor').textContent, 'Recruit opens Mercury’s hall with their name filled in, and you send the invite there.');
|
|
424
|
+
assert.equal(ui.$('boardFor').hidden, true, 'only the shown sub-page’s line');
|
|
425
|
+
ui.$('findQ').value = 'pri';
|
|
426
|
+
await ui.submit();
|
|
427
|
+
const [priya, hidden] = kids(ui.$('findList'));
|
|
428
|
+
const acts = find(priya, 'mineRow__actions');
|
|
429
|
+
assert.equal(recruitOf(acts).attrs.href, 'https://mercury.example/builders/watch?invite=priyan', 'by GitHub login, never the handle');
|
|
430
|
+
assert.equal(recruitOf(hidden), null, 'your own row: no door, and no twin in a row that keeps no columns');
|
|
431
|
+
assert.equal(ui.$('findStatus').textContent, '2 builders found.');
|
|
432
|
+
const recruiters = await boot({ pathname: '/community/recruiters', me: { github_login: 'owner' }, reads });
|
|
433
|
+
assert.deepEqual(recruiters.gets(), ['/api/bongos/scouting'], 'the recruiter list draws no Recruit door');
|
|
434
|
+
});
|
|
435
|
+
|
|
323
436
|
// ── 3. every bar carries the Community menu ──────────────────────────────────
|
|
324
437
|
|
|
325
438
|
const SERVES_A_CHILD = ['privacy.html', 'terms.html', 'contact.html', 'index.html', 'projects.html'];
|
|
@@ -437,11 +550,20 @@ test('the kit renders each sub-page for everyone, the empty board, and a project
|
|
|
437
550
|
assert.equal(states.states['out-find'].url, '/community/find');
|
|
438
551
|
assert.equal(states.states['in-recruiters'].url, '/community/recruiters');
|
|
439
552
|
assert.ok(states.states['in-recruiters'].expect.visible.includes('#recruitClosed'), 'signed in but not admitted: told, shown no one');
|
|
440
|
-
for (const [file, name, fixture] of [['community-empty.states.json', 'community-leaderboard', 'community-leaderboard-empty'], ['community-recruiter.states.json', 'scouting', 'scouting-recruiter']]) {
|
|
553
|
+
for (const [file, name, fixture] of [['community-empty.states.json', 'community-leaderboard', 'community-leaderboard-empty'], ['community-recruiter.states.json', 'scouting', 'scouting-recruiter'], ['community-owner.states.json', 'my-projects', 'my-projects-owner'], ['community-owner-two.states.json', 'my-projects', 'my-projects-owner-two']]) {
|
|
441
554
|
assert.equal(json(file).stub.fixtures[name], fixture);
|
|
442
555
|
assert.ok(fs.existsSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', fixture + '.json')), `fixture ${fixture}`);
|
|
443
556
|
}
|
|
444
557
|
assert.equal(json('community-recruiter.states.json').states['in-recruiter'].url, '/community/recruiters');
|
|
558
|
+
// the Recruit door (task 1002292): an owner of one project on the board and the search, and of two with the choice
|
|
559
|
+
assert.ok(json('community-owner.states.json').states['in-owner'].expect.count.some(([sel, n]) => sel === '#boardList a.commRecruit' && n === 6));
|
|
560
|
+
assert.equal(json('community-owner.states.json').states['in-owner-find'].url, '/community/find');
|
|
561
|
+
assert.ok(json('community-owner-two.states.json').states['in-owner-two-picked'].actions.some((a) => a[0] === 'select' && a[1] === '#boardForPick'));
|
|
562
|
+
for (const f of ['my-projects-owner', 'my-projects-owner-two']) {
|
|
563
|
+
const owned = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', f + '.json'), 'utf8')).owned;
|
|
564
|
+
assert.ok(owned.length && owned.every((p) => p.membership_kind === 'owner' && /^https:\/\//.test(p.origin)), `${f}: owned projects with web addresses`);
|
|
565
|
+
}
|
|
566
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', 'my-projects.json'), 'utf8')).owned, [], 'the default builder owns nothing, so every other state draws no door');
|
|
445
567
|
});
|
|
446
568
|
|
|
447
569
|
function stub(env = {}) {
|