@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.
- package/.bongos-core.json +19 -19
- package/docs/copy-inventory.md +175 -168
- package/docs/copy-registry.json +284 -220
- package/docs/design/mocks/hub/mine.html +89 -1
- package/docs/design/projects-hub-direction-v2.md +56 -0
- package/docs/module-api-changelog.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +98 -87
- package/public-landing/projects.html +310 -377
- package/public-landing/projects.probes.json +26 -21
- package/public-landing/projects.states.json +7 -5
- package/src/module-api.js +1 -1
- package/tests/projects_hub.mjs +104 -69
- package/tests/projects_hub_pre_uat.mjs +122 -78
|
@@ -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
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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
|
|
90
|
-
|
|
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('
|
|
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,
|
|
109
|
-
|
|
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('
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
assert.
|
|
326
|
-
assert.match(SCRIPT, /
|
|
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
|
|
336
|
-
|
|
337
|
-
|
|
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, /<
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
assert.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
//
|
|
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
|
|
355
|
-
assert.doesNotMatch(COSMOS,
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
assert.match(
|
|
363
|
-
assert.match(
|
|
364
|
-
assert.match(
|
|
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
|
|
371
|
-
const fold = fn('
|
|
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
|
|
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, /'
|
|
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('
|
|
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.
|
|
398
|
-
assert.match(card, /
|
|
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
|
|
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('
|
|
490
|
-
|
|
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.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
assert.match(fn('
|
|
498
|
-
assert.match(fn('
|
|
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, /
|
|
502
|
-
assert.match(fn('focusKeyOf'), /
|
|
503
|
-
assert.match(fn('focusCardControl'), /li\.querySelector\('\.actions a
|
|
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
|
-
|
|
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
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
assert.match(
|
|
565
|
-
assert.
|
|
566
|
-
assert.match(
|
|
567
|
-
assert.
|
|
568
|
-
|
|
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, /
|
|
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
|
|