@bongos/core 1.25.31 → 1.25.32
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 +26 -36
- package/docs/copy-inventory.md +13 -13
- package/docs/copy-registry.json +14 -14
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +202 -175
- package/modules/hall-ui/public/index.html +4 -4
- package/modules/hall-ui/public/shell-game.css +2 -3
- package/modules/hall-ui/public/shell-game.js +6 -5
- package/modules/hall-ui/public/welcome-boot.css +18 -31
- package/modules/hall-ui/public/welcome-boot.js +81 -97
- package/modules/hall-ui/records/start-screen.md +6 -6
- package/modules/hall-ui/records/welcome-tower.md +29 -20
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/hall_sky_time.mjs +3 -2
- package/tests/hall_start_screen.mjs +3 -2
- package/tests/welcome_boot.mjs +35 -27
- package/tests/welcome_boot_browser.mjs +25 -19
- package/modules/hall-ui/public/welcome-day.webp +0 -0
- package/modules/hall-ui/public/welcome-night.webp +0 -0
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.25.
|
|
84
|
+
const CORE_VERSION = '1.25.32'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
package/tests/hall_sky_time.mjs
CHANGED
|
@@ -112,8 +112,9 @@ test('the sky tokens are art: no control, no text and no chrome spends one', ()
|
|
|
112
112
|
const sel = m[1].trim();
|
|
113
113
|
if (f === 'style.css' && /(^|\n):root$/.test(sel)) continue; // the token block itself
|
|
114
114
|
// only the two scenes spend them: the City page's frame, stage and cloud; Home's scene, its
|
|
115
|
-
// stage, and (in game mode) the ground of the Home view
|
|
116
|
-
|
|
115
|
+
// stage, and (in game mode) the ground of the Home view; and the cloud scene behind the welcome
|
|
116
|
+
// and the game's start screens (task 1005308)
|
|
117
|
+
if (!/^(\.city-frame|\.city-stage|\.city-cloud|\.city-stars|\.hc-svg|\.hc-cloud|\.hc-stage|\.cloud-scene|html\[data-game="on"\] body\[data-page="home"\]$)/.test(sel)) { hits.push(`${f}: ${sel}`); continue; }
|
|
117
118
|
for (const d of m[2].split(';')) {
|
|
118
119
|
const prop = d.split(':')[0].trim();
|
|
119
120
|
if (/--sky-/.test(d) && (prop === 'color' || /^border|^outline|^caret|^text-/.test(prop))) hits.push(`${f}: ${sel} { ${prop} }`);
|
|
@@ -95,13 +95,14 @@ test('the load screen fills one segment per week, up to the week it is showing (
|
|
|
95
95
|
assert.equal(G.segsDone(0, 0), 0);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
test('a fresh sign-in is handed to the start screen with the
|
|
98
|
+
test('a fresh sign-in is handed to the start screen with the city in the clouds, and the craft picker waits for the world', () => {
|
|
99
99
|
const s = sandbox({ store: { 'hall-welcome-signin': '1' } });
|
|
100
100
|
const access = { authed: true }, builder = { id: 7, display_name: 'Ada' };
|
|
101
101
|
assert.equal(s.window.OTBWelcome.onAccess(access, builder), true, 'the shell is told a screen is up, so it asks no picker');
|
|
102
102
|
assert.equal(s.started.length, 1);
|
|
103
103
|
assert.equal(s.started[0].name, 'Harbor');
|
|
104
|
-
assert.
|
|
104
|
+
assert.equal(typeof s.started[0].scene, 'function', 'the scene is painted, not a picture (task 1005308)');
|
|
105
|
+
assert.equal(s.started[0].art, undefined, 'the circuit-city painting is retired');
|
|
105
106
|
assert.equal(s.store['hall-welcome-signin'], undefined, 'the flag is spent');
|
|
106
107
|
assert.deepEqual(s.asked, [], 'nothing is asked while the start screen is up');
|
|
107
108
|
s.started[0].done();
|
package/tests/welcome_boot.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// tests/welcome_boot.mjs — the welcome's pure parts (tasks 1004785, 1004818).
|
|
2
2
|
//
|
|
3
3
|
// WHY. The welcome decides things a reader would only notice going wrong over days: whether today
|
|
4
|
-
// is the first visit, whether it is day or night
|
|
5
|
-
//
|
|
4
|
+
// is the first visit, whether it is day or night, what the banner names, where each role goes,
|
|
5
|
+
// and that the scene is the city in the clouds with its doors taken out (task 1005308). Those are pure, so this
|
|
6
6
|
// runs the REAL welcome-boot.js (it is Node-require-safe). The overlay itself is
|
|
7
7
|
// tests/welcome_boot_browser.mjs.
|
|
8
8
|
import assert from 'node:assert/strict';
|
|
@@ -39,40 +39,48 @@ test('the greeting follows the local hour, and the name falls back sensibly', ()
|
|
|
39
39
|
assert.equal(W.nameOf({}), 'builder');
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
test('day is 06:00 to 17:59 local, night the rest
|
|
42
|
+
test('day is 06:00 to 17:59 local, night the rest (the ink and the veil follow it)', () => {
|
|
43
43
|
const at = (h, m = 0) => W.timeOfDay(new Date(2026, 9, 23, h, m));
|
|
44
44
|
assert.deepEqual([at(5, 59), at(6), at(12), at(17, 59), at(18), at(23, 30), at(0)],
|
|
45
45
|
['night', 'day', 'day', 'day', 'night', 'night', 'night']);
|
|
46
|
-
assert.equal(W.artFor('day'), '/builders/welcome-day.webp');
|
|
47
|
-
assert.equal(W.artFor('night'), '/builders/welcome-night.webp');
|
|
48
46
|
});
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
assert.equal(buf.toString('latin1', 12, 16), 'VP8X', url + ' carries its size');
|
|
58
|
-
const w = 1 + buf.readUIntLE(24, 3), h = 1 + buf.readUIntLE(27, 3);
|
|
59
|
-
assert.deepEqual([w, h], [W.AW, W.AH], url + ' is the painting the lights were placed on');
|
|
48
|
+
// Task 1005308 (owner, 2026-11-14): the circuit-city painting is retired everywhere; the scene is the
|
|
49
|
+
// city in the clouds Home's own painters draw (task 1005196), as a backdrop.
|
|
50
|
+
test('the circuit-city painting is retired: no picture file, no lights pinned to its pixels', () => {
|
|
51
|
+
for (const name of ['welcome-day.webp', 'welcome-night.webp']) assert.equal(fs.existsSync(path.join(HALL, name)), false, name + ' is gone');
|
|
52
|
+
for (const gone of ['ART', 'artFor', 'TOWERS', 'LANES', 'AW', 'AH', 'pulseRuns', 'BEACONS']) assert.equal(W[gone], undefined, gone + ' went with the painting');
|
|
53
|
+
for (const file of ['welcome-boot.js', 'welcome-boot.css', 'shell-game.js', 'shell-game.css', 'index.html']) {
|
|
54
|
+
assert.doesNotMatch(fs.readFileSync(path.join(HALL, file), 'utf8'), /welcome-(day|night)\.webp|wb__img|gs__img|wb-motion/, file + ' names no part of the painting');
|
|
60
55
|
}
|
|
61
56
|
});
|
|
62
57
|
|
|
63
|
-
test(
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
58
|
+
test("the scene is the city in the clouds, drawn by Home's own painters, with every door taken out", async () => {
|
|
59
|
+
const vm = await import('node:vm');
|
|
60
|
+
const ctx = { window: {}, console };
|
|
61
|
+
vm.createContext(ctx);
|
|
62
|
+
vm.runInContext(fs.readFileSync(path.join(HALL, 'city-draw.js'), 'utf8'), ctx);
|
|
63
|
+
const Gov = ctx.window.GovCity;
|
|
64
|
+
const Draw = require(path.join(HALL, 'home-city-draw.js'));
|
|
65
|
+
assert.ok(Gov && Draw, 'the painters load');
|
|
66
|
+
const s = W.sceneShapes(Draw, Gov);
|
|
67
|
+
assert.match(s.viewBox, /^0 0 1000 \d+$/);
|
|
68
|
+
const all = [];
|
|
69
|
+
(function walk(n) { all.push(n); n.kids.forEach(walk); })({ tag: 'root', attrs: {}, kids: s.nodes });
|
|
70
|
+
assert.ok(all.some((n) => /hc-cloud/.test(n.attrs.class || '')), 'it stands in cloud');
|
|
71
|
+
assert.ok(all.filter((n) => n.tag === 'path').length > 50, 'the core and the museum are drawn');
|
|
72
|
+
assert.equal(all.filter((n) => n.tag === 'a').length, 0, 'no link: it is a backdrop');
|
|
73
|
+
for (const n of all) {
|
|
74
|
+
for (const k of ['href', 'tabindex', 'aria-label', 'data-tip', 'data-tip-body', 'data-goal']) assert.equal(n.attrs[k], undefined, k + ' is taken out');
|
|
75
|
+
if (n.attrs.id) assert.ok(n.attrs.id.startsWith(W.SCENE_ID), "its ids never meet Home's own city's");
|
|
76
|
+
for (const v of Object.values(n.attrs)) assert.doesNotMatch(String(v), /url\(#(?!cs-)/, 'every reference points at its own prefixed id');
|
|
77
|
+
assert.doesNotMatch(n.attrs.class || '', /hc-quiet/, 'nothing is faded: the core leads');
|
|
74
78
|
}
|
|
75
|
-
|
|
79
|
+
const json = JSON.stringify(s);
|
|
80
|
+
assert.doesNotMatch(json, /#[0-9a-f]{6}\b/i, "no typed colour: the hour's sky tokens only");
|
|
81
|
+
assert.match(json, /--sky-now-/, "it paints in the hour's sky");
|
|
82
|
+
assert.equal(W.sceneShapes(null, Gov), null, 'no painters: no city, and the sky stands alone');
|
|
83
|
+
assert.equal(W.paintScene(null), false);
|
|
76
84
|
});
|
|
77
85
|
|
|
78
86
|
test('the banner is the logo, named from the branding pack, never a hardcoded word (task 1004953)', () => {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// tests/welcome_boot_browser.mjs — the welcome in a real browser (tasks 1004785, 1004818).
|
|
2
2
|
//
|
|
3
3
|
// WHY. "Once a day", "Skip", "the craft picker does not also open", "Patron only with a seat", the day
|
|
4
|
-
// and night
|
|
4
|
+
// and night veils over the city in the clouds and the slow way out are behaviour across the real shell.js, craft-mode.js and
|
|
5
5
|
// welcome-boot.js together — the shell is what decides which of the two greets a builder — so this
|
|
6
|
-
// serves the REAL Home markup
|
|
6
|
+
// serves the REAL Home markup and the REAL hall scripts (the city's painters too) from a routed origin
|
|
7
7
|
// (localStorage needs one) in the ui-design kit's launcher. Every wait is on DOM state, never on a
|
|
8
8
|
// flag that flips before the DOM settles (the task 1004780 CI flake). Skips when the machine has no
|
|
9
9
|
// system Edge or Chrome, as ui_design_kit_fit does.
|
|
@@ -33,7 +33,9 @@ const INDEX = fs.readFileSync(path.join(HALL, 'index.html'), 'utf8')
|
|
|
33
33
|
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, '')
|
|
34
34
|
.replace(/\{\{[^}]*\}\}/g, '');
|
|
35
35
|
const STUB = '<script>window.__BRANDING__ = { identity: { worldName: "Harbor" } }; window.BongosClient = { createClient: () => ({ request: async () => ({ ok: false, status: 404, data: null }) }) };</script>';
|
|
36
|
-
|
|
36
|
+
// The city's three painters draw the scene (task 1005308: the city in the clouds, not a painting).
|
|
37
|
+
const PAINTERS = ['sky-time.js', 'city-draw.js', 'home-city-draw.js'];
|
|
38
|
+
const SCRIPTS = ['dom-utils.js', ...PAINTERS, 'shell-trail.js', 'craft-mode.js', 'welcome-boot.js', 'shell.js'] // dom-utils first, as on the real page (task 1004876)
|
|
37
39
|
.map((f) => `<script src="/builders/${f}"></script>`).join('');
|
|
38
40
|
const PAGE = INDEX.replace('</body>', STUB + SCRIPTS + '</body>');
|
|
39
41
|
|
|
@@ -46,7 +48,7 @@ async function hall({ art = true, reducedMotion = 'no-preference', time = null }
|
|
|
46
48
|
if (p === '/api/bongos/auth/web/start') return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>github</title><p id="auth">sign in</p>' });
|
|
47
49
|
if (p === '/studio' || p === '/city') return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>room</title><p id="room">' + p + '</p>' });
|
|
48
50
|
const ext = path.extname(p);
|
|
49
|
-
if (
|
|
51
|
+
if (!art && PAINTERS.includes(path.basename(p))) return route.fulfill({ status: 404, body: '' });
|
|
50
52
|
const f = path.join(HALL, p.replace(/^\/builders\//, ''));
|
|
51
53
|
if (p.startsWith('/builders/') && TYPES[ext] && fs.existsSync(f)) return route.fulfill({ contentType: TYPES[ext], body: fs.readFileSync(f) });
|
|
52
54
|
return route.fulfill({ status: 404, body: '' });
|
|
@@ -91,8 +93,8 @@ test('first visit of the day: the welcome, not the craft picker; Skip brings the
|
|
|
91
93
|
'the welcome shows, the picker is marked asked and does not stack on it');
|
|
92
94
|
assert.equal(await page.evaluate(() => document.querySelector('.wb__banner').tagName), 'DIV',
|
|
93
95
|
'over a signed-in hall the shell keeps the one <header>; the banner is a plain block (task 1005080)');
|
|
94
|
-
// The
|
|
95
|
-
await page.
|
|
96
|
+
// The city in the clouds is drawn behind it (task 1005308).
|
|
97
|
+
await page.waitForSelector('#welcome-boot .cloud-scene .cloud-scene__svg path');
|
|
96
98
|
assert.equal(await page.isVisible('[data-wb-skip]'), true);
|
|
97
99
|
await page.click('[data-wb-skip]');
|
|
98
100
|
await done(page);
|
|
@@ -227,17 +229,18 @@ test('roles: a seat holder sees Patron; Artisan stays on Home in Artisan', { ski
|
|
|
227
229
|
await ctx.close();
|
|
228
230
|
});
|
|
229
231
|
|
|
230
|
-
test('no
|
|
232
|
+
test('no city painters: the greeting and the choice still come up, over the sky colour', { skip }, async () => {
|
|
231
233
|
const ctx = await hall({ art: false });
|
|
232
234
|
const { page } = await open(ctx);
|
|
233
235
|
await signIn(page);
|
|
234
236
|
await done(page);
|
|
235
237
|
const s = await page.evaluate(() => ({
|
|
236
238
|
bare: document.getElementById('welcome-boot').classList.contains('wb--bare'),
|
|
237
|
-
|
|
239
|
+
city: !!document.querySelector('#welcome-boot .cloud-scene__svg'),
|
|
240
|
+
sky: !!document.querySelector('#welcome-boot .cloud-scene'),
|
|
238
241
|
choices: document.querySelectorAll('.wb__choice').length,
|
|
239
242
|
}));
|
|
240
|
-
assert.deepEqual(s, { bare: true,
|
|
243
|
+
assert.deepEqual(s, { bare: true, city: false, sky: true, choices: 2 });
|
|
241
244
|
await ctx.close();
|
|
242
245
|
});
|
|
243
246
|
|
|
@@ -256,15 +259,16 @@ const look = (page) => page.evaluate(() => {
|
|
|
256
259
|
return {
|
|
257
260
|
day: r.classList.contains('wb--day'), night: r.classList.contains('wb--night'),
|
|
258
261
|
veil: getComputedStyle(r.querySelector('.wb__veil')).backgroundColor,
|
|
259
|
-
|
|
260
|
-
|
|
262
|
+
city: !!r.querySelector('.cloud-scene__svg'),
|
|
263
|
+
hour: document.documentElement.getAttribute('data-sky-hour'),
|
|
264
|
+
img: r.querySelectorAll('img:not(.wb__logo)').length,
|
|
261
265
|
};
|
|
262
266
|
});
|
|
263
267
|
|
|
264
|
-
test('by day the
|
|
268
|
+
test('by day the city opens from a white veil, by night from black, in the sky of that hour (task 1005308)', { skip }, async () => {
|
|
265
269
|
for (const [time, want] of [
|
|
266
|
-
[new Date(2026, 9, 23, 10, 0), { day: true, night: false, veil: 'rgb(255, 255, 255)',
|
|
267
|
-
[new Date(2026, 9, 23, 22, 0), { day: false, night: true, veil: 'rgb(0, 0, 0)',
|
|
270
|
+
[new Date(2026, 9, 23, 10, 0), { day: true, night: false, veil: 'rgb(255, 255, 255)', city: true, hour: '10', img: 0 }],
|
|
271
|
+
[new Date(2026, 9, 23, 22, 0), { day: false, night: true, veil: 'rgb(0, 0, 0)', city: true, hour: '22', img: 0 }],
|
|
268
272
|
]) {
|
|
269
273
|
const ctx = await hall({ time });
|
|
270
274
|
const { page } = await open(ctx);
|
|
@@ -305,19 +309,21 @@ test('the banner is the animated logo, the motion is laid on, and the drawn towe
|
|
|
305
309
|
const ctx = await hall();
|
|
306
310
|
const { page } = await open(ctx);
|
|
307
311
|
await signIn(page);
|
|
308
|
-
await page.waitForSelector('#welcome-boot.wb--lit .
|
|
312
|
+
await page.waitForSelector('#welcome-boot.wb--lit .cloud-scene__svg');
|
|
309
313
|
const s = await page.evaluate(() => ({
|
|
310
314
|
banner: document.querySelector('.wb__banner').textContent,
|
|
311
315
|
name: document.querySelector('.wb__banner .wb__name').textContent,
|
|
312
316
|
mark: !!document.querySelector('.wb__banner svg.wb__mark .wb__moonlet'),
|
|
313
317
|
ringAnim: getComputedStyle(document.querySelector('.wb__ring')).animationName,
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
318
|
+
// anywhere on the page, under any container: the painting's lights must not come back
|
|
319
|
+
pulses: document.querySelectorAll('.wb-pulse, .wb-pulses').length,
|
|
320
|
+
cars: document.querySelectorAll('.wb-car').length,
|
|
321
|
+
// every earlier scene: the drawn tower and city, the painting and its light layer
|
|
322
|
+
old: document.querySelectorAll('.wb-floor, .wb-bldg, .wb-person, #wb-beam, #wb-city, #wb-fx, .wb-motion, .wb-moon, .wb-star, .wb__img').length,
|
|
317
323
|
}));
|
|
318
324
|
assert.doesNotMatch(s.banner, /Working on the core of/, 'task 1004953: the banner text is gone');
|
|
319
325
|
assert.deepEqual({ name: s.name, mark: s.mark, ringAnim: s.ringAnim, pulses: s.pulses, cars: s.cars, old: s.old },
|
|
320
|
-
{ name: 'Harbor', mark: true, ringAnim: 'wb-ring', pulses:
|
|
326
|
+
{ name: 'Harbor', mark: true, ringAnim: 'wb-ring', pulses: 0, cars: 0, old: 0 }, 'the painting\'s lights went with it (task 1005308)');
|
|
321
327
|
await ctx.close();
|
|
322
328
|
});
|
|
323
329
|
|
|
Binary file
|
|
Binary file
|