@bongos/core 1.24.50 → 1.24.52
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 +104 -89
- package/docs/copy-inventory.md +259 -277
- package/docs/copy-registry.json +461 -634
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +35 -2
- package/docs/page-readings.json +518 -547
- package/modules/hall-ui/public/ai-studio.html +1 -0
- package/modules/hall-ui/public/approval-queue.css +3 -11
- package/modules/hall-ui/public/approval-queue.js +13 -20
- package/modules/hall-ui/public/atlas.html +1 -0
- package/modules/hall-ui/public/blockers.html +1 -0
- package/modules/hall-ui/public/board-room.html +1 -0
- package/modules/hall-ui/public/bulk-toolbar.js +5 -1
- package/modules/hall-ui/public/city.html +1 -0
- package/modules/hall-ui/public/collab.html +1 -0
- package/modules/hall-ui/public/commands.html +1 -0
- package/modules/hall-ui/public/controls.css +100 -0
- package/modules/hall-ui/public/copy-desk.css +7 -11
- package/modules/hall-ui/public/copy-desk.html +17 -16
- package/modules/hall-ui/public/copy-desk.js +13 -9
- package/modules/hall-ui/public/deploy.html +1 -0
- package/modules/hall-ui/public/goal-inbox.js +32 -17
- package/modules/hall-ui/public/goal-map.html +1 -0
- package/modules/hall-ui/public/goals-page.js +7 -1
- package/modules/hall-ui/public/goals.html +1 -0
- package/modules/hall-ui/public/government.html +1 -0
- package/modules/hall-ui/public/government.js +10 -2
- package/modules/hall-ui/public/hall-dialog.js +255 -0
- package/modules/hall-ui/public/hall-kit.js +9 -2
- package/modules/hall-ui/public/idea.html +1 -0
- package/modules/hall-ui/public/ideas.html +1 -0
- package/modules/hall-ui/public/index.html +1 -0
- package/modules/hall-ui/public/market-watch-item.html +2 -1
- package/modules/hall-ui/public/market-watch-lib.js +16 -9
- package/modules/hall-ui/public/market-watch-rules.html +1 -0
- package/modules/hall-ui/public/market-watch.css +2 -3
- package/modules/hall-ui/public/market-watch.html +2 -1
- package/modules/hall-ui/public/modules.html +1 -0
- package/modules/hall-ui/public/money.html +1 -0
- package/modules/hall-ui/public/my-agents.html +1 -0
- package/modules/hall-ui/public/oversight.css +3 -33
- package/modules/hall-ui/public/primer.html +1 -0
- package/modules/hall-ui/public/profile.html +1 -0
- package/modules/hall-ui/public/project-settings.html +1 -0
- package/modules/hall-ui/public/roster.html +1 -0
- package/modules/hall-ui/public/settings-ai-keys.js +5 -1
- package/modules/hall-ui/public/settings-passkeys.js +5 -1
- package/modules/hall-ui/public/settings-sessions.js +10 -2
- package/modules/hall-ui/public/settings.html +1 -0
- package/modules/hall-ui/public/settings.js +5 -1
- package/modules/hall-ui/public/street.html +1 -0
- package/modules/hall-ui/public/studio.css +6 -32
- package/modules/hall-ui/public/studio.html +4 -3
- package/modules/hall-ui/public/studio.js +40 -35
- package/modules/hall-ui/public/style.css +2 -8
- package/modules/hall-ui/public/task.html +1 -0
- package/modules/hall-ui/public/task.js +26 -12
- package/modules/hall-ui/public/thinking.html +1 -0
- package/modules/hall-ui/public/tweak-editor.html +1 -0
- package/modules/hall-ui/public/watch.html +1 -0
- package/modules/hall-ui/public/work.html +1 -0
- package/modules/hall-ui/public/work.js +7 -1
- 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 +12 -0
- package/src/module-api.js +1 -1
- package/tests/bulk_toolbar.mjs +1 -1
- package/tests/hall_approval_queue.mjs +8 -2
- package/tests/hall_audit.mjs +1 -1
- package/tests/hall_city.mjs +1 -1
- package/tests/hall_collab_world.mjs +9 -6
- package/tests/hall_dialog.mjs +401 -0
- package/tests/hall_error_envelope.mjs +1 -1
- package/tests/hall_oversight_world.mjs +9 -7
- package/tests/hall_record_world.mjs +1 -1
- package/tests/hall_street.mjs +1 -1
- package/tests/hall_studio_home.mjs +1 -1
- package/tests/home_attention_cards.mjs +2 -0
- package/tests/kind_axis_unset.mjs +1 -1
- package/tests/loading_mark.mjs +6 -6
- package/tests/market_watch_pages.mjs +3 -1
- package/tests/status_company_attribution.mjs +1 -1
- package/tests/status_page_wording.mjs +129 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// tests/hall_dialog.mjs — the hall's ONE dialog and its branded confirm (task 1005085, goal 1000138).
|
|
2
|
+
//
|
|
3
|
+
// WHY. The UI audit found six dialog builds (attn, cd, mw, aq, ov, st) and fourteen window.confirm()
|
|
4
|
+
// calls. modules/hall-ui/public/hall-dialog.js replaces both. This proves three things:
|
|
5
|
+
// 1. the component, in a real browser: role + aria-modal + labelled by its title, focus trapped,
|
|
6
|
+
// Escape closes the TOP dialog only and hands focus back to the trigger, the scrim closes, the
|
|
7
|
+
// confirm resolves true only on its confirm button, and it wears the shared layer, shadow and
|
|
8
|
+
// panel corner (skips when the machine has no system Edge or Chrome, as ui_design_kit_fit does);
|
|
9
|
+
// 2. every replaced confirm flow: the REAL function, lifted out of its page script, asks the
|
|
10
|
+
// branded confirm (what will happen, in words) and runs its write only when the answer is yes;
|
|
11
|
+
// 3. the guards: no window.confirm anywhere in modules/hall-ui, and each of the six builds now
|
|
12
|
+
// runs on OTBDialog with no scrim/backdrop rule of its own.
|
|
13
|
+
import assert from 'node:assert/strict';
|
|
14
|
+
import { test, after } from 'node:test';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import vm from 'node:vm';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
24
|
+
const read = (f) => fs.readFileSync(path.join(PUB, f), 'utf8');
|
|
25
|
+
const lib = require(path.join(ROOT, 'modules', 'ui-design', 'kit', 'lib.js'));
|
|
26
|
+
|
|
27
|
+
// ── the source helpers ──────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
// Comments out, strings kept: a mention of window.confirm in a comment is history, not a call.
|
|
30
|
+
function stripComments(src) {
|
|
31
|
+
return src
|
|
32
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
33
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
34
|
+
.replace(/(^|[^:'"`\\])\/\/[^\n]*/g, '$1');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The source of `function name(...) { ... }` (async or not), by brace matching. The flows below
|
|
38
|
+
// hold no unbalanced brace in a string, and the vm compile is the check that the cut is whole.
|
|
39
|
+
function extractFn(src, name) {
|
|
40
|
+
const m = new RegExp(`(?:async\\s+)?function\\s+${name}\\s*\\(`).exec(src);
|
|
41
|
+
assert.ok(m, `function ${name} is in the source`);
|
|
42
|
+
let i = src.indexOf('{', src.indexOf(')', m.index + m[0].length - 1));
|
|
43
|
+
let depth = 0;
|
|
44
|
+
for (let j = i; j < src.length; j++) {
|
|
45
|
+
if (src[j] === '{') depth++;
|
|
46
|
+
else if (src[j] === '}') { depth--; if (depth === 0) return src.slice(m.index, j + 1); }
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`unbalanced function ${name}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Run one real flow function with its page's free names stubbed. `answer` is what the branded
|
|
52
|
+
// confirm returns. Reports the confirms asked and the writes sent.
|
|
53
|
+
async function runFlow(file, name, args, { answer, globals = {} } = {}) {
|
|
54
|
+
const asked = [];
|
|
55
|
+
const writes = [];
|
|
56
|
+
const record = (m, u, b) => { writes.push({ method: m, url: String(u), body: b }); return { ok: true, status: 200, data: { permission_count: 3 } }; };
|
|
57
|
+
const window = {
|
|
58
|
+
OTBDialog: { confirm: async (o) => { asked.push(o); return answer; } },
|
|
59
|
+
location: { assign() {} },
|
|
60
|
+
confirm: () => { throw new Error('window.confirm was called'); },
|
|
61
|
+
};
|
|
62
|
+
const ctx = vm.createContext({
|
|
63
|
+
window,
|
|
64
|
+
API: '/api/bongos',
|
|
65
|
+
api: { request: async (m, u, o) => record(m, u, o && o.body) },
|
|
66
|
+
postJSON: async (u, b) => record('POST', u, b),
|
|
67
|
+
sendJson: async (m, u) => record(m, u),
|
|
68
|
+
claimAction: {
|
|
69
|
+
release: async (c, t) => { record('POST', `/claims/${c}/resolve`, { task: t }); return { ok: true }; },
|
|
70
|
+
releaseMany: async (e) => { record('POST', '/claims/batch/release', e); return { ok: true }; },
|
|
71
|
+
},
|
|
72
|
+
toast() {}, render: async () => {}, reloadRanks: async () => {}, toAuth() {}, curLabel: () => 'credits',
|
|
73
|
+
fail: (r) => new Error(String(r.status)), deleteRefusal: () => '',
|
|
74
|
+
renderArtKeyCard() {}, wireArtKeyCard() {},
|
|
75
|
+
$: () => null,
|
|
76
|
+
console,
|
|
77
|
+
...globals,
|
|
78
|
+
});
|
|
79
|
+
const fn = vm.runInContext(`(${extractFn(read(file), name)})`, ctx);
|
|
80
|
+
await fn(...args);
|
|
81
|
+
return { asked, writes };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const fakeBtn = () => ({ disabled: false, textContent: '' });
|
|
85
|
+
|
|
86
|
+
// Every replaced window.confirm, as { file, fn, args, globals, write: what the yes sends }.
|
|
87
|
+
const FLOWS = [
|
|
88
|
+
{ flow: 'end the session you are using', file: 'settings-sessions.js', fn: 'endOne',
|
|
89
|
+
args: () => [{ closest: () => ({ getAttribute: () => 's1', querySelector: () => ({}) }), disabled: false }],
|
|
90
|
+
write: /me\/sessions\/revoke$/ },
|
|
91
|
+
{ flow: 'sign out everywhere', file: 'settings-sessions.js', fn: 'endAll',
|
|
92
|
+
args: () => [], globals: { $: () => fakeBtn() }, write: /revoke-all/ },
|
|
93
|
+
{ flow: 'remove a passkey card', file: 'settings-passkeys.js', fn: 'remove', args: () => ['cred-1'], write: /passkeys\/remove/ },
|
|
94
|
+
{ flow: 'remove an AI studio key', file: 'settings-ai-keys.js', fn: 'removeKey', args: () => ['openai', 'OpenAI'], write: /ai-studio\/keys\/openai/ },
|
|
95
|
+
{ flow: 'remove the image-generation key', file: 'settings.js', fn: 'removeArtKey', args: () => [], write: /art-key\/own/ },
|
|
96
|
+
{ flow: 'reset a rank to its defaults', file: 'government.js', fn: 'resetRank',
|
|
97
|
+
args: () => [{ rank_key: 'metic', label: 'Metic' }, fakeBtn()], write: /ranks\/metic\/reset/ },
|
|
98
|
+
{ flow: 'delete a rank', file: 'government.js', fn: 'deleteRank',
|
|
99
|
+
args: () => [{ rank_key: 'scribe', label: 'Scribe' }, fakeBtn()], write: /ranks\/scribe$/ },
|
|
100
|
+
{ flow: 'release a claim on /work', file: 'work.js', fn: 'doRelease',
|
|
101
|
+
args: () => [11, 1001, fakeBtn()], globals: { pending: new Set() }, write: /claims\/11\/resolve/ },
|
|
102
|
+
{ flow: 'release a claim on a goal', file: 'goals-page.js', fn: 'doGoalRelease',
|
|
103
|
+
args: () => [12, 1002, fakeBtn()], globals: { pendingClaims: new Set() }, write: /claims\/12\/resolve/ },
|
|
104
|
+
{ flow: 'confirm a verify task', file: 'task.js', fn: 'confirmTask',
|
|
105
|
+
args: () => [{ id: 1003, kind: 'verify' }, fakeBtn()],
|
|
106
|
+
globals: { runLifecycle: (b, u, body) => ({ __w: true, u, body }) }, write: /tasks\/1003\/confirm/, lifecycle: true },
|
|
107
|
+
{ flow: 'retire a task', file: 'task.js', fn: 'retireTask',
|
|
108
|
+
args: () => [{ id: 1004 }, fakeBtn(), 'superseded by 1005'],
|
|
109
|
+
globals: { runWrite: async (b, fn) => fn(), abandonOpen: true }, write: /tasks\/1004\/abandon/ },
|
|
110
|
+
{ flow: 'delete a saved view', file: 'hall-kit.js', fn: 'deleteView',
|
|
111
|
+
args: () => [{ id: 5, name: 'Mine' }],
|
|
112
|
+
globals: { act: (fn) => fn(), call: (m, u) => ({ m, u }), pageQ: 'page=work' }, write: /\/5\?page=work/, viewCall: true },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
for (const f of FLOWS) {
|
|
116
|
+
test(`${f.flow}: the branded confirm says what happens, and the write runs only on yes`, async () => {
|
|
117
|
+
const extra = { ...(f.globals || {}) };
|
|
118
|
+
// task.js hands the write to runLifecycle; the saved-view menu hands it to call(). Record both.
|
|
119
|
+
let sent = [];
|
|
120
|
+
if (f.lifecycle) extra.runLifecycle = (b, u, body) => { sent.push({ url: u, body }); };
|
|
121
|
+
if (f.viewCall) extra.call = (m, u) => { sent.push({ url: u, method: m }); };
|
|
122
|
+
const no = await runFlow(f.file, f.fn, f.args(), { answer: false, globals: extra });
|
|
123
|
+
const noSent = sent; sent = [];
|
|
124
|
+
assert.equal(no.asked.length, 1, 'the confirm is shown once');
|
|
125
|
+
const o = no.asked[0];
|
|
126
|
+
assert.ok(o.title && o.title.length > 3, 'it has a title');
|
|
127
|
+
assert.ok(o.body && o.body.length > 10, 'its body says what will happen');
|
|
128
|
+
assert.ok(o.confirmLabel, 'the confirm button names the act');
|
|
129
|
+
assert.equal(no.writes.filter((w) => f.write.test(w.url)).length + noSent.filter((w) => f.write.test(w.url)).length, 0,
|
|
130
|
+
'Cancel sends nothing');
|
|
131
|
+
const yes = await runFlow(f.file, f.fn, f.args(), { answer: true, globals: extra });
|
|
132
|
+
assert.equal(yes.asked.length, 1);
|
|
133
|
+
assert.equal(yes.writes.filter((w) => f.write.test(w.url)).length + sent.filter((w) => f.write.test(w.url)).length, 1,
|
|
134
|
+
'the yes sends the write, once');
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
test('a verify-kind confirm attests the verification; any other kind asks nothing', async () => {
|
|
139
|
+
const sent = [];
|
|
140
|
+
const runLifecycle = (b, u, body) => sent.push({ u, body });
|
|
141
|
+
const plain = await runFlow('task.js', 'confirmTask', [{ id: 9, kind: 'build' }, fakeBtn()], { answer: false, globals: { runLifecycle } });
|
|
142
|
+
assert.equal(plain.asked.length, 0, 'no confirm for a non-verify task');
|
|
143
|
+
assert.equal(JSON.stringify(sent.map((s) => s.body)), "[{}]");
|
|
144
|
+
await runFlow('task.js', 'confirmTask', [{ id: 9, kind: 'verify' }, fakeBtn()], { answer: true, globals: { runLifecycle } });
|
|
145
|
+
assert.equal(JSON.stringify(sent[1].body), JSON.stringify({ verified: true }));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('ending a session that is not this one asks nothing', async () => {
|
|
149
|
+
const btn = { closest: () => ({ getAttribute: () => 's2', querySelector: () => null }), disabled: false };
|
|
150
|
+
const r = await runFlow('settings-sessions.js', 'endOne', [btn], { answer: false });
|
|
151
|
+
assert.equal(r.asked.length, 0);
|
|
152
|
+
assert.equal(r.writes.length, 1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('the bulk release on the shared toolbar asks first, and releases only on yes', async () => {
|
|
156
|
+
const { makeBulkToolbar } = require(path.join(PUB, 'bulk-toolbar.js'));
|
|
157
|
+
for (const answer of [false, true]) {
|
|
158
|
+
const asked = [];
|
|
159
|
+
const els = {};
|
|
160
|
+
for (const id of ['bar', 'count', 'claim', 'release', 'parallel', 'sequential', 'clear']) {
|
|
161
|
+
els[id] = { hidden: true, disabled: false, textContent: '', listeners: {}, addEventListener(t, fn) { this.listeners[t] = fn; } };
|
|
162
|
+
}
|
|
163
|
+
globalThis.document = { getElementById: (id) => els[id] || null };
|
|
164
|
+
globalThis.window = { OTBDialog: { confirm: async (o) => { asked.push(o); return answer; } }, confirm: () => { throw new Error('window.confirm'); } };
|
|
165
|
+
const released = [];
|
|
166
|
+
const selection = { claim: new Set(), release: new Map([['77', '9'], ['78', '10']]) };
|
|
167
|
+
const bar = makeBulkToolbar({
|
|
168
|
+
ids: { bar: 'bar', count: 'count', claim: 'claim', release: 'release', parallel: 'parallel', sequential: 'sequential', clear: 'clear' },
|
|
169
|
+
selection, findTask: () => null,
|
|
170
|
+
claimAction: { validateMany: async () => ({ ok: true, byId: new Map() }), claimMany: async () => ({ ok: true }), releaseMany: async (e) => { released.push(e); return { ok: true }; } },
|
|
171
|
+
getJSON: async () => ({}), API: '/api/bongos', copyText: async () => true, toast() {}, curLabel: () => 'credits',
|
|
172
|
+
describeReason: (r) => r, onClear() {},
|
|
173
|
+
});
|
|
174
|
+
bar.wire();
|
|
175
|
+
await els.release.listeners.click();
|
|
176
|
+
assert.equal(asked.length, 1);
|
|
177
|
+
assert.match(asked[0].title, /Release 2 tasks\?/);
|
|
178
|
+
assert.match(asked[0].body, /return to the queue/);
|
|
179
|
+
assert.equal(released.length, answer ? 1 : 0, answer ? 'yes releases' : 'cancel releases nothing');
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ── the guards ──────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
function hallFiles() {
|
|
186
|
+
const out = [];
|
|
187
|
+
const walk = (d) => {
|
|
188
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
189
|
+
const p = path.join(d, e.name);
|
|
190
|
+
if (e.isDirectory()) walk(p);
|
|
191
|
+
else if (/\.(js|html|mjs|cjs)$/.test(e.name)) out.push(p);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
walk(path.join(ROOT, 'modules', 'hall-ui'));
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
test('no window.confirm anywhere in modules/hall-ui (comments stripped)', () => {
|
|
199
|
+
const hits = [];
|
|
200
|
+
for (const f of hallFiles()) {
|
|
201
|
+
const src = stripComments(fs.readFileSync(f, 'utf8'));
|
|
202
|
+
// a call to the browser's box: window.confirm, or a bare confirm( that is not a method or a definition
|
|
203
|
+
if (/\bwindow\s*\.\s*confirm\b/.test(src) || /(^|[^.\w$])(?<!function\s)confirm\s*\(/m.test(src.replace(/function\s+confirm\s*\(/g, ''))) {
|
|
204
|
+
hits.push(path.relative(ROOT, f));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
assert.deepEqual(hits, [], 'use OTBDialog.confirm (hall-dialog.js) instead of the browser confirm');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('the guard catches a window.confirm when one comes back', () => {
|
|
211
|
+
const src = stripComments("// window.confirm('old') is history\nif (!window.confirm('Delete?')) return;");
|
|
212
|
+
assert.ok(/\bwindow\s*\.\s*confirm\b/.test(src));
|
|
213
|
+
assert.ok(!/\bwindow\s*\.\s*confirm\b/.test(stripComments('/* window.confirm() was here */ OTBDialog.confirm({})')));
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('every hall page loads the shared dialog right after the shell, deferred alike', () => {
|
|
217
|
+
for (const f of fs.readdirSync(PUB).filter((n) => n.endsWith('.html'))) {
|
|
218
|
+
const html = read(f);
|
|
219
|
+
const shell = html.match(/<script src="\/builders\/shell\.js[^"]*"( defer)?><\/script>\n<script src="\/builders\/hall-dialog\.js"( defer)?><\/script>/);
|
|
220
|
+
if (!/\/builders\/shell\.js/.test(html)) continue;
|
|
221
|
+
assert.ok(shell, `${f} loads hall-dialog.js right after shell.js`);
|
|
222
|
+
assert.equal(shell[1], shell[2], `${f}: deferred alike`);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('the six dialog builds run on OTBDialog, with no scrim, layer or backdrop of their own', () => {
|
|
227
|
+
// attn — Home's attention window
|
|
228
|
+
const inbox = read('goal-inbox.js');
|
|
229
|
+
assert.match(inbox, /<dialog class="hall-dialog hall-dialog--wide" id="attention-dialog"/);
|
|
230
|
+
assert.match(inbox, /OTBDialog\.bind\(/);
|
|
231
|
+
// cd — the copy desk's three dialogs
|
|
232
|
+
const cdHtml = read('copy-desk.html');
|
|
233
|
+
assert.equal((cdHtml.match(/<dialog id="cd-[a-z-]+" class="hall-dialog cd-dialog" aria-labelledby="[a-z-]+">/g) || []).length, 3);
|
|
234
|
+
assert.match(read('copy-desk.js'), /OTBDialog\.bind\(/);
|
|
235
|
+
// mw — the market watch's propose dialog
|
|
236
|
+
assert.match(read('market-watch-lib.js'), /OTBDialog\.bind\(/);
|
|
237
|
+
for (const p of ['market-watch.html', 'market-watch-item.html']) assert.match(read(p), /<dialog class="hall-dialog mw-dialog" id="mw-propose"/);
|
|
238
|
+
// aq — the approval queue's glass
|
|
239
|
+
assert.match(read('approval-queue.js'), /OTBDialog\.bind\(/);
|
|
240
|
+
// st — the studio's two glass panels
|
|
241
|
+
assert.equal((read('studio.js').match(/OTBDialog\.bind\(/g) || []).length, 2);
|
|
242
|
+
// ov — the oversight family's confirm modal is gone; copy-desk was its last user
|
|
243
|
+
assert.doesNotMatch(read('oversight.css'), /\.ov-modal/);
|
|
244
|
+
// nobody but controls.css draws a dialog backdrop or a dialog's own z layer
|
|
245
|
+
for (const f of fs.readdirSync(PUB).filter((n) => n.endsWith('.css') && n !== 'controls.css')) {
|
|
246
|
+
assert.doesNotMatch(stripComments(read(f)), /::backdrop/, `${f} draws no backdrop of its own`);
|
|
247
|
+
}
|
|
248
|
+
// the div overlays take the shared .hall-scrim (its layer and placement) and keep no layer of their own
|
|
249
|
+
assert.match(read('approval-queue.js'), /scrim\.className = 'hall-scrim aq-overlay'/);
|
|
250
|
+
assert.match(read('studio.html'), /class="hall-scrim st-overlay" id="st-panel"/);
|
|
251
|
+
assert.match(read('studio.html'), /class="hall-scrim st-reviews" id="st-reviews"/);
|
|
252
|
+
assert.match(read('studio.html'), /class="hall-dialog hall-dialog--wide st-reviews__dialog"/);
|
|
253
|
+
for (const [f, sel] of [['approval-queue.css', '.aq-overlay'], ['studio.css', '.st-overlay'], ['studio.css', '.st-reviews']]) {
|
|
254
|
+
const css = stripComments(read(f));
|
|
255
|
+
const at = css.indexOf(`\n${sel} {`);
|
|
256
|
+
assert.ok(at !== -1, `${sel} rule`);
|
|
257
|
+
const rule = css.slice(at, css.indexOf('}', at));
|
|
258
|
+
assert.doesNotMatch(rule, /z-index|position:/, `${sel} keeps no layer of its own`);
|
|
259
|
+
}
|
|
260
|
+
// the glass keeps its rim but floats on the one shared shadow
|
|
261
|
+
assert.match(read('approval-queue.css'), /box-shadow: inset 0 1px 0 var\(--aq-glass-rim\), var\(--ds-shadow-pop\);/);
|
|
262
|
+
assert.match(read('studio.css'), /box-shadow: inset 0 1px 0 var\(--st-panel-rim\), var\(--ds-shadow-pop\);/);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('the dialog look lives once, in controls.css, on the shared tokens', () => {
|
|
266
|
+
const css = stripComments(read('controls.css'));
|
|
267
|
+
const rule = css.slice(css.indexOf('.hall-dialog {'), css.indexOf('}', css.indexOf('.hall-dialog {')));
|
|
268
|
+
for (const want of [/background: var\(--bg-card\)/, /border-radius: var\(--ds-radius-panel\)/, /box-shadow: var\(--ds-shadow-pop\)/, /z-index: var\(--ds-z-modal\)/]) {
|
|
269
|
+
assert.match(rule, want);
|
|
270
|
+
}
|
|
271
|
+
assert.match(css, /\.hall-dialog::backdrop \{ background: var\(--scrim\); \}/);
|
|
272
|
+
assert.match(css, /\.hall-scrim \{[^}]*z-index: var\(--ds-z-modal\)[^}]*background: var\(--scrim\)/);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ── the component, in a real browser ────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
let browser = null;
|
|
278
|
+
try { browser = await lib.launchBrowser(); } catch (e) { if (!(e instanceof lib.NoBrowserError)) throw e; }
|
|
279
|
+
after(() => browser && browser.close());
|
|
280
|
+
const skip = !browser && 'no system Edge or Chrome';
|
|
281
|
+
|
|
282
|
+
const SHEETS = [path.join(ROOT, 'modules', 'ui-design', 'public', 'tokens.css'), path.join(PUB, 'controls.css')]
|
|
283
|
+
.map((f) => fs.readFileSync(f, 'utf8')).join('\n');
|
|
284
|
+
const PAGE = `<!doctype html><html><head><style>:root{--bg-card:#fff;--ink:#111;--rule:#ccc;--line:1px;--scrim:rgba(0,0,0,.55);--danger:#b3261e;--radius-pill:999px}</style></head>
|
|
285
|
+
<body><main><button id="go">Delete</button><button id="other">Other</button>
|
|
286
|
+
<div class="hall-scrim" id="ov" hidden><div class="panel" role="dialog" aria-labelledby="ov-t"><h2 id="ov-t">Overlay</h2><button id="ov-a">a</button><button id="ov-b">b</button></div></div>
|
|
287
|
+
</main></body></html>`;
|
|
288
|
+
|
|
289
|
+
async function boot() {
|
|
290
|
+
const ctx = await browser.newContext({ viewport: { width: 1000, height: 700 } });
|
|
291
|
+
const page = await ctx.newPage();
|
|
292
|
+
await page.setContent(PAGE);
|
|
293
|
+
await page.addStyleTag({ content: SHEETS });
|
|
294
|
+
await page.addScriptTag({ content: read('dom-utils.js') }); // the shared escaper, first as on every page
|
|
295
|
+
await page.addScriptTag({ content: read('hall-dialog.js') });
|
|
296
|
+
return { ctx, page };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
test('the confirm: a labelled modal on the shared layer, shadow and corner; Cancel first when it is destructive', { skip }, async () => {
|
|
300
|
+
const { ctx, page } = await boot();
|
|
301
|
+
await page.focus('#go');
|
|
302
|
+
await page.evaluate(() => { window.__r = window.OTBDialog.confirm({ title: 'Delete the Scribe rank?', body: 'Its permissions go with it.\n\nThis cannot be undone.', confirmLabel: 'Delete rank', danger: true }); });
|
|
303
|
+
const s = await page.evaluate(() => {
|
|
304
|
+
const d = document.querySelector('dialog.hall-dialog');
|
|
305
|
+
const cs = getComputedStyle(d);
|
|
306
|
+
const lab = document.getElementById(d.getAttribute('aria-labelledby'));
|
|
307
|
+
return {
|
|
308
|
+
open: d.open, modal: d.matches(':modal'), role: d.getAttribute('role'), ariaModal: d.getAttribute('aria-modal'),
|
|
309
|
+
name: lab && lab.textContent, paras: d.querySelectorAll('.hall-dialog__body p').length,
|
|
310
|
+
shadow: cs.boxShadow, radius: cs.borderTopLeftRadius, z: cs.zIndex,
|
|
311
|
+
focus: document.activeElement.textContent, danger: !!d.querySelector('.btn.btn--danger'), ghost: !!d.querySelector('.btn.btn--ghost'),
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
assert.equal(s.open, true);
|
|
315
|
+
assert.equal(s.modal, true, 'opened with showModal: the page behind is inert');
|
|
316
|
+
assert.equal(s.role, 'dialog');
|
|
317
|
+
assert.equal(s.ariaModal, 'true');
|
|
318
|
+
assert.equal(s.name, 'Delete the Scribe rank?', 'labelled by its title');
|
|
319
|
+
assert.equal(s.paras, 2, 'a blank line starts a paragraph');
|
|
320
|
+
assert.match(s.shadow, /rgba\(0, 0, 0, 0\.28\)/, 'the one floating shadow');
|
|
321
|
+
assert.equal(s.radius, '16px', 'the panel corner');
|
|
322
|
+
assert.equal(s.z, '160', 'the modal layer');
|
|
323
|
+
assert.equal(s.focus, 'Cancel', 'a destructive confirm starts on Cancel');
|
|
324
|
+
assert.ok(s.danger && s.ghost);
|
|
325
|
+
await page.keyboard.press('Enter');
|
|
326
|
+
assert.equal(await page.evaluate(() => window.__r), false, 'Enter on Cancel says no');
|
|
327
|
+
await ctx.close();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('the confirm resolves true only on its confirm button, and is gone after', { skip }, async () => {
|
|
331
|
+
const { ctx, page } = await boot();
|
|
332
|
+
await page.evaluate(() => { window.__r = window.OTBDialog.confirm({ title: 'Release task 7?', body: 'It goes back to the queue.', confirmLabel: 'Release' }); });
|
|
333
|
+
await page.click('dialog.hall-dialog .btn:not(.btn--ghost)');
|
|
334
|
+
assert.equal(await page.evaluate(() => window.__r), true);
|
|
335
|
+
assert.equal(await page.evaluate(() => document.querySelectorAll('dialog').length), 0, 'removed on close');
|
|
336
|
+
await ctx.close();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('focus is trapped: Tab and Shift+Tab wrap inside the dialog', { skip }, async () => {
|
|
340
|
+
const { ctx, page } = await boot();
|
|
341
|
+
await page.focus('#go');
|
|
342
|
+
await page.evaluate(() => { window.OTBDialog.confirm({ title: 'T', body: 'what happens', confirmLabel: 'Yes' }); });
|
|
343
|
+
const seen = [];
|
|
344
|
+
for (let i = 0; i < 5; i++) {
|
|
345
|
+
await page.keyboard.press('Tab');
|
|
346
|
+
seen.push(await page.evaluate(() => !!document.activeElement.closest('dialog')));
|
|
347
|
+
}
|
|
348
|
+
await page.keyboard.press('Shift+Tab');
|
|
349
|
+
seen.push(await page.evaluate(() => !!document.activeElement.closest('dialog')));
|
|
350
|
+
assert.deepEqual(seen, seen.map(() => true), 'focus never leaves the dialog');
|
|
351
|
+
await ctx.close();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('Escape closes, says no, and hands focus back to the trigger', { skip }, async () => {
|
|
355
|
+
const { ctx, page } = await boot();
|
|
356
|
+
await page.focus('#go');
|
|
357
|
+
await page.evaluate(() => { window.__r = window.OTBDialog.confirm({ title: 'T', body: 'what happens', confirmLabel: 'Yes' }); });
|
|
358
|
+
await page.keyboard.press('Escape');
|
|
359
|
+
assert.equal(await page.evaluate(() => window.__r), false);
|
|
360
|
+
assert.equal(await page.evaluate(() => document.activeElement.id), 'go');
|
|
361
|
+
await ctx.close();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('a click on the scrim says no', { skip }, async () => {
|
|
365
|
+
const { ctx, page } = await boot();
|
|
366
|
+
await page.evaluate(() => { window.__r = window.OTBDialog.confirm({ title: 'T', body: 'what happens', confirmLabel: 'Yes' }); });
|
|
367
|
+
await page.mouse.click(10, 10);
|
|
368
|
+
assert.equal(await page.evaluate(() => window.__r), false);
|
|
369
|
+
await ctx.close();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('a confirm over a bound overlay: Escape closes the confirm only, then the overlay, and focus walks back', { skip }, async () => {
|
|
373
|
+
const { ctx, page } = await boot();
|
|
374
|
+
await page.focus('#other');
|
|
375
|
+
await page.evaluate(() => {
|
|
376
|
+
const ov = document.getElementById('ov');
|
|
377
|
+
window.__h = window.OTBDialog.bind(ov, { panel: ov.querySelector('.panel') });
|
|
378
|
+
window.__h.open();
|
|
379
|
+
});
|
|
380
|
+
assert.equal(await page.evaluate(() => document.getElementById('ov').hidden), false);
|
|
381
|
+
assert.equal(await page.evaluate(() => document.activeElement.id), 'ov-a', 'focus moves in');
|
|
382
|
+
await page.evaluate(() => { window.__r = window.OTBDialog.confirm({ title: 'T', body: 'what happens', confirmLabel: 'Yes' }); });
|
|
383
|
+
await page.keyboard.press('Escape');
|
|
384
|
+
assert.equal(await page.evaluate(() => window.__r), false);
|
|
385
|
+
assert.equal(await page.evaluate(() => document.getElementById('ov').hidden), false, 'the overlay stays');
|
|
386
|
+
assert.equal(await page.evaluate(() => document.activeElement.id), 'ov-a', 'focus returns into the overlay');
|
|
387
|
+
await page.keyboard.press('Tab');
|
|
388
|
+
await page.keyboard.press('Tab');
|
|
389
|
+
assert.equal(await page.evaluate(() => document.activeElement.id), 'ov-a', 'the overlay traps Tab too');
|
|
390
|
+
await page.keyboard.press('Escape');
|
|
391
|
+
assert.equal(await page.evaluate(() => document.getElementById('ov').hidden), true);
|
|
392
|
+
assert.equal(await page.evaluate(() => document.activeElement.id), 'other', 'and back to the trigger');
|
|
393
|
+
const ov = await page.evaluate(() => {
|
|
394
|
+
const cs = getComputedStyle(document.getElementById('ov'));
|
|
395
|
+
const p = document.querySelector('#ov .panel');
|
|
396
|
+
return { z: cs.zIndex, modal: p.getAttribute('aria-modal') };
|
|
397
|
+
});
|
|
398
|
+
assert.equal(ov.z, '160');
|
|
399
|
+
assert.equal(ov.modal, 'true');
|
|
400
|
+
await ctx.close();
|
|
401
|
+
});
|
|
@@ -185,7 +185,7 @@ async function bootTaskPage({ task, failWith }) {
|
|
|
185
185
|
location: { hostname: 'builders.example.com', pathname: `/task/${task.id}`, href: '', assign() {} },
|
|
186
186
|
window: {
|
|
187
187
|
OTBBranding: { taskCreditValue: () => null, taskCreditLabel: () => 'credits' },
|
|
188
|
-
confirm: () => true,
|
|
188
|
+
OTBDialog: { confirm: async () => true }, // the hall's branded confirm (task 1005085)
|
|
189
189
|
location: { hostname: 'builders.example.com', pathname: `/task/${task.id}`, assign() {} },
|
|
190
190
|
// The generated typed client's result mode: { status, ok, data }.
|
|
191
191
|
BongosClient: {
|
|
@@ -45,7 +45,7 @@ test('the family sheet spends only the fifteen: no literal colour', () => {
|
|
|
45
45
|
test('the row is the family\'s archetype, and every page renders it', () => {
|
|
46
46
|
const css = read('oversight.css');
|
|
47
47
|
for (const cls of ['.ov-row', '.ov-row__title', '.ov-row__sub', '.ov-row__facts', '.ov-row__right',
|
|
48
|
-
'.ov-group__head', '.ov-figs', '.ov-
|
|
48
|
+
'.ov-group__head', '.ov-figs', '.ov-form', '.ov-toggle', '.ov-lines', '.ov-prose']) {
|
|
49
49
|
assert.ok(new RegExp(`^${cls.replace(/[.]/g, '\\.')}[\\s,{]`, 'm').test(css), `oversight.css defines ${cls}`);
|
|
50
50
|
}
|
|
51
51
|
for (const p of PAGES) assert.match(read(`${p}.js`), /class="ov-row/, `${p}.js renders .ov-row`);
|
|
@@ -90,12 +90,14 @@ test('Gate carries no coloured band: state is a pill', () => {
|
|
|
90
90
|
}
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
test('the
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
assert.
|
|
97
|
-
const
|
|
98
|
-
|
|
93
|
+
test('the family keeps no confirm modal: it is the hall\'s one dialog, on the modal layer with one hairline', () => {
|
|
94
|
+
// task 1005085: the family's .ov-modal (copy desk its last user) became controls.css's
|
|
95
|
+
// .hall-dialog. It floats over a scrim, so it spends the one shadow DESIGN.md allows.
|
|
96
|
+
assert.doesNotMatch(read('oversight.css'), /\.ov-modal/, 'oversight.css draws no modal of its own');
|
|
97
|
+
const css = read('controls.css');
|
|
98
|
+
const modal = css.slice(css.indexOf('.hall-dialog {'), css.indexOf('}', css.indexOf('.hall-dialog {')));
|
|
99
|
+
assert.match(modal, /z-index: var\(--ds-z-modal\)/, 'the shell layer, not a guessed number');
|
|
100
|
+
assert.match(modal, /box-shadow: var\(--ds-shadow-pop\)/, 'the one floating shadow');
|
|
99
101
|
assert.match(modal, /border: var\(--line\) solid var\(--rule\)/, 'one rule hairline');
|
|
100
102
|
// task 1004829 made the Gate's approve an ask card with no modal; task 1004859
|
|
101
103
|
// removed the approve altogether. The page still opens no modal — and styles none.
|
|
@@ -412,7 +412,7 @@ function renderTask(task, { lastClaim = null, viewer = null, goal = null, goalsL
|
|
|
412
412
|
},
|
|
413
413
|
location: { hostname: 'builders.cloudbongos.com', pathname: `/task/${task && task.id ? task.id : '1'}`, search, origin: 'https://builders.cloudbongos.com', assign() {} },
|
|
414
414
|
addEventListener() {},
|
|
415
|
-
confirm: () => true,
|
|
415
|
+
OTBDialog: { confirm: async () => true }, // the hall's branded confirm (task 1005085)
|
|
416
416
|
navigator: { clipboard: { writeText: async () => {} } },
|
|
417
417
|
};
|
|
418
418
|
// task 1004477: the shell is what fills a [data-shell-exit] link. `shell: true`
|
package/tests/hall_street.mjs
CHANGED
|
@@ -489,7 +489,7 @@ test('the page holds the prototype\'s header, the street, the readings and the c
|
|
|
489
489
|
assert.match(html, /<a class="street-mark" id="streetHome" href="\/builders\/city"[^>]*>\{\{productName\}\}</);
|
|
490
490
|
for (const id of ['hudNeeds', 'hudMoving', 'hudShare', 'streetMode', 'streetSigns', 'streetPins', 'streetDoors', 'streetCard']) assert.match(html, new RegExp(`id="${id}"`), id);
|
|
491
491
|
const scripts = [...html.matchAll(/<script src="\/builders\/([a-z-]+)\.js/g)].map((m) => m[1]);
|
|
492
|
-
assert.deepEqual(scripts.filter((s) => !/^street-(deck|office)$/.test(s)), ['dom-utils', 'shell-trail', 'craft-mode', 'shell', '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');
|
|
492
|
+
assert.deepEqual(scripts.filter((s) => !/^street-(deck|office)$/.test(s)), ['dom-utils', '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');
|
|
493
493
|
assert.equal(City.STREET_HREF, '/builders/street', 'the city\'s door is this page');
|
|
494
494
|
});
|
|
495
495
|
|
|
@@ -88,7 +88,7 @@ const RESUME_READ = { ok: true, counter: { changed: 12, total: 38 } };
|
|
|
88
88
|
// ── the mini-DOM over the page's own markup ─────────────────────────────────
|
|
89
89
|
const HTML = read('studio.html');
|
|
90
90
|
const BODY = /<body[^>]*>([\s\S]*)<\/body>/.exec(HTML)[1].replace(/<script[\s\S]*?<\/script>/g, '');
|
|
91
|
-
const SOURCES = ['dom-utils.js', 'tweak-editor-lib.js', 'studio-lib.js', 'studio.js'].map((f) => [f, read(f)]);
|
|
91
|
+
const SOURCES = ['dom-utils.js', 'hall-dialog.js', 'tweak-editor-lib.js', 'studio-lib.js', 'studio.js'].map((f) => [f, read(f)]);
|
|
92
92
|
let boot = null;
|
|
93
93
|
|
|
94
94
|
function augment(el) {
|
|
@@ -17,6 +17,7 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
17
17
|
const lib = require(path.join(ROOT, 'modules', 'ui-design', 'kit', 'lib.js'));
|
|
18
18
|
const GOAL_INBOX = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'goal-inbox.js'), 'utf8');
|
|
19
19
|
const HALL_ASK = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'hall-ask.js'), 'utf8');
|
|
20
|
+
const HALL_DIALOG = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'hall-dialog.js'), 'utf8');
|
|
20
21
|
|
|
21
22
|
let browser = null;
|
|
22
23
|
try { browser = await lib.launchBrowser(); } catch (e) { if (!(e instanceof lib.NoBrowserError)) throw e; }
|
|
@@ -50,6 +51,7 @@ async function bootInbox() {
|
|
|
50
51
|
await page.setContent(PAGE);
|
|
51
52
|
await page.addScriptTag({ content: STUBS });
|
|
52
53
|
await page.addScriptTag({ content: HALL_ASK }); // task 1004820: the rows are OTBAsk cards
|
|
54
|
+
await page.addScriptTag({ content: HALL_DIALOG }); // the window is the hall's one dialog (task 1005085)
|
|
53
55
|
await page.addScriptTag({ content: GOAL_INBOX });
|
|
54
56
|
await page.evaluate(() => window.__widget.mount(document.getElementById('attention-scroll')));
|
|
55
57
|
await page.waitForFunction(() => document.querySelectorAll('.attn-card').length === 2);
|
|
@@ -126,7 +126,7 @@ const USES = {
|
|
|
126
126
|
'task.js': {
|
|
127
127
|
src: scanned(taskJs),
|
|
128
128
|
nonRender: {
|
|
129
|
-
"if (t.kind === 'verify' && !window.confirm(": 'a BRANCH on the value — gates a confirm prompt, renders nothing',
|
|
129
|
+
"if (t.kind === 'verify' && !(await window.OTBDialog.confirm({": 'a BRANCH on the value — gates a confirm prompt, renders nothing',
|
|
130
130
|
"const body = t.kind === 'verify' ? { verified: true } : {};": 'a REQUEST body — sent to the route, never displayed',
|
|
131
131
|
},
|
|
132
132
|
},
|
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
|
});
|
|
@@ -213,7 +213,7 @@ test('a corrected entry reads corrected on the item page; one marked wrong is no
|
|
|
213
213
|
// task 1005018: the dialog's screen-reader name follows the action that opened it.
|
|
214
214
|
test('the propose dialog is named by its own title: propose a change, stop tracking or send a new scout', () => {
|
|
215
215
|
const titleOf = (html) => {
|
|
216
|
-
const all = [...html.matchAll(/<
|
|
216
|
+
const all = [...html.matchAll(/<h2[^>]*id="mw-propose-title"[^>]*>([^<]*)<\/h2>/g)];
|
|
217
217
|
assert.equal(all.length, 1, 'exactly one title to point at');
|
|
218
218
|
return all[0][1];
|
|
219
219
|
};
|
|
@@ -232,6 +232,8 @@ test('the propose dialog is named by its own title: propose a change, stop track
|
|
|
232
232
|
querySelector: (s) => (s === 'form' ? form : stub()), querySelectorAll: () => [], showModal() {}, close() {},
|
|
233
233
|
};
|
|
234
234
|
const saved = globalThis.document;
|
|
235
|
+
// the hall's one dialog (hall-dialog.js, task 1005085) opens it; a stand-in records the open
|
|
236
|
+
globalThis.OTBDialog = { bind: () => ({ open() {}, close() {} }) };
|
|
235
237
|
globalThis.document = { getElementById: (id) => (id === 'mw-propose' ? dlg : null) };
|
|
236
238
|
try { MW.openPropose(mode, { item, dimensions: [], entries: [] }, () => {}); } finally { globalThis.document = saved; }
|
|
237
239
|
assert.equal(attrs.has('aria-label'), false, `${mode}: no fixed label left to override the title`);
|
|
@@ -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', () => {
|