@bongos/core 1.23.10 → 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 +65 -35
- package/docs/adr/0337-the-hub-account-is-the-person-a-hall-account-is-their-seat-in-one-project.md +1 -0
- package/docs/copy-inventory.md +857 -816
- package/docs/copy-registry.json +1517 -1091
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +15 -3
- package/docs/page-readings.json +588 -576
- package/modules/public-landing/public/account.html +2 -1
- package/modules/public-landing/public/assets/hub-bar.js +4 -2
- 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 +124 -11
- package/modules/public-landing/public/community.states.json +3 -1
- package/modules/public-landing/public/connections-nosuggest.states.json +15 -0
- package/modules/public-landing/public/connections.html +23 -3
- package/modules/public-landing/public/connections.states.json +1 -0
- package/modules/public-landing/public/contact.html +2 -1
- package/modules/public-landing/public/guild.html +2 -1
- package/modules/public-landing/public/index.html +6 -2
- package/modules/public-landing/public/invitations.html +2 -1
- package/modules/public-landing/public/privacy.html +2 -1
- package/modules/public-landing/public/projects.html +4 -1
- package/modules/public-landing/public/terms.html +2 -1
- package/modules/ui-design/kit/fixtures/connections-nosuggest.json +12 -0
- 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 +12 -0
- package/src/module-api.js +1 -1
- package/tests/hub_account_page.mjs +7 -1
- package/tests/hub_community_page.mjs +149 -9
- package/tests/hub_connections_page.mjs +53 -4
- package/tests/hub_guild_page.mjs +1 -0
- package/tests/landing_legal_pages.mjs +7 -1
|
@@ -861,6 +861,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
861
861
|
<li class="bandCell"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li>
|
|
862
862
|
<li class="bandCell"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li>
|
|
863
863
|
<li class="bandCell"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li>
|
|
864
|
+
<li class="bandCell knowCell" hidden><a class="bandT" href="/connections#connSuggest">Builders you may know</a><span class="bandC">connections or projects in common</span></li>
|
|
864
865
|
</ul>
|
|
865
866
|
</div>
|
|
866
867
|
</div><!--apex-only:end-->
|
|
@@ -873,7 +874,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
873
874
|
<li class="bandCell"><a class="bandT" href="?view=map&sub=table" data-nav="map" data-sub="table">Table</a><span class="bandC">the same projects as rows</span></li>
|
|
874
875
|
<li class="bandCell"><a class="bandT" href="?view=mine" data-nav="mine">Your projects</a><span class="bandC">the ones you own</span></li>
|
|
875
876
|
<li class="bandCell"><a class="bandT" href="?view=new" data-nav="new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
876
|
-
<!--apex-only:start--><li class="bandCell bandFold"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li><!--apex-only:end-->
|
|
877
|
+
<!--apex-only:start--><li class="bandCell bandFold"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li><li class="bandCell bandFold knowCell" hidden><a class="bandT" href="/connections#connSuggest">Builders you may know</a><span class="bandC">connections or projects in common</span></li><!--apex-only:end-->
|
|
877
878
|
</ul>
|
|
878
879
|
<!-- About and Docs are site pages, not project views: they live in the
|
|
879
880
|
footer row with Privacy / Terms / Contact (owner, 2026-08-28; task 1003332). -->
|
|
@@ -11808,6 +11809,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
11808
11809
|
/* every Your-projects item a band carries (§6 asked for querySelectorAll so
|
|
11809
11810
|
a second data-nav="mine" item could never become a signed-out leak) */
|
|
11810
11811
|
document.querySelectorAll('[data-nav="mine"]').forEach(function (a) { (a.closest('.bandCell') || a).hidden = !signedIn; });
|
|
11812
|
+
/* Builders you may know reads your own connections (task 1004256) */
|
|
11813
|
+
document.querySelectorAll('.knowCell').forEach(function (c) { c.hidden = !signedIn; });
|
|
11811
11814
|
$('onlyMine').closest('.switch').hidden = !signedIn;
|
|
11812
11815
|
if (!signedIn) closeMenus();
|
|
11813
11816
|
if (!signedIn && $('onlyMine').checked) { $('onlyMine').checked = false; applyMapFilter(); }
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
<li class="bandCell"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li>
|
|
73
73
|
<li class="bandCell"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li>
|
|
74
74
|
<li class="bandCell"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li>
|
|
75
|
+
<li class="bandCell knowCell" hidden><a class="bandT" href="/connections#connSuggest">Builders you may know</a><span class="bandC">connections or projects in common</span></li>
|
|
75
76
|
</ul>
|
|
76
77
|
</div>
|
|
77
78
|
</div><!--apex-only:end-->
|
|
@@ -82,7 +83,7 @@
|
|
|
82
83
|
<li class="bandCell"><a class="bandT" href="/projects?view=map&sub=table">Table</a><span class="bandC">the same projects as rows</span></li>
|
|
83
84
|
<li class="bandCell" id="mineCell"><a class="bandT" href="/projects?view=mine">Your projects</a><span class="bandC">the ones you own</span></li>
|
|
84
85
|
<li class="bandCell"><a class="bandT" href="/projects?view=new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
85
|
-
<!--apex-only:start--><li class="bandCell bandFold"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li><!--apex-only:end-->
|
|
86
|
+
<!--apex-only:start--><li class="bandCell bandFold"><a class="bandT" href="/community">Leaderboard</a><span class="bandC">who earned the most, across every project</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/find">Find a builder</a><span class="bandC">anyone building here, by name or role</span></li><li class="bandCell bandFold"><a class="bandT" href="/community/recruiters">Open to recruiters</a><span class="bandC">who is open to recruiting, for project owners</span></li><li class="bandCell bandFold knowCell" hidden><a class="bandT" href="/connections#connSuggest">Builders you may know</a><span class="bandC">connections or projects in common</span></li><!--apex-only:end-->
|
|
86
87
|
</ul>
|
|
87
88
|
</div>
|
|
88
89
|
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The connections graph with nothing to suggest (task 1004256): one connection, nothing pending, no builder you may know. Opened by the Community menu's item (/connections#connSuggest), the panel says so rather than vanishing.",
|
|
3
|
+
"connections": [
|
|
4
|
+
{ "id": "41", "connected_at": "2026-09-30T10:00:00Z", "handle": "mira", "display_name": "Mira Okafor", "avatar_url": "/__kit/avatar.svg" }
|
|
5
|
+
],
|
|
6
|
+
"incoming": [],
|
|
7
|
+
"outgoing": [],
|
|
8
|
+
"suggestions": [],
|
|
9
|
+
"directory": [
|
|
10
|
+
{ "handle": "nadia", "display_name": "Nadia Haddad", "avatar_url": "/__kit/avatar.svg" }
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"_":"GET /my-projects for a builder who owns TWO projects (task 1002292): the community pages' Recruit door asks which project it recruits to.","owned":[{"client_id":"mercury","name":"Mercury","origin":"https://mercury.example.test","hall_origin":null,"membership_kind":"owner","joined_at":"2026-08-01T10:00:00.000Z","last_active":"2026-10-20T18:00:00.000Z"},{"client_id":"tidepool","name":"Tidepool","origin":"https://tidepool.example.test","hall_origin":null,"membership_kind":"owner","joined_at":"2026-09-02T10:00:00.000Z","last_active":"2026-10-24T18:00:00.000Z"}],"building":[],"applied":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"_":"GET /my-projects for a builder who owns ONE project (task 1002292): the community pages' Recruit door shows on every row and opens Mercury's hall invite box.","owned":[{"client_id":"mercury","name":"Mercury","origin":"https://mercury.example.test","hall_origin":null,"membership_kind":"owner","joined_at":"2026-08-01T10:00:00.000Z","last_active":"2026-10-20T18:00:00.000Z"}],"building":[{"client_id":"otb","name":"Example World","origin":"https://example.com","hall_origin":"https://builders.example.com","membership_kind":"member","joined_at":"2026-07-20T10:00:00.000Z","last_active":"2026-08-27T18:00:00.000Z"}],"applied":[]}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.12",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.23.
|
|
9
|
+
"version": "1.23.12",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.12",
|
|
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/release-notes.json
CHANGED
|
@@ -10447,5 +10447,17 @@
|
|
|
10447
10447
|
"id": "1003363",
|
|
10448
10448
|
"text": "cloudbongos.com now has a Community menu: a leaderboard of builders across every project (by week, month, year or all time, and by craft), a search for builders by name, and, for project owners, a list of builders who keep the"
|
|
10449
10449
|
}
|
|
10450
|
+
],
|
|
10451
|
+
"1.23.11": [
|
|
10452
|
+
{
|
|
10453
|
+
"id": "1004256",
|
|
10454
|
+
"text": "Signed-in builders now find 'Builders you may know' in the Community menu; it opens their suggestions on the Connections page, which says so plainly when there's no one to suggest yet."
|
|
10455
|
+
}
|
|
10456
|
+
],
|
|
10457
|
+
"1.23.12": [
|
|
10458
|
+
{
|
|
10459
|
+
"id": "1002292",
|
|
10460
|
+
"text": "Project owners can now press Recruit next to any builder on the community leaderboard or search, which opens their project's invite page with that builder's name already filled in."
|
|
10461
|
+
}
|
|
10450
10462
|
]
|
|
10451
10463
|
}
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.23.
|
|
84
|
+
const CORE_VERSION = '1.23.12'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -137,6 +137,7 @@ test('the bands say what the hub’s say and lead into /projects; Account is the
|
|
|
137
137
|
['Your projects', '/projects?view=mine'], ['New project', '/projects?view=new'],
|
|
138
138
|
// the Community menu's three sub-pages, folded into the band under 480px (task 1003363)
|
|
139
139
|
['Leaderboard', '/community'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
|
|
140
|
+
['Builders you may know', '/connections#connSuggest'],
|
|
140
141
|
], 'this page has no views of its own, so every item names /projects in full');
|
|
141
142
|
assert.deepEqual(bandLinks(bandOf(PAGE, 'menuProfile')), [
|
|
142
143
|
['My projects', '/projects?view=mine'], ['Invitations', '/invitations'], ['Connections', '/connections'], ['Account', '/account'], ['Sign out', '/api/bongos/auth/logout'],
|
|
@@ -213,6 +214,8 @@ function boot(answer) {
|
|
|
213
214
|
// The bar: the two triggers, and each band with its links in markup order. An
|
|
214
215
|
// item's cell is its <li>; only #mineCell carries an id, so only it can hide.
|
|
215
216
|
const trigs = [...els.values()].filter((e) => /\btrig\b/.test(e.attrs.class || ''));
|
|
217
|
+
// the signed-in-only "Builders you may know" cells (task 1004256), by class: a band and its phone fold
|
|
218
|
+
const knowCells = [...PAGE.matchAll(/<li class="bandCell(?: bandFold)? knowCell" hidden>/g)].map(() => ({ hidden: true }));
|
|
216
219
|
const bands = [...PAGE.matchAll(/<div class="menuBand" id="(\w+)"[\s\S]*?\n<\/div>/g)].map((m) => {
|
|
217
220
|
const band = els.get(m[1]);
|
|
218
221
|
band.items = [...m[0].matchAll(/<li class="bandCell"(?: id="(\w+)")?><a class="([^"]+)" href="([^"]+)"([^>]*)>([^<]+)<\/a>/g)].map((a) => ({
|
|
@@ -243,7 +246,7 @@ function boot(answer) {
|
|
|
243
246
|
});
|
|
244
247
|
const calls = [];
|
|
245
248
|
const dom = {
|
|
246
|
-
els, radios, calls, focused: null, assigned: null,
|
|
249
|
+
els, radios, calls, knowCells, focused: null, assigned: null,
|
|
247
250
|
active: () => document.activeElement,
|
|
248
251
|
// a click bubbles: the target's own listeners, its band's, then the document's
|
|
249
252
|
click(target, detail = 1) {
|
|
@@ -274,6 +277,7 @@ function boot(answer) {
|
|
|
274
277
|
},
|
|
275
278
|
querySelectorAll(sel) {
|
|
276
279
|
if (sel === '#profileForm [data-link]') return links;
|
|
280
|
+
if (sel === '.knowCell') return knowCells;
|
|
277
281
|
if (sel === '.trig') return trigs;
|
|
278
282
|
if (sel === '.menuBand') return bands;
|
|
279
283
|
const names = [...sel.matchAll(/input\[name="(\w+)"\]/g)].map((m) => m[1]);
|
|
@@ -311,6 +315,7 @@ test('signed out: a 401 opens the door and shows nothing else', async () => {
|
|
|
311
315
|
assert.equal(dom.els.get('who').hidden, true, 'no chip without a session');
|
|
312
316
|
assert.equal(dom.els.get('signinTop').hidden, false, 'the sign-in door takes its slot');
|
|
313
317
|
assert.equal(dom.els.get('mineCell').hidden, true, 'and the Projects band offers no Your projects');
|
|
318
|
+
assert.deepEqual(dom.knowCells.map((c) => c.hidden), [true, true], 'nor Builders you may know, in the Community band or its fold');
|
|
314
319
|
});
|
|
315
320
|
|
|
316
321
|
test('signed in: every control is painted from the server’s answer', async () => {
|
|
@@ -680,6 +685,7 @@ test('the chip is the hub’s identity read: GET /me fills the trigger and the p
|
|
|
680
685
|
assert.equal(dom.els.get(id).hidden, false);
|
|
681
686
|
}
|
|
682
687
|
assert.equal(dom.els.get('mineCell').hidden, false);
|
|
688
|
+
assert.deepEqual(dom.knowCells.map((c) => c.hidden), [false, false]);
|
|
683
689
|
});
|
|
684
690
|
|
|
685
691
|
test('an account with no avatar gets a chip with no picture rather than a broken one', async () => {
|
|
@@ -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'];
|
|
@@ -330,13 +443,17 @@ const ITEMS = [
|
|
|
330
443
|
['/community/find', 'Find a builder', 'anyone building here, by name or role'],
|
|
331
444
|
['/community/recruiters', 'Open to recruiters', 'who is open to recruiting, for project owners'],
|
|
332
445
|
];
|
|
446
|
+
// signed in only, and it opens /connections' own panel rather than a second one here (task 1004256)
|
|
447
|
+
const KNOW = '<a class="bandT" href="/connections#connSuggest">Builders you may know</a><span class="bandC">connections or projects in common</span></li>';
|
|
333
448
|
|
|
334
449
|
test('every page that wears the hub’s bar carries the Community menu before Projects; apex-only where the page serves a child', () => {
|
|
335
450
|
const { stripApexOnly } = require('../src/platform-server.js');
|
|
336
451
|
const trig = '<button class="trig commTrig" type="button" id="commTrig" data-menu="community" aria-expanded="false" aria-controls="menuCommunity"';
|
|
337
452
|
const band = ['<div class="menuBand" id="menuCommunity" data-menu="community" hidden>', ' <div class="bandIn">', ' <ul class="bandRow">',
|
|
338
|
-
...ITEMS.map(([h, t, c]) => ` <li class="bandCell"><a class="bandT" href="${h}">${t}</a><span class="bandC">${c}</span></li>`),
|
|
339
|
-
|
|
453
|
+
...ITEMS.map(([h, t, c]) => ` <li class="bandCell"><a class="bandT" href="${h}">${t}</a><span class="bandC">${c}</span></li>`),
|
|
454
|
+
` <li class="bandCell knowCell" hidden>${KNOW}`, ' </ul>', ' </div>', '</div>'].join('\n');
|
|
455
|
+
const fold = ITEMS.map(([h, t, c]) => `<li class="bandCell bandFold"><a class="bandT" href="${h}">${t}</a><span class="bandC">${c}</span></li>`).join('')
|
|
456
|
+
+ `<li class="bandCell bandFold knowCell" hidden>${KNOW}`;
|
|
340
457
|
for (const f of [...SERVES_A_CHILD, ...HUB_ONLY]) {
|
|
341
458
|
const html = read(f);
|
|
342
459
|
const apex = SERVES_A_CHILD.includes(f);
|
|
@@ -346,10 +463,20 @@ test('every page that wears the hub’s bar carries the Community menu before Pr
|
|
|
346
463
|
assert.ok(html.includes(wrap(apex, band)), `${f}: the Community band, its three sub-pages`);
|
|
347
464
|
assert.ok(html.indexOf('id="menuCommunity"') < html.indexOf('id="menuProjects"'), `${f}: the bands in the bar’s order`);
|
|
348
465
|
assert.ok(html.includes(wrap(apex, fold)), `${f}: the three folded into the Projects band for phones`);
|
|
349
|
-
if (apex) assert.doesNotMatch(stripApexOnly(html, false).split('<body>')[1], /\/community|commTrig|menuCommunity/, `${f}: a child has no community pages, so no trace of the menu`);
|
|
466
|
+
if (apex) assert.doesNotMatch(stripApexOnly(html, false).split('<body>')[1], /\/community|commTrig|menuCommunity|connSuggest/, `${f}: a child has no community pages, so no trace of the menu`);
|
|
350
467
|
}
|
|
351
468
|
});
|
|
352
469
|
|
|
470
|
+
test('Builders you may know: shown only once GET /me names someone, and drawn once, on /connections', () => {
|
|
471
|
+
// every bar script that paints the signed-in state shows or hides both cells with it
|
|
472
|
+
assert.match(read('assets/hub-bar.js'), /\$\('mineCell'\)\.hidden = !me;\n\s*Array\.prototype\.forEach\.call\(document\.querySelectorAll\('\.knowCell'\), function \(c\) \{ c\.hidden = !me; \}\);/);
|
|
473
|
+
assert.match(read('projects.html'), /function paintShellIdentity\(\) \{[\s\S]*?document\.querySelectorAll\('\.knowCell'\)\.forEach\(function \(c\) \{ c\.hidden = !signedIn; \}\);/);
|
|
474
|
+
assert.match(read('index.html'), /if \(mine\) mine\.hidden = false;\n\s*document\.querySelectorAll\('\.knowCell'\)\.forEach\(function \(c\) \{ c\.hidden = false; \}\);/);
|
|
475
|
+
// the panel it opens exists, and the community page reads no suggestions of its own
|
|
476
|
+
assert.match(read('connections.html'), /<section id="connSuggest" aria-labelledby="connSuggestH" hidden>/);
|
|
477
|
+
assert.doesNotMatch(read('community.html').split('<script>')[1], /connections\/suggestions/);
|
|
478
|
+
});
|
|
479
|
+
|
|
353
480
|
test('the menu folds into the Projects band under 480px on the hub and 900px on the landing; the arrows skip what CSS does not draw', () => {
|
|
354
481
|
const css = fs.readFileSync(path.join(LANDING, 'assets', 'cosmos.css'), 'utf8').replace(/\r\n/g, '\n');
|
|
355
482
|
assert.match(css, /\.trig\[aria-expanded="true"\],\.trig\[aria-current="true"\]\{color:var\(--ink\);text-decoration:underline;/, 'on its own pages the menu reads as the current section');
|
|
@@ -357,6 +484,10 @@ test('the menu folds into the Projects band under 480px on the hub and 900px on
|
|
|
357
484
|
assert.match(css.split('@media (max-width:480px){')[1].split('\n}')[0], /\.commTrig\{display:none;\}\s*\.bandFold\{display:block;\}/);
|
|
358
485
|
const index = read('index.html');
|
|
359
486
|
assert.match(index, /\n\.bandFold\{display:none\}\n/);
|
|
487
|
+
// a hidden fold cell (signed in only, task 1004256) must stay down where the fold shows:
|
|
488
|
+
// cosmos.css's [hidden] rule is !important; the landing has none, so it carries its own
|
|
489
|
+
assert.match(css, /\n\[hidden\]\{display:none !important;\}\n/);
|
|
490
|
+
assert.match(index, /\n\.bandFold\[hidden\]\{display:none\}\n/);
|
|
360
491
|
assert.match(index.split('@media (max-width:900px){')[1], /^[\s\S]*?\.commTrig\{display:none\}\s*\.bandFold\{display:block\}/);
|
|
361
492
|
for (const f of ['assets/hub-bar.js', 'projects.html', 'index.html']) {
|
|
362
493
|
assert.match(read(f), /return !a\.closest\('\[hidden\]'\) && a\.offsetParent !== null; \}\);/, `${f}: the arrows skip a cell CSS does not draw`);
|
|
@@ -419,11 +550,20 @@ test('the kit renders each sub-page for everyone, the empty board, and a project
|
|
|
419
550
|
assert.equal(states.states['out-find'].url, '/community/find');
|
|
420
551
|
assert.equal(states.states['in-recruiters'].url, '/community/recruiters');
|
|
421
552
|
assert.ok(states.states['in-recruiters'].expect.visible.includes('#recruitClosed'), 'signed in but not admitted: told, shown no one');
|
|
422
|
-
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']]) {
|
|
423
554
|
assert.equal(json(file).stub.fixtures[name], fixture);
|
|
424
555
|
assert.ok(fs.existsSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'fixtures', fixture + '.json')), `fixture ${fixture}`);
|
|
425
556
|
}
|
|
426
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');
|
|
427
567
|
});
|
|
428
568
|
|
|
429
569
|
function stub(env = {}) {
|
|
@@ -128,6 +128,7 @@ function el(tag) {
|
|
|
128
128
|
n.appendChild = (c) => { c.parent = n; n.children.push(c); return c; };
|
|
129
129
|
n.setAttribute = (k, v) => { n.attrs[k] = String(v); };
|
|
130
130
|
n.addEventListener = (t, f) => { (n._on[t] ||= []).push(f); };
|
|
131
|
+
n.scrollIntoView = () => { n.scrolled = (n.scrolled || 0) + 1; };
|
|
131
132
|
return n;
|
|
132
133
|
}
|
|
133
134
|
const settle = async () => { for (let i = 0; i < 60; i++) await Promise.resolve(); };
|
|
@@ -161,7 +162,7 @@ const GRAPH = () => ({
|
|
|
161
162
|
], count: 2 } },
|
|
162
163
|
});
|
|
163
164
|
|
|
164
|
-
async function boot({ me = { builder: { github_login: 'builder' } }, reads = GRAPH(), writes = [] } = {}) {
|
|
165
|
+
async function boot({ me = { builder: { github_login: 'builder' } }, reads = GRAPH(), writes = [], hash = '' } = {}) {
|
|
165
166
|
const ids = new Map();
|
|
166
167
|
for (const id of [...PAGE.matchAll(/ id="([^"]+)"/g)].map((m) => m[1])) {
|
|
167
168
|
const n = el('x');
|
|
@@ -180,15 +181,18 @@ async function boot({ me = { builder: { github_login: 'builder' } }, reads = GRA
|
|
|
180
181
|
return Promise.resolve({ ok: a.status >= 200 && a.status < 300, status: a.status, json: () => (a.body === undefined ? Promise.reject(new SyntaxError('no body')) : Promise.resolve(a.body)) });
|
|
181
182
|
};
|
|
182
183
|
const document = { getElementById: (id) => ids.get(id) || null, createElement: el };
|
|
183
|
-
const
|
|
184
|
-
const
|
|
184
|
+
const winOn = {};
|
|
185
|
+
const window = { HubBar: { paint: () => {}, ready: Promise.resolve(me) }, addEventListener: (t, f) => { (winOn[t] ||= []).push(f); } };
|
|
186
|
+
const location = { hash };
|
|
187
|
+
const sandbox = { window, document, fetch, location, console, Promise, JSON, String, Object, Array, URL, Date, isNaN, encodeURIComponent };
|
|
185
188
|
vm.createContext(sandbox);
|
|
186
189
|
vm.runInContext(SCRIPT, sandbox, { filename: 'connections.html' });
|
|
187
190
|
await settle();
|
|
188
191
|
const click = async (b) => { for (const f of b._on.click || []) f({}); await settle(); };
|
|
189
192
|
const key = async (n, k) => { const e = { key: k, preventDefault() { e.prevented = true; } }; for (const f of n._on.keydown || []) f(e); await settle(); return e; };
|
|
190
193
|
const gets = () => requests.filter((r) => r.method === 'GET').map((r) => r.url);
|
|
191
|
-
|
|
194
|
+
const hashTo = async (h) => { location.hash = h; for (const f of winOn.hashchange || []) f({}); await settle(); };
|
|
195
|
+
return { ids, requests, gets, click, key, hashTo, $: (id) => ids.get(id) };
|
|
192
196
|
}
|
|
193
197
|
const button = (row, label) => all(row, 'btn').find((b) => b.textContent === label);
|
|
194
198
|
|
|
@@ -283,6 +287,51 @@ test('a failed suggestions read drops only that panel', async () => {
|
|
|
283
287
|
assert.equal(kids(ui.$('connList')).length, 2);
|
|
284
288
|
});
|
|
285
289
|
|
|
290
|
+
test('opened by the Community menu’s item (#connSuggest): scrolled to once, and an empty panel says so', async () => {
|
|
291
|
+
// the panel draws after the browser's own jump to the fragment, so the page makes it, once
|
|
292
|
+
let ui = await boot({ hash: '#connSuggest', writes: [{ status: 201, body: { status: 'pending' } }] });
|
|
293
|
+
assert.equal(ui.$('connSuggest').hidden, false);
|
|
294
|
+
assert.equal(ui.$('suggestNone').hidden, true, 'suggestions need no explaining');
|
|
295
|
+
assert.equal(ui.$('connSuggest').scrolled, 1);
|
|
296
|
+
await ui.click(button(kids(ui.$('suggestList'))[0], 'Connect'));
|
|
297
|
+
assert.equal(ui.gets().length, 6);
|
|
298
|
+
assert.equal(ui.$('connSuggest').scrolled, 1, 'a re-read after Connect does not jump the page again');
|
|
299
|
+
|
|
300
|
+
const none = GRAPH();
|
|
301
|
+
none['/connections/suggestions'].body = { suggestions: [] };
|
|
302
|
+
ui = await boot({ hash: '#connSuggest', reads: none });
|
|
303
|
+
assert.equal(ui.$('connSuggest').hidden, false, 'asked for by name, the panel answers rather than vanishing');
|
|
304
|
+
assert.equal(ui.$('suggestNone').hidden, false);
|
|
305
|
+
assert.match(PAGE, /<p class="mineEmpty" id="suggestNone" hidden>No one to suggest yet\. Suggestions come from the connections and projects you have in common with other builders\.<\/p>/);
|
|
306
|
+
|
|
307
|
+
const failed = GRAPH();
|
|
308
|
+
failed['/connections/suggestions'] = { status: 500, body: {} };
|
|
309
|
+
ui = await boot({ hash: '#connSuggest', reads: failed });
|
|
310
|
+
assert.equal(ui.$('connSuggest').hidden, true, 'an unread list is not "no one to suggest"');
|
|
311
|
+
assert.equal(ui.$('connSuggest').scrolled, undefined);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('the menu item pressed while already on the page: the hash change is answered as an arrival', async () => {
|
|
315
|
+
const none = GRAPH();
|
|
316
|
+
none['/connections/suggestions'].body = { suggestions: [] };
|
|
317
|
+
let ui = await boot({ reads: none });
|
|
318
|
+
assert.equal(ui.$('connSuggest').hidden, true, 'a plain visit: no suggestions, no empty shell');
|
|
319
|
+
await ui.hashTo('#connSuggest');
|
|
320
|
+
assert.equal(ui.gets().length, 6, 'one re-read');
|
|
321
|
+
assert.equal(ui.$('connSuggest').hidden, false);
|
|
322
|
+
assert.equal(ui.$('suggestNone').hidden, false);
|
|
323
|
+
|
|
324
|
+
ui = await boot();
|
|
325
|
+
await ui.hashTo('#connSuggest');
|
|
326
|
+
assert.equal(ui.$('connSuggest').scrolled, 1, 'scrolled to once the panel is drawn');
|
|
327
|
+
await ui.hashTo('#connList');
|
|
328
|
+
assert.equal(ui.gets().length, 6, 'any other hash reads nothing');
|
|
329
|
+
|
|
330
|
+
ui = await boot({ me: null });
|
|
331
|
+
await ui.hashTo('#connSuggest');
|
|
332
|
+
assert.deepEqual(ui.requests, [], 'signed out, the door stands and nothing is read');
|
|
333
|
+
});
|
|
334
|
+
|
|
286
335
|
test('Accept, Decline, Remove and Withdraw: one call each, then the page re-reads', async () => {
|
|
287
336
|
const cases = [
|
|
288
337
|
['incomingList', 'Accept', 'POST', '/api/bongos/connections/51/accept', { status: 200, body: { status: 'accepted' } }, 'Connected.'],
|
package/tests/hub_guild_page.mjs
CHANGED
|
@@ -216,6 +216,7 @@ function boot({ pathname = '/g/forge-crew', me = null, answer = () => NOT_FOUND,
|
|
|
216
216
|
querySelectorAll(sel) {
|
|
217
217
|
if (sel === '.trig') return trigs;
|
|
218
218
|
if (sel === '.menuBand') return bands;
|
|
219
|
+
if (sel === '.knowCell') return [];
|
|
219
220
|
const m = sel.match(/^input\[name="(\w+)"\]$/);
|
|
220
221
|
if (!m) throw new Error(`unstubbed selector: ${sel}`);
|
|
221
222
|
return radios.filter((r) => r.name === m[1]);
|
|
@@ -195,6 +195,7 @@ for (const p of PAGES) {
|
|
|
195
195
|
['Your projects', '/projects?view=mine'], ['New project', '/projects?view=new'],
|
|
196
196
|
// the Community menu's three sub-pages, folded into the band under 480px (task 1003363); apex-only
|
|
197
197
|
['Leaderboard', '/community'], ['Find a builder', '/community/find'], ['Open to recruiters', '/community/recruiters'],
|
|
198
|
+
['Builders you may know', '/connections#connSuggest'],
|
|
198
199
|
], 'this page has no views of its own, so every item names /projects in full');
|
|
199
200
|
assert.deepEqual(bandLinks(bandOf(stripApexOnly(html, false), 'menuProjects')).map((l) => l[0]), ['Sky', 'Table', 'Your projects', 'New project'], 'a child has no /community');
|
|
200
201
|
assert.deepEqual(bandLinks(bandOf(html, 'menuProfile')), [
|
|
@@ -264,6 +265,8 @@ function bootBar(html, answer, pathname) {
|
|
|
264
265
|
els.set(m[3], el(m[3], /\shidden\b/.test(m[2] + m[4]), attrs));
|
|
265
266
|
}
|
|
266
267
|
const trigs = [...els.values()].filter((e) => /\btrig\b/.test(e.attrs.class || ''));
|
|
268
|
+
// the signed-in-only "Builders you may know" cells (task 1004256), by class; a child has none
|
|
269
|
+
const knowCells = [...html.matchAll(/<li class="bandCell(?: bandFold)? knowCell" hidden>/g)].map(() => ({ hidden: true }));
|
|
267
270
|
const bands = [...html.matchAll(/<div class="menuBand" id="(\w+)"[\s\S]*?\n<\/div>/g)].map((m) => {
|
|
268
271
|
const band = els.get(m[1]);
|
|
269
272
|
band.items = [...m[0].matchAll(/<li class="bandCell"(?: id="(\w+)")?><a class="([^"]+)" href="([^"]+)"([^>]*)>([^<]+)<\/a>/g)].map((a) => ({
|
|
@@ -287,7 +290,7 @@ function bootBar(html, answer, pathname) {
|
|
|
287
290
|
const event = (extra) => ({ prevented: false, preventDefault() { this.prevented = true; }, ...extra });
|
|
288
291
|
const calls = [];
|
|
289
292
|
const dom = {
|
|
290
|
-
els, calls, assigned: null,
|
|
293
|
+
els, calls, knowCells, assigned: null,
|
|
291
294
|
active: () => document.activeElement,
|
|
292
295
|
band: (id) => els.get(id),
|
|
293
296
|
item: (id, text) => els.get(id).items.find((i) => i.text === text),
|
|
@@ -312,6 +315,7 @@ function bootBar(html, answer, pathname) {
|
|
|
312
315
|
querySelectorAll(sel) {
|
|
313
316
|
if (sel === '.trig') return trigs;
|
|
314
317
|
if (sel === '.menuBand') return bands;
|
|
318
|
+
if (sel === '.knowCell') return knowCells;
|
|
315
319
|
throw new Error(`unstubbed selector: ${sel}`);
|
|
316
320
|
},
|
|
317
321
|
};
|
|
@@ -341,6 +345,7 @@ for (const p of PAGES) {
|
|
|
341
345
|
assert.equal(out.els.get('who').hidden, true, 'no chip without a session');
|
|
342
346
|
assert.equal(out.els.get('signinTop').hidden, false);
|
|
343
347
|
assert.equal(out.els.get('mineCell').hidden, true, 'and the Projects band offers no Your projects');
|
|
348
|
+
assert.deepEqual(out.knowCells.map((c) => c.hidden), hub ? [true, true] : [], 'nor Builders you may know; a child has no community menu at all');
|
|
344
349
|
assert.equal(await out.window.HubBar.ready, null);
|
|
345
350
|
|
|
346
351
|
const dom = bootBar(served, signedIn, p.route);
|
|
@@ -352,6 +357,7 @@ for (const p of PAGES) {
|
|
|
352
357
|
assert.equal(dom.els.get('who').getAttribute('aria-label'), 'Account, octocat');
|
|
353
358
|
for (const id of ['whoImg', 'bandWhoImg']) assert.equal(dom.els.get(id).src, 'https://avatars.example/u/1');
|
|
354
359
|
assert.equal(dom.els.get('mineCell').hidden, false);
|
|
360
|
+
assert.deepEqual(dom.knowCells.map((c) => c.hidden), hub ? [false, false] : []);
|
|
355
361
|
assert.deepEqual(dom.band('menuProfile').items.map((i) => i.text), hub ? ['My projects', 'Invitations', 'Connections', 'Account', 'Sign out'] : ['My projects', 'Sign out']);
|
|
356
362
|
});
|
|
357
363
|
}
|