@bongos/core 1.19.532 → 1.19.533
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 +23 -18
- package/docs/adr/0244-a-catalog-search-is-a-view-over-the-rows-never-an-edit-to-the-selection.md +116 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +61 -59
- package/docs/copy-registry.json +90 -72
- package/docs/module-api-changelog.md +2 -0
- package/modules/public-landing/public/projects.html +61 -1
- package/modules/public-landing/public/projects.states.json +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/projects_hub.mjs +22 -0
- package/tests/projects_hub_module_picker.mjs +207 -0
|
@@ -1028,6 +1028,14 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1028
1028
|
<p class="lede">Optional — skip it and you get the suggested set below, shaped by what you just told us. These are extras on top of everything every project already has, and you can switch any of them on or off later.</p>
|
|
1029
1029
|
<form data-next="5">
|
|
1030
1030
|
<div class="wizField"><span id="modLbl">Extras for this project</span>
|
|
1031
|
+
<!-- the catalog search (task 1002340, BV1.R21). The wizard's own
|
|
1032
|
+
search idiom (#ghSearch): a plain field on one hairline, no
|
|
1033
|
+
new class and no icon. It filters the band below and NOTHING
|
|
1034
|
+
else — what is switched on is unaffected by what is on
|
|
1035
|
+
screen, and the line under it counts the ones it is hiding
|
|
1036
|
+
(ADR 0244). -->
|
|
1037
|
+
<input type="text" id="modQ" placeholder="Search extras…" autocomplete="off" aria-label="Search the extras by name" aria-describedby="modFilterNote">
|
|
1038
|
+
<div class="hint" id="modFilterNote" aria-live="polite"></div>
|
|
1031
1039
|
<div class="typeCards" id="modCards" role="group" aria-labelledby="modLbl"></div>
|
|
1032
1040
|
</div>
|
|
1033
1041
|
<div class="modWhy" id="modWhy" aria-live="polite"></div>
|
|
@@ -3262,10 +3270,52 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3262
3270
|
for (var i = 0; i < rows.length; i++) if (rows[i].key === key) return rows[i].label;
|
|
3263
3271
|
return key;
|
|
3264
3272
|
}
|
|
3273
|
+
/* ── the catalog search (task 1002340, BV1.R21) ──
|
|
3274
|
+
Lexical and deliberately dumb: it matches the words the owner can already
|
|
3275
|
+
see. Search by MEANING ("somewhere to chat") is a different feature and is
|
|
3276
|
+
deferred (idea 1000552), so nothing here pretends to understand a query.
|
|
3277
|
+
|
|
3278
|
+
It is a VIEW over the rows and never an edit to the answer (ADR 0244):
|
|
3279
|
+
filtering paints fewer rows, and state.modules does not move. That is what
|
|
3280
|
+
lets a switched-on module be off screen without being switched off — the
|
|
3281
|
+
count under the field says so rather than leaving it to be discovered. */
|
|
3282
|
+
var moduleQuery = ''; /* what is typed in the search box; never saved to the draft */
|
|
3283
|
+
/* A query as the terms to match: lower-cased, split on whitespace, empties
|
|
3284
|
+
dropped — so a box holding only spaces is the same as an empty one. */
|
|
3285
|
+
function searchTerms(q) {
|
|
3286
|
+
return String(q == null ? '' : q).toLowerCase().split(/\s+/).filter(function (t) { return t.length > 0; });
|
|
3287
|
+
}
|
|
3288
|
+
/* Does one catalog row answer this search? Every term must appear somewhere in
|
|
3289
|
+
the row's own words — its name, its one-line summary, or its key — as a
|
|
3290
|
+
substring, case-insensitively. AND across terms, so adding a word narrows;
|
|
3291
|
+
substring, so a half-typed word still finds its module. */
|
|
3292
|
+
function moduleMatches(mod, terms) {
|
|
3293
|
+
if (!terms.length) return true;
|
|
3294
|
+
var hay = (((mod && mod.label) || '') + ' ' + ((mod && mod.summary) || '') + ' ' + ((mod && mod.key) || '')).toLowerCase();
|
|
3295
|
+
for (var i = 0; i < terms.length; i++) if (hay.indexOf(terms[i]) === -1) return false;
|
|
3296
|
+
return true;
|
|
3297
|
+
}
|
|
3298
|
+
/* The catalog rows the current search leaves on screen, in catalog order. */
|
|
3299
|
+
function visibleModules() {
|
|
3300
|
+
var terms = searchTerms(moduleQuery);
|
|
3301
|
+
return ((starterBundles && starterBundles.optional) || []).filter(function (m) { return moduleMatches(m, terms); });
|
|
3302
|
+
}
|
|
3303
|
+
/* The line under the search box. Silent with nothing typed — there is nothing
|
|
3304
|
+
to explain. Once a search is hiding rows it says how many are left AND how
|
|
3305
|
+
many extras are on, counting the hidden ones: a switched-on module a filter
|
|
3306
|
+
took off screen would otherwise read as switched off. */
|
|
3307
|
+
function filterNote(shown, total, onCount) {
|
|
3308
|
+
if (!searchTerms(moduleQuery).length) return '';
|
|
3309
|
+
var line = shown ? 'Showing ' + shown + ' of ' + total + '.' : 'No extras match what you typed.';
|
|
3310
|
+
return onCount
|
|
3311
|
+
? line + ' ' + onCount + (onCount === 1 ? ' extra is' : ' extras are') + ' switched on, including any this search is hiding.'
|
|
3312
|
+
: line;
|
|
3313
|
+
}
|
|
3265
3314
|
function renderModules() {
|
|
3266
3315
|
ensureBundles();
|
|
3267
3316
|
var band = $('modCards');
|
|
3268
3317
|
var note = $('modCoreNote');
|
|
3318
|
+
$('modQ').hidden = !starterBundles; /* a search box over a list that is not there can only disappoint */
|
|
3269
3319
|
if (!starterBundles) {
|
|
3270
3320
|
/* nothing yet. While the read is still in flight that is a blank band for
|
|
3271
3321
|
a moment; only a read that ANSWERED badly gets the clause, so a slow
|
|
@@ -3273,6 +3323,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3273
3323
|
band.innerHTML = '';
|
|
3274
3324
|
$('modWhy').innerHTML = '';
|
|
3275
3325
|
$('modWhy').classList.remove('on');
|
|
3326
|
+
$('modFilterNote').textContent = '';
|
|
3276
3327
|
note.textContent = bundlesFailed
|
|
3277
3328
|
? 'We couldn’t load the list of extras just now. Your project is created with everything it needs either way, and you can add extras from its settings.'
|
|
3278
3329
|
: '';
|
|
@@ -3280,11 +3331,13 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3280
3331
|
return;
|
|
3281
3332
|
}
|
|
3282
3333
|
var on = pickedModules();
|
|
3283
|
-
|
|
3334
|
+
var offered = visibleModules();
|
|
3335
|
+
band.innerHTML = offered.map(function (m) {
|
|
3284
3336
|
return '<button type="button" class="tpill" data-mod="' + esc(m.key) + '" aria-pressed="'
|
|
3285
3337
|
+ (on.indexOf(m.key) === -1 ? 'false' : 'true') + '"><span><b>' + esc(m.label)
|
|
3286
3338
|
+ '</b><small>' + esc(m.summary) + '</small></span></button>';
|
|
3287
3339
|
}).join('');
|
|
3340
|
+
$('modFilterNote').textContent = filterNote(offered.length, (starterBundles.optional || []).length, on.length);
|
|
3288
3341
|
/* The tailoring, said out loud (task 1002341). A preselect an owner cannot
|
|
3289
3342
|
see the reason for is one they have to audit, which is the thing ADR 0237
|
|
3290
3343
|
says is worse than no preselect at all. Rendered as its own block above the
|
|
@@ -3317,6 +3370,13 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3317
3370
|
renderModules();
|
|
3318
3371
|
saveDraft();
|
|
3319
3372
|
});
|
|
3373
|
+
/* Typing repaints the band and nothing else (task 1002340): no read (the
|
|
3374
|
+
catalog is already here) and no saveDraft — a search is not an answer, so
|
|
3375
|
+
it must not be able to turn an untouched picker into an answered one. */
|
|
3376
|
+
$('modQ').addEventListener('input', function (ev) {
|
|
3377
|
+
moduleQuery = (ev && ev.target && ev.target.value) || '';
|
|
3378
|
+
renderModules();
|
|
3379
|
+
});
|
|
3320
3380
|
/* Next off this panel CONFIRMS what is on screen — the owner looked at the
|
|
3321
3381
|
set and accepted it, which is a different fact from never being asked. */
|
|
3322
3382
|
root.querySelector('#panel-modules form').addEventListener('submit', function () {
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"out-new": { "auth": false, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
|
|
31
31
|
"in-new-type": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-type"],"hidden":["#panel-name","#qWrap"]} },
|
|
32
32
|
"in-new-about": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-detail"],"hidden":["#panel-type","#qWrap"]} },
|
|
33
|
-
"in-new-modules": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900]], "expect": {"visible":["#panel-modules","#modCards .tpill[aria-pressed=\"true\"]","#modCoreNote"],"hidden":["#panel-detail","#qWrap"]} },
|
|
33
|
+
"in-new-modules": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900]], "expect": {"visible":["#panel-modules","#modQ","#modCards .tpill[aria-pressed=\"true\"]","#modCoreNote"],"hidden":["#panel-detail","#qWrap"]} },
|
|
34
|
+
"in-new-modules-search": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["fill","#modQ","discord"],["wait",200]], "expect": {"visible":["#panel-modules","#modQ"],"value":[["#modQ","discord"]],"count":[["#modCards .tpill",1],["#modCards .tpill[data-mod=\"discord\"]",1]],"text":[["#modFilterNote","Showing 1 of 3. 2 extras are switched on, including any this search is hiding."]]} },
|
|
34
35
|
"in-new-home": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-home"],"hidden":["#panel-detail","#qWrap"]} },
|
|
35
36
|
"in-new-address": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-address"],"hidden":["#panel-home","#qWrap"]} },
|
|
36
37
|
"in-new-review": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-review","#reviewList li"],"hidden":["#panel-address","#qWrap"]} },
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.533",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.533",
|
|
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.19.
|
|
3
|
+
"version": "1.19.533",
|
|
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/src/module-api.js
CHANGED
|
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
45
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
46
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
48
|
+
const CORE_VERSION = '1.19.533'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
49
|
|
|
50
50
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
51
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -2141,6 +2141,28 @@ test('the Modules step ships as real markup and is skippable like About (task 10
|
|
|
2141
2141
|
'the optional step has no validation clause — Next past an untouched picker is the point');
|
|
2142
2142
|
});
|
|
2143
2143
|
|
|
2144
|
+
test('the picker\'s search ships as real markup, in the wizard\'s own field grammar (task 1002340)', () => {
|
|
2145
|
+
const panel = (HUB.match(/<section class="panel" id="panel-modules"[\s\S]*?<\/section>/) || [''])[0];
|
|
2146
|
+
assert.match(panel, /<input type="text" id="modQ"[^>]*>/, 'the search is a real field, not one built by script');
|
|
2147
|
+
assert.match(panel, /id="modQ"[^>]*aria-label="Search the extras by name"/, 'and it is named for a screen reader');
|
|
2148
|
+
assert.match(panel, /id="modFilterNote"[^>]*aria-live="polite"/, 'the count under it is announced when it changes');
|
|
2149
|
+
// it wears the wizard's own control skin (#view-new input[type=text]) rather
|
|
2150
|
+
// than a class of its own — the C1 rule the type and team bands follow
|
|
2151
|
+
assert.doesNotMatch(panel, /id="modQ"[^>]*class=/, 'no bespoke class for one field');
|
|
2152
|
+
// the band, the field and the count are one wizField — a search that sat
|
|
2153
|
+
// outside the field it filters would read as filtering the whole panel
|
|
2154
|
+
const field = (panel.match(/<div class="wizField"><span id="modLbl"[\s\S]*?<\/div>\s*<\/div>/) || [''])[0];
|
|
2155
|
+
assert.match(field, /id="modQ"/, 'the field holds the search');
|
|
2156
|
+
assert.match(field, /id="modCards"/, 'and the band it filters');
|
|
2157
|
+
// display only: the filter reaches the render, never state.modules (ADR 0244)
|
|
2158
|
+
assert.match(SCRIPT, /function moduleMatches\(mod, terms\)/, 'the matcher is a named function the harness can run');
|
|
2159
|
+
assert.match(SCRIPT, /function visibleModules\(\)/, 'and the rows it leaves are derived, not stored');
|
|
2160
|
+
const onInput = SCRIPT.slice(SCRIPT.indexOf("$('modQ').addEventListener('input'"));
|
|
2161
|
+
const body = onInput.slice(0, onInput.indexOf('});'));
|
|
2162
|
+
assert.doesNotMatch(body, /state\.modules/, 'typing never writes an answer');
|
|
2163
|
+
assert.doesNotMatch(body, /saveDraft/, 'and a search is not part of the draft');
|
|
2164
|
+
});
|
|
2165
|
+
|
|
2144
2166
|
test('the picker reads ONE public declaration, and it is the one the bundles come from (task 1002339)', () => {
|
|
2145
2167
|
// A second source for "what may be chosen" is exactly what would let a
|
|
2146
2168
|
// preselect name a module the panel has no row for (ADR 0237 §5).
|
|
@@ -205,6 +205,13 @@ function boot({ state: seed = {}, fetchOk = true, payload = PAYLOAD } = {}) {
|
|
|
205
205
|
skip: () => $('skipModules')._handlers.click(),
|
|
206
206
|
forCreate: () => sandbox.modulesForCreate(),
|
|
207
207
|
picked: () => sandbox.pickedModules(),
|
|
208
|
+
// BV1.R21 (task 1002340) — the search box. type() is the real `input` event
|
|
209
|
+
// the browser fires, so a test drives the same path a keystroke does.
|
|
210
|
+
type: (q) => { $('modQ')._handlers.input({ target: { value: q } }); },
|
|
211
|
+
filterNote: () => $('modFilterNote').textContent,
|
|
212
|
+
searchHidden: () => $('modQ').hidden === true,
|
|
213
|
+
// the pure matcher on its own, one row against one query
|
|
214
|
+
matches: (mod, q) => sandbox.moduleMatches(mod, sandbox.searchTerms(q)),
|
|
208
215
|
};
|
|
209
216
|
}
|
|
210
217
|
|
|
@@ -580,3 +587,203 @@ test('the picker and the server resolve the same set for every answer an owner c
|
|
|
580
587
|
}
|
|
581
588
|
}
|
|
582
589
|
});
|
|
590
|
+
|
|
591
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
592
|
+
// BV1.R21 (task 1002340) — keyword search over the catalog. ADR 0244: the search
|
|
593
|
+
// is a VIEW over the rows and never an edit to the answer, so everything below
|
|
594
|
+
// asserts both halves — what the band shows, and that what is switched on did
|
|
595
|
+
// not move.
|
|
596
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
597
|
+
|
|
598
|
+
const DEV_BOX = PAYLOAD.optional[1];
|
|
599
|
+
|
|
600
|
+
test('the matcher reads a row\'s own words — its name, its summary and its key', () => {
|
|
601
|
+
// Lexical, not semantic (idea 1000552 holds the by-meaning half): the only
|
|
602
|
+
// words it knows are the ones the owner can see, plus the key they cannot.
|
|
603
|
+
const h = boot({ state: { projType: 'game' } });
|
|
604
|
+
|
|
605
|
+
assert.equal(h.matches(DEV_BOX, 'boxes'), true, 'by a word in the name');
|
|
606
|
+
assert.equal(h.matches(DEV_BOX, 'workspace'), true, 'by a word in the summary');
|
|
607
|
+
assert.equal(h.matches(DEV_BOX, 'dev-box'), true, 'by the key');
|
|
608
|
+
assert.equal(h.matches(DEV_BOX, 'discord'), false, 'and never by a word that is in none of them');
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test('a term matches as a SUBSTRING, so a half-typed word still finds its module', () => {
|
|
612
|
+
const h = boot({ state: { projType: 'game' } });
|
|
613
|
+
|
|
614
|
+
assert.equal(h.matches(DEV_BOX, 'work'), true, 'the owner is still typing');
|
|
615
|
+
assert.equal(h.matches(DEV_BOX, 'ace'), true, 'even mid-word — a dumb filter beats a clever one that finds nothing');
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test('matching is case-insensitive in both directions', () => {
|
|
619
|
+
const h = boot({ state: { projType: 'game' } });
|
|
620
|
+
|
|
621
|
+
assert.equal(h.matches(DEV_BOX, 'DEV BOXES'), true, 'a shouted query finds a title-cased row');
|
|
622
|
+
assert.equal(h.matches(PAYLOAD.optional[0], 'ai'), true, 'and a quiet one finds a shouted row');
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test('every term must match — typing more words narrows, it never widens', () => {
|
|
626
|
+
// AND, not OR. An owner adding a word is telling us the list is still too long.
|
|
627
|
+
const h = boot({ state: { projType: 'game' } });
|
|
628
|
+
|
|
629
|
+
assert.equal(h.matches(DEV_BOX, 'cloud change'), true, 'both words are in the row');
|
|
630
|
+
assert.equal(h.matches(DEV_BOX, 'cloud discord'), false, 'one of them is not, so the row is out');
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test('an empty search is not a search — every row stays', () => {
|
|
634
|
+
const h = boot({ state: { projType: 'game' } });
|
|
635
|
+
|
|
636
|
+
assert.equal(h.matches(DEV_BOX, ''), true);
|
|
637
|
+
assert.equal(h.matches(DEV_BOX, ' '), true, 'and a box holding only spaces is the same as an empty one');
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test('typing filters the band to the rows that answer, in catalog order', async () => {
|
|
641
|
+
// Arrange
|
|
642
|
+
const h = boot({ state: { projType: 'game' } });
|
|
643
|
+
await h.open();
|
|
644
|
+
assert.equal(h.rows().length, 3, 'the whole catalog before anything is typed');
|
|
645
|
+
|
|
646
|
+
// Act — a word from one row's NAME
|
|
647
|
+
h.type('boxes');
|
|
648
|
+
|
|
649
|
+
// Assert
|
|
650
|
+
assert.deepEqual(h.rows().map((r) => r.key), ['dev-box']);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test('a summary word and a key find their row just as a name word does', async () => {
|
|
654
|
+
const h = boot({ state: { projType: 'game' } });
|
|
655
|
+
await h.open();
|
|
656
|
+
|
|
657
|
+
h.type('workspace');
|
|
658
|
+
assert.deepEqual(h.rows().map((r) => r.key), ['dev-box'], 'by summary');
|
|
659
|
+
|
|
660
|
+
h.type('dev-box');
|
|
661
|
+
assert.deepEqual(h.rows().map((r) => r.key), ['dev-box'], 'by key');
|
|
662
|
+
|
|
663
|
+
h.type('DISCORD');
|
|
664
|
+
assert.deepEqual(h.rows().map((r) => r.key), ['discord'], 'and case is not part of the question');
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test('two terms narrow to the rows that carry both', async () => {
|
|
668
|
+
const h = boot({ state: { projType: 'game' } });
|
|
669
|
+
await h.open();
|
|
670
|
+
|
|
671
|
+
h.type('cloud change');
|
|
672
|
+
assert.deepEqual(h.rows().map((r) => r.key), ['dev-box']);
|
|
673
|
+
|
|
674
|
+
h.type('cloud discord');
|
|
675
|
+
assert.deepEqual(h.rows(), [], 'and no row carries both of these');
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test('a search that matches nothing says so, and invents no rows', async () => {
|
|
679
|
+
// The band would otherwise just be empty, which reads as "the catalog failed"
|
|
680
|
+
// — the one thing the couldn't-load clause exists to distinguish.
|
|
681
|
+
const h = boot({ state: { projType: 'game' } });
|
|
682
|
+
await h.open();
|
|
683
|
+
|
|
684
|
+
h.type('kubernetes');
|
|
685
|
+
|
|
686
|
+
assert.deepEqual(h.rows(), []);
|
|
687
|
+
assert.match(h.filterNote(), /No extras match/, 'the panel says why the band is empty');
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test('a switched-on module the search is HIDING is still switched on', async () => {
|
|
691
|
+
// The property ADR 0244 is about. A filter is a view; the answer is the set of
|
|
692
|
+
// toggles, and a view must not be able to edit it.
|
|
693
|
+
const h = boot({ state: { projType: 'game' } });
|
|
694
|
+
await h.open();
|
|
695
|
+
assert.deepEqual(h.picked(), ['dev-box', 'discord'], 'the game bundle is on');
|
|
696
|
+
|
|
697
|
+
// Act — search for the one module that is OFF, hiding both that are on
|
|
698
|
+
h.type('agents');
|
|
699
|
+
|
|
700
|
+
// Assert
|
|
701
|
+
assert.deepEqual(h.rows().map((r) => r.key), ['agents'], 'only the match is on screen');
|
|
702
|
+
assert.equal(h.rows()[0].pressed, false, 'and it is off, as it was');
|
|
703
|
+
assert.deepEqual(h.picked(), ['dev-box', 'discord'], 'the two hidden ones are still switched on');
|
|
704
|
+
assert.equal(h.state.modules, null, 'and typing answered nothing on the owner\'s behalf');
|
|
705
|
+
assert.match(h.filterNote(), /2 extras are switched on/, 'the count under the field says so out loud');
|
|
706
|
+
assert.match(h.filterNote(), /including any this search is hiding/, 'and that some of them are hidden');
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test('the count under the field states what is shown and what is on, singular included', async () => {
|
|
710
|
+
const h = boot({ state: { projType: 'research' } });
|
|
711
|
+
await h.open();
|
|
712
|
+
assert.equal(h.filterNote(), '', 'with nothing typed there is nothing to explain');
|
|
713
|
+
|
|
714
|
+
h.type('agents');
|
|
715
|
+
|
|
716
|
+
assert.equal(h.filterNote(),
|
|
717
|
+
'Showing 1 of 3. 1 extra is switched on, including any this search is hiding.');
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test('with nothing switched on the count says only what is shown', async () => {
|
|
721
|
+
const h = boot({ state: { projType: 'not-sure' } });
|
|
722
|
+
await h.open();
|
|
723
|
+
|
|
724
|
+
h.type('a');
|
|
725
|
+
|
|
726
|
+
assert.equal(h.filterNote(), 'Showing 3 of 3.', 'no "0 extras are switched on" noise');
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test('the review names the hidden picks too — a filter never reaches the create request', async () => {
|
|
730
|
+
const h = boot({ state: { projType: 'game', step: 7 } });
|
|
731
|
+
await h.open();
|
|
732
|
+
h.click('agents');
|
|
733
|
+
assert.deepEqual(h.forCreate(), ['agents', 'dev-box', 'discord']);
|
|
734
|
+
|
|
735
|
+
h.type('discord');
|
|
736
|
+
|
|
737
|
+
assert.deepEqual(h.rows().map((r) => r.key), ['discord'], 'one row on screen');
|
|
738
|
+
assert.deepEqual(h.forCreate(), ['agents', 'dev-box', 'discord'], 'three modules in the answer');
|
|
739
|
+
assert.equal(h.extrasRow(), 'Extra agents, Dev boxes, Discord', 'and the review names all three');
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test('clearing the search brings every row back, untouched', async () => {
|
|
743
|
+
const h = boot({ state: { projType: 'game' } });
|
|
744
|
+
await h.open();
|
|
745
|
+
h.type('agents');
|
|
746
|
+
assert.equal(h.rows().length, 1);
|
|
747
|
+
|
|
748
|
+
h.type('');
|
|
749
|
+
|
|
750
|
+
assert.deepEqual(h.rows().map((r) => r.key), ['agents', 'dev-box', 'discord']);
|
|
751
|
+
assert.deepEqual(h.rows().filter((r) => r.pressed).map((r) => r.key), ['dev-box', 'discord'],
|
|
752
|
+
'with the preselect exactly as it was before the search');
|
|
753
|
+
assert.equal(h.filterNote(), '', 'and the count goes quiet again');
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
test('a toggle made while filtered lands in the answer in catalog order, and the filter stands', async () => {
|
|
757
|
+
// The path BV1.R24 walks: preselect -> search -> add -> enabled.
|
|
758
|
+
const h = boot({ state: { projType: 'non-profit' } });
|
|
759
|
+
await h.open();
|
|
760
|
+
|
|
761
|
+
h.type('workspace');
|
|
762
|
+
h.click('dev-box');
|
|
763
|
+
|
|
764
|
+
assert.deepEqual(h.state.modules, ['dev-box', 'discord'], 'catalog order, not click order');
|
|
765
|
+
assert.deepEqual(h.rows().map((r) => r.key), ['dev-box'], 'and the search is still the search');
|
|
766
|
+
assert.equal(h.rows()[0].pressed, true, 'the row they just switched on shows it');
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
test('a search is not an answer: it saves no draft and costs no round trip', async () => {
|
|
770
|
+
const h = boot({ state: { projType: 'game' } });
|
|
771
|
+
await h.open();
|
|
772
|
+
const savesBefore = h.saves();
|
|
773
|
+
|
|
774
|
+
h.type('dev');
|
|
775
|
+
h.type('');
|
|
776
|
+
|
|
777
|
+
assert.equal(h.saves(), savesBefore, 'nothing is written to the draft');
|
|
778
|
+
assert.equal(h.fetches(), 1, 'and the catalog is already here — typing re-reads nothing');
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test('with no catalog there is nothing to search, so the box is not offered', async () => {
|
|
782
|
+
// A search field over a list that failed to load is an affordance that can
|
|
783
|
+
// only disappoint.
|
|
784
|
+
const h = boot({ state: { projType: 'game' }, fetchOk: false });
|
|
785
|
+
await h.open();
|
|
786
|
+
|
|
787
|
+
assert.equal(h.searchHidden(), true);
|
|
788
|
+
assert.equal(h.filterNote(), '', 'and no count for a band with no rows');
|
|
789
|
+
});
|