@bongos/core 1.21.38 → 1.21.40

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.
@@ -31,6 +31,7 @@ const require = createRequire(import.meta.url);
31
31
  require(path.join(PUB, 'city-draw.js'));
32
32
  require(path.join(PUB, 'street-draw.js'));
33
33
  const C = require(path.join(PUB, 'street-card.js'));
34
+ const D = require(path.join(PUB, 'street-desk.js'));
34
35
  const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8');
35
36
  const NOW = Date.parse('2026-10-02T12:00:00.000Z');
36
37
  const VIEWER = '95';
@@ -77,8 +78,12 @@ test('an open unanimous sitting that waits on you: the proposer\'s words, the ch
77
78
  assert.equal(cv.counsel.revise, false);
78
79
  assert.deepEqual(cv.actions.map((a) => [a.key, a.label, a.tone]), [['for', 'vote for', 'primary'], ['object', 'object', 'quiet']]);
79
80
  assert.deepEqual(cv.textActions.map((a) => a.key), ['abstain']);
80
- assert.ok(cv.objectForm.sections.length >= 3);
81
81
  assert.equal(cv.readOnly, false);
82
+ // more (task 1004566): the whole rationale, who and when, and every vote in order
83
+ assert.equal(cv.more.text[0].words, 'Three pieces of finished work are waiting because no one is allowed to test them.\n\nMore below.');
84
+ assert.equal(cv.more.who, 'Put up by Ana Ruiz.');
85
+ assert.equal(cv.more.when, 'opened 2026-10-01 · closes 2026-10-03');
86
+ assert.deepEqual(cv.more.history, ['2026-10-01 · Ana Ruiz voted for', '2026-10-01 · Theo voted for', '2026-10-01 · Mira voted for']);
82
87
  });
83
88
 
