@bongos/core 1.21.51 → 1.21.53
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 +88 -73
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +7 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +135 -3
- package/docs/api-reference.md +4 -2
- package/docs/copy-inventory.md +195 -150
- package/docs/copy-registry.json +740 -281
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +8 -4
- package/docs/page-readings.json +336 -333
- package/modules/hall-ui/public/atlas.html +3 -3
- package/modules/hall-ui/public/blockers.html +3 -3
- package/modules/hall-ui/public/board-room.html +3 -3
- package/modules/hall-ui/public/city.html +3 -3
- package/modules/hall-ui/public/collab.html +3 -3
- package/modules/hall-ui/public/commands.html +3 -3
- package/modules/hall-ui/public/copy-desk.html +3 -3
- package/modules/hall-ui/public/craft-mode.js +7 -1
- package/modules/hall-ui/public/deploy.html +3 -3
- package/modules/hall-ui/public/diagrams.html +3 -3
- package/modules/hall-ui/public/drachmae.html +3 -3
- package/modules/hall-ui/public/fleet.html +3 -3
- package/modules/hall-ui/public/gate.html +3 -3
- package/modules/hall-ui/public/goal-map.html +3 -3
- package/modules/hall-ui/public/goals.html +3 -3
- package/modules/hall-ui/public/government.html +3 -3
- package/modules/hall-ui/public/idea.html +3 -3
- package/modules/hall-ui/public/ideas.html +3 -3
- package/modules/hall-ui/public/index.html +3 -3
- package/modules/hall-ui/public/lightbulb.js +213 -0
- package/modules/hall-ui/public/modules.html +3 -3
- package/modules/hall-ui/public/primer.html +3 -3
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/project-settings.html +3 -3
- package/modules/hall-ui/public/ranks.html +3 -3
- package/modules/hall-ui/public/roadmap.html +3 -3
- package/modules/hall-ui/public/roster.html +3 -3
- package/modules/hall-ui/public/sessions.html +3 -3
- package/modules/hall-ui/public/settings.html +3 -3
- package/modules/hall-ui/public/street.html +3 -3
- package/modules/hall-ui/public/studio.html +3 -3
- package/modules/hall-ui/public/style.css +1 -0
- package/modules/hall-ui/public/task.html +3 -3
- package/modules/hall-ui/public/thinking.html +3 -3
- package/modules/hall-ui/public/tweak-editor.html +3 -3
- package/modules/hall-ui/public/watch.html +3 -3
- package/modules/hall-ui/public/work.html +3 -3
- package/modules/hall-ui/records/craft-mode.md +8 -0
- package/modules/page-notes/db.js +34 -0
- package/modules/page-notes/notes.js +54 -8
- package/modules/page-notes/routes/page-notes.js +49 -1
- package/modules/page-notes/split.js +39 -8
- package/modules/public-landing/public/account.html +12 -101
- package/modules/public-landing/public/assets/hub-bar.js +127 -0
- package/modules/public-landing/public/contact.html +60 -9
- package/modules/public-landing/public/privacy.html +60 -9
- package/modules/public-landing/public/terms.html +60 -9
- 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/hall_tweak_editor.mjs +1 -1
- package/tests/hub_account_page.mjs +22 -5
- package/tests/landing_legal_pages.mjs +275 -0
- package/tests/lightbulb.mjs +385 -0
- package/tests/page_notes_panel.mjs +3 -1
- package/tests/paintbrush.mjs +2 -1
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
// the builders/status hosts), they are PORTABLE (server-stamped, no baked apex or
|
|
8
8
|
// repo, apex-only claims removed off the hub), and BOTH footers link them.
|
|
9
9
|
//
|
|
10
|
+
// They also wear the hub's bar (task 1004489): the same markup as projects.html
|
|
11
|
+
// and account.html, and assets/hub-bar.js EXECUTED against each page's served
|
|
12
|
+
// shape on the hub and on a child instance.
|
|
13
|
+
//
|
|
10
14
|
// DB-free: static HTML through the same transforms landingPage() applies, plus
|
|
11
15
|
// the platform app over an ephemeral listener (the public_landing_projects shape).
|
|
12
16
|
//
|
|
@@ -17,6 +21,7 @@ import { test, before, after } from 'node:test';
|
|
|
17
21
|
import http from 'node:http';
|
|
18
22
|
import fs from 'node:fs';
|
|
19
23
|
import path from 'node:path';
|
|
24
|
+
import vm from 'node:vm';
|
|
20
25
|
import { fileURLToPath } from 'node:url';
|
|
21
26
|
import { createRequire } from 'node:module';
|
|
22
27
|
|
|
@@ -126,6 +131,276 @@ test('cosmos.css carries the footer-nav rule beside .foot a', () => {
|
|
|
126
131
|
assert.match(css, /\.foot \.footNav\{/);
|
|
127
132
|
});
|
|
128
133
|
|
|
134
|
+
// ── the hub's bar (task 1004489) ───────────────────────────────────────────
|
|
135
|
+
//
|
|
136
|
+
// /projects and /account wear the hub's bar and its two bands; the doors used to
|
|
137
|
+
// draw their own simple header, so a footer link swapped the whole chrome. Now
|
|
138
|
+
// they wear the same bar: the hub's markup, cosmos.css's rules, no map search,
|
|
139
|
+
// band links naming /projects in full, no item the current place, and Account
|
|
140
|
+
// apex-only (these pages serve on EVERY instance; /account only on the hub).
|
|
141
|
+
// The behaviour is assets/hub-bar.js, shared with account.html, EXECUTED below
|
|
142
|
+
// against each page's own markup in both shapes.
|
|
143
|
+
|
|
144
|
+
const HUB = read('projects.html');
|
|
145
|
+
const ACCOUNT = read('account.html');
|
|
146
|
+
const BAR = read('assets/hub-bar.js');
|
|
147
|
+
const bandOf = (html, id) => {
|
|
148
|
+
const m = html.match(new RegExp(`<div class="menuBand" id="${id}"[\\s\\S]*?\\n</div>`));
|
|
149
|
+
assert.ok(m, `#${id} is a whole band`);
|
|
150
|
+
return m[0];
|
|
151
|
+
};
|
|
152
|
+
const bandLinks = (band) => [...band.matchAll(/<a class="bandT[^"]*" href="([^"]+)"[^>]*>([^<]+)<\/a>/g)].map((m) => [m[2], m[1]]);
|
|
153
|
+
// the bar and both bands, from the skip link to the end of the profile band
|
|
154
|
+
const chrome = (html) => {
|
|
155
|
+
const m = html.match(/<a class="skip vh"[\s\S]*?<div class="menuBand" id="menuProfile"[\s\S]*?\n<\/div>\n/);
|
|
156
|
+
assert.ok(m, 'the skip link, the bar and both bands are one block');
|
|
157
|
+
return m[0];
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
for (const p of PAGES) {
|
|
161
|
+
test(`${p.file}: the bar is the hub's own — its markup, no simple header, no map search`, () => {
|
|
162
|
+
const html = read(p.file);
|
|
163
|
+
assert.doesNotMatch(html, /<header class="top">|class="btn-space"|class="mark"/, 'the simple header is gone');
|
|
164
|
+
assert.doesNotMatch(html, /id="(mapQ|qWrap|qToggle|onlyMine)"/, 'the search only filters the map, so it stays on the map');
|
|
165
|
+
for (const piece of [
|
|
166
|
+
'<header class="shell bar">',
|
|
167
|
+
'<a class="barMark" id="barMark" href="/">',
|
|
168
|
+
'<ellipse cx="8.4" cy="5.6" rx="6.6" ry="2.9"/>',
|
|
169
|
+
'<button class="trig" type="button" id="projTrig" data-menu="projects" aria-expanded="false" aria-controls="menuProjects">Projects<svg class="cv" viewBox="0 0 20 20" aria-hidden="true"><path d="m5.6 8 4.4 4.4L14.4 8"/></svg></button>',
|
|
170
|
+
'<button class="trig who" type="button" id="who" data-menu="profile" aria-expanded="false" aria-controls="menuProfile" hidden>',
|
|
171
|
+
'<div class="menuBand" id="menuProjects" data-menu="projects" hidden>',
|
|
172
|
+
'<div class="menuBand" id="menuProfile" data-menu="profile" hidden>',
|
|
173
|
+
'<div class="bandIn bandProfile">',
|
|
174
|
+
'<!--apex-only:start--><li class="bandCell"><a class="bandT" href="/account">Account</a></li><!--apex-only:end-->',
|
|
175
|
+
'<li class="bandCell"><a class="bandT signout" href="/api/bongos/auth/logout">Sign out</a></li>',
|
|
176
|
+
]) {
|
|
177
|
+
assert.ok(HUB.includes(piece), `projects.html no longer carries: ${piece}`);
|
|
178
|
+
assert.ok(html.includes(piece), `${p.file} does not carry the hub's: ${piece}`);
|
|
179
|
+
}
|
|
180
|
+
assert.ok(html.includes(`<a class="signinDoor" id="signinTop" hidden href="/api/bongos/auth/web/start?return=${p.route}&repos=1">Sign in</a>`),
|
|
181
|
+
'signed out, the door in the chip’s slot returns to this page, asking for repo access like every hub door (task 1004549)');
|
|
182
|
+
assert.match(html, /<a class="skip vh" href="#legal">Skip to content<\/a>\n/);
|
|
183
|
+
assert.match(html, /<main class="legal" id="legal">/, 'the skip link has its landmark');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test(`${p.file}: the bands say what the hub's say and lead into /projects; nothing here is the current place`, () => {
|
|
187
|
+
const html = read(p.file);
|
|
188
|
+
const clauses = (band) => [...band.matchAll(/>([^<]+)<\/a><span class="bandC">([^<]+)<\/span>/g)].map((m) => [m[1], m[2]]);
|
|
189
|
+
assert.deepEqual(clauses(bandOf(html, 'menuProjects')), clauses(bandOf(HUB, 'menuProjects')), 'the Projects band’s items and clauses are the hub’s');
|
|
190
|
+
assert.deepEqual(bandLinks(bandOf(html, 'menuProjects')), [
|
|
191
|
+
['Sky', '/projects?view=map'], ['Table', '/projects?view=map&sub=table'],
|
|
192
|
+
['Your projects', '/projects?view=mine'], ['New project', '/projects?view=new'],
|
|
193
|
+
], 'this page has no views of its own, so every item names /projects in full');
|
|
194
|
+
assert.deepEqual(bandLinks(bandOf(html, 'menuProfile')), [
|
|
195
|
+
['My projects', '/projects?view=mine'], ['Account', '/account'], ['Sign out', '/api/bongos/auth/logout'],
|
|
196
|
+
]);
|
|
197
|
+
assert.doesNotMatch(chrome(html), /aria-current/, 'no band item is this page');
|
|
198
|
+
const child = stripApexOnly(html, false);
|
|
199
|
+
assert.deepEqual(bandLinks(bandOf(child, 'menuProfile')), [
|
|
200
|
+
['My projects', '/projects?view=mine'], ['Sign out', '/api/bongos/auth/logout'],
|
|
201
|
+
], 'a child instance has no account page, so it loses the item whole');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test(`${p.file}: the bar is not restyled, and its script is the shared asset`, () => {
|
|
205
|
+
const html = read(p.file);
|
|
206
|
+
const styles = [...html.matchAll(/<style>([\s\S]*?)<\/style>/g)].map((m) => m[1]).join('\n');
|
|
207
|
+
assert.doesNotMatch(styles, /\.(shell|bar|barMark|barLeft|barRight|trig|who|signinDoor|menuBand|band[A-Z]\w*)\b[^{}]*\{/, 'no bar or band rule in the page');
|
|
208
|
+
assert.match(html, /<\/footer>\n<script src="\/assets\/hub-bar\.js"><\/script>\n<\/body>/, 'one shared script, after the markup it drives');
|
|
209
|
+
assert.doesNotMatch(html, /<script>/, 'and no inline copy of the bands');
|
|
210
|
+
assert.match(html, /<a data-link-hall href="\{\{buildersOrigin\}\}">Builders' hall<\/a><\/nav><\/footer>/, 'the hall keeps a door in the footer row now the bar has no Hall pill, as on /account');
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
test('the three doors and the account page carry one bar: identical but for the sign-in return and the current place', () => {
|
|
215
|
+
const norm = (html, route) => chrome(html)
|
|
216
|
+
.replace(`return=${route}&`, 'return=ROUTE&')
|
|
217
|
+
.replace(/href="#\w+">Skip to content/, 'href="#MAIN">Skip to content')
|
|
218
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
219
|
+
.replace(/^[ \t]*\n/gm, '')
|
|
220
|
+
.replace(' aria-current="page"', '');
|
|
221
|
+
const doors = PAGES.map((p) => norm(read(p.file), p.route));
|
|
222
|
+
assert.equal(doors[1], doors[0], 'terms.html and privacy.html');
|
|
223
|
+
assert.equal(doors[2], doors[0], 'contact.html and privacy.html');
|
|
224
|
+
assert.equal(norm(ACCOUNT, '/account'), doors[0], 'account.html and the doors: one bar, so a change to it is a change to all four');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('cosmos.css still carries the bar and the bands the doors lean on', () => {
|
|
228
|
+
const css = fs.readFileSync(path.join(LANDING, 'assets', 'cosmos.css'), 'utf8');
|
|
229
|
+
for (const rule of ['.bar{', '.barMark{', '.trig{', '.signinDoor{', '.menuBand{']) assert.ok(css.includes(rule), `cosmos.css still carries ${rule}`);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('assets/hub-bar.js builds no markup from strings and makes only the bar’s two calls', () => {
|
|
233
|
+
assert.doesNotMatch(BAR, /innerHTML|insertAdjacentHTML|outerHTML/);
|
|
234
|
+
const calls = [...BAR.matchAll(/fetch\(API \+ '([^']+)', \{ method: '(GET|POST)'/g)].map((m) => `${m[2]} ${m[1]}`);
|
|
235
|
+
assert.deepEqual(calls.sort(), ['GET /me', 'POST /auth/logout']);
|
|
236
|
+
assert.doesNotMatch(BAR, /gtag|plausible|googletagmanager/, 'the privacy page promises no trackers');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// A DOM stub just wide enough for the bar: every id in the markup is an element
|
|
240
|
+
// (hidden where the markup says so), each band's links are items in markup order,
|
|
241
|
+
// and fetch answers from a handler. Booted from the SERVED shape of the page —
|
|
242
|
+
// hub or child — so the child's missing Account item is what the script meets.
|
|
243
|
+
function bootBar(html, answer, pathname) {
|
|
244
|
+
const els = new Map();
|
|
245
|
+
const listeners = (o) => { o._on = {}; o.addEventListener = (t, f) => { (o._on[t] ||= []).push(f); }; return o; };
|
|
246
|
+
const el = (id, hidden, attrs) => listeners({
|
|
247
|
+
id, hidden, attrs, textContent: '',
|
|
248
|
+
getAttribute(n) { return n in attrs ? attrs[n] : null; },
|
|
249
|
+
setAttribute(n, v) { attrs[n] = v; },
|
|
250
|
+
closest(sel) { return sel === '.trig' && /\btrig\b/.test(attrs.class || '') ? this : null; },
|
|
251
|
+
focus() { document.activeElement = this; },
|
|
252
|
+
});
|
|
253
|
+
for (const m of html.matchAll(/<(\w+)([^>]*?)\sid="([\w-]+)"([^>]*)>/g)) {
|
|
254
|
+
const attrs = Object.fromEntries([...(m[2] + m[4]).matchAll(/\s([\w-]+)="([^"]*)"/g)].map((a) => [a[1], a[2]]));
|
|
255
|
+
els.set(m[3], el(m[3], /\shidden\b/.test(m[2] + m[4]), attrs));
|
|
256
|
+
}
|
|
257
|
+
const trigs = [...els.values()].filter((e) => /\btrig\b/.test(e.attrs.class || ''));
|
|
258
|
+
const bands = [...html.matchAll(/<div class="menuBand" id="(\w+)"[\s\S]*?\n<\/div>/g)].map((m) => {
|
|
259
|
+
const band = els.get(m[1]);
|
|
260
|
+
band.items = [...m[0].matchAll(/<li class="bandCell"(?: id="(\w+)")?><a class="([^"]+)" href="([^"]+)"([^>]*)>([^<]+)<\/a>/g)].map((a) => ({
|
|
261
|
+
cls: a[2], href: a[3], text: a[5], cell: els.get(a[1]) || { hidden: false },
|
|
262
|
+
focus() { document.activeElement = this; },
|
|
263
|
+
closest(sel) {
|
|
264
|
+
if (sel === 'a') return this;
|
|
265
|
+
if (sel === '[aria-current="page"]') return a[4].includes(' aria-current="page"') ? this : null;
|
|
266
|
+
if (sel === 'a.signout') return /\bsignout\b/.test(this.cls) ? this : null;
|
|
267
|
+
if (sel === '.menuBand') return band;
|
|
268
|
+
if (sel === '[hidden]') return this.cell.hidden ? this.cell : null;
|
|
269
|
+
return null;
|
|
270
|
+
},
|
|
271
|
+
}));
|
|
272
|
+
band.contains = (x) => band.items.includes(x);
|
|
273
|
+
band.querySelector = () => band.items.find((i) => !i.cell.hidden) || null;
|
|
274
|
+
band.querySelectorAll = () => band.items;
|
|
275
|
+
return band;
|
|
276
|
+
});
|
|
277
|
+
const docOn = {};
|
|
278
|
+
const event = (extra) => ({ prevented: false, preventDefault() { this.prevented = true; }, ...extra });
|
|
279
|
+
const calls = [];
|
|
280
|
+
const dom = {
|
|
281
|
+
els, calls, assigned: null,
|
|
282
|
+
active: () => document.activeElement,
|
|
283
|
+
band: (id) => els.get(id),
|
|
284
|
+
item: (id, text) => els.get(id).items.find((i) => i.text === text),
|
|
285
|
+
click(target, detail = 1) {
|
|
286
|
+
const band = bands.find((b) => b.items.includes(target));
|
|
287
|
+
const ev = event({ target, detail });
|
|
288
|
+
for (const f of [...(target._on ? target._on.click || [] : []), ...(band ? band._on.click || [] : []), ...(docOn.click || [])]) f(ev);
|
|
289
|
+
return ev;
|
|
290
|
+
},
|
|
291
|
+
key(key) { const ev = event({ key }); for (const f of docOn.keydown || []) f(ev); return ev; },
|
|
292
|
+
focusout(band, relatedTarget) { for (const f of band._on.focusout || []) f({ relatedTarget }); },
|
|
293
|
+
};
|
|
294
|
+
const document = {
|
|
295
|
+
cookie: '', activeElement: null,
|
|
296
|
+
addEventListener: (t, f) => { (docOn[t] ||= []).push(f); },
|
|
297
|
+
getElementById: (id) => els.get(id) || null,
|
|
298
|
+
querySelector(sel) {
|
|
299
|
+
if (sel === '.menuBand:not([hidden])') return bands.find((b) => !b.hidden) || null;
|
|
300
|
+
if (sel === '.trig[aria-expanded="true"]') return trigs.find((t) => t.attrs['aria-expanded'] === 'true') || null;
|
|
301
|
+
throw new Error(`unstubbed selector: ${sel}`);
|
|
302
|
+
},
|
|
303
|
+
querySelectorAll(sel) {
|
|
304
|
+
if (sel === '.trig') return trigs;
|
|
305
|
+
if (sel === '.menuBand') return bands;
|
|
306
|
+
throw new Error(`unstubbed selector: ${sel}`);
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
const fetch = (url, init = {}) => {
|
|
310
|
+
const p = url.replace(/^\/api\/bongos/, '');
|
|
311
|
+
calls.push({ method: init.method || 'GET', path: p });
|
|
312
|
+
const { status, data } = answer(init.method || 'GET', p);
|
|
313
|
+
return Promise.resolve({ status, ok: status >= 200 && status < 300, json: () => Promise.resolve(data) });
|
|
314
|
+
};
|
|
315
|
+
const window = { location: { assign(to) { dom.assigned = to; } } };
|
|
316
|
+
vm.runInNewContext(BAR, { document, fetch, window, location: { hostname: 'cloudbongos.com', pathname }, Array, Promise });
|
|
317
|
+
dom.window = window;
|
|
318
|
+
return dom;
|
|
319
|
+
}
|
|
320
|
+
const settle = async () => { for (let i = 0; i < 6; i++) await new Promise((r) => setImmediate(r)); };
|
|
321
|
+
const ME = { builder: { id: '1', github_login: 'octocat', avatar_url: 'https://avatars.example/u/1' } };
|
|
322
|
+
const signedIn = (m, p) => (p === '/me' ? { status: 200, data: ME } : { status: 204, data: null });
|
|
323
|
+
|
|
324
|
+
for (const p of PAGES) {
|
|
325
|
+
for (const hub of [true, false]) {
|
|
326
|
+
const shape = hub ? 'the hub' : 'a child instance';
|
|
327
|
+
test(`${p.file} on ${shape}, executed: signed out the door takes the chip's slot; signed in the chip names the login`, async () => {
|
|
328
|
+
const served = stripApexOnly(read(p.file), hub);
|
|
329
|
+
const out = bootBar(served, () => ({ status: 401, data: { error: { code: 'unauthenticated' } } }), p.route);
|
|
330
|
+
await settle();
|
|
331
|
+
assert.deepEqual(out.calls, [{ method: 'GET', path: '/me' }], 'one identity read, nothing else');
|
|
332
|
+
assert.equal(out.els.get('who').hidden, true, 'no chip without a session');
|
|
333
|
+
assert.equal(out.els.get('signinTop').hidden, false);
|
|
334
|
+
assert.equal(out.els.get('mineCell').hidden, true, 'and the Projects band offers no Your projects');
|
|
335
|
+
assert.equal(await out.window.HubBar.ready, null);
|
|
336
|
+
|
|
337
|
+
const dom = bootBar(served, signedIn, p.route);
|
|
338
|
+
await settle();
|
|
339
|
+
assert.equal(dom.els.get('who').hidden, false);
|
|
340
|
+
assert.equal(dom.els.get('signinTop').hidden, true);
|
|
341
|
+
assert.equal(dom.els.get('whoLogin').textContent, 'octocat');
|
|
342
|
+
assert.equal(dom.els.get('bandWhoLogin').textContent, 'octocat');
|
|
343
|
+
assert.equal(dom.els.get('who').getAttribute('aria-label'), 'Account, octocat');
|
|
344
|
+
for (const id of ['whoImg', 'bandWhoImg']) assert.equal(dom.els.get(id).src, 'https://avatars.example/u/1');
|
|
345
|
+
assert.equal(dom.els.get('mineCell').hidden, false);
|
|
346
|
+
assert.deepEqual(dom.band('menuProfile').items.map((i) => i.text), hub ? ['My projects', 'Account', 'Sign out'] : ['My projects', 'Sign out']);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
test(`${p.file}, executed: the bands open and close the way the hub's do — toggle, one at a time, outside, Tab out, keys`, async () => {
|
|
351
|
+
const dom = bootBar(stripApexOnly(read(p.file), true), signedIn, p.route);
|
|
352
|
+
await settle();
|
|
353
|
+
const [projTrig, who] = [dom.els.get('projTrig'), dom.els.get('who')];
|
|
354
|
+
const [projects, profile] = [dom.band('menuProjects'), dom.band('menuProfile')];
|
|
355
|
+
const expanded = (t) => t.getAttribute('aria-expanded');
|
|
356
|
+
|
|
357
|
+
dom.click(projTrig);
|
|
358
|
+
assert.deepEqual([projects.hidden, expanded(projTrig)], [false, 'true']);
|
|
359
|
+
dom.click(projTrig);
|
|
360
|
+
assert.deepEqual([projects.hidden, expanded(projTrig)], [true, 'false'], 'the trigger toggles');
|
|
361
|
+
dom.click(projTrig);
|
|
362
|
+
dom.click(who);
|
|
363
|
+
assert.deepEqual([projects.hidden, profile.hidden, expanded(who)], [true, false, 'true'], 'opening one closes the other');
|
|
364
|
+
dom.click({ closest: () => null });
|
|
365
|
+
assert.deepEqual([profile.hidden, expanded(who)], [true, 'false'], 'a click outside closes');
|
|
366
|
+
|
|
367
|
+
dom.click(who);
|
|
368
|
+
const acct = dom.click(dom.item('menuProfile', 'Account'));
|
|
369
|
+
assert.equal(acct.prevented, false, 'Account is a real navigation from here — this page is not it');
|
|
370
|
+
assert.equal(profile.hidden, true, 'an item activated closes its band');
|
|
371
|
+
|
|
372
|
+
dom.click(projTrig);
|
|
373
|
+
dom.focusout(projects, null);
|
|
374
|
+
assert.equal(projects.hidden, false, 'a null relatedTarget is left to the document click');
|
|
375
|
+
dom.focusout(projects, { closest: () => null });
|
|
376
|
+
assert.equal(projects.hidden, true, 'Tab out of the band closes it');
|
|
377
|
+
|
|
378
|
+
assert.equal(dom.key('ArrowDown').prevented, false, 'with nothing open the keys are the page’s');
|
|
379
|
+
dom.click(projTrig, 0);
|
|
380
|
+
assert.equal(dom.active().text, 'Sky', 'a keyboard activation lands on the first item');
|
|
381
|
+
dom.key('ArrowUp');
|
|
382
|
+
assert.equal(dom.active().text, 'New project', 'ArrowUp wraps past the first item');
|
|
383
|
+
dom.key('ArrowDown');
|
|
384
|
+
dom.key('ArrowDown');
|
|
385
|
+
assert.equal(dom.active().text, 'Table');
|
|
386
|
+
dom.key('Escape');
|
|
387
|
+
assert.equal(projects.hidden, true);
|
|
388
|
+
assert.equal(dom.active(), projTrig, 'Escape hands focus back to the trigger');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test(`${p.file}, executed: sign out is the profile band's anchor upgraded to the POST, then this page again`, async () => {
|
|
392
|
+
const dom = bootBar(stripApexOnly(read(p.file), false), signedIn, p.route);
|
|
393
|
+
await settle();
|
|
394
|
+
assert.equal(dom.click(dom.item('menuProfile', 'My projects')).prevented, false, 'every other item is a real navigation');
|
|
395
|
+
dom.click(dom.els.get('who'));
|
|
396
|
+
const ev = dom.click(dom.item('menuProfile', 'Sign out'));
|
|
397
|
+
assert.equal(ev.prevented, true, 'the GET the href names would leave the session live');
|
|
398
|
+
await settle();
|
|
399
|
+
assert.deepEqual(dom.calls.at(-1), { method: 'POST', path: '/auth/logout' });
|
|
400
|
+
assert.equal(dom.assigned, p.route);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
129
404
|
// ── served on the apex, under the instance's identity, never elsewhere ─────
|
|
130
405
|
|
|
131
406
|
let server;
|