@bongos/core 1.24.62 → 1.24.63

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.
@@ -10,6 +10,9 @@
10
10
  // branded confirm (what will happen, in words) and runs its write only when the answer is yes;
11
11
  // 3. the guards: no window.confirm anywhere in modules/hall-ui, and each of the six builds now
12
12
  // runs on OTBDialog with no scrim/backdrop rule of its own.
13
+ // Task 1005136 adds the prompt variant (OTBDialog.prompt, which replaced every window.prompt), its
14
+ // flows, and widens the guard repo-wide: no browser confirm, alert or prompt in any served page
15
+ // (every module's public/ and views/, the API docs, and the pages src/ renders).
13
16
  import assert from 'node:assert/strict';
14
17
  import { test, after } from 'node:test';
15
18
  import { createRequire } from 'node:module';
@@ -55,9 +58,13 @@ async function runFlow(file, name, args, { answer, globals = {} } = {}) {
55
58
  const writes = [];
56
59
  const record = (m, u, b) => { writes.push({ method: m, url: String(u), body: b }); return { ok: true, status: 200, data: { permission_count: 3 } }; };
57
60
  const window = {
58
- OTBDialog: { confirm: async (o) => { asked.push(o); return answer; } },
61
+ OTBDialog: {
62
+ confirm: async (o) => { asked.push(o); return answer; },
63
+ prompt: async (o) => { asked.push(o); return answer; },
64
+ },
59
65
  location: { assign() {} },
60
66
  confirm: () => { throw new Error('window.confirm was called'); },
67
+ prompt: () => { throw new Error('window.prompt was called'); },
61
68
  };
62
69
  const ctx = vm.createContext({
63
70
  window,
@@ -135,6 +142,37 @@ for (const f of FLOWS) {
135
142
  });
136
143
  }
137
144
 
145
+ // Every replaced window.prompt whose flow is a named function: the branded prompt is asked, a
146
+ // dismissal (null) sends nothing, and the typed text is what the write carries.
147
+ const PROMPT_FLOWS = [
148
+ { flow: 'name a passkey card', file: 'settings-passkeys.js', fn: 'rename',
149
+ args: () => ['cred-1', { querySelector: (q) => (q === 'strong' ? { textContent: 'Old card' } : {}) }],
150
+ write: /passkeys\/rename/, carries: (w, text) => w.body.nickname === text },
151
+ { flow: 'release a studio round', file: 'studio.js', fn: 'releasePage', args: () => ['page-1', 77, fakeBtn()],
152
+ globals: { lib: { PAGE_ID_RE: /^[a-z0-9-]+$/ }, WRITE_ROUTES: { release: (id) => `/tweak/${id}/release` }, loadPages: async () => {}, paintPanel() {}, byId: () => null },
153
+ write: /tweak\/page-1\/release/, carries: (w, text) => w.body.note === text && w.body.task_id === 77 },
154
+ { flow: 'rename a saved view', file: 'hall-kit.js', fn: 'renameView', args: () => [{ id: 5, name: 'Mine' }],
155
+ viewCall: true, globals: { act: (fn) => fn(), views: { page: 'work' } },
156
+ write: /^\/5$/, carries: (w, text) => w.body.name === text },
157
+ ];
158
+
159
+ for (const f of PROMPT_FLOWS) {
160
+ test(`${f.flow}: the branded prompt asks, a dismissal sends nothing, the typed text is sent`, async () => {
161
+ let sent = [];
162
+ const extra = { ...(f.globals || {}) };
163
+ if (f.viewCall) extra.call = (m, u, b) => { sent.push({ method: m, url: u, body: b }); };
164
+ const no = await runFlow(f.file, f.fn, f.args(), { answer: null, globals: extra });
165
+ assert.equal(no.asked.length, 1, 'the prompt is shown once');
166
+ assert.ok(no.asked[0].title && no.asked[0].confirmLabel, 'it has a title and names its act');
167
+ assert.equal([...no.writes, ...sent].filter((w) => f.write.test(w.url)).length, 0, 'a dismissal sends nothing');
168
+ sent = [];
169
+ const yes = await runFlow(f.file, f.fn, f.args(), { answer: 'A new line', globals: extra });
170
+ const hits = [...yes.writes, ...sent].filter((w) => f.write.test(w.url));
171
+ assert.equal(hits.length, 1, 'the answer sends the write, once');
172
+ assert.ok(f.carries(hits[0], 'A new line'), 'the write carries the typed text');
173
+ });
174
+ }
175
+
138
176
  test('a verify-kind confirm attests the verification; any other kind asks nothing', async () => {
139
177
  const sent = [];
140
178
  const runLifecycle = (b, u, body) => sent.push({ u, body });
@@ -182,35 +220,74 @@ test('the bulk release on the shared toolbar asks first, and releases only on ye
182
220
 
183
221
  // ── the guards ──────────────────────────────────────────────────────────────
184
222
 
185
- function hallFiles() {
223
+ // Every file a browser is served, repo-wide (task 1005136): each module's public/ and views/, the
224
+ // API docs, and src/ (whose server-rendered pages carry inline scripts). Vendored bundles excluded.
225
+ function servedFiles() {
186
226
  const out = [];
187
227
  const walk = (d) => {
228
+ if (!fs.existsSync(d)) return;
188
229
  for (const e of fs.readdirSync(d, { withFileTypes: true })) {
189
230
  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);
231
+ if (e.isDirectory()) { if (!/^(node_modules|vendor|fixtures)$/.test(e.name)) walk(p); }
232
+ else if (/\.(js|html|mjs|cjs)$/.test(e.name) && !/\.(standalone|min)\.js$/.test(e.name)) out.push(p);
192
233
  }
193
234
  };
194
- walk(path.join(ROOT, 'modules', 'hall-ui'));
235
+ for (const m of fs.readdirSync(path.join(ROOT, 'modules'))) {
236
+ walk(path.join(ROOT, 'modules', m, 'public'));
237
+ walk(path.join(ROOT, 'modules', m, 'views'));
238
+ }
239
+ walk(path.join(ROOT, 'public-docs'));
240
+ walk(path.join(ROOT, 'src'));
195
241
  return out;
196
242
  }
197
243
 
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));
244
+ // A call to one of the browser's three boxes: window.confirm/alert/prompt, or a bare confirm( /
245
+ // alert( / prompt( that is not a method (OTBDialog.confirm) or the definition of one. Read line
246
+ // by line, comments out: a whole-file comment stripper eats code across a `/*` inside a string
247
+ // ('/api/bongos/*'), which would let a real call hide behind it. A block comment is one that
248
+ // OPENS a line (how every file here writes them); a same-line /* */, <!-- --> and a trailing
249
+ // // comment are cut where they stand.
250
+ const BOX = /\bwindow\s*\.\s*(confirm|alert|prompt)\b|(^|[^.\w$])(confirm|alert|prompt)\s*\(/;
251
+ function browserBoxCall(rawSrc) {
252
+ let block = null;
253
+ for (const raw of String(rawSrc).split('\n')) {
254
+ let t = raw.trim();
255
+ if (block) {
256
+ const end = t.indexOf(block);
257
+ if (end === -1) continue;
258
+ t = t.slice(end + block.length);
259
+ block = null;
205
260
  }
261
+ if (t.startsWith('/*') && !t.includes('*/')) { block = '*/'; continue; }
262
+ if (t.startsWith('<!--') && !t.includes('-->')) { block = '-->'; continue; }
263
+ if (/^(\/\/|\*)/.test(t)) continue;
264
+ const code = t.replace(/\/\*.*?\*\//g, '').replace(/<!--.*?-->/g, '')
265
+ .replace(/(^|[^:'"`\\])\/\/.*$/, '$1').replace(/function\s+(confirm|alert|prompt)\s*\(/g, '');
266
+ if (BOX.test(code)) return true;
206
267
  }
207
- assert.deepEqual(hits, [], 'use OTBDialog.confirm (hall-dialog.js) instead of the browser confirm');
268
+ return false;
269
+ }
270
+
271
+ test('no browser confirm, alert or prompt in any served page (comments stripped)', () => {
272
+ const files = servedFiles();
273
+ assert.ok(files.length > 200 && files.some((f) => f.includes(`${path.sep}public-landing${path.sep}`)) && files.some((f) => f.includes(`${path.sep}raise${path.sep}`)),
274
+ 'the scan reaches beyond the hall');
275
+ const hits = files.filter((f) => browserBoxCall(fs.readFileSync(f, 'utf8'))).map((f) => path.relative(ROOT, f));
276
+ assert.deepEqual(hits, [], 'use OTBDialog.confirm / OTBDialog.prompt (hall-dialog.js) instead of the browser\'s box');
208
277
  });
209
278
 
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({})')));
279
+ test('the guard catches a browser box when one comes back, and leaves the shared dialog alone', () => {
280
+ for (const bad of ["if (!window.confirm('Delete?')) return;", "const n = window.prompt('Name');", "alert('done')",
281
+ "if (x && !confirm('Discard?')) return;", "var r = prompt('Why?') || null;"]) {
282
+ assert.ok(browserBoxCall(bad), `caught: ${bad}`);
283
+ }
284
+ // a `/*` inside a string must not hide a real call on a later line
285
+ assert.ok(browserBoxCall("const p = '/api/bongos/*';\nif (!confirm('Go?')) return;\nconst q = 'a */ b';"), 'caught past a /* in a string');
286
+ for (const ok of ["// window.confirm('old') is history\nOTBDialog.confirm({})", '/* window.prompt() was here */ await window.OTBDialog.prompt({ title: "x" })',
287
+ '/* the browser box:\n window.confirm() calls whose box\n*/ OTBDialog.confirm({})', '<!-- an old\n confirm() lived here -->',
288
+ 'function confirm(opts) {}', 'const confirmAction = () => {}; confirmAction();']) {
289
+ assert.ok(!browserBoxCall(ok), `left alone: ${ok}`);
290
+ }
214
291
  });
215
292
 
216
293
  test('every hall page loads the shared dialog right after the shell, deferred alike', () => {
@@ -399,3 +476,78 @@ test('a confirm over a bound overlay: Escape closes the confirm only, then the o
399
476
  assert.equal(ov.modal, 'true');
400
477
  await ctx.close();
401
478
  });
479
+
480
+ // ── the prompt (task 1005136) ────────────────────────────────────────────────
481
+
482
+ test('the prompt: a labelled field in the shared dialog, focused with its text selected; Enter answers with what was typed', { skip }, async () => {
483
+ const { ctx, page } = await boot();
484
+ await page.focus('#go');
485
+ await page.evaluate(() => { window.__r = window.OTBDialog.prompt({ title: 'Name this card', label: 'Shown on your sign-in list', value: 'Old name', confirmLabel: 'Save' }); });
486
+ const s = await page.evaluate(() => {
487
+ const d = document.querySelector('dialog.hall-dialog');
488
+ const f = d.querySelector('input.hall-dialog__field');
489
+ const lab = document.querySelector(`label[for="${f.id}"]`);
490
+ return {
491
+ modal: d.matches(':modal'), title: document.getElementById(d.getAttribute('aria-labelledby')).textContent,
492
+ label: lab && lab.textContent, focused: document.activeElement === f,
493
+ selected: f.value.slice(f.selectionStart, f.selectionEnd), buttons: [...d.querySelectorAll('.hall-dialog__actions button')].map((b) => b.textContent),
494
+ width: Math.round(f.getBoundingClientRect().width), panel: Math.round(d.querySelector('.hall-dialog__body').getBoundingClientRect().width),
495
+ };
496
+ });
497
+ assert.equal(s.modal, true);
498
+ assert.equal(s.title, 'Name this card');
499
+ assert.equal(s.label, 'Shown on your sign-in list', 'the field is labelled');
500
+ assert.equal(s.focused, true, 'the field takes focus');
501
+ assert.equal(s.selected, 'Old name', 'its text is selected, so typing replaces it');
502
+ assert.deepEqual(s.buttons, ['Cancel', 'Save']);
503
+ assert.equal(s.width, s.panel, 'the field spans the dialog');
504
+ await page.keyboard.type('New name');
505
+ await page.keyboard.press('Enter');
506
+ assert.equal(await page.evaluate(() => window.__r), 'New name', 'Enter answers with the typed text');
507
+ assert.equal(await page.evaluate(() => document.querySelector('dialog.hall-dialog')), null, 'and the dialog is gone');
508
+ assert.equal(await page.evaluate(() => document.activeElement.id), 'go', 'focus is back on the trigger');
509
+ await ctx.close();
510
+ });
511
+
512
+ test('the prompt: Cancel, Escape and the scrim answer null; an empty answer is still an answer', { skip }, async () => {
513
+ const { ctx, page } = await boot();
514
+ const ask = (o) => page.evaluate((opts) => { window.__r = window.OTBDialog.prompt(opts); }, o);
515
+ await ask({ title: 'Why?' });
516
+ assert.equal(await page.evaluate(() => document.querySelector('input.hall-dialog__field').getAttribute('aria-label')), 'Why?', 'no label: the title names the field');
517
+ await page.click('.hall-dialog__actions .btn--ghost');
518
+ assert.equal(await page.evaluate(() => window.__r), null, 'Cancel is null');
519
+ await ask({ title: 'Why?' });
520
+ await page.keyboard.press('Escape');
521
+ assert.equal(await page.evaluate(() => window.__r), null, 'Escape is null');
522
+ await ask({ title: 'Why?' });
523
+ await page.mouse.click(5, 5);
524
+ assert.equal(await page.evaluate(() => window.__r), null, 'the scrim is null');
525
+ await ask({ title: 'Why?', confirmLabel: 'Go' });
526
+ await page.click('.hall-dialog__actions .btn:not(.btn--ghost)');
527
+ assert.equal(await page.evaluate(() => window.__r), '', 'confirming a blank field answers the empty string, not null');
528
+ await ctx.close();
529
+ });
530
+
531
+ test('the prompt: a required field keeps its confirm off until something is typed', { skip }, async () => {
532
+ const { ctx, page } = await boot();
533
+ await page.evaluate(() => { window.__r = window.OTBDialog.prompt({ title: 'New name for this view', required: true, confirmLabel: 'Rename' }); });
534
+ const ok = '.hall-dialog__actions .btn:not(.btn--ghost)';
535
+ assert.equal(await page.$eval(ok, (b) => b.disabled), true, 'blank: the confirm is off');
536
+ await page.keyboard.press('Enter');
537
+ assert.ok(await page.evaluate(() => !!document.querySelector('dialog.hall-dialog')), 'Enter on a blank required field does not answer');
538
+ await page.keyboard.type(' ');
539
+ assert.equal(await page.$eval(ok, (b) => b.disabled), true, 'spaces are still blank');
540
+ await page.keyboard.type('Mine');
541
+ assert.equal(await page.$eval(ok, (b) => b.disabled), false, 'typed: the confirm is on');
542
+ await page.click(ok);
543
+ assert.equal(await page.evaluate(() => window.__r), ' Mine', 'the text is handed back exactly as typed');
544
+ await ctx.close();
545
+ });
546
+
547
+ test('the prompt\'s title, body and label are text, never markup', { skip }, async () => {
548
+ const { ctx, page } = await boot();
549
+ await page.evaluate(() => { window.OTBDialog.prompt({ title: '<b>t</b>', body: '<img src=x onerror="window.__x=1">', label: '<i>l</i>' }); });
550
+ const s = await page.evaluate(() => ({ img: !!document.querySelector('.hall-dialog img'), i: !!document.querySelector('.hall-dialog i'), b: !!document.querySelector('.hall-dialog b'), x: window.__x }));
551
+ assert.deepEqual(s, { img: false, i: false, b: false, x: undefined });
552
+ await ctx.close();
553
+ });
@@ -0,0 +1,157 @@
1
+ // tests/landing_shared_controls.mjs — the landing, the hub and the raise page ask in the shared
2
+ // dialog and draw their buttons and dialog frame on the shared scales (task 1005136, goal 1000138).
3
+ //
4
+ // The UI check found the hub's one question (start another project while one builds) and the raise
5
+ // editor's "discard your changes?" still in the browser's confirm box, the hub's project card and
6
+ // both pages' buttons on typed-in corners, sizes and durations, and the signed-in landing bar at
7
+ // 320 overflowing (331px) with the mark under the "projects" label. This pins:
8
+ // 1. both pages load the shared dialog (hall-dialog.js, after the shared escaper) and ask in it;
9
+ // 2. the dialog the shared component builds has a frame on each page, on the shared scales;
10
+ // 3. the buttons and the project card spend the shared corner, line, layer, size and duration;
11
+ // 4. the signed-in bar folds the second-account door into the profile band below 900, keeping
12
+ // the same door there;
13
+ // 5. in a real browser (skips without a system Edge or Chrome): the hub's confirm opens in the
14
+ // shared component wearing the landing's frame and buttons, and the signed-in landing bar,
15
+ // built from index.html's own sheet and markup, holds one row at 320 with the mark clear of
16
+ // the "projects" trigger. The full page is the kit's: render.js --page / (0 problems).
17
+ import { test, after } from 'node:test';
18
+ import assert from 'node:assert/strict';
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { fileURLToPath } from 'node:url';
22
+ import { createRequire } from 'node:module';
23
+
24
+ const require = createRequire(import.meta.url);
25
+ const kit = require('../modules/ui-design/kit/lib.js');
26
+
27
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
28
+ const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
29
+ const LANDING = 'modules/public-landing/public/';
30
+ const COSMOS = read(`${LANDING}assets/cosmos.css`);
31
+ const RAISE_CSS = read('modules/raise/public/raise.css');
32
+
33
+ // The declarations of the first rule whose selector list is exactly `sel` (compact or spaced).
34
+ function rule(css, sel) {
35
+ const esc = sel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
36
+ const m = css.match(new RegExp(`(?:^|\\n|\\})\\s*${esc}\\s*\\{([^}]*)\\}`));
37
+ assert.ok(m, `a rule for ${sel}`);
38
+ return m[1];
39
+ }
40
+
41
+ test('the hub and the raise editor load the shared dialog after the shared escaper, and ask in it', () => {
42
+ const hub = read(`${LANDING}projects.html`);
43
+ assert.match(hub, /<script src="\/modules\/hall-ui\/public\/dom-utils\.js"><\/script>\n(?:<!--[^\n]*-->\n)?<script src="\/modules\/hall-ui\/public\/hall-dialog\.js"><\/script>/);
44
+ assert.match(hub, /window\.OTBDialog\.confirm\(\{ title: 'Start a new one\?'/, 'the hub asks in the shared confirm');
45
+ const page = read('modules/raise/page.js');
46
+ assert.match(page, /dom-utils\.js" defer><\/script><script src="\/modules\/hall-ui\/public\/hall-dialog\.js" defer><\/script><script src="\$\{BASE\}\/assets\/edit\.js/,
47
+ 'the editor gets the escaper, then the dialog, then itself');
48
+ assert.match(read('modules/raise/public/edit.js'), /window\.OTBDialog\.confirm\(\{ title: 'Discard your unsaved changes\?'/);
49
+ });
50
+
51
+ test('the shared dialog has a frame on each page, on the shared scales', () => {
52
+ for (const [name, css] of [['cosmos.css', COSMOS], ['raise.css', RAISE_CSS]]) {
53
+ const frame = rule(css, '.hall-dialog');
54
+ for (const want of [/z-index:\s*var\(--ds-z-modal\)/, /border-radius:\s*var\(--ds-radius-panel\)/, /border:\s*var\(--ds-line\) solid var\(--rule\)/]) {
55
+ assert.match(frame, want, `${name} .hall-dialog: ${want}`);
56
+ }
57
+ // the raise page floats it on the shared shadow; the apex world spends no shadow on UI
58
+ assert.match(frame, name === 'raise.css' ? /box-shadow:\s*var\(--ds-shadow-pop\)/ : /box-shadow:none/, `${name}: the shadow its world allows`);
59
+ assert.match(rule(css, '.hall-dialog::backdrop'), /background:\s*rgba\(0,\s*0,\s*0,\s*\.55\)/, `${name}: a black scrim, the one legal literal`);
60
+ for (const part of ['.hall-dialog__head', '.hall-dialog__title', '.hall-dialog__x', '.hall-dialog__actions']) rule(css, part);
61
+ }
62
+ // the buttons the component builds (btn btn--ghost / btn btn--danger) answer on the landing
63
+ assert.match(COSMOS, /\.btn\.ghost,\.btn\.btn--ghost\{/);
64
+ assert.match(COSMOS, /\.btn\.danger,\.btn\.btn--danger\{/);
65
+ assert.match(RAISE_CSS, /\.btn--ghost \{/);
66
+ });
67
+
68
+ test('the buttons and the project card spend the shared scales, not typed-in values', () => {
69
+ const btn = rule(COSMOS, '.btn');
70
+ assert.match(btn, /border-radius:var\(--ds-radius-pill\)/);
71
+ assert.match(btn, /font-size:var\(--ds-text-lg\)/);
72
+ assert.doesNotMatch(btn, /\.15s|999px|font-size:\d/, 'no typed-in duration, corner or size');
73
+ const rbtn = rule(RAISE_CSS, '.btn');
74
+ assert.match(rbtn, /border-radius: var\(--ds-radius-pill\)/);
75
+ assert.match(rbtn, /var\(--ds-text-xl\)/);
76
+ assert.doesNotMatch(rbtn, /\.12s|999px/, 'no typed-in duration or corner');
77
+ const scrim = rule(COSMOS, '.cardScrim');
78
+ assert.match(scrim, /z-index:var\(--ds-z-modal\)/, 'the card sits on the shared modal layer');
79
+ const card = rule(COSMOS, '.projCard');
80
+ assert.match(card, /border:var\(--ds-line\) solid var\(--rule\)/);
81
+ assert.match(card, /border-radius:var\(--ds-radius-panel\)/);
82
+ });
83
+
84
+ test('signed in, the narrow landing bar folds the second-account door into the profile band', () => {
85
+ const html = read(`${LANDING}index.html`);
86
+ const narrow = html.slice(html.indexOf('@media (max-width:900px){'));
87
+ assert.match(narrow.slice(0, narrow.indexOf('\n}\n')), /\.barRight:has\(\.who\) \.newacct\{display:none\}/, 'the pill leaves the bar once the chip is in it');
88
+ const pill = html.match(/<a class="pill newacct" href="([^"]+)"/);
89
+ assert.ok(pill, 'the bar still carries the door');
90
+ const band = html.slice(html.indexOf('id="menuProfile"'));
91
+ const fold = band.match(/<li class="bandCell bandFold"><a class="bandT" href="([^"]+)"[^>]*>New account<\/a><\/li>/);
92
+ assert.ok(fold, 'the profile band carries its folded twin');
93
+ assert.equal(fold[1], pill[1], 'the same door, to the same place');
94
+ });
95
+
96
+ // ── in a real browser ───────────────────────────────────────────────────────
97
+
98
+ let browser = null;
99
+ try { browser = await kit.launchBrowser(); } catch (e) { if (!(e instanceof kit.NoBrowserError)) throw e; }
100
+ after(() => browser && browser.close());
101
+ const skip = !browser && 'no system Edge or Chrome';
102
+ const TOKENS = read('modules/ui-design/public/tokens.css');
103
+
104
+ test('the hub\'s confirm opens in the shared dialog, in the landing\'s frame and buttons', { skip }, async () => {
105
+ const ctx = await browser.newContext({ viewport: { width: 390, height: 700 } });
106
+ const page = await ctx.newPage();
107
+ await page.setContent('<!doctype html><html data-mode="dark"><head><meta charset="utf-8"></head><body><button id="go">Start another project</button></body></html>');
108
+ await page.addStyleTag({ content: TOKENS + COSMOS });
109
+ await page.addScriptTag({ content: read('modules/hall-ui/public/dom-utils.js') });
110
+ await page.addScriptTag({ content: read('modules/hall-ui/public/hall-dialog.js') });
111
+ await page.focus('#go');
112
+ await page.evaluate(() => { window.__r = window.OTBDialog.confirm({ title: 'Start a new one?', body: 'Acme keeps building.', confirmLabel: 'Start a new one' }); });
113
+ const s = await page.evaluate(() => {
114
+ const d = document.querySelector('dialog.hall-dialog');
115
+ const cs = getComputedStyle(d);
116
+ const ghost = getComputedStyle(d.querySelector('.btn.btn--ghost'));
117
+ const ok = d.querySelector('.hall-dialog__actions .btn:not(.btn--ghost)');
118
+ return { modal: d.matches(':modal'), radius: cs.borderTopLeftRadius, line: cs.borderTopWidth, shadow: cs.boxShadow, z: cs.zIndex,
119
+ ghostBg: ghost.backgroundColor, ghostEdge: ghost.borderTopWidth, okH: ok.getBoundingClientRect().height, okR: getComputedStyle(ok).borderTopLeftRadius };
120
+ });
121
+ assert.equal(s.modal, true, 'the shared component, opened modal');
122
+ assert.equal(s.radius, '16px', 'the panel corner');
123
+ assert.equal(s.line, '1px', 'one hairline');
124
+ assert.equal(s.shadow, 'none', 'the apex world spends no shadow on UI');
125
+ assert.equal(s.z, '160', 'the shared modal layer');
126
+ assert.equal(s.ghostBg, 'rgba(0, 0, 0, 0)', 'Cancel takes the landing\'s ghost button through the shared name');
127
+ assert.equal(s.ghostEdge, '2px');
128
+ assert.ok(s.okH >= 24, 'the confirm clears the tap floor');
129
+ assert.equal(s.okR, '999px', 'the pill corner');
130
+ await page.click('.hall-dialog__actions .btn:not(.btn--ghost)');
131
+ assert.equal(await page.evaluate(() => window.__r), true);
132
+ await ctx.close();
133
+ });
134
+
135
+ test('signed in, the landing bar holds one row at 320 with the mark clear of the triggers', { skip }, async () => {
136
+ const html = read(`${LANDING}index.html`);
137
+ const styles = [...html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)].map((m) => m[1]).join('\n');
138
+ const bar = html.slice(html.indexOf('<header class="topbar bar">'), html.indexOf('</header>') + 9)
139
+ .replace(/<!--apex-only:(start|end)-->/g, '')
140
+ .replace(/<span class="whoSlot" id="who">[\s\S]*?<\/span>/, '<span class="whoSlot" id="who"><button type="button" class="trig who"><img width="26" height="26" alt=""><span class="who-name">builder</span></button></span>');
141
+ // the artist-review door the signed-in bar also carries (page-notes.css draws it 36px wide here)
142
+ const withReview = bar.replace('<span class="whoSlot"', '<button type="button" class="arv-btn" style="width:36px;min-height:36px">R</button><span class="whoSlot"');
143
+ const ctx = await browser.newContext({ viewport: { width: 320, height: 640 } });
144
+ const page = await ctx.newPage();
145
+ for (const [name, markup, signedIn] of [['signed in', withReview, true], ['signed out', bar.replace(/<span class="whoSlot" id="who">[\s\S]*?<\/span>/, '<span class="whoSlot" id="who"><a class="pill signin" href="#">Sign in</a></span>'), false]]) {
146
+ await page.setContent(`<!doctype html><html data-mode="dark"><head><meta charset="utf-8"></head><body>${markup}</body></html>`);
147
+ await page.addStyleTag({ content: TOKENS + styles });
148
+ const r = await page.evaluate(() => {
149
+ const box = (s) => { const e = document.querySelector(s); const b = e && e.getBoundingClientRect(); return b && b.width ? { l: b.left, r: b.right } : null; };
150
+ return { sw: document.documentElement.scrollWidth, mark: box('.barMark'), proj: box('#projTrig'), newacct: box('.newacct') };
151
+ });
152
+ assert.ok(r.sw <= 320, `${name}: no sideways scroll at 320 (scrollWidth ${r.sw})`);
153
+ assert.ok(r.mark.r <= r.proj.l, `${name}: the mark ends before the projects trigger starts (${r.mark.r} vs ${r.proj.l})`);
154
+ assert.equal(!!r.newacct, !signedIn, signedIn ? 'the second-account door has folded into the profile band' : 'signed out, the door stays in the bar');
155
+ }
156
+ await ctx.close();
157
+ });
@@ -227,5 +227,5 @@ test('the page renders the Release section and posts to the route', () => {
227
227
  const src = fs.readFileSync(new URL('../modules/npm-release/public/work.js', import.meta.url), 'utf8');
228
228
  assert.match(src, /\$\{releaseHtml\(data\)\}/);
229
229
  assert.match(src, /client\.request\('POST', `\$\{API\}\/npm-release\/release`, \{ body: \{ version \} \}\)/);
230
- assert.match(src, /window\.confirm\(/, 'a release is confirmed before it is sent');
230
+ assert.match(src, /await window\.OTBDialog\.confirm\(\{/, 'a release is confirmed in the hall\'s own dialog before it is sent (task 1005136)');
231
231
  });
@@ -607,7 +607,7 @@ test('the platform answers its own figures by the same rule: faces from its lead
607
607
  test('the poster is in the world: 560 wide, one hairline, no shadow, the disc surviving over the art, the facts a ledger', () => {
608
608
  const block = (COSMOS.match(/\.projCard\{[^}]*\}/) || [''])[0];
609
609
  assert.match(block, /width:min\(560px,100%\)/);
610
- assert.match(block, /border:1px solid var\(--rule\)/);
610
+ assert.match(block, /border:var\(--ds-line\) solid var\(--rule\)/, 'one hairline, on the shared line (task 1005136)');
611
611
  assert.match(block, /box-shadow:none/);
612
612
  assert.match(block, /container-type:inline-size/, 'the disc and the callouts measure the card, not the viewport');
613
613
  assert.match(COSMOS, /\.projCard \.cardArt\{[^}]*aspect-ratio:5\/2;object-fit:cover/, 'a fixed 5:2 crop');
@@ -225,7 +225,9 @@ test('the manifest step acknowledges code_received, offers a restart after a sta
225
225
  });
226
226
 
227
227
  test('"Start another project" asks before abandoning a standup still being watched', () => {
228
- assert.match(SCRIPT, /if \(instanceId && pollTimer && !confirm\(/);
228
+ // asked in the shared dialog since task 1005136, never the browser's box
229
+ assert.match(SCRIPT, /var asked = instanceId && pollTimer\n\s*\? window\.OTBDialog\.confirm\(\{/);
230
+ assert.match(SCRIPT, /asked\.then\(function \(yes\) \{ if \(yes\) startAnother\(\); \}\);/, 'it starts over only on yes');
229
231
  });
230
232
 
231
233
  /* ── every recovery sign-in from the wizard keeps the repo scope ── */
@@ -139,7 +139,9 @@ async function boot(opts = {}) {
139
139
  OTBBranding: { currencyLabel: () => 'Credits' },
140
140
  BongosClient: { createClient: () => api },
141
141
  location: locationObj,
142
- prompt: (msg) => { prompts.push(msg); return 'left for a new job'; },
142
+ // the reason is asked in the hall's own dialog (task 1005136); the browser box must not run
143
+ OTBDialog: { prompt: async (o) => { prompts.push(o.title); return 'left for a new job'; } },
144
+ prompt: () => { throw new Error('window.prompt was called'); },
143
145
  addEventListener() {},
144
146
  };
145
147
  // The shell's own /me read, shared on window.__hallReads (shell.js probeMe).