@bongos/core 1.21.90 → 1.21.92

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.
Files changed (46) hide show
  1. package/.bongos-core.json +67 -52
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +4 -0
  4. package/clients/bongos-client/index.cjs +4 -0
  5. package/clients/bongos-client/index.d.ts +4 -0
  6. package/clients/bongos-client/index.mjs +4 -0
  7. package/docs/api/openapi.json +36 -1
  8. package/docs/api-reference.md +7 -1
  9. package/docs/copy-inventory.md +364 -374
  10. package/docs/copy-registry.json +566 -648
  11. package/docs/module-api-changelog.md +4 -0
  12. package/docs/page-inventory.json +15 -18
  13. package/docs/page-readings.json +1500 -1191
  14. package/modules/hall-ui/public/craft-mode.js +60 -0
  15. package/modules/hall-ui/public/style.css +32 -0
  16. package/modules/hall-ui/records/craft-mode.md +1 -1
  17. package/modules/lifecycle/module.json +2 -1
  18. package/modules/lifecycle/moving.js +55 -0
  19. package/modules/lifecycle/routes/moving.js +30 -0
  20. package/modules/public-landing/public/projects-hall-live.states.json +6 -35
  21. package/modules/public-landing/public/projects.html +1019 -983
  22. package/modules/public-landing/public/projects.probes.json +15 -18
  23. package/modules/public-landing/public/projects.states.json +22 -25
  24. package/package-lock.json +2 -2
  25. package/package.json +1 -1
  26. package/release-notes.json +12 -0
  27. package/src/module-api.js +1 -1
  28. package/tests/craft_mode_tallies.mjs +149 -0
  29. package/tests/onboard.mjs +8 -8
  30. package/tests/project_invite_ui.mjs +6 -5
  31. package/tests/projects_hub.mjs +77 -98
  32. package/tests/projects_hub_app_status.mjs +3 -3
  33. package/tests/projects_hub_app_step.mjs +8 -8
  34. package/tests/projects_hub_cloud_hop.mjs +29 -53
  35. package/tests/projects_hub_look.mjs +29 -39
  36. package/tests/projects_hub_module_picker.mjs +32 -39
  37. package/tests/projects_hub_pre_uat.mjs +11 -14
  38. package/tests/wizard_demo.mjs +30 -25
  39. package/tests/wizard_draft_resume.mjs +24 -32
  40. package/tests/wizard_front_door.mjs +91 -243
  41. package/tests/wizard_intent_resume.mjs +28 -48
  42. package/tests/wizard_one_page.mjs +201 -0
  43. package/tests/wizard_physics.mjs +46 -41
  44. package/tests/wizard_physics_more.mjs +71 -106
  45. package/tests/wizard_ship_mode.mjs +1 -1
  46. package/tests/wizard_six_screens.mjs +0 -129
