@bongos/core 1.21.43 → 1.21.44
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 +31 -31
- package/docs/copy-inventory.md +11 -11
- package/docs/copy-registry.json +13 -13
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +5 -2
- package/docs/page-readings.json +391 -401
- package/modules/hall-ui/public/city-draw.js +51 -13
- package/modules/hall-ui/public/city.css +9 -2
- package/modules/hall-ui/public/city.html +3 -3
- package/modules/hall-ui/public/city.js +23 -12
- package/modules/hall-ui/public/city.states.json +146 -12
- package/modules/hall-ui/public/street-card.css +6 -0
- package/modules/hall-ui/public/street-card.js +31 -3
- package/modules/hall-ui/public/street-draw.js +96 -19
- package/modules/hall-ui/public/street.css +23 -7
- package/modules/hall-ui/public/street.html +8 -7
- package/modules/hall-ui/public/street.js +45 -13
- package/modules/hall-ui/public/street.states.json +68 -0
- package/modules/hall-ui/records/governor-street.md +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/hall-preview/server.js +5 -2
- package/src/module-api.js +1 -1
- package/tests/hall_city.mjs +42 -11
- package/tests/hall_matter_card.mjs +17 -0
- package/tests/hall_street.mjs +185 -12
package/tests/hall_street.mjs
CHANGED
|
@@ -32,6 +32,7 @@ const PUB = path.join(ROOT, 'modules/hall-ui/public');
|
|
|
32
32
|
const require = createRequire(import.meta.url);
|
|
33
33
|
const City = require(path.join(PUB, 'city-draw.js'));
|
|
34
34
|
const S = require(path.join(PUB, 'street-draw.js'));
|
|
35
|
+
require(path.join(PUB, 'street-desk.js')); // hangs GovDesk (the line rule's fit) on the global the street reads
|
|
35
36
|
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8');
|
|
36
37
|
const NOW = Date.parse('2026-10-02T12:00:00.000Z'); // a Friday
|
|
37
38
|
|
|
@@ -185,17 +186,118 @@ test('every civic door is the prototype\'s door, and every door opens where its
|
|
|
185
186
|
|
|
186
187
|
// ── what the street says comes from the docket, and only from it ───────────
|
|
187
188
|
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
// task 1004569 (owner, 2026-10-04): the pins stacked on top of each other and
|
|
190
|
+
// their text overlapped. Deliberate change from the prototype's drawing: ONE
|
|
191
|
+
// pin per building, in the tier of its most important matter.
|
|
192
|
+
test('one pin per building, never more: it carries the count of the building\'s open matters and every matter, most important first', () => {
|
|
193
|
+
const ms = [
|
|
194
|
+
matter({ id: 'blockers:0', state: 'moving' }), matter({ id: 'blockers:1', state: 'needs' }),
|
|
195
|
+
matter({ id: 'blockers:2', state: 'settled' }), matter({ id: 'blockers:3', state: 'needs', deadline: '2026-10-03T06:00:00.000Z' }),
|
|
196
|
+
matter({ id: 'board:9', building: 'charter', state: 'moving' }),
|
|
197
|
+
];
|
|
190
198
|
const m = model({ docket: docket(ms) });
|
|
191
199
|
const here = m.pins.filter((p) => p.building === 'planning');
|
|
192
|
-
assert.equal(here.length,
|
|
193
|
-
assert.
|
|
194
|
-
assert.equal(
|
|
195
|
-
assert.
|
|
200
|
+
assert.equal(here.length, 1, 'one pin, not one per matter');
|
|
201
|
+
assert.equal(m.pins.length, 2, 'one per building that has a matter');
|
|
202
|
+
assert.equal(here[0].count, '3', 'three open (the settled one is not open)');
|
|
203
|
+
assert.deepEqual(here[0].matters, ['blockers:3', 'blockers:1', 'blockers:0', 'blockers:2'], 'urgent, needs, moving, settled: the pager\'s order');
|
|
204
|
+
assert.equal(here[0].key, 'blockers:3', 'the pin opens on its most important matter');
|
|
205
|
+
assert.equal(here[0].state, 'urgent');
|
|
206
|
+
assert.match(here[0].aria, /^3 open at planning, the first needs you within a day/);
|
|
196
207
|
assert.equal(m.cols.find((c) => c.key === 'planning').state, '0 goals open', 'Planning says how many goals are open');
|
|
197
208
|
});
|
|
198
209
|
|
|
210
|
+
test('a building\'s pin takes the tier of its most important matter: urgent is needs-you due within 24 hours', () => {
|
|
211
|
+
const tierOf = (list) => model({ docket: docket(list) }).pins.find((p) => p.building === 'planning').state;
|
|
212
|
+
const at = (h) => new Date(NOW + h * 3600000).toISOString();
|
|
213
|
+
assert.equal(tierOf([matter({ id: 'a:1', state: 'needs', deadline: at(23) })]), 'urgent', 'due inside a day');
|
|
214
|
+
assert.equal(tierOf([matter({ id: 'a:1', state: 'needs', deadline: at(-5) })]), 'urgent', 'overdue');
|
|
215
|
+
assert.equal(tierOf([matter({ id: 'a:1', state: 'needs', deadline: at(25) })]), 'needs', 'due after a day');
|
|
216
|
+
assert.equal(tierOf([matter({ id: 'a:1', state: 'moving', deadline: at(1) })]), 'moving', 'a deadline never makes moving work urgent');
|
|
217
|
+
assert.equal(tierOf([matter({ id: 'a:1', state: 'moving' }), matter({ id: 'a:2', state: 'needs' })]), 'needs', 'needs beats moving, wherever it stands in the docket');
|
|
218
|
+
assert.equal(tierOf([matter({ id: 'a:1', state: 'settled' }), matter({ id: 'a:2', state: 'moving' })]), 'moving');
|
|
219
|
+
assert.equal(tierOf([matter({ id: 'a:1', state: 'settled' })]), 'settled');
|
|
220
|
+
assert.equal(tierOf([matter({ id: 'a:1', state: 'weird' })]), 'moving', 'an unknown state is never needs');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('labels: urgent and needs-you show one short label cut by the line rule; moving waits for a hover; settled is a dot with no label and no count', () => {
|
|
224
|
+
const Desk = require(path.join(PUB, 'street-desk.js'));
|
|
225
|
+
const ms = [
|
|
226
|
+
matter({ id: 'board:1', building: 'charter', state: 'needs', short: 'a very long matter name that runs on', deadline: '2026-10-06T12:00:00.000Z' }),
|
|
227
|
+
matter({ id: 'blockers:2', building: 'automation', state: 'moving', short: 'moving work' }),
|
|
228
|
+
matter({ id: 'removal_proposals:3', building: 'treasury', state: 'settled', short: 'settled thing' }),
|
|
229
|
+
];
|
|
230
|
+
const m = model({ docket: docket(ms), goals: [1, 2, 3, 4].map((i) => goal(i)) });
|
|
231
|
+
const by = Object.fromEntries(m.pins.map((p) => [p.building, p]));
|
|
232
|
+
assert.equal(by.charter.label, Desk.fit('a very long matter name that runs on', S.PIN_LABEL - 6) + ' · tue', 'fit() cuts the name, the deadline survives');
|
|
233
|
+
assert.ok(by.charter.label.length <= S.PIN_LABEL);
|
|
234
|
+
assert.equal(by.charter.shown, true);
|
|
235
|
+
assert.deepEqual([by.automation.label, by.automation.shown], ['moving work', false], 'moving: the label is there for hover and focus, not shown');
|
|
236
|
+
assert.deepEqual([by.treasury.label, by.treasury.count, by.treasury.shown], ['', '', false], 'settled: no label, no count');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// A dense docket: every civic building holds several matters in every tier,
|
|
240
|
+
// with long names and deadlines, and work waits at Review.
|
|
241
|
+
function denseDocket() {
|
|
242
|
+
const keys = ['charter', 'rules', 'disputes', 'people', 'planning', 'automation', 'release', 'treasury'];
|
|
243
|
+
const ms = [];
|
|
244
|
+
keys.forEach((b, i) => {
|
|
245
|
+
const day = (h) => new Date(NOW + h * 3600000).toISOString();
|
|
246
|
+
ms.push(matter({ id: `dense:${b}:1`, building: b, state: i % 3 === 2 ? 'moving' : 'needs', short: `${b} needs a long careful decision`, deadline: i % 2 ? day(6) : day(30 + i * 24) }));
|
|
247
|
+
ms.push(matter({ id: `dense:${b}:2`, building: b, state: 'needs', short: `second ${b} matter` }));
|
|
248
|
+
ms.push(matter({ id: `dense:${b}:3`, building: b, state: 'moving', short: `moving at ${b}` }));
|
|
249
|
+
ms.push(matter({ id: `dense:${b}:4`, building: b, state: 'settled', short: `settled at ${b}` }));
|
|
250
|
+
});
|
|
251
|
+
[1, 2, 3, 4].forEach((i) => ms.push(matter({ id: `criteria_uat:${i}`, source: 'criteria_uat', building: 'review', state: 'needs', title: `Criterion ${i}` })));
|
|
252
|
+
return docket(ms);
|
|
253
|
+
}
|
|
254
|
+
const meets = (a, b) => a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1;
|
|
255
|
+
function overlaps(boxes) {
|
|
256
|
+
const hits = [];
|
|
257
|
+
boxes.forEach((a, i) => boxes.slice(i + 1).forEach((b) => { if (meets(a, b)) hits.push([a.key, b.key]); }));
|
|
258
|
+
return hits;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
test('no overlap, ever: on a dense docket the collision pass lifts or quiets labels until no two shown labels meet, both modes', () => {
|
|
262
|
+
for (const mode of ['dark', 'light']) {
|
|
263
|
+
const m = model({ mode, docket: denseDocket(), goals: [1, 2, 3, 4].map((i) => goal(i)) });
|
|
264
|
+
const shown = S.shownLabels(m.pins);
|
|
265
|
+
assert.deepEqual(overlaps(shown), [], `${mode}: no two shown labels meet`);
|
|
266
|
+
assert.ok(shown.length >= 7, 'the labels that need you show');
|
|
267
|
+
assert.equal(new Set(m.pins.map((p) => p.building)).size, m.pins.length, 'one pin per building');
|
|
268
|
+
// The street's roofs stagger the pins' heights, so its own row rarely
|
|
269
|
+
// collides. The worst case is every pin at one height (a future roof, a
|
|
270
|
+
// new building): the pass must still keep every shown label clear, lifting
|
|
271
|
+
// some and quieting the lowest tier, never a higher one over a lower one.
|
|
272
|
+
const flat = m.pins.map((p) => ({ ...p, y: 300, h: 78, shown: false, lifted: false }));
|
|
273
|
+
const naive = flat.filter((p) => ['urgent', 'needs', 'queue'].includes(p.rank)).map((p) => ({ key: p.key, ...S.labelBox(p) }));
|
|
274
|
+
assert.ok(overlaps(naive).length > 0, 'at one height, without the pass, labels would meet');
|
|
275
|
+
S.placeLabels(flat);
|
|
276
|
+
assert.deepEqual(overlaps(S.shownLabels(flat)), [], `${mode}: worst case, still no two shown labels meet`);
|
|
277
|
+
assert.ok(flat.some((p) => p.lifted), 'a label stepped up to the alternate height');
|
|
278
|
+
const T = ['urgent', 'needs', 'queue'];
|
|
279
|
+
flat.filter((p) => T.includes(p.rank) && !p.shown).forEach((q) => {
|
|
280
|
+
const at = (y) => S.labelBox({ x: q.x, y, label: q.label });
|
|
281
|
+
const blockers = flat.filter((p) => p.shown && (meets(S.labelBox(p), at(300)) || meets(S.labelBox(p), at(300 - S.PIN_STACK))));
|
|
282
|
+
assert.ok(blockers.length && blockers.every((b) => T.indexOf(b.rank) <= T.indexOf(q.rank)), `${q.key} gave way only to an equal or higher tier`);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('the collision pass places in tier order: when only one of two can show, the lower tier waits for hover and keeps its count and colour', () => {
|
|
288
|
+
const pins = [
|
|
289
|
+
{ key: 'lo', rank: 'needs', state: 'needs', x: 100, y: 400, h: 78, label: 'aaaaaaaaaaaaaaaa', count: '2' },
|
|
290
|
+
{ key: 'hi', rank: 'urgent', state: 'urgent', x: 120, y: 400, h: 78, label: 'bbbbbbbbbbbbbbbb', count: '1' },
|
|
291
|
+
{ key: 'mid', rank: 'needs', state: 'needs', x: 110, y: 400, h: 78, label: 'cccccccccccccccc', count: '3' },
|
|
292
|
+
];
|
|
293
|
+
S.placeLabels(pins);
|
|
294
|
+
const by = Object.fromEntries(pins.map((p) => [p.key, p]));
|
|
295
|
+
assert.deepEqual([by.hi.shown, by.hi.lifted], [true, false], 'the urgent label keeps the pin\'s own height');
|
|
296
|
+
assert.deepEqual([by.lo.shown, by.lo.lifted, by.lo.y, by.lo.h], [true, true, 400 - S.PIN_STACK, 78 + S.PIN_STACK], 'the next steps up to the alternate height');
|
|
297
|
+
assert.equal(by.mid.shown, false, 'the third has nowhere to go: its label waits for hover');
|
|
298
|
+
assert.deepEqual([by.mid.state, by.mid.count], ['needs', '3'], 'its count and colour stay');
|
|
299
|
+
});
|
|
300
|
+
|
|
199
301
|
test('no docket: no pins, the readings say so, and nothing is invented', () => {
|
|
200
302
|
const m = S.streetModel({ docket: null, goals: [goal(1)], rollups: [], mode: 'dark', now: NOW });
|
|
201
303
|
assert.equal(m.pins.length, 0);
|
|
@@ -443,8 +545,9 @@ function fakeDom() {
|
|
|
443
545
|
return { document, byId, html, focused };
|
|
444
546
|
}
|
|
445
547
|
|
|
446
|
-
async function bootPage({ feeds = {}, stored = null, osDark = true, hash = '', shell = undefined } = {}) {
|
|
548
|
+
async function bootPage({ feeds = {}, stored = null, osDark = true, hash = '', shell = undefined, width = 1440 } = {}) {
|
|
447
549
|
const { document, byId, html, focused } = fakeDom();
|
|
550
|
+
byId.get('streetWindow').clientWidth = width;
|
|
448
551
|
const store = new Map(stored ? [['hall-theme', stored]] : []);
|
|
449
552
|
const calls = [];
|
|
450
553
|
const listeners = {};
|
|
@@ -468,6 +571,8 @@ async function bootPage({ feeds = {}, stored = null, osDark = true, hash = '', s
|
|
|
468
571
|
vm.runInContext(read('city-draw.js'), ctx);
|
|
469
572
|
vm.runInContext(read('street-draw.js'), ctx);
|
|
470
573
|
vm.runInContext(read('street-card.js'), ctx);
|
|
574
|
+
vm.runInContext(read('street-inside.js'), ctx);
|
|
575
|
+
vm.runInContext(read('street-desk.js'), ctx);
|
|
471
576
|
vm.runInContext(read('street.js'), ctx);
|
|
472
577
|
await window.GovStreetPage.ready;
|
|
473
578
|
const key = (t, e) => (listeners[t] || []).forEach((fn) => fn(e));
|
|
@@ -482,16 +587,23 @@ const FEEDS = (ms, goals = [goal(1)], rollups = [rollup(1, { done: 2, active: 1
|
|
|
482
587
|
});
|
|
483
588
|
|
|
484
589
|
test('the page reads the four feeds and draws the docket\'s pins, the readings and the header', async () => {
|
|
485
|
-
const
|
|
590
|
+
const soon = new Date(Date.now() + 3 * 3600000).toISOString();
|
|
591
|
+
const ms = [matter({ id: 'board:1', building: 'charter', state: 'needs', deadline: soon }), matter({ id: 'blockers:2', state: 'moving' }), matter({ id: 'blockers:3', state: 'moving' }), matter({ id: 'access_requests:4', building: 'people', state: 'needs' }), matter({ id: 'override_requests:5', building: 'release', state: 'settled' })];
|
|
486
592
|
const p = await bootPage({ feeds: FEEDS(ms) });
|
|
487
593
|
assert.deepEqual([...p.calls].sort(), ['/api/bongos/builders/95/earnings', '/api/bongos/goals', '/api/bongos/goals/rollups', '/api/bongos/government/docket']);
|
|
488
594
|
const pins = p.byId.get('streetPins').children;
|
|
489
|
-
assert.deepEqual(pins.map((s) => s.className).sort(), ['street-pin street-pin--moving', 'street-pin street-pin--needs']);
|
|
490
|
-
|
|
595
|
+
assert.deepEqual(pins.map((s) => s.className).sort(), ['street-pin street-pin--moving', 'street-pin street-pin--needs street-pin--shown', 'street-pin street-pin--settled', 'street-pin street-pin--urgent street-pin--shown']);
|
|
596
|
+
const pin = (k) => pins.find((s) => s.attrs['data-matter'] === k);
|
|
597
|
+
assert.equal(pin('board:1').children[1].children[0].className, 'street-pin__ring', 'only the urgent pin pulses');
|
|
598
|
+
assert.ok(!pin('access_requests:4').children[1].children.some((c) => c.className === 'street-pin__ring'), 'a needs-you pin is solid orange, no ring');
|
|
599
|
+
assert.equal(pin('blockers:2').attrs['data-matters'], 'blockers:2 blockers:3');
|
|
600
|
+
assert.deepEqual(pin('blockers:2').children[1].children.map((c) => c.className), ['', 'street-pin__badge'], 'the glyph, then the count of open matters');
|
|
601
|
+
assert.equal(pin('blockers:2').children[1].children[1].textContent, '2');
|
|
602
|
+
assert.equal(pin('override_requests:5').children[1].children.length, 0, 'a settled pin is a plain dot');
|
|
491
603
|
assert.equal(p.byId.get('streetSigns').children.length, 6);
|
|
492
604
|
assert.ok(p.byId.get('streetSigns').children.every((a) => /^\/builders\/street#building=/.test(a.attrs.href)));
|
|
493
|
-
assert.equal(p.byId.get('hudNeeds').textContent, '
|
|
494
|
-
assert.equal(p.byId.get('hudMoving').textContent, '
|
|
605
|
+
assert.equal(p.byId.get('hudNeeds').textContent, '2');
|
|
606
|
+
assert.equal(p.byId.get('hudMoving').textContent, '2');
|
|
495
607
|
assert.equal(p.byId.get('hudShare').textContent, '$24');
|
|
496
608
|
assert.equal(p.byId.get('hudDot').className, 'street-hud__dot street-hud__dot--hot');
|
|
497
609
|
assert.equal(p.byId.get('streetDoors').children.length, 10, 'nine buildings and one district, each a door');
|
|
@@ -515,6 +627,67 @@ test('a pin opens its short card, Escape closes it and focus goes back to the pi
|
|
|
515
627
|
assert.equal(p.focused.el.attrs['data-matter'], 'board:1');
|
|
516
628
|
});
|
|
517
629
|
|
|
630
|
+
test('the card pager: a pin with several matters opens the most important with "1 of N", and previous/next reach every matter there', async () => {
|
|
631
|
+
const ms = [matter({ id: 'blockers:1', state: 'moving', title: 'Third' }), matter({ id: 'blockers:2', state: 'needs', title: 'First' }), matter({ id: 'blockers:3', state: 'needs', title: 'Second' }), matter({ id: 'blockers:4', state: 'settled', title: 'Fourth' })];
|
|
632
|
+
const p = await bootPage({ feeds: FEEDS(ms) });
|
|
633
|
+
const pin = p.byId.get('streetPins').children.find((s) => s.attrs['data-building'] === 'planning');
|
|
634
|
+
pin.fire('click');
|
|
635
|
+
const body = p.byId.get('streetCardBody');
|
|
636
|
+
const pager = () => body.children[0];
|
|
637
|
+
const btn = (dir) => pager().children.find((c) => c.attrs['data-act'] === 'page-' + dir);
|
|
638
|
+
assert.equal(p.byId.get('streetCardTitle').textContent, 'First', 'the most important matter first');
|
|
639
|
+
assert.equal(pager().className, 'mcard-pager');
|
|
640
|
+
assert.equal(pager().children[1].textContent, '1 of 4');
|
|
641
|
+
const seen = [p.byId.get('streetCardTitle').textContent];
|
|
642
|
+
for (let i = 0; i < 3; i++) { btn('next').fire('click'); seen.push(p.byId.get('streetCardTitle').textContent); }
|
|
643
|
+
assert.deepEqual(seen, ['First', 'Second', 'Third', 'Fourth'], 'next reaches every matter at the building');
|
|
644
|
+
assert.equal(pager().children[1].textContent, '4 of 4');
|
|
645
|
+
assert.equal(p.focused.el && p.focused.el.attrs['data-act'], 'page-next', 'focus stays on the arrow');
|
|
646
|
+
btn('next').fire('click');
|
|
647
|
+
assert.equal(p.byId.get('streetCardTitle').textContent, 'First', 'and wraps');
|
|
648
|
+
btn('prev').fire('click');
|
|
649
|
+
assert.equal(p.byId.get('streetCardTitle').textContent, 'Fourth', 'previous goes back, wrapping too');
|
|
650
|
+
assert.equal(p.byId.get('streetCard').hidden, false, 'all without leaving the street');
|
|
651
|
+
assert.equal(p.byId.get('streetPins').children.find((s) => s.attrs['data-building'] === 'planning').attrs['aria-expanded'], 'true', 'the pin still says its card is open');
|
|
652
|
+
p.key('keydown', { key: 'Escape', preventDefault() {} });
|
|
653
|
+
assert.equal(p.focused.el.attrs['data-building'], 'planning', 'Escape returns focus to the pin');
|
|
654
|
+
// one matter: no pager
|
|
655
|
+
const q = await bootPage({ feeds: FEEDS([matter({ id: 'board:1', building: 'charter' })]) });
|
|
656
|
+
q.byId.get('streetPins').children[0].fire('click');
|
|
657
|
+
assert.ok(!q.byId.get('streetCardBody').children.some((c) => c.className === 'mcard-pager'), 'one matter, no pager');
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test('no two shown labels meet on a dense docket at 1440, 1024, 390 and 320 wide, as the page draws them', async () => {
|
|
661
|
+
for (const width of [1440, 1024, 390, 320]) {
|
|
662
|
+
const p = await bootPage({ feeds: { ...FEEDS([]), '/api/bongos/government/docket': denseDocket(), '/api/bongos/goals': { goals: [1, 2, 3, 4].map((i) => goal(i)) } }, width });
|
|
663
|
+
const s = Number(p.byId.get('streetBoard').style['--street-scale']);
|
|
664
|
+
assert.ok(s > 0, `${width}: the board is scaled`);
|
|
665
|
+
const pins = p.byId.get('streetPins').children;
|
|
666
|
+
assert.equal(pins.length, 9, `${width}: one pin per civic building`);
|
|
667
|
+
const shown = pins.filter((el) => /street-pin--shown/.test(el.className)).map((el) => {
|
|
668
|
+
const b = S.labelBox({ x: parseFloat(el.style.left), y: parseFloat(el.style.top), label: el.children[0].textContent });
|
|
669
|
+
return { key: el.attrs['data-matter'], x0: b.x0 * s, x1: b.x1 * s, y0: b.y0 * s, y1: b.y1 * s };
|
|
670
|
+
});
|
|
671
|
+
assert.ok(shown.length >= 5, `${width}: labels show`);
|
|
672
|
+
assert.deepEqual(overlaps(shown), [], `${width}: no two shown labels meet on screen`);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
test('the header counts wear the pins\' colours, so they double as the legend', () => {
|
|
677
|
+
const html = read('street.html');
|
|
678
|
+
assert.match(html, /<span class="street-hud__dot" id="hudDot" aria-hidden="true"><\/span>\s*<span class="street-label">needs you<\/span>/);
|
|
679
|
+
assert.match(html, /<span class="street-hud__dot street-hud__dot--moving" aria-hidden="true"><\/span>\s*<span class="street-label">moving<\/span>/);
|
|
680
|
+
const css = read('street.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
681
|
+
assert.match(css, /\.street-hud__dot \{[^}]*border: 1\.5px solid var\(--accent\)/, 'needs you is orange, a ring while nothing waits');
|
|
682
|
+
assert.match(css, /\.street-hud__dot--hot \{ background: var\(--accent\); \}/);
|
|
683
|
+
assert.match(css, /\.street-hud__dot--moving \{[^}]*background: var\(--ink\)/, 'moving is the moving pin\'s ink');
|
|
684
|
+
assert.match(css, /\.street-pin--moving \.street-pin__head \{[^}]*background: var\(--ink\)/);
|
|
685
|
+
assert.match(css, /\.street-pin--urgent \.street-pin__head, \.street-pin--needs \.street-pin__head \{ background: var\(--accent\)/);
|
|
686
|
+
// size carries the tier too, so it reads without colour
|
|
687
|
+
const size = (t) => Number((css.match(new RegExp(`\\.street-pin--${t} \\.street-pin__head \\{ width: (\\d+)px`)) || [])[1] || 28);
|
|
688
|
+
assert.ok(size('urgent') > size('needs') && size('needs') > size('moving') && size('moving') > size('settled'), 'urgent > needs > moving > settled');
|
|
689
|
+
});
|
|
690
|
+
|
|
518
691
|
test('the switch reads and writes the hall\'s own theme preference and redraws the street', async () => {
|
|
519
692
|
const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: true });
|
|
520
693
|
assert.equal(p.html.attrs['data-theme'], 'light', 'the stored hall choice beats the OS');
|