@bongos/core 1.20.75 → 1.20.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +53 -33
- package/docs/copy-inventory.md +546 -482
- package/docs/copy-registry.json +1208 -631
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +10 -1
- package/docs/page-readings.json +1204 -1061
- package/modules/hall-ui/public/profile-roles.js +170 -0
- package/modules/hall-ui/public/profile.css +42 -5
- package/modules/hall-ui/public/profile.html +12 -5
- package/modules/hall-ui/public/profile.js +66 -27
- package/modules/hall-ui/public/profile.states.json +39 -1
- package/modules/hall-ui/records/profile-roles.md +32 -0
- package/modules/provisioning/planet-physics.js +8 -0
- package/modules/provisioning/soft-limits.js +9 -0
- package/modules/public-landing/public/projects.html +579 -37
- package/modules/public-landing/public/projects.probes.json +6 -3
- package/modules/public-landing/public/projects.states.json +20 -14
- package/modules/ui-design/kit/serve.js +26 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/founding_builders.mjs +2 -1
- package/tests/helpers.mjs +4 -0
- package/tests/profile_role_headline.mjs +201 -0
- package/tests/profile_ui_cross_project.mjs +3 -0
- package/tests/projects_hub_app_step.mjs +4 -1
- package/tests/provisioning_planet_physics.mjs +24 -2
- package/tests/wizard_physics.mjs +11 -4
- package/tests/wizard_physics_more.mjs +470 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// tests/profile_role_headline.mjs — the profile's header and "By the numbers"
|
|
2
|
+
// lead with each person's own craft (task 1004434 / WA6.RP04, goal 1000095,
|
|
3
|
+
// criterion wa6-roles-read-true-in-public).
|
|
4
|
+
//
|
|
5
|
+
// The done-when: "engineer, artist and ideator fixtures each render their own
|
|
6
|
+
// headline, matching the mock". Three layers, each run for real:
|
|
7
|
+
//
|
|
8
|
+
// 1. The words. profile-roles.js (pure) over the three harness fixtures the
|
|
9
|
+
// renders were taken from: each craft's tally, tiles and subtitle.
|
|
10
|
+
// 2. The page. The REAL profile.js in a vm, with the REAL hall kit, over the
|
|
11
|
+
// same fixtures: what lands in the header, the numbers and the subtitle.
|
|
12
|
+
// 3. The whole path. The REAL server reader (src/bongos/role-stats.js) with
|
|
13
|
+
// the three role ports stubbed, through JSON, into the REAL page, so a field
|
|
14
|
+
// the server renames or drops cannot leave this suite green on a fixture
|
|
15
|
+
// that still carries it.
|
|
16
|
+
|
|
17
|
+
import { strict as assert } from 'node:assert';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
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
|
+
import { makeRunner } from './helpers.mjs';
|
|
24
|
+
|
|
25
|
+
process.env.NODE_ENV = 'test';
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
28
|
+
const FIX = (n) => JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts/hall-preview/fixtures', n + '.json'), 'utf8'));
|
|
29
|
+
|
|
30
|
+
const { headline } = require('../modules/hall-ui/public/profile-roles.js');
|
|
31
|
+
const OTBKit = require('../modules/hall-ui/public/hall-kit.js');
|
|
32
|
+
const ROLES_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/profile-roles.js'), 'utf8');
|
|
33
|
+
const PAGE_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/profile.js'), 'utf8');
|
|
34
|
+
|
|
35
|
+
const ENGINEER = FIX('builders__profile');
|
|
36
|
+
const ARTIST = FIX('builders__2001__profile');
|
|
37
|
+
const IDEATOR = FIX('builders__2002__profile');
|
|
38
|
+
const OPTS = { fmt: (n) => Number(n).toLocaleString('en-US'), currency: 'credits', rank: 'Contributor' };
|
|
39
|
+
const words = (items) => items.map((t) => `${t.n} ${t.w}`);
|
|
40
|
+
|
|
41
|
+
const { test, summary } = makeRunner();
|
|
42
|
+
|
|
43
|
+
// ---- 1. the words --------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
await test('an engineer leads with works shipped, and keeps karma', () => {
|
|
46
|
+
const v = headline(ENGINEER, OPTS);
|
|
47
|
+
assert.deepEqual(words(v.tally), ['4,630 credits', '24 works shipped', '1 karma']);
|
|
48
|
+
assert.deepEqual(words(v.tiles).slice(0, 4), ['24 works shipped', '4,630 credits', '1 karma', '2 achievements']);
|
|
49
|
+
assert.equal(v.sub, 'Builder 4 — Contributor · 24 works shipped');
|
|
50
|
+
assert.deepEqual(v.surfaces, [], 'the per-surface rows are the artist\'s');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await test('an artist who never shipped a task leads with pages approved and the per-surface share', () => {
|
|
54
|
+
const v = headline(ARTIST, OPTS);
|
|
55
|
+
assert.deepEqual(words(v.tally), ['412 credits', '9 pages approved', '+38 this week']);
|
|
56
|
+
assert.deepEqual(words(v.tiles), ['9 pages approved', '270 credits from pages', '+38 this week', '4 thoughts filed', '6 karma']);
|
|
57
|
+
assert.equal(v.tiles[0].f, '3 surfaces');
|
|
58
|
+
assert.equal(v.sub, 'Builder 7 — Contributor · 9 pages approved');
|
|
59
|
+
assert.ok(!v.sub.includes('works shipped'), 'the subtitle no longer says works shipped for an artist');
|
|
60
|
+
assert.deepEqual(v.surfaces.map((s) => s.line), ['6 of 12 tweaked', '2 of 19 tweaked', '1 of 6 tweaked']);
|
|
61
|
+
assert.ok(!words(v.tiles).some((w) => /works shipped/.test(w)), 'no engineer figure on an artist\'s numbers');
|
|
62
|
+
assert.equal(words(v.tiles).at(-1), '6 karma', 'karma ends every non-engineer set (the owner, 2026-10-01)');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await test('an ideator leads with thoughts, size and what is still alive', () => {
|
|
66
|
+
const v = headline(IDEATOR, OPTS);
|
|
67
|
+
assert.deepEqual(words(v.tally), ['186 credits', '31 thoughts filed', '7 ignited']);
|
|
68
|
+
assert.deepEqual(words(v.tiles), ['31 thoughts filed', '7 ignited', '3 Full Ideas', '186~ total size', '72% still alive', '3 karma']);
|
|
69
|
+
assert.equal(v.tiles[2].f, '1 ratified by the board');
|
|
70
|
+
assert.match(v.tiles[3].title, /Includes 24 credits of historical estimate/, 'the ~ says what it means');
|
|
71
|
+
assert.equal(v.sub, 'Builder 9 — Contributor · 31 thoughts filed');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await test('several crafts each show, main first, in the server\'s order', () => {
|
|
75
|
+
assert.deepEqual(headline(ARTIST, OPTS).crafts.map((c) => c.label), ['Artist, main role', 'Ideator']);
|
|
76
|
+
assert.deepEqual(headline(ENGINEER, OPTS).crafts.map((c) => c.label), ['Engineer, main role', 'Ideator']);
|
|
77
|
+
// The other craft adds its one headline tile after the main craft's set.
|
|
78
|
+
assert.equal(words(headline(ENGINEER, OPTS).tiles).at(-1), '9 thoughts filed');
|
|
79
|
+
const reordered = { ...ARTIST, roles: { ...ARTIST.roles, main: 'ideator', shown: ['ideator', 'artist'] } };
|
|
80
|
+
const v = headline(reordered, OPTS);
|
|
81
|
+
assert.deepEqual(v.crafts.map((c) => c.label), ['Ideator, main role', 'Artist']);
|
|
82
|
+
assert.equal(words(v.tiles).at(-2), '9 pages approved', 'the other craft\'s tile, just before karma');
|
|
83
|
+
assert.deepEqual(v.surfaces, [], 'the per-surface rows follow the MAIN craft, not any craft');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await test('a number the instance could not read is a dash, never a zero', () => {
|
|
87
|
+
const noDesk = { ...ARTIST, roles: { ...ARTIST.roles, artist: { pages_approved: null, credits: null, this_week: null, surfaces_touched: null, surfaces: [], unavailable: true } } };
|
|
88
|
+
const v = headline(noDesk, OPTS);
|
|
89
|
+
assert.deepEqual(words(v.tally).slice(1), ['— pages approved', '— this week']);
|
|
90
|
+
assert.equal(v.tiles[0].f, '', 'no "0 surfaces" under an unknown count');
|
|
91
|
+
const noWork = { ...IDEATOR, roles: { ...IDEATOR.roles, ideator: { ...IDEATOR.roles.ideator, alive: { share: null, has_work: false, attributable: null } } } };
|
|
92
|
+
assert.deepEqual(headline(noWork, OPTS).tiles.at(-2), { n: '—', w: 'still alive', f: 'nothing has come of them yet' });
|
|
93
|
+
const untraced = { ...IDEATOR, roles: { ...IDEATOR.roles, ideator: { ...IDEATOR.roles.ideator, alive: { share: null, has_work: true, attributable: false } } } };
|
|
94
|
+
assert.equal(headline(untraced, OPTS).tiles.at(-2).f, 'not measurable yet');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await test('a response from before the roles block reads as the engineer it always was', () => {
|
|
98
|
+
const { roles, ...old } = ENGINEER;
|
|
99
|
+
const v = headline(old, OPTS);
|
|
100
|
+
assert.deepEqual(v.crafts.map((c) => c.label), ['Engineer, main role']);
|
|
101
|
+
assert.equal(v.sub, 'Builder 4 — Contributor · 24 works shipped');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ---- 2. the page ---------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
async function boot(profileData) {
|
|
107
|
+
const els = new Map();
|
|
108
|
+
const makeEl = () => ({
|
|
109
|
+
hidden: false, textContent: '', _html: '', dataset: {}, style: {},
|
|
110
|
+
set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; },
|
|
111
|
+
insertAdjacentHTML() {}, addEventListener() {}, setAttribute() {}, querySelector: () => null, querySelectorAll: () => [],
|
|
112
|
+
classList: { add() {}, remove() {}, toggle() {} },
|
|
113
|
+
});
|
|
114
|
+
const $ = (sel) => { if (!els.has(sel)) els.set(sel, makeEl()); return els.get(sel); };
|
|
115
|
+
const escapeHtml = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
116
|
+
const json = JSON.parse(JSON.stringify(profileData)); // what the wire carries
|
|
117
|
+
const api = {
|
|
118
|
+
async request(_m, url) {
|
|
119
|
+
if (url === '/api/bongos/me') return { status: 401, ok: false, data: null };
|
|
120
|
+
if (/\/builders\/[^/]+\/profile$/.test(url)) return { status: 200, ok: true, data: json };
|
|
121
|
+
return { status: 404, ok: false, data: null };
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
const windowObj = {
|
|
125
|
+
__BRANDING__: { currency: { label: 'credits' } },
|
|
126
|
+
__MODULES__: { responsibilities: { engineer: 'E.', artist: 'A.', ideator: 'I.' } },
|
|
127
|
+
BongosClient: { createClient: () => api },
|
|
128
|
+
// The real kit draws the surface bars; the section rail needs a real DOM and
|
|
129
|
+
// is not what this suite is about, so it is left out (profile.js skips it).
|
|
130
|
+
OTBKit: { ...OTBKit, makeSectionRail: undefined },
|
|
131
|
+
OTB: {
|
|
132
|
+
$, escapeHtml,
|
|
133
|
+
fmtNum: (n) => Number(n || 0).toLocaleString('en-US'),
|
|
134
|
+
fmtDate: (d) => String(d).slice(0, 10),
|
|
135
|
+
rankLabel: () => 'Contributor',
|
|
136
|
+
roleAxisLabel: () => 'Role', roleLabel: (d) => d, roleIsUnset: (d) => !d, toast() {},
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const sandbox = {
|
|
140
|
+
window: windowObj, location: { search: '?id=' + profileData.builder.id, hash: '' }, URLSearchParams, console,
|
|
141
|
+
document: { querySelector: () => null, querySelectorAll: () => [], addEventListener() {} },
|
|
142
|
+
addEventListener() {}, history: { replaceState() {} },
|
|
143
|
+
};
|
|
144
|
+
sandbox.globalThis = sandbox;
|
|
145
|
+
vm.createContext(sandbox);
|
|
146
|
+
vm.runInContext(ROLES_SRC, sandbox, { filename: 'profile-roles.js' });
|
|
147
|
+
vm.runInContext(PAGE_SRC, sandbox, { filename: 'profile.js' });
|
|
148
|
+
for (let i = 0; i < 6; i++) await new Promise((r) => setImmediate(r));
|
|
149
|
+
return { head: $('#profile-head').innerHTML, nums: $('#numbers-body').innerHTML, sub: $('#profile-sub').textContent };
|
|
150
|
+
}
|
|
151
|
+
const text = (html) => html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
152
|
+
|
|
153
|
+
await test('the real page draws the artist\'s tally, chips, tiles and surface rows', async () => {
|
|
154
|
+
const p = await boot(ARTIST);
|
|
155
|
+
assert.equal(p.sub, 'Builder 7 — Contributor · 9 pages approved');
|
|
156
|
+
assert.match(text(p.head), /Artist, main role Ideator/);
|
|
157
|
+
assert.match(text(p.head), /412 credits 9 pages approved \+38 this week/);
|
|
158
|
+
assert.match(p.head, /Answerable for:/, 'the responsibility line stays');
|
|
159
|
+
assert.match(text(p.nums), /^9 pages approved 3 surfaces 270 credits from pages \+38 this week 4 thoughts filed 1 ignited/);
|
|
160
|
+
assert.equal((p.nums.match(/kit-bars__row/g) || []).length, 3, 'one kit bar row per surface');
|
|
161
|
+
assert.match(text(p.nums), /Landing 6 of 12 tweaked/);
|
|
162
|
+
assert.ok(!/works shipped/i.test(p.sub + p.head + p.nums), 'nothing on an artist\'s header says works shipped');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await test('the real page draws the ideator\'s and the engineer\'s own headlines', async () => {
|
|
166
|
+
const i = await boot(IDEATOR);
|
|
167
|
+
assert.match(text(i.head), /186 credits 31 thoughts filed 7 ignited/);
|
|
168
|
+
assert.match(text(i.nums), /186~ total size/);
|
|
169
|
+
assert.match(text(i.nums), /72% still alive/);
|
|
170
|
+
assert.ok(!i.nums.includes('kit-bars'), 'no surface rows for an ideator');
|
|
171
|
+
const e = await boot({ ...ENGINEER, recent_ships: [] }); // the works ledger is not this suite's
|
|
172
|
+
assert.equal(e.sub, 'Builder 4 — Contributor · 24 works shipped');
|
|
173
|
+
assert.match(text(e.head), /4,630 credits 24 works shipped 1 karma/);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ---- 3. the whole path ----------------------------------------------------
|
|
177
|
+
|
|
178
|
+
await test('the server\'s roles block, through JSON, renders the artist\'s headline', async () => {
|
|
179
|
+
const seams = require('../src/module-seams.js');
|
|
180
|
+
const { readRoleStats } = require('../src/bongos/role-stats.js');
|
|
181
|
+
seams._reset();
|
|
182
|
+
seams.registerProvider('reward', {
|
|
183
|
+
craftForPreference: (d) => ({ engineer: 'engineer', artist: 'artist', ui: 'artist', ideator: 'ideator' }[d] || null),
|
|
184
|
+
async creditsByCraft(ids) { return new Map([[String(ids[0]), { engineer: 0, artist: 370, ideator: 42, other: 0, total: 412 }]]); },
|
|
185
|
+
});
|
|
186
|
+
seams.registerProvider('copy-desk.artistStats', async () => ARTIST.roles.artist);
|
|
187
|
+
seams.registerProvider('ideas.ideatorStats', async () => ARTIST.roles.ideator);
|
|
188
|
+
const roles = await readRoleStats('2001', {
|
|
189
|
+
shippedCount: 0,
|
|
190
|
+
getRoleChoice: async () => ({ main_discipline: 'artist', preferred_disciplines: ['artist', 'ideator'] }),
|
|
191
|
+
});
|
|
192
|
+
seams._reset();
|
|
193
|
+
const { roles: _fixture, ...rest } = ARTIST;
|
|
194
|
+
const p = await boot({ ...rest, roles });
|
|
195
|
+
assert.equal(p.sub, 'Builder 7 — Contributor · 9 pages approved');
|
|
196
|
+
assert.match(text(p.head), /Artist, main role Ideator/);
|
|
197
|
+
assert.match(text(p.head), /412 credits 9 pages approved \+38 this week/);
|
|
198
|
+
assert.equal((p.nums.match(/kit-bars__row/g) || []).length, 3);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
summary();
|
|
@@ -29,6 +29,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
29
29
|
|
|
30
30
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
31
|
const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/profile.js'), 'utf8');
|
|
32
|
+
const ROLES_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/profile-roles.js'), 'utf8');
|
|
32
33
|
const HTML = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/profile.html'), 'utf8');
|
|
33
34
|
|
|
34
35
|
// ---- fixtures -------------------------------------------------------------
|
|
@@ -98,6 +99,8 @@ async function boot({ search = '', me, profile } = {}) {
|
|
|
98
99
|
};
|
|
99
100
|
sandbox.globalThis = sandbox;
|
|
100
101
|
vm.createContext(sandbox);
|
|
102
|
+
// The header is drawn from profile-roles.js (task 1004434), loaded first as the page does.
|
|
103
|
+
vm.runInContext(ROLES_SRC, sandbox, { filename: 'profile-roles.js' });
|
|
101
104
|
vm.runInContext(SRC, sandbox, { filename: 'profile.js' });
|
|
102
105
|
// load() is fire-and-forget; let its await chain settle.
|
|
103
106
|
for (let i = 0; i < 4; i++) await new Promise((r) => setImmediate(r));
|
|
@@ -114,7 +114,10 @@ function load({ draft = {}, answers = {} } = {}) {
|
|
|
114
114
|
const watches = [];
|
|
115
115
|
const ctx = { root, state, $, document, Object, API: '/api/bongos', fetch, JSON, String, Number, encodeURIComponent, Array, saveDraft: () => saves.push(JSON.stringify(state)),
|
|
116
116
|
// the shared status watch (task 1004353) lives outside this block; record that it is started
|
|
117
|
-
watchApp: (id, box) => { watches.push({ id, box }); return () => {}; }
|
|
117
|
+
watchApp: (id, box) => { watches.push({ id, box }); return () => {}; },
|
|
118
|
+
// the planet-physics step skip (task 1004419) lives outside this block too; the
|
|
119
|
+
// framing that skips this step is wizard_physics_more.mjs's to test
|
|
120
|
+
physStepSkipped: () => false };
|
|
118
121
|
vm.createContext(ctx);
|
|
119
122
|
vm.runInContext(ESC + '\n' + APPEL + '\n' + CONNECT + '\n' + BLOCK + '\nthis.api = { appPlatformChoice, syncAppStep, appRow, syncRenderSetup, validateRender, renderRequestBody, sendRenderSetup, planLabel };', ctx);
|
|
120
123
|
const api = ctx.api;
|
|
@@ -24,7 +24,7 @@ import { createRequire } from 'node:module';
|
|
|
24
24
|
import http from 'node:http';
|
|
25
25
|
// ESM named imports off the CJS modules on purpose — knip cannot trace a
|
|
26
26
|
// createRequire() import (the tests/provisioning_module_selection.mjs note).
|
|
27
|
-
import { EU_COUNTRIES, COUNTRY_CODES, REGION_CODES, crossAnswerFault }
|
|
27
|
+
import { EU_COUNTRIES, COUNTRY_CODES, REGION_CODES, crossAnswerFault, canonicalAnswer as physicsCanonical }
|
|
28
28
|
from '../modules/provisioning/planet-physics.js';
|
|
29
29
|
import { SOFT_LIMIT_RULES, EFFECTS, softLimitsFor } from '../modules/provisioning/soft-limits.js';
|
|
30
30
|
import { SCREENING_RULES, STOP_MESSAGE, screenProject } from '../modules/provisioning/screening.js';
|
|
@@ -164,7 +164,11 @@ await t('every rule carries a reason, a known effect, and names only real questi
|
|
|
164
164
|
const q = provisioning.PROJECT_DETAIL_QUESTIONS[key];
|
|
165
165
|
assert.ok(q, `${rule.id}: ${key} is a question`);
|
|
166
166
|
const named = test.in || [test.has ?? test.lacks];
|
|
167
|
-
|
|
167
|
+
// a count question has a range, not a list (task 1004419's solo rule)
|
|
168
|
+
for (const v of named) {
|
|
169
|
+
const real = q.kind === 'count' ? physicsCanonical(q, v) !== undefined : q.values.includes(v);
|
|
170
|
+
assert.ok(real, `${rule.id}: ${key}=${v} is a real answer`);
|
|
171
|
+
}
|
|
168
172
|
}
|
|
169
173
|
}
|
|
170
174
|
});
|
|
@@ -221,6 +225,24 @@ await t('the table hides whole questions too: a new planet has no existing work,
|
|
|
221
225
|
assert.ok(!opts({ framing: 'hybrid' }).includes('services'));
|
|
222
226
|
});
|
|
223
227
|
|
|
228
|
+
await t('rewards are hidden for a founder building alone with nobody expected, and only then (task 1004419)', () => {
|
|
229
|
+
const hidden = (answers) => softLimitsFor(answers).limits.some((l) => l.option === 'reward_intents' && l.effect === 'hide');
|
|
230
|
+
assert.equal(hidden({ builders_at_start: 1, builders_expected: 1 }), true, 'solo');
|
|
231
|
+
assert.equal(hidden({ builders_at_start: 1, builders_expected: 4 }), false, 'alone now, company expected');
|
|
232
|
+
assert.equal(hidden({ builders_at_start: 3, builders_expected: 8 }), false, 'a team');
|
|
233
|
+
assert.equal(hidden({ builders_at_start: 1 }), false, 'the growth guess never answered is not "nobody"');
|
|
234
|
+
const reason = softLimitsFor({ builders_at_start: 1, builders_expected: 1 }).limits.find((l) => l.option === 'reward_intents').reason;
|
|
235
|
+
assert.match(reason, /building solo/);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await t('the voice is a stored detail: planet or professional, null to un-answer, anything else refused (spec D5)', () => {
|
|
239
|
+
assert.deepEqual(provisioning.PROJECT_DETAIL_VOCAB.voice, ['planet', 'professional']);
|
|
240
|
+
assert.equal(badProjectDetail({ voice: 'professional' }), null);
|
|
241
|
+
assert.equal(badProjectDetail({ voice: null }), null);
|
|
242
|
+
assert.match(badProjectDetail({ voice: 'pirate' }).message, /"voice" must be one of: planet, professional/);
|
|
243
|
+
assert.deepEqual(provisioning.normalizeProjectDetail({ voice: 'planet', entity: 'for-profit' }), { entity: 'for-profit', voice: 'planet' });
|
|
244
|
+
});
|
|
245
|
+
|
|
224
246
|
await t('the soft limits read back on every owner read of the project, from the stored answers', () => {
|
|
225
247
|
const view = provisioning.publicInstance({ id: 1, slug: 'kestrel', detail: provisioning.normalizeProjectDetail({ entity: 'nonprofit' }) });
|
|
226
248
|
assert.equal(view.soft_limits.enforced, false);
|
package/tests/wizard_physics.mjs
CHANGED
|
@@ -93,7 +93,8 @@ test('a builder who loops the check is slowed down, and another builder is not',
|
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
// ── the page's vocabularies are the server's ─────────────────────────────────
|
|
96
|
-
|
|
96
|
+
// questions 5 to 7 joined the same region (task 1004419, tests/wizard_physics_more.mjs)
|
|
97
|
+
const SCRIPT = HUB.slice(HUB.indexOf('/* ── planet physics, questions 1 to 7 (tasks 1004418 and 1004419, BV2.PS07 + PS11) ──'),
|
|
97
98
|
HUB.indexOf(' /* per-panel validation on Next */'));
|
|
98
99
|
|
|
99
100
|
test('the page carries the server\'s countries, regions and entity forms, exactly', () => {
|
|
@@ -115,7 +116,9 @@ test('every choice the questions offer is an answer the server accepts', () => {
|
|
|
115
116
|
const seen = [...panel.matchAll(/data-phys="([a-z_]+)" data-val="([^"]+)"/g)];
|
|
116
117
|
assert.ok(seen.length >= 20, 'the four questions\' choices');
|
|
117
118
|
for (const [, key, raw] of seen) {
|
|
118
|
-
const
|
|
119
|
+
const one = raw === 'true' ? true : raw === 'false' ? false : raw;
|
|
120
|
+
// a list question's choice is one member of the list it sends (task 1004419)
|
|
121
|
+
const value = physics.PLANET_PHYSICS_QUESTIONS[key] && physics.PLANET_PHYSICS_QUESTIONS[key].kind === 'many' ? [one] : one;
|
|
119
122
|
assert.notEqual(physics.canonicalAnswer(physics.PLANET_PHYSICS_QUESTIONS[key], value), undefined, `${key}=${raw}`);
|
|
120
123
|
}
|
|
121
124
|
for (const [, work] of panel.matchAll(/data-work="([^"]+)"/g)) {
|
|
@@ -196,7 +199,11 @@ test('Continue walks the questions and moves the wizard on only after the last',
|
|
|
196
199
|
assert.deepEqual(w.shown, [], 'no entity: still here');
|
|
197
200
|
a().entity = 'for-profit';
|
|
198
201
|
w.run('physAdvance()');
|
|
199
|
-
|
|
202
|
+
await tick(); await tick();
|
|
203
|
+
// task 1004419: questions 5 to 7 and the summary follow inside the same step
|
|
204
|
+
// (walked to the name step in tests/wizard_physics_more.mjs)
|
|
205
|
+
assert.equal(w.state.physics.q, 5, 'on to question 5');
|
|
206
|
+
assert.deepEqual(w.shown, [], 'and the wizard has not moved on');
|
|
200
207
|
});
|
|
201
208
|
|
|
202
209
|
test('a stopped description lands on the stop screen with the server\'s words', async () => {
|
|
@@ -216,7 +223,7 @@ test('the create sends the answers as detail: existing work only for an existing
|
|
|
216
223
|
Object.assign(w.state.physics.answers, { project_state: 'new', existing_work: ['code'], main_work: 'code', home_country: 'NL', builder_geography: 'global', minors_on_board: false, entity: 'intent-solo' });
|
|
217
224
|
w.state.teamShape = 'solo';
|
|
218
225
|
assert.deepEqual(JSON.parse(JSON.stringify(w.run('createDetail()'))), {
|
|
219
|
-
project_state: 'new', home_country: 'NL', builder_geography: 'global', minors_on_board: false, entity: 'intent-solo', team_shape: 'solo',
|
|
226
|
+
project_state: 'new', home_country: 'NL', builder_geography: 'global', minors_on_board: false, entity: 'intent-solo', voice: 'planet', team_shape: 'solo',
|
|
220
227
|
});
|
|
221
228
|
w.state.physics.answers = {}; w.state.teamShape = null;
|
|
222
229
|
assert.equal(w.run('createDetail()'), undefined, 'nothing answered: the detail is omitted, as before');
|