@bongos/core 1.23.13 → 1.23.14
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 +27 -27
- package/docs/copy-inventory.md +77 -69
- package/docs/copy-registry.json +238 -165
- package/docs/module-api-changelog.md +2 -2
- package/docs/page-inventory.json +3 -2
- package/docs/page-readings.json +18 -18
- package/modules/copy-desk/page-status.js +2 -2
- package/modules/copy-desk/tests/copy_page_status.mjs +1 -1
- package/modules/hall-ui/public/approval-queue-lib.js +1 -1
- package/modules/hall-ui/public/approval-queue.css +26 -12
- package/modules/hall-ui/public/approval-queue.js +40 -16
- package/modules/hall-ui/public/studio-lib.js +5 -3
- package/modules/hall-ui/public/studio.html +8 -8
- package/modules/hall-ui/public/studio.js +25 -5
- package/modules/hall-ui/public/studio.states.json +54 -4
- package/modules/hall-ui/public/welcome-boot.js +62 -12
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +9 -1
- package/src/module-api.js +1 -1
- package/tests/hall_approval_queue.mjs +33 -9
- package/tests/hall_studio_home.mjs +74 -7
- package/tests/welcome_boot_browser.mjs +45 -3
|
@@ -278,7 +278,7 @@
|
|
|
278
278
|
},
|
|
279
279
|
"approvals": {
|
|
280
280
|
"auth": true,
|
|
281
|
-
"_": "The Approval queue (task 1004323): the studio's quick action opens the glass panel, 1 of 2
|
|
281
|
+
"_": "The Approval queue (task 1004323): the studio's quick action opens the glass panel, 1 of 2. The card is the changed lines, what each was beside what it is now, with Approve and ship and Send it back under them. Against the kit's own stub it opens on its could-not-read line; populated, run it against the hall-preview harness (fixtures copy-desk__approvals.json, tasks__visuals.json and the two CANNED_WRITES).",
|
|
282
282
|
"actions": [
|
|
283
283
|
[
|
|
284
284
|
"wait",
|
|
@@ -297,13 +297,14 @@
|
|
|
297
297
|
"visible": [
|
|
298
298
|
".aq-dialog",
|
|
299
299
|
".aq-eyebrow",
|
|
300
|
-
".aq-
|
|
300
|
+
".aq-line",
|
|
301
|
+
"[data-approve]"
|
|
301
302
|
]
|
|
302
303
|
}
|
|
303
304
|
},
|
|
304
305
|
"approvals-before": {
|
|
305
306
|
"auth": true,
|
|
306
|
-
"_": "
|
|
307
|
+
"_": "See it on the page, switched to before: the page as it was.",
|
|
307
308
|
"actions": [
|
|
308
309
|
[
|
|
309
310
|
"wait",
|
|
@@ -317,6 +318,14 @@
|
|
|
317
318
|
"wait",
|
|
318
319
|
1200
|
|
319
320
|
],
|
|
321
|
+
[
|
|
322
|
+
"click",
|
|
323
|
+
"[data-peek]"
|
|
324
|
+
],
|
|
325
|
+
[
|
|
326
|
+
"wait",
|
|
327
|
+
500
|
|
328
|
+
],
|
|
320
329
|
[
|
|
321
330
|
"click",
|
|
322
331
|
"[data-phase=\"before\"]"
|
|
@@ -336,7 +345,7 @@
|
|
|
336
345
|
},
|
|
337
346
|
"approvals-phone": {
|
|
338
347
|
"auth": true,
|
|
339
|
-
"_": "
|
|
348
|
+
"_": "See it on the page, on the phone render.",
|
|
340
349
|
"actions": [
|
|
341
350
|
[
|
|
342
351
|
"wait",
|
|
@@ -350,6 +359,14 @@
|
|
|
350
359
|
"wait",
|
|
351
360
|
1200
|
|
352
361
|
],
|
|
362
|
+
[
|
|
363
|
+
"click",
|
|
364
|
+
"[data-peek]"
|
|
365
|
+
],
|
|
366
|
+
[
|
|
367
|
+
"wait",
|
|
368
|
+
500
|
|
369
|
+
],
|
|
353
370
|
[
|
|
354
371
|
"click",
|
|
355
372
|
"[data-device=\"phone\"]"
|
|
@@ -473,6 +490,39 @@
|
|
|
473
490
|
".aq-outcome--sent"
|
|
474
491
|
]
|
|
475
492
|
}
|
|
493
|
+
},
|
|
494
|
+
"approvals-page": {
|
|
495
|
+
"auth": true,
|
|
496
|
+
"_": "See it on the page: the optional rendered page opens under the words, on the AFTER render.",
|
|
497
|
+
"actions": [
|
|
498
|
+
[
|
|
499
|
+
"wait",
|
|
500
|
+
700
|
|
501
|
+
],
|
|
502
|
+
[
|
|
503
|
+
"click",
|
|
504
|
+
"#studio-approvals"
|
|
505
|
+
],
|
|
506
|
+
[
|
|
507
|
+
"wait",
|
|
508
|
+
1200
|
|
509
|
+
],
|
|
510
|
+
[
|
|
511
|
+
"click",
|
|
512
|
+
"[data-peek]"
|
|
513
|
+
],
|
|
514
|
+
[
|
|
515
|
+
"wait",
|
|
516
|
+
700
|
|
517
|
+
]
|
|
518
|
+
],
|
|
519
|
+
"expect": {
|
|
520
|
+
"visible": [
|
|
521
|
+
".aq-dialog",
|
|
522
|
+
".aq-line",
|
|
523
|
+
".aq-render"
|
|
524
|
+
]
|
|
525
|
+
}
|
|
476
526
|
}
|
|
477
527
|
},
|
|
478
528
|
"reducedMotion": {
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
// hall underneath.
|
|
11
11
|
//
|
|
12
12
|
// Rules:
|
|
13
|
-
// -
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// - A signed-in builder sees it once per LOCAL day, and again after every fresh sign-in (the
|
|
14
|
+
// sign-in click leaves a flag in localStorage that the return visit consumes). Storage that
|
|
15
|
+
// throws cannot remember, so such a browser is never shown it (it would replay on every visit).
|
|
16
|
+
// - A visitor who is not signed in sees the Welcome on Home, every visit, with a "Sign in with
|
|
17
|
+
// GitHub" button in place of the role choices. Only on Home, never over a deep link (#/task/N, #/idea/N).
|
|
16
18
|
// - shell.js asks onAccess() BEFORE the craft picker; a load where the welcome showed marks the
|
|
17
19
|
// picker asked (OTBCraftMode.markAsked), so the two never stack.
|
|
18
20
|
// - Picking a role also sets the hall's craft mode. Governing is a seat (craft-mode.js), so the
|
|
@@ -26,6 +28,8 @@
|
|
|
26
28
|
'use strict';
|
|
27
29
|
|
|
28
30
|
var DAY_KEY = 'hall-welcome-day:';
|
|
31
|
+
var SIGNIN_KEY = 'hall-welcome-signin';
|
|
32
|
+
var SIGNIN_HREF = '/api/bongos/auth/web/start';
|
|
29
33
|
var ART = { day: '/builders/welcome-day.webp', night: '/builders/welcome-night.webp' };
|
|
30
34
|
var ART_WAIT = 1500; // the veil lifts once the painting is in, or after this long
|
|
31
35
|
var RUN_MS = 3400; // the city powering up before the greeting
|
|
@@ -159,15 +163,25 @@
|
|
|
159
163
|
'<span class="wb__version" id="wb-version" hidden></span></div>' +
|
|
160
164
|
'<div class="wb__veil" aria-hidden="true"></div>' +
|
|
161
165
|
'<button class="wb__skip" type="button" data-wb-skip>Skip</button>' +
|
|
162
|
-
'<div class="wb__after">' +
|
|
163
|
-
|
|
166
|
+
'<div class="wb__after">' + (builder ? signedInHtml(builder, now) : signedOutHtml()) + '</div>';
|
|
167
|
+
}
|
|
168
|
+
function signedInHtml(builder, now) {
|
|
169
|
+
return '<h2 class="wb__hi" id="wb-hi">' + esc(greetingFor(now) + ', ' + nameOf(builder)) + '</h2>' +
|
|
164
170
|
'<p class="wb__lede">Where do you want to start the day?</p>' +
|
|
165
171
|
'<div class="wb__choices">' +
|
|
166
172
|
'<button class="wb__choice" type="button" data-wb-roles aria-expanded="false" aria-controls="wb-roles"><b>Start with your role</b><span>Pick today’s craft and go straight to its room.</span></button>' +
|
|
167
173
|
'<button class="wb__choice" type="button" data-wb-home><b>Your home page</b><span>Your work, what needs you, and what shipped.</span></button>' +
|
|
168
174
|
'</div>' +
|
|
169
|
-
'<div class="wb__roles" id="wb-roles" hidden></div>'
|
|
170
|
-
|
|
175
|
+
'<div class="wb__roles" id="wb-roles" hidden></div>';
|
|
176
|
+
}
|
|
177
|
+
// A visitor who is not signed in: the same scene, one way in.
|
|
178
|
+
function signedOutHtml() {
|
|
179
|
+
return '<h2 class="wb__hi" id="wb-hi">Welcome to ' + esc(projectName(window.__BRANDING__)) + '</h2>' +
|
|
180
|
+
'<p class="wb__lede">Sign in to see your work, what needs you, and what shipped.</p>' +
|
|
181
|
+
'<div class="wb__choices">' +
|
|
182
|
+
'<button class="wb__choice" type="button" data-wb-signin><b>Sign in with GitHub</b><span>Pick up where you left off.</span></button>' +
|
|
183
|
+
'<button class="wb__choice" type="button" data-wb-home><b>Look around first</b><span>See the public side of the project.</span></button>' +
|
|
184
|
+
'</div>';
|
|
171
185
|
}
|
|
172
186
|
function rolesHtml() {
|
|
173
187
|
return rolesFor(craft()).map(function (r) {
|
|
@@ -250,6 +264,7 @@
|
|
|
250
264
|
if (!btn || !root || leaving) return;
|
|
251
265
|
if (btn.hasAttribute('data-wb-skip')) { begin(); finish(); return; }
|
|
252
266
|
if (btn.hasAttribute('data-wb-home')) { leave(); return; }
|
|
267
|
+
if (btn.hasAttribute('data-wb-signin')) { markSigningIn(); go(SIGNIN_HREF); return; }
|
|
253
268
|
if (btn.hasAttribute('data-wb-roles')) {
|
|
254
269
|
var box = root.querySelector('#wb-roles');
|
|
255
270
|
var open = box.hidden;
|
|
@@ -299,19 +314,54 @@
|
|
|
299
314
|
at(ART_WAIT, begin);
|
|
300
315
|
var skip = root.querySelector('[data-wb-skip]');
|
|
301
316
|
if (skip && skip.focus) skip.focus();
|
|
302
|
-
loadVersion();
|
|
317
|
+
if (builder) loadVersion(); // an anonymous visit costs no request
|
|
303
318
|
}
|
|
304
319
|
|
|
305
320
|
// A link straight to an overlay (#/task/N, #/idea/N, #/blocker/N) is not greeted over.
|
|
306
321
|
function deepLink() { return /^#\//.test((window.location && window.location.hash) || ''); }
|
|
307
322
|
|
|
323
|
+
// Home is the bare hall URL with no routed view; a signed-out visitor is only greeted there.
|
|
324
|
+
function onHome() {
|
|
325
|
+
var p = String((window.location && window.location.pathname) || '/').replace(/\/+$/, '');
|
|
326
|
+
var h = (window.location && window.location.hash) || '';
|
|
327
|
+
return (p === '' || p === '/builders') && (h === '' || h === '#' || h === '#home');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// A sign-in in flight is remembered, so the welcome greets the builder when they come back signed in.
|
|
331
|
+
function markSigningIn() { try { window.localStorage.setItem(SIGNIN_KEY, '1'); } catch (_) { /* cannot remember */ } }
|
|
332
|
+
function takeSignedIn() {
|
|
333
|
+
try {
|
|
334
|
+
var was = window.localStorage.getItem(SIGNIN_KEY) === '1';
|
|
335
|
+
if (was) window.localStorage.removeItem(SIGNIN_KEY);
|
|
336
|
+
return was;
|
|
337
|
+
} catch (_) { return false; }
|
|
338
|
+
}
|
|
339
|
+
// Any sign-in link on the page (the shell's own, in the top bar) counts the same as the welcome's button.
|
|
340
|
+
function watchSignInLinks() {
|
|
341
|
+
if (!document.addEventListener) return;
|
|
342
|
+
document.addEventListener('click', function (e) {
|
|
343
|
+
var a = e.target && e.target.closest && e.target.closest('a[href]');
|
|
344
|
+
// Same-origin only: the href must START with the sign-in path, not merely mention it.
|
|
345
|
+
if (a && String(a.getAttribute('href')).indexOf(SIGNIN_HREF) === 0) markSigningIn();
|
|
346
|
+
}, true);
|
|
347
|
+
}
|
|
348
|
+
|
|
308
349
|
// shell.js's call, before the craft picker's. True when the welcome is (or was) up on this load.
|
|
309
350
|
function onAccess(access, builder) {
|
|
310
351
|
if (shown) return true;
|
|
311
|
-
if (
|
|
352
|
+
if (deepLink()) return false;
|
|
353
|
+
if (access && access.authed === false) {
|
|
354
|
+
if (!onHome()) return false;
|
|
355
|
+
shown = true;
|
|
356
|
+
show(null);
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
if (!access || access.authed !== true || !builder || builder.id == null) return false;
|
|
312
360
|
var key = DAY_KEY + builder.id, now = new Date();
|
|
313
361
|
try {
|
|
314
|
-
|
|
362
|
+
// Every fresh sign-in is greeted; otherwise only the day's first visit.
|
|
363
|
+
var fresh = takeSignedIn();
|
|
364
|
+
if (!fresh && !firstVisitToday(window.localStorage.getItem(key), now)) return false;
|
|
315
365
|
window.localStorage.setItem(key, localDay(now));
|
|
316
366
|
} catch (_) { return false; }
|
|
317
367
|
shown = true;
|
|
@@ -322,11 +372,11 @@
|
|
|
322
372
|
}
|
|
323
373
|
|
|
324
374
|
var api = {
|
|
325
|
-
DAY_KEY: DAY_KEY, ROLES: ROLES, ART: ART, TOWERS: TOWERS, BEACONS: BEACONS, LANES: LANES, AW: AW, AH: AH,
|
|
375
|
+
DAY_KEY: DAY_KEY, SIGNIN_KEY: SIGNIN_KEY, ROLES: ROLES, ART: ART, TOWERS: TOWERS, BEACONS: BEACONS, LANES: LANES, AW: AW, AH: AH,
|
|
326
376
|
localDay: localDay, firstVisitToday: firstVisitToday, greetingFor: greetingFor, nameOf: nameOf, timeOfDay: timeOfDay,
|
|
327
377
|
artFor: artFor, projectName: projectName, bannerText: bannerText, rolesFor: rolesFor, pulseRuns: pulseRuns,
|
|
328
378
|
onAccess: onAccess, finish: finish, close: close,
|
|
329
379
|
};
|
|
330
|
-
if (typeof window !== 'undefined') window.OTBWelcome = api;
|
|
380
|
+
if (typeof window !== 'undefined') { window.OTBWelcome = api; watchSignInLinks(); }
|
|
331
381
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
332
382
|
})();
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.14",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.23.
|
|
9
|
+
"version": "1.23.14",
|
|
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.23.
|
|
3
|
+
"version": "1.23.14",
|
|
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
|
@@ -10460,7 +10460,15 @@
|
|
|
10460
10460
|
"text": "Project owners can now press Recruit next to any builder on the community leaderboard or search, which opens their project's invite page with that builder's name already filled in."
|
|
10461
10461
|
}
|
|
10462
10462
|
],
|
|
10463
|
-
"1.23.
|
|
10463
|
+
"1.23.14": [
|
|
10464
|
+
{
|
|
10465
|
+
"id": "1004839",
|
|
10466
|
+
"text": "The studio is simpler for artists. Its four doors now read in plain words: Resume, Work through tweaks, Approve and confirm tweaks, and Needs to be reviewed. The approve door only shows for artists at metic or above, and an em"
|
|
10467
|
+
},
|
|
10468
|
+
{
|
|
10469
|
+
"id": "1004851",
|
|
10470
|
+
"text": "The Welcome page now greets people every time they sign in, not only on their first visit of the day. Visitors who are not signed in now land on the Welcome with a Sign in with GitHub button."
|
|
10471
|
+
},
|
|
10464
10472
|
{
|
|
10465
10473
|
"id": "1004644",
|
|
10466
10474
|
"text": "The tweak editor's split view now shows the landing and status pages under the words, instead of opening a second window."
|
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.23.
|
|
84
|
+
const CORE_VERSION = '1.23.14'; // 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');
|
|
@@ -188,14 +188,19 @@ await test('open: the queue\'s head, the applied page\'s render, and the words b
|
|
|
188
188
|
assert.ok(b.classes.has('aq-open'), 'the page behind stops scrolling');
|
|
189
189
|
assert.equal(text(b.q('.aq-eyebrow')), 'approval queue · 1 of 2');
|
|
190
190
|
assert.equal(text(b.q('.aq-title')), 'Home · landing · 1 line rewritten, applied · 1 not applied');
|
|
191
|
+
assert.equal(b.q('img'), null, 'the words lead: no picture until the artist asks for it');
|
|
192
|
+
assert.equal(b.requests.filter((r) => r.url.endsWith('/visuals')).length, 0, 'and none is fetched until then');
|
|
193
|
+
await b.click(b.q('[data-peek]'));
|
|
191
194
|
const img = b.q('img');
|
|
192
195
|
assert.equal(img.attrs.src, url(41, 'after-desktop-light'), 'the AFTER render, desktop, in the hall\'s own mode');
|
|
193
196
|
assert.equal(img.attrs.alt, 'Home, after, desktop, light');
|
|
194
197
|
assert.deepEqual(b.qa('[data-phase]').map((x) => [text(x), x.attrs['aria-pressed']]), [['before', 'false'], ['after', 'true']]);
|
|
198
|
+
await b.click(b.q('[data-peek]'));
|
|
199
|
+
assert.equal(b.q('img'), null, 'and it folds away again');
|
|
195
200
|
assert.equal(b.qa('mark').length, 1, 'one applied line carries the mark');
|
|
196
201
|
assert.equal(text(b.q('mark')), 'Agents write the code.');
|
|
197
202
|
assert.match(text(b.q('.aq-line--refused')), /not applied: the page no longer shows it/);
|
|
198
|
-
assert.equal(b.body.querySelectorAll('img').length,
|
|
203
|
+
assert.equal(b.body.querySelectorAll('img').length, 0, 'a hostile line is text, never markup');
|
|
199
204
|
assert.equal(b.win.pwned, undefined);
|
|
200
205
|
assert.equal(text(b.q('.aq-hint')), 'highlighted: the words you changed');
|
|
201
206
|
assert.deepEqual(b.counts, [2], 'the door hears how many wait');
|
|
@@ -205,9 +210,10 @@ await test('open: the queue\'s head, the applied page\'s render, and the words b
|
|
|
205
210
|
await test('the switches: before and after, desktop and phone, light and dark, each a render of the applied branch', async () => {
|
|
206
211
|
const b = bootPanel({ answers: { [APPROVALS]: queueFor(), [vis(41)]: slotsFor(41) } });
|
|
207
212
|
await b.panel.open(); await settle(); await settle();
|
|
213
|
+
await b.click(b.q('[data-peek]'));
|
|
208
214
|
await b.click(b.q('[data-phase="before"]'));
|
|
209
215
|
assert.equal(b.q('img').attrs.src, url(41, 'before-desktop-light'));
|
|
210
|
-
assert.equal(b.qa('mark').length,
|
|
216
|
+
assert.equal(b.qa('mark').length, 1, 'the words beside the picture do not change with it');
|
|
211
217
|
await b.click(b.q('[data-device="phone"]'));
|
|
212
218
|
assert.equal(b.q('img').attrs.src, url(41, 'before-phone-light'));
|
|
213
219
|
assert.match(b.q('img').attrs.class, /aq-render--phone/);
|
|
@@ -216,17 +222,19 @@ await test('the switches: before and after, desktop and phone, light and dark, e
|
|
|
216
222
|
assert.equal(b.requests.filter((r) => r.url.endsWith('/visuals')).length, 1, 'the renders are read once per round');
|
|
217
223
|
});
|
|
218
224
|
|
|
219
|
-
await test('a missing render says what it shows instead; no renders at all
|
|
225
|
+
await test('a missing render says what it shows instead; no renders at all never blocks the decision', async () => {
|
|
220
226
|
const b = bootPanel({ answers: { [APPROVALS]: queueFor(), [vis(41)]: slotsFor(41, ['after-desktop-dark']) } });
|
|
221
227
|
await b.panel.open(); await settle(); await settle();
|
|
228
|
+
await b.click(b.q('[data-peek]'));
|
|
222
229
|
assert.equal(b.q('img').attrs.src, url(41, 'after-desktop-dark'));
|
|
223
230
|
assert.equal(text(b.q('.aq-frame__swap')), 'No desktop light render; showing desktop dark.');
|
|
224
231
|
const none = bootPanel({ answers: { [APPROVALS]: queueFor(), [vis(41)]: slotsFor(41, []) } });
|
|
225
232
|
await none.panel.open(); await settle(); await settle();
|
|
233
|
+
assert.ok(!none.q('[data-approve]').disabled, 'the words are what is approved: no picture is needed');
|
|
234
|
+
await none.click(none.q('[data-peek]'));
|
|
226
235
|
assert.equal(none.q('img'), null);
|
|
227
236
|
assert.match(text(none.q('.aq-frame__note')), /No render is attached/);
|
|
228
|
-
assert.ok(none.q('[data-
|
|
229
|
-
assert.ok(!none.q('[data-open-note]').disabled, 'sending it back does not');
|
|
237
|
+
assert.ok(!none.q('[data-open-note]').disabled, 'sending it back is open too');
|
|
230
238
|
});
|
|
231
239
|
|
|
232
240
|
await test('approve and ship: one POST through the host\'s write, then the artboard\'s line and the next page', async () => {
|
|
@@ -297,6 +305,24 @@ await test('a builder who may not decide reads the queue with a sentence, not tw
|
|
|
297
305
|
assert.match(text(b.q('.aq-sealed')), /Only the artist who wrote this page, or another artist/);
|
|
298
306
|
});
|
|
299
307
|
|
|
308
|
+
await test('open(pageId) lands on that page card, says so when it is not in the queue, and can skip the second read', async () => {
|
|
309
|
+
const b = bootPanel({ answers: { [APPROVALS]: queueFor(), [vis(42)]: slotsFor(42) } });
|
|
310
|
+
await b.panel.open('landing:projects');
|
|
311
|
+
await settle(); await settle();
|
|
312
|
+
assert.equal(text(b.q('.aq-eyebrow')), 'approval queue · 2 of 2', 'the Projects card, not the head');
|
|
313
|
+
const miss = bootPanel({ answers: { [APPROVALS]: queueFor() } });
|
|
314
|
+
await miss.panel.open('landing:nowhere');
|
|
315
|
+
await settle(); await settle();
|
|
316
|
+
assert.equal(text(miss.q('.aq-eyebrow')), 'approval queue · 1 of 2');
|
|
317
|
+
assert.equal(text(miss.q('.aq-error')), 'That page is not waiting for your sign-off right now.');
|
|
318
|
+
const once = bootPanel({ answers: { [APPROVALS]: queueFor() } });
|
|
319
|
+
await once.panel.refresh();
|
|
320
|
+
await once.panel.open(undefined, { reload: false });
|
|
321
|
+
await settle(); await settle();
|
|
322
|
+
assert.equal(once.requests.filter((r) => r.url.endsWith('/copy-desk/approvals')).length, 1, 'refreshed once, opened without re-reading');
|
|
323
|
+
assert.equal(once.panel.notAppliedCount(), 0);
|
|
324
|
+
});
|
|
325
|
+
|
|
300
326
|
await test('empty, closed with Escape, and focus goes back where it was', async () => {
|
|
301
327
|
const b = bootPanel({ answers: { [APPROVALS]: queueFor({ tasks: [] }) } });
|
|
302
328
|
const opener = { focus() { boot.focused = opener; } };
|
|
@@ -391,9 +417,6 @@ await test('a line holding {{productName}} reads as the product name in a chip,
|
|
|
391
417
|
assert.ok(chips.every((c) => c.attrs['data-hole'] === 'productName' && text(c) === 'Cloud Bongos'));
|
|
392
418
|
assert.ok(b.q('mark').querySelector('.aq-hole'), 'the chip sits inside the changed words\' mark');
|
|
393
419
|
assert.ok(!/\{\{/.test(b.scrim.innerHTML || '') && !/\{\{/.test(text(b.scrim)), 'no raw {{token}} anywhere in the panel');
|
|
394
|
-
b.scrim.fire('click', { target: b.q('[data-phase="before"]') }); await settle();
|
|
395
|
-
assert.equal(lineWords(), 'Both are how Cloud Bongos itself works.');
|
|
396
|
-
assert.equal(asideWords(), 'becomes: This project, Cloud Bongos, has two doors.');
|
|
397
420
|
});
|
|
398
421
|
|
|
399
422
|
await test('the dialog fits the viewport at every width: capped by the screen, the page shrinks first, rows wrap', () => {
|
|
@@ -411,7 +434,8 @@ await test('the dialog fits the viewport at every width: capped by the screen, t
|
|
|
411
434
|
assert.match(rule('.aq-foot'), /flex-wrap: wrap/);
|
|
412
435
|
assert.match(rule('.aq-body'), /min-width: 0/);
|
|
413
436
|
assert.match(rule('.aq-frame'), /flex-grow: 1;[\s\S]*min-width: 0/, 'the rendered page is what shrinks first');
|
|
414
|
-
assert.match(rule('.aq-lines'), /width:
|
|
437
|
+
assert.match(rule('.aq-lines'), /min-width: 0/, 'the words take the full width and can shrink');
|
|
438
|
+
assert.match(rule('.aq-line'), /grid-template-columns: minmax\(0, 1fr\) minmax\(0, 1fr\)/, 'was and now sit side by side');
|
|
415
439
|
assert.match(rule('.aq-line__text'), /overflow-wrap: anywhere/, 'a long word breaks inside its column');
|
|
416
440
|
});
|
|
417
441
|
|
|
@@ -129,7 +129,7 @@ function heldClock(h, m) {
|
|
|
129
129
|
return Held;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
function bootStudio({ answers = {}, signedIn = true, at = null, body = BODY } = {}) {
|
|
132
|
+
function bootStudio({ answers = {}, signedIn = true, at = null, body = BODY, rank = 'metic', queue = null } = {}) {
|
|
133
133
|
const root = augment(makeEl('body', {}));
|
|
134
134
|
root.innerHTML = body;
|
|
135
135
|
const byId = (id) => walk(root, (c) => (c.id === id ? c : null));
|
|
@@ -156,7 +156,7 @@ function bootStudio({ answers = {}, signedIn = true, at = null, body = BODY } =
|
|
|
156
156
|
const body = opts ? opts.body : undefined;
|
|
157
157
|
requests.push({ method, url, body });
|
|
158
158
|
const key = `${method} ${url.replace(/^\/api\/bongos/, '')}`;
|
|
159
|
-
if (key === 'GET /me') return signedIn ? ok({ builder: { id: ME, display_name: 'Artist A', rank
|
|
159
|
+
if (key === 'GET /me') return signedIn ? ok({ builder: { id: ME, display_name: 'Artist A', rank } }) : { ok: false, status: 401, data: {} };
|
|
160
160
|
const a = answers[key];
|
|
161
161
|
if (typeof a === 'function') return a(body);
|
|
162
162
|
if (a) return a;
|
|
@@ -170,6 +170,7 @@ function bootStudio({ answers = {}, signedIn = true, at = null, body = BODY } =
|
|
|
170
170
|
localStorage: { getItem: () => null, setItem() {} },
|
|
171
171
|
addEventListener() {},
|
|
172
172
|
scrollTo() {},
|
|
173
|
+
...(queue ? { OTBApprovalQueue: queue.api, OTBApprovalQueueLib: require('../modules/hall-ui/public/approval-queue-lib.js') } : {}),
|
|
173
174
|
};
|
|
174
175
|
// setInterval is held, never real (a real one would keep the run alive): the
|
|
175
176
|
// test fires each registered tick by hand, as a minute passing.
|
|
@@ -243,8 +244,8 @@ try {
|
|
|
243
244
|
]);
|
|
244
245
|
assert.equal(lib.landingHost('https://cloudbongos.com/', 'builders.x.test'), 'cloudbongos.com', 'the stamped origin wins');
|
|
245
246
|
assert.equal(lib.landingHost('{{publicOrigin}}', 'builders.x.test'), 'x.test', 'an unstamped token is no address');
|
|
246
|
-
assert.equal(lib.askedLine(NEXT.asked_pages), '1 page has been asked for. It comes up first in
|
|
247
|
-
assert.equal(lib.askedLine(4), '4 pages have been asked for. They come up first in
|
|
247
|
+
assert.equal(lib.askedLine(NEXT.asked_pages), '1 page has been asked for. It comes up first in Work through tweaks.');
|
|
248
|
+
assert.equal(lib.askedLine(4), '4 pages have been asked for. They come up first in Work through tweaks.');
|
|
248
249
|
assert.equal(lib.askedLine(0), '');
|
|
249
250
|
});
|
|
250
251
|
|
|
@@ -260,7 +261,7 @@ try {
|
|
|
260
261
|
assert.deepEqual(rows.map((r) => [r.title, r.state, r.age, r.stuck]), [
|
|
261
262
|
['Contact', 'Approved, going live', 'for 17 days', true],
|
|
262
263
|
['Tasks', 'Submitted, waiting to be applied', 'for 1 day', false],
|
|
263
|
-
['Status', 'Needs
|
|
264
|
+
['Status', 'Needs to be reviewed', 'since today', true],
|
|
264
265
|
]);
|
|
265
266
|
assert.deepEqual(lib.inFlightRows(null, NOW), []);
|
|
266
267
|
});
|
|
@@ -309,13 +310,79 @@ try {
|
|
|
309
310
|
assert.equal(s.requests.filter((r) => /copy-desk\/(tally|next|pages)/.test(r.url)).length, 0);
|
|
310
311
|
});
|
|
311
312
|
|
|
313
|
+
// A stand-in for the Approval queue panel: what the studio asks of it is the point here.
|
|
314
|
+
const stubQueue = ({ count = 2, pending = 0 } = {}) => {
|
|
315
|
+
const calls = { open: [], refresh: 0 };
|
|
316
|
+
const api = { mount: (o) => ({
|
|
317
|
+
open: async (...a) => { calls.open.push(a); },
|
|
318
|
+
refresh: async () => { calls.refresh += 1; o.onCount(count); return count; },
|
|
319
|
+
count: () => count,
|
|
320
|
+
notAppliedCount: () => pending,
|
|
321
|
+
}) };
|
|
322
|
+
return { calls, api };
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
await test('the approve door shows for metic and up, and never mounts the queue below that', async () => {
|
|
326
|
+
const q = stubQueue();
|
|
327
|
+
const metic = bootStudio({ answers: HOME, queue: q });
|
|
328
|
+
await metic.settle();
|
|
329
|
+
assert.equal(metic.el('studio-approvals').hidden, false);
|
|
330
|
+
assert.equal(q.calls.refresh, 1, 'the door hears how many wait');
|
|
331
|
+
const low = stubQueue();
|
|
332
|
+
const thetes = bootStudio({ answers: HOME, queue: low, rank: 'thetes' });
|
|
333
|
+
await thetes.settle();
|
|
334
|
+
assert.equal(thetes.el('studio-approvals').hidden, true, 'below metic the door stays hidden');
|
|
335
|
+
assert.equal(low.calls.refresh, 0, 'and its queue is never read');
|
|
336
|
+
const gone = bootStudio({ answers: HOME, rank: 'archon' });
|
|
337
|
+
await gone.settle();
|
|
338
|
+
assert.equal(gone.el('studio-approvals').hidden, true, 'no panel loaded: no door to nowhere');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
await test('an empty approval queue says so in a toast; a submitted-not-applied round still opens the dialog', async () => {
|
|
342
|
+
const empty = stubQueue({ count: 0 });
|
|
343
|
+
const a = bootStudio({ answers: HOME, queue: empty });
|
|
344
|
+
await a.settle();
|
|
345
|
+
await a.click('studio-approvals');
|
|
346
|
+
assert.equal(empty.calls.open.length, 0, 'no blank dialog');
|
|
347
|
+
assert.match(a.el('toast').textContent, /Nothing is waiting for your sign-off/);
|
|
348
|
+
const pending = stubQueue({ count: 0, pending: 1 });
|
|
349
|
+
const b = bootStudio({ answers: HOME, queue: pending });
|
|
350
|
+
await b.settle();
|
|
351
|
+
await b.click('studio-approvals');
|
|
352
|
+
assert.equal(pending.calls.open.length, 1, 'the not-applied list is still reachable');
|
|
353
|
+
const full = stubQueue({ count: 2 });
|
|
354
|
+
const c = bootStudio({ answers: HOME, queue: full });
|
|
355
|
+
await c.settle();
|
|
356
|
+
await c.click('studio-approvals');
|
|
357
|
+
assert.equal(full.calls.open.length, 1);
|
|
358
|
+
assert.equal(full.calls.open[0][1].reload, false, 'one read, then open without re-reading');
|
|
359
|
+
assert.equal(full.calls.refresh, 2, 'the page load and the click, nothing more');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
await test('a page that Needs to be reviewed opens its own approval card from the status panel', async () => {
|
|
363
|
+
const q = stubQueue();
|
|
364
|
+
const waiting = { ...PAGES, in_flight: [{ page_id: 'landing:contact', title: 'Contact', surface: 'landing', state: 'waiting_for_artist', state_word: 'Waiting for your review', since: day(1), stuck: false }] };
|
|
365
|
+
const s = bootStudio({ answers: { ...HOME, 'GET /copy-desk/pages': ok(waiting) }, queue: q });
|
|
366
|
+
await s.settle();
|
|
367
|
+
await s.click('st-status');
|
|
368
|
+
const overlay = s.el('st-panel');
|
|
369
|
+
overlay.fire('click', { target: overlay.querySelector('[data-review-page]'), preventDefault() {} });
|
|
370
|
+
await s.settle();
|
|
371
|
+
assert.equal(s.el('st-panel').hidden, true, 'the panel gives way to the card');
|
|
372
|
+
assert.deepEqual(JSON.parse(JSON.stringify(q.calls.open)).map((c) => c[0]), ['landing:contact']);
|
|
373
|
+
const low = bootStudio({ answers: { ...HOME, 'GET /copy-desk/pages': ok(waiting) }, queue: stubQueue(), rank: 'thetes' });
|
|
374
|
+
await low.settle();
|
|
375
|
+
await low.click('st-status');
|
|
376
|
+
assert.equal(low.el('st-panel-body').querySelector('[data-review-page]'), null, 'below metic the row opens the editor instead');
|
|
377
|
+
});
|
|
378
|
+
|
|
312
379
|
await test('the Tweak/CopyWrite Flow: the recommendation, pick another swaps it, take it claims THAT page and opens it', async () => {
|
|
313
380
|
const s = bootStudio({ answers: { ...HOME, 'POST /copy-desk/pages/builders:studio/claim': ok({ ok: true, outcome: 'created' }) } });
|
|
314
381
|
await s.settle();
|
|
315
382
|
await s.click('st-flow');
|
|
316
383
|
assert.equal(s.el('st-panel').hidden, false);
|
|
317
384
|
assert.ok(s.html.classList.contains('st-open'));
|
|
318
|
-
assert.equal(s.el('st-panel-title').textContent, '
|
|
385
|
+
assert.equal(s.el('st-panel-title').textContent, 'Work through tweaks');
|
|
319
386
|
const body = () => s.el('st-panel-body').innerHTML;
|
|
320
387
|
assert.match(body(), /st-next__page">Projects</);
|
|
321
388
|
assert.match(body(), /landing · 2 people asked for it/);
|
|
@@ -346,7 +413,7 @@ try {
|
|
|
346
413
|
await s.settle();
|
|
347
414
|
await s.click('st-status');
|
|
348
415
|
const body = s.el('st-panel-body').innerHTML;
|
|
349
|
-
assert.equal(s.el('st-panel-title').textContent, '
|
|
416
|
+
assert.equal(s.el('st-panel-title').textContent, 'Needs to be reviewed');
|
|
350
417
|
assert.deepEqual([...body.matchAll(/st-surface__name">([^<]*)</g)].map((m) => m[1]), ['Landing', 'Builders hall', 'Status']);
|
|
351
418
|
assert.match(body, /st-surface__where">cloudbongos\.com</);
|
|
352
419
|
assert.match(body, /1 of 2 tweaked/);
|
|
@@ -43,6 +43,7 @@ async function hall({ art = true, reducedMotion = 'no-preference', time = null }
|
|
|
43
43
|
await ctx.route(ORIGIN + '/**', (route) => {
|
|
44
44
|
const p = new URL(route.request().url()).pathname;
|
|
45
45
|
if (p === '/' || p === '/index.html') return route.fulfill({ contentType: 'text/html', body: PAGE });
|
|
46
|
+
if (p === '/api/bongos/auth/web/start') return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>github</title><p id="auth">sign in</p>' });
|
|
46
47
|
if (p === '/thinking' || p === '/city') return route.fulfill({ contentType: 'text/html', body: '<!doctype html><title>room</title><p id="room">' + p + '</p>' });
|
|
47
48
|
const ext = path.extname(p);
|
|
48
49
|
if (ext === '.webp' && !art) return route.fulfill({ status: 404, body: '' });
|
|
@@ -123,12 +124,53 @@ test('once a day: the next load the same day gets no welcome (and the picker is
|
|
|
123
124
|
await ctx.close();
|
|
124
125
|
});
|
|
125
126
|
|
|
126
|
-
test('a visitor who is not signed in
|
|
127
|
+
test('a visitor who is not signed in gets the Welcome on Home with a sign-in button, and no day is remembered', { skip }, async () => {
|
|
127
128
|
const ctx = await hall();
|
|
128
129
|
const { page } = await open(ctx);
|
|
129
130
|
await page.evaluate(() => window.OTBShell.applyAccess({ authed: false }));
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
await page.waitForSelector('#welcome-boot');
|
|
132
|
+
const s = await page.evaluate(() => ({
|
|
133
|
+
hi: document.getElementById('wb-hi').textContent,
|
|
134
|
+
signin: !!document.querySelector('[data-wb-signin]'),
|
|
135
|
+
roles: !!document.querySelector('[data-wb-roles]'),
|
|
136
|
+
keys: Object.keys(localStorage).filter((k) => k.startsWith('hall-welcome-day') || k === 'hall-welcome-signin'),
|
|
137
|
+
picker: !!document.getElementById('craft-picker'),
|
|
138
|
+
}));
|
|
139
|
+
assert.match(s.hi, /^Welcome to /);
|
|
140
|
+
assert.deepEqual([s.signin, s.roles, s.keys, s.picker], [true, false, [], false]);
|
|
141
|
+
await ctx.close();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('every fresh sign-in is welcomed again the same day, whichever sign-in link was clicked', { skip }, async () => {
|
|
145
|
+
for (const link of ['[data-wb-signin]', '#shell-signin']) {
|
|
146
|
+
const ctx = await hall();
|
|
147
|
+
const first = await open(ctx);
|
|
148
|
+
await signIn(first.page);
|
|
149
|
+
await first.page.waitForSelector('#welcome-boot');
|
|
150
|
+
await first.page.close(); // the day is now seen
|
|
151
|
+
const out = await open(ctx);
|
|
152
|
+
await out.page.evaluate(() => window.OTBShell.applyAccess({ authed: false }));
|
|
153
|
+
await out.page.waitForSelector('#welcome-boot');
|
|
154
|
+
// The welcome's own button, or the shell's top-bar link, leaves the flag and heads for GitHub.
|
|
155
|
+
await out.page.evaluate((sel) => document.querySelector(sel).click(), link);
|
|
156
|
+
await out.page.waitForSelector('#auth');
|
|
157
|
+
const { page } = await open(ctx);
|
|
158
|
+
await signIn(page);
|
|
159
|
+
await page.waitForSelector('#welcome-boot');
|
|
160
|
+
assert.equal(await page.evaluate(() => localStorage.getItem('hall-welcome-signin')), null, link + ': the flag is spent');
|
|
161
|
+
await ctx.close();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('a link that only mentions the sign-in path on another site sets no flag', { skip }, async () => {
|
|
166
|
+
const ctx = await hall();
|
|
167
|
+
const { page } = await open(ctx);
|
|
168
|
+
await page.evaluate(() => {
|
|
169
|
+
const a = document.createElement('a');
|
|
170
|
+
a.id = 'far'; a.href = 'https://elsewhere.test/auth/web/start'; a.addEventListener('click', (e) => e.preventDefault());
|
|
171
|
+
document.body.appendChild(a); a.click();
|
|
172
|
+
});
|
|
173
|
+
assert.equal(await page.evaluate(() => localStorage.getItem('hall-welcome-signin')), null);
|
|
132
174
|
await ctx.close();
|
|
133
175
|
});
|
|
134
176
|
|