@bongos/core 1.19.439 → 1.19.441

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.
@@ -79,16 +79,17 @@ test('the restart door appears ONLY where it has a reason to exist, and mirrors
79
79
  assert.match(body, /!inst\.open_intent/, 'the strip tells the story while work is in flight');
80
80
  });
81
81
 
82
- test('the restart door is offered on BOTH surfaces, and the address row points at it', () => {
83
- assert.match(fn('renderCard'), /canRestart\(inst\)[\s\S]*?data-restart=/, 'the list card');
84
- assert.match(fn('renderManage'), /canRestart\(inst\)[\s\S]*?data-m-restart/, 'the project’s own page — the two never disagree');
82
+ test('the restart door lives on the project’s own page, and the address row points at it from wherever it is read', () => {
83
+ // task 1003355: a list row carries only view and manage, so the door has ONE
84
+ // home — a second copy on the row would be the thing the owner asked to remove
85
+ assert.doesNotMatch(fn('renderCard'), /data-restart/, 'the row acts on nothing');
86
+ assert.match(fn('renderManage'), /canRestart\(inst\)[\s\S]*?data-m-restart/, 'the project’s own page');
85
87
  // the door and the sentence that sends the owner to it must ship together —
86
88
  // and the sentence must be gated by the SAME predicate, or a project with no
87
- // button (dedicated shape, or work already queued) is told to scroll to one
89
+ // button (dedicated shape, or work already queued) is told to go looking for one
88
90
  const addr = fn('addressRow');
89
- assert.match(addr, /or restart it yourself below/, 'the unreachable line names the action now that one exists');
90
- assert.match(addr, /canRestart\(inst\) \? ' — or restart it yourself below\.' : '\.'/,
91
- 'the pointer is conditioned on canRestart(), not on last_verified_up alone (ADR 0199 §8)');
91
+ assert.match(addr, /canRestart\(inst\) \? \(onManage \? ' — or restart it yourself below\.' : ' — or restart it from its own page\.'\) : '\.'/,
92
+ 'the pointer is conditioned on canRestart() and names a door the READING surface has (ADR 0199 §8)');
92
93
  });
93
94
 
94
95
  test('a queued restart has its own strip — never the generic "a <action> run is queued" fallthrough', () => {
@@ -102,11 +103,12 @@ test('a queued restart has its own strip — never the generic "a <action> run i
102
103
  });
103
104
 
104
105
  test('the restart sender posts to the restart ROUTE — re-POSTing the slug would be refused on every press', () => {
105
- const body = fn('doRestart');
106
+ const body = fn('mRestart');
106
107
  assert.match(body, /\/restart'/, 'its own endpoint');
107
108
  assert.doesNotMatch(body, /provisioning\/instances',/, 'NOT the create/re-provision POST — PROVISION_ENQUEUE_FROM excludes active');
108
- assert.match(body, /quietActions\(id\)/, 'the row goes quiet while it is in flight');
109
- assert.match(fn('mRestart'), /\/restart'/, 'and the manage page sends the same request');
109
+ assert.match(body, /\$\('mActs'\)\.querySelectorAll\('button'\)\.forEach\(function \(b\) \{ b\.disabled = true; \}\);/,
110
+ 'the doors go quiet while it is in flight');
111
+ assert.doesNotMatch(SCRIPT, /function doRestart\(/, 'and there is no second sender since the row lost its door (task 1003355)');
110
112
  });
111
113
  /* ── the moment a standup lands (task 1003243) ── */
112
114
  test('the landed moment is earned by a VERIFIED probe inside a window, not invented state', () => {
@@ -319,11 +321,14 @@ test('card metadata is span-wrapped so a separator can never strand; the manage
319
321
  assert.match(fn('repaintPlanets'), /if \(manageInst\) paintManagePlanet\(manageInst\);/, 'a catalog that lands after the manage page rendered repaints its head too');
320
322
  });
321
323
 
322
- test('an open teardown confirm survives the minute re-render', () => {
323
- assert.match(SCRIPT, /var cardAsking = null;/);
324
- assert.match(fn('renderCard'), /if \(cardAsking !== null && cardAsking === String\(inst\.id\)\) return;/);
325
- assert.match(fn('askTeardown'), /cardAsking = String\(id\);/);
326
- assert.match(SCRIPT, /function unAskTeardown\(id\) \{ cardAsking = null;/);
324
+ test('the one open teardown confirm — the ender’s — survives the minute re-render', () => {
325
+ // task 1003355: the row's two-press confirm is gone with the row's door, so
326
+ // there is exactly one confirmation on the hub and it is the typed-back name
327
+ assert.doesNotMatch(SCRIPT, /cardAsking/, 'the row holds no confirmation to protect');
328
+ assert.match(SCRIPT, /var mDelAsking = null;/);
329
+ assert.match(fn('renderDelete'), /if \(mDelAsking && mDelAsking === inst\.slug\) return;/,
330
+ 'and it is keyed by PROJECT, so a different project always repaints');
331
+ assert.match(fn('askDelete'), /mDelAsking = inst\.slug;/);
327
332
  });
328
333
 
329
334
  test('the inputs a stranger types into are labelled', () => {
@@ -332,48 +337,54 @@ test('the inputs a stranger types into are labelled', () => {
332
337
  assert.match(SCRIPT, /aria-label="A domain you own, for ' \+ esc\(inst\.slug\) \+ '"/);
333
338
  });
334
339
 
335
- /* ── the custom-domain field sits behind a fold (owner decision 2, 2026-08-24) ── */
336
- test('the domain field is a closed disclosure after the card’s doors — never the card’s loudest control', () => {
337
- const fold = fn('attachFold');
338
- assert.match(fold, /<details class="attachFold"/, 'the wizard’s address-step disclosure shape');
339
- assert.match(fold, /Use a domain I own/, 'labelled the way the wizard labels it');
340
- assert.match(fold, /inst\.status !== 'active' && inst\.status !== 'requested'\) return ''/, 'live + queued only — snagged and torn-down cards carry no fold');
341
- assert.match(fold, /if \(!canAttachDomain\(inst\)\) return ''/, 'the server-mirror gate still comes first');
342
- assert.match(fold, /inst\.open_intent\.action === 'domain-attach'\) return ''/, 'no second attach control while one is wiring');
343
- assert.doesNotMatch(fold, /Use your own domain/, 'the old filled-button label is gone');
340
+ /* ── the custom-domain field is a section of the project's own page (owner,
341
+ 2026-08-29 — task 1003355; it sat behind a fold on the list card until then) ── */
342
+ test('the row carries two doors and nothing that acts; the address field is a section of the project’s own page', () => {
344
343
  const card = fn('renderCard');
345
- assert.doesNotMatch(card, /<div class="attach">/, 'renderCard no longer inlines the bare field');
346
- const actions = card.indexOf('<div class="actions" id="act-');
347
- const foldCall = card.indexOf('body += attachFold(inst, iid);');
348
- assert.ok(actions >= 0 && foldCall > actions, 'Open site → Manage → (fold): the fold renders after the actions row');
349
- assert.match(COSMOS, /\.attachFold summary\{cursor:pointer;/, 'the summary reads as a control');
350
- // the 390px capture: the fold stacks field over button because the row wraps
351
- // and the field's minimum (220px) is narrower than a phone card's body; the
352
- // fold itself sets no width, so it can never be the thing that overflows
344
+ assert.doesNotMatch(card, /<button/, 'no sender survives on a row');
345
+ assert.doesNotMatch(card, /attachFold|<div class="attach">/, 'and no field either');
346
+ assert.match(card, /'<div class="actions">' \+ acts\.join\(''\)/, 'one doors strip, always rendered');
347
+ assert.match(card, /inst\.status === 'active' && inst\.domain\) acts\.push\([\s\S]{0,180}>View<\/a>'\)/,
348
+ 'View only where the project is live and has an address to open');
349
+ assert.match(card, /acts\.push\('<a class="btn small ghost" data-manage="' \+ iid \+ '" href="\?view=manage&id=' \+ iid \+ '">Manage<\/a>'\);/,
350
+ 'Manage on every row, as a real link');
351
+ // the section on the manage page: the heading is the disclosure, so the field
352
+ // needs no fold of its own, and the ender's hairline shapes are reused
353
+ assert.match(HUB, /<div class="card mSec" id="mAddress" hidden>/, 'a settings section like the three above it');
354
+ assert.match(HUB, /<h2>Its address<\/h2>/);
355
+ for (const id of ['mDom', 'mDomWhat']) assert.match(HUB, new RegExp('id="' + id + '"'), '#' + id + ' is real markup');
356
+ const sec = fn('renderAddress');
357
+ assert.match(sec, /canAttachDomain\(inst\) && \(inst\.status === 'active' \|\| inst\.status === 'requested'\)/,
358
+ 'the server-mirror gate first, then live + queued only — a snagged or torn-down project has nothing to point an address at');
359
+ assert.match(sec, /\$\('mAddress'\)\.hidden = !offer;/, 'and the whole section goes, not just the button');
360
+ assert.match(sec, /inst\.open_intent\.action === 'domain-attach'/, 'no second attach control while one is wiring');
361
+ assert.match(sec, /<div class="attach">/, 'the ender’s hairline field');
362
+ assert.match(sec, /data-m-attach/, 'and the manage page’s own marker');
363
+ assert.doesNotMatch(SCRIPT, /function doAttach\(/, 'the row’s sender is gone with the row’s field');
364
+ // the 390px capture: the row wraps and the field's minimum (220px) is
365
+ // narrower than a phone's body, so it can never be the thing that overflows
353
366
  assert.match(COSMOS, /\.attach\{display:flex;gap:10px;margin-top:10px;flex-wrap:wrap;\}/, 'the row wraps at phone width');
354
- assert.match(COSMOS, /\.attach input\[type=text\]\{flex:1;min-width:220px;/, 'the field fits a 390px card and grows on a laptop');
355
- assert.doesNotMatch(COSMOS, /\.attachFold[^{]*\{[^}]*(width|min-width):(?!fit-content)/, 'the fold sets no width of its own');
356
- // and in both themes: the summary reads from tokens, never a literal colour
357
- assert.doesNotMatch((COSMOS.match(/\.attachFold[^\n]*/g) || []).join('\n'), /#[0-9a-f]{3,8}\b/i, 'the fold paints from --ink-faint / --ink-soft, so dark mode needs no twin');
358
- });
359
-
360
- test('the F19 re-kick door survives the fold: open, pre-filled, one button', () => {
361
- const fold = fn('attachFold');
362
- assert.match(fold, /var rekick = inst\.status === 'active' && inst\.domain && inst\.error_note;/);
363
- assert.match(fold, /\(rekick \? ' open' : ''\)/, 'the fold opens on its own for a stalled address');
364
- assert.match(fold, /\(prefill \? ' value="' \+ esc\(inst\.domain\) \+ '"' : ''\)/, 'pre-filled with the address in play');
365
- assert.match(fold, /var prefill = rekick \|\| repoint;/, 'a stalled address and a correctable one both arrive in the field');
366
- assert.match(fold, /rekick \? 'Try the address again' : repoint \? 'Change the address' : 'Use this domain'/, 'and the button says what it does');
367
+ assert.match(COSMOS, /\.attach input\[type=text\]\{flex:1;min-width:220px;/, 'the field fits a 390px page and grows on a laptop');
368
+ assert.doesNotMatch(COSMOS, /attachFold/, 'the fold’s rules went with the fold');
369
+ });
370
+
371
+ test('the F19 re-kick door survives the move: pre-filled, one button, its own sentence', () => {
372
+ const sec = fn('renderAddress');
373
+ assert.match(sec, /var rekick = inst\.status === 'active' && inst\.domain && inst\.error_note;/);
374
+ assert.match(sec, /\(prefill \? ' value="' \+ esc\(inst\.domain\) \+ '"' : ''\)/, 'pre-filled with the address in play');
375
+ assert.match(sec, /var prefill = rekick \|\| repoint;/, 'a stalled address and a correctable one both arrive in the field');
376
+ assert.match(sec, /rekick \? 'Try the address again' : repoint \? 'Change the address' : 'Use this domain'/, 'and the button says what it does');
377
+ assert.match(sec, /rekick \? 'The wiring for this address stopped\./, 'the section says WHY the field is pre-filled — the fold said it by opening itself');
367
378
  assert.match(fn('canAttachDomain'), /inst\.status === 'active' && inst\.domain && inst\.error_note && !inst\.open_intent\) return true;/, 'the route-side re-kick gate is untouched');
368
379
  });
369
380
 
370
- test('the correction door: a bring-your-own address can be changed, and the fold says what that costs (task 1003237)', () => {
371
- const fold = fn('attachFold');
381
+ test('the correction door: a bring-your-own address can be changed, and the section says what that costs (task 1003237)', () => {
382
+ const fold = fn('renderAddress');
372
383
  // the three doors are told apart by what the project is actually addressed as
373
384
  assert.match(fold, /var byo = inst\.domain && inst\.domain !== assignedDefaultFor\(inst\);/,
374
- 'the platform-assigned <slug>.<apex> is NOT a correction — moving off it is the upgrade the fold already offered');
385
+ 'the platform-assigned <slug>.<apex> is NOT a correction — moving off it is the upgrade this control already offered');
375
386
  assert.match(fold, /var repoint = !rekick && byo;/, 'a stalled address keeps the F19 door, which is louder');
376
- assert.match(fold, /'Use a different domain'/, 'the summary names the correction');
387
+ assert.match(fold, /repoint \? 'It answers on an address you brought\./, 'the lead names the correction');
377
388
  assert.match(fold, /attachNote/, 'and the consequence is stated where the button is');
378
389
  assert.match(fold, /won’t use that address any more/, 'letting the old address go is SAID, not discovered');
379
390
  // the door itself: an addressed, live project is no longer excluded
@@ -392,13 +403,23 @@ test('the retired refusal is gone from the card: domain_already_set can no longe
392
403
  'the server stopped sending it (task 1003237) — a branch for it would be unreachable copy');
393
404
  });
394
405
 
395
- test('a heartbeat re-render keeps an open fold open and a half-typed domain typed', () => {
406
+ test('the minute tick keeps a half-typed domain typed, and its receipt on the page', () => {
407
+ // the rule moved with the field (task 1003355): renderManage repaints every
408
+ // section once a minute, so the address section must survive its own repaint
409
+ const sec = fn('renderAddress');
410
+ assert.match(sec, /old\.value !== old\.defaultValue \? old\.value : ''/, 'typing is told apart from the pre-fill');
411
+ assert.match(sec, /document\.activeElement === old/, 'the caret comes back too');
412
+ // lastIndexOf: the in-flight branch above writes the same slot, and returns
413
+ const set = sec.lastIndexOf("$('mDom').innerHTML =");
414
+ assert.ok(sec.indexOf('var typed =') < set && sec.indexOf('if (field && typed) field.value = typed;') > set,
415
+ 'read before the swap, restore after it');
416
+ assert.match(sec, /if \(mDomFor !== inst\.id\) \{ mDomFor = inst\.id; mDomNote = null; \}/,
417
+ 'and the receipt is keyed by PROJECT, so a different project never wears the last one’s');
418
+ assert.match(sec, /\(mDomNote \? mDomNote\.html : ''\)/, 'a repaint re-renders the receipt rather than swallowing it');
419
+ // the row keeps nothing to restore but the door that had focus
396
420
  const card = fn('renderCard');
397
- assert.match(card, /var keepOpen = !!\(oldFold && oldFold\.open\);/);
398
- assert.match(card, /oldInput\.value !== oldInput\.defaultValue \? oldInput\.value : ''/, 'typing is told apart from the re-kick pre-fill');
399
- assert.match(card, /document\.activeElement === oldInput/, 'the caret comes back too');
400
- const set = card.indexOf('li.innerHTML = orbFor(inst)');
401
- assert.ok(card.indexOf('var keepOpen') < set && card.indexOf('if (fold && keepOpen) fold.open = true;') > set, 'read before the swap, restore after it');
421
+ assert.doesNotMatch(card, /oldFold|oldInput|keepOpen/, 'the row holds no field or fold to carry across the swap');
422
+ assert.match(card, /if \(wasInside\) focusCardControl\(inst\.id, prefer\);/);
402
423
  });
403
424
 
404
425
  /* ── the owner's five calls of 2026-08-24 (task 1003250) ── */
@@ -464,7 +485,18 @@ test('an unmapped create failure gets a plain sentence with the raw text folded
464
485
  test('a manifest error says a plain sentence, folds the route text, and sends a domainless project to its address', () => {
465
486
  const steps = fn('renderStepOauthApp');
466
487
  assert.doesNotMatch(steps, /'GitHub setup hit a snag' \+ \(live\.manifestMsg \? ': ' \+ esc\(live\.manifestMsg\)/, 'the raw route text is no longer the headline');
467
- assert.match(steps, /else if \(live\.manifestCode === 'domain_required'\) snag = 'This project needs an address first — attach one from <a href="\?view=mine">your projects page<\/a>/);
488
+ assert.match(steps, /else if \(live\.manifestCode === 'domain_required'\) snag = 'This project needs an address first — attach one from ' \+ projectPageLink\('its own page'\)/,
489
+ 'the address lives on the project’s own page since task 1003355 — the list would be a dead end');
490
+ assert.match(fn('projectPageLink'), /instanceId \? '\?view=manage&id=' \+ encodeURIComponent\(instanceId\) : '\?view=mine'/,
491
+ 'and it is named by id whenever the flow has one');
492
+ // every done-panel pointer that names a CONTROL goes the same way; the ones
493
+ // that only say "watch it there" still name the list, which does show it
494
+ assert.equal((SCRIPT.match(/projectPageLink\(/g) || []).length, 6,
495
+ 'the helper and its five callers: the locked step, the domain_required snag, the domainless standup, the snagged retry, the torn-down revive');
496
+ for (const dead of ['Retry it \\(and see the technical details\\) from <a href="\\?view=mine">',
497
+ 'Bring it back any time from <a href="\\?view=mine">']) {
498
+ assert.doesNotMatch(SCRIPT, new RegExp(dead), 'a control the list no longer has must not be pointed at there');
499
+ }
468
500
  assert.match(steps, /\(raw \? '<details><summary>Technical details<\/summary><p>' \+ esc\(raw\) \+ '<\/p><\/details>' : ''\)/);
469
501
  assert.match(SCRIPT, /e\.code = \(d\.error && d\.error\.code\) \|\| '';/, 'the route code travels with the error');
470
502
  assert.match(SCRIPT, /live\.manifestCode = String\(\(err && err\.code\) \|\| ''\);/);
@@ -486,21 +518,24 @@ test('the rail at the done panel keeps its marker on Create until the project an
486
518
  assert.match(HUB, /<li class="now" data-step="1" aria-current="step">/, 'the static rail starts with the same marker');
487
519
  });
488
520
 
489
- test('card action results announce, nothing under the pointer is deleted, and focus stays on the card', () => {
490
- assert.match(fn('renderCard'), /id="msg-' \+ iid \+ '" role="status" aria-live="polite">/, 'the card slot is a live region');
521
+ test('every action result announces, nothing under the pointer is deleted, and focus stays where it was', () => {
522
+ // task 1003355 moved every sender to the project's own page, so the live
523
+ // regions that matter are that page's; the row keeps its own for the watch's
524
+ // "can't see this project any more"
525
+ assert.match(fn('renderCard'), /id="msg-' \+ iid \+ '" role="status" aria-live="polite">/, 'the row slot is a live region');
491
526
  assert.match(HUB, /<div class="projMsg" id="mMsg" role="status" aria-live="polite"><\/div>/, 'so is the manage page’s');
492
- assert.doesNotMatch(SCRIPT, /act\.innerHTML = '<span class="projMsg">Asking…<\/span>'/, 'no action replaces the row it was pressed in');
493
- assert.match(fn('doTeardown'), /quietActions\(id\);\n\s*msg\(id, 'Asking…'\);/);
494
- assert.match(fn('reProvision'), /quietActions\(id\);\n\s*msg\(id, 'Asking…'\);/);
495
- assert.match(fn('quietActions'), /act\.querySelectorAll\('button'\)\.forEach\(function \(b\) \{ b\.disabled = true; \}\);/);
496
- assert.match(fn('quietActions'), /li\.setAttribute\('data-focus-back', ''\)/, 'a disabled button that held focus leaves a note');
497
- assert.match(fn('askTeardown'), /<div class="confirmRow" tabindex="-1">/);
498
- assert.match(fn('askTeardown'), /if \(row && row\.focus\) row\.focus\(\);/, 'the question takes focus when the button goes');
527
+ assert.match(fn('renderAddress'), /id="mDomMsg" role="status" aria-live="polite"/, 'and the address section’s own');
528
+ for (const sender of ['mReProvision', 'mRestart']) {
529
+ assert.match(fn(sender), /\$\('mActs'\)\.querySelectorAll\('button'\)\.forEach\(function \(b\) \{ b\.disabled = true; \}\);/,
530
+ sender + ' quiets the doors while it is in flight');
531
+ }
532
+ assert.match(fn('mAttach'), /if \(btn\) btn\.disabled = true;/, 'and so does the address control');
533
+ assert.match(fn('askDelete'), /if \(field\) field\.focus\(\);/, 'the typed-back name takes focus when the ask opens');
499
534
  const card = fn('renderCard');
500
535
  assert.match(card, /var wasInside = li\.contains\(document\.activeElement\) && document\.activeElement !== li;/);
501
- assert.match(card, /else if \(focusBack\) focusCardControl\(inst\.id, prefer\);/);
502
- assert.match(fn('focusKeyOf'), /data-teardown-yes'\) \|\| el\.hasAttribute\('data-teardown-no'\)\) return '\[data-teardown\]'/, 'Yes / Keep land back on the door that opened them');
503
- assert.match(fn('focusCardControl'), /li\.querySelector\('\.actions a, \.actions button'\) \|\|\n\s*li\.querySelector\('details\.attachFold summary'\)/, 'nearest control as the fallback');
536
+ assert.match(card, /if \(wasInside\) focusCardControl\(inst\.id, prefer\);/);
537
+ assert.match(fn('focusKeyOf'), /el\.hasAttribute\('data-manage'\) \? '\[data-manage\]' : null/, 'Manage lands back on Manage');
538
+ assert.match(fn('focusCardControl'), /li\.querySelector\('\.actions a'\)/, 'nearest door as the fallback');
504
539
  });
505
540
 
506
541
  /* ── copy follow-ups (task 1003245) ── */
@@ -539,7 +574,14 @@ test('repo visibility: the refusal is one line, the mechanism lives in the fold'
539
574
  test('a failed manage read offers a retry; the no-address sentence offers a door', () => {
540
575
  assert.match(SCRIPT, /a\.innerHTML = 'Couldn’t read this project right now\. <button type="button" class="btn small ghost" id="mAlertRetry">Try again<\/button>';/);
541
576
  assert.match(SCRIPT, /\$\('mAlert'\)\.addEventListener\('click', function \(ev\) \{\n\s*if \(ev\.target\.closest && ev\.target\.closest\('#mAlertRetry'\)\) loadManage\(\);/, 'delegated — the catch rewrites the alert on every failure');
542
- assert.match(fn('addressRow'), /No address yet<\/span> — sign-in on this project can’t be set up until it has one\. <a href="\?view=mine" data-nav-to="mine">Give it an address from your projects<\/a>\./);
577
+ // the door the sentence offers must be one the READING surface has: on the
578
+ // project's own page it is the section below, on a row it is that project's
579
+ // page — never the list, which since task 1003355 carries no field at all
580
+ const addr = fn('addressRow');
581
+ assert.match(addr, /No address yet<\/span> — sign-in on this project can’t be set up until it has one\. '/);
582
+ assert.match(addr, /onManage \? 'Give it an address below\.'/);
583
+ assert.match(addr, /'<a href="\?view=manage&id=' \+ esc\(String\(inst\.id\)\) \+ '" data-manage="' \+ esc\(String\(inst\.id\)\) \+ '">Give it an address on its own page<\/a>\.'/,
584
+ 'and it carries data-manage, so the list’s delegated handler opens it in place');
543
585
  assert.match(SCRIPT, /var a = ev\.target\.closest && ev\.target\.closest\('\[data-nav-to\]'\);\n\s*if \(!a \|\| !isPlainClick\(ev\)\) return;/, 'data-nav-to is delegated, so a link injected after boot switches views in place');
544
586
  assert.doesNotMatch(SCRIPT, /querySelectorAll\('\[data-nav-to\]'\)/, 'the boot-time sweep is gone');
545
587
  });
@@ -558,20 +600,22 @@ test('the done panel’s progress region is stable, change-only, and says each s
558
600
  assert.match(fn('stripMarkup'), /role="status" aria-live="polite"/, 'the mine-card strip is left alone this pass');
559
601
  });
560
602
 
561
- test('the map table sorts from the keyboard and an owned row’s name is a real link', () => {
562
- assert.match(HUB, /<th class="colName" data-sort="name" aria-sort="ascending"><button type="button" class="thSort">Project<\/button><\/th>/);
563
- assert.match(HUB, /<th class="right colBuilders" data-sort="builders"><button type="button" class="thSort">Builders<\/button><\/th>/);
564
- assert.match(SCRIPT, /var btn = th\.querySelector\('button'\);\n\s*if \(!btn\) return;\n\s*btn\.addEventListener\('click'/, 'the listener sits on the button, not the th');
565
- assert.doesNotMatch(SCRIPT, /th\.style\.cursor/, 'no JS-set cursor on the th');
566
- assert.match(COSMOS, /\.tbl th button\{all:unset;cursor:pointer;\}/);
567
- assert.match(COSMOS, /\.tbl th button:focus-visible\{outline:2px solid var\(--accent\);outline-offset:2px;\}/, 'keyboard focus on the sort button is visible');
568
- const generated = (SCRIPT.match(/return '<tr data-generated([\s\S]*?)'<\/tr>';/) || [, ''])[1];
603
+ test('the map list sorts from the keyboard and an owned row’s name is a real link', () => {
604
+ // the sort was a button inside a <th> until task 1003356 took the header row
605
+ // away with the table; it is a pill in a group now, and still a real button
606
+ assert.match(HUB, /<button type="button" class="sortPill" data-sort="name" aria-pressed="true">name <span class="dir">a to z<\/span><\/button>/);
607
+ assert.match(HUB, /<button type="button" class="sortPill" data-sort="builders" aria-pressed="false">builders<\/button>/);
608
+ assert.match(SCRIPT, /document\.querySelectorAll\('\.tblSort button\[data-sort\]'\)\.forEach\(function \(btn\) \{\r?\n\s*btn\.addEventListener\('click'/, 'the listener sits on the button itself');
609
+ assert.doesNotMatch(SCRIPT, /th\.style\.cursor/, 'no JS-set cursor on a header that no longer exists');
610
+ assert.match(COSMOS, /\.tblSort button\{[^}]*min-height:36px;/, 'the sort pill clears the 24px floor on its own, with no all:unset to undo');
611
+ assert.match(COSMOS, /\.tblSort button:focus-visible\{outline:2px solid var\(--accent\);outline-offset:2px;\}/, 'keyboard focus on the sort button is visible');
612
+ const generated = (SCRIPT.match(/return '<li class="tblRow" data-generated([\s\S]*?)'<\/li>';/) || [, ''])[1];
569
613
  assert.match(generated, /<a class="ptitle" data-manage="' \+ esc\(String\(p\._inst\.id\)\) \+ '" href="\?view=manage&id=' \+ esc\(String\(p\._inst\.id\)\) \+ '">/, 'an owned row’s title is a manage anchor');
570
614
  const rows = SCRIPT.slice(SCRIPT.indexOf("$('mapRows').addEventListener('click'"));
571
615
  const branch = rows.indexOf("closest('a[data-manage]')");
572
616
  const bail = rows.indexOf("if (ev.target.closest && ev.target.closest('a')) return;");
573
617
  assert.ok(branch > 0 && bail > branch, 'the manage branch runs before the anchor bail-out');
574
- assert.match(HUB, /Sort by Project or Builders\./, 'the hint names the two sortable columns');
618
+ assert.match(HUB, /Sorted by name, A to Z\./, 'the live order is stated where the hint used to name the columns');
575
619
  assert.match(COSMOS, /\.tbl \.ptitle\{[^}]*text-decoration:none;\}/, 'the link wears the title’s ink, not the UA underline');
576
620
  });
577
621