@bongos/core 1.24.80 → 1.24.82
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 +154 -124
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md +142 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +86 -2
- package/docs/api-reference.md +4 -2
- package/docs/copy-inventory.md +51 -50
- package/docs/copy-registry.json +121 -123
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +2 -3
- package/docs/page-readings.json +226 -220
- package/modules/builder-settings/hall-view-prefs.js +50 -0
- package/modules/builder-settings/routes/settings.js +32 -0
- package/modules/hall-ui/public/ai-studio.html +5 -3
- package/modules/hall-ui/public/ai-studio.states.json +1 -0
- package/modules/hall-ui/public/atlas.html +3 -3
- package/modules/hall-ui/public/atlas.states.json +1 -0
- package/modules/hall-ui/public/blockers.html +3 -3
- package/modules/hall-ui/public/board-room.html +3 -3
- package/modules/hall-ui/public/board-room.states.json +1 -0
- package/modules/hall-ui/public/city.css +1 -3
- package/modules/hall-ui/public/city.html +6 -9
- package/modules/hall-ui/public/city.js +10 -55
- package/modules/hall-ui/public/city.states.json +7 -32
- package/modules/hall-ui/public/collab.html +3 -3
- package/modules/hall-ui/public/collab.states.json +1 -0
- package/modules/hall-ui/public/commands.html +3 -3
- package/modules/hall-ui/public/commands.states.json +1 -0
- package/modules/hall-ui/public/controls.css +63 -11
- package/modules/hall-ui/public/copy-desk.html +3 -3
- package/modules/hall-ui/public/copy-desk.states.json +1 -0
- package/modules/hall-ui/public/deploy.html +3 -3
- package/modules/hall-ui/public/deploy.states.json +1 -0
- package/modules/hall-ui/public/goal-map.html +4 -4
- package/modules/hall-ui/public/goals.html +3 -3
- package/modules/hall-ui/public/goals.states.json +1 -0
- package/modules/hall-ui/public/government.html +3 -3
- package/modules/hall-ui/public/government.states.json +1 -0
- package/modules/hall-ui/public/idea.html +3 -3
- package/modules/hall-ui/public/idea.states.json +1 -0
- package/modules/hall-ui/public/ideas.html +3 -3
- package/modules/hall-ui/public/ideas.states.json +1 -0
- package/modules/hall-ui/public/index-founding.states.json +1 -0
- package/modules/hall-ui/public/index-genesis-decide.states.json +1 -0
- package/modules/hall-ui/public/index-genesis-demo.states.json +1 -0
- package/modules/hall-ui/public/index-genesis.states.json +1 -0
- package/modules/hall-ui/public/index.html +5 -5
- package/modules/hall-ui/public/index.states.json +1 -0
- package/modules/hall-ui/public/market-watch-item.html +3 -3
- package/modules/hall-ui/public/market-watch-item.states.json +1 -0
- package/modules/hall-ui/public/market-watch-rules.html +3 -3
- package/modules/hall-ui/public/market-watch-rules.states.json +1 -0
- package/modules/hall-ui/public/market-watch.html +3 -3
- package/modules/hall-ui/public/market-watch.states.json +1 -0
- package/modules/hall-ui/public/modules.html +3 -3
- package/modules/hall-ui/public/modules.states.json +1 -0
- package/modules/hall-ui/public/money.html +3 -3
- package/modules/hall-ui/public/money.states.json +1 -0
- package/modules/hall-ui/public/my-agents.html +3 -3
- package/modules/hall-ui/public/my-agents.states.json +1 -0
- package/modules/hall-ui/public/options-menu.js +2 -1
- package/modules/hall-ui/public/primer.html +3 -3
- package/modules/hall-ui/public/primer.states.json +1 -0
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/profile.states.json +1 -0
- package/modules/hall-ui/public/project-settings.html +3 -3
- package/modules/hall-ui/public/roster.html +3 -3
- package/modules/hall-ui/public/roster.states.json +1 -0
- package/modules/hall-ui/public/settings-view.js +75 -0
- package/modules/hall-ui/public/settings.html +17 -4
- package/modules/hall-ui/public/settings.states.json +1 -0
- package/modules/hall-ui/public/shell-game.css +97 -42
- package/modules/hall-ui/public/shell-game.js +138 -40
- package/modules/hall-ui/public/shell.js +12 -43
- package/modules/hall-ui/public/street-card.states.json +1 -0
- package/modules/hall-ui/public/street.css +1 -5
- package/modules/hall-ui/public/street.html +5 -9
- package/modules/hall-ui/public/street.js +8 -59
- package/modules/hall-ui/public/street.states.json +7 -32
- package/modules/hall-ui/public/studio.html +3 -3
- package/modules/hall-ui/public/studio.states.json +1 -0
- package/modules/hall-ui/public/style.css +126 -65
- package/modules/hall-ui/public/task.html +3 -3
- package/modules/hall-ui/public/task.states.json +1 -0
- package/modules/hall-ui/public/thinking.html +4 -4
- package/modules/hall-ui/public/thinking.states.json +1 -0
- package/modules/hall-ui/public/tweak-editor.html +3 -3
- package/modules/hall-ui/public/tweak-editor.states.json +1 -0
- package/modules/hall-ui/public/visor.js +7 -1
- package/modules/hall-ui/public/watch.html +3 -3
- package/modules/hall-ui/public/watch.states.json +1 -0
- package/modules/hall-ui/public/work-founding.states.json +1 -0
- package/modules/hall-ui/public/work.html +3 -3
- package/modules/hall-ui/public/work.states.json +1 -0
- package/modules/hall-ui/records/game-mode.md +5 -4
- package/modules/hall-ui/records/start-screen.md +10 -2
- package/modules/hall-ui/specimens/components.states.json +2 -1
- package/modules/ui-design/public/brand.html +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/hall-preview/README.md +2 -3
- package/scripts/hall-preview/pref-writes.js +8 -1
- package/src/bongos/serve-internal.js +7 -2
- package/src/hall-helm.js +79 -0
- package/src/module-api.js +1 -1
- package/tests/design_reference.mjs +1 -1
- package/tests/hall_builders_page_pass.mjs +2 -1
- package/tests/hall_city.mjs +15 -29
- package/tests/hall_controls.mjs +52 -4
- package/tests/hall_fullscreen.mjs +42 -21
- package/tests/hall_helm.mjs +187 -0
- package/tests/hall_record_world.mjs +4 -2
- package/tests/hall_start_screen.mjs +103 -5
- package/tests/hall_street.mjs +13 -26
- package/tests/hall_topbar_320.mjs +4 -2
- package/tests/hall_view_prefs.mjs +42 -0
- package/tests/hall_visor.mjs +3 -2
- package/tests/nav_permission_atoms.mjs +1 -1
|
@@ -94,7 +94,8 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
|
|
|
94
94
|
get cookie() { return ''; }, set cookie(_v) {},
|
|
95
95
|
};
|
|
96
96
|
const assigned = []; // task 1005144: where the page sent the browser (game mode's Escape opens the menu)
|
|
97
|
-
const
|
|
97
|
+
const replaced = []; // task 1005200: a classic page in game mode sends the browser to the city
|
|
98
|
+
const location = { hostname: 'h', origin: 'https://h', href: 'https://h/' + page + search, pathname: path || '/' + page, hash, search, assign(u) { assigned.push(u); }, replace(u) { replaced.push(u); } };
|
|
98
99
|
// `session` is the TAB's sessionStorage: pass one object to several boots and
|
|
99
100
|
// they are page loads in one tab (the trail, task 1004476).
|
|
100
101
|
const sessionStorage = {
|
|
@@ -135,6 +136,7 @@ function boot({ page = 'thinking', optIn = true, store = {}, storageThrows = fal
|
|
|
135
136
|
return {
|
|
136
137
|
store,
|
|
137
138
|
assigned,
|
|
139
|
+
replaced,
|
|
138
140
|
game: () => windowObj.OTBShellGame,
|
|
139
141
|
gameStamp: () => root.dataset.game === 'on',
|
|
140
142
|
// The "how to leave" line: showing = the shell created it and it is not hidden.
|
|
@@ -517,17 +519,36 @@ test('game mode: every hall page carries the switch — none opts out, and each
|
|
|
517
519
|
}
|
|
518
520
|
});
|
|
519
521
|
|
|
520
|
-
|
|
522
|
+
// task 1005200 (owner, 2026-11-11): game OR classic, never both. Inside game mode there is
|
|
523
|
+
// no way back to the bars — the corner button is hidden (style.css) and, pressed
|
|
524
|
+
// anyway, does nothing; Escape is the options menu's, where the view is switched.
|
|
525
|
+
test('game mode: there is no way back to the bars inside it, and a page\'s own memory is never rewritten', () => {
|
|
521
526
|
const store = { 'hall-game': 'on', 'hall-fullscreen:thinking': 'on' };
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
assert.equal(
|
|
525
|
-
assert.equal(store['hall-fullscreen:
|
|
526
|
-
assert.equal(boot({ page: 'goals', optIn: false, store }).on(), true, 'the next load opens without bars again');
|
|
527
|
+
const profile = boot({ page: 'profile', optIn: false, store });
|
|
528
|
+
profile.button().click();
|
|
529
|
+
assert.equal(profile.on(), true, 'the corner button does not leave game mode');
|
|
530
|
+
assert.equal(store['hall-fullscreen:profile'], undefined, 'nothing remembered for the page');
|
|
527
531
|
const sky = boot({ page: 'thinking', store });
|
|
528
532
|
sky.button().click();
|
|
529
|
-
|
|
533
|
+
sky.press('Escape');
|
|
534
|
+
assert.equal(sky.on(), true, 'nor does Escape');
|
|
530
535
|
assert.equal(store['hall-fullscreen:thinking'], 'on', 'the sky\'s own choice survives game mode');
|
|
536
|
+
assert.match(read('style.css'), /html\[data-game="on"\] \.fullscreen-toggle \{ display: none; \}/, 'and the button is not drawn');
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test('game mode: only the game\'s places are there — a classic page sends the browser to the city', () => {
|
|
540
|
+
const store = { 'hall-game': 'on' };
|
|
541
|
+
assert.deepEqual(boot({ page: 'goals', optIn: false, store }).replaced, ['/'], 'Goals is a classic page');
|
|
542
|
+
assert.deepEqual(boot({ page: 'work', optIn: false, store, path: '/builders/work' }).replaced, ['/builders/'], 'on the apex too');
|
|
543
|
+
for (const page of ['home', 'city', 'goal-map', 'thinking', 'atlas', 'settings', 'profile']) {
|
|
544
|
+
assert.deepEqual(boot({ page, optIn: false, store }).replaced, [], `${page} is a game place`);
|
|
545
|
+
}
|
|
546
|
+
assert.deepEqual(boot({ page: 'goals', optIn: false, store: {} }).replaced, [], 'classic reaches every page');
|
|
547
|
+
const G = boot({ page: 'city', store }).game();
|
|
548
|
+
assert.equal(G.isPlace('/city'), true);
|
|
549
|
+
assert.equal(G.isPlace('/builders/'), true);
|
|
550
|
+
assert.equal(G.isPlace('/work'), false);
|
|
551
|
+
assert.equal(G.isPlace('/blockers?x=1'), false);
|
|
531
552
|
});
|
|
532
553
|
|
|
533
554
|
// task 1005144 (BONGOS-V3 R04, ADR 0399 §2.4 row 6): in game mode the last Escape is the
|
|
@@ -562,35 +583,35 @@ test('game mode: the Escape hand-off is unchanged — a press the page used, or
|
|
|
562
583
|
|
|
563
584
|
test('game mode: set() switches the page live, and turning it off hands each page back its own memory', () => {
|
|
564
585
|
const store = { 'hall-fullscreen:thinking': 'on' };
|
|
565
|
-
const goals = boot({ page: '
|
|
586
|
+
const goals = boot({ page: 'profile', optIn: false, store });
|
|
566
587
|
goals.game().set(true);
|
|
567
588
|
assert.equal(store['hall-game'], 'on');
|
|
568
589
|
assert.equal(goals.on(), true, 'on at once, no reload');
|
|
569
590
|
assert.ok(goals.resizes() >= 1, 'a page that sizes to its box is told to re-measure');
|
|
570
591
|
goals.game().set(false);
|
|
571
|
-
assert.equal(goals.on(), false, '
|
|
592
|
+
assert.equal(goals.on(), false, 'the profile remembered nothing, so its bars return');
|
|
572
593
|
assert.equal(goals.gameStamp(), false);
|
|
573
|
-
assert.equal(store['hall-fullscreen:
|
|
594
|
+
assert.equal(store['hall-fullscreen:profile'], undefined, 'and switching the mode wrote nothing for the page');
|
|
595
|
+
const work = boot({ page: 'work', optIn: false, store: {} });
|
|
596
|
+
work.game().set(true);
|
|
597
|
+
assert.deepEqual(work.replaced, ['/'], 'switched to game on a classic page: to the city');
|
|
574
598
|
const sky = boot({ page: 'thinking', store: Object.assign(store, { 'hall-game': 'on' }) });
|
|
575
599
|
sky.game().set(false);
|
|
576
600
|
assert.equal(sky.on(), true, 'the sky had chosen full screen itself, and keeps it');
|
|
577
601
|
});
|
|
578
602
|
|
|
579
|
-
test('game mode:
|
|
603
|
+
test('game mode: the address does not switch it — Options and the sign-in chooser do (task 1005200)', () => {
|
|
580
604
|
const store = {};
|
|
581
|
-
assert.equal(boot({ page: '
|
|
582
|
-
assert.equal(store['hall-game'], '
|
|
583
|
-
assert.
|
|
584
|
-
assert.equal(boot({ page: 'work', optIn: false, store, search: '?game=off' }).on(), false);
|
|
585
|
-
boot({ page: 'work', optIn: false, store, search: '?game=%22%3E%3Cimg%3E' });
|
|
586
|
-
assert.equal(store['hall-game'], 'off', 'a value that is not on/off is not stored');
|
|
605
|
+
assert.equal(boot({ page: 'profile', optIn: false, store, search: '?game=on' }).on(), false);
|
|
606
|
+
assert.equal(store['hall-game'], undefined, 'nothing stored from the URL');
|
|
607
|
+
assert.doesNotMatch(read('shell-game.js'), /get\('game'\)/, 'the ?game= reader is gone');
|
|
587
608
|
});
|
|
588
609
|
|
|
589
|
-
test('game mode
|
|
610
|
+
test('game mode never says how to leave full screen (there is no leaving); outside it the shell\'s rule stands', () => {
|
|
590
611
|
const store = { 'hall-game': 'on' };
|
|
591
612
|
const session = {};
|
|
592
|
-
assert.equal(boot({ page: '
|
|
593
|
-
assert.equal(boot({ page: '
|
|
613
|
+
assert.equal(boot({ page: 'profile', optIn: false, store, session }).hintShowing(), false, 'no line in game mode');
|
|
614
|
+
assert.equal(boot({ page: 'city', store, session }).hintShowing(), false);
|
|
594
615
|
const own = { 'hall-fullscreen:thinking': 'on' };
|
|
595
616
|
assert.equal(boot({ page: 'thinking', store: own, session: {} }).hintShowing(), true);
|
|
596
617
|
assert.equal(boot({ page: 'thinking', store: own, session: {} }).hintShowing(), true, 'a remembered page says it on every open, as before');
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// tests/hall_helm.mjs — Helm, the look the builders' hall wears (task 1005192, ADR 0402).
|
|
2
|
+
//
|
|
3
|
+
// WHY. Two owner decisions are easy to break without anyone seeing it on their own
|
|
4
|
+
// machine: Helm is the hall's alone (the landing, the hub and the status page keep
|
|
5
|
+
// their look), and the hall is dark only (no page offers light, and a light OS never
|
|
6
|
+
// flashes it). Both are decided in one serve-time seam, src/hall-helm.js, which
|
|
7
|
+
// recognises a hall page by the sheet it links. This file runs that seam over the
|
|
8
|
+
// real pages, and holds the pieces of the kit a browser would hide:
|
|
9
|
+
// * every hall page is served with html[data-theme="dark"], and nothing else is;
|
|
10
|
+
// * the Helm faces reach a hall whose pack names the default faces, and never a
|
|
11
|
+
// hall that chose its own, and never the landing or the status page;
|
|
12
|
+
// * the font stylesheet loads the way tests/google_fonts_nonblocking.mjs asks of
|
|
13
|
+
// a typed-in link (both preconnects on the page, display=swap), which that sweep
|
|
14
|
+
// cannot see because the link is built at serve time;
|
|
15
|
+
// * no hall script writes a theme choice, and the shell offers no switch;
|
|
16
|
+
// * the Helm neutrals: the two dark twins carry the night, the ice and no black;
|
|
17
|
+
// * the kit's recipes exist once, as hall-tier tokens and shared rules.
|
|
18
|
+
//
|
|
19
|
+
// Run: node --test tests/hall_helm.mjs
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
30
|
+
const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8');
|
|
31
|
+
const decomment = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
32
|
+
|
|
33
|
+
const { applyHelm, isHallPage, wearsHelmFaces, stampHtml, HELM_FONTS_HREF } = require('../src/hall-helm.js');
|
|
34
|
+
const { injectBrandingGlobal } = require('../src/bongos/serve-internal.js');
|
|
35
|
+
const { clientBranding, resolveBranding } = require('../src/branding.js');
|
|
36
|
+
|
|
37
|
+
const NEUTRAL = JSON.parse(read('config', 'branding.neutral.json'));
|
|
38
|
+
const DEFAULTS = NEUTRAL.theme.ui.fonts;
|
|
39
|
+
const hallPages = fs.readdirSync(HALL).filter((f) => f.endsWith('.html'));
|
|
40
|
+
const htmlTag = (html) => (html.match(/<html\b[^>]*>/i) || [''])[0];
|
|
41
|
+
|
|
42
|
+
// ── the seam ────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
test('a hall page is the one that links the hall sheet; the landing, the hub and the status page are not', () => {
|
|
45
|
+
for (const f of hallPages) assert.ok(isHallPage(read('modules', 'hall-ui', 'public', f)), `${f} links the hall's sheet`);
|
|
46
|
+
for (const p of [['modules', 'public-landing', 'public', 'index.html'], ['modules', 'public-landing', 'public', 'projects.html'], ['modules', 'status-ui', 'public', 'index.html'], ['modules', 'ui-design', 'public', 'brand.html']]) {
|
|
47
|
+
assert.equal(isHallPage(read(...p)), false, `${p.join('/')} is not a hall page`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('every hall page is served pinned dark, in the Helm faces, and no other surface is touched', () => {
|
|
52
|
+
const brand = clientBranding(resolveBranding({ neutral: NEUTRAL }));
|
|
53
|
+
for (const f of hallPages) {
|
|
54
|
+
const out = injectBrandingGlobal(read('modules', 'hall-ui', 'public', f), brand, {});
|
|
55
|
+
const tag = htmlTag(out);
|
|
56
|
+
assert.match(tag, / data-theme="dark"/, `${f}: dark before first paint`);
|
|
57
|
+
assert.match(tag, / data-faces="helm"/, `${f}: the Helm faces`);
|
|
58
|
+
assert.equal((tag.match(/data-theme=/g) || []).length, 1, `${f}: stamped once`);
|
|
59
|
+
assert.ok(out.includes(`href="${HELM_FONTS_HREF.replace(/&/g, '&')}"`), `${f}: the two families are linked`);
|
|
60
|
+
// the link follows the pack's own splice, so it is the last word in <head>
|
|
61
|
+
assert.ok(out.indexOf('Barlow') > out.indexOf('--font-display:'), `${f}: after the pack's :root`);
|
|
62
|
+
}
|
|
63
|
+
for (const p of [['modules', 'public-landing', 'public', 'index.html'], ['modules', 'status-ui', 'public', 'index.html']]) {
|
|
64
|
+
const src = read(...p);
|
|
65
|
+
const out = injectBrandingGlobal(src, brand, {});
|
|
66
|
+
assert.equal(htmlTag(out), htmlTag(src), `${p.join('/')}: its <html> tag is as authored`);
|
|
67
|
+
assert.doesNotMatch(out, /Barlow|data-faces/, `${p.join('/')}: no Helm face`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('a hall that wears faces of its own keeps them; it is still dark only', () => {
|
|
72
|
+
const page = '<!doctype html><html lang="en"><head><link rel="stylesheet" href="/builders/style.css?v=1"></head><body></body></html>';
|
|
73
|
+
const own = { fonts: { display: '"Archivo", sans-serif', body: DEFAULTS.body, links: [] } };
|
|
74
|
+
assert.equal(wearsHelmFaces(own, DEFAULTS), false, 'one face of its own is a pack that chose');
|
|
75
|
+
const kept = applyHelm(page, own, DEFAULTS);
|
|
76
|
+
assert.match(htmlTag(kept.html), /data-theme="dark"/);
|
|
77
|
+
assert.doesNotMatch(kept.html, /data-faces/);
|
|
78
|
+
assert.equal(kept.tail, '', 'and no Helm stylesheet is linked');
|
|
79
|
+
// the default faces, and a pack that names none, take Helm
|
|
80
|
+
assert.equal(wearsHelmFaces({ fonts: { display: DEFAULTS.display, body: DEFAULTS.body } }, DEFAULTS), true);
|
|
81
|
+
assert.equal(wearsHelmFaces({}, DEFAULTS), true);
|
|
82
|
+
// a look from the style library with its own faces (ADR 0358) keeps them through the real resolver
|
|
83
|
+
const looked = clientBranding(resolveBranding({ neutral: NEUTRAL, env: { BONGOS_LOOK: 'expedition' } }));
|
|
84
|
+
if (looked.theme.look === 'expedition') {
|
|
85
|
+
const out = injectBrandingGlobal(page, looked, {});
|
|
86
|
+
assert.match(htmlTag(out), /data-theme="dark"/);
|
|
87
|
+
assert.doesNotMatch(out, /data-faces|Barlow/, 'the expedition look keeps Archivo');
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('the stamp replaces an attribute already there and leaves the others alone', () => {
|
|
92
|
+
assert.equal(stampHtml('<html lang="en" data-artist="on">', 'data-theme', 'dark'), '<html lang="en" data-artist="on" data-theme="dark">');
|
|
93
|
+
assert.equal(stampHtml('<html data-theme="light" lang="en">', 'data-theme', 'dark'), '<html lang="en" data-theme="dark">');
|
|
94
|
+
assert.equal(applyHelm('<html><head></head></html>', {}, DEFAULTS).html, '<html><head></head></html>', 'a page with no hall sheet is untouched');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('the Helm stylesheet loads without blocking: swap, and both preconnects on every hall page', () => {
|
|
98
|
+
assert.match(HELM_FONTS_HREF, /^https:\/\/fonts\.googleapis\.com\/css2\?/);
|
|
99
|
+
assert.match(HELM_FONTS_HREF, /[?&]display=swap$/);
|
|
100
|
+
assert.match(HELM_FONTS_HREF, /family=Barlow\+Condensed:wght@400;500;600/);
|
|
101
|
+
assert.match(HELM_FONTS_HREF, /family=Barlow:wght@400;500/);
|
|
102
|
+
const missing = [];
|
|
103
|
+
for (const f of hallPages) {
|
|
104
|
+
const html = read('modules', 'hall-ui', 'public', f);
|
|
105
|
+
if (!/<link rel="preconnect" href="https:\/\/fonts\.googleapis\.com">/.test(html)) missing.push(`${f}: fonts.googleapis.com`);
|
|
106
|
+
if (!/<link rel="preconnect" href="https:\/\/fonts\.gstatic\.com" crossorigin>/.test(html)) missing.push(`${f}: fonts.gstatic.com (crossorigin)`);
|
|
107
|
+
}
|
|
108
|
+
assert.deepEqual(missing, [], 'a hall page that links a web font warms both font origins first');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ── dark only ───────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
test('no hall script stores or offers a theme, and the shell pins dark', () => {
|
|
114
|
+
const shell = read('modules', 'hall-ui', 'public', 'shell.js');
|
|
115
|
+
assert.match(shell, /function applyTheme\(\) \{\s*document\.documentElement\.dataset\.theme = 'dark';\s*\}/, 'the shell sets dark, whatever was stored');
|
|
116
|
+
assert.doesNotMatch(shell, /theme-toggle|toggleTheme/, 'no switch on the top bar');
|
|
117
|
+
const writers = [];
|
|
118
|
+
for (const f of fs.readdirSync(HALL).filter((x) => x.endsWith('.js'))) {
|
|
119
|
+
const src = read('modules', 'hall-ui', 'public', f).replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:'"])\/\/.*$/gm, '$1');
|
|
120
|
+
if (/['"]hall-theme['"]/.test(src)) writers.push(`${f}: names the retired 'hall-theme' preference`);
|
|
121
|
+
if (/dataset\.theme\s*=\s*(?![\s=]|'dark')|setAttribute\(\s*['"]data-theme['"]\s*,\s*(?!\s|'dark')/.test(src)) writers.push(`${f}: writes a theme other than dark`);
|
|
122
|
+
}
|
|
123
|
+
assert.deepEqual(writers, []);
|
|
124
|
+
for (const f of hallPages) assert.doesNotMatch(read('modules', 'hall-ui', 'public', f), /id="(theme-toggle|cityMode|streetMode)"/, `${f}: no light/dark switch`);
|
|
125
|
+
assert.doesNotMatch(decomment(read('modules', 'hall-ui', 'public', 'style.css')), /\.theme-toggle/, 'and no styles for one');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ── the Helm tier ───────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
const STYLE = read('modules', 'hall-ui', 'public', 'style.css');
|
|
131
|
+
const blockAt = (marker) => {
|
|
132
|
+
const at = STYLE.indexOf(marker);
|
|
133
|
+
assert.notEqual(at, -1, `style.css holds ${marker}`);
|
|
134
|
+
const open = STYLE.indexOf('{', at);
|
|
135
|
+
let depth = 0;
|
|
136
|
+
for (let i = open; i < STYLE.length; i++) {
|
|
137
|
+
if (STYLE[i] === '{') depth++;
|
|
138
|
+
else if (STYLE[i] === '}' && --depth === 0) return STYLE.slice(open + 1, i);
|
|
139
|
+
}
|
|
140
|
+
throw new Error('unbalanced');
|
|
141
|
+
};
|
|
142
|
+
const decls = (body) => Object.fromEntries([...decomment(body).matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)].map((m) => [m[1], m[2].replace(/\s+/g, ' ').trim()]));
|
|
143
|
+
const root = decls(blockAt('\n:root {'));
|
|
144
|
+
const dark = decls(blockAt('html[data-theme="dark"] {'));
|
|
145
|
+
|
|
146
|
+
test('the dark tier is the Helm neutrals: night grounds, white and ice inks, ice hairlines', () => {
|
|
147
|
+
assert.deepEqual(
|
|
148
|
+
Object.fromEntries(['--bg', '--bg-card', '--bg-deep', '--bg-frame', '--ink', '--ink-soft', '--rule', '--rule-soft', '--void'].map((k) => [k, dark[k]])),
|
|
149
|
+
{ '--bg': '#050d16', '--bg-card': '#0a1622', '--bg-deep': '#050d16', '--bg-frame': '#07111b', '--ink': '#ffffff', '--ink-soft': '#c8f1f7', '--rule': 'rgba(200, 241, 247, .4)', '--rule-soft': 'rgba(200, 241, 247, .2)', '--void': '#050d16' },
|
|
150
|
+
);
|
|
151
|
+
// the accent is still the pack's: the moon is never pinned (the Unpinned Accent Rule)
|
|
152
|
+
for (const name of ['--accent', '--accent-deep', '--sea', '--terracotta', '--font-display', '--font-body']) assert.equal(dark[name], undefined, `${name} is not set in the dark tier`);
|
|
153
|
+
// in dark a label is the name voice
|
|
154
|
+
assert.equal(dark['--label-face'], 'var(--font-display)');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('the Helm faces are hall-tier tokens, handed to the two font tokens only under html[data-faces="helm"]', () => {
|
|
158
|
+
assert.match(root['--helm-display'], /^"Barlow Condensed", /);
|
|
159
|
+
assert.match(root['--helm-body'], /^"Barlow", /);
|
|
160
|
+
// the contract tier still equals the pack (tests/hall_tokens.mjs holds that); Helm does not overwrite it there
|
|
161
|
+
assert.equal(root['--font-display'], DEFAULTS.display);
|
|
162
|
+
const faces = decls(blockAt('\nhtml[data-faces="helm"] {'));
|
|
163
|
+
assert.deepEqual(faces, { '--font-display': 'var(--helm-display)', '--font-body': 'var(--helm-body)' });
|
|
164
|
+
assert.equal((decomment(STYLE).match(/Barlow/g) || []).length, 2, 'the family names live once each, in the token block');
|
|
165
|
+
for (const f of fs.readdirSync(HALL).filter((x) => x.endsWith('.css') && x !== 'style.css')) {
|
|
166
|
+
assert.doesNotMatch(read('modules', 'hall-ui', 'public', f), /Barlow/, `${f} takes the face from the tokens`);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('the kit\'s recipes are named once: the name voice, the cut, the segment, the panel over art', () => {
|
|
171
|
+
assert.equal(root['--name-face'], 'var(--font-display)');
|
|
172
|
+
assert.equal(root['--name-case'], 'uppercase');
|
|
173
|
+
for (const k of ['--name-space', '--name-space-wide', '--name-space-tight']) {
|
|
174
|
+
const em = Number((root[k].match(/^(\.\d+)em$/) || [])[1]);
|
|
175
|
+
assert.ok(em >= 0.16 && em <= 0.3, `${k} is wide tracking (${root[k]})`);
|
|
176
|
+
}
|
|
177
|
+
assert.equal(root['--cut'], '12px');
|
|
178
|
+
assert.equal(root['--seg-skew'], '-28deg');
|
|
179
|
+
assert.equal(root['--night-glass'], 'color-mix(in srgb, var(--bg) 72%, transparent)');
|
|
180
|
+
const controls = decomment(read('modules', 'hall-ui', 'public', 'controls.css'));
|
|
181
|
+
const seg = controls.match(/\n\.seg \{([^}]*)\}/);
|
|
182
|
+
assert.ok(seg, 'the segment is a shared rule');
|
|
183
|
+
assert.match(seg[1], /transform: skewX\(var\(--seg-skew\)\);/);
|
|
184
|
+
assert.match(seg[1], /height: var\(--seg-h\);/);
|
|
185
|
+
assert.match(controls, /\n\.seg--done \{ background: var\(--ink-soft\);/, 'a finished step is information, so it is ice, not the accent');
|
|
186
|
+
assert.match(controls, /\n\.seg--now \{ background: var\(--accent\);/, 'the accent is the step you can act on');
|
|
187
|
+
});
|
|
@@ -152,14 +152,16 @@ test('.btn and .btn--ghost are defined, and say what .btn-accent / .btn-ghost sa
|
|
|
152
152
|
assert.match(btn, /background:\s*var\(--accent\)/, '.btn is the accent door');
|
|
153
153
|
assert.match(btn, /color:\s*var\(--accent-on\)/, '--accent-on is the dark ink for the salmon');
|
|
154
154
|
const shape = ruleHolding(controlsCss, '.btn', 'border-radius');
|
|
155
|
-
|
|
155
|
+
// Helm (task 1005192, ADR 0402): the hall's corner is the cut; the pill is the fallback where the hall's tier is not loaded
|
|
156
|
+
assert.match(shape, /border-radius:\s*var\(--press-radius, var\(--radius-pill\)\)/, 'the cut corner in the hall, the pill elsewhere (DESIGN.md, The hall)');
|
|
157
|
+
assert.match(shape, /clip-path:\s*var\(--cut-path, none\)/, 'anything you can press takes the cut corner');
|
|
156
158
|
// collab.css's .btn--quiet sets only a border-COLOUR; a modifier cannot paint
|
|
157
159
|
// an edge that was never reserved, so the base has to carry a transparent one.
|
|
158
160
|
assert.match(shape, /border:\s*var\(--line\)\s+solid\s+transparent/,
|
|
159
161
|
'the edge is reserved so a modifier that sets only border-color still draws');
|
|
160
162
|
const ghost = ruleHolding(controlsCss, '.btn--ghost', 'background');
|
|
161
163
|
assert.ok(ghost, '.btn--ghost has a rule');
|
|
162
|
-
assert.match(ghost, /background:\s*transparent/, 'the quiet sibling is a hairline
|
|
164
|
+
assert.match(ghost, /background:\s*var\(--press-fill, transparent\)/, 'the quiet sibling is the press fill inside a hairline, never the accent');
|
|
163
165
|
assert.equal(ghost, ruleHolding(controlsCss, '.btn-ghost', 'background'), 'and .btn-ghost says the same, in the same rule');
|
|
164
166
|
assert.match(ghost, /border-color:\s*var\(--rule\)/, 'and it takes the reserved edge');
|
|
165
167
|
});
|
|
@@ -82,6 +82,16 @@ test('the tips rotate, and every one is a sentence', () => {
|
|
|
82
82
|
for (const t of G.TIPS) assert.match(t, /^[A-Z].*\.$/);
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
test('the load screen fills one segment per week, up to the week it is showing (task 1005192)', () => {
|
|
86
|
+
const { window } = sandbox();
|
|
87
|
+
const G = window.OTBShellGame;
|
|
88
|
+
assert.equal(G.segsDone(10, 0), 1, 'the oldest week is the first segment');
|
|
89
|
+
assert.equal(G.segsDone(10, 4.5), 5);
|
|
90
|
+
assert.equal(G.segsDone(10, 9), 10, 'now fills the row');
|
|
91
|
+
assert.equal(G.segsDone(10, 99), 10, 'never more than there are weeks');
|
|
92
|
+
assert.equal(G.segsDone(0, 0), 0);
|
|
93
|
+
});
|
|
94
|
+
|
|
85
95
|
test('a fresh sign-in is handed to the start screen with the still, and the craft picker waits for the world', () => {
|
|
86
96
|
const s = sandbox({ store: { 'hall-welcome-signin': '1' } });
|
|
87
97
|
const access = { authed: true }, builder = { id: 7, display_name: 'Ada' };
|
|
@@ -113,6 +123,13 @@ test('Home loads the sheet and the script, and the sheet types in no size, corne
|
|
|
113
123
|
assert.doesNotMatch(css, /border-radius:\s*[\d.]/);
|
|
114
124
|
assert.doesNotMatch(css, /@media/);
|
|
115
125
|
assert.doesNotMatch(css, /#[0-9a-fA-F]{3,8}\b/, 'tokens, or black and white: no private palette');
|
|
126
|
+
// Helm (task 1005192, ADR 0402): the literal floor is zero, the voice and the panel are the hall tier's tokens
|
|
127
|
+
assert.doesNotMatch(css, /rgba?\(|hsla?\(|box-shadow:\s*[\d-]|text-shadow:\s*[\d-]|font-family:(?!\s*var\()/, 'no typed colour, shadow or face');
|
|
128
|
+
assert.match(css, /\.gs__title \{[^}]*font-family: var\(--name-face\);[^}]*letter-spacing: var\(--name-space-wide\); text-transform: var\(--name-case\);/, 'the title is the name voice');
|
|
129
|
+
assert.match(css, /\.gs__item \{[^}]*font-family: var\(--name-face\);[^}]*text-transform: var\(--name-case\);/, 'so is every choice');
|
|
130
|
+
assert.match(css, /\.gs__menu, \.gs__load \{[^}]*background: var\(--night-glass\);/, 'both panels are the night over the art');
|
|
131
|
+
assert.match(css, /\.gs__item\[data-gs="start"\] \{\s*background: var\(--accent\); color: var\(--accent-on\);/, 'Start is the one thing lit, under the dark ink');
|
|
132
|
+
assert.match(css, /\.gs__item:focus-visible \{[^}]*box-shadow: var\(--press-focus\);/, 'a row rings inside itself, in two tones');
|
|
116
133
|
});
|
|
117
134
|
|
|
118
135
|
// ---------- in a browser: the two screens over Home's own markup ----------
|
|
@@ -130,8 +147,8 @@ const SCRIPTS = ['dom-utils.js', 'welcome-boot.js', 'shell-game.js', 'shell-trai
|
|
|
130
147
|
.map((f) => `<script src="/builders/${f}"></script>`).join('');
|
|
131
148
|
const PAGE = INDEX.replace('</body>', STUB + SCRIPTS + '</body>');
|
|
132
149
|
|
|
133
|
-
async function hall({ city = true, reducedMotion = 'no-preference' } = {}) {
|
|
134
|
-
const ctx = await browser.newContext({ viewport
|
|
150
|
+
async function hall({ city = true, reducedMotion = 'no-preference', viewport = { width: 1280, height: 800 } } = {}) {
|
|
151
|
+
const ctx = await browser.newContext({ viewport, reducedMotion });
|
|
135
152
|
await ctx.route(ORIGIN + '/**', (route) => {
|
|
136
153
|
const p = new URL(route.request().url()).pathname;
|
|
137
154
|
if (p === '/' || p === '/index.html') return route.fulfill({ contentType: 'text/html', body: PAGE });
|
|
@@ -171,15 +188,45 @@ test('a sign-in passes through the start screen and the load screen before the w
|
|
|
171
188
|
const { page, errors } = await signedIn(ctx);
|
|
172
189
|
await page.waitForSelector('#game-start');
|
|
173
190
|
assert.deepEqual(await state(page), { start: true, welcome: false, picker: false, game: null, full: null },
|
|
174
|
-
'the start screen, not the welcome and not the picker; game mode is not on until
|
|
191
|
+
'the start screen, not the welcome and not the picker; game mode is not on until it is chosen');
|
|
192
|
+
// task 1005200: first the chooser — game or classic — then, for game, the start screen.
|
|
193
|
+
const views = await page.evaluate(() => ({
|
|
194
|
+
items: Array.from(document.querySelectorAll('.gs__items:not([hidden]) .gs__item')).map((e) => e.textContent),
|
|
195
|
+
focus: document.activeElement.textContent,
|
|
196
|
+
}));
|
|
197
|
+
assert.deepEqual(views, { items: ['GameThe city, full screen', 'ClassicThe hall with its sidebar'], focus: 'GameThe city, full screen' });
|
|
198
|
+
await page.keyboard.press('Enter');
|
|
199
|
+
await page.waitForSelector('#gs-items:not([hidden])');
|
|
200
|
+
assert.equal((await state(page)).game, 'on', 'choosing Game turns game mode on');
|
|
175
201
|
const menu = await page.evaluate(() => ({
|
|
176
202
|
title: document.getElementById('gs-title').textContent,
|
|
177
|
-
items: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras) .gs__item')).map((e) => e.textContent),
|
|
203
|
+
items: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras):not([hidden]) .gs__item')).map((e) => e.textContent),
|
|
178
204
|
focus: document.activeElement.textContent,
|
|
179
|
-
small: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras) .gs__item')).filter((e) => e.getBoundingClientRect().height < 46).length,
|
|
205
|
+
small: Array.from(document.querySelectorAll('.gs__items:not(.gs__items--extras):not([hidden]) .gs__item')).filter((e) => e.getBoundingClientRect().height < 46).length,
|
|
180
206
|
}));
|
|
181
207
|
assert.deepEqual(menu, { title: 'Harbor', items: ['Start', 'Options', 'Achievements', 'Extras', 'Quit'], focus: 'Start', small: 0 });
|
|
182
208
|
|
|
209
|
+
// Helm (task 1005192): one full-height night panel on the left, the mark over the name, a line at the foot.
|
|
210
|
+
const panel = await page.evaluate(() => {
|
|
211
|
+
const box = (sel) => { const r = document.querySelector(sel).getBoundingClientRect(); return { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) }; };
|
|
212
|
+
const start = document.querySelector('[data-gs="start"]'), title = document.getElementById('gs-title');
|
|
213
|
+
return {
|
|
214
|
+
menu: box('.gs__menu'), start: box('[data-gs="start"]'), view: { w: window.innerWidth, h: window.innerHeight },
|
|
215
|
+
mark: !!document.querySelector('.gs__head .gs__mark'), line: document.getElementById('gs-line').textContent,
|
|
216
|
+
caps: getComputedStyle(title).textTransform, startCaps: getComputedStyle(start).textTransform,
|
|
217
|
+
key: getComputedStyle(start, '::after').content, sideways: document.getElementById('game-start').scrollWidth - document.getElementById('game-start').clientWidth,
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
assert.equal(panel.menu.w, 400, 'the panel is 400 wide on a desk');
|
|
221
|
+
assert.ok(panel.menu.x > 0 && panel.menu.x < panel.view.w / 4, 'and stands on the left, off the edge');
|
|
222
|
+
assert.deepEqual([panel.menu.y, panel.menu.h], [0, panel.view.h], 'the full height of the screen');
|
|
223
|
+
assert.ok(panel.start.w >= panel.menu.w - 4, 'Start is a bar the width of the panel');
|
|
224
|
+
assert.equal(panel.mark, true, 'the mark stands over the name');
|
|
225
|
+
assert.match(panel.line, /^[A-Z].*\.$/, 'the foot carries one sentence');
|
|
226
|
+
assert.deepEqual([panel.caps, panel.startCaps], ['uppercase', 'uppercase'], 'names are capitals');
|
|
227
|
+
assert.equal(panel.key, '"Enter"', 'Start names the key that takes it');
|
|
228
|
+
assert.equal(panel.sideways, 0);
|
|
229
|
+
|
|
183
230
|
// The arrows walk the menu; Escape stays with the screen and does not flip the shell underneath.
|
|
184
231
|
await page.keyboard.press('ArrowDown');
|
|
185
232
|
assert.equal(await page.evaluate(() => document.activeElement.textContent), 'Options');
|
|
@@ -201,7 +248,11 @@ test('a sign-in passes through the start screen and the load screen before the w
|
|
|
201
248
|
assert.match(await page.textContent('#gs-tip'), /\.$/);
|
|
202
249
|
await page.waitForFunction(() => /weeks? ago$/.test(document.getElementById('gs-when').textContent));
|
|
203
250
|
assert.ok(await page.evaluate(() => document.querySelectorAll('#gs-city *').length > 20), 'the city is drawn, not a placeholder');
|
|
251
|
+
const weeks = JSON.parse(CITY).weeks.length;
|
|
252
|
+
assert.equal(await page.evaluate(() => document.querySelectorAll('#gs-segs:not([hidden]) > .seg').length), weeks, 'one segment per week of the history');
|
|
204
253
|
await page.waitForFunction(() => { const w = document.getElementById('gs-when'); return !w || w.textContent === 'The city, now'; });
|
|
254
|
+
const segs = await page.evaluate(() => { const b = document.getElementById('gs-segs'); return b ? [b.querySelectorAll('.seg--done').length, b.getAttribute('aria-hidden')] : null; });
|
|
255
|
+
if (segs) assert.deepEqual(segs, [weeks, 'true'], 'at now every segment is filled; the words beside them say it, so the row is hidden from a screen reader');
|
|
205
256
|
await page.waitForSelector('#game-start', { state: 'detached' });
|
|
206
257
|
await page.waitForSelector('#craft-picker');
|
|
207
258
|
assert.deepEqual(await state(page), { start: false, welcome: false, picker: true, game: 'on', full: 'on' },
|
|
@@ -211,11 +262,57 @@ test('a sign-in passes through the start screen and the load screen before the w
|
|
|
211
262
|
await ctx.close();
|
|
212
263
|
});
|
|
213
264
|
|
|
265
|
+
// task 1005200: Classic at the chooser — the screen goes, game mode is off, and the account
|
|
266
|
+
// is told. The view chosen last time is the one marked and focused.
|
|
267
|
+
test('choosing Classic at sign-in closes the screen, leaves game mode off, and saves it to the account', { skip }, async () => {
|
|
268
|
+
const ctx = await hall();
|
|
269
|
+
const saved = [];
|
|
270
|
+
await ctx.route(ORIGIN + '/api/bongos/me/hall-view', (route) => {
|
|
271
|
+
if (route.request().method() === 'PATCH') saved.push(JSON.parse(route.request().postData()));
|
|
272
|
+
return route.fulfill({ contentType: 'application/json', body: JSON.stringify({ view: null }) });
|
|
273
|
+
});
|
|
274
|
+
const page = await ctx.newPage();
|
|
275
|
+
await page.addInitScript(() => { try { localStorage.setItem('hall-welcome-signin', '1'); localStorage.setItem('hall-game', 'off'); } catch (_) { /* no storage */ } });
|
|
276
|
+
await page.goto(ORIGIN + '/');
|
|
277
|
+
await page.waitForFunction(() => !!window.OTBShell && !!window.OTBWelcome && !!window.OTBShellGame);
|
|
278
|
+
await page.evaluate(() => { const me = { authed: true, rank: 'metic', builder: { id: 7, display_name: 'Ada', github_login: 'ada' } }; window.OTBShell.applyAccess(me); window.OTBShell.applyAccess(me); });
|
|
279
|
+
await page.waitForSelector('#game-start');
|
|
280
|
+
assert.equal(await page.evaluate(() => document.activeElement.textContent), 'ClassicLast timeThe hall with its sidebar', 'the view chosen last time is focused and marked');
|
|
281
|
+
await page.keyboard.press('Enter');
|
|
282
|
+
await page.waitForSelector('#game-start', { state: 'detached' });
|
|
283
|
+
assert.equal((await state(page)).game, null);
|
|
284
|
+
assert.deepEqual(saved, [{ view: 'classic' }]);
|
|
285
|
+
await ctx.close();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('on a phone the panel is the whole width, nothing scrolls sideways and every choice clears the floor (task 1005192)', { skip }, async () => {
|
|
289
|
+
for (const [w, h] of [[390, 844], [320, 700]]) {
|
|
290
|
+
const ctx = await hall({ viewport: { width: w, height: h } });
|
|
291
|
+
const { page, errors } = await signedIn(ctx);
|
|
292
|
+
await page.waitForSelector('#game-start');
|
|
293
|
+
const m = await page.evaluate(() => {
|
|
294
|
+
const gs = document.getElementById('game-start'), r = document.querySelector('.gs__menu').getBoundingClientRect();
|
|
295
|
+
return {
|
|
296
|
+
x: Math.round(r.left), w: Math.round(r.width), sideways: gs.scrollWidth - gs.clientWidth,
|
|
297
|
+
small: Array.from(document.querySelectorAll('.gs__item')).filter((e) => !e.closest('[hidden]')).filter((e) => { const b = e.getBoundingClientRect(); return b.height < 24 || b.width < 24 || b.right > window.innerWidth + 1; }).length,
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
assert.deepEqual(m, { x: 0, w, sideways: 0, small: 0 }, `at ${w}px`);
|
|
301
|
+
await page.click('[data-gs="view-game"]');
|
|
302
|
+
await page.click('[data-gs="start"]');
|
|
303
|
+
await page.waitForSelector('#gs-load:not([hidden])');
|
|
304
|
+
assert.equal(await page.evaluate(() => { const gs = document.getElementById('game-start'); return gs.scrollWidth - gs.clientWidth; }), 0, `the load screen fits ${w}px`);
|
|
305
|
+
assert.deepEqual(errors, []);
|
|
306
|
+
await ctx.close();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
214
310
|
test('Quit closes the tab, and says so when the browser keeps it open', { skip }, async () => {
|
|
215
311
|
const ctx = await hall();
|
|
216
312
|
const { page } = await signedIn(ctx);
|
|
217
313
|
await page.waitForSelector('#game-start');
|
|
218
314
|
await page.evaluate(() => { window.__closed = 0; window.close = () => { window.__closed += 1; }; });
|
|
315
|
+
await page.click('[data-gs="view-game"]');
|
|
219
316
|
await page.click('[data-gs="quit"]');
|
|
220
317
|
await page.waitForSelector('#gs-note:not([hidden])');
|
|
221
318
|
assert.equal(await page.evaluate(() => window.__closed), 1);
|
|
@@ -228,6 +325,7 @@ test('with no city to draw, or with reduced motion, the still and the tip stand
|
|
|
228
325
|
const ctx = await hall(opts);
|
|
229
326
|
const { page, errors } = await signedIn(ctx);
|
|
230
327
|
await page.waitForSelector('#game-start');
|
|
328
|
+
await page.click('[data-gs="view-game"]');
|
|
231
329
|
await page.click('[data-gs="start"]');
|
|
232
330
|
await page.waitForSelector('#gs-load:not([hidden])');
|
|
233
331
|
assert.match(await page.textContent('#gs-tip'), /\.$/);
|
package/tests/hall_street.mjs
CHANGED
|
@@ -488,7 +488,8 @@ test('the page holds the prototype\'s header, the street, the readings and the c
|
|
|
488
488
|
assert.equal((html.match(/<main\b/g) || []).length, 1);
|
|
489
489
|
assert.equal((html.match(/<h1\b/g) || []).length, 1);
|
|
490
490
|
assert.match(html, /<a class="street-mark" id="streetHome" href="\/builders\/city"[^>]*>\{\{productName\}\}</);
|
|
491
|
-
|
|
491
|
+
assert.doesNotMatch(html, /streetMode/, 'no light/dark switch: the hall is dark only (task 1005192, ADR 0402)');
|
|
492
|
+
for (const id of ['hudNeeds', 'hudMoving', 'hudShare', 'streetSigns', 'streetPins', 'streetDoors', 'streetCard']) assert.match(html, new RegExp(`id="${id}"`), id);
|
|
492
493
|
const scripts = [...html.matchAll(/<script src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
|
|
493
494
|
assert.deepEqual(scripts.filter((s) => !/^street-(deck|office)$/.test(s)), ['dom-utils', 'shell-game', 'shell-trail', 'craft-mode', 'shell', 'hall-dialog', 'palette', 'gov-living', 'gov-feeds', 'city-draw', 'street-draw', 'street-card', 'street-inside', 'street-depth', 'street-desk', 'street-amend', 'street'], 'the hall shell (task 1004544), the living model and its one reader (task 1004685: city-draw.js counts with it, so it loads first), the shared kit, then the card (task 1004528), the interiors (task 1004526), the living street\'s exterior (street-depth.js), the floor desk (task 1004558) and the drafted amendment (task 1004530); the deck rides beside them');
|
|
494
495
|
assert.equal(City.STREET_HREF, '/builders/street', 'the city\'s door is this page');
|
|
@@ -552,7 +553,7 @@ function fakeDom() {
|
|
|
552
553
|
};
|
|
553
554
|
const focused = { el: null };
|
|
554
555
|
const html = mk('html');
|
|
555
|
-
const ids = ['
|
|
556
|
+
const ids = ['streetWindow', 'streetSizer', 'streetBoard', 'streetSvg', 'streetFlat', 'streetBlds', 'streetCrates', 'streetCols', 'streetGroups', 'streetDoors', 'streetPins', 'streetSigns', 'streetEvening', 'streetEveningLine', 'hudNeeds', 'hudMoving', 'hudShare', 'hudDot', 'streetHome', 'streetCard', 'streetCardWhere', 'streetCardWhen', 'streetCardTitle', 'streetCardBody', 'streetCardOpen', 'streetCardClose', 'streetToast'];
|
|
556
557
|
for (const id of ids) mk('x').setAttribute('id', id);
|
|
557
558
|
const document = { documentElement: html, getElementById: (id) => byId.get(id) || null, createElement: (t) => mk(t), createElementNS: (_ns, t) => mk(t) };
|
|
558
559
|
return { document, byId, html, focused };
|
|
@@ -701,32 +702,18 @@ test('the header counts wear the pins\' colours, so they double as the legend',
|
|
|
701
702
|
assert.ok(size('urgent') > size('needs') && size('needs') > size('moving') && size('moving') > size('settled'), 'urgent > needs > moving > settled');
|
|
702
703
|
});
|
|
703
704
|
|
|
704
|
-
test('the
|
|
705
|
-
const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark:
|
|
706
|
-
assert.equal(p.html.attrs['data-theme'], '
|
|
707
|
-
assert.equal(p.
|
|
708
|
-
assert.equal(p.
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
assert.
|
|
712
|
-
assert.equal(p.html.attrs['data-theme'], 'dark');
|
|
713
|
-
assert.notEqual(JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs), before, 'the street redraws in the new mode');
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
test('in the hall, the street and the top bar\'s theme switch never disagree (task 1004544)', async () => {
|
|
717
|
-
let shellPaints = 0;
|
|
718
|
-
const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: true, shell: { applyTheme() { shellPaints++; } } });
|
|
719
|
-
assert.equal(p.observers.length, 1, 'the page watches html[data-theme]');
|
|
720
|
-
p.byId.get('streetMode').fire('click');
|
|
721
|
-
assert.equal(shellPaints, 1, 'this switch repaints the shell\'s');
|
|
722
|
-
const before = JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs);
|
|
723
|
-
p.store.set('hall-theme', 'light'); p.html.setAttribute('data-theme', 'light'); // the shell's switch
|
|
724
|
-
p.observers[0].fn();
|
|
725
|
-
assert.equal(p.byId.get('streetModeWord').textContent, 'dark', 'this switch follows the shell\'s');
|
|
726
|
-
assert.notEqual(JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs), before, 'and the street redraws in that mode');
|
|
727
|
-
assert.equal(shellPaints, 1, 'following the shell does not echo back to it');
|
|
705
|
+
test('the street is dark only: no stored choice and no OS setting reaches it (task 1005192, ADR 0402)', async () => {
|
|
706
|
+
const p = await bootPage({ feeds: FEEDS([]), stored: 'light', osDark: false, shell: { applyTheme() { throw new Error('the street no longer drives the shell'); } } });
|
|
707
|
+
assert.equal(p.html.attrs['data-theme'], 'dark', 'a stored light choice and a light OS are both ignored');
|
|
708
|
+
assert.equal(p.store.get('hall-theme'), 'light', 'and the old preference is never written');
|
|
709
|
+
assert.equal(p.observers.length, 0, 'nothing flips the theme, so nothing watches it');
|
|
710
|
+
const q = await bootPage({ feeds: FEEDS([]), stored: null, osDark: true });
|
|
711
|
+
assert.equal(JSON.stringify(q.byId.get('streetBlds').children[0].children[0].attrs), JSON.stringify(p.byId.get('streetBlds').children[0].children[0].attrs), 'one drawing whatever was stored');
|
|
712
|
+
assert.doesNotMatch(read('street.js'), /hall-theme|prefers-color-scheme/, 'street.js reads no theme preference');
|
|
728
713
|
});
|
|
729
714
|
|
|
715
|
+
|
|
716
|
+
|
|
730
717
|
test('arriving through a door marks that building, and a failed docket says so', async () => {
|
|
731
718
|
const p = await bootPage({ feeds: FEEDS([]), hash: '#building=treasury' });
|
|
732
719
|
const here = p.byId.get('streetDoors').children.filter((a) => a.attrs['aria-current'] === 'location');
|
|
@@ -63,7 +63,7 @@ async function measureBar(width, scheme) {
|
|
|
63
63
|
const ctx = await context(width, scheme);
|
|
64
64
|
const page = await ctx.newPage();
|
|
65
65
|
await page.goto(ORIGIN + '/work');
|
|
66
|
-
await page.waitForSelector('#app-topbar .topbar-side #
|
|
66
|
+
await page.waitForSelector('#app-topbar .topbar-side #zoom-toggle');
|
|
67
67
|
return page.evaluate(() => {
|
|
68
68
|
const side = document.querySelector('#app-topbar .topbar-side');
|
|
69
69
|
if (!document.getElementById('upd-pill')) {
|
|
@@ -86,9 +86,11 @@ for (const scheme of ['light', 'dark']) {
|
|
|
86
86
|
test(`at 320px the top bar fits, every button inside it and at least 24px (${scheme})`, { skip }, async () => {
|
|
87
87
|
const m = await measureBar(320, scheme);
|
|
88
88
|
const ids = m.buttons.map((b) => b.id);
|
|
89
|
-
for (const want of ['upd-pill', 'topbar-jump', 'zoom-toggle', '
|
|
89
|
+
for (const want of ['upd-pill', 'topbar-jump', 'zoom-toggle', 'fullscreen-toggle']) {
|
|
90
90
|
assert.ok(ids.includes(want), `${want} is on the bar (got ${ids.join(', ')})`);
|
|
91
91
|
}
|
|
92
|
+
// the hall is dark only (task 1005192, ADR 0402): the bar offers no light/dark switch
|
|
93
|
+
assert.ok(!ids.includes('theme-toggle'), 'no theme switch on the bar');
|
|
92
94
|
assert.ok(m.scrollWidth <= 320, `the page is ${m.scrollWidth}px wide at 320 (the top bar spills)`);
|
|
93
95
|
for (const b of m.buttons) {
|
|
94
96
|
assert.ok(b.left >= 0 && b.right <= 320, `${b.id} sits at ${Math.round(b.left)}..${Math.round(b.right)}, outside 0..320`);
|