@bongos/core 1.19.864 → 1.19.866
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 +43 -28
- package/docs/copy-inventory.md +131 -104
- package/docs/copy-registry.json +410 -122
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/blockers.html +4 -1
- package/modules/hall-ui/public/blockers.js +283 -18
- package/modules/hall-ui/public/builders.js +2 -2
- package/modules/hall-ui/public/goal-inbox.js +6 -0
- package/modules/hall-ui/public/hall-render.js +50 -4
- package/modules/hall-ui/public/people.css +9 -0
- package/modules/hall-ui/public/roster.js +11 -1
- package/modules/hall-ui/public/style.css +28 -0
- package/modules/hall-ui/public/task-detail.js +92 -3
- package/modules/lifecycle/routes/tasks.js +33 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/run-unit-tests.js +5 -0
- package/src/module-api.js +1 -1
- package/tests/acclaim_feed_evidence_db.mjs +186 -0
- package/tests/blockers_hall.mjs +92 -0
- package/tests/blockers_page_ui.mjs +325 -0
- package/tests/hall_builders_page_pass.mjs +324 -0
- package/tests/helpers.mjs +13 -0
- package/tests/joinability_invite_only.mjs +11 -1
- package/tests/task_detail_ui.mjs +79 -1
package/tests/helpers.mjs
CHANGED
|
@@ -281,6 +281,10 @@ export function makeEl(tagName, attrs = {}) {
|
|
|
281
281
|
attrs,
|
|
282
282
|
children: [],
|
|
283
283
|
parentNode: null,
|
|
284
|
+
// parentElement — every parent this stub ever creates IS an element (no
|
|
285
|
+
// text-node parents tracked separately), so it is always the same value
|
|
286
|
+
// as parentNode; a getter keeps the two from drifting if that changes.
|
|
287
|
+
get parentElement() { return this.parentNode; },
|
|
284
288
|
className: attrs.class || '',
|
|
285
289
|
id: attrs.id || '',
|
|
286
290
|
name: attrs.name,
|
|
@@ -325,6 +329,10 @@ export function makeEl(tagName, attrs = {}) {
|
|
|
325
329
|
walk(this, (c) => { if (matches(c, sel)) out.push(c); return null; });
|
|
326
330
|
return out;
|
|
327
331
|
},
|
|
332
|
+
// HTMLSelectElement.options — real <select> elements have it; hall-kit.js's
|
|
333
|
+
// wirePager() reads `.length` off it before deciding whether to (re)build
|
|
334
|
+
// the per-page choices.
|
|
335
|
+
get options() { return this.tagName === 'select' ? this.querySelectorAll('option') : undefined; },
|
|
328
336
|
closest(sel) {
|
|
329
337
|
let node = this;
|
|
330
338
|
while (node) { if (matches(node, sel)) return node; node = node.parentNode; }
|
|
@@ -340,6 +348,11 @@ export function makeEl(tagName, attrs = {}) {
|
|
|
340
348
|
},
|
|
341
349
|
addEventListener(type, fn) { (this.listeners[type] = this.listeners[type] || []).push(fn); },
|
|
342
350
|
focus() {},
|
|
351
|
+
// HTMLFormElement.reset() — real forms have it, most elements do not, but a
|
|
352
|
+
// universal no-op is harmless (nothing currently reads a cleared value back)
|
|
353
|
+
// and lets a page script that calls form.reset() run without a stub-specific
|
|
354
|
+
// workaround in every test that exercises one.
|
|
355
|
+
reset() {},
|
|
343
356
|
fire(type, event) { (this.listeners[type] || []).forEach((fn) => fn(event)); },
|
|
344
357
|
};
|
|
345
358
|
for (const [k, v] of Object.entries(attrs)) {
|
|
@@ -177,7 +177,17 @@ test('the hall landing renders one door per mode, and its form collapses on the
|
|
|
177
177
|
assert.match(door, /mode === 'invite_only'/);
|
|
178
178
|
assert.match(door, /there is nothing to apply for here/);
|
|
179
179
|
assert.match(door, /id="access-request-form"/, 'the apply door keeps the request form');
|
|
180
|
-
|
|
180
|
+
// This read `${landingDoorHtml(joinabilityMode())}` inline. Task 1004004 gave
|
|
181
|
+
// the apply door a real sign-in button, so renderLanding resolves the mode
|
|
182
|
+
// ONCE and hands the same value to both doors. The invariant is unchanged and
|
|
183
|
+
// now reaches further: BOTH doors are painted from the pack's own answer, so
|
|
184
|
+
// neither can drift onto a second reading of joinability.
|
|
185
|
+
const landing = (hall.match(/function renderLanding\(\) \{[\s\S]*?\n \}\n/) || [''])[0];
|
|
186
|
+
assert.ok(landing, 'renderLanding() must exist');
|
|
187
|
+
assert.match(landing, /const mode = joinabilityMode\(\);/, 'the pack names the mode, once');
|
|
188
|
+
assert.match(landing, /\$\{landingDoorHtml\(mode\)\}/, 'renderLanding paints the door the pack names');
|
|
189
|
+
assert.match(landing, /\$\{returningDoorHtml\(mode\)\}/, 'and the returning-builder door off the same answer');
|
|
190
|
+
assert.equal((landing.match(/joinabilityMode\(\)/g) || []).length, 1, 'joinability is read once, not per door');
|
|
181
191
|
assert.match(hall, /code === 'invite_only' \|\| code === 'open_enrollment'/, 'the refusal codes are handled, not shown red');
|
|
182
192
|
});
|
|
183
193
|
|
package/tests/task_detail_ui.mjs
CHANGED
|
@@ -73,6 +73,18 @@ const DEFAULT_SUGGESTIONS = {
|
|
|
73
73
|
BV1: [{ id: 1000058, title: 'Consumer path', score: 4, matched_terms: ['overlay'] }],
|
|
74
74
|
BV2: [{ id: 1000062, title: 'Idea surfaces', score: 6, matched_terms: ['promote', 'merge'] }],
|
|
75
75
|
};
|
|
76
|
+
// task 1003456: the blocker resolve panel rides the SAME overlay, so it rides
|
|
77
|
+
// the same executing harness — a source-regex test cannot catch the exact
|
|
78
|
+
// class of bug this file's own header names (a ReferenceError inside a submit
|
|
79
|
+
// handler that passes review and 500s at click time).
|
|
80
|
+
const DEFAULT_BLOCKER = {
|
|
81
|
+
id: 5000042,
|
|
82
|
+
status: 'open',
|
|
83
|
+
title: 'Need the owner’s AWS card on file',
|
|
84
|
+
body_md: 'Raise the S3 budget or approve a spend.',
|
|
85
|
+
created_at: '2026-09-01T00:00:00.000Z',
|
|
86
|
+
blocking_task_ids: [],
|
|
87
|
+
};
|
|
76
88
|
|
|
77
89
|
const escapeHtml = (s) => String(s == null ? '' : s).replace(/[&<>"']/g,
|
|
78
90
|
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
@@ -89,7 +101,9 @@ function fail(status, code, message) {
|
|
|
89
101
|
|
|
90
102
|
async function boot(opts = {}) {
|
|
91
103
|
const {
|
|
104
|
+
kind = 'idea',
|
|
92
105
|
idea = DEFAULT_IDEA,
|
|
106
|
+
blocker = DEFAULT_BLOCKER,
|
|
93
107
|
rank = 'metic',
|
|
94
108
|
versions = DEFAULT_VERSIONS,
|
|
95
109
|
suggestions = DEFAULT_SUGGESTIONS,
|
|
@@ -98,8 +112,10 @@ async function boot(opts = {}) {
|
|
|
98
112
|
createdTask = { id: 1003600, title: 'Cover the overlay handlers' },
|
|
99
113
|
createFails = null,
|
|
100
114
|
patchFails = null,
|
|
115
|
+
resolveFails = null,
|
|
101
116
|
mergeTargets = { 1000900: { id: 1000900, title: 'The canonical idea' } },
|
|
102
117
|
} = opts;
|
|
118
|
+
const ref = kind === 'blocker' ? blocker : idea;
|
|
103
119
|
|
|
104
120
|
const requests = [];
|
|
105
121
|
const toasts = [];
|
|
@@ -130,6 +146,15 @@ async function boot(opts = {}) {
|
|
|
130
146
|
}
|
|
131
147
|
if (method === 'POST' && route === '/tasks') return createFails || ok({ task: { ...createdTask, title: body.title } });
|
|
132
148
|
if (method === 'PATCH' && route.startsWith('/inbox/')) return patchFails || ok({ idea: { ...idea, status: body.status } });
|
|
149
|
+
// task 1003456 — the blocker overlay panel rides the same stub.
|
|
150
|
+
if (method === 'GET' && route.startsWith('/blockers/')) {
|
|
151
|
+
const id = Number(route.slice('/blockers/'.length));
|
|
152
|
+
if (id === Number(blocker.id)) return ok({ blocker });
|
|
153
|
+
return { status: 404, ok: false, data: null };
|
|
154
|
+
}
|
|
155
|
+
if (method === 'POST' && /^\/blockers\/\d+\/resolve$/.test(route)) {
|
|
156
|
+
return resolveFails || ok({ blocker: { ...blocker, status: 'resolved', resolution_note: body.resolution_note } });
|
|
157
|
+
}
|
|
133
158
|
return ok({});
|
|
134
159
|
},
|
|
135
160
|
};
|
|
@@ -138,7 +163,7 @@ async function boot(opts = {}) {
|
|
|
138
163
|
hostname: 'builders.example.test',
|
|
139
164
|
pathname: '/builders',
|
|
140
165
|
search: '',
|
|
141
|
-
hash:
|
|
166
|
+
hash: `#/${kind}/${ref.id}`,
|
|
142
167
|
href: '',
|
|
143
168
|
assign(u) { navigations.push(['assign', u]); },
|
|
144
169
|
replace(u) { navigations.push(['replace', u]); },
|
|
@@ -246,6 +271,59 @@ await test('a closed idea offers no triage panel', async () => {
|
|
|
246
271
|
assert.equal(h.panel(), null, 'a settled idea has nothing left to decide');
|
|
247
272
|
});
|
|
248
273
|
|
|
274
|
+
// ===========================================================================
|
|
275
|
+
// task 1003456 — the blocker resolve panel, on the SAME overlay
|
|
276
|
+
// ===========================================================================
|
|
277
|
+
|
|
278
|
+
await test('an open blocker offers a Metic the Resolve panel', async () => {
|
|
279
|
+
const h = await boot({ kind: 'blocker' });
|
|
280
|
+
assert.ok(h.overlay(), 'a #deeplink-card overlay is mounted');
|
|
281
|
+
const p = h.panel();
|
|
282
|
+
assert.ok(p, 'an open blocker carries the resolve panel');
|
|
283
|
+
const verbs = p.querySelectorAll('[data-go]').map((b) => b.dataset.go);
|
|
284
|
+
assert.deepEqual(verbs, ['resolve'], 'one verdict — resolve — not the idea three');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await test('a Xenos gets the blocker card but no resolve panel (the server gate is mirrored, not trusted)', async () => {
|
|
288
|
+
const h = await boot({ kind: 'blocker', rank: 'xenos' });
|
|
289
|
+
assert.ok(h.overlay(), 'the record is still readable');
|
|
290
|
+
assert.equal(h.panel(), null, 'the action is not offered below Metic');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await test('a resolved blocker offers no resolve panel, and says what closed it', async () => {
|
|
294
|
+
const h = await boot({ kind: 'blocker', blocker: { ...DEFAULT_BLOCKER, status: 'resolved', resolution_note: 'raised the budget' } });
|
|
295
|
+
assert.equal(h.panel(), null, 'a settled blocker has nothing left to decide');
|
|
296
|
+
const note = h.overlay().querySelectorAll('.deeplink-card__note').map((n) => n.textContent).join(' | ');
|
|
297
|
+
assert.match(note, /Resolved: raised the budget/);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await test('resolving posts to the real route, toasts, dispatches otb:blocker-resolved, and closes the overlay', async () => {
|
|
301
|
+
const h = await boot({ kind: 'blocker' });
|
|
302
|
+
await h.clickGo('resolve');
|
|
303
|
+
const form = h.panel().querySelector('[data-form="resolve"]');
|
|
304
|
+
form.note.value = 'raised the budget';
|
|
305
|
+
await h.submitForm('resolve');
|
|
306
|
+
const posts = h.requestsTo('POST', /\/blockers\/5000042\/resolve$/);
|
|
307
|
+
assert.equal(posts.length, 1, 'exactly one resolve POST');
|
|
308
|
+
assert.equal(posts[0].body.resolution_note, 'raised the budget');
|
|
309
|
+
assert.ok(h.toasts.some((t) => /Blocker #5000042 resolved\./.test(t)));
|
|
310
|
+
assert.equal(h.events.length, 1, 'the otb:blocker-resolved event fired');
|
|
311
|
+
assert.equal(h.events[0].type, 'otb:blocker-resolved');
|
|
312
|
+
assert.equal(h.events[0].detail.id, 5000042);
|
|
313
|
+
assert.equal(h.overlay(), null, 'the overlay closes on a settled verdict, the idea-panel precedent');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
await test('a 403 on resolve renders HONEST copy and leaves the form open — never a silent no-op', async () => {
|
|
317
|
+
const h = await boot({
|
|
318
|
+
kind: 'blocker',
|
|
319
|
+
resolveFails: { status: 403, ok: false, data: { error: { code: 'permission_forbidden', message: 'nope' } } },
|
|
320
|
+
});
|
|
321
|
+
await h.clickGo('resolve');
|
|
322
|
+
await h.submitForm('resolve');
|
|
323
|
+
assert.equal(h.errText(), 'You need Metic+ access to resolve blockers.');
|
|
324
|
+
assert.ok(h.overlay(), 'the overlay stays open so the builder can see why it failed');
|
|
325
|
+
});
|
|
326
|
+
|
|
249
327
|
// ===========================================================================
|
|
250
328
|
// Promote — the form the version and goal pickers live on
|
|
251
329
|
// ===========================================================================
|