@bongos/core 1.25.24 → 1.25.26
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 +32 -27
- package/docs/copy-inventory.md +227 -222
- package/docs/copy-registry.json +290 -245
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +275 -269
- package/modules/hall-ui/public/city.css +3 -2
- package/modules/hall-ui/public/style.css +2 -1
- package/modules/hall-ui/public/thinking.css +8 -6
- package/modules/hall-ui/public/visor.css +65 -31
- package/modules/hall-ui/public/visor.js +121 -6
- package/modules/hall-ui/records/visor.md +14 -0
- package/modules/public-landing/public/projects.html +6 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/auth-github.js +17 -0
- package/src/bongos/auth.js +2 -1
- package/src/bongos/routes/auth.js +7 -7
- package/src/module-api.js +1 -1
- package/tests/auth_switch_account.mjs +3 -7
- package/tests/github_signup_handoff.mjs +135 -0
- package/tests/hall_visor.mjs +100 -1
|
@@ -710,6 +710,7 @@ module.exports = function buildAuthRouter() {
|
|
|
710
710
|
// ?switch=1 — sign in as a DIFFERENT GitHub account (task 1004789): show
|
|
711
711
|
// GitHub's account picker instead of re-using the one logged in there.
|
|
712
712
|
const wantsSwitch = req.query.switch === '1' || req.query.switch === 'true';
|
|
713
|
+
const wantsSignup = req.query.signup === '1' || req.query.signup === 'true'; // no GitHub account yet: its sign-up page (task 1005283)
|
|
713
714
|
// A sign-in begun on a satellite hub host (builders./status.) restarts on
|
|
714
715
|
// the canonical host (task 1004357). The handshake cookies are host-only now,
|
|
715
716
|
// and the callback always lands on the canonical host, so a state cookie set
|
|
@@ -720,8 +721,7 @@ module.exports = function buildAuthRouter() {
|
|
|
720
721
|
const raw = safeReturnTo(req.query.return) || safeReturnTo(req.headers.referer) || '/';
|
|
721
722
|
const back = raw.startsWith('/') ? `https://${host}${raw}` : raw;
|
|
722
723
|
const params = new URLSearchParams({ return: back });
|
|
723
|
-
|
|
724
|
-
if (wantsSwitch) params.set('switch', '1');
|
|
724
|
+
for (const [k, on] of [['repos', wantsRepos], ['switch', wantsSwitch], ['signup', wantsSignup]]) if (on) params.set(k, '1');
|
|
725
725
|
if (req.query.via === 'github') params.set('via', 'github');
|
|
726
726
|
return res.redirect(`${WEB_REDIRECT_ORIGIN.replace(/\/+$/, '')}${apiBase(req)}/auth/web/start?${params}`);
|
|
727
727
|
}
|
|
@@ -729,7 +729,7 @@ module.exports = function buildAuthRouter() {
|
|
|
729
729
|
// host (a builder who signed in on the apex, then opened builders.): no
|
|
730
730
|
// GitHub round-trip, just hand that host its copy. A repo-scope sign-in
|
|
731
731
|
// still goes to GitHub — it exists to ask for the broader grant.
|
|
732
|
-
if (!wantsRepos && !wantsSwitch) {
|
|
732
|
+
if (!wantsRepos && !wantsSwitch && !wantsSignup) {
|
|
733
733
|
const target = safeReturnTo(req.query.return);
|
|
734
734
|
if (target && !target.startsWith('/')) {
|
|
735
735
|
try {
|
|
@@ -746,7 +746,7 @@ module.exports = function buildAuthRouter() {
|
|
|
746
746
|
// lands on one chooser — Continue with GitHub or Continue with Google — so no
|
|
747
747
|
// page needs its own pair of buttons. `via=github` is the GitHub button (and
|
|
748
748
|
// any door that needs GitHub itself); "New account" names a GitHub account.
|
|
749
|
-
if (req.query.via !== 'github' && !wantsSwitch && googleConfigured()) {
|
|
749
|
+
if (req.query.via !== 'github' && !wantsSwitch && !wantsSignup && googleConfigured()) {
|
|
750
750
|
const ret = safeReturnTo(req.query.return) || safeReturnTo(req.headers.referer);
|
|
751
751
|
const gh = new URLSearchParams({ via: 'github' });
|
|
752
752
|
if (ret) gh.set('return', ret);
|
|
@@ -803,9 +803,9 @@ module.exports = function buildAuthRouter() {
|
|
|
803
803
|
// Federate through the hub when configured (ADR 0141 §1); otherwise the direct
|
|
804
804
|
// GitHub authorize, carrying the repo-scope opt-in. Same CSRF state cookie either
|
|
805
805
|
// way — the callback validates it identically, so the hub leg is CSRF-protected too.
|
|
806
|
-
res.redirect(auth.
|
|
807
|
-
|
|
808
|
-
|
|
806
|
+
if (auth.idpConfigured()) return res.redirect(auth.buildIdpAuthorizeUrl(state, redirectUri));
|
|
807
|
+
const authorizeUrl = auth.buildAuthorizeUrl(state, redirectUri, { repos: wantsRepos, switchAccount: wantsSwitch });
|
|
808
|
+
res.redirect((wantsSignup && auth.githubSignupUrl(authorizeUrl)) || authorizeUrl);
|
|
809
809
|
});
|
|
810
810
|
|
|
811
811
|
// GET /auth/web/handoff?code=&return= — the far side of the one-time session
|
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.25.
|
|
84
|
+
const CORE_VERSION = '1.25.26'; // 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');
|
|
@@ -24,13 +24,9 @@ test('switchAccount adds prompt=select_account; a plain sign-in does not', () =>
|
|
|
24
24
|
assert.equal(sw.searchParams.get('scope'), plain.searchParams.get('scope'));
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
assert.match(src, /switchAccount: wantsSwitch/);
|
|
31
|
-
// a switch must reach GitHub, never short-circuit through the live-session handoff
|
|
32
|
-
assert.match(src, /if \(!wantsRepos && !wantsSwitch\) \{/);
|
|
33
|
-
});
|
|
27
|
+
// The start route itself is now RUN, not read (task 1005283): tests/github_signup_handoff.mjs
|
|
28
|
+
// calls GET /auth/web/start with ?switch=1 and asserts the redirect carries
|
|
29
|
+
// prompt=select_account and goes to GitHub, never through the live-session handoff.
|
|
34
30
|
|
|
35
31
|
test('the landing carries the New account door beside Sign in', () => {
|
|
36
32
|
const html = fs.readFileSync(path.join(ROOT, 'modules/public-landing/public/index.html'), 'utf8');
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// tests/github_signup_handoff.mjs — no GitHub account yet: GitHub's own sign-up page, with
|
|
2
|
+
// "Continue with Google", and back to project creation (task 1005283).
|
|
3
|
+
//
|
|
4
|
+
// WHY. Bongos cannot make a GitHub account for anyone, and every project lives in a GitHub
|
|
5
|
+
// repository. A person with only a Google account used to meet "Continue with GitHub" and
|
|
6
|
+
// nothing else. GitHub's sign-up page offers "Continue with Google", and during an OAuth
|
|
7
|
+
// sign-in GitHub's own screen links it with `return_to` = the authorize step, so the
|
|
8
|
+
// sign-in resumes once the account exists (checked on GitHub's live pages, signed out,
|
|
9
|
+
// 2026-11-13). `?signup=1` on our sign-in start opens THAT page directly.
|
|
10
|
+
//
|
|
11
|
+
// What this pins, by RUNNING the start route's handler (no network, no database):
|
|
12
|
+
// 1. ?signup=1 redirects to https://github.com/signup with return_to = the same
|
|
13
|
+
// authorize step (same state cookie, same client, same repo scope, same callback);
|
|
14
|
+
// 2. it skips the GitHub-or-Google chooser, which would offer a Bongos Google sign-in
|
|
15
|
+
// that cannot make a project;
|
|
16
|
+
// 3. a plain sign-in still goes to the authorize step, and ?switch=1 still adds the
|
|
17
|
+
// account picker (converted here from tests/auth_switch_account.mjs's source read);
|
|
18
|
+
// 4. githubSignupUrl hands on only github.com's own authorize path;
|
|
19
|
+
// 5. the creation page's gate says why GitHub is needed and carries the door, whose
|
|
20
|
+
// link runs through the route above and returns to /projects?view=new.
|
|
21
|
+
//
|
|
22
|
+
// Run: node --test tests/github_signup_handoff.mjs
|
|
23
|
+
|
|
24
|
+
import assert from 'node:assert/strict';
|
|
25
|
+
import { test } from 'node:test';
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
|
|
31
|
+
process.env.BONGOS_GITHUB_CLIENT_ID = process.env.BONGOS_GITHUB_CLIENT_ID || 'gh-test-client';
|
|
32
|
+
process.env.BONGOS_GITHUB_CLIENT_SECRET = process.env.BONGOS_GITHUB_CLIENT_SECRET || 'gh-test-secret';
|
|
33
|
+
// Google sign-in on, so the chooser would show for a plain sign-in: the door must skip it
|
|
34
|
+
process.env.BONGOS_GOOGLE_CLIENT_ID = 'google-test-client';
|
|
35
|
+
process.env.BONGOS_GOOGLE_CLIENT_SECRET = 'google-test-secret';
|
|
36
|
+
|
|
37
|
+
const require = createRequire(import.meta.url);
|
|
38
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
39
|
+
// "Continue with Google" is offered only when the hub that keeps Google sign-ins is present
|
|
40
|
+
const seams = require(path.join(ROOT, 'src/module-seams.js'));
|
|
41
|
+
if (!seams.hasProvider('platform-identity.hub')) {
|
|
42
|
+
seams.registerProvider('platform-identity.hub', { signInWithGoogle: async () => null, recordHubSignin: async () => {} });
|
|
43
|
+
}
|
|
44
|
+
const buildAuthRouter = require(path.join(ROOT, 'src/bongos/routes/auth.js'));
|
|
45
|
+
const { githubSignupUrl } = require(path.join(ROOT, 'src/bongos/auth.js'));
|
|
46
|
+
const { attachFail } = require(path.join(ROOT, 'src/bongos/middleware/error-envelope.js'));
|
|
47
|
+
|
|
48
|
+
function fakeRes() {
|
|
49
|
+
const res = {
|
|
50
|
+
_status: 200, _body: null, _redirect: null, _cookies: {},
|
|
51
|
+
status(c) { this._status = c; return this; },
|
|
52
|
+
type() { return this; },
|
|
53
|
+
send(b) { this._body = b; return this; },
|
|
54
|
+
json(b) { this._body = b; return this; },
|
|
55
|
+
set() { return this; },
|
|
56
|
+
redirect(a, b) { this._redirect = b === undefined ? a : b; return this; },
|
|
57
|
+
cookie(n, v, o) { this._cookies[n] = { v, o }; return this; },
|
|
58
|
+
clearCookie() { return this; },
|
|
59
|
+
};
|
|
60
|
+
attachFail({}, res, () => {});
|
|
61
|
+
return res;
|
|
62
|
+
}
|
|
63
|
+
const start = (() => {
|
|
64
|
+
const layer = buildAuthRouter().stack.find((l) => l.route?.path === '/auth/web/start' && l.route.methods.get);
|
|
65
|
+
assert.ok(layer, 'GET /auth/web/start exists');
|
|
66
|
+
return layer.route.stack.at(-1).handle;
|
|
67
|
+
})();
|
|
68
|
+
const req = (query) => ({ query, headers: { host: 'localhost' }, baseUrl: '/api/bongos', body: {} });
|
|
69
|
+
async function run(query) { const res = fakeRes(); await start(req(query), res); return res; }
|
|
70
|
+
|
|
71
|
+
test('?signup=1 opens GitHub’s sign-up page, which resumes this same sign-in', async () => {
|
|
72
|
+
const res = await run({ via: 'github', signup: '1', return: '/projects?view=new', repos: '1' });
|
|
73
|
+
assert.ok(res._redirect, 'a redirect, not the chooser page');
|
|
74
|
+
const signup = new URL(res._redirect);
|
|
75
|
+
assert.equal(signup.origin + signup.pathname, 'https://github.com/signup');
|
|
76
|
+
assert.equal(signup.searchParams.get('source'), 'oauth', 'the same marker GitHub’s own "Create an account" link carries');
|
|
77
|
+
const back = new URL(signup.searchParams.get('return_to'), 'https://github.com');
|
|
78
|
+
assert.equal(back.pathname, '/login/oauth/authorize', 'the way back is the authorize step');
|
|
79
|
+
assert.equal(back.searchParams.get('client_id'), process.env.BONGOS_GITHUB_CLIENT_ID);
|
|
80
|
+
const stateCookie = Object.values(res._cookies).find((c) => /^[0-9a-f]{32}$/.test(String(c.v)));
|
|
81
|
+
assert.ok(stateCookie, 'a state cookie is set');
|
|
82
|
+
assert.equal(back.searchParams.get('state'), stateCookie.v,
|
|
83
|
+
'the state GitHub hands back is the one in this browser’s cookie, so the callback accepts it');
|
|
84
|
+
assert.match(back.searchParams.get('scope'), /\brepo\b/, 'the repo access project creation needs is asked once, on the way back');
|
|
85
|
+
assert.match(back.searchParams.get('redirect_uri'), /\/auth\/web\/callback$/);
|
|
86
|
+
assert.ok(Object.values(res._cookies).some((c) => c.v === '/projects?view=new'), 'after the callback, the person lands on project creation');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('the door skips the GitHub-or-Google chooser; a plain sign-in still shows it', async () => {
|
|
90
|
+
const signup = await run({ signup: '1', return: '/projects?view=new', repos: '1' });
|
|
91
|
+
assert.match(signup._redirect || '', /^https:\/\/github\.com\/signup\?/, 'no via=github needed: a GitHub sign-up is GitHub');
|
|
92
|
+
const plain = await run({ return: '/projects?view=new', repos: '1' });
|
|
93
|
+
assert.equal(plain._redirect, null);
|
|
94
|
+
assert.match(plain._body, /Continue with GitHub/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('without ?signup a sign-in goes to the authorize step, and ?switch=1 still adds the account picker', async () => {
|
|
98
|
+
const plain = await run({ via: 'github', return: '/projects?view=new', repos: '1' });
|
|
99
|
+
const p = new URL(plain._redirect);
|
|
100
|
+
assert.equal(p.origin + p.pathname, 'https://github.com/login/oauth/authorize');
|
|
101
|
+
assert.equal(p.searchParams.get('prompt'), null);
|
|
102
|
+
const sw = await run({ switch: '1', return: '/' });
|
|
103
|
+
const s = new URL(sw._redirect);
|
|
104
|
+
assert.equal(s.pathname, '/login/oauth/authorize');
|
|
105
|
+
assert.equal(s.searchParams.get('prompt'), 'select_account', 'a switch reaches GitHub, never the live-session handoff');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('githubSignupUrl hands on only github.com’s own authorize path', () => {
|
|
109
|
+
const ok = githubSignupUrl('https://github.com/login/oauth/authorize?client_id=c&state=s&scope=repo');
|
|
110
|
+
assert.equal(new URL(ok).searchParams.get('return_to'), '/login/oauth/authorize?client_id=c&state=s&scope=repo');
|
|
111
|
+
for (const bad of [
|
|
112
|
+
'https://evil.example/login/oauth/authorize?state=s',
|
|
113
|
+
'http://github.com/login/oauth/authorize?state=s',
|
|
114
|
+
'https://github.com/settings/tokens',
|
|
115
|
+
'https://github.com.evil.example/login/oauth/authorize',
|
|
116
|
+
'not a url',
|
|
117
|
+
null,
|
|
118
|
+
]) assert.equal(githubSignupUrl(bad), null, String(bad));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('the creation page says why GitHub is needed and offers the door, returning to creation', () => {
|
|
122
|
+
const hub = fs.readFileSync(path.join(ROOT, 'modules/public-landing/public/projects.html'), 'utf8').replace(/\r\n/g, '\n');
|
|
123
|
+
const gate = (hub.match(/<section class="panel" id="panel-gate"[\s\S]*?<\/section>/) || [''])[0];
|
|
124
|
+
assert.ok(gate, 'the gate is findable');
|
|
125
|
+
assert.match(gate, /<p class="finePrint" id="wizWhyGh">Your project’s files live in a GitHub repository, so it needs a GitHub account\.<\/p>/);
|
|
126
|
+
const door = gate.match(/<a id="wizSignup" href="([^"]+)">Make one free with Google<\/a>/);
|
|
127
|
+
assert.ok(door, 'the door sits on the gate');
|
|
128
|
+
assert.ok(gate.indexOf('id="wizSignin"') < gate.indexOf('id="wizSignup"'), 'under Continue with GitHub, not instead of it');
|
|
129
|
+
const href = new URL(door[1].replace(/&/g, '&'), 'https://hub.example');
|
|
130
|
+
assert.equal(href.pathname, '/api/bongos/auth/web/start');
|
|
131
|
+
assert.equal(href.searchParams.get('signup'), '1');
|
|
132
|
+
assert.equal(href.searchParams.get('repos'), '1', 'the same repo access the GitHub button asks for');
|
|
133
|
+
assert.equal(href.searchParams.get('return'), '/projects?view=new');
|
|
134
|
+
assert.match(gate, /choose <strong>Continue with Google<\/strong>, and you come straight back here/);
|
|
135
|
+
});
|
package/tests/hall_visor.mjs
CHANGED
|
@@ -236,7 +236,7 @@ test('source: the minimal visor spends the Helm tokens and types in no colour, s
|
|
|
236
236
|
assert.ok(phone, 'the phone rules sit in the shared phone block');
|
|
237
237
|
assert.match(phone[0], /\.visor__hint:not\(\[data-visor-sheet\]\) \{ display: none; \}/, 'a phone keeps only Actions of the hints');
|
|
238
238
|
assert.doesNotMatch(read('home-city.css'), /visor-on/, 'Home has no top-right pill to clear any more');
|
|
239
|
-
assert.match(read('city.css'), /\nhtml\.visor-on \.city-door \{ bottom:
|
|
239
|
+
assert.match(read('city.css'), /\nhtml\.visor-on \.city-door \{ bottom: calc\(180px - var\(--visor-drop, 0px\)\); \}/, 'the city\'s door clears the centred button at every width (task 1005266: the row dropped, except on a phone)');
|
|
240
240
|
});
|
|
241
241
|
|
|
242
242
|
// ---------- in a browser: the glass over a stand-in scene ----------
|
|
@@ -554,3 +554,102 @@ test('the glass is the kit\'s: the cut accent button, ice hints with no box, the
|
|
|
554
554
|
assert.match(f.ring, /inset/, 'focus is the inset two-tone ring (--press-focus)');
|
|
555
555
|
await ctx.close();
|
|
556
556
|
});
|
|
557
|
+
|
|
558
|
+
// ---------- task 1005266: the glass rests while you look and wakes slim when you act ----------
|
|
559
|
+
test('the top arc cuts into pieces of the same curve (task 1005266)', () => {
|
|
560
|
+
const V = loadPure();
|
|
561
|
+
assert.equal(V.arcPiece(0, 1), 'M76 100Q500 -100 924 100', 'the whole of it is the arc itself');
|
|
562
|
+
const end = (d) => d.split(' ').slice(-2).join(' '), start = (d) => d.slice(1, d.indexOf('Q'));
|
|
563
|
+
assert.equal(end(V.arcPiece(0, 0.5)), start(V.arcPiece(0.5, 1)), 'two halves meet');
|
|
564
|
+
assert.equal(start(V.arcPiece(0.5, 1)), '500 0', 'at the top of the arc');
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test('the glass rests when you leave it alone and wakes when you reach for it (task 1005266)', { skip }, async () => {
|
|
568
|
+
const { ctx, page, errors } = await open({ helm: true });
|
|
569
|
+
const look = () => page.evaluate(() => {
|
|
570
|
+
const op = (s) => getComputedStyle(document.querySelector(s)).opacity;
|
|
571
|
+
return { resting: OTBVisor.resting(), main: op('.visor__main'), hints: op('.visor__hints'), who: op('.visor__who'), place: op('.visor__top'), sprite: op('.visor__sprite'), sight: op('.visor__sight') };
|
|
572
|
+
});
|
|
573
|
+
assert.equal((await look()).resting, false, 'arriving at a place shows the whole glass');
|
|
574
|
+
await page.mouse.move(640, 300);
|
|
575
|
+
await page.waitForFunction(() => OTBVisor.resting(), null, { timeout: 8000 });
|
|
576
|
+
await page.waitForTimeout(400);
|
|
577
|
+
assert.deepEqual(await look(), { resting: true, main: '0', hints: '0', who: '0', place: '0', sprite: '1', sight: '1' }, 'at rest only the frame and your picture are drawn');
|
|
578
|
+
await page.mouse.move(640, 790);
|
|
579
|
+
await page.waitForTimeout(400);
|
|
580
|
+
assert.deepEqual(await look(), { resting: false, main: '1', hints: '1', who: '1', place: '1', sprite: '1', sight: '1' }, 'the pointer near the bottom of the view wakes it');
|
|
581
|
+
await page.mouse.move(640, 300);
|
|
582
|
+
await page.mouse.down();
|
|
583
|
+
assert.equal((await look()).resting, true, 'a press on the scene rests it at once');
|
|
584
|
+
await page.mouse.move(700, 790, { steps: 3 });
|
|
585
|
+
assert.equal((await look()).resting, true, 'a drag that passes the bottom of the view is looking, not reaching');
|
|
586
|
+
await page.mouse.up();
|
|
587
|
+
// a visor key wakes it, and the open sheet holds it awake
|
|
588
|
+
await page.mouse.move(640, 300);
|
|
589
|
+
await page.keyboard.press('a');
|
|
590
|
+
assert.deepEqual(await page.evaluate(() => [OTBVisor.isOpen(), OTBVisor.resting()]), [true, false], 'A opens the sheet on a waking glass');
|
|
591
|
+
await page.mouse.click(300, 300);
|
|
592
|
+
assert.equal((await look()).resting, false, 'the sheet holds the glass awake');
|
|
593
|
+
await page.keyboard.press('Escape');
|
|
594
|
+
assert.equal(await page.evaluate(() => document.activeElement.matches('[data-visor-sheet]')), true, 'the keyboard is back on Actions, which also holds it');
|
|
595
|
+
await page.mouse.click(300, 300);
|
|
596
|
+
assert.deepEqual(await page.evaluate(() => [OTBVisor.resting(), document.activeElement === document.body]), [true, true], 'going back to the scene rests it and hands the keyboard back');
|
|
597
|
+
// a control you cannot see is never pressed by accident: the first press only wakes
|
|
598
|
+
await page.evaluate(() => document.querySelector('[data-visor-sheet]').click());
|
|
599
|
+
assert.deepEqual(await page.evaluate(() => [OTBVisor.isOpen(), OTBVisor.resting()]), [false, false]);
|
|
600
|
+
assert.deepEqual(errors, []);
|
|
601
|
+
await ctx.close();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test('a key hint is its key, and pointing at it names it above the row without moving another (task 1005266)', { skip }, async () => {
|
|
605
|
+
const { ctx, page, errors } = await open({ helm: true });
|
|
606
|
+
const boxes = () => page.evaluate(() => [...document.querySelectorAll('.visor__hint')].map((h) => Math.round(h.getBoundingClientRect().left)));
|
|
607
|
+
const before = await boxes();
|
|
608
|
+
await page.hover('[data-visor-act="thinking"]');
|
|
609
|
+
assert.equal(await page.evaluate(() => document.querySelector('.visor__hints').getAttribute('data-name')), 'Your thinking');
|
|
610
|
+
assert.deepEqual(await boxes(), before, 'naming a key moves no key');
|
|
611
|
+
assert.equal(await page.textContent('[data-visor-act="thinking"]'), 'TYour thinking', 'the name is still in the page for a screen reader');
|
|
612
|
+
await page.mouse.move(640, 300);
|
|
613
|
+
assert.equal(await page.evaluate(() => document.querySelector('.visor__hints').hasAttribute('data-name')), false);
|
|
614
|
+
assert.deepEqual(errors, []);
|
|
615
|
+
await ctx.close();
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test('the top arc is the version\'s progress while awake, and one plain arc at rest (task 1005266)', { skip }, async () => {
|
|
619
|
+
const { ctx, page, errors } = await open({ helm: true });
|
|
620
|
+
await ctx.route(ORIGIN + '/api/bongos/versions/progress', (r) => r.fulfill({ contentType: 'application/json', body: JSON.stringify({ progress: [{ version_id: 'BONGOS-V2', name: 'The ten working areas', status: 'building', percent_complete: 89 }] }) }));
|
|
621
|
+
await ctx.route(ORIGIN + '/api/bongos/versions/focus', (r) => r.fulfill({ contentType: 'application/json', body: JSON.stringify({ focus_version_id: 'BONGOS-V2' }) }));
|
|
622
|
+
await page.reload();
|
|
623
|
+
await page.waitForSelector('.visor__frame--segs');
|
|
624
|
+
const read = () => page.evaluate(() => {
|
|
625
|
+
const shown = (e) => getComputedStyle(e).display !== 'none';
|
|
626
|
+
return {
|
|
627
|
+
segs: [...document.querySelectorAll('.visor__seg:not(.visor__line--under)')].map((p) => (/visor__seg--(\w+)/.exec(p.getAttribute('class')) || [])[1]),
|
|
628
|
+
segsShown: [...document.querySelectorAll('.visor__seg')].every(shown),
|
|
629
|
+
arcShown: [...document.querySelectorAll('.visor__arc')].every(shown),
|
|
630
|
+
words: document.querySelector('.visor__progress-words [aria-hidden]').textContent,
|
|
631
|
+
said: document.querySelector('.visor__progress-words .sr-only').textContent,
|
|
632
|
+
bars: document.querySelectorAll('.visor .segs').length,
|
|
633
|
+
line: [...document.querySelectorAll('.visor__top > *')].map((e) => Math.round(e.getBoundingClientRect().top)),
|
|
634
|
+
};
|
|
635
|
+
});
|
|
636
|
+
const awake = await read();
|
|
637
|
+
assert.deepEqual(awake.segs, ['done', 'done', 'done', 'now'], 'four quarters: three built, the fourth being built');
|
|
638
|
+
assert.deepEqual([awake.segsShown, awake.arcShown], [true, false], 'awake, the pieces stand in for the plain arc');
|
|
639
|
+
assert.deepEqual([awake.words, awake.said, awake.bars], ['BONGOS-V2 · 89%', 'BONGOS-V2, The ten working areas: 89% built', 0], 'the reading is still said in words; the separate row of bars is gone');
|
|
640
|
+
assert.equal(awake.line.length, 2);
|
|
641
|
+
assert.ok(Math.abs(awake.line[0] - awake.line[1]) <= 2, 'the place and the reading share one line');
|
|
642
|
+
await page.mouse.move(640, 300);
|
|
643
|
+
await page.mouse.down(); await page.mouse.up();
|
|
644
|
+
const rest = await read();
|
|
645
|
+
assert.deepEqual([rest.segsShown, rest.arcShown], [false, true], 'at rest it is the one plain arc');
|
|
646
|
+
assert.deepEqual(errors, []);
|
|
647
|
+
await ctx.close();
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test('a phone keeps the whole glass: there is no pointer to reach with (task 1005266)', { skip }, async () => {
|
|
651
|
+
const { ctx, page } = await open({ helm: true, width: 390, height: 844 });
|
|
652
|
+
await page.evaluate(() => OTBVisor.rest());
|
|
653
|
+
assert.deepEqual(await page.evaluate(() => [OTBVisor.resting(), document.getElementById('visor').classList.contains('visor--rest'), getComputedStyle(document.querySelector('.visor__main')).opacity]), [false, false, '1']);
|
|
654
|
+
await ctx.close();
|
|
655
|
+
});
|