@bongos/core 1.23.12 → 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 +30 -30
- package/docs/copy-inventory.md +77 -69
- package/docs/copy-registry.json +238 -165
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +3 -2
- package/docs/page-readings.json +29 -29
- 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/tweak-editor-lib.js +7 -6
- package/modules/hall-ui/public/welcome-boot.js +62 -12
- package/modules/hall-ui/records/tweak-editor.md +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +14 -0
- 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/hall_tweak_editor.mjs +8 -11
- 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": {
|
|
@@ -223,12 +223,13 @@
|
|
|
223
223
|
//
|
|
224
224
|
// The SIBLINGS (task 1004393): once the proxy answers the apex and status hosts
|
|
225
225
|
// with `frame-ancestors 'self' https://builders.<apex>` instead of SAMEORIGIN,
|
|
226
|
-
// those two pages may split in place too.
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
// apex and status.<apex> are
|
|
230
|
-
//
|
|
231
|
-
|
|
226
|
+
// those two pages may split in place too. The proxy change is live (checked on
|
|
227
|
+
// the apex and status hosts in task 1004644), so FRAME_SIBLINGS is on. Set it
|
|
228
|
+
// back to false if the proxy ever stops sending frame-ancestors: framing them
|
|
229
|
+
// would show the browser's refusal page. Only the apex and status.<apex> are
|
|
230
|
+
// siblings, never another subdomain: the proxy keeps every other host
|
|
231
|
+
// unframeable, sign-in and settings included.
|
|
232
|
+
const FRAME_SIBLINGS = true;
|
|
232
233
|
function siblingOrigins(loc) {
|
|
233
234
|
const here = new URL(loc.href || loc.origin);
|
|
234
235
|
if (!/^builders\./.test(here.hostname)) return [];
|
|
@@ -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
|
})();
|
|
@@ -27,7 +27,7 @@ The paper is paper and the room is the room whatever the hall's theme: the stage
|
|
|
27
27
|
- **Boxes grow to their words** rather than the artboard's rows 1 or 2.
|
|
28
28
|
- **A refused upload opens a panel** (task 1004354, `#tw-refused`): the "is in" panel's glass and scrim over the whole window, a headline per refusal (`lib.uploadRefusal`), the server's words, the lines it names, "what to do", and "download this page again" or close (Escape too). An artist uploads from the bar, often far down the page, where the paper's notice was off screen. The notice stays on the paper after the panel closes. The kit has no file-upload action, so the panel is proven in `tests/hall_tweak_editor.mjs` and by shots of a real browser at 1440 and 390, not by a states.json state.
|
|
29
29
|
- **The check, the restore and Shift + Enter (task 1004385, owner-asked).** Beside each line a round restore button (only on a changed line: it puts the page's words back and takes the check off) and a round green check. A checked line takes a light green wash; the owner picked the check over a tint alone. **Shift + Enter** in a line checks it and lands in the next, and a green hint above the words says so (with "N of M checked"), because it is the quickest way through a page. Enter alone is unchanged. The checks are the artist's place-keeper and stay in the browser (`localStorage`, keyed by page and reading hash, so a page that moved never ticks the wrong words); the server has no such field and `WRITES` is unchanged. A page the caller cannot write has none of it. Rules in `tweak-editor-lib.js` (`nextLineKey`, `confirmedCount`, `parseConfirmed`, `confirmStoreKey`, `hintText`).
|
|
30
|
-
- **No page title or sub text at the top of the paper, and "open the page" is a menu (task 1004386, owner-asked).** The bar already names the page, so the paper opens on its first section; the h1 stays, visually hidden, for the one-h1 floor. "open the page" is a **split button** (owner-amended mid-build): the main half repeats the artist's last choice ("open the page ↗" for a new tab, "open in split view"), and the ▾ beside it opens **open in new tab** and **open in split view**. Click, keyboard and touch; hover alone opens nothing, because a hover menu closes when the pointer wanders and does not exist on touch. Split view has a **draggable divider** (mouse, touch, or the arrow keys and Home/End on the focused divider; the live page takes 20% to 80% of the window, `lib.clampSplit`), and the frame stops taking the pointer while it is dragged. The choice, the size and whether split view was open are remembered (`hall-tweak-open-as`, `hall-tweak-split-size`, `hall-tweak-split`), so the next page opens the same way; a split that would be a second window never reopens on load, because a browser blocks a popup nobody clicked. At phone width the live page takes at most half the screen, whatever the saved size, because the two-row bar is tall there. Split view pins the live page under the words (words above, page below, both scroll; "close split view" unloads it; the frame loads only when opened). **The live site answers `X-Frame-Options: SAMEORIGIN`** (set at the proxy in front of the app, not in this repo), so only a page on the editor's own host can be framed. A landing or status page is on another host, so its split view opens the page in a second window docked to the bottom half of the screen (`lib.canFrame`, `lib.splitWindowFeatures`). **Letting them split in place (task 1004393)** is two steps, in this order. First the proxy answers the apex and `status.<apex>` with `<redacted>: frame-ancestors 'self' https://builders.<apex>` in place of `X-Frame-Options: SAMEORIGIN`, and every other host keeps SAMEORIGIN (sign-in and settings stay unframeable). Then `FRAME_SIBLINGS` in `tweak-editor-lib.js` flips to `true
|
|
30
|
+
- **No page title or sub text at the top of the paper, and "open the page" is a menu (task 1004386, owner-asked).** The bar already names the page, so the paper opens on its first section; the h1 stays, visually hidden, for the one-h1 floor. "open the page" is a **split button** (owner-amended mid-build): the main half repeats the artist's last choice ("open the page ↗" for a new tab, "open in split view"), and the ▾ beside it opens **open in new tab** and **open in split view**. Click, keyboard and touch; hover alone opens nothing, because a hover menu closes when the pointer wanders and does not exist on touch. Split view has a **draggable divider** (mouse, touch, or the arrow keys and Home/End on the focused divider; the live page takes 20% to 80% of the window, `lib.clampSplit`), and the frame stops taking the pointer while it is dragged. The choice, the size and whether split view was open are remembered (`hall-tweak-open-as`, `hall-tweak-split-size`, `hall-tweak-split`), so the next page opens the same way; a split that would be a second window never reopens on load, because a browser blocks a popup nobody clicked. At phone width the live page takes at most half the screen, whatever the saved size, because the two-row bar is tall there. Split view pins the live page under the words (words above, page below, both scroll; "close split view" unloads it; the frame loads only when opened). **The live site answers `X-Frame-Options: SAMEORIGIN`** (set at the proxy in front of the app, not in this repo), so only a page on the editor's own host can be framed. A landing or status page is on another host, so its split view opens the page in a second window docked to the bottom half of the screen (`lib.canFrame`, `lib.splitWindowFeatures`). **Letting them split in place (task 1004393)** is two steps, in this order. First the proxy answers the apex and `status.<apex>` with `<redacted>: frame-ancestors 'self' https://builders.<apex>` in place of `X-Frame-Options: SAMEORIGIN`, and every other host keeps SAMEORIGIN (sign-in and settings stay unframeable). Then `FRAME_SIBLINGS` in `tweak-editor-lib.js` flips to `true` (done in task 1004644, after checking both hosts answer frame-ancestors and no X-Frame-Options). `lib.canFrame` then also admits exactly those two origins, same scheme and port, and only from the builders host; never another subdomain or a lookalike. Flipping it before the proxy changes would put the browser's refusal page in the split, which is why it shipped off until the proxy was live; if the proxy ever stops sending frame-ancestors, flip it back.
|
|
31
31
|
- **Not on the artboard at all:** the phone layout (the bar becomes a two-row glass card, notes drop under their lines), the shell's full-screen button top right, the read-only and refusal notices on the paper, the chrome view (the switch off), and light mode.
|
|
32
32
|
|
|
33
33
|
## The proof
|
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
|
@@ -10459,5 +10459,19 @@
|
|
|
10459
10459
|
"id": "1002292",
|
|
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
|
+
],
|
|
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
|
+
},
|
|
10472
|
+
{
|
|
10473
|
+
"id": "1004644",
|
|
10474
|
+
"text": "The tweak editor's split view now shows the landing and status pages under the words, instead of opening a second window."
|
|
10475
|
+
}
|
|
10462
10476
|
]
|
|
10463
10477
|
}
|
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/);
|
|
@@ -675,11 +675,13 @@ try {
|
|
|
675
675
|
await test('split view can frame only its own host; the live site refuses to be framed from another', () => {
|
|
676
676
|
const here = { origin: 'https://builders.cloudbongos.com', href: 'https://builders.cloudbongos.com/builders/tweak-editor' };
|
|
677
677
|
assert.equal(lib.canFrame('https://builders.cloudbongos.com/studio', here), true);
|
|
678
|
-
assert.equal(lib.canFrame('https://cloudbongos.com/', here),
|
|
678
|
+
assert.equal(lib.canFrame('https://cloudbongos.com/', here), true, 'the apex is a sibling the proxy lets the builders host frame');
|
|
679
|
+
assert.equal(lib.canFrame('https://cloudbongos.com/', here, false), false, 'the apex is another origin when siblings are off');
|
|
679
680
|
assert.equal(lib.canFrame('not a url', { origin: '', href: '' }), false);
|
|
680
681
|
// task 1004393: the apex and status pages split in place only once the proxy allows it.
|
|
681
|
-
assert.equal(lib.FRAME_SIBLINGS,
|
|
682
|
-
assert.equal(lib.canFrame('https://status.cloudbongos.com/', here),
|
|
682
|
+
assert.equal(lib.FRAME_SIBLINGS, true, 'on: the proxy sends frame-ancestors for the builders host (task 1004644)');
|
|
683
|
+
assert.equal(lib.canFrame('https://status.cloudbongos.com/', here), true, 'the default follows the switch');
|
|
684
|
+
assert.equal(lib.canFrame('https://status.cloudbongos.com/', here, false), false, 'switched off, only the editor host frames');
|
|
683
685
|
assert.equal(lib.canFrame('https://cloudbongos.com/pricing', here, true), true, 'the apex, switched on');
|
|
684
686
|
assert.equal(lib.canFrame('https://status.cloudbongos.com/', here, true), true, 'the status host, switched on');
|
|
685
687
|
assert.equal(lib.canFrame('https://box.cloudbongos.com/', here, true), false, 'never another subdomain');
|
|
@@ -791,19 +793,14 @@ try {
|
|
|
791
793
|
} finally { PAGE.url = saved; }
|
|
792
794
|
});
|
|
793
795
|
|
|
794
|
-
await test('a page on the apex splits
|
|
796
|
+
await test('a page on the apex splits in place under the words, with no second window (task 1004644)', async () => {
|
|
795
797
|
const b = bootEditor({
|
|
796
798
|
answers: { 'GET /copy-desk/pages/landing:index/draft': ok(editorRead()) },
|
|
797
799
|
stored: { 'hall-tweak-open-as': 'split', 'hall-tweak-split': 'on' },
|
|
798
800
|
});
|
|
799
801
|
await b.settle();
|
|
800
|
-
assert.equal(b.opened.length, 0, 'no popup
|
|
801
|
-
assert.equal(b.el('tw-frame').hidden,
|
|
802
|
-
await b.click('tw-open');
|
|
803
|
-
assert.equal(b.opened.length, 1);
|
|
804
|
-
assert.deepEqual(b.opened[0].slice(0, 2), ['https://cloudbongos.com/', 'tw-live-page']);
|
|
805
|
-
assert.equal(b.opened[0][2], 'popup=yes,width=1600,height=500,left=0,top=500');
|
|
806
|
-
assert.equal(b.el('tw-frame').hidden, true);
|
|
802
|
+
assert.equal(b.opened.length, 0, 'no popup: the page is framed in place');
|
|
803
|
+
assert.equal(b.el('tw-frame').hidden, false, 'the apex page shows under the words');
|
|
807
804
|
});
|
|
808
805
|
|
|
809
806
|
// task 1004776: a guarded line shows what it must still say under it, and the
|