@bongos/core 1.21.4 → 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 +118 -63
- 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 +2 -0
- package/docs/page-inventory.json +17 -25
- package/docs/page-readings.json +1008 -1246
- 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/governor-docket.js +192 -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 +14 -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/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
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
// tests/wizard_physics_more.mjs — planet physics, questions 5 to 7, the
|
|
2
|
-
//
|
|
1
|
+
// tests/wizard_physics_more.mjs — planet physics, questions 5 to 7, the one plain voice and
|
|
2
|
+
// the prefilled setup (task 1004419 / BV2.PS11, goal 1000121; the planet voice and the
|
|
3
|
+
// question rail retired by task 1004504, PS16; the questions regrouped by the same task).
|
|
3
4
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// the
|
|
5
|
+
// Since task 1004504 the founder answers the "few quick checks" (country, builders'
|
|
6
|
+
// geography, under-18s, who is behind it) on screen 3, and questions 5 to 7 are rows of the
|
|
7
|
+
// review on screen 4 ("Check what we set up."): Team size, Rewards and We run, each
|
|
8
|
+
// PREFILLED (one person, all managed) and confirmed or changed in place. The summary, the
|
|
9
|
+
// forming planet and the pre-fill notes are gone; confirming a row is the answer.
|
|
10
|
+
//
|
|
11
|
+
// Done-when: a new owner walks from the quick checks to a review whose every row is
|
|
12
|
+
// confirmed, with the setup prefilled. So the walk below is RUN end to end — the page's own
|
|
13
|
+
// physics controller, its team pills, its app choice, the review's row machinery and
|
|
14
|
+
// validateStep, sliced out of projects.html into a vm under a DOM stub built from the
|
|
15
|
+
// screens' own markup (the tests/wizard_physics.mjs idiom), its pre-create check answered by
|
|
16
|
+
// the REAL soft-limit table (modules/provisioning/soft-limits.js), never a hand-written
|
|
17
|
+
// verdict. The rest pins each rule on its own: rewards hidden when solo, the framing's suite
|
|
18
|
+
// and its reasons, the one voice every founder reads (spec D5), what the create sends, and
|
|
19
|
+
// which rows are fixed.
|
|
12
20
|
|
|
13
21
|
import { strict as assert } from 'node:assert';
|
|
14
22
|
import { test } from 'node:test';
|
|
@@ -25,115 +33,176 @@ const HUB = fs.readFileSync(path.join(LANDING, 'projects.html'), 'utf8').replace
|
|
|
25
33
|
const physics = require('../modules/provisioning/planet-physics.js');
|
|
26
34
|
const { SOFT_LIMIT_RULES, softLimitsFor } = require('../modules/provisioning/soft-limits.js');
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
// Every region is cut at code that exists, and every edge is asserted, so a rename fails
|
|
37
|
+
// loudly instead of testing nothing.
|
|
38
|
+
function slice(start, end, what) {
|
|
39
|
+
const a = HUB.indexOf(start), b = HUB.indexOf(end, a);
|
|
40
|
+
assert.ok(a > -1, `${what}: the start is on the page`);
|
|
41
|
+
assert.ok(b > a, `${what}: the end follows it`);
|
|
42
|
+
return HUB.slice(a, b);
|
|
43
|
+
}
|
|
44
|
+
const SCRIPT = slice('/* ── planet physics (tasks 1004418 and 1004419, BV2.PS07 + PS11; regrouped by task 1004504) ──',
|
|
45
|
+
' /* ── checks the old steps made on Next, now made by their rows ── */', 'the physics controller');
|
|
46
|
+
const TEAM = slice(' function teamLabel(t) {', " $('f-desc').value = state.description || '';", 'the team pills');
|
|
47
|
+
const APP = slice(' function appPlatformChoice() {', ' var ROWS = [', 'the app choice');
|
|
48
|
+
const REVIEW = slice(' var ROWS = [', ' function renderRecap() {', 'the review rows');
|
|
49
|
+
const VALIDATE = slice(' /* per-screen validation on Continue */', " root.querySelectorAll('.panel form')", 'validateStep');
|
|
50
|
+
for (const [name, region] of [['checksAdvance', SCRIPT], ['paintChecks', SCRIPT], ['physDefaults', SCRIPT], ['confirmRow', REVIEW],
|
|
51
|
+
['rowValid', REVIEW], ['rowFixed', REVIEW], ['paintSetup', REVIEW], ['validateStep', VALIDATE], ['paintTeam', TEAM]]) {
|
|
52
|
+
assert.ok(region.includes(`function ${name}(`), `the slice carries ${name}()`);
|
|
53
|
+
}
|
|
54
|
+
// the screens the controller paints: the quick checks and the review
|
|
55
|
+
const PANEL = slice('<section class="panel" id="panel-checks"', '<!-- 5 · a look, then Create', 'the checks and review markup');
|
|
31
56
|
const plain = (v) => JSON.parse(JSON.stringify(v)); // values born in the vm
|
|
32
57
|
|
|
33
|
-
// ── a small DOM: the
|
|
58
|
+
// ── a small DOM: the screens' own elements, as a tree, matched by selector ────
|
|
34
59
|
class El {
|
|
35
60
|
constructor(tag, attrs = {}) {
|
|
36
|
-
this.tagName = tag; this.attrs = { ...attrs }; this.hidden = 'hidden' in attrs; this.disabled =
|
|
61
|
+
this.tagName = tag; this.attrs = { ...attrs }; this.hidden = 'hidden' in attrs; this.disabled = 'disabled' in attrs;
|
|
37
62
|
this.value = attrs.value || ''; this.textContent = ''; this.title = ''; this.className = attrs.class || '';
|
|
38
|
-
this.listeners = {}; this.children = []; this.options = [{}];
|
|
63
|
+
this.listeners = {}; this.children = []; this.options = [{}]; this.parent = null; this.checked = 'checked' in attrs;
|
|
39
64
|
const self = this;
|
|
40
65
|
this.classes = new Set(this.className.split(/\s+/).filter(Boolean));
|
|
41
|
-
this.classList = { toggle(c, on) { if (on) self.classes.add(c); else self.classes.delete(c); }, contains: (c) => self.classes.has(c),
|
|
66
|
+
this.classList = { toggle(c, on) { if (on) self.classes.add(c); else self.classes.delete(c); }, contains: (c) => self.classes.has(c),
|
|
67
|
+
add: (c) => self.classes.add(c), remove: (c) => self.classes.delete(c) };
|
|
42
68
|
}
|
|
43
69
|
setAttribute(k, v) { this.attrs[k] = String(v); }
|
|
44
70
|
getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; }
|
|
71
|
+
hasAttribute(k) { return k in this.attrs; }
|
|
45
72
|
removeAttribute(k) { delete this.attrs[k]; }
|
|
46
73
|
addEventListener(t, fn) { this.listeners[t] = fn; }
|
|
47
|
-
|
|
48
|
-
|
|
74
|
+
// a click runs the element's own listener, then bubbles to the delegated ones above it
|
|
75
|
+
click() { for (let n = this; n; n = n.parent) if (n.listeners.click) n.listeners.click({ target: this }); }
|
|
76
|
+
appendChild(c) { this.children.push(c); c.parent = this; if (this.tagName === 'select') this.options.push(c); return c; }
|
|
49
77
|
removeChild(c) { const i = this.children.indexOf(c); if (i !== -1) this.children.splice(i, 1); if (this.tagName === 'select') this.options.pop(); }
|
|
50
78
|
get firstChild() { return this.children[0] || null; }
|
|
51
79
|
get lastChild() { return this.tagName === 'select' ? this.options[this.options.length - 1] : this.children[this.children.length - 1]; }
|
|
52
80
|
focus() {}
|
|
53
|
-
|
|
81
|
+
descendants() { return this.children.flatMap((c) => [c, ...c.descendants()]); }
|
|
82
|
+
querySelectorAll(sel) { return this.descendants().filter((e) => matches(e, sel)); }
|
|
83
|
+
querySelector(sel) { return this.querySelectorAll(sel)[0] || null; }
|
|
84
|
+
closest(sel) { for (let n = this; n && n.tagName !== '#root'; n = n.parent) if (matches(n, sel)) return n; return null; }
|
|
54
85
|
}
|
|
55
|
-
|
|
56
86
|
function parseAttrs(s) {
|
|
57
87
|
const out = {};
|
|
58
88
|
for (const m of s.matchAll(/([a-zA-Z-]+)(?:="([^"]*)")?/g)) out[m[1]] = m[2] === undefined ? '' : m[2];
|
|
59
89
|
return out;
|
|
60
90
|
}
|
|
91
|
+
function compound(c) {
|
|
92
|
+
const bare = c.replace(/\[[^\]]*\]/g, '');
|
|
93
|
+
return {
|
|
94
|
+
tag: (bare.match(/^[a-z][a-z0-9]*/i) || [null])[0],
|
|
95
|
+
id: (bare.match(/#([\w-]+)/) || [])[1],
|
|
96
|
+
classes: [...bare.matchAll(/\.([\w-]+)/g)].map((m) => m[1]),
|
|
97
|
+
attrs: [...c.matchAll(/\[([\w-]+)(?:="?([^"\]]*)"?)?\]/g)].map((m) => [m[1], m[2]]),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function fits(el, c) {
|
|
101
|
+
if (c === '#view-new') return true; // every element here lives inside the wizard's view
|
|
102
|
+
const p = compound(c);
|
|
103
|
+
if (p.tag && el.tagName !== p.tag) return false;
|
|
104
|
+
if (p.id && el.attrs.id !== p.id) return false;
|
|
105
|
+
if (p.classes.some((k) => !el.classes.has(k))) return false;
|
|
106
|
+
return p.attrs.every(([k, v]) => k in el.attrs && (v === undefined || el.attrs[k] === v));
|
|
107
|
+
}
|
|
108
|
+
function matches(el, sel) {
|
|
109
|
+
return sel.split(',').some((group) => {
|
|
110
|
+
const parts = group.trim().split(/\s+/);
|
|
111
|
+
if (!fits(el, parts[parts.length - 1])) return false;
|
|
112
|
+
let n = el.parent;
|
|
113
|
+
for (let i = parts.length - 2; i >= 0; i -= 1) {
|
|
114
|
+
if (parts[i] === '#view-new') continue;
|
|
115
|
+
while (n && !fits(n, parts[i])) n = n.parent;
|
|
116
|
+
if (!n) return false;
|
|
117
|
+
n = n.parent;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const VOID = new Set(['input', 'img', 'br', 'hr', 'path', 'source']);
|
|
61
123
|
function buildDom() {
|
|
62
|
-
const
|
|
124
|
+
const top = new El('#root');
|
|
63
125
|
const byId = {};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
126
|
+
let cur = top;
|
|
127
|
+
const raw = new Map();
|
|
128
|
+
const src = PANEL.replace(/<!--[\s\S]*?-->/g, '');
|
|
129
|
+
for (const m of src.matchAll(/<(\/?)([a-zA-Z][a-zA-Z0-9]*)\b([^>]*?)(\/?)>|([^<]+)/g)) {
|
|
130
|
+
if (m[5] !== undefined) { raw.set(cur, (raw.get(cur) || '') + m[5]); continue; }
|
|
131
|
+
const tag = m[2].toLowerCase();
|
|
132
|
+
if (m[1]) {
|
|
133
|
+
for (let n = cur; n !== top; n = n.parent) {
|
|
134
|
+
if (n.tagName === tag) {
|
|
135
|
+
// a leaf's words are its text, as the browser would read them
|
|
136
|
+
if (!n.children.length) n.textContent = (raw.get(n) || '').replace(/ /g, ' ').replace(/&/g, '&').replace(/\s+/g, ' ').trim();
|
|
137
|
+
cur = n.parent; break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const el = new El(tag, parseAttrs(m[3]));
|
|
143
|
+
el.parent = cur; cur.children.push(el);
|
|
67
144
|
if (el.attrs.id) byId[el.attrs.id] = el;
|
|
145
|
+
if (!VOID.has(tag) && !m[4]) cur = el;
|
|
68
146
|
}
|
|
69
|
-
|
|
70
|
-
const kids = (id) => {
|
|
71
|
-
const at = PANEL.indexOf(`id="${id}"`);
|
|
72
|
-
const chunk = PANEL.slice(at, PANEL.indexOf('</span>', at));
|
|
73
|
-
const n = (chunk.match(/<img /g) || []).length;
|
|
74
|
-
return all.filter((e) => e.tagName === 'img' && e.attrs.src && e.attrs.src.includes('bead')).slice(id === 'physGhosts' ? 0 : 4, id === 'physGhosts' ? n : 4 + n);
|
|
75
|
-
};
|
|
76
|
-
const lists = { '#physMoons img': kids('physMoons'), '#physGhosts img': kids('physGhosts') };
|
|
77
|
-
lists['#physMoons img, #physGhosts img'] = [...lists['#physMoons img'], ...lists['#physGhosts img']];
|
|
78
|
-
const matches = (el, compound) => {
|
|
79
|
-
if (compound.startsWith('.')) return el.classes.has(compound.slice(1));
|
|
80
|
-
for (const m of compound.matchAll(/\[([a-z-]+)(?:="([^"]*)")?\]/g)) {
|
|
81
|
-
if (!(m[1] in el.attrs)) return false;
|
|
82
|
-
if (m[2] !== undefined && el.attrs[m[1]] !== m[2]) return false;
|
|
83
|
-
}
|
|
84
|
-
return /\[/.test(compound);
|
|
85
|
-
};
|
|
86
|
-
const root = {
|
|
87
|
-
querySelectorAll(sel) {
|
|
88
|
-
if (lists[sel]) return lists[sel];
|
|
89
|
-
if (sel === '#physRail li') return all.filter((e) => e.tagName === 'li' && 'data-q' in e.attrs);
|
|
90
|
-
if (sel === '#physStage .physBelt') return all.filter((e) => e.classes.has('physBelt'));
|
|
91
|
-
if (sel === '#panel-physics .physQ') return all.filter((e) => e.classes.has('physQ'));
|
|
92
|
-
const last = sel.trim().split(/\s+/).pop();
|
|
93
|
-
return all.filter((e) => matches(e, last));
|
|
94
|
-
},
|
|
95
|
-
querySelector(sel) { return this.querySelectorAll(sel)[0] || null; },
|
|
96
|
-
};
|
|
97
|
-
return { all, byId, root };
|
|
147
|
+
return { top, byId, root: top };
|
|
98
148
|
}
|
|
99
149
|
|
|
100
|
-
// the pre-create check, answered by the real table
|
|
101
|
-
function boot({ check = (body) => ({ stop: false, soft_limits: softLimitsFor(body.detail) }), draft = {} } = {}) {
|
|
150
|
+
// the pre-create check, answered by the real table; /provisioning/instances by `instances`
|
|
151
|
+
function boot({ check = (body) => ({ stop: false, soft_limits: softLimitsFor(body.detail) }), draft = {}, instances = [], renderOk = true } = {}) {
|
|
102
152
|
const dom = buildDom();
|
|
103
153
|
const els = { ...dom.byId };
|
|
104
154
|
const $ = (id) => (els[id] = els[id] || new El('div', { id }));
|
|
105
155
|
const shown = [];
|
|
106
156
|
const fetched = [];
|
|
107
|
-
const state = Object.assign({ description: '', teamShape: null, physics: null, projType:
|
|
157
|
+
const state = Object.assign({ step: 4, name: 'Kestrel', description: '', teamShape: null, physics: null, projType: 'game', mode: 'greenfield',
|
|
158
|
+
appPlatform: 'later', demo: null, modules: null, confirmed: {}, termsAck: true }, draft);
|
|
108
159
|
const sandbox = {
|
|
109
160
|
$, state, API: '/api/bongos', root: dom.root, Intl,
|
|
110
161
|
document: { createElement: (t) => new El(t) },
|
|
111
|
-
saveDraft() {}, show(n) { shown.push(n); },
|
|
162
|
+
saveDraft() {}, show(n) { shown.push(n); state.step = n; },
|
|
163
|
+
setMode(m) { state.mode = m; }, authKnown: true,
|
|
164
|
+
// the code, address and Render checks are their own files' to run (projects_hub_app_step,
|
|
165
|
+
// public_landing_projects); here they answer as told
|
|
166
|
+
validateCode: () => true, validateAddress: () => true, validateRender: () => renderOk,
|
|
167
|
+
checkSlugAvailable() {}, renderAddrPanel() {}, renderModules() {}, syncHome() {}, syncRenderSetup() {}, renderLater: () => false,
|
|
168
|
+
slugAvailState: () => 'free', looksLikeDomain: () => false, resolvedDomain: () => 'kestrel.example.com',
|
|
169
|
+
// the extras row's words are tests/projects_hub_module_picker.mjs's
|
|
170
|
+
starterBundles: { optional: [{ key: 'discord', label: 'Discord' }] }, pickedModules: () => ['discord'], extrasRow: () => '', typeLabel: () => 'Game',
|
|
112
171
|
fetch: async (url, opts) => {
|
|
113
172
|
const body = opts && opts.body ? JSON.parse(opts.body) : null;
|
|
114
173
|
fetched.push({ url, body });
|
|
115
|
-
return { ok: true, json: async () => (url.includes('physics-check') ? check(body) :
|
|
174
|
+
return { ok: true, json: async () => (url.includes('physics-check') ? check(body) : instances) };
|
|
116
175
|
},
|
|
117
176
|
};
|
|
118
177
|
vm.createContext(sandbox);
|
|
119
|
-
vm.runInContext(SCRIPT, sandbox, { filename: 'projects.html#physics' });
|
|
178
|
+
vm.runInContext([TEAM, SCRIPT, APP, REVIEW, VALIDATE].join('\n'), sandbox, { filename: 'projects.html#physics' });
|
|
120
179
|
const q = (sel) => dom.root.querySelectorAll(sel);
|
|
180
|
+
const row = (key) => q(`#reviewRows .setRow[data-row="${key}"]`)[0];
|
|
121
181
|
return {
|
|
122
|
-
state, $, shown, fetched, q, dom,
|
|
182
|
+
state, $, shown, fetched, q, dom, row,
|
|
123
183
|
run: (code) => vm.runInContext(code, sandbox),
|
|
124
184
|
a: () => state.physics.answers,
|
|
125
185
|
pick: (key, val) => q(`[data-phys="${key}"][data-val="${val}"]`)[0].click(),
|
|
126
|
-
|
|
186
|
+
rowSays: (key) => [row(key).querySelector('[data-row-v]').textContent, row(key).querySelector('[data-row-s]').textContent],
|
|
187
|
+
ok: (key) => row(key).querySelector('[data-row-ok]').click(),
|
|
188
|
+
change: (key) => row(key).querySelector('[data-row-chg]').click(),
|
|
189
|
+
use: (key) => row(key).querySelector('[data-row-use]').click(),
|
|
190
|
+
isOpen: (key) => !row(key).querySelector('.opts').hidden,
|
|
191
|
+
go: () => $('reviewGo'),
|
|
192
|
+
settle: async () => { for (let i = 0; i < 4; i += 1) await tick(); },
|
|
127
193
|
};
|
|
128
194
|
}
|
|
129
195
|
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
130
|
-
//
|
|
131
|
-
const
|
|
132
|
-
|
|
196
|
+
// the quick checks, answered (the Kestrel sample of the canvas)
|
|
197
|
+
const CHECKS = { home_country: 'US', home_region: 'US-CA', builder_geography: 'us-eu', minors_on_board: false, entity: 'intent-solo' };
|
|
198
|
+
// on the review, with the checks answered, the prefills written and the table asked
|
|
199
|
+
async function onReview(answers = {}, opts = {}) {
|
|
133
200
|
const w = boot(opts);
|
|
134
201
|
w.run('phys()');
|
|
135
|
-
Object.assign(w.state.physics.answers, answers);
|
|
136
|
-
w.run(
|
|
202
|
+
Object.assign(w.state.physics.answers, CHECKS, answers);
|
|
203
|
+
w.run('physDefaults()');
|
|
204
|
+
const res = await w.run('physCheck()');
|
|
205
|
+
w.run(`physLimits = ${JSON.stringify(res.soft_limits.limits)}; paintSetup();`);
|
|
137
206
|
return w;
|
|
138
207
|
}
|
|
139
208
|
|
|
@@ -141,24 +210,23 @@ function at(q, answers = FOUR, opts = {}) {
|
|
|
141
210
|
test('the page carries the server\'s rewards, framings and services, in order, and says every one', () => {
|
|
142
211
|
const box = {};
|
|
143
212
|
vm.createContext(box);
|
|
144
|
-
vm.runInContext(SCRIPT.slice(0, SCRIPT.indexOf(' var
|
|
213
|
+
vm.runInContext(SCRIPT.slice(0, SCRIPT.indexOf(' var physLimits = [];')).replace(/\bvar /g, 'this.'), box);
|
|
145
214
|
const Q = physics.PLANET_PHYSICS_QUESTIONS;
|
|
146
215
|
assert.deepEqual(plain(box.PHYS_REWARDS), [...Q.reward_intents.values]);
|
|
147
216
|
assert.deepEqual(plain(box.PHYS_FRAMINGS), [...Q.framing.values]);
|
|
148
217
|
assert.deepEqual(plain(box.PHYS_SERVICES), [...Q.services.values]);
|
|
149
|
-
assert.deepEqual(Object.keys(box.PHYS_ORBIT), [...Q.framing.values]);
|
|
150
218
|
for (const key of ['project_state', 'builder_geography', 'entity', 'reward_intents', 'framing', 'services']) {
|
|
151
|
-
assert.deepEqual(Object.keys(box.PHYS_SAY[key]).sort(), [...Q[key].values].sort(), `the
|
|
219
|
+
assert.deepEqual(Object.keys(box.PHYS_SAY[key]).sort(), [...Q[key].values].sort(), `the review can say every ${key}`);
|
|
152
220
|
}
|
|
153
|
-
|
|
221
|
+
assert.deepEqual(plain(box.PHYS_SAY.reward_intents), { 'non-monetary': 'Learning and fun', cash: 'Cash for work', equity: 'Equity', 'future-payouts': 'Future payouts' },
|
|
222
|
+
'the reward words are the review\'s pills (task 1004504)');
|
|
223
|
+
assert.equal(box.PHYS_ORGS, undefined, 'the organization list that picked the voice is gone (task 1004504)');
|
|
224
|
+
assert.equal(box.PHYS_ORBIT, undefined, 'the orbit per framing left with the forming planet');
|
|
225
|
+
assert.equal(box.PHYS_TYPE, undefined, 'the kind is the founder\'s own answer now, never pre-filled from the entity');
|
|
154
226
|
for (const [f, list] of Object.entries(box.PHYS_SUITE_START)) {
|
|
155
227
|
assert.ok(['hybrid', 'local'].includes(f));
|
|
156
228
|
assert.notEqual(physics.canonicalAnswer(Q.services, plain(list)), undefined, `${f}'s starting suite is a services answer`);
|
|
157
229
|
}
|
|
158
|
-
const TYPES = ['game', 'research', 'business', 'non-profit', 'not-sure'];
|
|
159
|
-
for (const [entity, type] of Object.entries(box.PHYS_TYPE)) {
|
|
160
|
-
assert.ok(Q.entity.values.includes(entity)); assert.ok(TYPES.includes(type), `${type} is a project type`);
|
|
161
|
-
}
|
|
162
230
|
});
|
|
163
231
|
|
|
164
232
|
test('the kit stub\'s physics hide rules are the table\'s own, word for word', () => {
|
|
@@ -174,50 +242,149 @@ test('the kit stub\'s physics hide rules are the table\'s own, word for word', (
|
|
|
174
242
|
}
|
|
175
243
|
});
|
|
176
244
|
|
|
177
|
-
// ──
|
|
178
|
-
test('
|
|
179
|
-
const w =
|
|
180
|
-
|
|
181
|
-
assert.equal(w.
|
|
182
|
-
assert.
|
|
245
|
+
// ── screen 3, the quick checks ───────────────────────────────────────────────
|
|
246
|
+
test('the quick checks reveal what an answer opens: the age tiers, the legal forms, a state or province', () => {
|
|
247
|
+
const w = boot({ draft: { step: 3 } });
|
|
248
|
+
w.run('paintChecks()');
|
|
249
|
+
assert.equal(w.$('physAges').hidden, true, 'no tiers before a yes');
|
|
250
|
+
assert.equal(w.$('physForms').hidden, true, 'the plain intents first');
|
|
251
|
+
assert.equal(w.$('physIntent').hidden, false);
|
|
252
|
+
w.pick('minors_on_board', 'true');
|
|
253
|
+
assert.equal(w.a().minors_on_board, true);
|
|
254
|
+
assert.equal(w.$('physAges').hidden, false, 'a yes shows the tiers young builders work under');
|
|
255
|
+
assert.equal(w.q('[data-phys="minors_on_board"][data-val="true"]')[0].getAttribute('aria-pressed'), 'true');
|
|
256
|
+
w.pick('minors_on_board', 'true');
|
|
257
|
+
assert.equal(w.a().minors_on_board, null, 'a second click un-says it');
|
|
258
|
+
assert.equal(w.$('physAges').hidden, true);
|
|
259
|
+
w.$('physHaveEntity').click();
|
|
260
|
+
assert.equal(w.$('physForms').hidden, false, 'an existing entity opens the full legal list');
|
|
261
|
+
assert.equal(w.$('physIntent').hidden, true);
|
|
262
|
+
w.pick('entity', 'single-owner-llc');
|
|
263
|
+
assert.equal(w.a().entity, 'single-owner-llc');
|
|
264
|
+
w.$('physNoEntity').click();
|
|
265
|
+
assert.equal(w.$('physForms').hidden, true, 'and the way back closes it');
|
|
266
|
+
assert.equal(w.a().entity, null, 'taking the legal form with it');
|
|
267
|
+
w.$('physCountry').value = 'US';
|
|
268
|
+
w.$('physCountry').listeners.change();
|
|
269
|
+
assert.equal(w.$('physRegionField').hidden, false, 'a country with states asks for one');
|
|
270
|
+
assert.ok(w.$('physRegion').children.some((o) => o.value === 'US-CA'));
|
|
271
|
+
w.$('physCountry').value = 'NL';
|
|
272
|
+
w.$('physCountry').listeners.change();
|
|
273
|
+
assert.equal(w.$('physRegionField').hidden, true, 'and one without does not');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('the quick checks fill in from the account\'s last project, never its state, and offer nothing without one', async () => {
|
|
277
|
+
const none = boot({ draft: { step: 3 } });
|
|
278
|
+
none.run('physLoadAccountOnce()');
|
|
279
|
+
await none.settle();
|
|
280
|
+
assert.equal(none.$('physFillOffer').hidden, true, 'no earlier answers: no offer');
|
|
281
|
+
const w = boot({ draft: { step: 3 }, instances: { instances: [
|
|
282
|
+
{ created_at: '2026-01-01', detail: { home_country: 'GB', entity: 'intent-unsure' } },
|
|
283
|
+
{ created_at: '2026-06-01', detail: { home_country: 'NL', builder_geography: 'global', minors_on_board: false, entity: 'for-profit', project_state: 'existing', framing: 'local' } },
|
|
284
|
+
] } });
|
|
285
|
+
w.run('physLoadAccountOnce()');
|
|
286
|
+
await w.settle();
|
|
287
|
+
assert.equal(w.$('physFillOffer').hidden, false);
|
|
288
|
+
w.$('physFromAccount').click();
|
|
289
|
+
assert.deepEqual(plain(w.a()), { home_country: 'NL', builder_geography: 'global', minors_on_board: false, entity: 'for-profit' }, 'the newest project\'s four checks, and nothing of its own');
|
|
290
|
+
assert.equal(w.$('physCountry').value, 'NL', 'painted where the founder can see it');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('Continue on the checks writes the review\'s prefills, asks the real table, and opens the review', async () => {
|
|
294
|
+
const w = boot({ draft: { step: 3, description: 'A field guide app for birdwatchers' } });
|
|
295
|
+
w.run('phys()');
|
|
296
|
+
Object.assign(w.a(), CHECKS);
|
|
297
|
+
assert.equal(w.run('checksAdvance()'), false, 'the screen never says yes itself');
|
|
298
|
+
await w.settle();
|
|
299
|
+
assert.deepEqual(w.shown, [4]);
|
|
300
|
+
const sent = w.fetched.find((f) => f.url.endsWith('/provisioning/physics-check')).body;
|
|
301
|
+
assert.equal(sent.description, 'A field guide app for birdwatchers');
|
|
302
|
+
assert.deepEqual({ s: sent.detail.project_state, f: sent.detail.framing, n: [sent.detail.builders_at_start, sent.detail.builders_expected] },
|
|
303
|
+
{ s: 'new', f: 'managed', n: [1, 1] }, 'a new project, all managed, one person');
|
|
304
|
+
assert.equal(w.state.teamShape, 'solo');
|
|
305
|
+
assert.ok(w.run('physLimits').some((l) => l.option === 'reward_intents' && l.effect === 'hide'), 'the table\'s verdicts shape the review');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('a stop from the server ends on the stop screen, before anything exists', async () => {
|
|
309
|
+
const w = boot({ draft: { step: 3, description: 'ransomware' }, check: () => ({ stop: true, message: 'We cannot host this.' }) });
|
|
310
|
+
w.run('phys()');
|
|
311
|
+
Object.assign(w.a(), CHECKS);
|
|
312
|
+
w.run('checksAdvance()');
|
|
313
|
+
await w.settle();
|
|
314
|
+
assert.deepEqual(w.shown, [], 'no review');
|
|
315
|
+
assert.equal(w.$('physStopMsg').textContent, 'We cannot host this.');
|
|
316
|
+
assert.equal(w.$('checksStop').hidden, false);
|
|
317
|
+
assert.equal(w.$('checksMain').hidden, true);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ── the Team size row (question 5) ───────────────────────────────────────────
|
|
321
|
+
test('Team size: one or many, a start count, and a guessed growth the founder corrects', async () => {
|
|
322
|
+
const w = await onReview();
|
|
323
|
+
assert.deepEqual(w.rowSays('team'), ['One person', 'Just you.'], 'prefilled: one person');
|
|
324
|
+
assert.equal(w.q('[data-phys-solo="one"]')[0].getAttribute('aria-pressed'), 'true');
|
|
325
|
+
assert.equal(w.$('physTeam').hidden, true);
|
|
326
|
+
w.change('team');
|
|
327
|
+
assert.equal(w.isOpen('team'), true);
|
|
183
328
|
w.q('[data-phys-solo="many"]')[0].click();
|
|
184
329
|
assert.equal(w.a().builders_at_start, 2);
|
|
185
330
|
assert.equal(w.a().builders_expected, 5, 'the guess: about 2.6 times the start');
|
|
331
|
+
assert.equal(w.state.teamShape, 'small-team', 'a team invites its people by default');
|
|
186
332
|
assert.equal(w.$('physTeam').hidden, false);
|
|
187
333
|
w.q('[data-phys-count="1"]')[0].click();
|
|
188
334
|
assert.equal(w.a().builders_at_start, 3);
|
|
189
335
|
assert.equal(w.a().builders_expected, 8, 'the guess follows the start');
|
|
190
|
-
assert.
|
|
191
|
-
assert.match(w.$('physGuessLine').textContent, /about 8
|
|
336
|
+
assert.equal(w.$('physStartN').textContent, '3');
|
|
337
|
+
assert.match(w.$('physGuessLine').textContent, /about 8 people by month three/);
|
|
192
338
|
w.$('physExpected').value = '12';
|
|
193
339
|
w.$('physExpected').listeners.input();
|
|
194
340
|
assert.equal(w.a().builders_expected, 12, 'the founder corrects it');
|
|
195
341
|
w.q('[data-phys-count="1"]')[0].click();
|
|
196
342
|
assert.equal(w.a().builders_expected, 12, 'and once corrected the guess stays theirs');
|
|
197
|
-
assert.
|
|
198
|
-
|
|
199
|
-
assert.equal(w.
|
|
343
|
+
assert.deepEqual(w.rowSays('team'), ['4 people now', 'About 12 by month three · people you invite.']);
|
|
344
|
+
w.state.teamShape = 'community'; w.run('paintSetup()');
|
|
345
|
+
assert.equal(w.rowSays('team')[1], 'About 12 by month three · anyone can apply.');
|
|
346
|
+
w.use('team');
|
|
347
|
+
assert.equal(w.state.confirmed.team, true);
|
|
348
|
+
assert.equal(w.isOpen('team'), false, 'Use this closes the row');
|
|
200
349
|
});
|
|
201
350
|
|
|
202
|
-
test('
|
|
203
|
-
const w =
|
|
351
|
+
test('changing the team re-asks the rows it moves: rewards and extras', async () => {
|
|
352
|
+
const w = await onReview({ builders_at_start: 3, builders_expected: 8 });
|
|
353
|
+
w.state.confirmed = { rewards: true, extras: true, code: true };
|
|
204
354
|
w.q('[data-phys-solo="one"]')[0].click();
|
|
205
|
-
assert.
|
|
206
|
-
assert.equal(w.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
assert.
|
|
213
|
-
assert.equal(w.
|
|
214
|
-
w
|
|
215
|
-
assert.equal(w.
|
|
355
|
+
assert.deepEqual(Object.keys(w.state.confirmed), ['code'], 'rewards and extras come back to be confirmed; the repo does not');
|
|
356
|
+
assert.equal(w.state.teamShape, 'solo');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('a team row with no count cannot be confirmed: it opens instead', async () => {
|
|
360
|
+
const w = await onReview();
|
|
361
|
+
w.state.physics.answers.builders_at_start = 4; w.state.physics.answers.builders_expected = 2; // fewer later than now
|
|
362
|
+
assert.equal(w.run("confirmRow('team')"), false);
|
|
363
|
+
assert.equal(w.isOpen('team'), true, 'the row opens with its controls');
|
|
364
|
+
assert.equal(w.row('team').classList.contains('open'), true);
|
|
365
|
+
assert.equal(w.row('team').querySelector('[data-row-chg]').getAttribute('aria-expanded'), 'true');
|
|
366
|
+
assert.equal(w.state.confirmed.team, undefined);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// ── the Rewards row (question 6) ─────────────────────────────────────────────
|
|
370
|
+
test('rewards are hidden for a solo founder: the real table hides the row, and the create sends none', async () => {
|
|
371
|
+
const w = await onReview({ reward_intents: ['cash'] });
|
|
372
|
+
assert.equal(w.run("rowApplies('rewards')"), false, 'the REAL softLimitsFor hides reward_intents for one-and-one');
|
|
373
|
+
assert.equal(w.row('rewards').hidden, true, 'and the row goes with it');
|
|
374
|
+
assert.ok(!w.run('unconfirmedRows()').includes('rewards'), 'nothing to confirm for a question never asked');
|
|
375
|
+
assert.equal(w.go().textContent, 'Confirm 6 more', 'six rows apply');
|
|
376
|
+
assert.equal(w.run('createDetail()').reward_intents, undefined);
|
|
377
|
+
w.change('team');
|
|
378
|
+
w.q('[data-phys-solo="many"]')[0].click();
|
|
379
|
+
await w.settle();
|
|
380
|
+
assert.equal(w.row('rewards').hidden, false, 'a team brings the question back, once the table is asked again');
|
|
381
|
+
assert.equal(w.go().textContent, 'Confirm 7 more');
|
|
216
382
|
});
|
|
217
383
|
|
|
218
384
|
test('rewards: a greyed option is disabled and leaves the list, and each reason is said once', async () => {
|
|
219
|
-
const w =
|
|
220
|
-
|
|
385
|
+
const w = await onReview({ minors_on_board: true, builders_at_start: 3, builders_expected: 8 });
|
|
386
|
+
assert.deepEqual(w.rowSays('rewards'), ['Not decided yet', 'Intent only. You set the real terms later.']);
|
|
387
|
+
w.change('rewards');
|
|
221
388
|
w.pick('reward_intents', 'non-monetary');
|
|
222
389
|
w.pick('reward_intents', 'future-payouts');
|
|
223
390
|
assert.deepEqual(plain(w.a().reward_intents), ['non-monetary', 'future-payouts']);
|
|
@@ -227,23 +394,24 @@ test('rewards: a greyed option is disabled and leaves the list, and each reason
|
|
|
227
394
|
const list = w.q('[data-why-list="reward_intents"]')[0];
|
|
228
395
|
assert.equal(list.hidden, false);
|
|
229
396
|
const lines = list.children.map((li) => li.textContent);
|
|
230
|
-
assert.ok(lines.some((l) => /^Equity
|
|
397
|
+
assert.ok(lines.some((l) => /^Equity: Equity is off until you form a company/.test(l)), lines.join('\n'));
|
|
231
398
|
assert.equal(lines.filter((l) => /13 to 15/.test(l)).length, 1, 'the minors reason names its three options in one line');
|
|
232
|
-
assert.ok(lines.some((l) => /^Cash for work, Equity
|
|
399
|
+
assert.ok(lines.some((l) => /^Cash for work, Equity, Future payouts: Builders aged 13 to 15/.test(l)));
|
|
233
400
|
assert.ok(lines.some((l) => /^Cash for work: Cash payouts to builders in the EU/.test(l)), 'a note is said too');
|
|
401
|
+
assert.deepEqual(w.rowSays('rewards'), ['Learning and fun, Future payouts', 'Intent only. You set the real terms later.']);
|
|
234
402
|
w.pick('reward_intents', 'future-payouts');
|
|
235
403
|
assert.deepEqual(plain(w.a().reward_intents), ['non-monetary'], 'a second click takes one away');
|
|
236
|
-
await w.next();
|
|
237
|
-
assert.equal(w.state.physics.q, 7);
|
|
238
404
|
});
|
|
239
405
|
|
|
240
|
-
// ── question 7
|
|
241
|
-
test('
|
|
242
|
-
const w =
|
|
243
|
-
|
|
244
|
-
|
|
406
|
+
// ── the We run row (question 7) ──────────────────────────────────────────────
|
|
407
|
+
test('We run: hybrid and local open the suite pre-picked; managed and open source hide it with the table\'s reason', async () => {
|
|
408
|
+
const w = await onReview({ builders_at_start: 3, builders_expected: 8, reward_intents: ['non-monetary'] });
|
|
409
|
+
assert.deepEqual(w.rowSays('run'), ['Everything, for you', 'Hosting, payouts and the starter kit, managed.'], 'prefilled: all managed');
|
|
410
|
+
w.state.confirmed.app = true;
|
|
411
|
+
w.change('run');
|
|
245
412
|
w.pick('framing', 'hybrid');
|
|
246
|
-
await
|
|
413
|
+
await w.settle();
|
|
414
|
+
assert.equal(w.state.confirmed.app, undefined, 'a new framing re-asks the App hosting row');
|
|
247
415
|
assert.deepEqual(plain(w.a().services), ['hosting', 'starter-kit', 'marketplace-buy', 'guild-access'], 'the canvas\'s hybrid suite');
|
|
248
416
|
assert.equal(w.$('physSuite').hidden, false);
|
|
249
417
|
assert.equal(w.$('physSuiteLbl').textContent, 'Your hybrid suite');
|
|
@@ -252,219 +420,183 @@ test('framing: hybrid and local open the suite pre-picked; managed and open sour
|
|
|
252
420
|
assert.equal(w.q('[data-phys="services"][data-val="public-marketing"]')[0].disabled, true, 'on request');
|
|
253
421
|
w.pick('services', 'marketplace-sell');
|
|
254
422
|
assert.ok(w.a().services.includes('marketplace-sell'));
|
|
423
|
+
assert.deepEqual(w.rowSays('run'), ['Some of it', 'Hosting, Starting module kit, Module marketplace: buy, Module marketplace: sell, Builder guild access']);
|
|
255
424
|
w.pick('framing', 'local');
|
|
256
|
-
await
|
|
425
|
+
await w.settle();
|
|
257
426
|
assert.ok(w.a().services.includes('marketplace-sell'), 'a suite the founder touched is theirs: not re-seeded');
|
|
427
|
+
assert.equal(w.$('physSuiteLbl').textContent, 'Your local suite');
|
|
258
428
|
w.pick('framing', 'managed');
|
|
259
|
-
await
|
|
429
|
+
await w.settle();
|
|
260
430
|
assert.equal(w.$('physSuite').hidden, true);
|
|
261
431
|
const why = w.q('[data-why="services"]')[0];
|
|
262
432
|
assert.equal(why.hidden, false);
|
|
263
433
|
assert.equal(why.textContent, SOFT_LIMIT_RULES.find((r) => r.id === 'managed-includes-every-service').reason);
|
|
264
434
|
assert.equal(w.run('createDetail()').services, undefined, 'all managed sends no suite');
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
assert.equal(w.
|
|
435
|
+
w.pick('framing', 'managed');
|
|
436
|
+
assert.equal(w.a().framing, null, 'a second click un-says it');
|
|
437
|
+
assert.equal(w.run("rowValid('run')"), false, 'and a run row with no framing cannot be confirmed');
|
|
268
438
|
});
|
|
269
439
|
|
|
270
440
|
test('a company already formed hides company creation from the suite', async () => {
|
|
271
|
-
const w =
|
|
441
|
+
const w = await onReview({ entity: 'for-profit', builders_at_start: 3, builders_expected: 8, reward_intents: ['equity'] });
|
|
272
442
|
w.pick('framing', 'hybrid');
|
|
273
|
-
await
|
|
443
|
+
await w.settle();
|
|
274
444
|
assert.equal(w.q('[data-phys="services"][data-val="company-creation"]')[0].hidden, true);
|
|
275
445
|
});
|
|
276
446
|
|
|
277
|
-
// ── the
|
|
278
|
-
test('
|
|
279
|
-
for (const [
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
assert.equal(w.
|
|
283
|
-
assert.equal(w.run(
|
|
284
|
-
|
|
285
|
-
assert.
|
|
286
|
-
assert.ok(pro.every((e) => e.hidden === (voice !== 'professional')), `${entity}: the professional copy shows only for an organization`);
|
|
287
|
-
assert.ok(planet.every((e) => e.hidden === (voice === 'professional')), `${entity}: the planet copy otherwise`);
|
|
447
|
+
// ── fixed rows: the App hosting row on your own machines, and a demo's own answers ──
|
|
448
|
+
test('a planet on its own machines fixes the App hosting row: it says so, confirms without the Render check, and never opens', async () => {
|
|
449
|
+
for (const [framing, fixed] of [['managed', false], ['hybrid', false], ['local', true], ['open-source', true]]) {
|
|
450
|
+
const w = await onReview({ framing }, { renderOk: false });
|
|
451
|
+
assert.equal(w.run("rowFixed('app')"), fixed, framing);
|
|
452
|
+
assert.equal(w.row('app').querySelector('[data-row-chg]').hidden, fixed, `${framing}: Change is ${fixed ? 'gone' : 'offered'}`);
|
|
453
|
+
assert.equal(w.run("confirmRow('app')"), fixed, `${framing}: ${fixed ? 'confirmed without' : 'held by'} the Render check`);
|
|
454
|
+
if (fixed) assert.equal(w.rowSays('app')[0], 'Runs on your own machines');
|
|
455
|
+
else assert.equal(w.isOpen('app'), true, 'a failed check opens the row with its reason');
|
|
288
456
|
}
|
|
289
|
-
const
|
|
290
|
-
assert.
|
|
291
|
-
assert.equal(pro.$('physCapLbl').textContent, 'project profile');
|
|
292
|
-
assert.match(pro.$('physCoords').textContent, /team 12 → 30/);
|
|
293
|
-
const planet = at(5, { ...FOUR, builders_at_start: 3, builders_expected: 8 });
|
|
294
|
-
assert.match(planet.$('physCallout').textContent, /moons · 3 builders now · the dashed orbit holds about 8/);
|
|
457
|
+
const demo = await onReview({ framing: 'local' }, { renderOk: false, draft: { demo: { people: 3, hours_each: 4 } } });
|
|
458
|
+
assert.equal(demo.run("rowFixed('app')"), false, 'a demo is all managed, whatever framing a walk left behind');
|
|
295
459
|
});
|
|
296
460
|
|
|
297
|
-
test('
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
461
|
+
test('a demo\'s team, rewards and run are the demo\'s own: fixed, said plainly, never opened', async () => {
|
|
462
|
+
const w = await onReview({}, { draft: { demo: { people: 3, hours_each: 4, idea: '', invites: [] } } });
|
|
463
|
+
for (const key of ['team', 'rewards', 'run']) {
|
|
464
|
+
assert.equal(w.run(`rowFixed('${key}')`), true, key);
|
|
465
|
+
assert.equal(w.row(key).querySelector('[data-row-chg]').hidden, true, `${key}: no Change`);
|
|
466
|
+
w.run(`openRow('${key}')`);
|
|
467
|
+
assert.equal(w.isOpen(key), false, `${key}: never opens`);
|
|
468
|
+
}
|
|
469
|
+
assert.deepEqual(w.rowSays('team'), ['3 people', '4 hours each. When the time box runs out, we ask if you want to keep it.']);
|
|
470
|
+
assert.deepEqual(w.rowSays('rewards'), ['None', 'It’s a demo, for learning.']);
|
|
471
|
+
assert.deepEqual(w.rowSays('run'), ['Everything, for you', 'A demo is all managed and private to your team.']);
|
|
301
472
|
});
|
|
302
473
|
|
|
303
|
-
// ── the
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
w.
|
|
308
|
-
w.run('
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
assert.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
assert.
|
|
321
|
-
assert.
|
|
322
|
-
|
|
323
|
-
assert.
|
|
324
|
-
|
|
325
|
-
assert.equal(w.
|
|
326
|
-
|
|
327
|
-
assert.equal(w.
|
|
474
|
+
// ── confirming, and the locked Continue ──────────────────────────────────────
|
|
475
|
+
test('the review locks Continue until every row that applies is confirmed, and validateStep opens the first one left', async () => {
|
|
476
|
+
const w = await onReview({ builders_at_start: 3, builders_expected: 8 });
|
|
477
|
+
assert.equal(w.go().disabled, true);
|
|
478
|
+
assert.equal(w.go().textContent, 'Confirm 7 more');
|
|
479
|
+
assert.equal(w.run('validateStep(4)'), false);
|
|
480
|
+
assert.equal(w.isOpen('code'), true, 'the first unconfirmed row opens');
|
|
481
|
+
for (const [i, key] of ['code', 'address', 'app', 'extras', 'team', 'rewards'].entries()) {
|
|
482
|
+
w.ok(key);
|
|
483
|
+
assert.equal(w.state.confirmed[key], true, key);
|
|
484
|
+
assert.equal(w.row(key).querySelector('[data-row-ok]').textContent, 'Confirmed');
|
|
485
|
+
assert.equal(w.row(key).querySelector('[data-row-ok]').disabled, true);
|
|
486
|
+
assert.equal(w.go().textContent, `Confirm ${6 - i} more`);
|
|
487
|
+
}
|
|
488
|
+
assert.equal(w.state.modules && plain(w.state.modules).join(), 'discord', 'confirming the extras is the answer the create sends');
|
|
489
|
+
w.ok('run');
|
|
490
|
+
assert.equal(w.go().disabled, false);
|
|
491
|
+
assert.equal(w.go().textContent, 'Continue');
|
|
492
|
+
assert.equal(w.run('validateStep(4)'), true);
|
|
493
|
+
w.change('team');
|
|
494
|
+
assert.equal(w.state.confirmed.team, undefined, 'Change takes a row back');
|
|
495
|
+
assert.equal(w.isOpen('team'), true);
|
|
496
|
+
assert.equal(w.go().textContent, 'Confirm 1 more');
|
|
497
|
+
w.change('team');
|
|
498
|
+
assert.equal(w.isOpen('team'), false, 'a second Change closes it again');
|
|
328
499
|
});
|
|
329
500
|
|
|
330
|
-
test('the
|
|
331
|
-
const w =
|
|
332
|
-
w.run('
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
assert.
|
|
337
|
-
|
|
338
|
-
assert.equal(w.state.
|
|
339
|
-
w.
|
|
340
|
-
|
|
341
|
-
assert.
|
|
501
|
+
test('the prefills never overwrite a founder\'s own answer', () => {
|
|
502
|
+
const w = boot();
|
|
503
|
+
w.run('phys()');
|
|
504
|
+
Object.assign(w.a(), CHECKS, { project_state: 'existing', existing_work: ['code'], main_work: 'code', framing: 'hybrid', builders_at_start: 3, builders_expected: 9 });
|
|
505
|
+
w.state.teamShape = 'community';
|
|
506
|
+
w.run('physDefaults()');
|
|
507
|
+
assert.deepEqual({ s: w.a().project_state, f: w.a().framing, n: [w.a().builders_at_start, w.a().builders_expected], t: w.state.teamShape },
|
|
508
|
+
{ s: 'existing', f: 'hybrid', n: [3, 9], t: 'community' });
|
|
509
|
+
assert.equal(w.state.mode, 'adopt', 'a project that already has code adopts its repo');
|
|
510
|
+
w.state.mode = 'greenfield'; w.state.confirmed = { code: true };
|
|
511
|
+
w.run('physDefaults()');
|
|
512
|
+
assert.equal(w.state.mode, 'greenfield', 'but never over a Code row the founder confirmed');
|
|
513
|
+
for (const gone of ['physPrefill', 'typePrefill', 'homePrefill', 'physSummary', 'data-phys-goto', 'physStepSkipped', 'physPastSkips']) {
|
|
514
|
+
assert.ok(!HUB.includes(gone), `${gone} is gone: the review shows every prefill as a row instead`);
|
|
515
|
+
}
|
|
342
516
|
});
|
|
343
517
|
|
|
344
|
-
// ──
|
|
345
|
-
test('
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
assert.equal(w.state.projType, 'game', 'their choice stands');
|
|
355
|
-
w.state.projType = 'non-profit'; // back to what we filled …
|
|
356
|
-
w.state.prefilled = { projType: 'non-profit' };
|
|
357
|
-
w.run('physPrefill()');
|
|
358
|
-
assert.equal(w.state.projType, 'research', '… and an untouched pre-fill follows the answer');
|
|
359
|
-
w.state.physics.answers.entity = 'intent-unsure';
|
|
360
|
-
w.run('physPrefill()');
|
|
361
|
-
assert.equal(w.state.projType, null, 'an answer that suggests nothing clears what we filled');
|
|
362
|
-
w.run('physPaintPrefillNotes()');
|
|
363
|
-
assert.equal(w.$('typePrefill').hidden, true);
|
|
518
|
+
// ── the voice (spec D5; one plain voice since task 1004504) ──────────────────
|
|
519
|
+
test('one plain voice for everyone: professional is stored for an organization and an individual alike', async () => {
|
|
520
|
+
assert.doesNotMatch(HUB, /data-voice=/, 'no voice variants are baked anywhere on the page');
|
|
521
|
+
assert.doesNotMatch(SCRIPT, /physVoice|physPaintVoice|PHYS_ORGS/, 'no voice switch is left in the controller');
|
|
522
|
+
for (const entity of ['for-profit', 'nonprofit', 'research', 'corporate-division',
|
|
523
|
+
'intent-company', 'intent-solo', 'social-group', 'university-project', 'private-individual', 'single-owner-llc']) {
|
|
524
|
+
const w = await onReview({ entity });
|
|
525
|
+
assert.equal(w.run('createDetail()').voice, 'professional', `${entity} stores the one voice`);
|
|
526
|
+
}
|
|
527
|
+
assert.doesNotMatch(HUB, /id="(physCoords|physCallout|physCapLbl)"/, 'the profile caption left with the forming planet');
|
|
364
528
|
});
|
|
365
529
|
|
|
366
|
-
test('
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
assert.equal(w.run('physStepSkipped(10)'), skipped, framing);
|
|
370
|
-
assert.equal(w.run('physPastSkips(10, 1)'), skipped ? 11 : 10);
|
|
371
|
-
assert.equal(w.run('physPastSkips(10, -1)'), skipped ? 9 : 10);
|
|
372
|
-
for (const n of [4, 5, 6, 7, 8, 9, 11, 12]) assert.equal(w.run(`physStepSkipped(${n})`), false, `step ${n} always applies`);
|
|
373
|
-
}
|
|
374
|
-
const w = at('sum', { ...SEVEN, framing: 'open-source' }, { draft: { appPlatform: 'render' } });
|
|
375
|
-
w.run('physPrefill()');
|
|
376
|
-
assert.equal(w.state.appPlatform, 'later', 'a skipped step keeps its default answer');
|
|
377
|
-
w.state.demo = { people: 3, hours_each: 4 };
|
|
378
|
-
assert.equal(w.run('physStepSkipped(10)'), false, 'a demo is all managed');
|
|
530
|
+
test('the one voice carries no planet words on the checks and the review', () => {
|
|
531
|
+
const words = PANEL.replace(/<!--[\s\S]*?-->/g, '').replace(/<[^>]*>/g, ' ');
|
|
532
|
+
assert.doesNotMatch(words, /\bplanet|\bmoons?\b|\borbit/i, 'the plain copy names no planet, moon or orbit');
|
|
379
533
|
});
|
|
380
534
|
|
|
381
535
|
// ── what the create sends ────────────────────────────────────────────────────
|
|
536
|
+
const SEVEN = { ...CHECKS, project_state: 'new', builders_at_start: 3, builders_expected: 8, reward_intents: ['non-monetary', 'future-payouts'], framing: 'hybrid', services: ['hosting', 'starter-kit'] };
|
|
382
537
|
test('the create sends questions 5 to 7 and the voice as detail, in the vocabulary\'s order', () => {
|
|
383
|
-
const w =
|
|
538
|
+
const w = boot();
|
|
539
|
+
w.run('phys()');
|
|
540
|
+
Object.assign(w.a(), SEVEN, { reward_intents: ['future-payouts', 'non-monetary'], services: ['starter-kit', 'hosting'] });
|
|
384
541
|
w.state.teamShape = 'small-team';
|
|
385
542
|
const d = plain(w.run('createDetail()'));
|
|
386
543
|
assert.deepEqual(d, {
|
|
387
544
|
project_state: 'new', home_country: 'US', home_region: 'US-CA', builder_geography: 'us-eu', minors_on_board: false, entity: 'intent-solo',
|
|
388
545
|
builders_at_start: 3, builders_expected: 8, framing: 'hybrid',
|
|
389
|
-
reward_intents: ['non-monetary', 'future-payouts'], services: ['hosting', 'starter-kit'], voice: '
|
|
546
|
+
reward_intents: ['non-monetary', 'future-payouts'], services: ['hosting', 'starter-kit'], voice: 'professional', team_shape: 'small-team',
|
|
390
547
|
});
|
|
391
548
|
const { badProjectDetail } = require('../modules/provisioning/routes/body-validators.js');
|
|
392
549
|
assert.equal(badProjectDetail(d), null, 'every key and value is one the create route accepts');
|
|
393
550
|
});
|
|
394
551
|
|
|
395
|
-
// ── the planet
|
|
396
|
-
test('the planet
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
assert.
|
|
401
|
-
assert.
|
|
402
|
-
assert.
|
|
403
|
-
assert.
|
|
404
|
-
assert.equal(w.$('physSun').hidden, false);
|
|
405
|
-
assert.equal(w.$('physSun').getAttribute('src'), '/assets/world/r2-bongos-chrome-a-cut.webp', 'the platform\'s own mark');
|
|
406
|
-
w.state.physics.answers.framing = 'open-source';
|
|
407
|
-
w.run('physPaintStage()');
|
|
408
|
-
assert.equal(w.$('physStage').getAttribute('data-orbit'), 'own');
|
|
409
|
-
assert.equal(w.$('physSun').hidden, true, 'open source is a system of its own');
|
|
410
|
-
w.state.physics.answers.builders_at_start = 9; w.state.physics.answers.builders_expected = 40;
|
|
411
|
-
w.run('physPaintStage()');
|
|
412
|
-
assert.equal(shown('#physMoons img'), 6, 'six at most');
|
|
413
|
-
assert.equal(shown('#physGhosts img'), 4, 'four at most');
|
|
414
|
-
w.state.physics.q = 'sum';
|
|
415
|
-
w.run('physPaintStage()');
|
|
416
|
-
assert.equal(w.$('physPlanet').getAttribute('data-size'), '6', 'the full planet');
|
|
417
|
-
assert.equal(w.$('physCapN').textContent, 'complete');
|
|
418
|
-
for (const plate of ['r3-frost-bead-cut', 'r3-gold-bead-cut', 'r3-obsidian-bead-cut', 'r2-bongos-chrome-a-cut']) {
|
|
419
|
-
assert.ok(fs.existsSync(path.join(LANDING, 'assets', 'world', `${plate}.webp`)), `${plate} is a world plate`);
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
test('the orbit and the moons move on CSS alone, and the reduced-motion switch stops them', () => {
|
|
424
|
-
const css = HUB.slice(HUB.indexOf('.physBelt{'), HUB.indexOf('/* under 520 the two doors'));
|
|
425
|
-
assert.match(css, /@media \(prefers-reduced-motion:no-preference\)\{\s*\.physMoons\{animation:physOrbit 160s/);
|
|
426
|
-
assert.doesNotMatch(css.replace(/@media \(prefers-reduced-motion:no-preference\)\{[\s\S]*?\n\}/, ''), /animation:/, 'no animation outside the no-preference block');
|
|
427
|
-
assert.doesNotMatch(css, /box-shadow/);
|
|
428
|
-
assert.doesNotMatch(SCRIPT, /\.animate\(|requestAnimationFrame|innerHTML/, 'no motion and no innerHTML in the controller');
|
|
552
|
+
// ── the planet left; the room moves on CSS alone ─────────────────────────────
|
|
553
|
+
test('the forming planet is gone, and the room behind the wizard moves on CSS alone, inside the reduced-motion switch', () => {
|
|
554
|
+
assert.doesNotMatch(HUB, /id="(physMoons|physGhosts|physSun|physStage|physPlanet)"/, 'no moons, rings, sun or stage');
|
|
555
|
+
assert.doesNotMatch(HUB, /physPaintStage|\.physBelt\{/);
|
|
556
|
+
const css = slice('#view-new .wizRoom{', '/* under 900 the window steps back', 'the room\'s rules');
|
|
557
|
+
assert.match(css, /@media \(prefers-reduced-motion:no-preference\)\{\s*#view-new \.rmSeed\{transition:transform 1\.2s ease;\}/);
|
|
558
|
+
assert.doesNotMatch(css.replace(/@media \(prefers-reduced-motion:no-preference\)\{[\s\S]*?\n\}/, ''), /animation:|transition:/, 'no motion outside the no-preference block');
|
|
559
|
+
assert.doesNotMatch(css, /box-shadow:[^;]*#[0-9a-f]{3,6}/i, 'no literal colour in a glow');
|
|
560
|
+
assert.doesNotMatch(SCRIPT + REVIEW, /\.animate\(|requestAnimationFrame|innerHTML/, 'no motion and no innerHTML in the controller or the review');
|
|
429
561
|
});
|
|
430
562
|
|
|
431
563
|
// ── the done-when, end to end ────────────────────────────────────────────────
|
|
432
|
-
test('a new owner walks from
|
|
433
|
-
const w = boot();
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
w
|
|
437
|
-
w.state.physics.answers.existing_work = ['code']; w.state.physics.answers.main_work = 'code';
|
|
438
|
-
w.state.description = 'An inventory tool our team started last year.';
|
|
439
|
-
await w.next();
|
|
440
|
-
assert.equal(w.state.physics.q, 2);
|
|
441
|
-
Object.assign(w.state.physics.answers, { home_country: 'US', home_region: 'US-NY' });
|
|
564
|
+
test('a new owner walks from the quick checks through a fully confirmed review, the setup prefilled', async () => {
|
|
565
|
+
const w = boot({ draft: { step: 3, description: 'An inventory tool our team started last year.' } });
|
|
566
|
+
w.run('paintChecks()');
|
|
567
|
+
w.$('physCountry').value = 'US'; w.$('physCountry').listeners.change();
|
|
568
|
+
w.$('physRegion').value = 'US-NY'; w.$('physRegion').listeners.change();
|
|
442
569
|
w.pick('builder_geography', 'global');
|
|
443
|
-
await w.next();
|
|
444
570
|
w.pick('minors_on_board', 'false');
|
|
445
|
-
|
|
446
|
-
w.pick('entity', 'for-profit');
|
|
447
|
-
|
|
448
|
-
|
|
571
|
+
w.$('physHaveEntity').click();
|
|
572
|
+
w.pick('entity', 'for-profit'); // a company already formed, so equity is on the table
|
|
573
|
+
w.run('checksAdvance()');
|
|
574
|
+
await w.settle();
|
|
575
|
+
assert.deepEqual(w.shown, [4], 'the review');
|
|
576
|
+
w.run('paintSetup()');
|
|
577
|
+
assert.equal(w.row('rewards').hidden, true, 'one person: no rewards row');
|
|
578
|
+
w.ok('code'); w.ok('address'); w.ok('app'); w.ok('extras');
|
|
579
|
+
w.change('team');
|
|
449
580
|
w.q('[data-phys-solo="many"]')[0].click();
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
w.
|
|
581
|
+
w.use('team');
|
|
582
|
+
await w.settle();
|
|
583
|
+
assert.equal(w.state.confirmed.extras, undefined, 'the team moved the extras\' preselect, so that row is asked again');
|
|
584
|
+
w.ok('extras');
|
|
585
|
+
assert.equal(w.row('rewards').hidden, false);
|
|
586
|
+
w.change('rewards');
|
|
453
587
|
w.pick('reward_intents', 'cash');
|
|
454
|
-
|
|
455
|
-
|
|
588
|
+
w.pick('reward_intents', 'equity');
|
|
589
|
+
w.use('rewards');
|
|
590
|
+
w.change('run');
|
|
456
591
|
w.pick('framing', 'open-source');
|
|
457
|
-
await
|
|
458
|
-
|
|
459
|
-
assert.equal(w.state.
|
|
460
|
-
assert.equal(w
|
|
461
|
-
|
|
462
|
-
assert.
|
|
463
|
-
assert.
|
|
464
|
-
{ type: 'business', mode: 'adopt', app: 'later', skipApp: true });
|
|
592
|
+
await w.settle();
|
|
593
|
+
w.use('run');
|
|
594
|
+
assert.equal(w.state.confirmed.app, undefined, 'the framing moved the App hosting row');
|
|
595
|
+
assert.equal(w.rowSays('app')[0], 'Runs on your own machines');
|
|
596
|
+
w.ok('app');
|
|
597
|
+
assert.equal(w.go().textContent, 'Continue');
|
|
598
|
+
assert.equal(w.run('validateStep(4)'), true);
|
|
465
599
|
const d = plain(w.run('createDetail()'));
|
|
466
|
-
assert.
|
|
467
|
-
|
|
468
|
-
assert.equal(d.services, undefined);
|
|
469
|
-
assert.deepEqual(d.reward_intents, ['cash', 'equity']);
|
|
600
|
+
assert.deepEqual({ voice: d.voice, framing: d.framing, services: d.services, team: [d.builders_at_start, d.builders_expected], shape: d.team_shape, rewards: d.reward_intents },
|
|
601
|
+
{ voice: 'professional', framing: 'open-source', services: undefined, team: [2, 5], shape: 'small-team', rewards: ['cash', 'equity'] });
|
|
470
602
|
});
|