@bongos/core 1.24.32 → 1.24.34

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.
@@ -0,0 +1,114 @@
1
+ // tests/craft_mode_home.mjs — choosing a craft leaves another craft's page for Home
2
+ // (task 1005067, goal 1000095).
3
+ //
4
+ // Owner, 2026-11-04: a governor on the city who picked Artistry, Ideating or Building
5
+ // stayed in the governor's city. Each craft's home is its own dashboard (Home draws it;
6
+ // the street is Governing's for now), so a page that belongs ONLY to other crafts is
7
+ // left for Home, while a shared page, a page no craft owns, and Home itself stay put.
8
+ // These cases run the REAL craft-mode.js and drive its own choose(), recording where
9
+ // the hop would go instead of navigating.
10
+ import assert from 'node:assert/strict';
11
+ import { test } from 'node:test';
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import vm from 'node:vm';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { realOTB } from './helpers.mjs';
17
+
18
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
+ const CRAFT = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/craft-mode.js'), 'utf8');
20
+
21
+ function el() {
22
+ const attrs = {};
23
+ return {
24
+ id: '', dataset: {}, children: [],
25
+ classList: { toggle() {}, add() {}, remove() {} },
26
+ setAttribute(k, v) { attrs[k] = String(v); }, getAttribute(k) { return k in attrs ? attrs[k] : null; },
27
+ hasAttribute(k) { return k in attrs; }, removeAttribute(k) { delete attrs[k]; },
28
+ appendChild(c) { this.children.push(c); return c; },
29
+ querySelector() { return null; }, querySelectorAll() { return []; },
30
+ };
31
+ }
32
+
33
+ // One page: `page` is body[data-page], `active` the nav item the shell marked.
34
+ function boot({ page, active = null, govCity = false }) {
35
+ const hops = [];
36
+ const body = el();
37
+ body.setAttribute('data-page', page);
38
+ const sidebar = el();
39
+ sidebar.querySelector = (sel) => {
40
+ if (sel !== '.nav-item--active' || !active) return null;
41
+ const a = el(); a.id = 'nav-' + active; return a;
42
+ };
43
+ const document = {
44
+ body, head: el(), documentElement: el(), readyState: 'complete',
45
+ getElementById: (id) => (id === 'app-sidebar' ? sidebar : null),
46
+ querySelector: () => null, querySelectorAll: () => [],
47
+ createElement: () => el(), createComment: () => el(),
48
+ addEventListener() {}, removeEventListener() {}, dispatchEvent() { return true; },
49
+ };
50
+ const store = () => { const o = {}; return { getItem: (k) => (k in o ? o[k] : null), setItem: (k, v) => { o[k] = String(v); }, removeItem: (k) => { delete o[k]; } }; };
51
+ const window = {
52
+ OTB: realOTB(), sessionStorage: store(), localStorage: store(),
53
+ OTBCraftHop: (h) => hops.push(h),
54
+ matchMedia: () => ({ matches: false }),
55
+ location: { search: '' },
56
+ };
57
+ if (govCity) window.GovCity = { STREET_HREF: '/builders/street' };
58
+ window.window = window;
59
+ vm.runInNewContext(CRAFT, { window, document, CustomEvent: function () {}, Promise, console });
60
+ return { CM: window.OTBCraftMode, hops };
61
+ }
62
+
63
+ test('leaving the governor\'s city for any other craft goes to Home', () => {
64
+ for (const mode of ['building', 'artistry', 'ideating']) {
65
+ const { CM, hops } = boot({ page: 'city', active: 'city' });
66
+ CM.choose(mode);
67
+ assert.deepEqual(hops, ['/'], `${mode} from the city goes Home`);
68
+ }
69
+ });
70
+
71
+ test('the street is Governing\'s own page, so other crafts leave it too', () => {
72
+ const { CM, hops } = boot({ page: 'street' });
73
+ assert.equal(CM.strandedBy('governing'), false);
74
+ CM.choose('ideating');
75
+ assert.deepEqual(hops, ['/']);
76
+ });
77
+
78
+ test('choosing Governing on another craft\'s page lands on the street', () => {
79
+ const { CM, hops } = boot({ page: 'studio', active: 'studio', govCity: true });
80
+ CM.choose('governing');
81
+ assert.deepEqual(hops, ['/builders/street']);
82
+ // without the street's script on the page, Home itself hops to the street
83
+ assert.equal(boot({ page: 'studio' }).CM.homeHref('governing'), '/');
84
+ });
85
+
86
+ test('a page the chosen craft also shows stays put', () => {
87
+ // goals is in Building, Artistry and Ideating alike
88
+ const { CM, hops } = boot({ page: 'goals', active: 'goals' });
89
+ CM.choose('ideating');
90
+ CM.choose('artistry');
91
+ assert.deepEqual(hops, []);
92
+ });
93
+
94
+ test('a page every craft keeps, and a page no craft owns, stay put', () => {
95
+ for (const [page, active] of [['roster', 'roster'], ['my-agents', 'my-agents'], ['task', null], ['idea', null]]) {
96
+ const { CM, hops } = boot({ page, active });
97
+ CM.choose('governing');
98
+ CM.choose('artistry');
99
+ assert.deepEqual(hops, [], `${page} stays`);
100
+ }
101
+ });
102
+
103
+ test('Home redraws itself for the craft, so it never hops from here', () => {
104
+ const { CM, hops } = boot({ page: 'home', active: 'home' });
105
+ CM.choose('artistry');
106
+ CM.choose('governing');
107
+ assert.deepEqual(hops, []);
108
+ });
109
+
110
+ test('the nav item wins over the page name (Economy is drachmae.html)', () => {
111
+ const { CM } = boot({ page: 'drachmae', active: 'economy' });
112
+ assert.equal(CM.strandedBy('artistry'), true);
113
+ assert.equal(CM.strandedBy('building'), false);
114
+ });
@@ -86,25 +86,33 @@ test('each looked mode frames its own door scene under the brand, and names itse
86
86
  const plate = p.plates()[0];
87
87
  assert.equal(p.brand.nextSibling, plate, `${m}: the plate sits right under the brand`);
88
88
  assert.equal(plate.getAttribute('data-plate'), m);
89
- assert.equal(plate.getAttribute('aria-hidden'), 'true', 'the plate is decoration');
90
- assert.equal(plate.innerHTML, p.craft.artHtml(m), `${m}: the plate IS the picker's door art, not a second copy`);
89
+ assert.equal(plate.tagName, 'A', `${m}: the plate is a link`);
90
+ assert.equal(plate.getAttribute('href'), '/', `${m}: the plate goes Home`);
91
+ assert.equal(plate.getAttribute('aria-label'), 'Home', `${m}: named Home for a screen reader`);
92
+ assert.ok(plate.innerHTML.startsWith(p.craft.artHtml(m)), `${m}: the plate IS the picker's door art, not a second copy`);
93
+ assert.match(plate.innerHTML, /class="craft-plate__home" aria-hidden="true"/, `${m}: a home mark sits over the scene`);
91
94
  assert.equal(p.marks().length, 1, `${m}: exactly one eyebrow`);
92
95
  assert.equal(p.title.nextSibling, p.marks()[0], `${m}: the eyebrow sits right after the page title`);
93
96
  assert.equal(p.marks()[0].textContent, WORDS[m], `${m}: in the prototype's own words`);
94
97
  }