@@ -0,0 +1,201 @@
1
+ // tests/wizard_one_page.mjs — project creation as one page with three parts, in the night room
2
+ // or the day room by the viewer's clock (task 1004648, BONGOS-V2).
3
+ //
4
+ // The design of record is docs/design/mocks/day-night-rooms/index.html (the owner approved it as
5
+ // drawn, 2026-10-08), written up in docs/design/project-startup-direction.md "One-page flow". Sign
6
+ // in stays the gate in front; then ONE page: "Make something.", the demo as a small link, part 01
7
+ // your project, part 02 how it's run (a summary to skim — every row's Change opens today's real
8
+ // control in place, and no row needs confirming), the terms tick, Create with a one-line recap,
9
+ // then part 03 Connect GitHub once the project exists.
10
+ //
11
+ // This file pins the shape and the words statically, and EXECUTES the three small pieces of new
12
+ // logic (the clock, what Create waits for, how full the page is) by the vm-slice idiom. The rows'
13
+ // controls are executed by tests/wizard_physics_more.mjs; the walk end to end by the kit probes.
14
+ //
15
+ // Run: node --test tests/wizard_one_page.mjs
16
+
17
+ import assert from 'node:assert/strict';
18
+ import { test } from 'node:test';
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import vm from 'node:vm';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
+ const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
26
+ const STYLE = HUB.slice(HUB.indexOf('<style>'), HUB.indexOf('</style>'));
27
+ const SCRIPT = HUB.slice(HUB.lastIndexOf('<script>'), HUB.lastIndexOf('</script>'));
28
+ const VIEW = (HUB.match(/<section id="view-new" hidden>[\s\S]*?\n<\/section>/) || [''])[0];
29
+ const MOCK = fs.readFileSync(path.join(ROOT, 'docs', 'design', 'mocks', 'day-night-rooms', 'index.html'), 'utf8').replace(/\r\n/g, '\n');
30
+ const PAGE = (VIEW.match(/<section class="panel on" id="panel-page"[\s\S]*?\n {10}<\/section>/) || [''])[0];
31
+ const part = (id) => (PAGE.match(new RegExp(`<div class="part[^"]*" id="${id}"[\\s\\S]*?\\n {14}</div>\\n`)) || [''])[0];
32
+ const row = (key) => (PAGE.match(new RegExp(`<div class="setRow" data-row="${key}">[\\s\\S]*?\\n {18}</div>\\n`)) || [''])[0];
33
+ const slice = (from, to) => {
34
+ const a = SCRIPT.indexOf(from), b = SCRIPT.indexOf(to, a);
35
+ assert.ok(a > 0 && b > a, `slice boundaries found: ${from} … ${to}`);
36
+ return SCRIPT.slice(a, b);
37
+ };
38
+
39
+ test('the gate, then one page: the controller knows three places, and the draft key moved with them', () => {
40
+ assert.ok(VIEW && PAGE, 'the view and its page are found');
41
+ const panels = [...VIEW.matchAll(/<section class="panel[^"]*" id="(panel-[a-z]+)"/g)].map((m) => m[1]);
42
+ assert.deepEqual(panels, ['panel-gate', 'panel-page'], 'the sign-in gate and the page, nothing else');
43
+ assert.match(SCRIPT, /var panels = \{ 0: 'panel-gate', 1: 'panel-page', 2: 'panel-page' \};/);
44
+ assert.match(SCRIPT, /var SCREEN = \{ 0: 'signin', 1: 'page', 2: 'done' \};/);
45
+ assert.match(SCRIPT, /var DRAFT_KEY = 'cb-create-draft-v10';/, 'a renumbering bumps the draft key, so an older draft is ignored rather than replayed');
46
+ });
47
+
48
+ test('the old screens, their doors and the confirm-every-row machinery are gone, not hidden', () => {
49
+ for (const id of ['panel-fork', 'panel-what', 'panel-checks', 'panel-review', 'panel-look', 'forkIntro', 'wizStart', 'wizBack', 'reviewGo', 'h-review', 'physNext', 'stepPager', 'hopWhile', 'hopSignin', 'doneStepsHead', 'h-projtype']) {
50
+ assert.doesNotMatch(HUB, new RegExp(`id="${id}"`), `#${id} is gone from the markup`);
51
+ assert.doesNotMatch(SCRIPT, new RegExp(`\\$\\('${id}'\\)`), `and nothing reaches for #${id}`);
52
+ }
53
+ for (const fn of ['confirmRow', 'confirmAllRows', 'unconfirm', 'unconfirmedRows', 'reopenReview', 'confirmedRows', 'validateStep', 'checksAdvance', 'paintDoors', 'prevStep', 'stepGo']) {
54
+ assert.doesNotMatch(SCRIPT, new RegExp(`function ${fn}\\(`), `${fn}() is removed`);
55
+ }
56
+ assert.doesNotMatch(SCRIPT, /state\.confirmed/, 'no row remembers being confirmed');
57
+ assert.doesNotMatch(VIEW, /data-next=|<form/, 'no screen-to-screen forms are left');
58
+ assert.doesNotMatch(STYLE, /\.doorPill|\.wizBack|\.stepPager|hopSignin/, 'nor their styles');
59
+ });
60
+
61
+ test('the page opens with the mockup\'s own words, in the mockup\'s order', () => {
62
+ const words = [
63
+ 'Make something.',
64
+ 'Three parts, one page. Just looking?',
65
+ 'Try a small demo instead',
66
+ 'Your project',
67
+ 'A few quick checks. They decide which legal and payout options we can offer.',
68
+ 'filled in from your answers',
69
+ 'I can sign for this project, and I accept the',
70
+ 'Create my project',
71
+ 'Connect GitHub',
72
+ 'opens once your project exists',
73
+ 'One trip to GitHub. Approve the app, pick your repo, and you land back here. We tick each step as it happens, from any tab or device.',
74
+ ];
75
+ let at = 0;
76
+ for (const w of words) {
77
+ assert.ok(MOCK.includes(w.replace('How it’s run', 'How it\'s run')), `"${w}" is the mockup's own`);
78
+ const i = PAGE.indexOf(w, at);
79
+ assert.ok(i >= at, `"${w}" comes next on the page`);
80
+ at = i;
81
+ }
82
+ assert.match(PAGE, /<h2 id="p2-h"><span class="n">02<\/span>How it’s run <span class="state">filled in from your answers<\/span><\/h2>/);
83
+ assert.match(PAGE, /In a sentence <small>\(optional, it goes on the public map\)<\/small>/);
84
+ assert.match(VIEW, /<h1 id="gate-h">Make something\.<\/h1>[\s\S]*Continue with GitHub/, 'the gate in front is unchanged');
85
+ });
86
+
87
+ test('part 02 is the mockup\'s rows, each with one Change that opens today\'s own control in place', () => {
88
+ const rows = [...part('part-run').matchAll(/<div class="setRow" data-row="([a-z]+)">/g)].map((m) => m[1]);
89
+ assert.deepEqual(rows, ['code', 'address', 'team', 'extras', 'rewards', 'app', 'run', 'ship', 'look']);
90
+ const labels = rows.map((k) => (row(k).match(/<span class="k">([^<]+)<\/span>/) || [])[1]);
91
+ assert.deepEqual(labels, ['Code', 'Address', 'Team', 'Extras', 'Rewards', 'Hosting', 'We run', 'Shipping', 'Look']);
92
+ for (const k of rows) {
93
+ assert.match(row(k), /<span class="acts"><button type="button" class="rowChg" data-row-chg aria-expanded="false">Change<\/button><\/span>/, `${k}: one Change, nothing else`);
94
+ assert.match(row(k), /<div class="opts" hidden>/, `${k}: Change opens the row's own controls`);
95
+ }
96
+ assert.match(SCRIPT, /chg\.textContent = 'Done';/, 'an open row reads Done');
97
+ assert.match(SCRIPT, /chg\.textContent = 'Change';/, 'and Change again once closed');
98
+ assert.match(SCRIPT, /var ROWS = \['code', 'address', 'team', 'extras', 'rewards', 'app', 'run', 'ship', 'look'\];/);
99
+ });
100
+
101
+ test('nothing the live wizard does is lost: every capability in the direction table is reachable on the page', () => {
102
+ const has = (where, html, ids) => { for (const id of ids) assert.match(html, new RegExp(`id="${id}"`), `${where} hosts #${id}`); };
103
+ has('part 01', part('part-project'), ['newName', 'typeCards', 'f-desc', 'physFillOffer', 'physFromAccount', 'demoSetup', 'demoIdea', 'demoInvites', 'demoStart',
104
+ 'physCountry', 'physRegionField', 'physAges', 'physIntent', 'physHaveEntity', 'physForms', 'checksStop', 'physStopMsg']);
105
+ has('the Code row', row('code'), ['physExisting', 'c-green', 'c-adopt', 'adoptNote', 'ghNoAccess', 'ghAdopt', 'ghSearch', 'ghFresh', 'manualHome', 'h-home']);
106
+ has('the Address row', row('address'), ['f-slug', 'addrConfirm', 'addrCustom', 'f-domain', 'f-noaddr']);
107
+ has('the Team row', row('team'), ['physTeam', 'physStartN', 'physExpected', 'teamCards']);
108
+ has('the Extras row', row('extras'), ['modQ', 'modCards', 'modWhy', 'modCoreNote']);
109
+ assert.match(row('rewards'), /data-phys="reward_intents" data-val="non-monetary"[\s\S]*data-val="future-payouts"/, 'Rewards: pick several');
110
+ has('the Hosting row', row('app'), ['appOpts', 'renderLater', 'renderSetup', 'f-renderKey', 'renderWs', 'renderSvc', 'renderNew']);
111
+ has('the We run row', row('run'), ['physSuite']);
112
+ assert.match(row('run'), /data-val="hybrid" aria-pressed="false">Some of it<\/button>/, 'We run: "Some of it" reveals the service checklist');
113
+ has('the Shipping row', row('ship'), ['shipOpts']);
114
+ has('the Look row', row('look'), ['wizLook', 'lookDot']);
115
+ has('the page', PAGE, ['wizResume', 'termsAck', 'doCreate', 'wizRecap', 'createWhy', 'createAlert', 'humanSteps', 'liveProgress', 'wizInvite', 'done-h']);
116
+ const ids = [...VIEW.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]);
117
+ assert.deepEqual(ids.filter((id, i) => ids.indexOf(id) !== i), [], 'moved, never copied: every id in the view is unique');
118
+ });
119
+
120
+ test('Create waits for a name and the terms tick only, and says why in place (executed)', () => {
121
+ const src = slice('var createBusy = false;', '$(\'termsAck\').checked = !!state.termsAck;');
122
+ const els = {};
123
+ const el = (id) => (els[id] = els[id] || { id, disabled: false, textContent: '', attrs: {}, setAttribute(k, v) { this.attrs[k] = v; } });
124
+ const ctx = { state: { step: 1, name: '', termsAck: false, projType: null }, $: el, renderRecap() {}, phys: () => ({ answers: {} }) };
125
+ vm.createContext(ctx);
126
+ vm.runInContext(src + '; this.paintCreate = paintCreate; this.createWhy = createWhy;', ctx);
127
+ ctx.paintCreate();
128
+ assert.equal(el('doCreate').disabled, true);
129
+ assert.equal(el('createWhy').textContent, 'Give it a name first.', 'no name: says so');
130
+ ctx.state.name = ' Kestrel ';
131
+ ctx.paintCreate();
132
+ assert.equal(el('doCreate').disabled, true);
133
+ assert.equal(el('createWhy').textContent, 'Tick the terms to create your project.', 'a name, no tick: says so');
134
+ ctx.state.termsAck = true;
135
+ ctx.paintCreate();
136
+ assert.equal(el('doCreate').disabled, false, 'a name and the tick: Create is enabled, whatever the rows say');
137
+ assert.equal(el('createWhy').textContent, ' ');
138
+ assert.equal(el('wizRoom').attrs['data-fill'], '2', 'the seed grows as the page fills in');
139
+ ctx.state.step = 2;
140
+ ctx.paintCreate();
141
+ assert.equal(el('doCreate').textContent, 'Created');
142
+ assert.equal(el('doCreate').disabled, true, 'once created, Create stands down');
143
+ });
144
+
145
+ test('Create runs the screening stop before anything exists, and a row that cannot be filed opens with its reason', () => {
146
+ const create = slice('$(\'doCreate\').addEventListener(\'click\'', '/* ── done panel');
147
+ const order = ['createWhy()', 'firstInvalidRow()', 'physScreen()', 'ensureFreshRepo()', "fetch(API + '/provisioning/instances'"].map((s) => create.indexOf(s));
148
+ assert.ok(order.every((i, k) => i > 0 && (k === 0 || i > order[k - 1])), 'name and tick, then the rows, then the screen, then the repo, then the create');
149
+ assert.match(create, /if \(!ok\) \{ var stop = new Error\('stopped'\); stop\.stopped = true; throw stop; \}/, 'a stop files nothing');
150
+ assert.match(SCRIPT, /if \(res && res\.stop\) \{ \$\('physStopMsg'\)\.textContent = res\.message \|\| ''; checksShow\('stop'\); return false; \}/, 'the stop is said in place, in the server\'s own words');
151
+ });
152
+
153
+ test('the room follows the viewer\'s own clock: day 07:00 to 18:59, re-checked each minute (executed)', () => {
154
+ const src = slice('function skyAt(d)', 'function paintSky()');
155
+ const ctx = {};
156
+ vm.createContext(ctx);
157
+ vm.runInContext(src + '; this.skyAt = skyAt;', ctx);
158
+ const at = (h, m) => ctx.skyAt(new Date(2026, 9, 8, h, m));
159
+ assert.equal(at(6, 59), 'night');
160
+ assert.equal(at(7, 0), 'day');
161
+ assert.equal(at(12, 0), 'day');
162
+ assert.equal(at(18, 59), 'day');
163
+ assert.equal(at(19, 0), 'night');
164
+ assert.equal(at(0, 0), 'night');
165
+ assert.match(SCRIPT, /function paintSky\(\) \{ root\.setAttribute\('data-sky', skyAt\(new Date\(\)\)\); \}\n\s*paintSky\(\);\n\s*setInterval\(paintSky, 60000\);/, 'painted at once and every minute');
166
+ });
167
+
168
+ test('the day room is the mockup\'s, drawn from the fifteen, and its motion sits inside the kill switch', () => {
169
+ const day = (STYLE.match(/#view-new\[data-sky="day"\] \.wizRoom\{[\s\S]*?\n\}/) || [''])[0];
170
+ assert.ok(day, 'the day room re-points the room\'s own tokens');
171
+ for (const t of ['--rm-room', '--rm-night', '--rm-moon', '--rm-tower-1', '--rm-pane', '--rm-floor', '--rm-glow', '--rm-leaf-1']) assert.match(day, new RegExp(`${t}:`), `${t} has a daylight value`);
172
+ const words = (STYLE.match(/#view-new\[data-sky="day"\]>\*\{[\s\S]*?\n\}/) || [''])[0];
173
+ assert.ok(words, 'the words get a light ground\'s tokens, on the view\'s children (never :root)');
174
+ for (const block of [day, words]) assert.doesNotMatch(block, /#(?!fff\b|000\b)[0-9a-f]{3,8}\b/i, 'no colour literal but black or white');
175
+ assert.match(words, /--ok:color-mix\(in srgb,var\(--wz-ok\) 40%,#000\);/, 'a status ink is read off the page\'s own through --wz-*, never itself');
176
+ assert.match(STYLE, /#view-new\[data-sky="day"\] \.rmCloud\{opacity:\.9;\}/, 'clouds by day');
177
+ assert.match(STYLE, /#view-new\[data-sky="day"\] \.rmSun\{opacity:\.7;\}/, 'and a patch of sun on the floor');
178
+ const calm = (STYLE.match(/@media \(prefers-reduced-motion:no-preference\)\{\n {2}#view-new \.rmSeed[\s\S]*?\n\}/) || [''])[0];
179
+ assert.match(calm, /#view-new \.rmCloud\{animation:rmDrift 60s linear infinite;\}/, 'the drift only without reduced motion');
180
+ assert.match(calm, /transition:background \.8s ease/, 'and the change of light too');
181
+ assert.doesNotMatch(STYLE.replace(calm, ''), /rmDrift 60s|animation:rmTwinkle|animation:rmBreathe/, 'no room motion outside the no-preference block');
182
+ });
183
+
184
+ test('part 03 is the checklist, every step on show, with the state line from the server\'s own record', () => {
185
+ assert.match(part('panel-done') || PAGE, /id="panel-done"/);
186
+ assert.match(PAGE, /<div class="part waiting" id="panel-done" aria-labelledby="done-part-h">/, 'part 03 waits until the project exists');
187
+ assert.match(SCRIPT, /done\.classList\.toggle\('waiting', !created\);/);
188
+ const steps = slice('function stepsPaint()', '(function hopInput()');
189
+ assert.match(steps, /'connected ✓'/);
190
+ assert.match(steps, /'waiting on GitHub…'/);
191
+ assert.match(steps, /'ready'/);
192
+ assert.doesNotMatch(steps, /innerHTML/, 'no markup built from strings');
193
+ assert.doesNotMatch(STYLE, /humanSteps>li:not\(\.cur\)/, 'the checklist is no longer paged one step at a time');
194
+ });
195
+
196
+ test('the name is typed, so the handle under it is built from nodes, never markup', () => {
197
+ const hint = slice('function paintNameHint()', 'if (state.name && state.slug) paintNameHint();');
198
+ assert.match(hint, /code\.textContent = state\.slug;/);
199
+ assert.doesNotMatch(hint, /innerHTML/);
200
+ assert.doesNotMatch(SCRIPT, /\$\('h-name'\)\.innerHTML/);
201
+ });
@@ -104,7 +104,7 @@ const SCRIPT = HUB.slice(HUB.indexOf(START), HUB.indexOf(END, HUB.indexOf(START)
104
104
 
105
105
  test('the page carries the server\'s countries, regions and entity forms, exactly', () => {
106
106
  assert.ok(SCRIPT.length > 1000, 'the physics controller was found');
107
- assert.ok(SCRIPT.includes('function checksAdvance()'), 'and it carries the quick checks\' Continue');
107
+ assert.ok(SCRIPT.includes('function physScreen()'), 'and it carries the screen Create asks (task 1004648)');
108
108
  const box = {};
109
109
  vm.createContext(box);
110
110
  vm.runInContext(SCRIPT.slice(0, SCRIPT.indexOf(' var physLimits = [];')).replace(/\bvar /g, 'this.'), box);
@@ -121,11 +121,11 @@ test('the page carries the server\'s countries, regions and entity forms, exactl
121
121
  });
122
122
 
123
123
  test('every choice the questions offer is an answer the server accepts', () => {
124
- // the quick checks, and the rows of the review that host the rest (task 1004504)
125
- const panel = HUB.slice(HUB.indexOf('<section id="view-new"'), HUB.indexOf('<!-- 5 · a look, then Create'));
126
- const checks = HUB.slice(HUB.indexOf('id="panel-checks"'), HUB.indexOf('<!-- 4 · check what we set up'));
127
- assert.deepEqual([...new Set([...checks.matchAll(/data-phys="([a-z_]+)"/g)].map((m) => m[1]))], ['builder_geography', 'minors_on_board', 'entity'],
128
- 'the quick checks ask where builders will be, about under-18s and who is behind it');
124
+ // the quick checks in part 01, and the rows of part 02 that host the rest (task 1004648)
125
+ const panel = HUB.slice(HUB.indexOf('<section id="view-new"'), HUB.indexOf('id="termsAck"'));
126
+ const checks = HUB.slice(HUB.indexOf('id="checksMain"'), HUB.indexOf('id="checksStop"'));
127
+ assert.deepEqual([...new Set([...checks.matchAll(/data-phys="([a-z_]+)"/g)].map((m) => m[1]))], ['minors_on_board', 'builder_geography', 'entity'],
128
+ 'the quick checks ask about under-18s, where builders will be and who is behind it in the mockup’s order');
129
129
  assert.match(checks, /<select id="physCountry">/, 'and where you are');
130
130
  const seen = [...panel.matchAll(/data-phys="([a-z_]+)" data-val="([^"]+)"/g)];
131
131
  assert.ok(seen.length >= 20, 'the questions\' choices');
@@ -172,13 +172,13 @@ function boot({ checkAnswer = { stop: false, soft_limits: { limits: [] } } } = {
172
172
  const $ = (id) => (els[id] = els[id] || new El(id));
173
173
  const shown = [];
174
174
  const fetched = [];
175
- const state = { description: '', teamShape: null, physics: null, mode: 'greenfield', confirmed: {} };
175
+ const state = { description: '', teamShape: null, physics: null, mode: 'greenfield' };
176
176
  const sandbox = {
177
177
  $, state, API: '/api/bongos', document: { createElement: () => new El('opt') },
178
178
  root: { querySelectorAll: () => [], querySelector: () => null },
179
179
  saveDraft() {}, show(n) { shown.push(n); }, Intl,
180
- // the review's machinery lives outside this region (tests/wizard_physics_more.mjs runs it)
181
- paintSetup() {}, unconfirm() {}, setMode(m) { state.mode = m; }, authKnown: true,
180
+ // part 02's machinery lives outside this region (tests/wizard_physics_more.mjs runs it)
181
+ paintSetup() {}, paintCreate() {}, setMode(m) { state.mode = m; }, authKnown: true,
182
182
  fetch: async (url, opts) => { fetched.push({ url, body: opts && opts.body && JSON.parse(opts.body) }); return { ok: true, json: async () => (url.includes('physics-check') ? checkAnswer : []) }; },
183
183
  };
184
184
  vm.createContext(sandbox);
@@ -187,56 +187,61 @@ function boot({ checkAnswer = { stop: false, soft_limits: { limits: [] } } } = {
187
187
  }
188
188
  const tick = () => new Promise((r) => setTimeout(r, 0));
189
189
 
190
- test('Continue on the quick checks asks for each answer, then the server, and only then moves on', async () => {
190
+ test('Create asks the server\'s screen with the answers given so far, prefills included, and its soft limits shape part 02 (task 1004648)', async () => {
191
191
  const w = boot({ checkAnswer: { stop: false, soft_limits: { limits: [{ option: 'equity', effect: 'note', reason: 'r' }] } } });
192
+ w.run('phys(); physDefaults();');
192
193
  const a = () => w.state.physics.answers;
193
- assert.equal(w.run('checksAdvance()'), false, 'the screen never says yes itself');
194
- assert.match(w.$('h-phys').textContent, /Pick your country/);
195
- a().home_country = 'US'; a().home_region = 'US-CA';
196
- w.run('checksAdvance()');
197
- assert.match(w.$('h-phys').textContent, /where builders will be/);
198
- a().builder_geography = 'us-eu';
199
- w.run('checksAdvance()');
200
- assert.match(w.$('h-phys').textContent, /under 18/);
201
- a().minors_on_board = true;
202
- w.run('checksAdvance()');
203
- assert.match(w.$('h-phys').textContent, /who’s behind it/);
204
- assert.equal(w.fetched.length, 0, 'nothing is asked of the server until every answer is given');
205
- a().entity = 'for-profit';
194
+ a().home_country = 'US'; a().home_region = 'US-CA'; a().builder_geography = 'us-eu'; a().minors_on_board = true; a().entity = 'for-profit';
206
195
  w.state.description = 'A co-op farming game.';
207
- w.run('checksAdvance()');
208
- w.run('checksAdvance()');
209
- await tick(); await tick();
210
- assert.equal(w.fetched.length, 1, 'a second Continue while the first is out asks nothing more');
196
+ let ok = null;
197
+ w.sandbox.done = (v) => { ok = v; };
198
+ w.run('physScreen().then(done)');
199
+ await tick(); await tick(); await tick();
200
+ assert.equal(ok, true, 'no stop: Create goes on');
201
+ assert.equal(w.fetched.length, 1);
211
202
  assert.equal(w.fetched[0].url, '/api/bongos/provisioning/physics-check');
212
- // the review's prefills are written on the way: a new project, one person, all managed
203
+ // part 02's prefills ride along: a new project, one person, all managed
213
204
  assert.deepEqual(w.fetched[0].body, { description: 'A co-op farming game.', detail: {
214
205
  project_state: 'new', home_country: 'US', home_region: 'US-CA', builder_geography: 'us-eu', minors_on_board: true, entity: 'for-profit',
215
206
  builders_at_start: 1, builders_expected: 1, framing: 'managed', voice: 'professional' } });
216
207
  assert.equal(w.state.teamShape, 'solo', 'one person is a solo team shape');
217
- assert.equal(w.$('h-phys').textContent, ' ', 'the ask clears');
218
- assert.deepEqual(w.shown, [4], 'the screen passed it: on to the review');
219
- assert.equal(JSON.stringify(w.run('physLimits')), JSON.stringify([{ option: 'equity', effect: 'note', reason: 'r' }]), 'and its soft limits shape the review');
208
+ assert.equal(JSON.stringify(w.run('physLimits')), JSON.stringify([{ option: 'equity', effect: 'note', reason: 'r' }]), 'and its soft limits shape the rows');
209
+ assert.deepEqual(w.shown, [], 'the screen moves nothing on itself: Create does');
210
+ const unanswered = boot();
211
+ unanswered.sandbox.done = (v) => { ok = v; };
212
+ ok = null;
213
+ unanswered.run('physScreen().then(done)');
214
+ await tick(); await tick(); await tick();
215
+ assert.equal(ok, true, 'an unanswered check is not a stop — the create runs the same screen and is the authority');
216
+ const demo = boot();
217
+ demo.state.demo = { people: 3 };
218
+ demo.sandbox.done = (v) => { ok = v; };
219
+ ok = null;
220
+ demo.run('physScreen().then(done)');
221
+ await tick();
222
+ assert.equal(ok, true);
223
+ assert.equal(demo.fetched.length, 0, 'a demo\'s answers are its own: nothing to screen');
220
224
  });
221
225
 
222
- test('a stopped description lands on the stop screen with the server\'s words, and Start over clears it all', async () => {
226
+ test('a stopped answer lands on the stop in place of the checks, with the server\'s words, and the answers survive the way back', async () => {
223
227
  const w = boot({ checkAnswer: { stop: true, message: STOP_MESSAGE, soft_limits: { limits: [] } } });
224
228
  w.run('phys()');
225
229
  Object.assign(w.state.physics.answers, { home_country: 'NL', builder_geography: 'global', minors_on_board: false, entity: 'intent-solo' });
226
230
  w.state.description = 'ransomware';
227
- w.run('checksAdvance()');
228
- await tick(); await tick();
231
+ let ok = null;
232
+ w.sandbox.done = (v) => { ok = v; };
233
+ w.run('physScreen().then(done)');
234
+ await tick(); await tick(); await tick();
235
+ assert.equal(ok, false, 'Create files nothing');
229
236
  assert.equal(w.$('physStopMsg').textContent, STOP_MESSAGE);
230
- assert.equal(w.$('checksStop').hidden, false, 'the stop screen shows');
237
+ assert.equal(w.$('checksStop').hidden, false, 'the stop shows');
231
238
  assert.equal(w.$('checksMain').hidden, true, 'in place of the questions');
232
239
  assert.deepEqual(w.shown, [], 'and nothing moves on: nothing exists to undo');
233
- w.state.confirmed = { code: true };
234
240
  w.$('physStartOver').listeners.click();
235
- assert.equal(w.state.description, '');
236
- assert.equal(JSON.stringify(w.state.physics.answers), '{}', 'every answer goes');
237
- assert.equal(JSON.stringify(w.state.confirmed), '{}', 'and every confirmed row');
238
- assert.deepEqual(w.shown, [2], 'back to "What are you making?"');
241
+ assert.equal(w.$('checksStop').hidden, true, 'Change my answers takes the stop down');
239
242
  assert.equal(w.$('checksMain').hidden, false, 'with the questions back in place');
243
+ assert.equal(w.state.description, 'ransomware', 'and every answer kept, to change the one that stopped it');
244
+ assert.equal(w.state.physics.answers.home_country, 'NL');
240
245
  });
241
246
 
242
247
  test('the create sends the answers as detail: existing work only for an existing planet, the team shape beside them', () => {
@@ -263,7 +268,7 @@ test('the forming planet is gone: the night room stands behind the wizard instea
263
268
  assert.ok(!HUB.includes(gone), `${gone} is gone`);
264
269
  }
265
270
  assert.doesNotMatch(HUB, /id="(physPlanet|physGuard|physStage|panel-physics|physSummary)"/, 'and so is its markup');
266
- assert.match(HUB, /<div class="wizRoom" id="wizRoom" data-screen="fork" aria-hidden="true">/, 'the room is decoration only');
271
+ assert.match(HUB, /<div class="wizRoom" id="wizRoom" data-screen="page" data-fill="0" aria-hidden="true">/, 'the room is decoration only');
267
272
  const seed = HUB.match(/<span class="rmSeed"><img src="\/assets\/world\/([a-z0-9-]+)\.webp" alt=""><\/span>/);
268
273
  assert.ok(seed, 'the room\'s seed is a world plate');
269
274
  assert.ok(fs.existsSync(path.join(LANDING, 'assets', 'world', `${seed[1]}.webp`)), `${seed[1]} exists`);