84
89
  test('a majority sitting with an objection, after your yes: voices for and against, what it still needs', () => {
@@ -104,7 +109,7 @@ test('a sitting that came back to you: counsel answers the objection, revise and
104
109
  assert.equal(cv.readOnly, false);
105
110
  });
106
111
 
107
- test('passed, not yours, a Full Idea, and a ballot not read yet are read-only', () => {
112
+ test('passed, not yours, and a ballot not read yet are read-only; a Full Idea is voted on here with its sections under more', () => {
108
113
  const passed = card([matter({ state: 'settled' })], 'board:41', room([], [item({ closed_at: '2026-10-02T09:00:00.000Z', outcome: 'passed', votes: [vote(4, 'Ana', 'yes'), vote(5, 'Theo', 'yes')] })]));
109
114
  assert.equal(passed.result, 'Passed 2–0. In force from 2026-10-02.');
110
115
  assert.deepEqual([passed.readOnly, passed.actions.length, passed.textActions.length], [true, 0, 0]);
@@ -113,9 +118,11 @@ test('passed, not yours, a Full Idea, and a ballot not read yet are read-only',
113
118
  assert.equal(notMine.actions.length, 0);
114
119
  assert.ok(!notMine.seats.some((s) => s.pos === 'you'), 'no seat drawn for you when you have none');
115
120
  const idea = card([matter({ building: 'planning', state: 'needs' })], 'board:41', room([item({ subject_type: 'full_idea', subject: { title: 'An idea', author_id: '4', field_values: { purpose: 'It does a thing.' } } })]));
116
- assert.equal(idea.actions.length, 0, 'a Full Idea is voted on with its sections in front of you');
117
- assert.match(idea.result, /Board Room/);
118
- assert.equal(idea.href, '/board-room?item=41');
121
+ assert.deepEqual(idea.actions.map((a) => a.key), ['for', 'object'], 'a Full Idea is voted on from the card (task 1004566)');
122
+ assert.equal(idea.result, 'Read its sections under more, then vote.');
123
+ assert.deepEqual(idea.more.text, [{ label: 'what it is for', words: 'It does a thing.' }], 'its sections are in front of the voter');
124
+ const ownIdea = card([matter({ building: 'planning', state: 'moving' })], 'board:41', room([item({ subject_type: 'full_idea', subject: { title: 'An idea', author_id: VIEWER, field_values: { purpose: 'Mine.' } }, votes: [] })]));
125
+ assert.deepEqual([ownIdea.readOnly, ownIdea.actions.length, ownIdea.result], [true, 0, 'Yours. It is with the sitting now.'], 'its author never votes on it');
119
126
  const loading = card([matter()], 'board:41', null);
120
127
  assert.deepEqual([loading.loading, loading.readOnly, loading.result], [true, true, 'Reading the sitting…']);
121
128
  const gone = card([matter()], 'board:41', room([]));
@@ -134,8 +141,10 @@ test('a constitution amendment shows each changed setting struck and inserted ag
134
141
 
135
142
  test('admissions: fine by me or object when it is yours, read-only when it is not', () => {
136
143
  const m = { id: 'access_requests:7', source: 'access_requests', building: 'people', rule: 'decide', state: 'needs', title: 'Admit Kai as a builder', short: 'admit kai', where: 'people · waiting to join · vouched', proposer: { login: 'kai', name: 'Kai' }, tally: null, href: '/watch', ref: { type: 'access_request', id: '7' } };
137
- const cv = card([m], 'access_requests:7');
138
- assert.equal(cv.quote.words, 'Kai asked to join as a builder, and someone vouched for them.');
144
+ const cv = card([{ ...m, detail: 'I build the docs site.' }], 'access_requests:7');
145
+ assert.equal(cv.quote, null, 'the compact card is the title, one fact and its acts');
146
+ assert.deepEqual(cv.more.text.map((t) => t.words), ['Kai asked to join as a builder, and someone vouched for them.', 'I build the docs site.']);
147
+ assert.equal(cv.more.who, 'Raised by Kai.');
139
148
  assert.deepEqual(cv.actions.map((a) => [a.key, a.label]), [['fine', 'fine by me'], ['refuse', 'object']]);
140
149
  const other = card([{ ...m, state: 'moving' }], 'access_requests:7');
141
150
  assert.deepEqual([other.readOnly, other.actions.length], [true, 0]);
@@ -312,21 +321,20 @@ test('vote for: one POST to the vote route, a toast, the docket re-read, and the
312
321
  assert.match(p.byId.get('streetPins').children[0].className, /street-pin--moving street-pin--answer/, 'the pin moved from needs to moving');
313
322
  });
314
323
 
315
- test('object: the form asks for the reason and the part, then puts both on the record', async () => {
324
+ test('object: "no" opens the desk row\'s one-tap reasons, and a tap puts the reason and the part on the record', async () => {
316
325
  const p = await bootPage({ feeds: BASE(docket([matter()])), posts: { 'POST /api/bongos/government/board/items/41/votes': { ok: true, vote: {}, item_closed: true, outcome: 'returned' } } });
317
326
  p.byId.get('streetPins').children[0].fire('click');
318
327
  await settle(); await settle();
319
- byAct(p.byId.get('streetCardBody'), 'object').fire('click');
320
- assert.equal(p.focused.el, p.byId.get('mcardReason'), 'focus goes to the reason');
321
- byAct(p.byId.get('streetCardBody'), 'object-send').fire('click');
322
- await settle();
323
- assert.equal(p.calls.filter((c) => c.method !== 'GET').length, 0, 'an empty objection is refused before it is sent');
324
- assert.match(walk(p.byId.get('streetCardBody'), (n) => n.className === 'mcard-error')[0].textContent, /^Say why/);
325
- p.byId.get('mcardReason').value = 'Volunteers go quiet.';
326
- p.byId.get('mcardSection').value = 'scope';
327
- byAct(p.byId.get('streetCardBody'), 'object-send').fire('click');
328
+ const body = () => p.byId.get('streetCardBody');
329
+ assert.equal(byAct(body(), 'object').textContent, D.ACT_WORDS.object, 'the desk row\'s word');
330
+ assert.equal(byAct(body(), 'object').attrs['aria-expanded'], 'false');
331
+ byAct(body(), 'object').fire('click');
332
+ assert.equal(p.calls.filter((c) => c.method !== 'GET').length, 0, 'asking sends nothing');
333
+ assert.equal(byAct(body(), 'object').attrs['aria-expanded'], 'true');
334
+ assert.deepEqual(walk(body(), (n) => n.className === 'mcard-chip').map((n) => n.textContent), Object.values(D.CHIPS), 'the same chips as the desk row');
335
+ byAct(body(), 'chip-scope').fire('click');
328
336
  await settle(); await settle(); await settle();
329
- assert.deepEqual(p.calls.filter((c) => c.method !== 'GET').map((c) => c.body), [{ direction: 'no', reason_md: 'Volunteers go quiet.', section_key: 'scope' }]);
337
+ assert.deepEqual(p.calls.filter((c) => c.method !== 'GET').map((c) => c.body), [{ direction: 'no', reason_md: 'Objects to what it changes (one tap on the floor desk).', section_key: 'scope' }]);
330
338
  assert.equal(p.byId.get('streetToast').textContent, 'Your objection is on the record. It goes back to its author to revise.');
331
339
  });
332
340
 
@@ -382,6 +390,123 @@ test('the card\'s motion is CSS only and dies under reduced motion; tokens only;
382
390
  assert.match(html, /id="streetToast" role="status" aria-live="polite"/);
383
391
  });
384
392
 
393
+ // ── one action model, and more in place (task 1004566) ──────────────────────
394
+ // Every docket source in the shape its route hands it, with the acts its owning
395
+ // module gives a viewer who may act. The card for each must offer the desk
396
+ // row's acts for the same item and link nowhere.
397
+ const SOURCE_FIXTURES = () => {
398
+ const ms = [
399
+ matter(),
400
+ matter({ id: 'board:43', building: 'planning', short: 'idea to ratify', title: 'Ask before a big change', ref: { type: 'board_item', id: '43' } }),
401
+ { id: 'blockers:11', source: 'blockers', building: 'planning', rule: 'decide', state: 'needs', title: 'Hosting for the demo', short: 'blocker', where: 'planning · holds 2 tasks', opened_at: '2026-09-30T09:00:00.000Z', proposer: null, tally: null, href: '/blockers#blocker-11', ref: { type: 'blocker', id: '11' }, acts: [{ key: 'decide', options: [{ key: 'go ahead', label: 'go ahead' }, { key: 'not now', label: 'not now' }] }], detail: 'Option A: Render\nOption B: Fly' },
402
+ { id: 'access_requests:7', source: 'access_requests', building: 'people', rule: 'decide', state: 'needs', title: 'Admit Kai as a builder', where: 'people · waiting to join', proposer: { login: 'kai', name: 'Kai' }, href: '/watch', ref: { type: 'access_request', id: '7' } },
403
+ { id: 'goal_join_requests:5', source: 'goal_join_requests', building: 'people', rule: 'decide', state: 'needs', title: 'Bo asks to join Sign-up', proposer: { login: 'bo' }, href: '/goals#/goal/1000125', ref: { type: 'goal_membership_request', id: '5' } },
404
+ { id: 'override_requests:9', source: 'override_requests', building: 'release', rule: 'decide', state: 'needs', title: 'Override: the new sign-up', proposer: { login: 'lu' }, href: '/task/12', ref: { type: 'override_request', id: '9' }, detail: 'The grader timed out twice.' },
405
+ { id: 'removal_proposals:61', source: 'removal_proposals', building: 'disputes', rule: 'decide', state: 'needs', title: 'Old search page', href: '/task/61', ref: { type: 'task', id: '61' }, acts: [{ key: 'remove' }, { key: 'keep' }] },
406
+ { id: 'redteam_reports:4', source: 'redteam_reports', building: 'disputes', rule: 'decide', state: 'needs', title: 'Security report: sign-in limit skipped', href: '/watch', ref: { type: 'vulnerability_report', id: '4' }, acts: [{ key: 'confirm' }, { key: 'dispute' }] },
407
+ { id: 'criteria_uat:3', source: 'criteria_uat', building: 'review', rule: 'signoff', state: 'needs', title: 'The export runs nightly', href: '/goals#/goal/7', ref: { type: 'criterion', id: '3' }, acts: [{ key: 'signoff', options: [{ key: 'live', label: 'tested it live' }] }, { key: 'force', options: [{ key: 'tested_by_hand', label: 'tested by hand' }] }] },
408
+ { id: 'core_update:1.21.20', source: 'core_update', building: 'release', rule: 'decide', state: 'needs', title: 'Take core 1.21.20 (running 1.21.19)', href: '/settings#software-update', ref: { type: 'core_version', id: '1.21.20' } },
409
+ { id: 'gate_approvals:88', source: 'gate_approvals', building: 'release', rule: 'decide', state: 'needs', title: 'PR 88 waits at the gate', href: '/gate', ref: { type: 'pull_request', id: '88' } },
410
+ { id: 'autobongos_fence:project', source: 'autobongos_fence', building: 'automation', rule: 'decide', state: 'needs', title: 'Unattended building is off', href: '/autobongos', ref: { type: 'autonomy_fence', id: 'project' }, acts: [{ key: 'switch_on' }] },
411
+ { id: 'module_rechecks:7', source: 'module_rechecks', building: 'release', rule: 'rule', state: 'needs', title: 'image-tools 1.2.0 failed its security check', href: '/modules', ref: { type: 'module_version', id: 'image-tools@1.2.0' }, acts: [{ key: 'recheck', options: [{ key: 'fixed', label: 'fixed since' }] }] },
412
+ ];
413
+ const d = docket(ms);
414
+ d.desks = [{ key: 'release.core', building: 'release', status: 'live', rows: [{ id: 'release.core:41', desk: 'release.core', building: 'release', name: 'Core 1.21.20', fact: 'not checked yet', facts: [], over: 'core_update', ref: { type: 'core_pin', id: '41' }, acts: [{ key: 'check', options: [{ key: '1.21.20', label: '1.21.20' }] }] }] }];
415
+ const idea = item({ id: '43', subject_type: 'full_idea', subject: { title: 'Ask before a big change', author_id: '4', field_values: { purpose: 'Ask first.', success: 'No surprises.' } }, votes: [] });
416
+ return { d, r: room([item(), idea]) };
417
+ };
418
+
419
+ test('every docket source kind opens a card with the desk row\'s acts when the viewer may act, and links nowhere', () => {
420
+ const expected = [...read('../../government/docket.js').split('const EXPECTED_SOURCES')[1].split(']);')[0].matchAll(/source: '([a-z_]+)'/g)].map((x) => x[1]);
421
+ const { d, r } = SOURCE_FIXTURES();
422
+ assert.deepEqual([...new Set(d.matters.map((m) => m.source))].sort(), expected.slice().sort(), 'a fixture for every source the docket names');
423
+ const { document } = fakeDom();
424
+ const matched = [];
425
+ for (const m of d.matters) {
426
+ const cv = D.cardFor(m.id, d, { room: r, now: NOW });
427
+ assert.ok(cv && cv.row && cv.row.actions.length, `${m.id}: the card acts`);
428
+ assert.equal(cv.readOnly, false, m.id);
429
+ // one action model: the card's acts ARE the desk row's for the same item
430
+ const row = [0, 1, 2, 3, 4, 5, 6, 7].reduce((all, f) => all.concat(D.rowsFor(m.building, f, d, { room: r, now: NOW })), [])
431
+ .find((x) => x.matter === m.id || (x.desk && m.source === 'core_update' && x.over === 'core_update'));
432
+ if (row) { matched.push(m.id); assert.deepEqual(cv.row.actions.map((a) => [a.key, a.label]), row.actions.map((a) => [a.key, a.label]), `${m.id}: the same acts as its desk row`); }
433
+ const host = document.createElement('div');
434
+ C.render(document, host, cv, { act() {}, openForm() {}, ask() {}, more() {}, pick() {} }, { more: true });
435
+ const links = walk(host, (n) => n.tagName === 'a' || (n.attrs && 'href' in n.attrs));
436
+ assert.deepEqual(links.map((n) => n.attrs.href), [], `${m.id}: no link out`);
437
+ assert.ok(walk(host, (n) => n.attrs && n.attrs['data-act'] === 'more').length === 1, `${m.id}: a more toggle`);
438
+ }
439
+ assert.deepEqual(matched.sort(), d.matters.map((m) => m.id).sort(), 'the comparison ran for every kind');
440
+ // the core keeps check-then-take: its card is the Release desk's core row
441
+ const core = D.cardFor('core_update:1.21.20', d, { now: NOW });
442
+ assert.deepEqual(core.row.actions.map((a) => a.key), ['check']);
443
+ assert.deepEqual(C.request(core.row.card, 'check', { option: '1.21.20' }), { method: 'POST', url: '/api/bongos/provisioning/instances/41/core-upgrade', body: { to: '1.21.20', dry_run: true } });
444
+ // a module version is checked again through the reassess route, with a one-tap reason
445
+ const rc = D.cardFor('module_rechecks:7', d, { now: NOW });
446
+ assert.deepEqual(C.request(rc.row.card, 'recheck', { option: 'fixed', label: 'fixed since' }), { method: 'POST', url: '/api/bongos/store/modules/image-tools/versions/1.2.0/reassess', body: { reason: 'Asked on the street: fixed since.' } });
447
+ // the review queue's pin opens its first piece of work, with that work's acts
448
+ const q = D.cardFor('queue', d, { room: r, now: NOW });
449
+ assert.equal(q.key, 'criteria_uat:3');
450
+ assert.deepEqual(q.row.actions.map((a) => a.key), ['signoff', 'force']);
451
+ // the page carries no "open it where it lives" link
452
+ assert.doesNotMatch(read('street.html'), /streetCardOpen|where it lives/);
453
+ assert.doesNotMatch(read('street.js'), /streetCardOpen/);
454
+ });
455
+
456
+ test('a viewer who may not act gets the card without acts and still no link out', () => {
457
+ const { d, r } = SOURCE_FIXTURES();
458
+ d.matters = d.matters.map((m) => { const x = { ...m, state: 'moving' }; delete x.acts; return x; });
459
+ d.desks = []; // a desk row is only handed to a viewer who may act on it
460
+ const { document } = fakeDom();
461
+ for (const m of d.matters.filter((x) => x.source !== 'board')) {
462
+ const cv = D.cardFor(m.id, d, { room: r, now: NOW });
463
+ assert.equal(cv.row, null, `${m.id}: nothing to act on`);
464
+ const host = document.createElement('div');
465
+ C.render(document, host, cv, { act() {}, openForm() {}, ask() {}, more() {}, pick() {} }, { more: true });
466
+ assert.deepEqual(walk(host, (n) => n.attrs && 'href' in n.attrs).map((n) => n.attrs.href), [], m.id);
467
+ }
468
+ });
469
+
470
+ test('more expands the same card in place: the full text, who and when, and the record; less folds it', async () => {
471
+ const { d } = SOURCE_FIXTURES();
472
+ d.matters = d.matters.filter((m) => m.source === 'blockers');
473
+ const p = await bootPage({ feeds: { ...BASE(d) } });
474
+ p.byId.get('streetPins').children[0].fire('click');
475
+ await settle(); await settle();
476
+ const body = () => p.byId.get('streetCardBody');
477
+ const toggle = () => byAct(body(), 'more');
478
+ assert.equal(toggle().attrs['aria-expanded'], 'false');
479
+ assert.equal(walk(body(), (n) => n.className === 'mcard-more').length, 0, 'compact first');
480
+ // compact: one fact and at most two buttons (the line rule)
481
+ assert.equal(walk(body(), (n) => n.className === 'street-card__result').length, 1);
482
+ assert.ok(walk(body(), (n) => /^mcard-btn /.test(n.className)).length <= 2);
483
+ toggle().fire('click');
484
+ assert.equal(toggle().attrs['aria-expanded'], 'true');
485
+ assert.equal(toggle().attrs['aria-controls'], 'mcardMore');
486
+ assert.equal(p.byId.get('streetCard').attrs['data-more'], 'open');
487
+ const more = walk(body(), (n) => n.className === 'mcard-more')[0];
488
+ assert.equal(more.attrs.id, 'mcardMore');
489
+ assert.deepEqual(walk(more, (n) => n.className === 'mcard-more__text').map((n) => n.textContent), ['Option A: Render\nOption B: Fly']);
490
+ assert.equal(walk(more, (n) => n.className === 'mcard-more__meta')[0].textContent, 'opened 2026-09-30');
491
+ assert.deepEqual(walk(more, (n) => n.tagName === 'li').map((n) => n.textContent), ['how it is decided: decides alone'], 'the where line is the card head already');
492
+ assert.equal(p.calls.filter((c) => c.method !== 'GET').length, 0, 'opening more sends nothing');
493
+ assert.equal(p.window.location.hash, '', 'and goes nowhere');
494
+ // the blocker's options stand open, as on the desk: deciding it is picking one
495
+ byAct(body(), 'chip-go ahead');
496
+ assert.ok(byAct(body(), 'chip-go ahead'), 'its options stand open');
497
+ toggle().fire('click');
498
+ assert.equal(walk(body(), (n) => n.className === 'mcard-more').length, 0, 'less folds it');
499
+ });
500
+
501
+ test('the docket hands a matter its detail only as text, bounded, and only when there is some', () => {
502
+ const src = read('../../government/docket.js');
503
+ assert.match(src, /\.\.\.detailField\(raw\.detail\)/);
504
+ assert.match(src, /const MAX_DETAIL = 4000;/);
505
+ for (const [f, re] of [['../../ideas/governor-docket.js', /detail: b\.body_md/], ['../../lifecycle/governor-docket.js', /detail: r\.rationale/], ['../../lifecycle/governor-docket.js', /detail: r\.description/], ['../../security/routes/reports.js', /detail: \[r\.target/], ['../../onboarding/routes/access-requests.js', /detail: r\.note/]]) {
506
+ assert.match(read(f), re, f);
507
+ }
508
+ });
509
+
385
510
  // ── the Board Room names the new subjects ───────────────────────────────────
386
511
 
387
512
  test('the Board Room labels a building\'s matter as that building\'s matter, and a who-decides change as its own amendment', () => {
@@ -505,7 +505,7 @@ test('a pin opens its short card, Escape closes it and focus goes back to the pi
505
505
  const card = p.byId.get('streetCard');
506
506
  assert.equal(card.hidden, false);
507
507
  assert.equal(p.byId.get('streetCardTitle').textContent, '<img src=x onerror=alert(1)>', 'docket text lands as text');
508
- assert.equal(p.byId.get('streetCardOpen').attrs.href, '/builders/board-room');
508
+ assert.equal(p.byId.get('streetCardOpen').attrs.href, undefined, 'the card links nowhere: it acts in place (task 1004566)');
509
509
  assert.equal(p.focused.el, p.byId.get('streetCardClose'));
510
510
  assert.equal(p.byId.get('streetPins').children[0].attrs['aria-expanded'], 'true');
511
511
  const e = { key: 'Escape', prevented: false, preventDefault() { this.prevented = true; } };
@@ -218,7 +218,7 @@ test('a source that is off, coming, not yours or unreadable never shows a number
218
218
  const none = I.civicInfo('release', { docket: null, now: NOW });
219
219
  assert.ok(none.floors.every((f) => f.count === '' && f.status !== 'ok' && f.status !== 'pending'), 'no docket: no numbers anywhere');
220
220
  assert.equal(none.onFile, 'the docket could not be read just now');
221
- assert.deepEqual(none.table, []);
221
+ assert.deepEqual(none.record, [], 'no docket: nothing filed');
222
222
  });
223
223
 
224
224
  test('a floor bound to a reading shows its figure and line when live; Planning counts the open goals', () => {
@@ -237,12 +237,12 @@ test('a floor bound to a reading shows its figure and line when live; Planning c
237
237
  assert.ok(m.texts.some((t) => t.v === '5' && t.s === 20), 'only open goals are counted on the wall');
238
238
  });
239
239
 
240
- test('on the table: the building\'s matters, needs first; each row names the matter it opens', () => {
241
- const ms = [matter({ id: 'board:1', source: 'board', building: 'charter', state: 'moving', title: 'B', deadline: '2026-10-04T09:00:00.000Z' }), matter({ id: 'board:2', source: 'board', building: 'charter', state: 'needs', title: 'A', deadline: '2026-10-02T20:00:00.000Z' }), matter({ id: 'blockers:3' })];
240
+ test('no separate list of matters: the floor desks carry every matter, so the model keeps no table (task 1004565)', () => {
241
+ const ms = [matter({ id: 'board:1', source: 'board', building: 'charter', state: 'moving', title: 'B' }), matter({ id: 'board:2', source: 'board', building: 'charter', state: 'needs', title: 'A' })];
242
242
  const m = I.buildingModel({ key: 'charter', docket: docket(ms), mode: 'light', now: NOW });
243
- assert.deepEqual(m.table.map((r) => [r.matter, r.label, r.status, r.state]), [['board:2', 'A', 'needs you · today', 'needs'], ['board:1', 'B', 'moving · sun', 'moving']]);
244
- assert.equal(m.table[0].color, S.theme('light').accentText);
245
- assert.ok(m.hasTable);
243
+ assert.ok(!('table' in m) && !('hasTable' in m), 'no table fields on the model');
244
+ assert.doesNotMatch(m.label, /on the table/, 'the building\'s label no longer counts a table');
245
+ assert.match(m.label, /floors in use\.$/);
246
246
  });
247
247
 
248
248
  test('the record files the building\'s matters by the month each was opened, newest drawer first', () => {
@@ -524,7 +524,8 @@ const FEEDS = (ms, extra = {}) => ({
524
524
 
525
525
  test('the markup carries the interior: one persistent lift, two flip blocks, the panel and the back link', () => {
526
526
  const html = read('street.html').replace(/<!--[\s\S]*?-->/g, '');
527
- for (const id of ['streetInside', 'insideBack', 'insideSvg', 'insideFloors', 'insideLift', 'insideAnnex', 'insidePanel', 'insideName', 'insideAmend', 'insideAmendNote', 'insideFill', 'insideTable', 'insideOpenA', 'insideOpenB', 'insideDrawers', 'insideDrawerItems', 'insideDName', 'insideLegend', 'insideItems']) assert.match(html, new RegExp(`id="${id}"`), id);
527
+ for (const id of ['streetInside', 'insideBack', 'insideSvg', 'insideFloors', 'insideLift', 'insideAnnex', 'insidePanel', 'insideName', 'insideAmend', 'insideAmendNote', 'insideFill', 'insideOpenA', 'insideOpenB', 'insideDrawers', 'insideDrawerItems', 'insideDName', 'insideLegend', 'insideItems']) assert.match(html, new RegExp(`id="${id}"`), id);
528
+ assert.doesNotMatch(html, /insideTable|on the table here/i, 'the "on the table here" list is gone (task 1004565)');
528
529
  assert.equal((html.match(/<h1\b/g) || []).length, 1, 'the street keeps its one h1; the interior names are h2');
529
530
  assert.match(html, /<section class="inside" id="streetInside"[^>]*hidden>/, 'hidden until an address names a building or a district');
530
531
  assert.match(html, /<button type="button" class="inside-textbtn" id="insideAmend"/);
@@ -539,8 +540,7 @@ test('a building address draws the interior in place of the street, from the doc
539
540
  assert.equal(p.byId.get('insideFloors').children.length, 7, 'a button per floor in use');
540
541
  assert.equal(p.byId.get('insideFloors').children[4].attrs['aria-pressed'], 'true', 'the floor being decided is chosen');
541
542
  assert.equal(p.byId.get('insideLift').style.transform, 'translateY(-208px)');
542
- assert.equal(p.byId.get('insideTable').children.length, 1);
543
- assert.equal(p.byId.get('insideTable').children[0].className, 'inside-row inside-row--needs');
543
+ assert.equal(p.byId.get('insideTable'), undefined, 'no separate list of matters');
544
544
  assert.equal(p.byId.get('insideSeats').children[0].className, 'inside-seat inside-seat--open');
545
545
  assert.equal(p.focused.el, null, 'a page opened at the address keeps the browser\'s own start');
546
546
  p.location.hash = '#building=rules';
@@ -575,15 +575,17 @@ test('picking a floor moves the SAME lift node and re-enters the chosen block; d
575
575
  assert.match(p.byId.get('insideAmendNote').textContent, /could not be read/);
576
576
  });
577
577
 
578
- test('a table row opens the short card; Escape gives focus back to the row; back to the street returns to the door', async () => {
578
+ test('a matter is a desk row: its name opens it in place; back to the street returns to the door', async () => {
579
579
  const ms = [matter({ id: 'board:1', source: 'board', building: 'charter', state: 'needs', title: '<b>x</b>' })];
580
580
  const p = await bootPage({ hash: '#building=charter', feeds: FEEDS(ms) });
581
- p.byId.get('insideTable').children[0].fire('click');
582
- assert.equal(p.byId.get('streetCard').hidden, false);
583
- assert.equal(p.byId.get('streetCardTitle').textContent, '<b>x</b>', 'docket text lands as text');
584
- p.fireWin('keydown', { key: 'Escape', preventDefault() {} });
585
- assert.equal(p.byId.get('streetCard').hidden, true);
586
- assert.equal(p.focused.el.attrs['data-matter'], 'board:1', 'focus goes back to the row');
581
+ const desk = () => (p.byId.get('insideOpenA').hidden ? p.byId.get('insideOpenB') : p.byId.get('insideOpenA'));
582
+ const find = (el, pred) => { if (pred(el)) return el; for (const c of el.children) { const hit = find(c, pred); if (hit) return hit; } return null; };
583
+ const nameOf = () => find(desk(), (el) => el.attrs && el.attrs['data-matter'] === 'board:1');
584
+ assert.ok(nameOf(), 'the desk carries the matter as a row');
585
+ assert.equal(nameOf().attrs['aria-expanded'], 'false');
586
+ nameOf().fire('click');
587
+ assert.equal(nameOf().attrs['aria-expanded'], 'true', 'the row opens in place');
588
+ assert.equal(p.byId.get('streetCard').hidden, true, 'no card is needed inside: the desk acts');
587
589
  p.byId.get('insideBack').fire('click');
588
590
  assert.deepEqual(p.history, ['/builders/street']);
589
591
  assert.equal(p.byId.get('streetInside').hidden, true);