@bongos/core 1.24.50 → 1.24.51
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 +25 -20
- package/docs/copy-inventory.md +62 -64
- package/docs/copy-registry.json +230 -238
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +2 -2
- package/docs/page-readings.json +70 -86
- package/modules/status-ui/public/index.html +43 -43
- package/modules/status-ui/public/status.js +26 -27
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/loading_mark.mjs +6 -6
- package/tests/status_company_attribution.mjs +1 -1
- package/tests/status_page_wording.mjs +129 -0
|
@@ -56,10 +56,9 @@ function fmtUsd(n) {
|
|
|
56
56
|
return Math.round(Number(n)).toLocaleString();
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function
|
|
59
|
+
function fmtMoney(n) {
|
|
60
60
|
if (n == null) return '—';
|
|
61
|
-
|
|
62
|
-
return `${Math.round(Number(n)).toLocaleString()} dr.`;
|
|
61
|
+
return `$${fmtUsd(n)}`;
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
function fmtRelativeTime(iso) {
|
|
@@ -99,13 +98,13 @@ async function fetchJson(path) {
|
|
|
99
98
|
// in ~756px of content width without wrapping.
|
|
100
99
|
|
|
101
100
|
const SECTIONS = [
|
|
102
|
-
{ id: 'headline-tablet', label: '
|
|
103
|
-
{ id: 'leader-tablet', label: '
|
|
104
|
-
{ id: 'foundry-tablet', label: '
|
|
105
|
-
{ id: 'versions-tablet', label: '
|
|
106
|
-
{ id: 'cost-tablet', label: '
|
|
107
|
-
{ id: 'shipped-tablet', label: '
|
|
108
|
-
{ id: 'world-tablet', label: '
|
|
101
|
+
{ id: 'headline-tablet', label: 'Overview' },
|
|
102
|
+
{ id: 'leader-tablet', label: 'Builders' },
|
|
103
|
+
{ id: 'foundry-tablet', label: 'Trends' },
|
|
104
|
+
{ id: 'versions-tablet', label: 'In progress' },
|
|
105
|
+
{ id: 'cost-tablet', label: 'Spending' },
|
|
106
|
+
{ id: 'shipped-tablet', label: 'Shipped' },
|
|
107
|
+
{ id: 'world-tablet', label: 'Snapshot' },
|
|
109
108
|
];
|
|
110
109
|
|
|
111
110
|
let navObserver = null;
|
|
@@ -354,7 +353,7 @@ async function loadVersions() {
|
|
|
354
353
|
if (rows.length === 0) {
|
|
355
354
|
const p = document.createElement('p');
|
|
356
355
|
p.className = 'empty';
|
|
357
|
-
p.textContent = '
|
|
356
|
+
p.textContent = 'Nothing is in progress right now.';
|
|
358
357
|
els.versions.appendChild(p);
|
|
359
358
|
return;
|
|
360
359
|
}
|
|
@@ -497,7 +496,7 @@ function renderCostFilterNote(data) {
|
|
|
497
496
|
const filtered = Number(data.filtered_total_usd) || 0;
|
|
498
497
|
const whole = Number(data.total_usd) || 0;
|
|
499
498
|
const pct = whole > 0 ? Math.round((filtered / whole) * 100) : 0;
|
|
500
|
-
note.textContent = `Breakdown filtered to ${parts.join(' · ')}: ${
|
|
499
|
+
note.textContent = `Breakdown filtered to ${parts.join(' · ')}: ${fmtMoney(filtered)} of ${fmtMoney(whole)} (${pct}% of all spend).`;
|
|
501
500
|
}
|
|
502
501
|
|
|
503
502
|
async function loadCosts() {
|
|
@@ -522,7 +521,7 @@ async function loadCosts() {
|
|
|
522
521
|
if (cats.length === 0) {
|
|
523
522
|
const tag = document.createElement('span');
|
|
524
523
|
tag.className = 'cost-cat';
|
|
525
|
-
tag.textContent = `No ${curLabel()} yet
|
|
524
|
+
tag.textContent = `No ${curLabel()} spent yet.`;
|
|
526
525
|
els.costCats.appendChild(tag);
|
|
527
526
|
} else {
|
|
528
527
|
for (const c of cats) {
|
|
@@ -531,7 +530,7 @@ async function loadCosts() {
|
|
|
531
530
|
const strong = document.createElement('strong');
|
|
532
531
|
strong.textContent = c.category;
|
|
533
532
|
tag.appendChild(strong);
|
|
534
|
-
tag.append(` · ${
|
|
533
|
+
tag.append(` · ${fmtMoney(c.total_usd)}`);
|
|
535
534
|
els.costCats.appendChild(tag);
|
|
536
535
|
}
|
|
537
536
|
}
|
|
@@ -557,7 +556,7 @@ async function loadCosts() {
|
|
|
557
556
|
if (total > 0) {
|
|
558
557
|
const pct = Math.round((unattributed / total) * 100);
|
|
559
558
|
els.costAttributionSplit.textContent =
|
|
560
|
-
`Attributed to builders: ${
|
|
559
|
+
`Attributed to builders: ${fmtMoney(attributed)} · Shared (unattributed): ${fmtMoney(unattributed)} (${pct}%).`;
|
|
561
560
|
} else {
|
|
562
561
|
els.costAttributionSplit.textContent = '';
|
|
563
562
|
}
|
|
@@ -577,14 +576,14 @@ async function loadCosts() {
|
|
|
577
576
|
: (b.display_name || b.github_login || `builder #${b.builder_id}`);
|
|
578
577
|
strong.textContent = label;
|
|
579
578
|
tag.appendChild(strong);
|
|
580
|
-
tag.append(` · ${
|
|
579
|
+
tag.append(` · ${fmtMoney(b.total_usd)}`);
|
|
581
580
|
els.costBuilders.appendChild(tag);
|
|
582
581
|
rendered++;
|
|
583
582
|
}
|
|
584
583
|
if (rendered === 0) {
|
|
585
584
|
const tag = document.createElement('span');
|
|
586
585
|
tag.className = 'cost-cat';
|
|
587
|
-
tag.textContent = `No ${curLabel()}
|
|
586
|
+
tag.textContent = `No ${curLabel()} attributed to a builder yet.`;
|
|
588
587
|
els.costBuilders.appendChild(tag);
|
|
589
588
|
}
|
|
590
589
|
}
|
|
@@ -666,7 +665,7 @@ async function loadCosts() {
|
|
|
666
665
|
borderWidth: 1.5,
|
|
667
666
|
},
|
|
668
667
|
{
|
|
669
|
-
label: 'value
|
|
668
|
+
label: 'value produced',
|
|
670
669
|
data: cumValue,
|
|
671
670
|
fill: false,
|
|
672
671
|
borderColor: t.info,
|
|
@@ -702,7 +701,7 @@ async function loadCosts() {
|
|
|
702
701
|
bodyFont: { family: t.body, size: 13 },
|
|
703
702
|
padding: 10,
|
|
704
703
|
callbacks: {
|
|
705
|
-
label: (c) => `${c.dataset.label}: ${c.parsed.y.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 })}
|
|
704
|
+
label: (c) => `${c.dataset.label}: ${c.parsed.y.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 2 })}`,
|
|
706
705
|
},
|
|
707
706
|
},
|
|
708
707
|
},
|
|
@@ -716,7 +715,7 @@ async function loadCosts() {
|
|
|
716
715
|
color: t.inkSoft,
|
|
717
716
|
},
|
|
718
717
|
ticks: {
|
|
719
|
-
callback: (v) => `${v}
|
|
718
|
+
callback: (v) => `${v}`,
|
|
720
719
|
font: { family: t.body, size: 12 },
|
|
721
720
|
color: t.inkSoft,
|
|
722
721
|
},
|
|
@@ -808,7 +807,7 @@ function renderShippedPage() {
|
|
|
808
807
|
if (items.length === 0) {
|
|
809
808
|
const li = document.createElement('li');
|
|
810
809
|
li.className = 'empty';
|
|
811
|
-
li.textContent = '
|
|
810
|
+
li.textContent = 'Nothing shipped in this window.';
|
|
812
811
|
els.shipped.appendChild(li);
|
|
813
812
|
// GDS #191: pad with invisible rows so an empty window still occupies
|
|
814
813
|
// the same vertical space as a full page. Keeps the section height
|
|
@@ -850,7 +849,7 @@ function renderShippedPage() {
|
|
|
850
849
|
const by = t.display_name || t.github_login;
|
|
851
850
|
// task 1004432: an approved page is the artist's; the session that applied it is a helper.
|
|
852
851
|
const helper = t.helper_display_name || t.helper_login;
|
|
853
|
-
meta.textContent = `${by ? `
|
|
852
|
+
meta.textContent = `${by ? `shipped by ${by} · ` : ''}${helper ? `with help from ${helper} · ` : ''}${fmtRelativeTime(t.shipped_at)}`;
|
|
854
853
|
li.appendChild(title);
|
|
855
854
|
if (thumb) li.appendChild(thumb);
|
|
856
855
|
li.appendChild(meta);
|
|
@@ -1063,7 +1062,7 @@ async function loadLeaderboard() {
|
|
|
1063
1062
|
function stampRefreshTime() {
|
|
1064
1063
|
if (!els.lastUpdated) return;
|
|
1065
1064
|
const t = new Date().toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
|
|
1066
|
-
els.lastUpdated.textContent = `
|
|
1065
|
+
els.lastUpdated.textContent = `updated at ${t}`;
|
|
1067
1066
|
}
|
|
1068
1067
|
|
|
1069
1068
|
// ---------- the Foundry — ecosystem dev curve (#291, C5) ----------
|
|
@@ -1121,8 +1120,8 @@ async function loadEcosystem() {
|
|
|
1121
1120
|
const shipVals = (data.ship_rate || []).map((r) => Number(r.n));
|
|
1122
1121
|
const durVals = (data.duration_trend || []).map((r) => Number(r.avg_actual));
|
|
1123
1122
|
chartsEl.innerHTML =
|
|
1124
|
-
`<div class="foundry__chart"><div class="foundry__chart-title">
|
|
1125
|
-
`<div class="foundry__chart"><div class="foundry__chart-title">
|
|
1123
|
+
`<div class="foundry__chart"><div class="foundry__chart-title">Tasks shipped per day</div>${foundrySparkline(shipVals, 'ink') || '<p class="empty">no data</p>'}</div>` +
|
|
1124
|
+
`<div class="foundry__chart"><div class="foundry__chart-title">Average time per task (min)</div>${foundrySparkline(durVals, 'info') || '<p class="empty">no data</p>'}</div>`;
|
|
1126
1125
|
|
|
1127
1126
|
const kinds = (data.top_kinds || []).slice(0, 6);
|
|
1128
1127
|
if (kindsEl) {
|
|
@@ -1218,13 +1217,13 @@ function renderStatusDeepCard(kind, id, state) {
|
|
|
1218
1217
|
const label = `#${id}`;
|
|
1219
1218
|
panel.appendChild(mk('p', 'deeplink__kind', kind.toUpperCase()));
|
|
1220
1219
|
if (state.loading) {
|
|
1221
|
-
panel.appendChild(mk('p', 'deeplink__title', `
|
|
1220
|
+
panel.appendChild(mk('p', 'deeplink__title', `Looking up ${label}…`));
|
|
1222
1221
|
} else if (state.notFound) {
|
|
1223
1222
|
panel.appendChild(mk('p', 'deeplink__title', `${label} — no such ${kind}`));
|
|
1224
1223
|
panel.appendChild(mk('p', 'deeplink__note', 'Records number tasks, ideas, and blockers separately from GitHub pull requests — this may be a PR, not a record here.'));
|
|
1225
1224
|
} else if (state.error) {
|
|
1226
1225
|
panel.appendChild(mk('p', 'deeplink__title', `Could not resolve ${label}`));
|
|
1227
|
-
panel.appendChild(mk('p', 'deeplink__note', 'The
|
|
1226
|
+
panel.appendChild(mk('p', 'deeplink__note', 'The server could not be reached. Try again in a moment.'));
|
|
1228
1227
|
} else {
|
|
1229
1228
|
const r = state.ref || {};
|
|
1230
1229
|
panel.appendChild(mk('p', 'deeplink__title', r.title ? `${label} — ${r.title}` : label));
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.51",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.51",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.51",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -11271,5 +11271,11 @@
|
|
|
11271
11271
|
"id": "1005089",
|
|
11272
11272
|
"text": "The public status page now looks like the rest of Cloud Bongos: same colours, fonts, logo and panels, with a dark mode, and its off-site backup copy is built from the same source so the two always match."
|
|
11273
11273
|
}
|
|
11274
|
+
],
|
|
11275
|
+
"1.24.51": [
|
|
11276
|
+
{
|
|
11277
|
+
"id": "1005090",
|
|
11278
|
+
"text": "The public status page now talks plainly: 'Top builders', 'Recently shipped', 'Spending' instead of the old ancient-city words, and costs show in dollars."
|
|
11279
|
+
}
|
|
11274
11280
|
]
|
|
11275
11281
|
}
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.51'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
package/tests/loading_mark.mjs
CHANGED
|
@@ -170,11 +170,11 @@ test('apex: each page waits with the size that fits it', () => {
|
|
|
170
170
|
|
|
171
171
|
test('status: its two waiting tablets wear the loader, and a failed or empty read stops it', () => {
|
|
172
172
|
const html = read(STATUS, 'index.html');
|
|
173
|
-
assert.ok(html.includes('<div id="versions" class="versions">\n <p class="empty loading-mark" role="status">
|
|
174
|
-
assert.ok(html.includes('<div id="world-snapshot" class="world-snapshot">\n <p class="empty loading-mark" role="status">
|
|
175
|
-
// the header's "
|
|
173
|
+
assert.ok(html.includes('<div id="versions" class="versions">\n <p class="empty loading-mark" role="status">Loading the versions…</p>'));
|
|
174
|
+
assert.ok(html.includes('<div id="world-snapshot" class="world-snapshot">\n <p class="empty loading-mark" role="status">Loading the latest snapshot…</p>'));
|
|
175
|
+
// the header's "Checking for updates…" is rewritten in place before any read
|
|
176
176
|
// starts (stampRefreshTime), so it is never a wait and keeps its plain words
|
|
177
|
-
assert.match(html, /<span class="herm__updated" id="last-updated">
|
|
177
|
+
assert.match(html, /<span class="herm__updated" id="last-updated">Checking for updates…<\/span>/);
|
|
178
178
|
// The Kill Switch is the last thing in the status sheet, so nothing can outrank it.
|
|
179
179
|
assert.match(STATUS_CSS.trimEnd(), /@media \(prefers-reduced-motion: reduce\) \{\n {2}\*, \*::before, \*::after \{ animation: none !important; transition: none !important; \}\n\}$/);
|
|
180
180
|
|
|
@@ -186,12 +186,12 @@ test('status: its two waiting tablets wear the loader, and a failed or empty rea
|
|
|
186
186
|
// ...and stopWaiting itself, run: it takes the class off and leaves the words alone.
|
|
187
187
|
const src = js.match(/function stopWaiting\(el\) \{[\s\S]*?\n\}/)[0];
|
|
188
188
|
const removed = [];
|
|
189
|
-
const wait = { textContent: '
|
|
189
|
+
const wait = { textContent: 'Loading the versions…', classList: { remove: (c) => removed.push(c) } };
|
|
190
190
|
const host = { querySelectorAll: (sel) => (sel === '.loading-mark' ? [wait] : []) };
|
|
191
191
|
const ctx = vm.createContext({});
|
|
192
192
|
vm.runInContext(`${src}; this.stopWaiting = stopWaiting;`, ctx);
|
|
193
193
|
ctx.stopWaiting(host);
|
|
194
194
|
ctx.stopWaiting(null);
|
|
195
195
|
assert.deepEqual(removed, ['loading-mark']);
|
|
196
|
-
assert.equal(wait.textContent, '
|
|
196
|
+
assert.equal(wait.textContent, 'Loading the versions…');
|
|
197
197
|
});
|
|
@@ -26,7 +26,7 @@ test('a pack with no company drops the whole attribution sentence, not half of i
|
|
|
26
26
|
const out = stamp({ name: '', site: 'https://ignored.example', tagline: 'ignored' });
|
|
27
27
|
assert.doesNotMatch(out, /is the work of|ignored/);
|
|
28
28
|
assert.doesNotMatch(out, /\{\{|brand-if/);
|
|
29
|
-
assert.match(out, /Tokens are
|
|
29
|
+
assert.match(out, /Tokens are awarded by review/, 'the neighbouring footer lines survive');
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
test('a full company renders the linked name and its tagline', () => {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// tests/status_page_wording.mjs — the status page speaks the project's current
|
|
2
|
+
// voice, not the old Greek-city ledger (task 1005090, goal 1000138; docs/voice.md).
|
|
3
|
+
//
|
|
4
|
+
// The page moved onto the chrome world in task 1005089 with its words untouched;
|
|
5
|
+
// this task rewrote them. What this pins: no word of the city vocabulary
|
|
6
|
+
// ("citizens who shape her stones", "tablet last consulted", "works wrought",
|
|
7
|
+
// "architects", drachmas…) is left anywhere a visitor can read it — the page's
|
|
8
|
+
// text, its title and meta, its accessible names, and every string status.js
|
|
9
|
+
// writes into the page. Class names, ids and code comments are not read, so a
|
|
10
|
+
// selector like `.foundry__chart` is not a finding; a sentence is.
|
|
11
|
+
//
|
|
12
|
+
// Run: node --test tests/status_page_wording.mjs
|
|
13
|
+
|
|
14
|
+
import { test } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import http from 'node:http';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import vm from 'node:vm';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
|
|
25
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const DIR = path.join(ROOT, 'modules', 'status-ui', 'public');
|
|
27
|
+
const read = (f) => fs.readFileSync(path.join(DIR, f), 'utf8');
|
|
28
|
+
|
|
29
|
+
// The retired vocabulary, as whole words.
|
|
30
|
+
const CITY = /\b(citizens?|stones?|tablets?|wrought|architects?|inscri(be|bed|ption|ptions)|lately|masons?|cartographers?|purse|treasury|foundry|forge|senate|lawmen|inspector|athens|civili[sz]ation|endeavou?rs?|portraits?|consult(ed|ing)?|archive|her stones|city|city's|ledger|hand at|hands)\b|\bdr\./i;
|
|
31
|
+
|
|
32
|
+
// What a visitor reads in the markup: text between tags, and the attributes that
|
|
33
|
+
// are read aloud or shown (title, alt, aria-label, placeholder, meta content).
|
|
34
|
+
function visibleHtml(html) {
|
|
35
|
+
const noComments = html.replace(/<!--[\s\S]*?-->/g, ' ').replace(/<(script|style)\b[\s\S]*?<\/\1>/gi, ' ');
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const m of noComments.matchAll(/\b(?:aria-label|title|alt|placeholder|content)="([^"]*)"/g)) out.push(m[1]);
|
|
38
|
+
for (const m of noComments.replace(/<[^>]*>/g, '\n').split('\n')) if (m.trim()) out.push(m.trim());
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// What status.js writes: its string literals, minus comments, minus any markup
|
|
43
|
+
// inside a template, minus the literals that are a selector, id or key.
|
|
44
|
+
function visibleJs(js) {
|
|
45
|
+
// line comments first: a `/*` inside one (a path like /public/*) would otherwise
|
|
46
|
+
// open a block comment that swallows the code after it
|
|
47
|
+
const code = js.replace(/(^|[^:\\'"`])\/\/.*$/gm, '$1').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const m of code.matchAll(/'((?:[^'\\\n]|\\.)*)'|"((?:[^"\\\n]|\\.)*)"|`((?:[^`\\]|\\.)*)`/g)) {
|
|
50
|
+
const lit = (m[1] ?? m[2] ?? m[3] ?? '').replace(/<[^>]*>/g, ' ').replace(/\$\{[^}]*\}/g, ' ').trim();
|
|
51
|
+
if (!lit) continue;
|
|
52
|
+
if (/^[a-z0-9_\-.#:[\]=" ]*$/.test(lit) && !/\s\S+\s/.test(lit)) continue; // a selector, id, class list or key
|
|
53
|
+
out.push(lit);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The pages as the server serves them on the status host (stamped + branded), so
|
|
59
|
+
// the check reads what a visitor's browser receives, not the template.
|
|
60
|
+
function get(port, p) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const req = http.request({ host: '127.0.0.1', port, path: p, headers: { Host: 'status.example.com' } }, (res) => {
|
|
63
|
+
let body = '';
|
|
64
|
+
res.setEncoding('utf8');
|
|
65
|
+
res.on('data', (c) => { body += c; });
|
|
66
|
+
res.on('end', () => resolve({ status: res.statusCode, body }));
|
|
67
|
+
});
|
|
68
|
+
req.on('error', reject);
|
|
69
|
+
req.end();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async function servedPages() {
|
|
73
|
+
const express = require('express');
|
|
74
|
+
const { mountInternalSurfaces } = require('../src/bongos/serve-internal.js');
|
|
75
|
+
const app = express();
|
|
76
|
+
mountInternalSurfaces(app);
|
|
77
|
+
const server = await new Promise((resolve) => { const s = app.listen(0, () => resolve(s)); });
|
|
78
|
+
try {
|
|
79
|
+
const port = server.address().port;
|
|
80
|
+
const out = {};
|
|
81
|
+
for (const f of fs.readdirSync(DIR).filter((n) => n.endsWith('.html'))) {
|
|
82
|
+
const r = await get(port, f === 'index.html' ? '/' : `/${f}`);
|
|
83
|
+
assert.equal(r.status, 200, f);
|
|
84
|
+
out[f] = r.body;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
} finally {
|
|
88
|
+
await new Promise((r) => server.close(r));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
test('no Greek-city word is left in what the status page shows', async () => {
|
|
93
|
+
const hits = [];
|
|
94
|
+
const pages = await servedPages();
|
|
95
|
+
assert.ok(Object.keys(pages).length >= 2);
|
|
96
|
+
for (const [f, html] of Object.entries(pages)) {
|
|
97
|
+
for (const line of visibleHtml(html)) if (CITY.test(line)) hits.push(`${f}: ${line.slice(0, 120)}`);
|
|
98
|
+
}
|
|
99
|
+
for (const f of ['status.js', 'explore.js']) {
|
|
100
|
+
for (const line of visibleJs(read(f))) if (CITY.test(line)) hits.push(`${f}: ${line.slice(0, 120)}`);
|
|
101
|
+
}
|
|
102
|
+
assert.deepEqual(hits, []);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('the reader really reads the page (a guard that sees nothing passes everything)', async () => {
|
|
106
|
+
const html = visibleHtml((await servedPages())['index.html']);
|
|
107
|
+
assert.ok(html.includes('Recently shipped'), 'a heading is read');
|
|
108
|
+
assert.ok(html.some((l) => /^How .+ is being built, in the open/.test(l)), 'the meta description is read');
|
|
109
|
+
assert.ok(html.includes('Filter shipped tasks by date range'), 'an accessible name is read');
|
|
110
|
+
const js = visibleJs(read('status.js'));
|
|
111
|
+
assert.ok(js.includes('Nothing shipped in this window.'), 'a string status.js writes is read');
|
|
112
|
+
assert.ok(!js.includes('foundry__chart'), 'a class name is not');
|
|
113
|
+
// and the old lines would have been caught
|
|
114
|
+
for (const old of ['Architects of {{worldName}}', 'tablet last consulted at', 'works wrought, last 7 days', 'Citizens who have set the most stone.', '12 dr.']) {
|
|
115
|
+
assert.ok(CITY.test(old), `the guard catches "${old}"`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('money reads as dollars, not drachmas', () => {
|
|
120
|
+
// run the page's own two formatters
|
|
121
|
+
const js = read('status.js');
|
|
122
|
+
const fn = (name) => js.match(new RegExp(`function ${name}\\(n\\) \\{[\\s\\S]*?\\n\\}`))[0];
|
|
123
|
+
const ctx = vm.createContext({});
|
|
124
|
+
vm.runInContext(`${fn('fmtUsd')}\n${fn('fmtMoney')}\nthis.fmtMoney = fmtMoney;`, ctx);
|
|
125
|
+
assert.equal(ctx.fmtMoney(12.5), '$12.50');
|
|
126
|
+
assert.equal(ctx.fmtMoney(48297.4), `$${(48297).toLocaleString()}`);
|
|
127
|
+
assert.equal(ctx.fmtMoney(null), '—');
|
|
128
|
+
assert.doesNotMatch(js, /fmtDr|dr\.`/);
|
|
129
|
+
});
|