@bongos/core 1.19.1006 → 1.19.1008
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 +25 -20
- package/docs/copy-inventory.md +38 -36
- package/docs/copy-registry.json +74 -56
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/idea.js +2 -2
- package/modules/hall-ui/public/ideas.css +21 -0
- package/modules/hall-ui/public/ideas.html +59 -33
- package/modules/hall-ui/public/ideas.js +128 -17
- package/modules/hall-ui/public/ideas.states.json +3 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/hall_record_world.mjs +24 -3
- package/tests/idea_spark_hall.mjs +195 -4
- package/tests/ideator_large_dead_thought_proof.mjs +399 -0
|
@@ -264,7 +264,11 @@
|
|
|
264
264
|
|
|
265
265
|
renderRecommendation(draft);
|
|
266
266
|
|
|
267
|
-
|
|
267
|
+
formRendered = true;
|
|
268
|
+
// The meter is unhidden through the disclosure, which owns the rule for both
|
|
269
|
+
// directions; setting it here as well would be a second source of truth for
|
|
270
|
+
// the same visibility and is what let the reopen case rot unnoticed.
|
|
271
|
+
setDisclosure(fullOpen);
|
|
268
272
|
renderMeterSkeleton();
|
|
269
273
|
}
|
|
270
274
|
|
|
@@ -902,24 +906,78 @@
|
|
|
902
906
|
return true;
|
|
903
907
|
}
|
|
904
908
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
909
|
+
// ---------------------------------------------------------------------------
|
|
910
|
+
// the disclosure (task 1004087 / spec R04, decision 18)
|
|
911
|
+
// ---------------------------------------------------------------------------
|
|
912
|
+
//
|
|
913
|
+
// THE FULL IDEA FORM IS CLOSED UNTIL SOMEONE ASKS FOR IT, and this is the
|
|
914
|
+
// whole point of the fold. Decision 18 put the five questions on the same
|
|
915
|
+
// surface as the one-line composer and named that the riskiest call in the
|
|
916
|
+
// spec: a spark's value is that it costs nothing to have, and a five-field
|
|
917
|
+
// form standing permanently open under the fast path is exactly how a page
|
|
918
|
+
// starts feeling like something to be filled in.
|
|
919
|
+
//
|
|
920
|
+
// So "on the same surface" is spent on ONE press, and nothing else. While the
|
|
921
|
+
// form is shut this page does not fetch the template, does not wire the
|
|
922
|
+
// per-keystroke scorer, and does not hold the rail reserve open — the one-line
|
|
923
|
+
// path is the first thing under the heading at full width, and it is ready
|
|
924
|
+
// before any of the Full Idea machinery has run.
|
|
925
|
+
//
|
|
926
|
+
// Closing again HIDES, it does not discard: the draft lives in localStorage
|
|
927
|
+
// exactly as it did before, so the toggle is reversible in both directions and
|
|
928
|
+
// the button can say so without lying.
|
|
929
|
+
|
|
930
|
+
let fullBooted = false; // the template read was STARTED once
|
|
931
|
+
let formRendered = false; // ...and actually produced a form (and so a meter)
|
|
932
|
+
let fullOpen = false;
|
|
933
|
+
|
|
934
|
+
// The rail reserve belongs to the meter, so it arrives and leaves with it.
|
|
935
|
+
// style.css reserves `--rail-right-w` from `.app-content--railed > main` at the
|
|
936
|
+
// wide breakpoint whether or not anything is in the rail, which is why this is
|
|
937
|
+
// a class toggle and not simply `#fi-meter.hidden`.
|
|
938
|
+
function setRail(on) {
|
|
939
|
+
const c = document.getElementById('fi-content');
|
|
940
|
+
if (!c || !c.classList) return;
|
|
941
|
+
if (on) c.classList.add('app-content--railed');
|
|
942
|
+
else c.classList.remove('app-content--railed');
|
|
943
|
+
}
|
|
913
944
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
945
|
+
function setDisclosure(open) {
|
|
946
|
+
fullOpen = open;
|
|
947
|
+
const form = $('#fi-form');
|
|
948
|
+
const btn = $('#fi-open');
|
|
949
|
+
const note = $('#fi-disclose-note');
|
|
950
|
+
if (form) form.hidden = !open;
|
|
951
|
+
if (btn) {
|
|
952
|
+
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
953
|
+
btn.textContent = open ? 'Put the five questions away' : 'Answer the five questions';
|
|
954
|
+
}
|
|
955
|
+
if (note) {
|
|
956
|
+
note.textContent = open
|
|
957
|
+
? 'Putting them away keeps your draft — it is saved as you type.'
|
|
958
|
+
: 'Nothing you have typed above is lost by opening this.';
|
|
922
959
|
}
|
|
960
|
+
// The meter is the form's readout, so it follows the form BOTH WAYS — and the
|
|
961
|
+
// condition is written as one expression rather than a hide-only branch
|
|
962
|
+
// because the hide-only version was a real bug: `renderForm` is the only
|
|
963
|
+
// thing that ever unhid the meter, `bootFullIdea` runs it at most once, so
|
|
964
|
+
// opening, closing and opening again brought the form and the rail back with
|
|
965
|
+
// the readout gone for good — the rail reserve held open for a panel that
|
|
966
|
+
// was no longer there, which is the exact anti-pattern the fold removes.
|
|
967
|
+
//
|
|
968
|
+
// `formRendered`, not `fullBooted`: the boot flag is set BEFORE the template
|
|
969
|
+
// read, so it is true even when that read failed and there is no meter to
|
|
970
|
+
// show. An empty meter is a readout claiming a score of nothing.
|
|
971
|
+
const meter = $('#fi-meter');
|
|
972
|
+
if (meter) meter.hidden = !(open && formRendered);
|
|
973
|
+
setRail(open);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Everything the Full Idea half needs, run at most once and never on the fast
|
|
977
|
+
// path. Returns nothing: its failures are stated on the page, as before.
|
|
978
|
+
async function bootFullIdea() {
|
|
979
|
+
if (fullBooted) return;
|
|
980
|
+
fullBooted = true;
|
|
923
981
|
|
|
924
982
|
const r = await api.inbox.getInboxTemplate({});
|
|
925
983
|
if (!r.ok || !r.data || !r.data.template) {
|
|
@@ -958,5 +1016,58 @@
|
|
|
958
1016
|
}
|
|
959
1017
|
}
|
|
960
1018
|
|
|
1019
|
+
async function openFullIdea() {
|
|
1020
|
+
setDisclosure(true);
|
|
1021
|
+
await bootFullIdea();
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async function toggleFullIdea() {
|
|
1025
|
+
if (fullOpen) { setDisclosure(false); return; }
|
|
1026
|
+
await openFullIdea();
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
async function main() {
|
|
1030
|
+
// THE QUEUE IS LOADED FIRST, AND UNCONDITIONALLY. The composer and the spark
|
|
1031
|
+
// queue are independent surfaces that happen to share a page: the queue needs
|
|
1032
|
+
// no template. Loading it after the template check meant one failed fetch took
|
|
1033
|
+
// BOTH down — the reported symptom was "Could not load the idea template" AND a
|
|
1034
|
+
// queue stuck on "Loading…" forever, which read as two bugs and was one.
|
|
1035
|
+
// A surface that does not depend on the failure must not be hostage to it.
|
|
1036
|
+
loadQueue();
|
|
1037
|
+
|
|
1038
|
+
// Quick filing (R08) is a THIRD independent surface on this page, offered
|
|
1039
|
+
// only outside develop mode — developing a spark always produces a Full
|
|
1040
|
+
// Idea, never a fresh quick report (see the section's own header comment).
|
|
1041
|
+
if (DEVELOP_ID) {
|
|
1042
|
+
const scroll = $('#fq-compose-scroll');
|
|
1043
|
+
if (scroll) scroll.hidden = true;
|
|
1044
|
+
} else {
|
|
1045
|
+
bootQuickComposer();
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const openBtn = $('#fi-open');
|
|
1049
|
+
if (openBtn) openBtn.addEventListener('click', toggleFullIdea);
|
|
1050
|
+
setDisclosure(false);
|
|
1051
|
+
|
|
1052
|
+
// THE THREE CASES A SHUT FORM WOULD STRAND (task 1004087 / spec R04). The
|
|
1053
|
+
// done-when is "no dead links", and a link that lands on a page whose
|
|
1054
|
+
// relevant half is folded away is dead in the only sense that matters to
|
|
1055
|
+
// whoever followed it:
|
|
1056
|
+
//
|
|
1057
|
+
// ?develop=<id> the spark queue's own rows, and /idea/:id — the whole
|
|
1058
|
+
// request is "develop this", so arriving at a shut form
|
|
1059
|
+
// would be the page ignoring it.
|
|
1060
|
+
// #full the deep link anything else uses to mean "the composer",
|
|
1061
|
+
// including /idea/:id's fallback action.
|
|
1062
|
+
// a saved draft half-written work already in localStorage. Leaving it
|
|
1063
|
+
// behind a press nobody knows to make is how "a partial
|
|
1064
|
+
// draft is not lost on navigation" (R152) quietly stops
|
|
1065
|
+
// being true — the draft would still be there and no one
|
|
1066
|
+
// would ever see it again.
|
|
1067
|
+
if (DEVELOP_ID || location.hash === '#full' || readDraft()) {
|
|
1068
|
+
await openFullIdea();
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
961
1072
|
main();
|
|
962
1073
|
})();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_": "The hall's IDEA COMPOSER through modules/ui-design/kit/render.js (task 1003305). The five questions are rendered from GET /inbox/template and the spark queue from GET /inbox/sparks, so the populated form needs the hall-preview harness; the kit's own stub answers neither and renders the honest `could not load the idea template` line instead, which is the `no-template` state below.\n\n node scripts/hall-preview/server.js --port 4612 --fixture-me\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/ideas.states.json --base-in http://builders.localhost:4612 --base-out http://builders.localhost:4612\n\
|
|
2
|
+
"_": "The hall's IDEA COMPOSER through modules/ui-design/kit/render.js (task 1003305). The five questions are rendered from GET /inbox/template and the spark queue from GET /inbox/sparks, so the populated form needs the hall-preview harness; the kit's own stub answers neither and renders the honest `could not load the idea template` line instead, which is the `no-template` state below.\n\n node scripts/hall-preview/server.js --port 4612 --fixture-me\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/ideas.states.json --base-in http://builders.localhost:4612 --base-out http://builders.localhost:4612\n\nTHE FOLD (task 1004087 / spec R04) is why there are now three states rather than two. `compose` is what a builder actually lands on — the one-line composer at the top, the Full Idea form folded away behind one press, and NO rail reserve, which is the whole reason that state is rendered at 1440 as well as at 390/320. `full` is the same page one press later: the five questions, the meter, and the rail. Rendering only one of them would measure half the page, and the half that is measured on every arrival is the first.",
|
|
3
3
|
"page": "/ideas",
|
|
4
4
|
"surface": "hall-ui",
|
|
5
5
|
"stub": { "prefix": "/builders/" },
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"_console": "The 501 is the harness, not the page: the meter re-scores through POST /inbox/score on every keystroke and scripts/hall-preview/server.js answers every write with preview_readonly by design. Declared rather than hidden.",
|
|
9
9
|
"ignoreConsole": ["/api/(gds|bongos)/", "\\b401\\b", "\\b404\\b", "\\b501\\b", "Unauthorized", "Not Found", "Not Implemented", "load failed"],
|
|
10
10
|
"states": {
|
|
11
|
-
"compose": { "auth": true, "actions": [["wait", 700]], "expect": { "visible": ["#
|
|
11
|
+
"compose": { "auth": true, "actions": [["wait", 700]], "expect": { "visible": ["#fq-form", "#fi-queue", "#fi-open"] } },
|
|
12
|
+
"full": { "auth": true, "actions": [["wait", 700], ["click", "#fi-open"], ["wait", 700]], "expect": { "visible": ["#fi-form", "#fi-fields", "#fi-meter"] } },
|
|
12
13
|
"develop": { "auth": true, "url": "/ideas?develop=1000884", "actions": [["wait", 700]], "expect": { "visible": ["#fi-develop"] } }
|
|
13
14
|
},
|
|
14
15
|
"reducedMotion": { "auth": true, "actions": [["wait", 700]] }
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.1008",
|
|
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.1008",
|
|
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.1008",
|
|
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
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.1008'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -105,9 +105,30 @@ test('the shell owns BOTH halves of the rail, measured from one value', () => {
|
|
|
105
105
|
'the panel is placed from --topbar-h; ideas.css hand-copied it as 6.5rem and nothing kept the two in step');
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
test('the ideas composer OPTS IN
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
test('the ideas composer OPTS IN, and since the fold it opts in WITH THE METER', () => {
|
|
109
|
+
// The rule this case has always guarded is that a page takes the rail from the
|
|
110
|
+
// shell's own opt-in and never reaches into shell layout from its own sheet.
|
|
111
|
+
// Task 1004087 (spec R04) folded the Full Idea form away by default, so the
|
|
112
|
+
// opt-in moved from the markup to the moment the meter appears: the reserve
|
|
113
|
+
// narrows the one-line composer at every wide width, and holding it open for a
|
|
114
|
+
// panel nobody asked for is the same defect as hand-copying its offset was.
|
|
115
|
+
const ideasJs = read('ideas.js');
|
|
116
|
+
assert.match(ideasHtml, /class="fi-meter app-rail"/, 'the meter still takes the shell\'s rail frame');
|
|
117
|
+
// Matched on a class ATTRIBUTE, not anywhere in the file: the markup carries a
|
|
118
|
+
// comment naming the class to say why it is not there, and a bare substring
|
|
119
|
+
// search cannot tell an explanation from the thing it explains.
|
|
120
|
+
assert.doesNotMatch(ideasHtml, /class="[^"]*app-content--railed/,
|
|
121
|
+
'and the reserve is no longer held open in the markup');
|
|
122
|
+
assert.match(ideasJs, /classList\.add\('app-content--railed'\)/, 'ideas.js grants the reserve');
|
|
123
|
+
assert.match(ideasJs, /classList\.remove\('app-content--railed'\)/,
|
|
124
|
+
'and hands it back — a reserve only ever added is a static one written the long way');
|
|
125
|
+
// The half that would otherwise rot: the page must still not style shell
|
|
126
|
+
// layout. Read with the comments STRIPPED — the sheet's header explains at
|
|
127
|
+
// length which override it used to carry and where that override went, and a
|
|
128
|
+
// check that cannot tell a rule from the note about it fails on the note.
|
|
129
|
+
const ideasRules = ideasCss.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
130
|
+
assert.doesNotMatch(ideasRules, /(^|[\s,>])\.app-(content|rail)\b/m,
|
|
131
|
+
'ideas.css says what the meter LOOKS like, never where the shell puts it');
|
|
111
132
|
});
|
|
112
133
|
|
|
113
134
|
// ---- 2. the button vocabulary that had no rule -----------------------------
|
|
@@ -65,6 +65,13 @@ function boot({
|
|
|
65
65
|
// `{ outcome: null }`, which is the server saying "nothing to state yet"; the
|
|
66
66
|
// line must go quiet for both, and there is a case below for each.
|
|
67
67
|
landing = undefined,
|
|
68
|
+
// task 1004087 (spec R04) — the #full deep link, which auto-opens the folded
|
|
69
|
+
// Full Idea form. Part of boot() because the fold reads it once, at boot.
|
|
70
|
+
hash = '',
|
|
71
|
+
// task 1004087: a template read that FAILS. bootFullIdea returns early, so no
|
|
72
|
+
// form and no meter — distinct from "folded", and the distinction is the
|
|
73
|
+
// reason the meter follows formRendered rather than the boot flag.
|
|
74
|
+
templateOk = true,
|
|
68
75
|
// OUT-OF-ORDER READS (task 1004108). Names of fakes whose answers are PARKED
|
|
69
76
|
// instead of returned: the call is recorded and its response computed at once,
|
|
70
77
|
// but the caller's `await` does not resolve until the test releases it. That is
|
|
@@ -79,8 +86,14 @@ function boot({
|
|
|
79
86
|
const makeEl = () => ({
|
|
80
87
|
dataset: {}, style: {}, textContent: '', value: '', hidden: false, disabled: false,
|
|
81
88
|
readOnly: false, tabIndex: 0, _html: '', _handlers: {},
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
// task 1004087: attributes and classes are RECORDED rather than swallowed.
|
|
90
|
+
// The fold's two tells are an aria-expanded on the toggle and the rail class
|
|
91
|
+
// on the content wrapper, and a no-op stub agrees with every implementation
|
|
92
|
+
// including a broken one.
|
|
93
|
+
_attrs: {}, _classes: new Set(),
|
|
94
|
+
get classList() { const s = this._classes; return { add: (c) => s.add(c), remove: (c) => s.delete(c), contains: (c) => s.has(c) }; },
|
|
95
|
+
setAttribute(k, v) { this._attrs[String(k)] = String(v); },
|
|
96
|
+
getAttribute(k) { const v = this._attrs[String(k)]; return v === undefined ? null : v; },
|
|
84
97
|
addEventListener(t, fn) { this._handlers[t] = fn; },
|
|
85
98
|
querySelector() { return makeEl(); }, querySelectorAll() { return []; }, appendChild() {},
|
|
86
99
|
set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; },
|
|
@@ -114,7 +127,10 @@ function boot({
|
|
|
114
127
|
|
|
115
128
|
const client = {
|
|
116
129
|
inbox: {
|
|
117
|
-
|
|
130
|
+
// RECORDED (task 1004087): the fold's cost claim is 'the template is not
|
|
131
|
+
// read until someone asks, and is read once'. A fake that does not count
|
|
132
|
+
// its calls cannot tell either half from a page that reads it every time.
|
|
133
|
+
getInboxTemplate: async () => { calls.push(['template', null]); return templateOk ? { status: 200, ok: true, data: { template: TEMPLATE } } : { status: 500, ok: false, data: null }; },
|
|
118
134
|
getInboxSparks: async () => { calls.push(['sparks', null]); return { status: 200, ok: true, data: { sparks, count: count == null ? sparks.length : count } }; },
|
|
119
135
|
getInboxId: async (a) => { calls.push(['idea', a]); return { status: ideaOk ? 200 : 404, ok: ideaOk, data: ideaOk ? { idea } : null }; },
|
|
120
136
|
postInboxScore: async (a) => { calls.push(['score', a]); return { status: 200, ok: true, data: { score: 60, fields: TEMPLATE.fields.map((f) => ({ key: f.key, substance: 24, floor: 40, ratio: 0.6 })) } }; },
|
|
@@ -186,7 +202,7 @@ function boot({
|
|
|
186
202
|
setItem: (k, v) => { store[k] = String(v); },
|
|
187
203
|
removeItem: (k) => { delete store[k]; },
|
|
188
204
|
},
|
|
189
|
-
location: { hostname: 'builders.cloudbongos.com', pathname: '/ideas', search, assign() {} },
|
|
205
|
+
location: { hostname: 'builders.cloudbongos.com', pathname: '/ideas', search, hash, assign() {} },
|
|
190
206
|
addEventListener() {},
|
|
191
207
|
};
|
|
192
208
|
|
|
@@ -200,6 +216,10 @@ function boot({
|
|
|
200
216
|
calls, toasts, store, el, deferred,
|
|
201
217
|
queueHtml: () => el('fi-queue').innerHTML,
|
|
202
218
|
fill: () => { for (const f of TEMPLATE.fields) el(`fi-f-${f.key}`).value = `an answer for ${f.key} long enough to count`; },
|
|
219
|
+
// task 1004087 (spec R04) — the one press that unfolds the Full Idea form.
|
|
220
|
+
// Every case below that reaches the five questions now goes through it,
|
|
221
|
+
// because a real builder does too.
|
|
222
|
+
openFull: async () => { el('fi-open')._handlers.click(); await settle(); await settle(); },
|
|
203
223
|
submit: () => el('fi-form')._handlers.submit({ preventDefault() {} }),
|
|
204
224
|
submitQuick: () => el('fq-form')._handlers.submit({ preventDefault() {} }),
|
|
205
225
|
};
|
|
@@ -345,6 +365,10 @@ await test('the plain composer is unaffected — develop mode is never entered',
|
|
|
345
365
|
// tests/idea_hall_composer.mjs pins over the HTML source; what matters at
|
|
346
366
|
// runtime is that nothing reveals it, and the tell is that no idea was fetched.
|
|
347
367
|
assert.ok(!r.calls.some((c) => c[0] === 'idea'), 'no idea is loaded without ?develop=');
|
|
368
|
+
// task 1004087 (spec R04): in plain mode the five questions are FOLDED, so a
|
|
369
|
+
// builder who wants them presses for them. That press is the whole cost of
|
|
370
|
+
// decision 18, and it is spent here rather than assumed away.
|
|
371
|
+
await r.openFull();
|
|
348
372
|
assert.equal(r.el('fi-title').readOnly, false, 'the title stays editable');
|
|
349
373
|
r.el('fi-title').value = 'a new idea';
|
|
350
374
|
r.fill();
|
|
@@ -789,5 +813,172 @@ await test('a slow preview answering after a faster one never repaints the stale
|
|
|
789
813
|
// page rather than pin it. That is a production bug to file, not a test to
|
|
790
814
|
// write, and the harness is now able to reach it the day it is fixed.
|
|
791
815
|
|
|
816
|
+
|
|
817
|
+
// --- the fold (task 1004087 / spec R04, decision 18) ----------------------
|
|
818
|
+
//
|
|
819
|
+
// Decision 18 put the five questions on the same surface as the one-line
|
|
820
|
+
// composer and called it the riskiest decision in the spec, because a spark's
|
|
821
|
+
// whole value is that it costs nothing to have. The cases below are what
|
|
822
|
+
// "folded" has to mean for that to stay true, and each one is written as a
|
|
823
|
+
// property of the FAST path, not of the form: a page that opened the five
|
|
824
|
+
// questions eagerly would pass a test that only asked "is the form present".
|
|
825
|
+
|
|
826
|
+
await test('the fast path costs nothing: no template read, no scorer, no rail', async () => {
|
|
827
|
+
const r = boot();
|
|
828
|
+
await settle(); await settle();
|
|
829
|
+
assert.ok(!r.calls.some((c) => c[0] === 'template'), 'the template is not read at all until someone asks for the questions');
|
|
830
|
+
assert.ok(!r.calls.some((c) => c[0] === 'score'), 'nothing is scored before anyone asks to write');
|
|
831
|
+
assert.ok(!r.el('fi-fields').innerHTML.includes(TEMPLATE.fields[0].label),
|
|
832
|
+
'and the five questions are not rendered — the template read has not happened at all');
|
|
833
|
+
assert.equal(r.el('fi-form').hidden, true, 'the five questions are folded away');
|
|
834
|
+
assert.equal(r.el('fi-meter').hidden, true, 'and so is their readout');
|
|
835
|
+
assert.equal(r.el('fi-content')._classes.has('app-content--railed'), false,
|
|
836
|
+
'the rail reserve is NOT held open — that reserve narrows the one-line form at every wide width');
|
|
837
|
+
assert.equal(r.el('fi-open').getAttribute('aria-expanded'), 'false');
|
|
838
|
+
// The quick composer is live at the same moment, so the fold costs the fast
|
|
839
|
+
// path nothing in readiness either.
|
|
840
|
+
assert.equal(typeof r.el('fq-form')._handlers.submit, 'function', 'the one-line path is already submittable');
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
await test('one press unfolds it, and only then does the page fetch the template', async () => {
|
|
844
|
+
const r = boot();
|
|
845
|
+
await settle(); await settle();
|
|
846
|
+
await r.openFull();
|
|
847
|
+
assert.equal(r.el('fi-form').hidden, false);
|
|
848
|
+
assert.equal(r.el('fi-meter').hidden, false, 'the meter arrives with the form it reads');
|
|
849
|
+
assert.equal(r.el('fi-content')._classes.has('app-content--railed'), true, 'and the rail reserve arrives with the meter');
|
|
850
|
+
assert.equal(r.el('fi-open').getAttribute('aria-expanded'), 'true');
|
|
851
|
+
assert.ok(r.el('fi-fields').innerHTML.includes(TEMPLATE.fields[0].label), 'the five questions rendered from the template read');
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
await test('putting them away hides them and KEEPS the draft — it is not a discard', async () => {
|
|
855
|
+
const r = boot();
|
|
856
|
+
await settle(); await settle();
|
|
857
|
+
await r.openFull();
|
|
858
|
+
r.el('fi-title').value = 'half a thought';
|
|
859
|
+
r.fill();
|
|
860
|
+
r.el(`fi-f-${TEMPLATE.fields[0].key}`)._handlers.input();
|
|
861
|
+
await new Promise((res) => setTimeout(res, 700));
|
|
862
|
+
const saved = JSON.stringify(r.store);
|
|
863
|
+
r.el('fi-open')._handlers.click();
|
|
864
|
+
await settle();
|
|
865
|
+
assert.equal(r.el('fi-form').hidden, true, 'the form folds away again');
|
|
866
|
+
assert.equal(r.el('fi-content')._classes.has('app-content--railed'), false, 'and hands the width back');
|
|
867
|
+
assert.equal(r.el('fi-meter').hidden, true);
|
|
868
|
+
assert.equal(JSON.stringify(r.store), saved, 'nothing written by the draft was thrown away');
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
await test('the template is read ONCE however many times it is folded', async () => {
|
|
872
|
+
const r = boot();
|
|
873
|
+
await settle(); await settle();
|
|
874
|
+
await r.openFull();
|
|
875
|
+
r.el('fi-open')._handlers.click(); await settle();
|
|
876
|
+
await r.openFull();
|
|
877
|
+
// The template fake does not push to `calls`, so the tell is the render: a
|
|
878
|
+
// second boot would replace the rendered fields with the loading line first.
|
|
879
|
+
assert.equal(r.calls.filter((c) => c[0] === 'template').length, 1,
|
|
880
|
+
'the template is fetched on the FIRST open and never again — a re-boot per open would also re-wire every listener');
|
|
881
|
+
assert.ok(r.el('fi-fields').innerHTML.includes(TEMPLATE.fields[0].label), 'and the form survives a fold and an unfold');
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
// --- the three things a shut form would strand ----------------------------
|
|
885
|
+
|
|
886
|
+
await test('?develop= opens it without being asked — the whole request was "develop this"', async () => {
|
|
887
|
+
const r = boot({ search: '?develop=77' });
|
|
888
|
+
await settle(); await settle();
|
|
889
|
+
assert.equal(r.el('fi-form').hidden, false, 'a develop link that landed on a folded form would be a dead link');
|
|
890
|
+
assert.equal(r.el('fi-develop').hidden, false);
|
|
891
|
+
assert.equal(r.el('fi-open').getAttribute('aria-expanded'), 'true');
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
await test('the #full deep link opens it — /idea/:id sends people here by that name', async () => {
|
|
895
|
+
const r = boot({ hash: '#full' });
|
|
896
|
+
await settle(); await settle();
|
|
897
|
+
assert.equal(r.el('fi-form').hidden, false);
|
|
898
|
+
assert.equal(r.el('fi-content')._classes.has('app-content--railed'), true);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
await test('a saved draft opens it — half-written work is never left behind a press', async () => {
|
|
902
|
+
const r = boot({ store: { 'hall-full-idea-draft': JSON.stringify({ title: 'a thought from yesterday', values: {} }) } });
|
|
903
|
+
await settle(); await settle();
|
|
904
|
+
assert.equal(r.el('fi-form').hidden, false, 'a draft nobody can see is a draft that was lost');
|
|
905
|
+
assert.equal(r.el('fi-title').value, 'a thought from yesterday', 'and it is restored, not merely revealed');
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
await test('an UNRELATED stored key does not open it — only a real draft does', async () => {
|
|
909
|
+
const r = boot({ store: { 'hall-full-idea-draft:develop:99': JSON.stringify({ title: 'someone else', values: {} }) } });
|
|
910
|
+
await settle(); await settle();
|
|
911
|
+
assert.equal(r.el('fi-form').hidden, true, 'a develop-scoped draft is not this page\'s draft');
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
// --- the markup half ------------------------------------------------------
|
|
915
|
+
//
|
|
916
|
+
// The stub DOM does not parse ideas.html, so every `hidden` above is the one
|
|
917
|
+
// ideas.js set at runtime. These two read the real file, for the same reason
|
|
918
|
+
// the receipts and gravity cases in this suite do: the runtime assertions
|
|
919
|
+
// would all still pass against a page that shipped the form open.
|
|
920
|
+
|
|
921
|
+
await test('ideas.html ships the form hidden and the fast path FIRST', async () => {
|
|
922
|
+
const html = fs.readFileSync(path.join(HALL, 'ideas.html'), 'utf8');
|
|
923
|
+
assert.match(html, /<form id="fi-form" novalidate hidden>/, 'the five questions ship folded');
|
|
924
|
+
assert.match(html, /id="fi-open"[^>]*aria-expanded="false"/, 'and the toggle ships saying so');
|
|
925
|
+
assert.ok(html.indexOf('id="fq-compose-scroll"') < html.indexOf('id="fi-compose-scroll"'),
|
|
926
|
+
'the one-line composer comes before the five questions in the DOM, not merely on screen');
|
|
927
|
+
assert.ok(html.indexOf('id="fq-compose-scroll"') < html.indexOf('id="fi-queue-scroll"'),
|
|
928
|
+
'and before the spark queue, which feeds the form rather than the fast path');
|
|
929
|
+
assert.doesNotMatch(html, /class="app-content app-content--railed"/,
|
|
930
|
+
'the rail reserve is no longer held open in the markup — ideas.js grants it with the meter');
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
await test('/idea/:id sends people to a form that will be open when they arrive', async () => {
|
|
934
|
+
const src = fs.readFileSync(path.join(HALL, 'idea.js'), 'utf8');
|
|
935
|
+
assert.match(src, /\/ideas\?develop=\$\{encodeURIComponent\(idea\.id\)\}/,
|
|
936
|
+
'the spark page develops THAT spark, rather than dropping someone on a bare composer');
|
|
937
|
+
assert.match(src, /\/ideas#full/, 'and the fallback action names the composer the fold answers to');
|
|
938
|
+
assert.doesNotMatch(src, /href="\$\{hallBase\(\)\}\/ideas"/,
|
|
939
|
+
'no link is left pointing at the page with no word about which half it meant');
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
// --- REOPENING, which is the case the first round of this task missed --------
|
|
945
|
+
//
|
|
946
|
+
// The fold's own interaction is open → close → open, and the first version of
|
|
947
|
+
// it shipped a meter that never came back: `renderForm` was the only thing that
|
|
948
|
+
// ever unhid the meter, `bootFullIdea` runs it at most once, so the second open
|
|
949
|
+
// restored the form AND the rail reserve with the readout gone for good. Every
|
|
950
|
+
// close case below passed, because none of them opened again.
|
|
951
|
+
//
|
|
952
|
+
// The lesson is the one in the harness's own header about the parked reads: a
|
|
953
|
+
// probe that reds nothing is not proof the code is right, and neither is a case
|
|
954
|
+
// that stops one step short of the interaction it is named after.
|
|
955
|
+
|
|
956
|
+
await test('reopening brings the METER back, not just the form and the rail', async () => {
|
|
957
|
+
const r = boot();
|
|
958
|
+
await settle(); await settle();
|
|
959
|
+
await r.openFull();
|
|
960
|
+
assert.equal(r.el('fi-meter').hidden, false, 'shown on the first open');
|
|
961
|
+
r.el('fi-open')._handlers.click(); await settle();
|
|
962
|
+
assert.equal(r.el('fi-meter').hidden, true, 'away with the form');
|
|
963
|
+
await r.openFull();
|
|
964
|
+
assert.equal(r.el('fi-form').hidden, false, 'the form comes back');
|
|
965
|
+
assert.equal(r.el('fi-content')._classes.has('app-content--railed'), true, 'and so does the rail reserve');
|
|
966
|
+
assert.equal(r.el('fi-meter').hidden, false,
|
|
967
|
+
'and so does the meter — a rail held open for a panel that never returns is the anti-pattern the fold removes');
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
await test('a template that never loaded shows NO meter, however often it is folded', async () => {
|
|
971
|
+
const r = boot({ templateOk: false });
|
|
972
|
+
await settle(); await settle();
|
|
973
|
+
await r.openFull();
|
|
974
|
+
assert.equal(r.el('fi-meter').hidden, true, 'there is no form, so there is nothing to read out');
|
|
975
|
+
r.el('fi-open')._handlers.click(); await settle();
|
|
976
|
+
await r.openFull();
|
|
977
|
+
assert.equal(r.el('fi-meter').hidden, true,
|
|
978
|
+
'and reopening must not reveal an empty gauge — a readout showing nothing is a score of nothing');
|
|
979
|
+
assert.equal(r.el('fi-content')._classes.has('app-content--railed'), true,
|
|
980
|
+
'the rail still follows the OPEN state, so this is a real distinction and not both flags moving together');
|
|
981
|
+
});
|
|
982
|
+
|
|
792
983
|
console.log(`\nidea_spark_hall: ${passed} passed, ${failed} failed`);
|
|
793
984
|
if (failed > 0) process.exit(1);
|