@bongos/core 1.21.164 → 1.21.166
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 +34 -29
- package/docs/copy-inventory.md +91 -91
- package/docs/copy-registry.json +126 -116
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +304 -305
- package/modules/builder-settings/builder-needs.js +5 -0
- package/modules/hall-ui/public/builders.js +3 -17
- package/modules/hall-ui/public/craft-mode.js +32 -158
- package/modules/hall-ui/public/craft-review.css +7 -120
- package/modules/hall-ui/public/hall-render.js +8 -24
- package/modules/hall-ui/public/index.html +3 -4
- package/modules/hall-ui/public/page-notes.css +64 -1
- package/modules/hall-ui/public/page-notes.js +96 -11
- package/modules/hall-ui/public/settings.js +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/src/bongos/routes/me.js +3 -0
- package/src/module-api.js +1 -1
- package/tests/craft_review_mode.mjs +21 -104
- package/tests/hall_board_world.mjs +18 -39
- package/tests/home_attention_cards.mjs +6 -2
- package/tests/page_notes_panel.mjs +161 -27
- package/tests/settings_art_key_absent.mjs +137 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// tests/page_notes_panel.mjs — the hall side of the artist's page feedback doc
|
|
2
|
-
// (task 1004496): the
|
|
3
|
-
// reader in page-notes.js. The server half is tests/page_notes.mjs.
|
|
2
|
+
// (task 1004496): the docked review panel craft-mode.js puts at the right edge
|
|
3
|
+
// in Artistry (task 1004771: no top-bar button), and the reader in page-notes.js. The server half is tests/page_notes.mjs.
|
|
4
4
|
//
|
|
5
|
-
// Runs the REAL craft-mode.js over a small DOM: the
|
|
6
|
-
// alone and
|
|
7
|
-
//
|
|
5
|
+
// Runs the REAL craft-mode.js over a small DOM: the panel docks in Artistry
|
|
6
|
+
// alone and closes when the mode changes, page-notes.js loads once (same ?v as
|
|
7
|
+
// craft-mode.js) and a ?note= link opens the doc in any mode. Then
|
|
8
8
|
// the REAL page-notes.js reader: it escapes the doc and shows only this module's
|
|
9
9
|
// own images, which is what keeps a doc from pulling a picture off another site.
|
|
10
10
|
import assert from 'node:assert/strict';
|
|
@@ -63,7 +63,7 @@ function boot({ session = {}, search = '' } = {}) {
|
|
|
63
63
|
// "Loading" page-notes.js: record the tag, then define the API it would define.
|
|
64
64
|
head.onAppend = (tag) => {
|
|
65
65
|
loaded.push(tag.src);
|
|
66
|
-
window.OTBPageNotes = { open: () => opened.push('open'), openNote: (id) => opened.push('note ' + id) };
|
|
66
|
+
window.OTBPageNotes = { open: () => opened.push('open'), openNote: (id) => opened.push('note ' + id), dock: () => opened.push('dock'), close: () => opened.push('close') };
|
|
67
67
|
tag.onload();
|
|
68
68
|
};
|
|
69
69
|
const sandbox = { window, document, Promise, CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } } };
|
|
@@ -74,36 +74,31 @@ function boot({ session = {}, search = '' } = {}) {
|
|
|
74
74
|
}
|
|
75
75
|
const flush = () => new Promise((r) => setTimeout(r, 0));
|
|
76
76
|
|
|
77
|
-
test('THE
|
|
77
|
+
test('THE PANEL: docked at the edge in Artistry alone, with no top-bar button, and closed when the mode changes', async () => {
|
|
78
78
|
const p = boot({ session: { 'hall-craft': 'artistry' } });
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
assert.
|
|
83
|
-
|
|
84
|
-
assert.
|
|
85
|
-
assert.equal(p.tools()[0].getAttribute('aria-label'), 'Artist review: write feedback on this page', 'one name with the public site\'s button (task 1004499)');
|
|
86
|
-
for (const m of ['building', 'governing']) {
|
|
87
|
-
p.craft.choose(m);
|
|
88
|
-
assert.equal(p.tools().length, 0, `no palette in ${m}`);
|
|
89
|
-
}
|
|
79
|
+
await flush();
|
|
80
|
+
assert.deepEqual(p.tools().map((t) => t.id), ['craft-tool-paintbrush'], 'the top bar keeps Edit words alone (task 1004771)');
|
|
81
|
+
assert.equal(p.tools()[0].nextSibling, p.sw, 'right before the mode switch');
|
|
82
|
+
assert.deepEqual(p.opened, ['dock'], 'the review panel docks instead');
|
|
83
|
+
for (const m of ['building', 'governing']) p.craft.choose(m);
|
|
84
|
+
assert.deepEqual(p.opened, ['dock', 'close', 'close'], 'and leaves with Artistry');
|
|
90
85
|
p.craft.choose('ideating');
|
|
91
|
-
assert.deepEqual(p.tools().map((t) => t.id), ['craft-tool-lightbulb'], 'Ideating
|
|
92
|
-
p.craft.choose('artistry');
|
|
86
|
+
assert.deepEqual(p.tools().map((t) => t.id), ['craft-tool-lightbulb'], 'Ideating shows its lightbulb alone (task 1004498)');
|
|
93
87
|
p.craft.choose('artistry');
|
|
94
|
-
|
|
95
|
-
assert.equal(
|
|
88
|
+
await flush();
|
|
89
|
+
assert.equal(p.opened.filter((x) => x === 'dock').length, 2, 'back in Artistry it docks again');
|
|
90
|
+
const none = boot();
|
|
91
|
+
await flush();
|
|
92
|
+
assert.equal(none.tools().length, 0, 'no tools with no mode at all');
|
|
93
|
+
assert.deepEqual(none.loaded, [], 'and nothing loads');
|
|
96
94
|
});
|
|
97
95
|
|
|
98
|
-
test('
|
|
96
|
+
test('the panel script loads once, at the same version as craft-mode.js', async () => {
|
|
99
97
|
const p = boot({ session: { 'hall-craft': 'artistry' } });
|
|
100
|
-
assert.deepEqual(p.loaded, [], 'nothing loads until the palette is pressed');
|
|
101
|
-
p.tools()[0].click();
|
|
102
98
|
await flush();
|
|
103
|
-
p.
|
|
99
|
+
p.craft.choose('artistry');
|
|
104
100
|
await flush();
|
|
105
101
|
assert.deepEqual(p.loaded, ['/builders/page-notes.js?v=2026-10-04-test']);
|
|
106
|
-
assert.deepEqual(p.opened, ['open', 'open']);
|
|
107
102
|
});
|
|
108
103
|
|
|
109
104
|
test('a ?note= link opens that doc on arrival, in any mode', async () => {
|
|
@@ -152,6 +147,145 @@ test('colours live in the sheets, and the panel and tool are styled there', () =
|
|
|
152
147
|
// the hall pulls it in through style.css, ahead of every rule.
|
|
153
148
|
const pn = fs.readFileSync(path.join(HALL, 'page-notes.css'), 'utf8');
|
|
154
149
|
for (const sel of ['.pn {', '.pn__panel {', '.pn__text {', '.pn-doc__img {']) assert.ok(pn.includes(sel) && !css.includes(sel), sel);
|
|
150
|
+
// Task 1004771: the docked, scrimless look is the review panel's alone
|
|
151
|
+
// (.pn--dock). The lightbulb rides plain .pn and keeps its scrim and its
|
|
152
|
+
// scroll lock, so docking the review panel cannot quietly change it.
|
|
153
|
+
assert.match(pn, /html\.pn-open \{ overflow: hidden; \}/, 'the lightbulb keeps its scroll lock');
|
|
154
|
+
assert.match(pn, /\.pn \{[^}]*inset: 0;[^}]*background: var\(--pn-scrim\);/, 'and its scrim');
|
|
155
|
+
assert.match(pn, /\.pn\.pn--dock \{[^}]*background: none;/, 'the docked review panel drops both');
|
|
156
|
+
assert.match(NOTES, /sheet\.className = docked \? 'pn pn--dock' : 'pn';/, 'and only a docked review panel wears it');
|
|
157
|
+
const bulb = fs.readFileSync(path.join(HALL, 'lightbulb.js'), 'utf8');
|
|
158
|
+
assert.doesNotMatch(bulb, /pn--dock/, 'the lightbulb does not');
|
|
155
159
|
const at = css.indexOf('@import url("page-notes.css");');
|
|
156
160
|
assert.ok(at !== -1 && at < css.indexOf('{'), 'style.css imports the sheet before any rule');
|
|
157
161
|
});
|
|
162
|
+
|
|
163
|
+
// ---------- the docked panel, REAL page-notes.js (task 1004771) ----------
|
|
164
|
+
// A small DOM that runs the real sheet: querySelector hands back one cached
|
|
165
|
+
// fake per selector, so the tab, the X and the mode row are real listeners.
|
|
166
|
+
function dockedPanel({ reviewing = null, opened = false } = {}) {
|
|
167
|
+
const calls = [];
|
|
168
|
+
let sheet = null;
|
|
169
|
+
const fake = (name) => {
|
|
170
|
+
const attrs = {}, listeners = {}, classes = new Set();
|
|
171
|
+
const kids = {};
|
|
172
|
+
const el = {
|
|
173
|
+
name, innerHTML: '', parentNode: null, id: '', className: '',
|
|
174
|
+
setAttribute(k, v) { attrs[k] = String(v); }, getAttribute(k) { return k in attrs ? attrs[k] : null; },
|
|
175
|
+
hasAttribute(k) { return k in attrs; }, removeAttribute(k) { delete attrs[k]; },
|
|
176
|
+
classList: { add: (c) => classes.add(c), remove: (c) => classes.delete(c), contains: (c) => classes.has(c),
|
|
177
|
+
toggle: (c, on) => { if (on === undefined) on = !classes.has(c); if (on) classes.add(c); else classes.delete(c); return on; } },
|
|
178
|
+
addEventListener(t, fn) { (listeners[t] ||= []).push(fn); },
|
|
179
|
+
fire(t, ev = {}) { (listeners[t] || []).forEach((fn) => fn({ target: el, preventDefault() {}, ...ev })); },
|
|
180
|
+
querySelector(sel) { return (kids[sel] ||= Object.assign(fake(sel), { parentNode: el })); },
|
|
181
|
+
querySelectorAll() { return []; },
|
|
182
|
+
contains(n) { for (let x = n; x; x = x.parentNode) if (x === el) return true; return false; },
|
|
183
|
+
focus() { document.activeElement = el; },
|
|
184
|
+
};
|
|
185
|
+
return el;
|
|
186
|
+
};
|
|
187
|
+
const body = fake('body');
|
|
188
|
+
body.appendChild = (c) => { c.parentNode = body; return c; };
|
|
189
|
+
body.removeChild = (c) => { c.parentNode = null; return c; };
|
|
190
|
+
body.dataset = { page: 'studio', pageTitle: 'Studio' };
|
|
191
|
+
const docKeys = [];
|
|
192
|
+
const document = {
|
|
193
|
+
body, activeElement: null, title: 'Studio — hall', documentElement: fake('html'),
|
|
194
|
+
createElement: () => (sheet = fake('div')),
|
|
195
|
+
addEventListener: (t, fn) => { if (t === 'keydown') docKeys.push(fn); },
|
|
196
|
+
removeEventListener() {},
|
|
197
|
+
};
|
|
198
|
+
const fetch = (url, opts) => {
|
|
199
|
+
calls.push(opts.method + ' ' + url);
|
|
200
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ note: { id: 9, status: 'draft', body_md: '' }, images: [], notes: [] }) });
|
|
201
|
+
};
|
|
202
|
+
const craft = {
|
|
203
|
+
reviewing: () => reviewing,
|
|
204
|
+
review: (id) => { reviewing = id; calls.push('review ' + id); },
|
|
205
|
+
stopReview: () => { reviewing = null; calls.push('stop'); },
|
|
206
|
+
experiences: () => [{ id: 'ideating', label: 'Ideator experience', short: 'Ideating' }, { id: 'building', label: 'Building experience', short: 'Building' }],
|
|
207
|
+
iconHtml: () => '<svg></svg>',
|
|
208
|
+
};
|
|
209
|
+
const window = { location: { pathname: '/studio' }, OTBCraftMode: craft };
|
|
210
|
+
const sandbox = { window, document, fetch, setTimeout, clearTimeout, Promise };
|
|
211
|
+
vm.createContext(sandbox);
|
|
212
|
+
vm.runInContext(NOTES, sandbox, { filename: 'page-notes.js' });
|
|
213
|
+
const api = window.OTBPageNotes;
|
|
214
|
+
if (opened) api.open(); else api.dock();
|
|
215
|
+
const press = (attr, value = '') => {
|
|
216
|
+
const btn = fake('button');
|
|
217
|
+
btn.setAttribute(attr, value);
|
|
218
|
+
sheet.fire('click', { target: { closest: () => btn } });
|
|
219
|
+
};
|
|
220
|
+
return { api, calls, document, docKeys, body, press, html: document.documentElement, get sheet() { return sheet; }, collapsed: () => sheet.classList.contains('pn--collapsed') };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
test('DOCKED: it waits collapsed behind its tab and asks for no draft until it is first shown', async () => {
|
|
224
|
+
const p = dockedPanel();
|
|
225
|
+
await flush();
|
|
226
|
+
assert.ok(p.collapsed(), 'tucked away on arrival');
|
|
227
|
+
assert.equal(p.sheet.parentNode, p.body, 'but on the page, so its tab shows');
|
|
228
|
+
assert.deepEqual(p.calls, [], 'no request while nobody has looked');
|
|
229
|
+
p.sheet.querySelector('[data-pn-tab]').fire('pointerenter', { pointerType: 'mouse' });
|
|
230
|
+
await flush();
|
|
231
|
+
assert.ok(!p.collapsed(), 'a mouse on the tab opens it');
|
|
232
|
+
assert.ok(p.calls.some((c) => /^POST /.test(c)), 'and the draft opens the first time it is shown');
|
|
233
|
+
const drafts = p.calls.filter((c) => /^POST /.test(c)).length;
|
|
234
|
+
p.sheet.fire('pointerleave', { pointerType: 'mouse' });
|
|
235
|
+
assert.ok(p.collapsed(), 'leaving the panel for the page tucks it away');
|
|
236
|
+
p.sheet.querySelector('[data-pn-tab]').fire('pointerenter', { pointerType: 'mouse' });
|
|
237
|
+
await flush();
|
|
238
|
+
assert.equal(p.calls.filter((c) => /^POST /.test(c)).length, drafts, 'showing it again opens no second draft');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('DOCKED: a touch never peeks, the tab click toggles, Escape and the X tuck it away and keep the tab', async () => {
|
|
242
|
+
const p = dockedPanel();
|
|
243
|
+
p.sheet.querySelector('[data-pn-tab]').fire('pointerenter', { pointerType: 'touch' });
|
|
244
|
+
assert.ok(p.collapsed(), 'a touch is not a hover');
|
|
245
|
+
p.press('data-pn-tab');
|
|
246
|
+
await flush();
|
|
247
|
+
assert.ok(!p.collapsed(), 'the tab click opens it (touch and keyboard)');
|
|
248
|
+
assert.equal(p.sheet.querySelector('[data-pn-tab]').getAttribute('aria-expanded'), 'true');
|
|
249
|
+
p.sheet.querySelector('[data-pn-close]').focus();
|
|
250
|
+
p.docKeys.forEach((fn) => fn({ key: 'Escape', preventDefault() {} }));
|
|
251
|
+
assert.ok(p.collapsed(), 'Escape inside the panel tucks it away');
|
|
252
|
+
assert.equal(p.sheet.querySelector('[data-pn-tab]').getAttribute('aria-expanded'), 'false');
|
|
253
|
+
p.press('data-pn-tab');
|
|
254
|
+
p.press('data-pn-close');
|
|
255
|
+
assert.ok(p.collapsed(), 'docked, the X tucks it away too');
|
|
256
|
+
assert.equal(p.sheet.parentNode, p.body, 'and leaves the tab on the page, the only way back in');
|
|
257
|
+
p.api.close();
|
|
258
|
+
assert.equal(p.sheet.parentNode, null, 'leaving Artistry (close) is what removes it');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('THE MODE ROW names each mode in the mode switch word and starts a review on a press', () => {
|
|
262
|
+
const p = dockedPanel();
|
|
263
|
+
assert.match(p.sheet.innerHTML, /<span>Ideating<\/span>/, 'the switch says Ideating, so the row does');
|
|
264
|
+
assert.doesNotMatch(p.sheet.innerHTML, />Ideator</);
|
|
265
|
+
assert.doesNotMatch(p.sheet.innerHTML, /Stop reviewing/, 'no way out of a review that has not started');
|
|
266
|
+
p.press('data-pn-mode', 'building');
|
|
267
|
+
assert.ok(p.calls.includes('review building'));
|
|
268
|
+
assert.match(p.sheet.querySelector('#pn-modes').innerHTML, /data-pn-mode="building" aria-pressed="true"/, 'the row repaints with the live one pressed');
|
|
269
|
+
assert.match(p.sheet.querySelector('#pn-modes').innerHTML, /Stop reviewing/);
|
|
270
|
+
p.press('data-pn-mode', '');
|
|
271
|
+
assert.ok(p.calls.includes('stop'), 'Stop reviewing ends it');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('OPENED, not docked (the public site Artist review button): the modal sheet it always was', async () => {
|
|
275
|
+
const p = dockedPanel({ opened: true });
|
|
276
|
+
await flush();
|
|
277
|
+
assert.equal(p.sheet.className, 'pn', 'no docked look: the scrim and full-viewport sheet stay');
|
|
278
|
+
assert.ok(p.html.classList.contains('pn-open'), 'the page behind is scroll-locked');
|
|
279
|
+
assert.match(p.sheet.innerHTML, /role="dialog" aria-modal="true"/);
|
|
280
|
+
assert.doesNotMatch(p.sheet.innerHTML, /data-pn-tab/, 'no tab');
|
|
281
|
+
assert.ok(p.calls.some((c) => /^POST /.test(c)), 'the draft opens straight away');
|
|
282
|
+
p.sheet.fire('pointerleave', { pointerType: 'mouse' });
|
|
283
|
+
assert.ok(!p.collapsed(), 'a pointer leaving does not tuck a modal sheet');
|
|
284
|
+
p.docKeys.forEach((fn) => fn({ key: 'Escape', preventDefault() {} }));
|
|
285
|
+
assert.equal(p.sheet.parentNode, null, 'Escape closes it');
|
|
286
|
+
assert.ok(!p.html.classList.contains('pn-open'), 'and lifts the scroll lock');
|
|
287
|
+
const q = dockedPanel({ opened: true });
|
|
288
|
+
const s = q.sheet;
|
|
289
|
+
s.fire('click', { target: s });
|
|
290
|
+
assert.equal(s.parentNode, null, 'a click on the scrim closes it');
|
|
291
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// tests/settings_art_key_absent.mjs — the image-generation key card on a project
|
|
2
|
+
// with no art pipeline (task 1004786).
|
|
3
|
+
//
|
|
4
|
+
// The key routes (/me/art-key/*) left the core with the art-pipeline module, but
|
|
5
|
+
// the Settings card and the art_key need stayed. On cloudbongos.com — which has
|
|
6
|
+
// no art pipeline — every builder past Xenos was told "your free art coverage has
|
|
7
|
+
// ended, add your key", and the card the nudge pointed at could never load, so
|
|
8
|
+
// there was nowhere to add one. Two halves, both pinned here:
|
|
9
|
+
//
|
|
10
|
+
// - builder-needs: with the art pipeline absent (ctx.artPipeline === false) the
|
|
11
|
+
// art_key need is not raised at all — nothing to nag about, nothing to add;
|
|
12
|
+
// - settings.js: a 404 from GET /me/art-key/own means the routes are not
|
|
13
|
+
// installed, so the section stays hidden instead of showing a reload error.
|
|
14
|
+
// A real failure (500) still shows the error.
|
|
15
|
+
//
|
|
16
|
+
// Pure vm + file-read (no DB/network) — runs in the DB-free unit gate.
|
|
17
|
+
import { strict as assert } from 'node:assert';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import vm from 'node:vm';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/settings.js'), 'utf8');
|
|
26
|
+
const ME = fs.readFileSync(path.join(ROOT, 'src/bongos/routes/me.js'), 'utf8');
|
|
27
|
+
const needs = createRequire(import.meta.url)('../modules/builder-settings/builder-needs.js');
|
|
28
|
+
|
|
29
|
+
let passed = 0, failed = 0;
|
|
30
|
+
async function t(name, fn) {
|
|
31
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
32
|
+
catch (e) { failed++; console.error(` FAIL ${name}\n ${e.message}`); }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeEl() {
|
|
36
|
+
return {
|
|
37
|
+
hidden: false, textContent: '', value: '', disabled: false, href: '', className: '',
|
|
38
|
+
dataset: {}, style: {}, children: [], _html: '',
|
|
39
|
+
set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; },
|
|
40
|
+
addEventListener() {}, appendChild(c) { return c; },
|
|
41
|
+
append() {}, prepend() {}, remove() {}, setAttribute() {}, removeAttribute() {}, getAttribute() { return null; },
|
|
42
|
+
querySelector() { return null; }, querySelectorAll() { return []; }, closest() { return null; },
|
|
43
|
+
classList: { add() {}, remove() {}, toggle() {}, contains() { return false; } },
|
|
44
|
+
focus() {},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Boot the real settings.js; GET /me/art-key/own answers with `artKeyStatus`.
|
|
49
|
+
async function boot(artKeyStatus) {
|
|
50
|
+
const els = new Map();
|
|
51
|
+
const $ = (sel) => { if (!els.has(sel)) els.set(sel, makeEl()); return els.get(sel); };
|
|
52
|
+
// The markup ships both hidden (settings.html), so the stub starts them hidden.
|
|
53
|
+
$('#art-key').hidden = true;
|
|
54
|
+
$('#artkey-banner').hidden = true;
|
|
55
|
+
const builder = { id: 4, github_login: 'b4', rank: 'thetes', preferred_disciplines: ['engineer'] };
|
|
56
|
+
const me = { builder, needs: { items: [needs.artKeyNeed({ rank: 'thetes' })] } };
|
|
57
|
+
const api = {
|
|
58
|
+
async request(method, url) {
|
|
59
|
+
if (method === 'GET' && url === '/api/bongos/me') return { status: 200, ok: true, data: me };
|
|
60
|
+
if (method === 'GET' && url === '/api/bongos/me/art-key/own') {
|
|
61
|
+
return artKeyStatus === 200
|
|
62
|
+
? { status: 200, ok: true, data: { set: false, storage_configured: true } }
|
|
63
|
+
: { status: artKeyStatus, ok: false, data: null };
|
|
64
|
+
}
|
|
65
|
+
return { status: 404, ok: false, data: null };
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const escapeHtml = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
69
|
+
const document = {
|
|
70
|
+
getElementById: (id) => $(`#${id}`), createElement: () => makeEl(),
|
|
71
|
+
dispatchEvent() {}, addEventListener() {}, querySelector: () => null, querySelectorAll: () => [],
|
|
72
|
+
};
|
|
73
|
+
const window = {
|
|
74
|
+
__BRANDING__: {}, __MODULES__: { enabled: {} },
|
|
75
|
+
BongosClient: { createClient: () => api },
|
|
76
|
+
OTB: {
|
|
77
|
+
$, escapeHtml, toast() {}, disciplines: () => ['engineer'], roleLabel: (d) => d,
|
|
78
|
+
hubOrigin: () => null, settingsPanels: () => [],
|
|
79
|
+
},
|
|
80
|
+
OTBKit: { emptyStateHtml: (m) => m, makeSectionRail: () => ({ refresh() {}, activate() {} }) },
|
|
81
|
+
location: { hash: '', pathname: '/settings', search: '', assign() {} },
|
|
82
|
+
scrollTo() {}, confirm: () => false, addEventListener() {},
|
|
83
|
+
};
|
|
84
|
+
const sandbox = {
|
|
85
|
+
window, document, console: { log() {}, warn() {}, error() {}, info() {} },
|
|
86
|
+
history: { replaceState() {} }, URLSearchParams, setTimeout, clearTimeout,
|
|
87
|
+
};
|
|
88
|
+
sandbox.globalThis = sandbox;
|
|
89
|
+
vm.createContext(sandbox);
|
|
90
|
+
vm.runInContext(SRC, sandbox, { filename: 'settings.js' });
|
|
91
|
+
for (let i = 0; i < 12; i++) await new Promise((r) => setImmediate(r));
|
|
92
|
+
return $;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('\nthe art_key need without an art pipeline:');
|
|
96
|
+
|
|
97
|
+
await t('artPipeline: false raises no art_key need at any rank', () => {
|
|
98
|
+
for (const rank of ['xenos', 'thetes', 'metic', 'archon']) {
|
|
99
|
+
assert.equal(needs.artKeyNeed({ rank, artPipeline: false }), null, rank);
|
|
100
|
+
const r = needs.computeNeeds({ rank, artPipeline: false });
|
|
101
|
+
assert.ok(!r.items.some((n) => n.id === 'art_key'), rank);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await t('with the pipeline present (or unstated) the need is unchanged', () => {
|
|
106
|
+
assert.equal(needs.artKeyNeed({ rank: 'thetes', artPipeline: true }).state, 'action_needed');
|
|
107
|
+
assert.equal(needs.artKeyNeed({ rank: 'thetes' }).state, 'action_needed');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await t('/me tells computeNeeds whether the art pipeline is installed', () => {
|
|
111
|
+
const call = ME.slice(ME.indexOf('settings.computeNeeds({'), ME.indexOf('isFoundingOwner'));
|
|
112
|
+
assert.match(call, /artPipeline:/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
console.log('\nthe Settings card:');
|
|
116
|
+
|
|
117
|
+
await t('routes not installed (404) → the section stays hidden, no reload error', async () => {
|
|
118
|
+
const $ = await boot(404);
|
|
119
|
+
assert.equal($('#art-key').hidden, true);
|
|
120
|
+
assert.equal($('#artkey-banner').hidden, true);
|
|
121
|
+
assert.doesNotMatch($('#artkey-card').innerHTML, /could not be loaded/);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await t('a real failure (500) still shows the error', async () => {
|
|
125
|
+
const $ = await boot(500);
|
|
126
|
+
assert.equal($('#art-key').hidden, false);
|
|
127
|
+
assert.match($('#artkey-card').innerHTML, /could not be loaded/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await t('routes present → the key box renders', async () => {
|
|
131
|
+
const $ = await boot(200);
|
|
132
|
+
assert.equal($('#art-key').hidden, false);
|
|
133
|
+
assert.match($('#artkey-card').innerHTML, /id="artkey-input"/);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
137
|
+
process.exit(failed ? 1 : 0);
|