@bongos/core 1.21.105 → 1.21.106
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 +53 -43
- package/docs/copy-inventory.md +368 -366
- package/docs/copy-registry.json +475 -457
- package/docs/design/project-startup-direction.md +14 -0
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +10 -3
- package/docs/page-readings.json +1023 -977
- package/modules/public-landing/public/projects-hall-live.states.json +3 -2
- package/modules/public-landing/public/projects-setup.states.json +91 -0
- package/modules/public-landing/public/projects.html +535 -804
- package/modules/public-landing/public/projects.probes.json +3 -3
- package/modules/public-landing/public/projects.states.json +1 -1
- package/modules/ui-design/kit/fixtures/provisioning-instances-hall-live.json +22 -2
- package/modules/ui-design/kit/fixtures/provisioning-instances-setup.json +195 -0
- package/modules/ui-design/kit/serve.js +22 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/cosmos_token_contrast.mjs +10 -18
- package/tests/github_handback_any_device.mjs +4 -3
- package/tests/github_one_trip.mjs +45 -47
- package/tests/github_org_account.mjs +2 -2
- package/tests/github_pick_up.mjs +40 -29
- package/tests/onboard.mjs +13 -7
- package/tests/project_invite_ui.mjs +6 -3
- package/tests/project_modules_ui.mjs +3 -3
- package/tests/projects_hub.mjs +8 -9
- package/tests/projects_hub_cloud_hop.mjs +77 -125
- package/tests/projects_hub_dns_ready.mjs +42 -51
- package/tests/projects_hub_pre_uat.mjs +68 -69
- package/tests/public_landing_projects.mjs +45 -102
- package/tests/setup_status_facts.mjs +400 -0
- package/tests/wizard_github_feedback.mjs +90 -87
- package/tests/wizard_one_page.mjs +11 -9
- package/tests/wizard_auto_open.mjs +0 -94
package/tests/github_pick_up.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// 2. the runner — a stopped sign-in is re-asked with the keys already placed and becomes
|
|
14
14
|
// 'done'; a placement that throws after GitHub's conversion is a SPENT code (never
|
|
15
15
|
// re-queued); GitHub's 404 on a conversion drops the dead code.
|
|
16
|
-
// 3. the REAL status route's answer run into the REAL page (seedSetup +
|
|
16
|
+
// 3. the REAL status route's answer run into the REAL page (seedSetup + part 03's facts, task 1004683):
|
|
17
17
|
// every reachable state draws a sentence and an action, none a chore or "contact us".
|
|
18
18
|
// 4. the page's own actions: pick-up posts to the pick-up route and watches again, Reopen
|
|
19
19
|
// GitHub goes through the mint route (which hands back the SAME state), and "Send it to
|
|
@@ -184,29 +184,29 @@ const express = require('express');
|
|
|
184
184
|
const app = express();
|
|
185
185
|
app.use('/api/bongos', require(path.join(ROOT, 'modules', 'provisioning', 'routes', 'github-setup.js'))());
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
const
|
|
187
|
+
// part 03's facts (task 1004683): the pure half of the facts region, the seed, and the actions
|
|
188
|
+
const FS = SCRIPT.indexOf(' function ghStep(key) {');
|
|
189
|
+
const FE = SCRIPT.indexOf(' /* ── drawing it: nodes and textContent only', FS);
|
|
189
190
|
const SS = SCRIPT.indexOf(' /* the checklist the server derived (task 1004649)');
|
|
190
191
|
const SE = SCRIPT.indexOf(' function setupOpen() {', SS);
|
|
191
192
|
const AS = SCRIPT.indexOf(' /* ── Pick up where it stopped (task 1004652).');
|
|
192
193
|
const AE = SCRIPT.indexOf(' function startManifest(btn) {', AS);
|
|
193
194
|
|
|
194
195
|
test('the page regions are findable (a rename must fail loudly)', () => {
|
|
195
|
-
assert.ok(
|
|
196
|
+
assert.ok(FS > 0 && FE > FS && SS > 0 && SE > SS && AS > 0 && AE > AS);
|
|
196
197
|
});
|
|
197
198
|
|
|
198
|
-
const esc = (x) => String(x).replace(/[&<>"']/g, (ch) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]));
|
|
199
199
|
function page(extra = {}) {
|
|
200
|
+
const now = new Date().toISOString();
|
|
200
201
|
const sandbox = {
|
|
201
|
-
live: { manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null, instStatus: 'active', addrReady: true, pickUp: null, verifying: false, askedPlatform: false
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
progressMarkup: (a, b, note) => `<prog>${note}</prog>`, encodeURIComponent, JSON,
|
|
202
|
+
live: { manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null, instStatus: 'active', addrReady: true, pickUp: null, verifying: false, askedPlatform: false,
|
|
203
|
+
inst: { id: 42, status: 'active', domain: 'mercury.example.com', dns_resolves: true, last_verified_up: true, last_verified_at: now, created_at: now, provisioned_at: now, open_intent: null }, readAt: Date.now(), gone: '', repo: null },
|
|
204
|
+
state: { name: 'Mercury', slug: 'mercury', owner: 'acme', repo: 'mercury' },
|
|
205
|
+
resolvedDomain: () => 'mercury.example.com', apexBase: () => 'example.com', addressReady: () => true, API: '/api/bongos',
|
|
206
206
|
...extra,
|
|
207
207
|
};
|
|
208
208
|
vm.createContext(sandbox);
|
|
209
|
-
vm.runInContext(`${SCRIPT.slice(
|
|
209
|
+
vm.runInContext(`${SCRIPT.slice(FS, FE)}\n${SCRIPT.slice(SS, SE)}\n${SCRIPT.slice(AS, AE)}`, sandbox);
|
|
210
210
|
return sandbox;
|
|
211
211
|
}
|
|
212
212
|
// what the page's poll does with one answer (pollManifest's tick, minus the timer)
|
|
@@ -216,7 +216,12 @@ function seedFrom(sb, d) {
|
|
|
216
216
|
sb.live.manifest = d.ready ? 'done' : d.status;
|
|
217
217
|
sb.live.manifestFailure = d.failure || null;
|
|
218
218
|
}
|
|
219
|
-
|
|
219
|
+
// the GitHub facts and the one sentence, as the founder reads them (vm-born values through JSON)
|
|
220
|
+
function facts(sb) {
|
|
221
|
+
return JSON.parse(JSON.stringify(vm.runInContext('(function () { var n = Date.now(), h = homeFacts(live.inst, n), g = ghFacts(n); return { gh: g, said: setupSentence(h, g) }; })()', sb)));
|
|
222
|
+
}
|
|
223
|
+
const words = (r) => [r.title, [].concat(r.sub || []).map((p) => (typeof p === 'string' ? p : p.t)).join('')].join(' ');
|
|
224
|
+
const acts = (r) => [].concat(r.act || []);
|
|
220
225
|
const CHORE = /\/contact|Contact us|delete it|Developer settings → GitHub Apps\) so|Try again anyway|Start the setup again|by hand/;
|
|
221
226
|
|
|
222
227
|
// every shape the status derivation can reach, as rows the real route reads
|
|
@@ -256,31 +261,37 @@ test('every state the setup can reach: the REAL status answer draws a sentence a
|
|
|
256
261
|
assert.equal(d.ok, true, name);
|
|
257
262
|
const sb = page();
|
|
258
263
|
seedFrom(sb, d);
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
264
|
+
const f = facts(sb);
|
|
265
|
+
const all = f.gh.map(words).join('\n') + '\n' + f.said.map((p) => p.t).join('');
|
|
266
|
+
const allActs = f.gh.flatMap(acts);
|
|
267
|
+
seen[name] = { pick: d.pick_up, f, all, allActs };
|
|
268
|
+
assert.doesNotMatch(all, CHORE, `${name}: no manual chore and no "contact us"`);
|
|
269
|
+
// a stopped fact always carries the action that fixes it
|
|
270
|
+
for (const r of f.gh.filter((x) => x.state === 'bad')) {
|
|
271
|
+
assert.ok(acts(r).some((a) => /^(pick-up|pick-up-link|connect|ask-platform|manage)$/.test(a)), `${name}: the stopped "${r.title}" has its button`);
|
|
272
|
+
}
|
|
273
|
+
// a pick-up the server offers is on the page, once
|
|
274
|
+
if (d.pick_up === 'check' || d.pick_up === 'convert') {
|
|
275
|
+
assert.equal(allActs.filter((a) => /^pick-up/.test(a) || a === 'check-again').length, 1, `${name}: ${d.pick_up} is offered once`);
|
|
265
276
|
}
|
|
266
|
-
|
|
267
|
-
if (d.pick_up === '
|
|
268
|
-
if (d.pick_up === 'operator') assert.match(html, /data-action="ask-platform"/, `${name}: the platform team is one click away`);
|
|
269
|
-
if (d.pick_up === 'reopen') assert.match(html, /data-action="reopen-github"/, `${name}: the same link can be reopened`);
|
|
277
|
+
if (d.pick_up === 'operator') assert.ok(allActs.includes('ask-platform'), `${name}: the platform team is one click away`);
|
|
278
|
+
if (d.pick_up === 'reopen') assert.ok(allActs.includes('reopen'), `${name}: the same link can be reopened`);
|
|
270
279
|
// a new link is offered ONLY where nothing was made on GitHub
|
|
271
|
-
if (
|
|
280
|
+
if (allActs.includes('connect')) assert.ok(d.pick_up === 'fresh', `${name}: Connect GitHub is offered only when no app exists (pick_up=${d.pick_up})`);
|
|
272
281
|
}
|
|
273
282
|
assert.equal(seen['waiting on GitHub'].pick, 'reopen');
|
|
274
283
|
assert.equal(seen['link lapsed'].pick, 'fresh');
|
|
275
284
|
assert.equal(seen['conversion stopped, code young'].pick, 'convert');
|
|
276
285
|
assert.equal(seen['conversion stopped, code spent'].pick, 'operator');
|
|
277
286
|
assert.equal(seen['sign-in unconfirmed, re-asking'].pick, null);
|
|
278
|
-
assert.match(seen['sign-in unconfirmed, re-asking'].
|
|
287
|
+
assert.match(seen['sign-in unconfirmed, re-asking'].all, /Asking your project again, with the app GitHub already made\./);
|
|
279
288
|
assert.equal(seen['sign-in unconfirmed, window over'].pick, 'check');
|
|
280
|
-
|
|
281
|
-
assert.
|
|
289
|
+
const never = seen['signed in, install never confirmed, window over'];
|
|
290
|
+
assert.equal(never.f.gh[1].sub, 'GitHub stopped before the app was on acme/mercury.', 'the mockup\'s sentence, from the real read');
|
|
291
|
+
assert.equal(never.f.said.map((p) => p.t).join(''), 'One step stopped. Nothing is lost, and no second app will be made.', 'said once, in the sentence');
|
|
292
|
+
assert.ok(seen['installed, device flow off, window over'].allActs.includes('check-again'));
|
|
282
293
|
assert.equal(seen['all four ticked'].pick, null);
|
|
283
|
-
assert.
|
|
294
|
+
assert.ok(!seen['all four ticked'].f.gh.some((r) => r.state === 'bad') && !seen['all four ticked'].allActs.some((a) => /^pick-up/.test(a)));
|
|
284
295
|
t.diagnostic(Object.entries(seen).map(([k, v]) => `${k}: ${v.pick}`).join(' · '));
|
|
285
296
|
});
|
|
286
297
|
|
|
@@ -289,13 +300,13 @@ test('every state the setup can reach: the REAL status answer draws a sentence a
|
|
|
289
300
|
function actions({ pickUp, answer, manifestState = null, blockerStatus = 201 }) {
|
|
290
301
|
const calls = { fetched: [], polled: [], progress: [], started: 0, renders: 0 };
|
|
291
302
|
const sb = page({
|
|
292
|
-
|
|
303
|
+
instanceId: 42, manifestState, pollTimer: null, manifestStartedAt: 1700000000000, ghTab: null,
|
|
293
304
|
fetch: async (url, init) => {
|
|
294
305
|
calls.fetched.push({ url, init });
|
|
295
306
|
if (/\/blockers$/.test(url)) return { ok: blockerStatus < 300, status: blockerStatus, json: async () => ({}) };
|
|
296
307
|
return { ok: true, status: 200, json: async () => answer };
|
|
297
308
|
},
|
|
298
|
-
rememberManifest: () => {},
|
|
309
|
+
rememberManifest: () => {}, repaintFacts: () => { calls.renders++; }, paintFacts: () => { calls.renders++; },
|
|
299
310
|
pollManifest: (s) => calls.polled.push(s), startProgress: (id) => calls.progress.push(id),
|
|
300
311
|
startManifest: () => { calls.started++; },
|
|
301
312
|
});
|
package/tests/onboard.mjs
CHANGED
|
@@ -193,11 +193,12 @@ test('every render surface carries the terminal hall step (task 1002703)', () =>
|
|
|
193
193
|
const cli = read('scripts/gds/onboard.js');
|
|
194
194
|
assert.match(cli, /builders hall/i, 'the CLI walkthrough ends at the hall');
|
|
195
195
|
assert.match(cli, /bongos dev/, 'the CLI offers the local-hall one-liner when no domain');
|
|
196
|
+
// the one page (task 1004683): the hall is entered through the Enter Genesis door, the
|
|
197
|
+
// hall's founding mode, once every required fact of part 03 is done
|
|
196
198
|
const hubHtml = read('modules/public-landing/public/projects.html');
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
assert.match(
|
|
200
|
-
assert.match(enterHall.join(''), /bongos dev/, 'the wizard offers the local hall when no domain');
|
|
199
|
+
assert.match(hubHtml, /<a class="btn" id="enterGenesis" data-onboard-step="enter-hall" target="_blank" rel="noopener">Enter Genesis<\/a>/,
|
|
200
|
+
'the ?view=new wizard tags the enter-hall step on its Enter Genesis door');
|
|
201
|
+
assert.match(hubHtml, /\$\('enterGenesis'\)\.setAttribute\('href', 'https:\/\/' \+ inst\.domain\);/, 'which opens the project\'s own address');
|
|
201
202
|
});
|
|
202
203
|
|
|
203
204
|
test('the wizard done panel is plan-driven, manifest-driven, and owner-honest (task 1002694)', () => {
|
|
@@ -206,8 +207,10 @@ test('the wizard done panel is plan-driven, manifest-driven, and owner-honest (t
|
|
|
206
207
|
const { readFileSync } = require('node:fs');
|
|
207
208
|
const path = require('node:path');
|
|
208
209
|
const src = readFileSync(path.join(import.meta.dirname, '..', 'modules/public-landing/public/projects.html'), 'utf8');
|
|
209
|
-
// gap 2 (idea 1000675): the checklist
|
|
210
|
-
|
|
210
|
+
// gap 2 (idea 1000675): the checklist follows the canonical plan. Since task 1004683 part
|
|
211
|
+
// 03 is a fixed list of facts the server checks, each GitHub fact named by the plan step
|
|
212
|
+
// it is (FACT_PLAN_STEP); the drift guard below holds that set equal to the plan's.
|
|
213
|
+
assert.match(src, /var FACT_PLAN_STEP = \{ approve: 'oauth-app', install: 'app-install', sign_in: 'first-sign-in', device_flow: 'device-flow' \};/, 'the facts name their plan steps');
|
|
211
214
|
// gap 3 (idea 1000676): the ADR 0133 one-click manifest flow has a UI caller again
|
|
212
215
|
assert.match(src, /\/github-app/, 'the done panel drives the GitHub-App manifest flow');
|
|
213
216
|
assert.match(src, /github-app\/status/, 'and polls its progress to reflect live state');
|
|
@@ -305,7 +308,10 @@ test('the web form (?view=new) declares EXACTLY the canonical human steps — dr
|
|
|
305
308
|
// keys are kebab-case literals; the charset excludes the page's DYNAMIC fallback
|
|
306
309
|
// tag (data-onboard-step="' + esc(s.key) + '"), which renders unknown plan steps
|
|
307
310
|
// at runtime and has no literal key to pin here (task 1002694).
|
|
308
|
-
|
|
311
|
+
// …plus the plan step each of part 03's facts is (task 1004683), set from one map
|
|
312
|
+
const facts = (src.match(/var FACT_PLAN_STEP = \{([^}]*)\};/) || [, ''])[1];
|
|
313
|
+
const found = [...src.matchAll(/data-onboard-step="([a-z0-9-]+)"/g)].map((m) => m[1])
|
|
314
|
+
.concat([...facts.matchAll(/: '([a-z0-9-]+)'/g)].map((m) => m[1]));
|
|
309
315
|
const uniq = [...new Set(found)].sort();
|
|
310
316
|
assert.deepEqual(uniq, humanKeys, `${name} (${rel}) must tag exactly the canonical human steps [${humanKeys}], found [${uniq}]`);
|
|
311
317
|
}
|
|
@@ -523,7 +523,9 @@ function bootGate({ me = { github_login: 'kestrel' }, created = { id: 7, slug: '
|
|
|
523
523
|
test('the wizard opens the invite step only once the hall is up, and only for a signed-in owner of a project that exists (tasks 1002346, 1004503)', () => {
|
|
524
524
|
assert.ok(gateSlice().includes('function paintInviteGate(live)'), 'the gate slice is found');
|
|
525
525
|
assert.match(SCRIPT, /function renderDone\(\) \{[\s\S]*?paintInviteGate\(false\);/, 'the done panel paints the gate shut');
|
|
526
|
-
|
|
526
|
+
// since task 1004683: "is alive" — and with it the invite — opens once every required fact
|
|
527
|
+
// of part 03 is done, which includes the hall answering (last_verified_up)
|
|
528
|
+
assert.match(SCRIPT, /var on = !!\(ready && inst && hostOk\(inst\.domain\) && addressReady\(inst\) && inst\.last_verified_up === true\);[\s\S]{0,900}paintInviteGate\(on\);/, 'and opens it only on the verified Live');
|
|
527
529
|
assert.equal((SCRIPT.match(/setInviteSlug\('wizInvite'/g) || []).length, 2, 'the gate is the only caller: shut, or open on Live');
|
|
528
530
|
|
|
529
531
|
const before = bootGate();
|
|
@@ -577,8 +579,9 @@ test('the wizard’s invite step ships as real markup in part 03, with the alive
|
|
|
577
579
|
assert.match(panel, /data-invite-send="wizInvite"/, 'the sender');
|
|
578
580
|
assert.match(panel, /data-invite-skip="wizInvite"/, 'and the skip is a CONTROL, not "leave it and move on"');
|
|
579
581
|
assert.match(panel, /Optional/, 'the panel says so before it asks');
|
|
580
|
-
assert.ok(panel.indexOf('id="
|
|
581
|
-
'
|
|
582
|
+
assert.ok(panel.indexOf('id="ghFacts"') > 0 && panel.indexOf('id="ghFacts"') < panel.indexOf('id="done-h"') && panel.indexOf('id="done-h"') < panel.indexOf('id="wizInvite"') &&
|
|
583
|
+
panel.indexOf('id="wizInvite"') < panel.indexOf('id="enterGenesis"'),
|
|
584
|
+
'the mockup\'s order: the facts, then "is alive", then the invite, then the Enter Genesis door (tasks 1004648, 1004683)');
|
|
582
585
|
assert.match(panel, /Invite your first builders\. They become founding builders\./, 'in the mockup\'s words');
|
|
583
586
|
assert.match(panel, /<p class="samePath" id="wizInviteLater"><\/p>/, 'and the way back afterwards is a slot beside the modules one');
|
|
584
587
|
assert.match(SCRIPT, /\$\('wizInviteLater'\)\.innerHTML = 'You can invite builders any time from ' \+ projectPageLink\('the project’s own page'\) \+ '\.';/,
|
|
@@ -299,10 +299,10 @@ test('the catalog is read ONCE however often the section repaints', async () =>
|
|
|
299
299
|
|
|
300
300
|
test('the wizard’s done panel points at the project’s settings for its extras', () => {
|
|
301
301
|
assert.match(HUB, /<p class="samePath" id="doneModules"><\/p>/, 'the panel carries the slot');
|
|
302
|
-
assert.match(SCRIPT,
|
|
303
|
-
'and renderDone fills it with one sentence pointing at the manage page');
|
|
302
|
+
assert.match(SCRIPT, /var mods = \$\('doneModules'\);\n\s*mods\.textContent = '';\n\s*partsInto\(mods, \['Add or remove modules later in ', \{ t: 'the project’s settings', href: manageHref\(\) \}, '\.'\]\);/,
|
|
303
|
+
'and renderDone fills it with one sentence pointing at the manage page, drawn from nodes (task 1004683)');
|
|
304
304
|
assert.equal((SCRIPT.match(/doneModules/g) || []).length, 1, 'one line, written in one place');
|
|
305
|
-
assert.match(SCRIPT, /function
|
|
305
|
+
assert.match(SCRIPT, /function manageHref\(\) \{\s*return instanceId \? '\?view=manage&id=' \+ encodeURIComponent\(instanceId\)/,
|
|
306
306
|
'and the link names the instance just created, not the list');
|
|
307
307
|
});
|
|
308
308
|
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -2190,7 +2190,7 @@ test('the wizard folds at one width: no rail fold is left, and no 900px rule is
|
|
|
2190
2190
|
assert.match(STYLE, /@media \(max-width:560px\)\{\s*\.setRow\{grid-template-columns:minmax\(0,1fr\) auto;grid-template-areas:"k acts" "v v";/, 'part 02\'s rows stack under 560, Change beside the label');
|
|
2191
2191
|
});
|
|
2192
2192
|
|
|
2193
|
-
test('the wizard\'s own structures wear the world: hairline fields, cells of one band,
|
|
2193
|
+
test('the wizard\'s own structures wear the world: hairline fields, cells of one band, part 02\'s rows, part 03\'s checked facts (tasks 1003346, 1004683)', () => {
|
|
2194
2194
|
const at = STYLE.indexOf('the create wizard (task 1002763');
|
|
2195
2195
|
assert.ok(at > -1, 'the wizard block is still the page\'s own');
|
|
2196
2196
|
const block = STYLE.slice(at);
|
|
@@ -2204,13 +2204,12 @@ test('the wizard\'s own structures wear the world: hairline fields, cells of one
|
|
|
2204
2204
|
assert.match(block, /\.setRows\{margin:14px 0 0;border-top:1px solid var\(--rule\);\}/, 'part 02\'s rows hang from one hairline');
|
|
2205
2205
|
assert.match(block, /\.setRow\{display:grid;grid-template-columns:108px minmax\(0,1fr\) auto;[^}]*border-bottom:1px solid var\(--rule\);\}/, 'each row is label · value · Change on its own hairline');
|
|
2206
2206
|
assert.doesNotMatch(block, /\.review( li)?\{/, 'the ledger\'s rules are gone with renderReview');
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
assert.match(block, /\.
|
|
2210
|
-
assert.match(block,
|
|
2211
|
-
assert.match(block, /\.
|
|
2212
|
-
assert.
|
|
2213
|
-
assert.match(block, /\.doneHead \.tick\{display:none;\}/, 'the tick leaves; the rail says filed');
|
|
2207
|
+
// task 1004683 (docs/design/mocks/day-night-rooms/setup-status.html): part 03 is one list
|
|
2208
|
+
// of facts on hairlines — a dot, the fact, when it was checked — and no stage strip at all
|
|
2209
|
+
assert.match(block, /\.setFact\{display:grid;grid-template-columns:24px minmax\(0,1fr\) auto;[^}]*border-top:1px solid var\(--rule\);\}/, 'each fact is dot · fact · when, on its own hairline');
|
|
2210
|
+
assert.match(block, /\.setFact\.done \.d\{background:color-mix\(in srgb,var\(--ok\) 12%,transparent\);color:var\(--ok\);border-color:var\(--ok\);\}/, 'a verified fact wears the tick in the ok ink');
|
|
2211
|
+
assert.match(block, /\.setFact\.bad \.d\{[^}]*color:var\(--err\);/, 'a stopped fact wears the error ink');
|
|
2212
|
+
assert.doesNotMatch(block, /\.prog\b|\.humanSteps|\.doneHead|\.stepsHead|\.ghbtn/, 'the standup strip, the old checklist and their sheet are gone');
|
|
2214
2213
|
assert.match(block, /\.addrBig code\{display:block;font-family:var\(--font-display\);/, 'the address stands in the display voice');
|
|
2215
2214
|
assert.doesNotMatch(block, /linear-gradient/, 'no tinted plates');
|
|
2216
2215
|
// the one exception is drawn, not chrome: the night room's moon glows in the window
|
|
@@ -2221,7 +2220,7 @@ test('the wizard\'s own structures wear the world: hairline fields, cells of one
|
|
|
2221
2220
|
assert.doesNotMatch(HUB, /data-back="|id="wizBack"/, 'no back door is left');
|
|
2222
2221
|
assert.match(HUB, /<div class="wizTop">\s*<span class="wizEyebrow" id="wizEyebrow">New project<\/span>\s*<\/div>/, 'the eyebrow alone in the top row');
|
|
2223
2222
|
assert.match(HUB, /<div class="createBar">\s*<button type="button" class="btn big" id="doCreate">Create my project<\/button>\s*<span class="wizRecap" id="wizRecap"><\/span>/, 'Create, with the one-line recap beside it');
|
|
2224
|
-
assert.match(HUB, /<h2 id="done-part-h"><span class="n">03<\/span>Connect GitHub <span class="state" id="ghState">opens once your project exists<\/span><\/h2>/, 'part 03 names itself and where it is');
|
|
2223
|
+
assert.match(HUB, /<h2 id="done-part-h"><span class="n">03<\/span><span id="donePartT">Connect GitHub<\/span> <span class="state" id="ghState">opens once your project exists<\/span><\/h2>/, 'part 03 names itself and where it is');
|
|
2225
2224
|
// what is enqueued is unchanged (ADR 0111): the filing body the control plane reads
|
|
2226
2225
|
assert.match(SCRIPT, /body: JSON\.stringify\(\{\s*slug: state\.slug,/, 'the filing still carries the slug first');
|
|
2227
2226
|
assert.match(SCRIPT, /hosting_shape: 'cloud-host',/, 'the shape is unchanged');
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
// tests/projects_hub_cloud_hop.mjs
|
|
2
2
|
//
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// The cloud hop (task 1004578, from idea 1001205): a little game with one cloud per
|
|
4
|
+
// PUBLIC project on the platform, played while a project is being set up. Since task
|
|
5
|
+
// 1004683 (the design of record docs/design/mocks/day-night-rooms/setup-status.html) it
|
|
6
|
+
// waits behind a "Play cloud hop while you wait" link and opens in place only when
|
|
7
|
+
// asked: the wait never depends on it, and nothing on a timer stands in for a fact. The
|
|
8
|
+
// owner set the game's rules on a played prototype, and the nausea ones are the ones a
|
|
9
|
+
// refactor would quietly lose: the clouds stand still, the sky stands still, the view
|
|
10
|
+
// does not follow a jump.
|
|
9
11
|
//
|
|
10
|
-
// EXECUTED by the vm-slice idiom:
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// primitives or through JSON.
|
|
12
|
+
// EXECUTED by the vm-slice idiom: the link (hopOpen), the arrival (hopArrive), the
|
|
13
|
+
// physics (hopStep) and the score line (hopScore) are sliced function by function and
|
|
14
|
+
// run under narrow stubs. Every slice is asserted to exist, so a rename fails loudly
|
|
15
|
+
// instead of testing nothing. vm-born values are compared as primitives or through JSON.
|
|
16
|
+
// Part 03's facts themselves are tests/setup_status_facts.mjs.
|
|
16
17
|
//
|
|
17
18
|
// Run: node --test tests/projects_hub_cloud_hop.mjs
|
|
18
|
-
|
|
19
19
|
import { test } from 'node:test';
|
|
20
20
|
import { strict as assert } from 'node:assert';
|
|
21
21
|
import fs from 'node:fs';
|
|
@@ -47,69 +47,71 @@ const HOP_CONSTS = (SCRIPT.match(/\n {4}var HOP_GAP = [^\n]*\n/) || [''])[0];
|
|
|
47
47
|
assert.ok(HOP_CONSTS.includes('HOP_JUMP'), 'the hop constants line must be findable');
|
|
48
48
|
const HOP_REGION = between('/* ══ THE CLOUD HOP (task 1004578)', '/* ── the hub\'s three doors ── */');
|
|
49
49
|
|
|
50
|
-
// ── the moment ──────────────────────────────────────────────────────────────────
|
|
51
|
-
const PROGRESS = between('var PROG_FLOWS = {', 'function startProgress(');
|
|
52
|
-
const OVERLAY = between('function verifiedAt(inst)', 'function chipMarkup(');
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
// ── behind its link (task 1004683) ──────────────────────────────────────────────
|
|
52
|
+
// The wait never depends on the game: it opens in place only when asked, and puts
|
|
53
|
+
// itself away (after the curtain, if it was running) once every required fact is done.
|
|
54
|
+
function bootLink({ reduced = false, running = false, since = 0 } = {}) {
|
|
55
|
+
const box = { hidden: true, classList: { remove() {}, add() {} } };
|
|
56
|
+
const link = { textContent: 'Play cloud hop while you wait', attrs: {}, setAttribute(k, v) { this.attrs[k] = v; } };
|
|
57
|
+
const calls = { start: 0, stop: 0, curtain: 0, raf: 0, done: null };
|
|
57
58
|
const sandbox = {
|
|
58
|
-
$: (id) => (
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
$: (id) => ({ hop: box, hopLink: link })[id] || null, REDUCED: reduced,
|
|
60
|
+
performance: { now: () => 10000 },
|
|
61
|
+
hopStart: () => { calls.start += 1; }, hopStop: () => { calls.stop += 1; },
|
|
62
|
+
hopCurtain: (done) => { calls.curtain += 1; calls.done = done; }, hopFrame() {},
|
|
63
|
+
requestAnimationFrame: () => { calls.raf += 1; return 1; }, cancelAnimationFrame() {},
|
|
61
64
|
};
|
|
62
65
|
vm.createContext(sandbox);
|
|
63
|
-
vm.runInContext(
|
|
64
|
-
return { sandbox,
|
|
66
|
+
vm.runInContext(`var hop = { target: '', raf: ${running ? 1 : 0}, since: ${since}, tr: 0 };\n${fn('hopOpen')}\n${fn('hopArrive')}`, sandbox);
|
|
67
|
+
return { sandbox, box, link, calls };
|
|
65
68
|
}
|
|
66
|
-
const NOW = () => new Date().toISOString();
|
|
67
|
-
const live = { status: 'active', domain: 'mercury.cloudbongos.com', dns_resolves: true, last_verified_up: true, last_verified_at: NOW(), open_intent: null };
|
|
68
|
-
|
|
69
|
-
test('the moment follows the same branches renderProgress speaks', () => {
|
|
70
|
-
const { sandbox } = bootMoment();
|
|
71
|
-
const m = (inst) => sandbox.momentFor(inst);
|
|
72
|
-
assert.equal(m({ status: 'requested' }), 'wait', 'filed: the hop');
|
|
73
|
-
assert.equal(m({ status: 'provisioning' }), 'wait', 'standing up: the hop');
|
|
74
|
-
assert.equal(m({ ...live, dns_resolves: false, last_verified_up: null }), 'wait', 'address still being set up: keep playing');
|
|
75
|
-
assert.equal(m({ ...live, last_verified_up: null }), 'wait', 'standing, not checked yet: keep playing');
|
|
76
|
-
assert.equal(m({ ...live, last_verified_up: false }), 'wait', 'not answering right now: keep playing');
|
|
77
|
-
assert.equal(m(live), 'arrive', 'answering on a resolved address: arrive');
|
|
78
|
-
assert.equal(m({ ...live, open_intent: { action: 'standup', state: 'running' } }), 'arrive',
|
|
79
|
-
'an intent renderProgress does not speak for (a running standup) falls through to the status');
|
|
80
|
-
assert.equal(m({ ...live, open_intent: { action: 'provision' } }), 'wait', 'a queued standup is still a wait');
|
|
81
|
-
assert.equal(m({ status: 'torn_down', open_intent: { action: 'provision' } }), 'wait', 'a queued revive is a wait');
|
|
82
|
-
});
|
|
83
69
|
|
|
84
|
-
test('
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
assert.equal(
|
|
88
|
-
assert.equal(
|
|
89
|
-
assert.equal(
|
|
90
|
-
assert.equal(
|
|
91
|
-
|
|
92
|
-
assert.equal(
|
|
70
|
+
test('the game opens in place only when asked, and its link says so', () => {
|
|
71
|
+
const h = bootLink();
|
|
72
|
+
h.sandbox.hopOpen(true);
|
|
73
|
+
assert.equal(h.box.hidden, false);
|
|
74
|
+
assert.equal(h.link.attrs['aria-expanded'], 'true');
|
|
75
|
+
assert.equal(h.sandbox.hop.target, 'wait', 'only an opened game may run (hopRunning reads it)');
|
|
76
|
+
assert.equal(h.calls.start, 1);
|
|
77
|
+
h.sandbox.hopOpen(false);
|
|
78
|
+
assert.equal(h.box.hidden, true);
|
|
79
|
+
assert.equal(h.link.attrs['aria-expanded'], 'false');
|
|
80
|
+
assert.equal(h.link.textContent, 'Play cloud hop while you wait');
|
|
81
|
+
assert.equal(h.sandbox.hop.target, '');
|
|
82
|
+
assert.equal(h.calls.stop, 1);
|
|
83
|
+
assert.match(HUB, /<button type="button" class="hopLink" id="hopLink" aria-expanded="false" aria-controls="hop">Play cloud hop while you wait<\/button>\s*<div class="hop" id="hop" hidden>/,
|
|
84
|
+
'baked shut, behind the mockup\'s link');
|
|
85
|
+
assert.match(SCRIPT, /\$\('hopLink'\)\.addEventListener\('click', function \(\) \{ hopOpen\(\$\('hop'\)\.hidden\); \}\);/, 'the link is the only way in');
|
|
86
|
+
assert.match(fn('hopStart'), /if \(!hop \|\| !hopEl \|\| hopEl\.hidden\) return;/, 'a closed game never starts, not on a resize nor a tab coming back');
|
|
93
87
|
});
|
|
94
88
|
|
|
95
|
-
test('
|
|
96
|
-
const {
|
|
97
|
-
|
|
98
|
-
sandbox
|
|
99
|
-
|
|
100
|
-
assert.equal(
|
|
101
|
-
|
|
102
|
-
assert.equal(
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
assert.
|
|
89
|
+
test('every required fact done: a running game gets the curtain, then puts itself away; a closed one stays away', () => {
|
|
90
|
+
const run = bootLink({ running: true, since: 1000 });
|
|
91
|
+
run.box.hidden = false;
|
|
92
|
+
run.sandbox.hopArrive();
|
|
93
|
+
assert.equal(run.calls.curtain, 1, 'the curtain plays from a running game');
|
|
94
|
+
assert.equal(run.box.hidden, false, 'and the game stays until the curtain lands');
|
|
95
|
+
run.calls.done();
|
|
96
|
+
assert.equal(run.box.hidden, true, 'then it is put away');
|
|
97
|
+
const shut = bootLink();
|
|
98
|
+
shut.sandbox.hopArrive();
|
|
99
|
+
assert.equal(shut.calls.curtain, 0, 'a game nobody opened plays nothing');
|
|
100
|
+
assert.equal(shut.box.hidden, true);
|
|
101
|
+
const still = bootLink({ reduced: true, running: true, since: 1000 });
|
|
102
|
+
still.box.hidden = false;
|
|
103
|
+
still.sandbox.hopArrive();
|
|
104
|
+
assert.equal(still.calls.curtain, 0, 'reduced motion: no curtain');
|
|
105
|
+
assert.equal(still.box.hidden, true);
|
|
106
|
+
assert.match(fn('paintAlive'), /if \(on\) hopArrive\(\);/, 'the facts decide the arrival, never the game');
|
|
106
107
|
});
|
|
107
108
|
|
|
108
|
-
test('
|
|
109
|
-
|
|
110
|
-
assert.
|
|
111
|
-
assert.
|
|
109
|
+
test('nothing on a timer stands in for a fact: no tick counter, no moment, no bar', () => {
|
|
110
|
+
assert.doesNotMatch(SCRIPT, /ticks > 400|ticks \+= 1/, 'the ~33-minute tick counter is gone');
|
|
111
|
+
assert.doesNotMatch(SCRIPT, /setMoment|momentFor|data-moment|hopMoment|hopSync/, 'no moment switch');
|
|
112
|
+
assert.doesNotMatch(HUB, /hopBar|id="hopStatus"/, 'no bar and no second status line');
|
|
112
113
|
assert.match(fn('resetForNewProject'), /hopReset\(\);/, 'a second project starts with a fresh hop');
|
|
114
|
+
assert.match(fn('renderDone'), /hopOpen\(false\);/, 'a fresh part 03 opens with the game put away');
|
|
113
115
|
});
|
|
114
116
|
|
|
115
117
|
// ── the physics — the owner's rules ─────────────────────────────────────────────
|
|
@@ -199,7 +201,7 @@ test('the loop never runs under reduced motion; the rest frame is a full still s
|
|
|
199
201
|
'the start, the frame (playing and curtain) and the curtain\'s kick-off — no other loop');
|
|
200
202
|
assert.match(fn('hopFrame'), /if \(!hopRunning\(\)\) \{ hopDraw\(now\); return; \}/,
|
|
201
203
|
'every frame re-asks the gate, so turning reduced motion on stops it');
|
|
202
|
-
assert.match(fn('
|
|
204
|
+
assert.match(fn('hopArrive'), /!REDUCED && hop\.raf/, 'the curtain plays only from a running game');
|
|
203
205
|
assert.match(STYLE, /\.hop\.isStill \.hopRow\{display:none;\}/, 'reduced motion has nothing to steer');
|
|
204
206
|
});
|
|
205
207
|
|
|
@@ -213,67 +215,17 @@ test('the game reads its colours off the page tokens — no literal but white or
|
|
|
213
215
|
assert.match(fn('hopColors'), /hopRead\('var\(--' \+ nm \+ '\)'\)/, 'every colour resolves from a token');
|
|
214
216
|
});
|
|
215
217
|
|
|
216
|
-
// ── the
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
const attrs = {};
|
|
220
|
-
return {
|
|
221
|
-
classList: { contains: (c) => set.has(c), toggle: (c, on) => (on ? set.add(c) : set.delete(c)), add: (c) => set.add(c), remove: (c) => set.delete(c) },
|
|
222
|
-
setAttribute: (k, v) => { attrs[k] = v; }, getAttribute: (k) => attrs[k], attrs, hidden: false, textContent: '', style: {},
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
test('the status line and the bar read what the strip says', () => {
|
|
227
|
-
const st = { textContent: '' }, fill = { style: {} };
|
|
228
|
-
const sandbox = { $: (id) => ({ hopStatus: st, hopBarFill: fill })[id] || null, Math };
|
|
229
|
-
vm.createContext(sandbox);
|
|
230
|
-
vm.runInContext(fn('hopSync'), sandbox);
|
|
231
|
-
const strip = {
|
|
232
|
-
querySelector: (q) => (q === '.pnote' ? { textContent: 'Raising the server, database, and address — nothing needed from you.' } : q === '.stages li.now' ? {} : null),
|
|
233
|
-
querySelectorAll: (q) => ({ length: q === '.stages li' ? 3 : 1 }),
|
|
234
|
-
};
|
|
235
|
-
sandbox.hopSync(strip);
|
|
236
|
-
assert.equal(st.textContent, 'Raising the server, database, and address — nothing needed from you.');
|
|
237
|
-
assert.equal(fill.style.width, '50%', 'one done and one under way of three');
|
|
238
|
-
assert.match(fn('setProgress'), /hopSync\(el\)/, 'every strip write syncs the line');
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// ── part 03's checklist (task 1004648): every step on show, the state line from it ──
|
|
242
|
-
function bootSteps(items, { step = 2, inFlight = false } = {}) {
|
|
243
|
-
const list = { children: items };
|
|
244
|
-
const ghState = { textContent: '' };
|
|
245
|
-
const sandbox = { $: (id) => ({ humanSteps: list, ghState })[id] || null, Array, state: { step }, manifestInFlight: () => inFlight };
|
|
246
|
-
vm.createContext(sandbox);
|
|
247
|
-
vm.runInContext(fn('stepsPaint'), sandbox);
|
|
248
|
-
sandbox.stepsPaint();
|
|
249
|
-
return ghState.textContent;
|
|
250
|
-
}
|
|
251
|
-
function li(key, cls = []) { const e = el(cls); e.key = key; return e; }
|
|
252
|
-
|
|
253
|
-
test('part 03\'s state line reads the checklist: ready, how many done, waiting on GitHub, connected', () => {
|
|
254
|
-
assert.equal(bootSteps([li('github-repo'), li('oauth-app')], { step: 1 }), 'opens once your project exists', 'before Create');
|
|
255
|
-
assert.equal(bootSteps([li('github-repo'), li('oauth-app')]), 'ready');
|
|
256
|
-
assert.equal(bootSteps([li('github-repo', ['sdone']), li('oauth-app'), li('enter-hall', ['slocked'])]), '1 of 3 done');
|
|
257
|
-
assert.equal(bootSteps([li('github-repo', ['sdone']), li('oauth-app')], { inFlight: true }), 'waiting on GitHub…', 'a GitHub leg in flight says so');
|
|
258
|
-
assert.equal(bootSteps([li('github-repo', ['sdone']), li('oauth-app', ['sdone'])]), 'connected ✓');
|
|
259
|
-
assert.doesNotMatch(fn('stepsPaint'), /innerHTML|\.cur|data-n/, 'no paging, and no markup from strings');
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
// ── the markup and the sheet ────────────────────────────────────────────────────
|
|
263
|
-
test('the wait stands the strip and the later lines down, the checklist stays on show, and nothing is paged', () => {
|
|
264
|
-
for (const id of ['hop', 'hopStatus', 'hopBarFill', 'hopCanvas', 'hopLeft', 'hopJump', 'hopRight', 'hopScore', 'humanSteps', 'ghState']) {
|
|
218
|
+
// ── the markup ──────────────────────────────────────────────────────────────────
|
|
219
|
+
test('the hop is baked into part 03 below the facts and above "is alive"; the old wait is gone', () => {
|
|
220
|
+
for (const id of ['hop', 'hopLink', 'hopCanvas', 'hopLeft', 'hopJump', 'hopRight', 'hopScore']) {
|
|
265
221
|
assert.ok(HUB.includes(`id="${id}"`), `#${id} is baked into part 03`);
|
|
266
222
|
}
|
|
267
|
-
for (const id of ['hopWhile', 'hopSignin', 'stepPager', 'stepCount', 'stepBack', 'stepNext']) {
|
|
268
|
-
assert.ok(!HUB.includes(`id="${id}"`), `#${id} is gone
|
|
223
|
+
for (const id of ['hopWhile', 'hopSignin', 'stepPager', 'stepCount', 'stepBack', 'stepNext', 'hopStatus', 'hopBarFill', 'humanSteps', 'liveProgress']) {
|
|
224
|
+
assert.ok(!HUB.includes(`id="${id}"`), `#${id} is gone (tasks 1004648, 1004683)`);
|
|
269
225
|
}
|
|
270
226
|
const panel = HUB.slice(HUB.indexOf('id="panel-done"'), HUB.indexOf('</section>', HUB.indexOf('id="panel-done"')));
|
|
271
|
-
assert.ok(panel.indexOf('id="
|
|
272
|
-
'the
|
|
273
|
-
assert.
|
|
274
|
-
assert.match(STYLE,
|
|
275
|
-
assert.doesNotMatch(STYLE, /#panel-done\[data-moment="wait"\][^{]*#humanSteps\{display:none;\}/, 'the wait no longer hides the checklist');
|
|
276
|
-
assert.doesNotMatch(STYLE, /humanSteps>li:not\(\.cur\)/, 'and the arrival does not page it');
|
|
277
|
-
assert.match(STYLE, /#panel-done\[data-moment="arrive"\] #liveProgress \.stages\{display:none;\}/, 'the arrival keeps the note with the address, not the dots');
|
|
278
|
-
assert.match(STYLE, /#panel-done \.humanSteps>li\.sdone::before\{content:"✓";/, 'a verified step wears the tick');
|
|
227
|
+
assert.ok(panel.indexOf('id="ghFacts"') < panel.indexOf('id="hopLink"') && panel.indexOf('id="hop"') < panel.indexOf('id="doneAlive"'),
|
|
228
|
+
'the facts first, then the game behind its link, then "is alive"');
|
|
229
|
+
assert.doesNotMatch(panel, /id="hopScore"[^>]*aria-live/, 'the score is not a live region: part 03 speaks through one');
|
|
230
|
+
assert.match(STYLE, /\.hop\[hidden\]\{display:none;\}/, 'the game stays out of the layout until asked');
|
|
279
231
|
});
|