@bongos/core 1.25.3 → 1.25.4
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 +36 -31
- package/docs/copy-inventory.md +17 -18
- package/docs/copy-registry.json +44 -43
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +43 -43
- package/modules/hall-ui/public/builders.js +3 -27
- package/modules/hall-ui/public/hall-render.js +5 -119
- package/modules/hall-ui/public/hall-widgets.js +3 -3
- package/modules/hall-ui/public/index.html +0 -7
- package/modules/hall-ui/public/shell-game.js +22 -4
- package/modules/hall-ui/public/style.css +3 -73
- package/modules/hall-ui/public/visor.css +31 -1
- package/modules/hall-ui/public/visor.js +53 -3
- package/modules/hall-ui/records/home-reshuffle.md +1 -1
- package/modules/hall-ui/records/visor.md +10 -0
- 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_board_world.mjs +7 -35
- package/tests/hall_home_ships_bounded.mjs +11 -27
- package/tests/hall_landing_boot.mjs +7 -8
- package/tests/hall_nav.mjs +2 -2
- package/tests/hall_visor.mjs +9 -2
- package/tests/hall_visor_profile.mjs +150 -0
- package/tests/leaderboard_roles.mjs +3 -17
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// tests/hall_visor_profile.mjs — you on the visor (task 1005249).
|
|
2
|
+
//
|
|
3
|
+
// The owner's ask (2026-11-12): the Standing chip that sat top right of Home is gone;
|
|
4
|
+
// the visor's bottom-left you-block shows your details on hover and opens your whole
|
|
5
|
+
// profile, in the game's window over the place, on click. This runs the real visor,
|
|
6
|
+
// the real shell-game window and the real dialog in a browser, plus the pure facts in
|
|
7
|
+
// a vm, and checks Home's page no longer carries the chip.
|
|
8
|
+
//
|
|
9
|
+
// Run: node --test tests/hall_visor_profile.mjs
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { test, after } from 'node:test';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import vm from 'node:vm';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const HALL = path.join(ROOT, 'modules/hall-ui/public');
|
|
21
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
22
|
+
|
|
23
|
+
// ---------- the facts, in a vm ----------
|
|
24
|
+
function loadVisor(me) {
|
|
25
|
+
const doc = {
|
|
26
|
+
readyState: 'complete', body: null, documentElement: { dataset: {}, classList: { contains: () => false, toggle() {}, remove() {} } },
|
|
27
|
+
addEventListener() {}, querySelector: () => null, createElement: () => ({}),
|
|
28
|
+
};
|
|
29
|
+
const win = { document: doc, location: { pathname: '/', assign() {} }, addEventListener() {}, OTBCraftMode: { me: () => me } };
|
|
30
|
+
win.window = win;
|
|
31
|
+
vm.runInNewContext(read('visor.js'), win);
|
|
32
|
+
return win.OTBVisor;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test('the card\'s facts come from /me: name, handle, rank, credits, karma, preferred work', () => {
|
|
36
|
+
const me = { builder: { display_name: 'Percy Jackson', github_login: 'percy', rank: 'metic', total_credits: 14441, karma: 96.4, preferred_disciplines: ['artisan', 'ui'] } };
|
|
37
|
+
const f = JSON.parse(JSON.stringify(loadVisor(me).meFacts(me)));
|
|
38
|
+
assert.deepEqual(f, { name: 'Percy Jackson', handle: '@percy', rank: 'Metic', credits: 14441, karma: 96, prefs: 'artisan, ui' });
|
|
39
|
+
const bare = JSON.parse(JSON.stringify(loadVisor(null).meFacts({ builder: { github_login: 'x' } })));
|
|
40
|
+
assert.equal(bare.name, 'x', 'no display name: the login');
|
|
41
|
+
assert.equal(bare.karma, 0);
|
|
42
|
+
assert.equal(bare.prefs, '', 'no preference: empty, and the card says "Not set yet"');
|
|
43
|
+
assert.equal(loadVisor(null).meFacts(null), null, 'before /me answers there is nothing to show');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('Home carries no Standing chip any more, and no sheet styles one', () => {
|
|
47
|
+
assert.doesNotMatch(read('index.html'), /hall-standing|home-standing-body/);
|
|
48
|
+
assert.doesNotMatch(read('style.css'), /\.hall-standing|\.home-who|\.home-facts/);
|
|
49
|
+
assert.doesNotMatch(read('hall-render.js'), /home-standing-body|hall-standing|leaderboardPlace/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ---------- in a browser ----------
|
|
53
|
+
const lib = require(path.join(ROOT, 'modules', 'ui-design', 'kit', 'lib.js'));
|
|
54
|
+
let browser = null;
|
|
55
|
+
try { browser = await lib.launchBrowser(); } catch (e) { if (!(e instanceof lib.NoBrowserError)) throw e; }
|
|
56
|
+
after(() => browser && browser.close());
|
|
57
|
+
const skip = !browser && 'no system Edge or Chrome';
|
|
58
|
+
|
|
59
|
+
const ORIGIN = 'http://hall.test';
|
|
60
|
+
const TYPES = { '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png', '.svg': 'image/svg+xml' };
|
|
61
|
+
const SCENE = `<script>
|
|
62
|
+
window.OTBCraftMode = { character() { return 'artisan'; }, current() { return null; },
|
|
63
|
+
me() { return { builder: { display_name: 'Percy Jackson', github_login: 'percy', rank: 'metic', total_credits: 14441, karma: 96, preferred_disciplines: [] } }; },
|
|
64
|
+
spriteSrc(id) { return '/builders/characters/' + id + '.png'; } };
|
|
65
|
+
window.__visorScene = { name: 'test', place: 'Test city', actions: [], keys: [] };
|
|
66
|
+
</script>`;
|
|
67
|
+
const PAGE = (withWindow) => `<!doctype html><html data-theme="dark" data-faces="helm"><head><meta charset="utf-8">
|
|
68
|
+
<link rel="stylesheet" href="/builders/style.css"><link rel="stylesheet" href="/builders/visor.css"></head>
|
|
69
|
+
<body>${SCENE}<script src="/builders/hall-dialog.js"></script>${withWindow ? '<script src="/builders/shell-game.js"></script>' : ''}<script src="/builders/visor.js"></script></body></html>`;
|
|
70
|
+
|
|
71
|
+
async function open({ withWindow = true } = {}) {
|
|
72
|
+
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
|
73
|
+
await ctx.route(ORIGIN + '/**', (route) => {
|
|
74
|
+
const p = new URL(route.request().url()).pathname;
|
|
75
|
+
if (p === '/') return route.fulfill({ contentType: 'text/html', body: PAGE(withWindow) });
|
|
76
|
+
if (p === '/profile') return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>profile</title><p id="pp">your profile</p><script src="/builders/shell-game.js"></script>' }); // the real page carries the game shell, which hands an Escape up
|
|
77
|
+
const f = path.join(HALL, p.replace(/^\/builders\//, ''));
|
|
78
|
+
if (p.startsWith('/builders/') && TYPES[path.extname(p)] && fs.existsSync(f)) return route.fulfill({ contentType: TYPES[path.extname(p)], body: fs.readFileSync(f) });
|
|
79
|
+
return route.fulfill({ status: 404, body: '' });
|
|
80
|
+
});
|
|
81
|
+
const page = await ctx.newPage();
|
|
82
|
+
const errors = [];
|
|
83
|
+
page.on('pageerror', (e) => errors.push(String(e.message)));
|
|
84
|
+
await page.addInitScript(() => { try { localStorage.setItem('hall-game', 'on'); document.documentElement.dataset.game = 'on'; window.OTB = { escapeHtml: (s) => String(s) }; } catch (_) { /* none */ } });
|
|
85
|
+
await page.goto(ORIGIN + '/');
|
|
86
|
+
await page.waitForFunction(() => !!document.querySelector('.visor__me'));
|
|
87
|
+
return { ctx, page, errors };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
test('hovering you on the visor shows your details; at rest they are hidden', { skip }, async () => {
|
|
91
|
+
const { ctx, page, errors } = await open();
|
|
92
|
+
const shown = () => page.evaluate(() => getComputedStyle(document.querySelector('.visor__card')).visibility);
|
|
93
|
+
assert.equal(await shown(), 'hidden', 'nothing over the scene at rest');
|
|
94
|
+
await page.hover('.visor__me');
|
|
95
|
+
await page.waitForFunction(() => getComputedStyle(document.querySelector('.visor__card')).opacity === '1');
|
|
96
|
+
assert.equal(await shown(), 'visible');
|
|
97
|
+
const text = await page.evaluate(() => [...document.querySelectorAll('.visor__card > span')].map((s) => s.innerText.replace(/\s+/g, ' ').trim()));
|
|
98
|
+
assert.match(text[0], /Percy Jackson @percy/i);
|
|
99
|
+
assert.match(text[1], /Rank Metic/i);
|
|
100
|
+
assert.match(text[1], /Credits 14,441/i);
|
|
101
|
+
assert.match(text[1], /Karma 96/i);
|
|
102
|
+
assert.match(text[1], /Preferred work Not set yet/i);
|
|
103
|
+
assert.match(text[2], /Open your profile/i);
|
|
104
|
+
const box = await page.evaluate(() => { const r = document.querySelector('.visor__card').getBoundingClientRect(); return { top: r.top, left: r.left }; });
|
|
105
|
+
assert.ok(box.top >= 0 && box.left >= 0, 'the card stays on screen');
|
|
106
|
+
assert.deepEqual(errors, []);
|
|
107
|
+
await ctx.close();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('clicking you opens your whole profile in the window over the place; Escape closes it', { skip }, async () => {
|
|
111
|
+
const { ctx, page, errors } = await open();
|
|
112
|
+
await page.click('.visor__me');
|
|
113
|
+
await page.waitForFunction(() => window.OTBOptionsOverlay.isOpen());
|
|
114
|
+
const w = await page.evaluate(() => ({
|
|
115
|
+
url: location.pathname,
|
|
116
|
+
src: document.querySelector('.opt-window__frame').getAttribute('src'),
|
|
117
|
+
label: document.querySelector('.opt-window__box').getAttribute('aria-label'),
|
|
118
|
+
close: document.querySelector('.opt-window__x').getAttribute('aria-label'),
|
|
119
|
+
}));
|
|
120
|
+
assert.equal(w.url, '/', 'the place stays: no navigation');
|
|
121
|
+
assert.equal(w.src, '/profile?embed=1');
|
|
122
|
+
assert.equal(w.label, 'Your profile');
|
|
123
|
+
assert.equal(w.close, 'Close your profile');
|
|
124
|
+
// the frame's own shell must be up before an Escape inside it can be handed up
|
|
125
|
+
await page.waitForFunction(() => { const f = document.querySelector('.opt-window__frame'); try { return !!(f && f.contentDocument.readyState === 'complete' && f.contentDocument.documentElement.dataset.embed === 'on'); } catch (_) { return false; } });
|
|
126
|
+
await page.keyboard.press('Escape');
|
|
127
|
+
await page.waitForFunction(() => !window.OTBOptionsOverlay.isOpen());
|
|
128
|
+
// the same window still opens Options under its own name
|
|
129
|
+
await page.evaluate(() => window.OTBOptionsOverlay.open(''));
|
|
130
|
+
assert.equal(await page.evaluate(() => document.querySelector('.opt-window__box').getAttribute('aria-label')), 'Options');
|
|
131
|
+
assert.equal(await page.evaluate(() => document.querySelector('.opt-window__frame').getAttribute('src')), '/settings?embed=1');
|
|
132
|
+
assert.deepEqual(errors, []);
|
|
133
|
+
await ctx.close();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('with no window to draw, you are still a link to your profile', { skip }, async () => {
|
|
137
|
+
const { ctx, page, errors } = await open({ withWindow: false });
|
|
138
|
+
const r = await page.evaluate(() => {
|
|
139
|
+
const me = document.querySelector('.visor__me');
|
|
140
|
+
const ev = new MouseEvent('click', { bubbles: true, cancelable: true });
|
|
141
|
+
// stop the browser before it leaves, once the visor has had its say
|
|
142
|
+
window.addEventListener('click', (e) => { window.__left = !e.defaultPrevented; e.preventDefault(); });
|
|
143
|
+
me.querySelector('.visor__name').dispatchEvent(ev);
|
|
144
|
+
return { href: me.getAttribute('href'), followed: window.__left };
|
|
145
|
+
});
|
|
146
|
+
assert.equal(r.href, '/profile');
|
|
147
|
+
assert.equal(r.followed, true, 'the visor lets the link take you to the profile page');
|
|
148
|
+
assert.deepEqual(errors, []);
|
|
149
|
+
await ctx.close();
|
|
150
|
+
});
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// that craft earned, and a dash, not a 0, for a builder with no AI sessions.
|
|
17
17
|
// (Two crafts since task 1005141: the engineer is the artisan, and the
|
|
18
18
|
// ideator folded into the artist, so idea credits rank as artist credits.)
|
|
19
|
-
//
|
|
19
|
+
// 5. THE STATUS PAGE: the real function, run. (4, the Standing chip, went in task 1005249.)
|
|
20
20
|
|
|
21
21
|
import { strict as assert } from 'node:assert';
|
|
22
22
|
import { createRequire } from 'node:module';
|
|
@@ -234,7 +234,7 @@ await test('the Artists toggle shows only artists, ranked by the credits that cr
|
|
|
234
234
|
assert.equal(r.rows.length, FIX.leaderboard.length, 'Everyone brings every builder back');
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
-
// ---- 4. the standing card
|
|
237
|
+
// ---- 4. (the standing card, gone; extract() serves section 5) ---------------------
|
|
238
238
|
|
|
239
239
|
function extract(src, start, end) {
|
|
240
240
|
const a = src.indexOf(start);
|
|
@@ -243,21 +243,7 @@ function extract(src, start, end) {
|
|
|
243
243
|
return src.slice(a, b);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
|
|
247
|
-
const src = read('modules/hall-ui/public/hall-render.js');
|
|
248
|
-
const code = extract(src, 'const lbState = { rows: [] };', ' return { lbState');
|
|
249
|
-
const sandbox = { rows: FIX.leaderboard };
|
|
250
|
-
vm.createContext(sandbox);
|
|
251
|
-
vm.runInContext(`${code}; lbState.rows = rows; this.place = leaderboardPlace; this.text = rolePlaceText;`, sandbox);
|
|
252
|
-
const art = sandbox.place('builder-7');
|
|
253
|
-
assert.equal(art.role.craft, 'artist');
|
|
254
|
-
assert.equal(sandbox.text(art), `#2 as an artist`, 'fourth overall, second among artists: something new, so it is said');
|
|
255
|
-
const top = sandbox.place(FIX.leaderboard[0].github_login);
|
|
256
|
-
assert.equal(sandbox.text(top), '', 'the top artisan is #1 either way, so nothing is added');
|
|
257
|
-
// task 1005054: the stats strip (and its rank tile) left Home; the Standing chip's card carries the role place
|
|
258
|
-
assert.match(src, /\$\{rolePlaceText\(rollPlace\) \? `<span title="Your place among the/);
|
|
259
|
-
assert.match(read('modules/hall-ui/public/builders.js'), /getJSON\(`\$\{API\}\/public\/leaderboard`\)/, 'the hall reads the list with the roles');
|
|
260
|
-
});
|
|
246
|
+
// (Its test of the Standing chip's role place went with the chip in task 1005249.)
|
|
261
247
|
|
|
262
248
|
// ---- 5. the status page -----------------------------------------------------------
|
|
263
249
|
|