@bongos/core 1.24.80 → 1.24.81
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 +125 -110
- 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/copy-inventory.md +26 -31
- package/docs/copy-registry.json +29 -86
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +1 -3
- package/docs/page-readings.json +94 -94
- 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 +5 -8
- 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 +3 -3
- 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 +4 -4
- 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/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.html +3 -3
- package/modules/hall-ui/public/settings.states.json +1 -0
- package/modules/hall-ui/public/shell-game.css +87 -42
- package/modules/hall-ui/public/shell-game.js +41 -8
- package/modules/hall-ui/public/shell.js +8 -40
- 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 +124 -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 +3 -3
- 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/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/start-screen.md +6 -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 +6 -0
- package/scripts/hall-preview/README.md +2 -3
- 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_helm.mjs +187 -0
- package/tests/hall_record_world.mjs +4 -2
- package/tests/hall_start_screen.mjs +65 -2
- package/tests/hall_street.mjs +13 -26
- package/tests/hall_topbar_320.mjs +4 -2
- package/tests/nav_permission_atoms.mjs +1 -1
|
@@ -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 });
|
|
@@ -180,6 +197,27 @@ test('a sign-in passes through the start screen and the load screen before the w
|
|
|
180
197
|
}));
|
|
181
198
|
assert.deepEqual(menu, { title: 'Harbor', items: ['Start', 'Options', 'Achievements', 'Extras', 'Quit'], focus: 'Start', small: 0 });
|
|
182
199
|
|
|
200
|
+
// Helm (task 1005192): one full-height night panel on the left, the mark over the name, a line at the foot.
|
|
201
|
+
const panel = await page.evaluate(() => {
|
|
202
|
+
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) }; };
|
|
203
|
+
const start = document.querySelector('[data-gs="start"]'), title = document.getElementById('gs-title');
|
|
204
|
+
return {
|
|
205
|
+
menu: box('.gs__menu'), start: box('[data-gs="start"]'), view: { w: window.innerWidth, h: window.innerHeight },
|
|
206
|
+
mark: !!document.querySelector('.gs__head .gs__mark'), line: document.getElementById('gs-line').textContent,
|
|
207
|
+
caps: getComputedStyle(title).textTransform, startCaps: getComputedStyle(start).textTransform,
|
|
208
|
+
key: getComputedStyle(start, '::after').content, sideways: document.getElementById('game-start').scrollWidth - document.getElementById('game-start').clientWidth,
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
assert.equal(panel.menu.w, 400, 'the panel is 400 wide on a desk');
|
|
212
|
+
assert.ok(panel.menu.x > 0 && panel.menu.x < panel.view.w / 4, 'and stands on the left, off the edge');
|
|
213
|
+
assert.deepEqual([panel.menu.y, panel.menu.h], [0, panel.view.h], 'the full height of the screen');
|
|
214
|
+
assert.ok(panel.start.w >= panel.menu.w - 4, 'Start is a bar the width of the panel');
|
|
215
|
+
assert.equal(panel.mark, true, 'the mark stands over the name');
|
|
216
|
+
assert.match(panel.line, /^[A-Z].*\.$/, 'the foot carries one sentence');
|
|
217
|
+
assert.deepEqual([panel.caps, panel.startCaps], ['uppercase', 'uppercase'], 'names are capitals');
|
|
218
|
+
assert.equal(panel.key, '"Enter"', 'Start names the key that takes it');
|
|
219
|
+
assert.equal(panel.sideways, 0);
|
|
220
|
+
|
|
183
221
|
// The arrows walk the menu; Escape stays with the screen and does not flip the shell underneath.
|
|
184
222
|
await page.keyboard.press('ArrowDown');
|
|
185
223
|
assert.equal(await page.evaluate(() => document.activeElement.textContent), 'Options');
|
|
@@ -201,7 +239,11 @@ test('a sign-in passes through the start screen and the load screen before the w
|
|
|
201
239
|
assert.match(await page.textContent('#gs-tip'), /\.$/);
|
|
202
240
|
await page.waitForFunction(() => /weeks? ago$/.test(document.getElementById('gs-when').textContent));
|
|
203
241
|
assert.ok(await page.evaluate(() => document.querySelectorAll('#gs-city *').length > 20), 'the city is drawn, not a placeholder');
|
|
242
|
+
const weeks = JSON.parse(CITY).weeks.length;
|
|
243
|
+
assert.equal(await page.evaluate(() => document.querySelectorAll('#gs-segs:not([hidden]) > .seg').length), weeks, 'one segment per week of the history');
|
|
204
244
|
await page.waitForFunction(() => { const w = document.getElementById('gs-when'); return !w || w.textContent === 'The city, now'; });
|
|
245
|
+
const segs = await page.evaluate(() => { const b = document.getElementById('gs-segs'); return b ? [b.querySelectorAll('.seg--done').length, b.getAttribute('aria-hidden')] : null; });
|
|
246
|
+
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
247
|
await page.waitForSelector('#game-start', { state: 'detached' });
|
|
206
248
|
await page.waitForSelector('#craft-picker');
|
|
207
249
|
assert.deepEqual(await state(page), { start: false, welcome: false, picker: true, game: 'on', full: 'on' },
|
|
@@ -211,6 +253,27 @@ test('a sign-in passes through the start screen and the load screen before the w
|
|
|
211
253
|
await ctx.close();
|
|
212
254
|
});
|
|
213
255
|
|
|
256
|
+
test('on a phone the panel is the whole width, nothing scrolls sideways and every choice clears the floor (task 1005192)', { skip }, async () => {
|
|
257
|
+
for (const [w, h] of [[390, 844], [320, 700]]) {
|
|
258
|
+
const ctx = await hall({ viewport: { width: w, height: h } });
|
|
259
|
+
const { page, errors } = await signedIn(ctx);
|
|
260
|
+
await page.waitForSelector('#game-start');
|
|
261
|
+
const m = await page.evaluate(() => {
|
|
262
|
+
const gs = document.getElementById('game-start'), r = document.querySelector('.gs__menu').getBoundingClientRect();
|
|
263
|
+
return {
|
|
264
|
+
x: Math.round(r.left), w: Math.round(r.width), sideways: gs.scrollWidth - gs.clientWidth,
|
|
265
|
+
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,
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
assert.deepEqual(m, { x: 0, w, sideways: 0, small: 0 }, `at ${w}px`);
|
|
269
|
+
await page.click('[data-gs="start"]');
|
|
270
|
+
await page.waitForSelector('#gs-load:not([hidden])');
|
|
271
|
+
assert.equal(await page.evaluate(() => { const gs = document.getElementById('game-start'); return gs.scrollWidth - gs.clientWidth; }), 0, `the load screen fits ${w}px`);
|
|
272
|
+
assert.deepEqual(errors, []);
|
|
273
|
+
await ctx.close();
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
214
277
|
test('Quit closes the tab, and says so when the browser keeps it open', { skip }, async () => {
|
|
215
278
|
const ctx = await hall();
|
|
216
279
|
const { page } = await signedIn(ctx);
|
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`);
|
|
@@ -103,7 +103,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
103
103
|
// task 1005070: the remembered nav (the rail paints before /me).
|
|
104
104
|
// task 1005088: the sidebar mark is drawn by reference to the one brand mark.
|
|
105
105
|
// task 1005144: My agents, Project settings, Permissions and Modules left for the options menu.
|
|
106
|
-
const SHELL_SHA = '
|
|
106
|
+
const SHELL_SHA = 'a4d41b30b41e49f4';
|
|
107
107
|
|
|
108
108
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
109
109
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|