@bongos/core 1.21.163 → 1.21.165
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 +60 -45
- package/.gitattributes +6 -0
- package/docs/copy-inventory.md +127 -127
- package/docs/copy-registry.json +205 -185
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +297 -462
- package/modules/copy-desk/page-status.js +3 -0
- package/modules/copy-desk/pages.js +5 -0
- package/modules/hall-ui/public/approval-queue-lib.js +4 -0
- package/modules/hall-ui/public/approval-queue.css +2 -0
- package/modules/hall-ui/public/approval-queue.js +1 -0
- package/modules/hall-ui/public/builders.js +6 -4
- package/modules/hall-ui/public/craft-mode.js +32 -158
- package/modules/hall-ui/public/craft-review.css +7 -120
- package/modules/hall-ui/public/goal-inbox.js +100 -7
- package/modules/hall-ui/public/goals.js +2 -4
- package/modules/hall-ui/public/hall-render.js +39 -9
- package/modules/hall-ui/public/hall-widgets.js +20 -10
- package/modules/hall-ui/public/index.html +7 -0
- package/modules/hall-ui/public/page-notes.css +64 -1
- package/modules/hall-ui/public/page-notes.js +96 -11
- package/modules/hall-ui/public/style.css +95 -0
- package/modules/hall-ui/public/task-detail.js +8 -0
- package/modules/hall-ui/records/home-reshuffle.md +31 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/scripts/gds/copy-apply.js +65 -2
- package/scripts/gds/git-merge-regen.js +14 -0
- package/scripts/gds/page-reader.js +46 -0
- package/src/module-api.js +1 -1
- package/tests/conflict_resolve.mjs +6 -0
- package/tests/craft_review_mode.mjs +21 -104
- package/tests/hall_approval_queue.mjs +16 -0
- package/tests/hall_board_world.mjs +7 -6
- package/tests/home_attention_cards.mjs +119 -0
- package/tests/home_bounded_reads.mjs +1 -1
- package/tests/page_notes_panel.mjs +161 -27
- package/tests/promote_goal_id.mjs +8 -0
- package/tests/tweak_batch_apply.mjs +40 -0
- package/tests/tweak_land_heal.mjs +139 -0
|
@@ -4565,3 +4565,98 @@ html.palette-open { overflow: hidden; }
|
|
|
4565
4565
|
.gr-cat { display: inline-flex; align-items: baseline; gap: 5px; margin-left: 8px; padding: 1px 8px; border: 1px solid var(--rule-soft); border-radius: 999px; font-size: 11px; font-weight: 600; letter-spacing: .01em; color: var(--ink-soft); background: var(--surface-2); vertical-align: baseline; white-space: nowrap; }
|
|
4566
4566
|
.gr-cat--inherited { font-weight: 550; color: var(--ink-faint); }
|
|
4567
4567
|
.gr-cat__from { font-style: normal; font-weight: 500; font-size: 10px; color: var(--ink-faint); opacity: .85; }
|
|
4568
|
+
|
|
4569
|
+
/* ---- Home reshuffle (task 1004774; mock docs/design/mocks/hall/home/v1.html) ----
|
|
4570
|
+
A slimmer banner pulled up under the greeting, the version as a progress tab
|
|
4571
|
+
under it, Standing as a chip at the top right with a hover card, the
|
|
4572
|
+
Needs-attention slices as cards that open a window, and Your work beside
|
|
4573
|
+
Recently shipped. */
|
|
4574
|
+
body[data-page="home"] .page-head--hall { position: relative; margin: 0 0 12px; padding-right: 180px; }
|
|
4575
|
+
body[data-page="home"] .stat-tile { padding: 12px 22px; min-height: 0; gap: 4px; }
|
|
4576
|
+
body[data-page="home"] .stat-tile__num { font-size: clamp(20px, 2vw, 26px); }
|
|
4577
|
+
body[data-page="home"] #home-stats .stat-row { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
|
4578
|
+
|
|
4579
|
+
.version-tab {
|
|
4580
|
+
display: flex; align-items: center; gap: 14px; min-height: 24px;
|
|
4581
|
+
padding: 10px 22px; color: var(--ink); text-decoration: none;
|
|
4582
|
+
background: var(--glass); border: 1px solid var(--glass-edge); border-top: 0;
|
|
4583
|
+
border-radius: 0 0 var(--radius) var(--radius);
|
|
4584
|
+
}
|
|
4585
|
+
.version-tab:hover { text-decoration: none; background: var(--glass-strong); }
|
|
4586
|
+
.version-tab[hidden] { display: none; }
|
|
4587
|
+
.version-tab__name { font-weight: 600; white-space: nowrap; }
|
|
4588
|
+
.version-tab__bar { flex: 1; height: 8px; border-radius: 4px; background: var(--rule-soft); overflow: hidden; }
|
|
4589
|
+
.version-tab__fill { display: block; height: 100%; width: 0; background: var(--accent); border-radius: 4px; }
|
|
4590
|
+
.version-tab__pct { color: var(--ink-soft); font-size: 13px; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
4591
|
+
|
|
4592
|
+
.hall-standing { position: absolute; top: 0; right: 0; height: 44px; outline: none; }
|
|
4593
|
+
.hall-standing[hidden] { display: none; }
|
|
4594
|
+
/* The chip IS the top of the card: anchored at the right, it grows LEFT as the
|
|
4595
|
+
name slides in, and only .hall-standing__more drops down below it. */
|
|
4596
|
+
.hall-standing__card {
|
|
4597
|
+
position: absolute; top: 0; right: 0; z-index: 20; min-width: 0;
|
|
4598
|
+
background: var(--bg-card); border-radius: 22px; padding: 4px 6px 4px 4px;
|
|
4599
|
+
transition: min-width .2s ease, border-radius .2s ease, padding .2s ease, box-shadow .2s ease;
|
|
4600
|
+
}
|
|
4601
|
+
.hall-standing__card .home-who { gap: 8px; padding: 0; }
|
|
4602
|
+
.hall-standing__card .home-who__avatar { width: 36px; height: 36px; }
|
|
4603
|
+
.hall-standing__card .home-who__meta {
|
|
4604
|
+
max-width: 0; opacity: 0; overflow: hidden; white-space: nowrap;
|
|
4605
|
+
transition: max-width .2s ease, opacity .2s ease;
|
|
4606
|
+
}
|
|
4607
|
+
.hall-standing__more {
|
|
4608
|
+
max-height: 0; overflow: hidden; opacity: 0;
|
|
4609
|
+
transition: max-height .25s ease, opacity .2s ease;
|
|
4610
|
+
}
|
|
4611
|
+
.hall-standing__more .home-facts { margin-top: 12px; }
|
|
4612
|
+
.hall-standing__more .card-foot { margin: 12px 0 0; }
|
|
4613
|
+
.hall-standing:hover .hall-standing__card,
|
|
4614
|
+
.hall-standing:focus-within .hall-standing__card {
|
|
4615
|
+
min-width: 300px; border-radius: var(--radius); padding: 14px 16px 16px;
|
|
4616
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, .35);
|
|
4617
|
+
}
|
|
4618
|
+
.hall-standing:hover .home-who__meta,
|
|
4619
|
+
.hall-standing:focus-within .home-who__meta { max-width: 200px; opacity: 1; }
|
|
4620
|
+
.hall-standing:hover .hall-standing__more,
|
|
4621
|
+
.hall-standing:focus-within .hall-standing__more { max-height: 420px; opacity: 1; }
|
|
4622
|
+
.hall-standing:focus-visible .hall-standing__card { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
4623
|
+
|
|
4624
|
+
.attn-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
|
|
4625
|
+
.attn-card {
|
|
4626
|
+
display: flex; flex-direction: column; align-items: flex-start; gap: 6px; min-height: 100px;
|
|
4627
|
+
padding: 16px 18px; text-align: left; font: inherit; color: var(--ink); cursor: pointer;
|
|
4628
|
+
background: var(--glass); border: 1px solid var(--glass-edge); border-radius: var(--radius);
|
|
4629
|
+
}
|
|
4630
|
+
.attn-card:hover { border-color: var(--accent); }
|
|
4631
|
+
.attn-card__n { font-family: var(--font-display); font-size: 30px; font-weight: 300; line-height: 1; font-variant-numeric: tabular-nums; }
|
|
4632
|
+
.attn-card__t { font-weight: 600; }
|
|
4633
|
+
.attn-card__s { margin-top: auto; font-size: 12px; color: var(--ink-soft); }
|
|
4634
|
+
.attn-dialog {
|
|
4635
|
+
width: min(640px, calc(100vw - 32px)); max-height: 80vh; padding: 22px 24px; overflow: auto;
|
|
4636
|
+
background: var(--bg-card); color: var(--ink); border: 0; border-radius: var(--radius);
|
|
4637
|
+
}
|
|
4638
|
+
.attn-dialog::backdrop { background: var(--scrim); }
|
|
4639
|
+
.attn-dialog__head { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
|
|
4640
|
+
.attn-dialog__h { flex: 1; margin: 0; font-family: var(--font-display); font-weight: 500; font-size: 20px; }
|
|
4641
|
+
.attn-dialog__x { min-width: 32px; min-height: 32px; background: none; border: 0; color: var(--ink-soft); font-size: 22px; cursor: pointer; }
|
|
4642
|
+
.attn-dialog__body .ginbox__sec-h { display: none; }
|
|
4643
|
+
|
|
4644
|
+
.home-split { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-items: start; }
|
|
4645
|
+
.home-split__col { min-width: 0; }
|
|
4646
|
+
.home-split > .home-split__col[hidden] + .home-split__col { grid-column: 1 / -1; }
|
|
4647
|
+
|
|
4648
|
+
@media (max-width: 760px) {
|
|
4649
|
+
body[data-page="home"] .page-head--hall { padding-right: 0; }
|
|
4650
|
+
.hall-standing { position: relative; display: block; margin-top: 10px; }
|
|
4651
|
+
.hall-standing__card { left: 0; right: auto; }
|
|
4652
|
+
.hall-standing:hover .hall-standing__card,
|
|
4653
|
+
.hall-standing:focus-within .hall-standing__card { min-width: min(300px, calc(100vw - 32px)); }
|
|
4654
|
+
.home-split { grid-template-columns: 1fr; }
|
|
4655
|
+
}
|
|
4656
|
+
/* With Standing gone from the band, a lone card there (Goals) takes the row. */
|
|
4657
|
+
.view-zone--band > .scroll:only-child { grid-column: 1 / -1; }
|
|
4658
|
+
/* The Standing hover card's stat row: four figures in a 2x2 grid, label over number. */
|
|
4659
|
+
.home-facts--grid { grid-template-columns: 1fr 1fr; gap: 10px 14px; padding: 12px 0; border-top: 1px solid var(--rule-soft); border-bottom: 1px solid var(--rule-soft); }
|
|
4660
|
+
.home-facts--grid > div { display: flex; flex-direction: column; gap: 2px; }
|
|
4661
|
+
.home-facts--grid dt, .home-facts--grid dd { border-top: 0; padding: 0; text-align: left; }
|
|
4662
|
+
.home-facts--grid dd { font-family: var(--font-display); font-size: 20px; font-weight: 300; }
|
|
@@ -450,6 +450,12 @@
|
|
|
450
450
|
var priOptions = [1, 2, 3, 4, 5].map(function (p) {
|
|
451
451
|
return '<option value="' + p + '"' + (p === pri ? ' selected' : '') + '>P' + p + '</option>';
|
|
452
452
|
}).join('');
|
|
453
|
+
// A create with no discipline the server cannot infer from the wording is
|
|
454
|
+
// refused (task 1002988), so the builder names the craft here. Engineer is
|
|
455
|
+
// the default, the same fallback bug routing uses (ideas/inbox.js).
|
|
456
|
+
var craftOptions = [['engineer', 'Engineer'], ['artist', 'Artist'], ['ideator', 'Ideator'], ['ui', 'UI']].map(function (c) {
|
|
457
|
+
return '<option value="' + c[0] + '"' + (c[0] === 'engineer' ? ' selected' : '') + '>' + c[1] + '</option>';
|
|
458
|
+
}).join('');
|
|
453
459
|
box.innerHTML =
|
|
454
460
|
'<form class="td-form" data-form="promote">'
|
|
455
461
|
+ '<label class="td-field td-field--wide"><span>Title</span>'
|
|
@@ -457,6 +463,7 @@
|
|
|
457
463
|
+ '<label class="td-field td-field--wide"><span>Version</span><select name="version">' + options + '</select></label>'
|
|
458
464
|
+ '<label class="td-field td-field--wide"><span>Goal</span><select name="goal" data-goal-select>' + goalOptions + '</select></label>'
|
|
459
465
|
+ '<label class="td-field"><span>Priority</span><select name="priority">' + priOptions + '</select></label>'
|
|
466
|
+
+ '<label class="td-field"><span>Craft</span><select name="discipline">' + craftOptions + '</select></label>'
|
|
460
467
|
+ '<label class="td-field"><span>Estimate (min)</span>'
|
|
461
468
|
+ '<input type="number" name="est" min="0" max="100000" step="5" placeholder="—"></label>'
|
|
462
469
|
+ '<label class="td-field"><span>Reward</span>'
|
|
@@ -504,6 +511,7 @@
|
|
|
504
511
|
title: title,
|
|
505
512
|
description: body + provenance,
|
|
506
513
|
priority: Number(form.priority.value) || 3,
|
|
514
|
+
discipline: (form.discipline && form.discipline.value) || 'engineer',
|
|
507
515
|
source: 'idea_inbox',
|
|
508
516
|
source_ref: 'idea:' + idea.id,
|
|
509
517
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# The Home reshuffle — slim banner, version tab, Standing chip, attention cards with windows (task 1004774)
|
|
2
|
+
|
|
3
|
+
The owner's Builders' Hall feedback (idea 1001262), home-page half, built to the approved mock at
|
|
4
|
+
[`docs/design/mocks/hall/home/v1.html`](../../../docs/design/mocks/hall/home/v1.html).
|
|
5
|
+
|
|
6
|
+
- **Banner + version tab.** The stat row is slimmer (`body[data-page="home"]` overrides), and under it
|
|
7
|
+
`#home-version` draws the version the greeting's sub-line names, from the same `currentVersion()`
|
|
8
|
+
read — `renderVersionTab` in `hall-render.js`. Hidden while nothing is being built.
|
|
9
|
+
- **Standing is a chip, not a card.** The `home-standing` widget is gone; `#hall-standing` sits in the
|
|
10
|
+
page head (it is chrome, not a `<section class="scroll">`, so the registry rule is untouched). The
|
|
11
|
+
card it opens on hover or keyboard focus is still `#home-standing-body`, filled by `renderProfile`.
|
|
12
|
+
The place row reads "Leaderboard place" — the owner asked for no "on the board" wording. The chip
|
|
13
|
+
IS the top of the card (owner, 2026-10-23): at rest it shows the avatar and rank with no text and
|
|
14
|
+
no link, and on hover or focus it widens LEFT as the name slides in, while only
|
|
15
|
+
`.hall-standing__more` (the facts and a "View full profile" link to /profile) drops down below it.
|
|
16
|
+
- **Needs attention is cards.** Every slice still renders into its own div exactly as before, kept in
|
|
17
|
+
the hidden `#attention-slices`. `OTBAttention.refresh()` rebuilds one card per non-empty slice from
|
|
18
|
+
the slice's heading (title, count pill = the server total, the `.ginbox__sub` note). Opening a card
|
|
19
|
+
MOVES the slice div into `#attention-dialog`, which sits inside the widget host — so the delegated
|
|
20
|
+
click handler still sees every button and no slice needed a second action path. Closing moves it back.
|
|
21
|
+
`tests/home_attention_cards.mjs` runs the real file in a browser: card totals, open, X, Escape, and a
|
|
22
|
+
window whose slice empties while open.
|
|
23
|
+
- **Goals card** lost its subtitle, and its foot is one "Browse goals" link; the "not joined" line shows only when you have joined none.
|
|
24
|
+
- **Next up is five rows**, level with the five Recently shipped rows, asked for AND capped client-side.
|
|
25
|
+
- **Renamed:** "Stuck on the owner" → "Requires project owner fix"; "Going quiet" → "Things not moving".
|
|
26
|
+
- **Your work beside Recently shipped** — `.home-split` in the `home-recent` widget; shipped takes the
|
|
27
|
+
full row when Next up is hidden, and both stack under 760px.
|
|
28
|
+
|
|
29
|
+
Not built, because the data does not exist yet: the "Tasks you are blocking in other goals"
|
|
30
|
+
(requested / total) card, and the any-owner / main-owner split on "Requires project owner fix" —
|
|
31
|
+
blockers carry no addressee today.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.165",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.165",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.165",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -10020,5 +10020,25 @@
|
|
|
10020
10020
|
"id": "1004773",
|
|
10021
10021
|
"text": "The Private option on the account page now opens with the artist line \"you are a ninja.\", and still tells people Private hides their page from everyone, them included."
|
|
10022
10022
|
}
|
|
10023
|
+
],
|
|
10024
|
+
"1.21.164": [
|
|
10025
|
+
{
|
|
10026
|
+
"id": "1004772",
|
|
10027
|
+
"text": "Promoting an idea from the home page's Triage button no longer fails with \"discipline was not supplied\". The Promote form now has a Craft choice (Engineer by default), so every promoted idea becomes a task."
|
|
10028
|
+
},
|
|
10029
|
+
{
|
|
10030
|
+
"id": "1004775",
|
|
10031
|
+
"text": "Approved page tweaks now land on their own. When two rounds were applied from the same main, the second used to hit a conflict in the page readings file and wait for a person; that file now merges page by page, so the second r"
|
|
10032
|
+
},
|
|
10033
|
+
{
|
|
10034
|
+
"id": "1004774",
|
|
10035
|
+
"text": "Home got the owner's reshuffle: a slimmer banner with the version's progress bar under it; your picture and rank at the top right, opening on hover into your standing card with a View full profile link; every Needs-attention s"
|
|
10036
|
+
}
|
|
10037
|
+
],
|
|
10038
|
+
"1.21.165": [
|
|
10039
|
+
{
|
|
10040
|
+
"id": "1004771",
|
|
10041
|
+
"text": "Artist review is now a small tab on the right edge of every hall page in artist mode. Hover the tab and the review panel slides out; move the cursor back onto the page and it tucks away, keeping your draft. The page stays scro"
|
|
10042
|
+
}
|
|
10023
10043
|
]
|
|
10024
10044
|
}
|
|
@@ -458,17 +458,73 @@ function applyBatchToSources({ batch, registry, readSource }) {
|
|
|
458
458
|
return { applied: applied.sort(byOrder).map(({ order, ...rest }) => rest), refused, sources };
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
+
// findPinningTests({ applied, listTests, readFile }) -> [{ key, tests: [rel…] }]
|
|
462
|
+
//
|
|
463
|
+
// Task 1004775. A test that asserts a line's OLD words verbatim goes red the
|
|
464
|
+
// moment the round lands, which used to surface only in CI, after the artist had
|
|
465
|
+
// approved, and needed a person. So at APPLY time every applied line's old text is
|
|
466
|
+
// searched for in the test files (tests/ and every module's own tests folder), and each test that
|
|
467
|
+
// still carries it is named in the applied block and shown in the Approval queue.
|
|
468
|
+
// Nothing is edited: a test may be guarding what the words MEAN, and only a person
|
|
469
|
+
// can tell that apart from a test that pins the wording alone.
|
|
470
|
+
//
|
|
471
|
+
// The needle is the line's old text; where it has {…} holes, its longest literal
|
|
472
|
+
// run between them, and a needle under MIN_PIN_NEEDLE characters is skipped (a
|
|
473
|
+
// two-word label matches half the suite and would name nothing useful).
|
|
474
|
+
const MIN_PIN_NEEDLE = 12;
|
|
475
|
+
const PIN_TEST_RE = /\.(mjs|cjs|js)$/;
|
|
476
|
+
|
|
477
|
+
function pinNeedle(text) {
|
|
478
|
+
const runs = String(text == null ? '' : text).split('{…}').map((s) => s.trim());
|
|
479
|
+
const longest = runs.reduce((a, b) => (b.length > a.length ? b : a), '');
|
|
480
|
+
return longest.length >= MIN_PIN_NEEDLE ? longest : null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function listTestFiles({ root = REPO_ROOT, fsImpl = fs } = {}) {
|
|
484
|
+
const out = [];
|
|
485
|
+
const walk = (rel) => {
|
|
486
|
+
let ents;
|
|
487
|
+
try { ents = fsImpl.readdirSync(path.join(root, rel), { withFileTypes: true }); } catch { return; }
|
|
488
|
+
for (const e of ents) {
|
|
489
|
+
const r = `${rel}/${e.name}`;
|
|
490
|
+
if (e.isDirectory()) { if (e.name !== 'node_modules' && e.name !== 'fixtures') walk(r); } else if (PIN_TEST_RE.test(e.name)) out.push(r);
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
walk('tests');
|
|
494
|
+
let mods = [];
|
|
495
|
+
try { mods = fsImpl.readdirSync(path.join(root, 'modules'), { withFileTypes: true }); } catch { mods = []; }
|
|
496
|
+
for (const m of mods) if (m.isDirectory()) walk(`modules/${m.name}/tests`);
|
|
497
|
+
return out.sort();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function findPinningTests({ applied, listTests = listTestFiles, readFile = (rel) => fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8') }) {
|
|
501
|
+
const wants = (applied || []).map((a) => ({ key: a.key, needle: pinNeedle(a.before) })).filter((w) => w.needle);
|
|
502
|
+
if (!wants.length) return [];
|
|
503
|
+
const hits = new Map();
|
|
504
|
+
for (const rel of listTests()) {
|
|
505
|
+
let src;
|
|
506
|
+
try { src = readFile(rel); } catch { continue; }
|
|
507
|
+
for (const w of wants) {
|
|
508
|
+
if (!src.includes(w.needle)) continue;
|
|
509
|
+
if (!hits.has(w.key)) hits.set(w.key, []);
|
|
510
|
+
hits.get(w.key).push(rel);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return wants.filter((w) => hits.has(w.key)).map((w) => ({ key: w.key, tests: hits.get(w.key) }));
|
|
514
|
+
}
|
|
515
|
+
|
|
461
516
|
// The ```page-tweak-applied``` block's content, from the page's NEW reading.
|
|
462
517
|
// lines_after is the non-shared lines' texts: shared shell lines are left out of
|
|
463
518
|
// drift on both sides (D8, builder pick 7), so they must be left out here too or
|
|
464
519
|
// every one of them would read as "removed" on the next drift check.
|
|
465
|
-
function appliedBlockFor({ pageId, readingPage, refused, appliedAt }) {
|
|
520
|
+
function appliedBlockFor({ pageId, readingPage, refused, appliedAt, pinnedTests = [] }) {
|
|
466
521
|
return {
|
|
467
522
|
page_id: pageId,
|
|
468
523
|
applied_at: appliedAt,
|
|
469
524
|
reading_hash_after: readingPage ? readingPage.reading_hash : null,
|
|
470
525
|
lines_after: readingPage ? (readingPage.lines || []).filter((l) => l && l.placement !== 'shared').map((l) => String(l.text)) : [],
|
|
471
526
|
refused: (refused || []).map((x) => ({ key: x.key, code: x.code })),
|
|
527
|
+
...(pinnedTests && pinnedTests.length ? { pinned_tests: pinnedTests.map((p) => ({ key: p.key, tests: p.tests.slice() })) } : {}),
|
|
472
528
|
};
|
|
473
529
|
}
|
|
474
530
|
|
|
@@ -564,7 +620,10 @@ async function batchMain({ taskId, batchFile, dryRun, pageId = null, now = () =>
|
|
|
564
620
|
return 4;
|
|
565
621
|
}
|
|
566
622
|
const readingPage = (deps.readingFor || ((id) => readingFor(id, { readFile })))(batch.page_id);
|
|
567
|
-
const
|
|
623
|
+
const oldText = new Map(batch.lines.map((l) => [l.key, l.before]));
|
|
624
|
+
const pinnedTests = (deps.findPinningTests || findPinningTests)({ applied: result.applied.map((a) => ({ key: a.key, before: oldText.get(a.key) })) });
|
|
625
|
+
for (const p of pinnedTests) log(` ${p.key} the old words are still asserted by: ${p.tests.join(', ')} (update the test or it fails once this lands)`);
|
|
626
|
+
const block = appliedBlockFor({ pageId: batch.page_id, readingPage, refused: result.refused, appliedAt: now(), pinnedTests });
|
|
568
627
|
if (batchFile) {
|
|
569
628
|
log('copy-apply --batch: the page-tweak-applied block (a --batch-file run writes no task):');
|
|
570
629
|
log(pages.composeAppliedBlock(block));
|
|
@@ -684,6 +743,10 @@ module.exports = {
|
|
|
684
743
|
applyBatchToSources,
|
|
685
744
|
appliedBlockFor,
|
|
686
745
|
batchMain,
|
|
746
|
+
// task 1004775: tests that still assert a rewritten line's old words.
|
|
747
|
+
findPinningTests,
|
|
748
|
+
listTestFiles,
|
|
749
|
+
pinNeedle,
|
|
687
750
|
surfaceRootFor,
|
|
688
751
|
checkPatchPath,
|
|
689
752
|
locateOccurrence,
|
|
@@ -115,6 +115,8 @@ const REGENERABLE = new Set([
|
|
|
115
115
|
'docs/session-log-index.md',
|
|
116
116
|
'docs/copy-inventory.md',
|
|
117
117
|
'docs/copy-registry.json',
|
|
118
|
+
// task 1004775: merged per page (page-reader.js mergeReadingDocs), see the rule below.
|
|
119
|
+
'docs/page-readings.json',
|
|
118
120
|
...G.NESTED_DIRS.map((d) => `${d}/CLAUDE.md`),
|
|
119
121
|
...API_DOC_FILES,
|
|
120
122
|
...API_CLIENT_FILES,
|
|
@@ -170,6 +172,18 @@ function regenerateResolved(relpath, { base = '', ours = '', theirs = '' } = {})
|
|
|
170
172
|
if (relpath === 'docs/copy-registry.json') {
|
|
171
173
|
return { content: `${JSON.stringify(CI.buildRegistry(), null, 2)}\n`, ok: true };
|
|
172
174
|
}
|
|
175
|
+
// docs/page-readings.json (task 1004775): one entry per page, and a re-read needs a
|
|
176
|
+
// browser no merge has. Two approved page-tweak rounds re-read DIFFERENT pages and
|
|
177
|
+
// both rewrite the top counts line, so every pair conflicted and the second round
|
|
178
|
+
// stranded for a person. Merge it per page from the three sides and recompute the
|
|
179
|
+
// counts (page-reader.js mergeReadingDocs). Unlike the scans above it reads the
|
|
180
|
+
// merge sides, not the tree, so it is not provisional; a page BOTH sides re-read
|
|
181
|
+
// differently is a real conflict and stays one.
|
|
182
|
+
if (relpath === 'docs/page-readings.json') {
|
|
183
|
+
const PR = require('./page-reader');
|
|
184
|
+
const m = PR.mergeReadingDocs({ base, ours, theirs });
|
|
185
|
+
return m.ok ? { content: m.content, ok: true } : { content: null, ok: false };
|
|
186
|
+
}
|
|
173
187
|
if (relpath === 'docs/copy-inventory.md') {
|
|
174
188
|
return { content: CI.renderReport(CI.buildRegistry()), ok: true };
|
|
175
189
|
}
|
|
@@ -600,6 +600,50 @@ function mergeReadings(inventory, previous, fresh) {
|
|
|
600
600
|
return inventory.pages.map((p) => byId.get(p.id)).filter(Boolean);
|
|
601
601
|
}
|
|
602
602
|
|
|
603
|
+
// mergeReadingDocs({ base, ours, theirs }) -> { ok, content, conflicted }
|
|
604
|
+
//
|
|
605
|
+
// The merge rule for docs/page-readings.json (task 1004775). Two approved page
|
|
606
|
+
// tweak rounds branched from the same main each re-read THEIR page, and both
|
|
607
|
+
// rewrite the top `counts` line — so git conflicts on every pair, and the second
|
|
608
|
+
// round stranded as unresolvable_conflict until a person merged it by hand. A
|
|
609
|
+
// re-read needs a browser, which a merge never has, but it does not need one: the
|
|
610
|
+
// file is one entry per page, so the merge is per page. A page only one side
|
|
611
|
+
// changed takes that side's reading; a page both changed the same way takes it; a
|
|
612
|
+
// page both changed DIFFERENTLY is a real conflict (two rounds re-read one page —
|
|
613
|
+
// their source files conflict too) and is named, never guessed. The summary counts
|
|
614
|
+
// are recomputed from the merged pages and the whole file re-serialized, so the
|
|
615
|
+
// result is byte-for-byte what the reader itself would write for those pages.
|
|
616
|
+
// Pages keep ours' order; a page only theirs has goes where theirs had it.
|
|
617
|
+
function mergeReadingDocs({ base, ours, theirs }) {
|
|
618
|
+
const parse = (s) => { try { return s && String(s).trim() ? JSON.parse(s) : { pages: [] }; } catch { return null; } };
|
|
619
|
+
const b = parse(base), o = parse(ours), t = parse(theirs);
|
|
620
|
+
if (!b || !o || !t) return { ok: false, content: null, conflicted: ['(unreadable json)'] };
|
|
621
|
+
const map = (d) => new Map((d.pages || []).map((p) => [p.id, p]));
|
|
622
|
+
const B = map(b), O = map(o), T = map(t);
|
|
623
|
+
const same = (x, y) => JSON.stringify(x === undefined ? null : x) === JSON.stringify(y === undefined ? null : y);
|
|
624
|
+
const order = [...O.keys()];
|
|
625
|
+
for (const id of T.keys()) {
|
|
626
|
+
if (order.includes(id)) continue;
|
|
627
|
+
const at = (t.pages || []).findIndex((p) => p.id === id);
|
|
628
|
+
const prev = at > 0 ? t.pages[at - 1].id : null;
|
|
629
|
+
const i = prev ? order.indexOf(prev) : -1;
|
|
630
|
+
order.splice(i + 1, 0, id);
|
|
631
|
+
}
|
|
632
|
+
const pages = [];
|
|
633
|
+
const conflicted = [];
|
|
634
|
+
for (const id of order) {
|
|
635
|
+
const bv = B.get(id), ov = O.get(id), tv = T.get(id);
|
|
636
|
+
let pick;
|
|
637
|
+
if (same(ov, tv)) pick = ov;
|
|
638
|
+
else if (same(ov, bv)) pick = tv;
|
|
639
|
+
else if (same(tv, bv)) pick = ov;
|
|
640
|
+
else { conflicted.push(id); continue; }
|
|
641
|
+
if (pick) pages.push(pick);
|
|
642
|
+
}
|
|
643
|
+
if (conflicted.length) return { ok: false, content: null, conflicted };
|
|
644
|
+
return { ok: true, content: serialize(buildDoc(pages)), conflicted: [] };
|
|
645
|
+
}
|
|
646
|
+
|
|
603
647
|
function loadPrevious() {
|
|
604
648
|
try { return readJsonFile(READINGS_REL); } catch { return null; }
|
|
605
649
|
}
|
|
@@ -671,6 +715,8 @@ module.exports = {
|
|
|
671
715
|
filesHash, readingHash, buildMatcher, placeText, collectStrings, holeData,
|
|
672
716
|
mergeStates, assembleReading, serialize, buildDoc, checkReadings, planRenders,
|
|
673
717
|
captureInPage, mergeReadings,
|
|
718
|
+
// task 1004775: the merge driver's per-page rule for docs/page-readings.json.
|
|
719
|
+
mergeReadingDocs,
|
|
674
720
|
// task 1004322: /tweak renders the page the reader reads, through the same harness.
|
|
675
721
|
startHarness, SURFACE_MODULE, PINNED_CLOCK,
|
|
676
722
|
};
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.165'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -591,7 +591,13 @@ test('1003546 every REGENERABLE artifact has a producer, and every producer is i
|
|
|
591
591
|
.map((p) => [p, 'scripts/gds/gen-api-client.js']),
|
|
592
592
|
]);
|
|
593
593
|
|
|
594
|
+
// task 1004775 — docs/page-readings.json is merged per page FROM THE THREE MERGE
|
|
595
|
+
// SIDES (page-reader.js mergeReadingDocs), never by scanning the tree, so the
|
|
596
|
+
// driver's result is already final; its only producer opens a browser, which the
|
|
597
|
+
// server never has. It is the one REGENERABLE entry with no post-merge generator.
|
|
598
|
+
const MERGED_FROM_SIDES = new Set(['docs/page-readings.json']);
|
|
594
599
|
for (const p of regen.REGENERABLE) {
|
|
600
|
+
if (MERGED_FROM_SIDES.has(p)) continue;
|
|
595
601
|
const producer = PRODUCED_BY.get(p);
|
|
596
602
|
assert.ok(producer, `REGENERABLE knows ${p} but this test has no producer for it — name the generator that writes it`);
|
|
597
603
|
assert.ok(
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// tests/craft_review_mode.mjs — artist review mode (task 1004686, goal 1000095).
|
|
2
2
|
//
|
|
3
|
-
// Owner ask, 2026-10-09:
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Owner ask, 2026-10-09: the artist picks one of the four experiences (Ideator,
|
|
4
|
+
// Governing, Building, Artistry) and reviews the hall in that mode's view, with
|
|
5
|
+
// graffiti across the top bar's empty space while it lasts. Since task 1004771
|
|
6
|
+
// the choice lives in the docked review panel's own Review mode row
|
|
7
|
+
// (page-notes.js; tests/page_notes_panel.mjs runs that row), not a top-bar menu. These cases run the REAL
|
|
7
8
|
// craft-mode.js (and, when it is loaded, the real craft-review.js) over a small
|
|
8
9
|
// DOM, and read the real craft-review.css for the motion rules.
|
|
9
10
|
import assert from 'node:assert/strict';
|
|
@@ -83,7 +84,7 @@ function boot({ session = {}, store = {}, narrow = false } = {}) {
|
|
|
83
84
|
head.onAppend = (tag) => {
|
|
84
85
|
loaded.push(tag.src);
|
|
85
86
|
if (/craft-review\.js/.test(tag.src)) vm.runInContext(REVIEW, sandbox, { filename: 'craft-review.js' });
|
|
86
|
-
else window.OTBPageNotes = { open: () => opened.push('open'), openNote: () => {} };
|
|
87
|
+
else window.OTBPageNotes = { open: () => opened.push('open'), openNote: () => {}, dock: () => opened.push('dock'), close: () => opened.push('close') };
|
|
87
88
|
tag.onload();
|
|
88
89
|
};
|
|
89
90
|
vm.runInContext(CRAFT, sandbox, { filename: 'craft-mode.js' });
|
|
@@ -93,107 +94,43 @@ function boot({ session = {}, store = {}, narrow = false } = {}) {
|
|
|
93
94
|
return {
|
|
94
95
|
craft: window.OTBCraftMode, root, topbar, side, sw, title, loaded, opened, session, byId, docListeners,
|
|
95
96
|
tools: () => all(side).filter((n) => n.hasAttribute('data-craft-tool')).map((n) => n.id.replace('craft-tool-', '')),
|
|
96
|
-
menu: () => byId('craft-review-menu'),
|
|
97
|
-
menuOpen: () => !!byId('craft-review-menu') && !byId('craft-review-menu').hasAttribute('hidden'),
|
|
98
|
-
// A click on a menu item: the menu's own listener reads the button off the target.
|
|
99
|
-
pick: (attrs) => byId('craft-review-menu').fire('click', { target: { closest: () => ({ getAttribute: (k) => (k in attrs ? attrs[k] : null) }) } }),
|
|
100
97
|
};
|
|
101
98
|
}
|
|
102
99
|
const artist = () => ({ 'hall-craft': 'artistry', 'hall-craft-asked': '1' });
|
|
103
100
|
|
|
104
|
-
test('
|
|
101
|
+
test('NO TOP-BAR REVIEW BUTTON: in Artistry the bar holds Edit words alone, and no review-mode menu', async () => {
|
|
105
102
|
const p = boot({ session: artist() });
|
|
106
|
-
|
|
107
|
-
assert.
|
|
108
|
-
assert.equal(p.byId('craft-
|
|
109
|
-
|
|
110
|
-
assert.
|
|
111
|
-
assert.equal(more.getAttribute('aria-haspopup'), 'menu');
|
|
112
|
-
assert.equal(more.getAttribute('aria-expanded'), 'false');
|
|
113
|
-
assert.equal(more.getAttribute('aria-controls'), 'craft-review-menu');
|
|
114
|
-
assert.equal(p.menu().getAttribute('role'), 'menu');
|
|
115
|
-
assert.ok(p.menu().hasAttribute('hidden'), 'the menu starts closed');
|
|
103
|
+
await flush();
|
|
104
|
+
assert.deepEqual(p.tools(), ['paintbrush'], 'the review panel docks at the edge instead (task 1004771)');
|
|
105
|
+
assert.equal(p.byId('craft-review'), null, 'no split pill');
|
|
106
|
+
assert.equal(p.byId('craft-review-menu'), null, 'no menu');
|
|
107
|
+
assert.deepEqual(p.opened, ['dock']);
|
|
116
108
|
assert.equal(p.byId('craft-graffiti'), null, 'no wall until a review starts');
|
|
117
109
|
assert.equal(p.root.getAttribute('data-craft-review'), null);
|
|
118
|
-
|
|
119
|
-
p.craft.choose(m);
|
|
120
|
-
assert.equal(p.byId('craft-review'), null, `no Review pill in ${m}`);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('THE MENU: write feedback first, then the four experiences in the owner\'s order and words', () => {
|
|
125
|
-
const p = boot({ session: artist() });
|
|
126
|
-
p.byId('craft-review-more').click({ detail: 1 });
|
|
127
|
-
assert.ok(p.menuOpen(), 'the chevron opens it');
|
|
128
|
-
assert.equal(p.byId('craft-review-more').getAttribute('aria-expanded'), 'true');
|
|
129
|
-
const html = p.menu().innerHTML;
|
|
130
|
-
const labels = [...html.matchAll(/<span>([^<]+)<\/span>/g)].map((m) => m[1]);
|
|
131
|
-
assert.deepEqual(labels, ['Write feedback on this page', 'Ideator experience', 'Governing experience', 'Building experience', 'Artistry experience']);
|
|
132
|
-
assert.deepEqual([...html.matchAll(/data-review-pick="([a-z]+)"/g)].map((m) => m[1]), ['ideating', 'governing', 'building', 'artistry']);
|
|
133
|
-
assert.equal((html.match(/role="menuitemradio" aria-checked="true"/g) || []).length, 0, 'nothing is checked before a review');
|
|
134
|
-
assert.doesNotMatch(html, /Stop reviewing/, 'no way out of a review that has not started');
|
|
135
|
-
p.byId('craft-review-more').click({ detail: 1 });
|
|
136
|
-
assert.ok(!p.menuOpen(), 'the chevron closes it again');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test('HOVER opens it for a mouse, never for a touch, and leaving closes it after a beat', async () => {
|
|
140
|
-
const p = boot({ session: artist() });
|
|
141
|
-
const pill = p.byId('craft-review');
|
|
142
|
-
pill.fire('pointerenter', { pointerType: 'touch' });
|
|
143
|
-
assert.ok(!p.menuOpen(), 'a touch is not a hover');
|
|
144
|
-
pill.fire('pointerenter', { pointerType: 'mouse' });
|
|
145
|
-
assert.ok(p.menuOpen(), 'a mouse over the pill opens the menu');
|
|
146
|
-
pill.fire('pointerleave', { pointerType: 'mouse' });
|
|
147
|
-
assert.ok(p.menuOpen(), 'it stays a beat, so the pointer can reach the menu');
|
|
148
|
-
pill.fire('pointerenter', { pointerType: 'mouse' });
|
|
149
|
-
await new Promise((r) => setTimeout(r, 260));
|
|
150
|
-
assert.ok(p.menuOpen(), 'coming back in cancels the close');
|
|
151
|
-
pill.fire('pointerleave', { pointerType: 'mouse' });
|
|
152
|
-
await new Promise((r) => setTimeout(r, 260));
|
|
153
|
-
assert.ok(!p.menuOpen(), 'and leaving for good closes it');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test('CLICKING Review still opens the feedback panel; on a phone a tap opens the menu instead', async () => {
|
|
157
|
-
const p = boot({ session: artist() });
|
|
158
|
-
p.byId('craft-tool-palette').click({ detail: 1 });
|
|
159
|
-
await flush();
|
|
160
|
-
assert.deepEqual(p.opened, ['open'], 'the feedback doc opens, as before');
|
|
161
|
-
assert.ok(!p.menuOpen());
|
|
162
|
-
const phone = boot({ session: artist(), narrow: true });
|
|
163
|
-
phone.byId('craft-tool-palette').click({ detail: 1 });
|
|
164
|
-
await flush();
|
|
165
|
-
assert.deepEqual(phone.opened, [], 'no panel on the tap');
|
|
166
|
-
assert.ok(phone.menuOpen(), 'the menu instead (a phone has no hover)');
|
|
167
|
-
phone.pick({ 'data-review-act': 'write' });
|
|
168
|
-
await flush();
|
|
169
|
-
assert.deepEqual(phone.opened, ['open'], 'and its first line opens the feedback doc');
|
|
110
|
+
assert.deepEqual(JSON.parse(JSON.stringify(p.craft.experiences().map((x) => x.label))), ['Ideator experience', 'Governing experience', 'Building experience', 'Artistry experience'], 'the four, in the owner\'s order and words, for the review panel row');
|
|
170
111
|
});
|
|
171
112
|
|
|
172
113
|
test('PICKING an experience: the hall wears that mode, the artist keeps the Review tools, and the wall goes up', async () => {
|
|
173
114
|
const p = boot({ session: artist() });
|
|
174
|
-
p.
|
|
175
|
-
p.pick({ 'data-review-pick': 'governing' });
|
|
176
|
-
assert.ok(!p.menuOpen(), 'the menu closes on a pick');
|
|
115
|
+
p.craft.review('governing');
|
|
177
116
|
assert.equal(p.root.dataset.craft, 'governing', 'the hall wears Governing: its tabs, look and tallies follow');
|
|
178
117
|
assert.equal(p.root.getAttribute('data-craft-review'), 'governing');
|
|
179
118
|
assert.equal(p.craft.current(), 'governing');
|
|
180
119
|
assert.equal(p.craft.reviewing(), 'governing');
|
|
181
120
|
assert.equal(p.session['hall-craft'], 'artistry', 'the artist\'s own mode stays Artistry');
|
|
182
121
|
assert.equal(p.session['hall-craft-review'], 'governing', 'the review lasts the session');
|
|
183
|
-
assert.deepEqual(p.tools(), ['
|
|
184
|
-
|
|
122
|
+
assert.deepEqual(p.tools(), ['paintbrush'], 'Edit words stays in the bar');
|
|
123
|
+
await flush();
|
|
124
|
+
assert.ok(p.opened.filter((x) => x === 'dock').length >= 2, 'the docked panel is told again, so its mode row repaints');
|
|
185
125
|
const wall = p.byId('craft-graffiti');
|
|
186
126
|
assert.ok(wall, 'the graffiti wall is up');
|
|
187
127
|
assert.equal(wall.parentNode, p.topbar, 'in the top bar');
|
|
188
128
|
assert.equal(wall.nextSibling, p.side, 'in the empty space before the controls');
|
|
189
129
|
assert.equal(wall.getAttribute('aria-hidden'), 'true', 'decorative');
|
|
190
130
|
await flush();
|
|
191
|
-
assert.
|
|
131
|
+
assert.ok(p.loaded.includes('/builders/craft-review.js?v=2026-10-09-test'), 'the wall\'s script loads on demand, at craft-mode.js\'s version');
|
|
192
132
|
assert.equal((wall.innerHTML.match(/class="gx-tag /g) || []).length, 6, 'six doodles');
|
|
193
133
|
assert.doesNotMatch(wall.innerHTML, /<text/, 'scribbles, not words (owner, 2026-10-09)');
|
|
194
|
-
p.byId('craft-review-more').click({ detail: 1 });
|
|
195
|
-
assert.match(p.menu().innerHTML, /aria-checked="true" data-review-pick="governing"/, 'the menu checks the live experience');
|
|
196
|
-
assert.match(p.menu().innerHTML, /data-review-act="stop"/, 'and offers a way out');
|
|
197
134
|
});
|
|
198
135
|
|
|
199
136
|
test('IT LASTS THE SESSION: a reload keeps the review; a new session starts without one', () => {
|
|
@@ -215,7 +152,7 @@ test('IT LASTS THE SESSION: a reload keeps the review; a new session starts with
|
|
|
215
152
|
test('REVIEWING Ideating shows its own tool too, marked a guest so a tiny phone can fold it away', () => {
|
|
216
153
|
const p = boot({ session: artist() });
|
|
217
154
|
p.craft.review('ideating');
|
|
218
|
-
assert.deepEqual(p.tools(), ['
|
|
155
|
+
assert.deepEqual(p.tools(), ['paintbrush', 'lightbulb'], 'the artist\'s tool, then the experience\'s own, in order');
|
|
219
156
|
assert.ok(p.byId('craft-tool-lightbulb').classList.contains('craft-tool--guest'));
|
|
220
157
|
assert.ok(!p.byId('craft-tool-paintbrush').classList.contains('craft-tool--guest'));
|
|
221
158
|
p.craft.choose('ideating');
|
|
@@ -227,13 +164,11 @@ test('ENDING it: Stop reviewing, or any mode from the switch, puts the artist ba
|
|
|
227
164
|
const p = boot({ session: artist() });
|
|
228
165
|
p.craft.review('building');
|
|
229
166
|
assert.equal(p.root.dataset.craft, 'building');
|
|
230
|
-
p.
|
|
231
|
-
p.pick({ 'data-review-act': 'stop' });
|
|
167
|
+
p.craft.stopReview();
|
|
232
168
|
assert.equal(p.root.dataset.craft, 'artistry', 'Stop reviewing is back in Artistry');
|
|
233
169
|
assert.equal(p.root.getAttribute('data-craft-review'), null);
|
|
234
170
|
assert.equal(p.byId('craft-graffiti'), null, 'the wall comes down');
|
|
235
171
|
assert.equal(p.session['hall-craft-review'], undefined, 'and the session forgets it');
|
|
236
|
-
assert.ok(!p.byId('craft-tool-palette').classList.contains('craft-tool--reviewing'));
|
|
237
172
|
p.craft.review('governing');
|
|
238
173
|
p.craft.choose('artistry');
|
|
239
174
|
assert.equal(p.craft.reviewing(), null, 'choosing a mode from the switch, even Artistry, ends a review');
|
|
@@ -242,25 +177,7 @@ test('ENDING it: Stop reviewing, or any mode from the switch, puts the artist ba
|
|
|
242
177
|
p.craft.choose('governing');
|
|
243
178
|
assert.equal(p.root.dataset.craft, 'governing');
|
|
244
179
|
assert.equal(p.root.getAttribute('data-craft-review'), null, 'Governing chosen for real is not a review');
|
|
245
|
-
assert.
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
test('THE KEYS: arrows move through the menu, Escape closes it, an outside click closes it', () => {
|
|
249
|
-
const p = boot({ session: artist() });
|
|
250
|
-
p.byId('craft-review-more').click({ detail: 1 });
|
|
251
|
-
assert.equal(p.docListeners.click && p.docListeners.click.size, 1, 'an open menu listens for a click outside');
|
|
252
|
-
p.menu().fire('keydown', { key: 'Escape' });
|
|
253
|
-
assert.ok(!p.menuOpen(), 'Escape closes it');
|
|
254
|
-
assert.equal(p.docListeners.click.size, 0, 'and stops listening');
|
|
255
|
-
p.byId('craft-review-more').fire('keydown', { key: 'ArrowDown' });
|
|
256
|
-
assert.ok(p.menuOpen(), 'ArrowDown on the chevron opens it');
|
|
257
|
-
[...p.docListeners.click][0]({ target: p.title });
|
|
258
|
-
assert.ok(!p.menuOpen(), 'a click elsewhere closes it');
|
|
259
|
-
p.byId('craft-tool-palette').fire('keydown', { key: 'ArrowDown' });
|
|
260
|
-
assert.ok(p.menuOpen(), 'ArrowDown on Review opens it too');
|
|
261
|
-
assert.match(CRAFT, /e\.key === 'ArrowUp'/);
|
|
262
|
-
assert.match(CRAFT, /e\.key === 'Home'/);
|
|
263
|
-
assert.match(CRAFT, /e\.key === 'End'/);
|
|
180
|
+
assert.ok(p.opened.includes('close'), 'and the review panel leaves with Artistry');
|
|
264
181
|
});
|
|
265
182
|
|
|
266
183
|
test('THE WALL is scribbled in CSS only, in ink alone, and stands still under reduced motion', () => {
|