@bongos/core 1.21.3 → 1.21.5
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 +128 -68
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +2 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0362-the-governor-docket-is-collected-from-contributed-sources.md +34 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +35 -1
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +389 -518
- package/docs/copy-registry.json +880 -2143
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +17 -25
- package/docs/page-readings.json +1008 -1246
- package/migrations/core_272_rename_cn_criterion_slugs.sql +40 -0
- package/modules/autonomy/governor-docket.js +101 -0
- package/modules/autonomy/routes/autonomy.js +5 -0
- package/modules/discord/board-broadcast.js +3 -1
- package/modules/discord/craft-broadcast.js +107 -0
- package/modules/discord/routes/discord.js +10 -0
- package/modules/discord/ship-broadcast.js +30 -2
- package/modules/economy/reward.js +3 -0
- package/modules/government/board.js +20 -5
- package/modules/government/docket-vocab.js +82 -0
- package/modules/government/docket.js +458 -0
- package/modules/government/routes/government.js +34 -0
- package/modules/hall-ui/public/genesis-home.js +9 -10
- package/modules/ideas/governor-docket.js +93 -0
- package/modules/ideas/routes/blockers.js +5 -0
- package/modules/lifecycle/criterion-ref.js +11 -0
- package/modules/lifecycle/db-tasks.js +7 -0
- package/modules/lifecycle/governor-docket.js +192 -0
- package/modules/lifecycle/routes/done-when.js +6 -0
- package/modules/lifecycle/routes/gate-approvals.js +78 -33
- package/modules/lifecycle/routes/lifecycle.js +3 -0
- package/modules/onboarding/routes/access-requests.js +68 -27
- package/modules/public-landing/public/assets/cosmos.css +6 -50
- package/modules/public-landing/public/projects.html +1170 -1659
- package/modules/public-landing/public/projects.probes.json +18 -18
- package/modules/public-landing/public/projects.states.json +23 -31
- package/modules/security/routes/reports.js +39 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/src/bongos/module-rechecks.js +77 -0
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/routes/core-update.js +41 -0
- package/src/bongos/routes/modules.js +17 -0
- package/src/module-api.js +1 -1
- package/tests/criterion_goal_attach.mjs +12 -4
- package/tests/criterion_ref_parse.mjs +41 -0
- package/tests/discord_craft_broadcast.mjs +162 -0
- package/tests/genesis_home.mjs +1 -1
- package/tests/government_routes.mjs +3 -1
- package/tests/governor_docket.mjs +552 -0
- package/tests/onboard.mjs +10 -4
- package/tests/platform_boot.mjs +1 -1
- package/tests/projects_hub.mjs +196 -89
- package/tests/projects_hub_app_status.mjs +3 -1
- package/tests/projects_hub_app_step.mjs +72 -48
- package/tests/projects_hub_dns_ready.mjs +10 -2
- package/tests/projects_hub_look.mjs +32 -22
- package/tests/projects_hub_module_picker.mjs +112 -62
- package/tests/projects_hub_pre_uat.mjs +48 -41
- package/tests/public_landing_projects.mjs +12 -6
- package/tests/wizard_demo.mjs +37 -11
- package/tests/wizard_draft_resume.mjs +34 -22
- package/tests/wizard_front_door.mjs +173 -103
- package/tests/wizard_intent_resume.mjs +28 -25
- package/tests/wizard_physics.mjs +94 -76
- package/tests/wizard_physics_more.mjs +409 -277
- package/tests/wizard_six_screens.mjs +126 -0
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// NOT covered here, deliberately: the team shape. The first draft of this task claimed the
|
|
25
25
|
// OAuth hop dropped it; that was wrong — teamShape, projType and modules all survive a
|
|
26
26
|
// reload (verified live). What looked like a drop was the documented "clicking the chosen
|
|
27
|
-
// pill again UNSAYS it" toggle being re-clicked by hand.
|
|
27
|
+
// pill again UNSAYS it" toggle being re-clicked by hand (a toggle task 1004504 retired; slice 3).
|
|
28
28
|
//
|
|
29
29
|
// Run: node --test --test-reporter=tap tests/wizard_draft_resume.mjs
|
|
30
30
|
|
|
@@ -70,29 +70,33 @@ function runStep({ authKnown, draftStep }) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
test('pre-auth: the placeholder renders step 1 but the draft KEEPS the real step', () => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
assert.
|
|
73
|
+
// step 4 is the review since task 1004504 — where the code row's repo access is granted
|
|
74
|
+
const { rendered, persisted } = runStep({ authKnown: false, draftStep: 4 });
|
|
75
|
+
assert.deepEqual(rendered, [1], 'still renders step 1 (the fork, since task 1004504) before identity settles');
|
|
76
|
+
assert.equal(persisted.step, 4, 'the draft must not be overwritten with the placeholder');
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
test('pre-auth: the in-memory step is restored too, so onAuth() can re-enter to it', () => {
|
|
79
80
|
// onAuth() calls enter() again and reads state.step from MEMORY, not from storage — so
|
|
80
81
|
// restoring only the stored copy would fix nothing.
|
|
81
|
-
const
|
|
82
|
-
|
|
82
|
+
for (const step of [2, 3, 4, 5]) {
|
|
83
|
+
const { state } = runStep({ authKnown: false, draftStep: step });
|
|
84
|
+
assert.equal(state.step, step, `step ${step} comes back`);
|
|
85
|
+
}
|
|
83
86
|
});
|
|
84
87
|
|
|
85
88
|
test('post-auth: the real step is rendered', () => {
|
|
86
|
-
const { rendered, persisted } = runStep({ authKnown: true, draftStep:
|
|
87
|
-
assert.deepEqual(rendered, [
|
|
88
|
-
assert.equal(persisted.step,
|
|
89
|
+
const { rendered, persisted } = runStep({ authKnown: true, draftStep: 4 });
|
|
90
|
+
assert.deepEqual(rendered, [4]);
|
|
91
|
+
assert.equal(persisted.step, 4);
|
|
89
92
|
});
|
|
90
93
|
|
|
91
94
|
test('a draft already at step 1 is unchanged, and an out-of-range step is not resurrected', () => {
|
|
92
95
|
assert.equal(runStep({ authKnown: false, draftStep: 1 }).persisted.step, 1);
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
+
// 6 is the done panel since task 1004504 (12 before it, 11 before task 1004421's Look step, 9 before task
|
|
97
|
+
// 1004417's front door, 8 before task 1004351), reached by its own branch above this slice; 0 is the gate.
|
|
98
|
+
// Neither is a resumable wizard position, so the placeholder is allowed to stand.
|
|
99
|
+
for (const bad of [0, 6, 13, 99]) {
|
|
96
100
|
assert.equal(runStep({ authKnown: false, draftStep: bad }).persisted.step, 1, `step ${bad} is not resumed`);
|
|
97
101
|
}
|
|
98
102
|
});
|
|
@@ -153,9 +157,11 @@ test('while the connect offer is up, the way back is not', () => {
|
|
|
153
157
|
// ── slice 3 · why this task's original team-shape claim was wrong ─────────────────
|
|
154
158
|
// The bug was filed claiming a GitHub round-trip drops the team shape. It does not: a
|
|
155
159
|
// planted draft reads teamShape back intact after a reload (verified live on 1.19.580).
|
|
156
|
-
// The
|
|
157
|
-
// already-chosen shape
|
|
158
|
-
//
|
|
160
|
+
// The explanation then was this handler — the pill was an UNSAY toggle, so re-clicking the
|
|
161
|
+
// already-chosen shape cleared it. Since task 1004504 the pills live on the review's Team
|
|
162
|
+
// size row, where a team always has a shape ("solo" follows a team of one), so the pill is
|
|
163
|
+
// a plain pick: a second click keeps the answer, and nothing in the handler can drop it.
|
|
164
|
+
// Pinned here so the claim rests on the handler's actual behaviour rather than on prose.
|
|
159
165
|
// Anchored on addEventListener: the same selector also appears inside paintTeam(), which
|
|
160
166
|
// only writes aria-pressed. Matching that one instead yields a slice that will not parse.
|
|
161
167
|
const TEAM_RE = /root\.querySelectorAll\('#teamCards \.tpill'\)\.forEach\(function \(b\) \{\s*b\.addEventListener[\s\S]*?\n {4}\}\);/;
|
|
@@ -166,17 +172,20 @@ test('the team-pill slice is still findable', () => {
|
|
|
166
172
|
assert.match(teamSlice, /state\.teamShape/, 'the handler owns the team-shape answer');
|
|
167
173
|
});
|
|
168
174
|
|
|
169
|
-
test('the team pill
|
|
175
|
+
test('the team pill is a plain pick on the Team size row — a second click keeps the answer (task 1004504)', () => {
|
|
170
176
|
const state = { teamShape: null };
|
|
171
177
|
const handlers = [];
|
|
178
|
+
const unconfirmed = [];
|
|
179
|
+
let saves = 0, paints = 0;
|
|
172
180
|
const pill = (team) => ({
|
|
173
181
|
getAttribute: (a) => (a === 'data-team' ? team : null),
|
|
174
182
|
addEventListener: (_e, fn) => handlers.push({ team, fn }),
|
|
175
183
|
});
|
|
176
184
|
const sandbox = {
|
|
177
185
|
state,
|
|
178
|
-
root: { querySelectorAll: () => [pill('
|
|
179
|
-
paintTeam() {},
|
|
186
|
+
root: { querySelectorAll: () => [pill('small-team'), pill('community')] },
|
|
187
|
+
paintTeam() {}, saveDraft() { saves += 1; }, paintSetup() { paints += 1; },
|
|
188
|
+
unconfirm(keys) { unconfirmed.push(...keys); },
|
|
180
189
|
};
|
|
181
190
|
sandbox.globalThis = sandbox;
|
|
182
191
|
vm.createContext(sandbox);
|
|
@@ -186,9 +195,12 @@ test('the team pill UNSAYS on a second click — the answer is not dropped, it i
|
|
|
186
195
|
click('small-team');
|
|
187
196
|
assert.equal(state.teamShape, 'small-team', 'first click answers');
|
|
188
197
|
click('small-team');
|
|
189
|
-
assert.equal(state.teamShape,
|
|
190
|
-
click('small-team');
|
|
191
|
-
assert.equal(state.teamShape, 'small-team', 'and a third click answers again');
|
|
198
|
+
assert.equal(state.teamShape, 'small-team', 'the SAME pill again keeps it — a team always has a shape');
|
|
192
199
|
click('community');
|
|
193
|
-
assert.equal(state.teamShape, 'community', 'a DIFFERENT pill replaces
|
|
200
|
+
assert.equal(state.teamShape, 'community', 'a DIFFERENT pill replaces it');
|
|
201
|
+
assert.equal(saves, 3, 'every pick is saved into the draft, so the GitHub round-trip keeps it');
|
|
202
|
+
assert.deepEqual([...new Set(unconfirmed)], ['extras'], 'the suggested extras follow the team, so the review asks about them again');
|
|
203
|
+
assert.equal(paints, 3, 'and the review repaints');
|
|
204
|
+
assert.deepEqual([...HUB.matchAll(/data-team="([a-z-]+)"/g)].map((m) => m[1]), ['small-team', 'community'],
|
|
205
|
+
'the row offers who can join a team; one person is the "One person" pill, not a team shape');
|
|
194
206
|
});
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
// tests/wizard_front_door.mjs — the create wizard's front door (task 1004417, BV2.PS05;
|
|
2
2
|
// goal 1000121, docs/design/project-startup-direction.md "Front door").
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// Since the six blended screens (task 1004504, the design of record
|
|
5
|
+
// docs/design/mocks/combined-onboarding/) the front door is ONE screen:
|
|
6
|
+
// 1 · fork — "You’re about to start a digital planet.": the terms as its one tick, recorded
|
|
7
|
+
// on the project against the terms version the SERVER stamps
|
|
8
|
+
// (tests/provisioning_terms_ack.mjs holds that half), and the world's door pill, two
|
|
9
|
+
// equal doors, try the demo / start my project, both waiting for the tick. The demo is
|
|
10
|
+
// its own route (BV2.PS09). The separate terms screen is gone.
|
|
11
|
+
// The wizard carries no progress rail: an eyebrow names the screen, one Back walks back, and
|
|
12
|
+
// the night room (#wizRoom) stands behind every screen in place of the planet and bongo pair.
|
|
10
13
|
// Sign-in stays where it was: the gate (step 0) comes first.
|
|
11
14
|
//
|
|
12
15
|
// Most of this RUNS the page's own script, the vm-slice idiom of tests/wizard_draft_resume.mjs:
|
|
13
|
-
// validateStep(), the front door's wiring, and show()'s
|
|
16
|
+
// validateStep(), the front door's wiring, and show()'s screen bookkeeping are sliced out of the
|
|
14
17
|
// inline script and executed under a minimal DOM stub. Every slice boundary is asserted, so a
|
|
15
18
|
// rename fails loudly instead of leaving this file quietly testing nothing.
|
|
16
19
|
//
|
|
@@ -32,74 +35,73 @@ const SCRIPT = HUB.slice(HUB.lastIndexOf('<script>'), HUB.lastIndexOf('</script>
|
|
|
32
35
|
const STYLE = HUB.slice(HUB.indexOf('<style>'), HUB.indexOf('</style>'));
|
|
33
36
|
|
|
34
37
|
const panel = (id) => (HUB.match(new RegExp(`<section class="panel[^"]*" id="${id}"[\\s\\S]*?\\n {10}</section>`)) || [''])[0];
|
|
35
|
-
const TERMS = panel('panel-terms');
|
|
36
38
|
const FORK = panel('panel-fork');
|
|
39
|
+
const VIEW_NEW = (HUB.match(/<section id="view-new" hidden>[\s\S]*?\n<\/section>/) || [''])[0];
|
|
37
40
|
const MAP = {};
|
|
38
41
|
for (const m of ((SCRIPT.match(/var panels = \{([^}]*)\}/) || [, ''])[1]).matchAll(/(\d+):\s*'([a-z-]+)'/g)) MAP[m[2]] = Number(m[1]);
|
|
39
42
|
|
|
40
43
|
// ── the order ───────────────────────────────────────────────────────────────────
|
|
41
|
-
test('the front door stands between sign-in and the
|
|
44
|
+
test('the front door stands between sign-in and the first question: gate 0, fork 1, what 2', () => {
|
|
42
45
|
assert.equal(MAP['panel-gate'], 0, 'sign-in stays where it is today, first');
|
|
43
|
-
assert.equal(MAP['panel-
|
|
44
|
-
assert.equal(MAP['panel-
|
|
45
|
-
|
|
46
|
-
assert.equal(MAP['panel-
|
|
47
|
-
assert.
|
|
48
|
-
assert.ok(HUB.indexOf('id="panel-gate"') < HUB.indexOf('id="panel-
|
|
49
|
-
&& HUB.indexOf('id="panel-
|
|
50
|
-
&& HUB.indexOf('id="panel-
|
|
51
|
-
|
|
52
|
-
assert.
|
|
53
|
-
assert.
|
|
54
|
-
assert.match(
|
|
46
|
+
assert.equal(MAP['panel-fork'], 1, 'the fork, with the terms as its tick, is the first screen after sign-in');
|
|
47
|
+
assert.equal(MAP['panel-what'], 2, 'the real-project door goes on to "What are you making?"');
|
|
48
|
+
assert.equal(MAP['panel-checks'], 3, 'then the quick checks (task 1004504)');
|
|
49
|
+
assert.equal(MAP['panel-terms'], undefined, 'the separate terms screen is gone — the tick lives on the fork');
|
|
50
|
+
assert.doesNotMatch(HUB, /id="panel-terms"/, 'and no markup is left for it');
|
|
51
|
+
assert.ok(HUB.indexOf('id="panel-gate"') < HUB.indexOf('id="panel-fork"')
|
|
52
|
+
&& HUB.indexOf('id="panel-fork"') < HUB.indexOf('id="panel-what"')
|
|
53
|
+
&& HUB.indexOf('id="panel-what"') < HUB.indexOf('id="panel-checks"'), 'the markup reads in the same order');
|
|
54
|
+
assert.match(FORK, /<form data-next="2">/, 'the fork\'s primary door goes on to the first question');
|
|
55
|
+
assert.ok(VIEW_NEW, 'the ?view=new section is findable');
|
|
56
|
+
assert.doesNotMatch(VIEW_NEW, /data-back=/, 'no per-panel Back buttons — one #wizBack walks the screens');
|
|
57
|
+
assert.equal((VIEW_NEW.match(/id="wizBack"/g) || []).length, 1, 'one Back, in the wizard\'s top row');
|
|
55
58
|
});
|
|
56
59
|
|
|
57
|
-
test('the rail is
|
|
60
|
+
test('the numbered rail is gone (task 1004504) — no steps list, no painter, no rule hiding it', () => {
|
|
58
61
|
const beats = [...HUB.matchAll(/<li[^>]*data-step="(\d+)"/g)].map((m) => Number(m[1]));
|
|
59
|
-
assert.deepEqual(beats, [
|
|
60
|
-
assert.
|
|
61
|
-
assert.
|
|
62
|
+
assert.deepEqual(beats, [], 'no rail beat is left anywhere on the page');
|
|
63
|
+
assert.doesNotMatch(HUB, /class="steps"/, 'no <ol class="steps"> in the wizard');
|
|
64
|
+
assert.doesNotMatch(SCRIPT, /paintRail\(/, 'no rail painter, and no call to one');
|
|
65
|
+
assert.doesNotMatch(STYLE, /\.wizFront \.steps|#view-new \.steps/, 'no rule left styling or hiding a rail');
|
|
62
66
|
});
|
|
63
67
|
|
|
64
|
-
// ── screen 1 · the terms
|
|
65
|
-
test('screen 1 is the
|
|
66
|
-
assert.ok(
|
|
67
|
-
assert.match(
|
|
68
|
-
assert.match(
|
|
69
|
-
assert.equal((
|
|
70
|
-
assert.match(
|
|
71
|
-
assert.match(
|
|
72
|
-
'the whole row is its label, so the target is the sentence, not a 20px box');
|
|
73
|
-
assert.match(
|
|
74
|
-
|
|
75
|
-
assert.equal((TERMS.match(/type="submit"/g) || []).length, 1, 'one primary door');
|
|
76
|
-
assert.match(TERMS, /<span data-brand-product-inline>Cloud Bongos<\/span>/, 'the product name is the pack\'s, re-branded on a child');
|
|
68
|
+
// ── screen 1 · the fork, with the terms as its one tick ─────────────────────────
|
|
69
|
+
test('screen 1 is the fork: one short lede, one tick that links the terms, one submit door', () => {
|
|
70
|
+
assert.ok(FORK, '#panel-fork ships as real markup (C7)');
|
|
71
|
+
assert.match(FORK, /^<section class="panel on" id="panel-fork"/, 'it carries the static `on` — step 1 for a no-script visitor');
|
|
72
|
+
assert.match(FORK, /<h1 id="fork-h">You’re about to start a digital planet\.<\/h1>/);
|
|
73
|
+
assert.equal((FORK.match(/<div id="forkIntro">[\s\S]*?<\/form>/)?.[0].match(/<p class="lede">/g) || []).length, 1, 'one short lede paragraph');
|
|
74
|
+
assert.match(FORK, /<input type="checkbox" id="termsAck" aria-describedby="h-terms">/, 'one checkbox, named by its hint');
|
|
75
|
+
assert.match(FORK, /<label class="termsAck">\s*<input[^>]*>\s*<span>I can sign for this project, and I accept the <a class="termsRead" href="\/terms" target="_blank" rel="noopener">terms<\/a>\.<\/span>/,
|
|
76
|
+
'the whole row is its label, so the target is the sentence, not a 20px box — and the terms are the platform\'s own /terms, in a new tab so the draft is not left behind');
|
|
77
|
+
assert.equal((FORK.match(/type="submit"/g) || []).length, 1, 'one primary door');
|
|
78
|
+
assert.doesNotMatch(FORK, /\bBongos\b/, 'no product name baked where a child instance would ship it');
|
|
77
79
|
});
|
|
78
80
|
|
|
79
81
|
test('the page never names a terms version — the server stamps which text was acknowledged', () => {
|
|
80
82
|
// A version baked here would be a second copy of TERMS_VERSION that drifts, and worse, a
|
|
81
83
|
// value a caller could send. The create request carries only the tick.
|
|
82
84
|
assert.doesNotMatch(HUB, /terms_version|termsVersion|terms_ack_version/, 'no version travels from the page');
|
|
83
|
-
assert.doesNotMatch(
|
|
85
|
+
assert.doesNotMatch(FORK, /\b20\d\d-\d\d-\d\d\b/, 'and none is printed on the screen');
|
|
84
86
|
});
|
|
85
87
|
|
|
86
|
-
test('the resume offer
|
|
88
|
+
test('the resume offer lives on step 1 — the fork, once', () => {
|
|
87
89
|
assert.equal((HUB.match(/id="wizResume"/g) || []).length, 1);
|
|
88
|
-
assert.ok(
|
|
90
|
+
assert.ok(FORK.includes('id="wizResume"'), 'a deliberate "New project" lands here, so the offer does too');
|
|
89
91
|
});
|
|
90
92
|
|
|
91
93
|
// ── the validator, executed ────────────────────────────────────────────────────
|
|
92
94
|
const VALIDATE_RE = /function validateStep\(step\) \{[\s\S]*?\n {4}\}/;
|
|
93
95
|
const validateSlice = (SCRIPT.match(VALIDATE_RE) || [null])[0];
|
|
94
96
|
|
|
95
|
-
function runValidate(step, termsAck) {
|
|
97
|
+
function runValidate(step, termsAck, extra = {}) {
|
|
96
98
|
const els = {};
|
|
97
99
|
const $ = (id) => (els[id] = els[id] || { textContent: '', className: '', classList: { add() {} }, focus() {} });
|
|
98
|
-
const sandbox = { $, state: { termsAck, name: 'Mercury', projType: 'game' }, validateRender: () => true, repoAccess: true };
|
|
100
|
+
const sandbox = { $, state: { termsAck, name: 'Mercury', projType: 'game', demo: null, confirmed: {}, ...extra }, validateRender: () => true, repoAccess: true };
|
|
99
101
|
sandbox.globalThis = sandbox;
|
|
100
102
|
vm.createContext(sandbox);
|
|
101
103
|
vm.runInContext(`${validateSlice}\nvar ok = validateStep(${step});`, sandbox);
|
|
102
|
-
return { ok: sandbox.ok, hint: els['h-terms'] };
|
|
104
|
+
return { ok: sandbox.ok, hint: els['h-terms'], state: sandbox.state };
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
test('validateStep is still findable, with the terms clause inside it', () => {
|
|
@@ -107,35 +109,44 @@ test('validateStep is still findable, with the terms clause inside it', () => {
|
|
|
107
109
|
assert.match(validateSlice, /if \(step === 1\) \{\s*if \(!state\.termsAck\)/, 'the clause is part of the slice');
|
|
108
110
|
});
|
|
109
111
|
|
|
110
|
-
test('
|
|
112
|
+
test('Start my project is refused until the box is ticked, and the refusal says why in the hint', () => {
|
|
111
113
|
const refused = runValidate(1, false);
|
|
112
|
-
assert.equal(refused.ok, false, 'no tick, no fork');
|
|
114
|
+
assert.equal(refused.ok, false, 'no tick, no way past the fork');
|
|
113
115
|
assert.equal(refused.hint.className, 'hint err');
|
|
114
116
|
assert.match(refused.hint.textContent, /Tick the box to go on/);
|
|
115
117
|
const given = runValidate(1, true);
|
|
116
118
|
assert.equal(given.ok, true, 'a tick goes on');
|
|
117
119
|
});
|
|
118
120
|
|
|
119
|
-
test('
|
|
120
|
-
|
|
121
|
-
// task 1004420: "Begin planet creation" is a real project, so a demo the founder
|
|
121
|
+
test('Start my project clears a demo started earlier — the submit door is a real project', () => {
|
|
122
|
+
// task 1004420: "Start my project" is a real project, so a demo the founder
|
|
122
123
|
// set up and then walked back from must not ride the create
|
|
123
|
-
|
|
124
|
+
const r = runValidate(1, true, { demo: { people: 3, hours_each: 4 }, confirmed: { team: true } });
|
|
125
|
+
assert.equal(r.ok, true);
|
|
126
|
+
assert.equal(r.state.demo, null, 'the walked-back demo does not ride the create');
|
|
127
|
+
assert.deepEqual(JSON.parse(JSON.stringify(r.state.confirmed)), {}, 'and its rows are asked again');
|
|
128
|
+
assert.equal(runValidate(1, false, { demo: { people: 3 } }).state.demo !== null, true, 'an unticked box changes nothing');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('"What are you making?" asks a name and a kind (task 1004504)', () => {
|
|
132
|
+
assert.equal(runValidate(2, true).ok, true, 'a name and a kind go on');
|
|
133
|
+
assert.equal(runValidate(2, true, { name: ' ' }).ok, false, 'no name, no way on');
|
|
134
|
+
assert.equal(runValidate(2, true, { projType: null }).ok, false, 'no kind, no way on — Not sure is a fine answer');
|
|
124
135
|
});
|
|
125
136
|
|
|
126
137
|
// ── the wiring, executed ───────────────────────────────────────────────────────
|
|
127
|
-
const WIRE_START =
|
|
138
|
+
const WIRE_START = ' function paintDoors() {';
|
|
128
139
|
const WIRE_END = ' /* ── the demo route (task 1004420, BV2.PS09) ──';
|
|
129
|
-
const wireSlice = SCRIPT.includes(WIRE_START) ? SCRIPT.slice(SCRIPT.indexOf(WIRE_START), SCRIPT.indexOf(WIRE_END)) : null;
|
|
140
|
+
const wireSlice = SCRIPT.includes(WIRE_START) && SCRIPT.includes(WIRE_END) ? SCRIPT.slice(SCRIPT.indexOf(WIRE_START), SCRIPT.indexOf(WIRE_END)) : null;
|
|
130
141
|
|
|
131
142
|
function runWiring(initialAck) {
|
|
132
143
|
const handlers = {};
|
|
133
144
|
const mk = (id) => ({
|
|
134
|
-
id, checked: false, hidden: false, textContent: '', className: '', attrs: {},
|
|
145
|
+
id, checked: false, hidden: false, disabled: true, textContent: '', className: '', attrs: {},
|
|
135
146
|
addEventListener(type, fn) { handlers[id + ':' + type] = fn; },
|
|
136
147
|
setAttribute(k, v) { this.attrs[k] = v; },
|
|
137
148
|
});
|
|
138
|
-
const els = { termsAck: mk('termsAck'), 'h-terms': mk('h-terms') };
|
|
149
|
+
const els = { termsAck: mk('termsAck'), 'h-terms': mk('h-terms'), wizDemo: mk('wizDemo'), wizStart: mk('wizStart') };
|
|
139
150
|
let saves = 0;
|
|
140
151
|
const sandbox = { $: (id) => els[id], state: { termsAck: initialAck }, saveDraft: () => { saves += 1; } };
|
|
141
152
|
sandbox.globalThis = sandbox;
|
|
@@ -147,6 +158,25 @@ function runWiring(initialAck) {
|
|
|
147
158
|
test('the front door\'s wiring is still findable', () => {
|
|
148
159
|
assert.ok(wireSlice && wireSlice.length > 100, 'the wiring block was not found in projects.html');
|
|
149
160
|
assert.match(wireSlice, /\$\('termsAck'\)\.addEventListener\('change'/, 'the tick is part of the slice');
|
|
161
|
+
assert.match(wireSlice, /\$\('wizStart'\)\.disabled = !state\.termsAck;/, 'and so is the door painter');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('both doors wait for the tick: disabled in the markup, and painted from the answer', () => {
|
|
165
|
+
assert.match(FORK, /<button type="button" class="btn big ghost" id="wizDemo"[^>]*\bdisabled>/, 'the demo door ships disabled');
|
|
166
|
+
assert.match(FORK, /<button type="submit" class="btn big" id="wizStart" disabled>/, 'and so does Start my project');
|
|
167
|
+
const fresh = runWiring(false);
|
|
168
|
+
assert.equal(fresh.els.wizDemo.disabled, true, 'an unticked draft keeps the demo door shut');
|
|
169
|
+
assert.equal(fresh.els.wizStart.disabled, true, 'and the real-project door');
|
|
170
|
+
fresh.els.termsAck.checked = true; fresh.handlers['termsAck:change'].call(fresh.els.termsAck);
|
|
171
|
+
assert.equal(fresh.els.wizDemo.disabled, false, 'ticking opens both doors');
|
|
172
|
+
assert.equal(fresh.els.wizStart.disabled, false);
|
|
173
|
+
fresh.els.termsAck.checked = false; fresh.handlers['termsAck:change'].call(fresh.els.termsAck);
|
|
174
|
+
assert.ok(fresh.els.wizDemo.disabled && fresh.els.wizStart.disabled, 'unticking shuts them again');
|
|
175
|
+
const resumed = runWiring(true);
|
|
176
|
+
assert.ok(!resumed.els.wizDemo.disabled && !resumed.els.wizStart.disabled, 'a draft that ticked comes back with its doors open');
|
|
177
|
+
assert.match(STYLE, /#view-new \.doorPill \.btn\[disabled\]\{opacity:\.35;cursor:default;\}/, 'a shut door reads as shut');
|
|
178
|
+
assert.match(SCRIPT, /\$\('wizDemo'\)\.addEventListener\('click', function \(\) \{ if \(state\.termsAck\) demoShow\('setup'\); \}\);/,
|
|
179
|
+
'and the demo door refuses an unticked box even if the attribute is stripped');
|
|
150
180
|
});
|
|
151
181
|
|
|
152
182
|
test('the tick mirrors into the draft both ways, and a resumed draft paints its own answer', () => {
|
|
@@ -167,14 +197,14 @@ test('the demo door names what it opens: the demo route\'s setup screen (task 10
|
|
|
167
197
|
assert.doesNotMatch(FORK, /demoSoon|isn’t open yet/, 'the placeholder that said the demo was not open is gone');
|
|
168
198
|
});
|
|
169
199
|
|
|
170
|
-
// ──
|
|
171
|
-
test('
|
|
172
|
-
assert.match(FORK, /<h1 id="fork-h">You
|
|
200
|
+
// ── the fork's door pill ────────────────────────────────────────────────────────
|
|
201
|
+
test('the fork is the door pill: two equal doors in one object, the accent only on the one that goes on', () => {
|
|
202
|
+
assert.match(FORK, /<h1 id="fork-h">You’re about to start a digital planet\.<\/h1>/);
|
|
173
203
|
const pill = (FORK.match(/<div class="doorPill" role="group" aria-label="How to begin">[\s\S]*?<\/div>/) || [''])[0];
|
|
174
204
|
assert.ok(pill, 'one grouped pill');
|
|
175
205
|
const doors = [...pill.matchAll(/<button type="(button|submit)" class="([^"]+)"[^>]*>([^<]+)<\/button>/g)];
|
|
176
206
|
assert.equal(doors.length, 2, 'exactly two doors');
|
|
177
|
-
assert.deepEqual(doors.map((d) => d[3]), ['Try the demo', '
|
|
207
|
+
assert.deepEqual(doors.map((d) => d[3]), ['Try the demo', 'Start my project']);
|
|
178
208
|
assert.equal(doors[0][2], 'btn big ghost', 'the demo door is the secondary: transparent at rest');
|
|
179
209
|
assert.equal(doors[1][2], 'btn big', 'begin carries the accent');
|
|
180
210
|
assert.equal(doors[1][1], 'submit', 'and is the form\'s one submit, so Enter goes on');
|
|
@@ -183,58 +213,94 @@ test('screen 2 is the door pill: two equal doors in one object, the accent only
|
|
|
183
213
|
assert.match(STYLE, /@media \(max-width:520px\)\{\s*\.doorPill\{display:grid;grid-template-columns:1fr;/, 'at a phone width the two stack, still one object');
|
|
184
214
|
});
|
|
185
215
|
|
|
186
|
-
test('the
|
|
187
|
-
assert.match(
|
|
188
|
-
assert.match(
|
|
216
|
+
test('the night room stands behind every screen — the planet stage and the bongo pair are gone (task 1004504)', () => {
|
|
217
|
+
assert.match(VIEW_NEW, /<div class="wizRoom" id="wizRoom" data-screen="fork" aria-hidden="true">/, 'decorative, so hidden from a screen reader, and baked at the fork for a no-script visitor');
|
|
218
|
+
assert.match(VIEW_NEW, /<span class="rmSeed"><img src="\/assets\/world\/r2-chrome-blob-one-cut\.webp" alt=""><\/span>/, 'the seed is a real plate, root-absolute so it is stamped');
|
|
189
219
|
assert.ok(fs.existsSync(path.join(LANDING, 'assets', 'world', 'r2-chrome-blob-one-cut.webp')), 'and it exists on disk');
|
|
190
|
-
assert.
|
|
191
|
-
assert.
|
|
192
|
-
const at = STYLE.indexOf('
|
|
193
|
-
const
|
|
194
|
-
assert.ok(at > -1 &&
|
|
195
|
-
|
|
220
|
+
assert.doesNotMatch(HUB, /id="forkStage"|class="forkSeed"|class="forkRoutes"|bongoCell|id="physStage"/, 'no fork stage, seed orbit, route list, bongo pair or forming planet left in the markup');
|
|
221
|
+
assert.doesNotMatch(STYLE, /\.forkStage\{|\.forkOrbit\{|\.bongoCell|\.physStage/, 'and no rule left for them');
|
|
222
|
+
const at = STYLE.indexOf('#view-new .wizRoom{');
|
|
223
|
+
const end = STYLE.indexOf('/* under 900 the window', at);
|
|
224
|
+
assert.ok(at > -1 && end > at, 'the night room\'s CSS block is findable');
|
|
225
|
+
const block = STYLE.slice(at, end);
|
|
226
|
+
const hexes = [...block.matchAll(/#[0-9a-f]{3,8}\b/gi)].map((m) => m[0].toLowerCase());
|
|
227
|
+
assert.ok(hexes.length > 0 && hexes.every((h) => h === '#000' || h === '#fff'), `every colour a mix of the fifteen with black or white (${[...new Set(hexes)]})`);
|
|
228
|
+
for (const m of block.matchAll(/--rm-[a-z0-9-]+:([^;]+);/g)) {
|
|
229
|
+
assert.match(m[1], /^color-mix\(in srgb,var\(--(sea|accent)\)|^rgba\(255,255,255,/, `a room token is derived, never a new literal: ${m[0]}`);
|
|
230
|
+
}
|
|
231
|
+
const motion = block.indexOf('@media (prefers-reduced-motion:no-preference){');
|
|
232
|
+
assert.ok(motion > -1, 'the room\'s motion sits inside the kill switch');
|
|
233
|
+
assert.doesNotMatch(block.slice(0, motion), /animation:|transition:/, 'and none outside it: the rest frame is the whole composition');
|
|
234
|
+
assert.doesNotMatch(block, /:root/, 'room tokens live on the room, never a page :root');
|
|
196
235
|
});
|
|
197
236
|
|
|
198
237
|
test('the two routes are described honestly for today: the demo is open (task 1004420), creation promises no questions that do not exist yet', () => {
|
|
199
|
-
const
|
|
200
|
-
assert.match(
|
|
201
|
-
assert.doesNotMatch(
|
|
202
|
-
assert.
|
|
203
|
-
assert.doesNotMatch(
|
|
204
|
-
assert.doesNotMatch(FORK, /\bBongos\b(?! pair)/, 'no product name baked where a child instance would ship it');
|
|
238
|
+
const intro = (FORK.match(/<div id="forkIntro">[\s\S]*?<\/form>/) || [''])[0];
|
|
239
|
+
assert.match(intro, /<p class="lede">Try a small demo first, or start your real project\.<\/p>/);
|
|
240
|
+
assert.doesNotMatch(FORK, /coming soon/, 'the demo is built, so nothing says it is coming');
|
|
241
|
+
assert.doesNotMatch(FORK, /seven questions/i, 'the copy promises no question count');
|
|
242
|
+
assert.doesNotMatch(FORK, /\bBongos\b/, 'no product name baked where a child instance would ship it');
|
|
205
243
|
});
|
|
206
244
|
|
|
207
|
-
// ── show(): the
|
|
245
|
+
// ── show(): the room, the eyebrow and the one Back, executed ───────────────────
|
|
246
|
+
// The slice runs from the SCREEN map through show(), so eyebrowFor() and prevStep() ride
|
|
247
|
+
// along; both ends are asserted.
|
|
248
|
+
const SHOW_START = ' var SCREEN = {';
|
|
208
249
|
const SHOW_RE = /function show\(step, moveFocus\) \{[\s\S]*?\n {4}\}/;
|
|
209
|
-
const
|
|
250
|
+
const showMatch = SCRIPT.match(SHOW_RE);
|
|
251
|
+
const showSlice = showMatch && SCRIPT.includes(SHOW_START) && SCRIPT.indexOf(SHOW_START) < showMatch.index
|
|
252
|
+
? SCRIPT.slice(SCRIPT.indexOf(SHOW_START), showMatch.index + showMatch[0].length) : null;
|
|
210
253
|
|
|
211
|
-
function runShow(step) {
|
|
254
|
+
function runShow(step, { demo = null, demoOpen = false } = {}) {
|
|
212
255
|
const cls = (name) => { const set = new Set(); return { name, set, toggle(c, on) { if (on) set.add(c); else set.delete(c); } }; };
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
256
|
+
const els = {};
|
|
257
|
+
const $ = (id) => (els[id] = els[id] || {
|
|
258
|
+
id, classList: cls(id), hidden: id === 'demoSetup' ? !demoOpen : false, textContent: '', attrs: {},
|
|
259
|
+
setAttribute(k, v) { this.attrs[k] = v; }, querySelector: () => null,
|
|
260
|
+
});
|
|
216
261
|
const sandbox = {
|
|
217
|
-
$, state: {}, panels: Object.fromEntries(Object.entries(MAP).map(([id, n]) => [n, id])),
|
|
218
|
-
|
|
219
|
-
|
|
262
|
+
$, state: { demo }, panels: Object.fromEntries(Object.entries(MAP).map(([id, n]) => [n, id])),
|
|
263
|
+
saveDraft() {}, authKnown: false, repoAccess: null,
|
|
264
|
+
paintDoors() {}, physLoadAccountOnce() {}, paintChecks() {}, physDefaults() {},
|
|
220
265
|
renderModules() {}, syncHome() {}, ensureRepoAccess() {}, checkSlugAvailable() {}, renderAddrPanel() {},
|
|
221
|
-
syncAppStep() {}, syncRenderSetup() {},
|
|
266
|
+
syncAppStep() {}, syncRenderSetup() {}, paintSetup() {}, renderLook() {}, renderRecap() {},
|
|
222
267
|
};
|
|
223
268
|
sandbox.globalThis = sandbox;
|
|
224
269
|
vm.createContext(sandbox);
|
|
225
270
|
vm.runInContext(`${showSlice}\nshow(${step}, false);`, sandbox);
|
|
226
|
-
return {
|
|
271
|
+
return {
|
|
272
|
+
on: Object.keys(els).filter((id) => els[id].classList.set.has('on')),
|
|
273
|
+
screen: els.wizRoom.attrs['data-screen'], eyebrow: els.wizEyebrow.textContent,
|
|
274
|
+
back: !els.wizBack.hidden, tag: !els.demoTag.hidden,
|
|
275
|
+
};
|
|
227
276
|
}
|
|
228
277
|
|
|
229
|
-
test('show()
|
|
230
|
-
assert.ok(showSlice, 'show() was not found in projects.html');
|
|
231
|
-
|
|
232
|
-
assert.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
278
|
+
test('show() turns one screen on, tells the room which screen it is, and names it in the eyebrow', () => {
|
|
279
|
+
assert.ok(showSlice, 'the SCREEN map through show() was not found in projects.html');
|
|
280
|
+
assert.match(showSlice, /function eyebrowFor\(step\)/, 'the eyebrow is part of the slice');
|
|
281
|
+
assert.doesNotMatch(showSlice, /paintRail|wizFront|wizFork|wizDone/, 'show() paints no rail and toggles no front-door or done layout (task 1004504)');
|
|
282
|
+
const screens = { 0: 'signin', 1: 'fork', 2: 'what', 3: 'checks', 4: 'review', 5: 'look', 6: 'done' };
|
|
283
|
+
for (const [n, screen] of Object.entries(screens)) {
|
|
284
|
+
const r = runShow(Number(n));
|
|
285
|
+
const id = Object.keys(MAP).find((k) => MAP[k] === Number(n));
|
|
286
|
+
assert.deepEqual(r.on, [id], `step ${n}: only ${id} is on`);
|
|
287
|
+
assert.equal(r.screen, screen, `step ${n}: the room grows to "${screen}"`);
|
|
288
|
+
}
|
|
289
|
+
assert.equal(runShow(1).eyebrow, 'New project');
|
|
290
|
+
assert.equal(runShow(1, { demoOpen: true }).eyebrow, 'Demo', 'the demo\'s own screen says so');
|
|
291
|
+
assert.equal(runShow(2, { demo: { people: 3 } }).eyebrow, 'New demo', 'and a demo past the fork stays marked');
|
|
292
|
+
assert.equal(runShow(6).eyebrow, 'Founding');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('one Back: hidden at sign-in, on the fork and on done; shown on every screen between', () => {
|
|
296
|
+
assert.equal(runShow(0).back, false, 'sign-in has nowhere to go back to');
|
|
297
|
+
assert.equal(runShow(1).back, false, 'the fork is the first screen');
|
|
298
|
+
assert.equal(runShow(1, { demoOpen: true }).back, true, 'the demo screen walks back to the fork');
|
|
299
|
+
for (const n of [2, 3, 4, 5]) assert.equal(runShow(n).back, true, `step ${n} walks back`);
|
|
300
|
+
assert.equal(runShow(6).back, false, 'a created project is not walked back into the wizard');
|
|
301
|
+
assert.equal(runShow(2).tag, false, 'no demo tag on a real project');
|
|
302
|
+
assert.equal(runShow(2, { demo: { people: 3 } }).tag, true, 'the demo tag rides every screen of a demo');
|
|
303
|
+
assert.match(SCRIPT, /function prevStep\(step\) \{\s*if \(step === 4 && state\.demo\) return 2;/, 'a demo, which skipped the checks, walks back past them too');
|
|
238
304
|
});
|
|
239
305
|
|
|
240
306
|
// ── the create ──────────────────────────────────────────────────────────────────
|
|
@@ -243,29 +309,33 @@ test('the create carries the tick, and is never filed without it', () => {
|
|
|
243
309
|
assert.match(create, /terms_acknowledged: state\.termsAck \? true : undefined,/,
|
|
244
310
|
'a real boolean when ticked, absent otherwise — the route refuses anything but a boolean');
|
|
245
311
|
const guard = create.indexOf('if (!state.termsAck) { show(1, true); validateStep(1); return; }');
|
|
246
|
-
assert.ok(guard > -1, 'an unticked draft is sent back to the
|
|
312
|
+
assert.ok(guard > -1, 'an unticked draft is sent back to the fork\'s tick');
|
|
247
313
|
assert.ok(guard < create.indexOf('ensureFreshRepo()'), 'BEFORE anything is created — the repo included');
|
|
248
314
|
});
|
|
249
315
|
|
|
250
316
|
test('the tick is part of the draft and of the reset, and the draft key moved with the renumbering', () => {
|
|
251
|
-
assert.match(SCRIPT, /var state = \{ step: 0,[^}]*termsAck: false \};/, 'the initial state declares it');
|
|
252
|
-
assert.match(SCRIPT, /state = \{ step: 1,[^}]*termsAck: false \};/, 'and "start another project" clears it — each project records its own');
|
|
253
|
-
assert.match(SCRIPT,
|
|
317
|
+
assert.match(SCRIPT, /var state = \{ step: 0,[^}]*termsAck: false, confirmed: \{\} \};/, 'the initial state declares it');
|
|
318
|
+
assert.match(SCRIPT, /state = \{ step: 1,[^}]*termsAck: false, confirmed: \{\} \};/, 'and "start another project" clears it — each project records its own');
|
|
319
|
+
assert.match(SCRIPT, /\$\('termsAck'\)\.checked = false; paintDoors\(\);/, 'and the reset shuts the doors with it');
|
|
320
|
+
assert.match(SCRIPT, /var DRAFT_KEY = 'cb-create-draft-v9';/, 'a draft saved under the old numbering is ignored, not replayed onto the wrong screen');
|
|
254
321
|
});
|
|
255
322
|
|
|
256
323
|
// ── the look-before-you-ship kit walks it ─────────────────────────────────────
|
|
257
|
-
|
|
324
|
+
// Every walk past the fork opens the way a founder does: the tick, then a door — Start my
|
|
325
|
+
// project, or the demo door and the demo screen's Continue.
|
|
326
|
+
test('every kit walk past the fork ticks first, then takes a door', () => {
|
|
258
327
|
const states = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.states.json'), 'utf8')).states;
|
|
259
|
-
|
|
260
|
-
assert.
|
|
261
|
-
const
|
|
328
|
+
assert.ok(states['in-new'], 'the in-new state exists');
|
|
329
|
+
for (const sel of ['#panel-fork', '#termsAck']) assert.ok((states['in-new'].expect.visible || []).includes(sel), `arriving at ?view=new pictures the fork and its tick (${sel})`);
|
|
330
|
+
const REAL = ['#termsAck', '#wizStart'];
|
|
331
|
+
const DEMO = ['#termsAck', '#wizDemo', '#demoStart'];
|
|
262
332
|
const probes = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.probes.json'), 'utf8')).probes;
|
|
263
333
|
const walks = [...Object.entries(states).map(([k, v]) => [k, v.actions || []]), ...probes.map((p) => [p.name, p.steps])]
|
|
264
334
|
.filter(([, steps]) => steps.some((st) => st[1] === '#newName'));
|
|
265
335
|
assert.ok(walks.length >= 12, `every wizard walk is found (${walks.length})`);
|
|
266
336
|
for (const [name, steps] of walks) {
|
|
267
337
|
const clicks = steps.filter((st) => st[0] === 'click').map((st) => st[1]);
|
|
268
|
-
|
|
338
|
+
const door = clicks[1] === '#wizDemo' ? DEMO : REAL;
|
|
339
|
+
assert.deepEqual(clicks.slice(0, door.length), door, `${name}: the tick, then a door, come first`);
|
|
269
340
|
}
|
|
270
|
-
assert.ok(probes.some((p) => /the front door \(task 1004417\)/.test(p.name)), 'a probe drives the refusal, the demo door and the way on');
|
|
271
341
|
});
|