@bongos/core 1.23.10 → 1.23.11

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.
Files changed (31) hide show
  1. package/.bongos-core.json +45 -35
  2. package/docs/adr/0337-the-hub-account-is-the-person-a-hall-account-is-their-seat-in-one-project.md +1 -0
  3. package/docs/copy-inventory.md +847 -806
  4. package/docs/copy-registry.json +1499 -1073
  5. package/docs/module-api-changelog.md +2 -0
  6. package/docs/page-inventory.json +9 -3
  7. package/docs/page-readings.json +581 -575
  8. package/modules/public-landing/public/account.html +2 -1
  9. package/modules/public-landing/public/assets/hub-bar.js +4 -2
  10. package/modules/public-landing/public/community.html +2 -1
  11. package/modules/public-landing/public/community.states.json +3 -1
  12. package/modules/public-landing/public/connections-nosuggest.states.json +15 -0
  13. package/modules/public-landing/public/connections.html +23 -3
  14. package/modules/public-landing/public/connections.states.json +1 -0
  15. package/modules/public-landing/public/contact.html +2 -1
  16. package/modules/public-landing/public/guild.html +2 -1
  17. package/modules/public-landing/public/index.html +6 -2
  18. package/modules/public-landing/public/invitations.html +2 -1
  19. package/modules/public-landing/public/privacy.html +2 -1
  20. package/modules/public-landing/public/projects.html +4 -1
  21. package/modules/public-landing/public/terms.html +2 -1
  22. package/modules/ui-design/kit/fixtures/connections-nosuggest.json +12 -0
  23. package/package-lock.json +2 -2
  24. package/package.json +1 -1
  25. package/release-notes.json +6 -0
  26. package/src/module-api.js +1 -1
  27. package/tests/hub_account_page.mjs +7 -1
  28. package/tests/hub_community_page.mjs +21 -3
  29. package/tests/hub_connections_page.mjs +53 -4
  30. package/tests/hub_guild_page.mjs +1 -0
  31. package/tests/landing_legal_pages.mjs +7 -1
@@ -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 window = { HubBar: { paint: () => {}, ready: Promise.resolve(me) } };
184
- const sandbox = { window, document, fetch, console, Promise, JSON, String, Object, Array, URL, Date, isNaN, encodeURIComponent };
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
- return { ids, requests, gets, click, key, $: (id) => ids.get(id) };
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.'],
@@ -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
  }