95
98
  });
96
99
 
97
- test('BUILDING AND NO MODE WEAR NO LOOK: no plate, no eyebrow, and switching to them takes the look away', () => {
100
+ test('BUILDING AND NO MODE WEAR NO LOOK: no eyebrow; Building keeps only the Home plate, showing the city', () => {
98
101
  const none = boot();
99
102
  assert.equal(none.root.dataset.craft, undefined);
100
103
  assert.equal(none.plates().length + none.marks().length, 0, 'no mode draws nothing');
101
104
  for (const m of ['building']) {
102
105
  const p = boot({ 'hall-craft': m });
103
- assert.equal(p.plates().length + p.marks().length, 0, `${m} at boot draws nothing`);
106
+ assert.equal(p.marks().length, 0, `${m} at boot names no look`);
107
+ assert.equal(p.plates().length, 1, `${m}: the Home plate is there too (task 1005065)`);
108
+ assert.equal(p.plates()[0].getAttribute('data-plate'), m);
109
+ assert.ok(p.plates()[0].innerHTML.startsWith(p.craft.artHtml('governing')), `${m}: its plate shows the city`);
104
110
  p.craft.choose('governing');
105
111
  assert.equal(p.plates().length, 1);
112
+ assert.equal(p.plates()[0].getAttribute('data-plate'), 'governing', 'switching mode swaps the plate');
106
113
  p.craft.choose(m);
107
- assert.equal(p.plates().length + p.marks().length, 0, `switching to ${m} removes the plate and the eyebrow`);
114
+ assert.equal(p.marks().length, 0, `switching to ${m} removes the eyebrow`);
115
+ assert.equal(p.plates()[0].getAttribute('data-plate'), m, 'and swaps the plate back');
108
116
  }
109
117
  assert.deepEqual(Object.keys(none.craft.LOOKS).sort(), [...LOOKED].sort(), 'only the three crafts have a look');
110
118
  });
@@ -112,7 +120,7 @@ test('BUILDING AND NO MODE WEAR NO LOOK: no plate, no eyebrow, and switching to
112
120
  test('the art is drawn once: craft-mode.js has one artHtml and exports it', () => {
113
121
  assert.equal(CRAFT.match(/function artHtml\(/g).length, 1);
114
122
  assert.match(CRAFT, /artHtml: artHtml,/);
115
- assert.match(CRAFT, /plate\.innerHTML = artHtml\(mode\);/);
123
+ assert.match(CRAFT, /plate\.innerHTML = artHtml\(art\) \+ HOME_MARK;/);
116
124
  });
117
125
 
118
126
  // Every selector in style.css that names a craft mode.
@@ -163,6 +171,8 @@ test('the shared treatment, the eyebrow and the plate are written as the directi
163
171
  }
164
172
  assert.match(CSS, /@media \(max-width: 640px\) \{ \.craft-eyebrow \{ display: none; \} \}/, 'the eyebrow is hidden on a phone');
165
173
  assert.match(CSS, /\.craft-plate \{[^}]*height: 40px;/, '40px in the closed rail');
174
+ assert.match(CSS, /\.craft-plate__home \{[^}]*width: 24px;[^}]*height: 24px;/, 'the home mark clears the 24px floor');
175
+ assert.match(CSS, /\.craft-plate:focus-visible \{ outline: 2px solid var\(--focus\);/, 'the Home plate shows keyboard focus');
166
176
  assert.match(CSS, /\.app-sidebar:focus-within \.craft-plate \{ height: 64px;/, '64px when the rail opens');
167
177
  assert.match(CSS, /html\[data-theme="light"\] \.craft-plate \.craft-art__city \{ filter: invert\(1\) hue-rotate\(180deg\); \}/,
168
178
  'the light city is the dark one inverted, not a second drawing');
@@ -0,0 +1,70 @@
1
+ // tests/hall_sitemap.mjs — the builders hall's generated sitemap stays fresh, and no hall
2
+ // link is broken (task 1005068, goal 1000095).
3
+ //
4
+ // docs/hall-sitemap.md is written by scripts/bongos/gen-hall-sitemap.js from the real
5
+ // sources: the page files, shell.js NAV, craft-mode.js's tabs (run, not read) and every
6
+ // in-hall link. It is maintained by regeneration, never by hand, so this fails the moment
7
+ // a page, a nav item, a craft's tabs or a link changes without a regenerate, and the
8
+ // moment any link points at a URL the hall does not serve.
9
+ import assert from 'node:assert/strict';
10
+ import { test } from 'node:test';
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { createRequire } from 'node:module';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
+ const require = createRequire(import.meta.url);
18
+ const gen = require('../scripts/bongos/gen-hall-sitemap.js');
19
+ const model = gen.build();
20
+
21
+ test('the parse found the hall, so nothing below passes against an empty map', () => {
22
+ const files = fs.readdirSync(path.join(ROOT, 'modules/hall-ui/public')).filter((f) => f.endsWith('.html'));
23
+ assert.equal(model.pages.length, files.length, 'one row per page file');
24
+ assert.ok(model.nav.length >= 25, `expected the shell's nav, found ${model.nav.length} items`);
25
+ for (const c of ['building', 'artistry', 'ideating', 'governing']) {
26
+ assert.ok(model.crafts.tabs[c].length > model.crafts.always.length, `${c} lists tabs of its own`);
27
+ }
28
+ assert.ok(model.chrome.includes('shell.js') && model.chrome.includes('craft-mode.js'), 'the chrome is the shell');
29
+ assert.ok(model.pages.reduce((n, p) => n + p.out.length, 0) >= 40, 'the pages link to each other');
30
+ });
31
+
32
+ test('docs/hall-sitemap.md is what the generator writes now', () => {
33
+ const committed = fs.readFileSync(path.join(ROOT, gen.OUT), 'utf8');
34
+ assert.ok(committed === gen.render(model),
35
+ `${gen.OUT} is stale — run: node scripts/bongos/gen-hall-sitemap.js`);
36
+ });
37
+
38
+ test('no hall link points at a URL the hall does not serve', () => {
39
+ assert.deepEqual(model.findings.broken, [],
40
+ 'fix the link, or name the server route in ROUTES in scripts/bongos/gen-hall-sitemap.js');
41
+ });
42
+
43
+ test('every craft tab is a real nav item', () => {
44
+ assert.deepEqual(model.findings.dangling, []);
45
+ });
46
+
47
+ test('a link is read the ways the hall writes one, and an unknown one is broken', () => {
48
+ const urls = new Set(model.pages.map((p) => p.url));
49
+ const found = gen.linksIn([
50
+ '<a href="/goals">', "x.innerHTML = '<a href=\"/builders/roster?id=2\">';",
51
+ "a.href = '/work';", "{ href: '/profile' }", "location.assign('/settings#x')",
52
+ "const STREET_HREF = '/builders/street';", '`<a href="${hallBase()}/idea/${id}">`',
53
+ "function itemHref(id) { return '/market-watch-item?id=' + id; }",
54
+ '`<a href="${hub}/connections">`', "fetch(API + '/goals')",
55
+ ].join('\n')).map((h) => gen.classify(h, urls));
56
+ assert.deepEqual(found.sort(), ['/goals', '/idea', '/market-watch-item', '/profile', '/roster', '/settings', '/street', '/work'].sort(),
57
+ 'each written form is a link to its page; a hub page and an API path are not hall links');
58
+ assert.equal(gen.classify('/no-such-page', urls), false, 'an unknown target is broken');
59
+ assert.equal(gen.classify('/task/12', urls), '/task', 'a task id lands on the task page');
60
+ assert.equal(gen.classify('/people', urls), '/roster', 'a retired URL follows its redirect');
61
+ assert.equal(gen.classify('/builders/style.css?v=1', urls), null, 'an asset is not a page link');
62
+ });
63
+
64
+ test('the street is Governing\'s home: no nav item, reached by clicking from the city', () => {
65
+ const street = model.pages.find((p) => p.url === '/street');
66
+ assert.ok(street, 'the street is a hall page');
67
+ assert.deepEqual(street.nav, []);
68
+ assert.ok(street.reachIn.includes('governing'));
69
+ assert.ok(street.in.includes('/city'), 'the city links to the street');
70
+ });