@bongos/core 1.19.400 → 1.19.402
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 +48 -38
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0200-re-point-a-bring-your-own-domain.md +105 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +3 -2
- package/docs/copy-inventory.md +233 -232
- package/docs/copy-registry.json +293 -284
- package/docs/design/projects-hub-direction-v2.md +38 -0
- package/docs/recipes/ui-look-before-you-ship.md +1 -1
- package/modules/provisioning/migrations/provisioning_012_domain_repoint.sql +35 -0
- package/modules/provisioning/provisioning.js +68 -8
- package/modules/provisioning/routes/provisioning.js +50 -23
- package/modules/ui-design/kit/lib.js +6 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +90 -35
- package/public-landing/index.html +13 -15
- package/public-landing/index.probes.json +8 -5
- package/public-landing/projects.html +49 -20
- package/public-landing/projects.probes.json +17 -10
- package/public-landing/projects.states.json +1 -1
- package/scripts/gds/provision-net.js +33 -0
- package/scripts/gds/provision-units.js +18 -0
- package/scripts/gds/provision.js +97 -5
- package/src/module-api.js +5 -1
- package/tests/landing_page.mjs +8 -4
- package/tests/projects_hub.mjs +28 -1
- package/tests/projects_hub_pre_uat.mjs +28 -2
- package/tests/provision.mjs +185 -12
- package/tests/provisioning_default_domain.mjs +5 -3
- package/tests/provisioning_domain_attach.mjs +166 -17
- package/tests/public_landing_projects.mjs +5 -1
- package/tests/ui_design_kit.mjs +5 -4
|
@@ -41,7 +41,7 @@ async function ta(name, fn) {
|
|
|
41
41
|
|
|
42
42
|
console.log('service slice — updateInstanceDomain / getInstanceByDomain:');
|
|
43
43
|
|
|
44
|
-
await ta('updateInstanceDomain: parameterized UPDATE of
|
|
44
|
+
await ta('updateInstanceDomain: parameterized UPDATE of the domain + the release queue, returns the row', async () => {
|
|
45
45
|
const calls = [];
|
|
46
46
|
const db = { query: async (sql, params) => {
|
|
47
47
|
calls.push({ sql, params });
|
|
@@ -50,8 +50,34 @@ await ta('updateInstanceDomain: parameterized UPDATE of only the domain (+update
|
|
|
50
50
|
const row = await provisioning.updateInstanceDomain(db, 18, 'spike.cloudbongos.com');
|
|
51
51
|
assert.equal(row.domain, 'spike.cloudbongos.com');
|
|
52
52
|
assert.match(calls[0].sql, /UPDATE provisioning_instances/);
|
|
53
|
-
assert.match(calls[0].sql, /SET domain = \$2
|
|
54
|
-
assert.
|
|
53
|
+
assert.match(calls[0].sql, /SET domain = \$2/);
|
|
54
|
+
assert.match(calls[0].sql, /released_domains/, 'the release queue rides the SAME statement (task 1003237)');
|
|
55
|
+
assert.match(calls[0].sql, /last_verified_up = CASE WHEN domain IS DISTINCT FROM \$2 THEN NULL/,
|
|
56
|
+
'a CHANGED address has never been probed - the overlay must not describe the address it left (F16)');
|
|
57
|
+
assert.doesNotMatch(calls[0].sql, /status/, 'still never drifts the lifecycle');
|
|
58
|
+
assert.match(calls[0].sql, /array_length\(released_domains, 1\), 0\) >= \$4/,
|
|
59
|
+
'the queue is CAPPED in SQL, not trusted from the caller - the runner drains it against a shared token');
|
|
60
|
+
assert.deepEqual(calls[0].params, [18, 'spike.cloudbongos.com', null, provisioning.MAX_RELEASED_DOMAINS],
|
|
61
|
+
'no `releasing` means a null 3rd param, and the CASE leaves the queue exactly as it stands');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await ta('updateInstanceDomain: a re-point APPENDS the old address, lowercased, and never duplicates it', async () => {
|
|
65
|
+
const calls = [];
|
|
66
|
+
const db = { query: async (sql, params) => { calls.push({ sql, params }); return { rows: [{ id: 18 }] }; } };
|
|
67
|
+
await provisioning.updateInstanceDomain(db, 18, 'right.example.com', 'TYPO.example.com');
|
|
68
|
+
assert.deepEqual(calls[0].params, [18, 'right.example.com', 'typo.example.com', provisioning.MAX_RELEASED_DOMAINS]);
|
|
69
|
+
assert.match(calls[0].sql, /array_append\(released_domains, \$3\)/, 'appends — a second correction must not drop the first');
|
|
70
|
+
assert.match(calls[0].sql, /\$3 = ANY\(released_domains\)/, 'idempotent: a retry never queues the same address twice');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await ta('clearReleasedDomains: empties the queue and touches nothing else', async () => {
|
|
74
|
+
const calls = [];
|
|
75
|
+
const db = { query: async (sql, params) => { calls.push({ sql, params }); return { rows: [{ id: 18 }] }; } };
|
|
76
|
+
await provisioning.clearReleasedDomains(db, 18);
|
|
77
|
+
assert.match(calls[0].sql, /SET released_domains = '\{\}'/);
|
|
78
|
+
assert.doesNotMatch(calls[0].sql, /updated_at|status|domain = /,
|
|
79
|
+
'draining a queue is not an owner-visible change (the clearErrorNote rule)');
|
|
80
|
+
assert.deepEqual(calls[0].params, [18]);
|
|
55
81
|
});
|
|
56
82
|
|
|
57
83
|
await ta('getInstanceByDomain: case-insensitive exact match, parameterized', async () => {
|
|
@@ -80,8 +106,8 @@ let createResult = null; // what createInstance reports (re-request test
|
|
|
80
106
|
let manifestRows = [];
|
|
81
107
|
provisioning.getInstanceById = async (_db, id) => (instance ? { ...instance, id } : null);
|
|
82
108
|
provisioning.getInstanceByDomain = async (_db, d) => domainHolder;
|
|
83
|
-
provisioning.updateInstanceDomain = async (_db, id, domain) => {
|
|
84
|
-
updateCalls.push({ id, domain });
|
|
109
|
+
provisioning.updateInstanceDomain = async (_db, id, domain, releasing = null) => {
|
|
110
|
+
updateCalls.push({ id, domain, releasing });
|
|
85
111
|
instance = { ...instance, domain };
|
|
86
112
|
return { ...instance, id };
|
|
87
113
|
};
|
|
@@ -100,10 +126,24 @@ const api = require('../src/module-api.js');
|
|
|
100
126
|
api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
|
|
101
127
|
api.requirePermission = () => (_req, _res, next) => next();
|
|
102
128
|
api.branding = () => ({
|
|
103
|
-
|
|
129
|
+
// instanceBase is the platform APEX the route's reserved + in-apex-squat guards
|
|
130
|
+
// read (instanceApex()). Set here so the re-point suite below can exercise them
|
|
131
|
+
// against a real apex instead of the guard-free '' the stub used to imply.
|
|
132
|
+
domains: { publicOrigin: 'https://cb.test', instanceBase: 'cb.test' },
|
|
104
133
|
identity: { productName: 'Cloud Bongos' },
|
|
105
134
|
});
|
|
106
135
|
|
|
136
|
+
// The domain route is rate-limited PER BUILDER (task 1003237) and every test in
|
|
137
|
+
// this file posts as builder 7, so the real cap would throttle the suite rather
|
|
138
|
+
// than the behaviour each test is about. Raise it for the shared app below —
|
|
139
|
+
// `limiterMax = null` restores the route's own number, which the dedicated
|
|
140
|
+
// limiter test at the end of this file uses to exercise the real 429.
|
|
141
|
+
const rateLimit = require('../modules/provisioning/rate-limit.js');
|
|
142
|
+
const realMakeAskRateLimiter = rateLimit.makeAskRateLimiter;
|
|
143
|
+
let limiterMax = 10_000;
|
|
144
|
+
rateLimit.makeAskRateLimiter = (opts = {}) =>
|
|
145
|
+
realMakeAskRateLimiter({ ...opts, ...(limiterMax === null ? {} : { max: limiterMax }) });
|
|
146
|
+
|
|
107
147
|
const express = require('express');
|
|
108
148
|
const provisioningRoutes = require('../modules/provisioning/routes/provisioning.js');
|
|
109
149
|
const app = express();
|
|
@@ -121,10 +161,12 @@ const server = app.listen(0);
|
|
|
121
161
|
await new Promise((r) => server.once('listening', r));
|
|
122
162
|
const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
|
|
123
163
|
|
|
124
|
-
function post(path, body = {}) {
|
|
164
|
+
function post(path, body = {}) { return postTo(base, path, body); }
|
|
165
|
+
|
|
166
|
+
function postTo(origin, path, body = {}) {
|
|
125
167
|
return new Promise((resolve, reject) => {
|
|
126
168
|
const payload = JSON.stringify(body);
|
|
127
|
-
const req = http.request(`${
|
|
169
|
+
const req = http.request(`${origin}${path}`, {
|
|
128
170
|
method: 'POST',
|
|
129
171
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
130
172
|
}, (res) => {
|
|
@@ -137,9 +179,9 @@ function post(path, body = {}) {
|
|
|
137
179
|
});
|
|
138
180
|
}
|
|
139
181
|
|
|
140
|
-
function reset({ inst } = {}) {
|
|
182
|
+
function reset({ inst, holder } = {}) {
|
|
141
183
|
instance = inst || null;
|
|
142
|
-
domainHolder = null;
|
|
184
|
+
domainHolder = holder || null;
|
|
143
185
|
updateCalls = [];
|
|
144
186
|
enqueueCalls = [];
|
|
145
187
|
enqueueResult = null;
|
|
@@ -155,7 +197,7 @@ await ta('ACTIVE domainless instance: domain is validated, recorded, and a domai
|
|
|
155
197
|
assert.equal(res.status, 200, res.raw);
|
|
156
198
|
assert.equal(res.body.ok, true);
|
|
157
199
|
assert.equal(res.body.domain, 'spike.cloudbongos.com', 'normalized before storing');
|
|
158
|
-
assert.deepEqual(updateCalls, [{ id: 18, domain: 'spike.cloudbongos.com' }]);
|
|
200
|
+
assert.deepEqual(updateCalls, [{ id: 18, domain: 'spike.cloudbongos.com', releasing: null }]);
|
|
159
201
|
assert.deepEqual(enqueueCalls.map((c) => c.action), ['domain-attach']);
|
|
160
202
|
assert.equal(res.body.queued, true);
|
|
161
203
|
});
|
|
@@ -229,21 +271,91 @@ await ta('re-kick honors enqueueIntent created:false — a racing enqueue is nev
|
|
|
229
271
|
} finally { provisioning.enqueueIntent = savedEnqueue; }
|
|
230
272
|
});
|
|
231
273
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
274
|
+
// -- the re-point (task 1003237, owner decision 2026-08-28 / ADR 0200) --------
|
|
275
|
+
// A mistyped bring-your-own address used to be a permanent 409 domain_already_set.
|
|
276
|
+
// It is now correctable, under exactly the guards the FIRST attach applied. The
|
|
277
|
+
// apex here is cb.test (the harness's branding publicOrigin), so "inside the
|
|
278
|
+
// apex" means *.cb.test.
|
|
279
|
+
|
|
280
|
+
await ta('a DIFFERENT bring-your-own domain RE-POINTS — the address changes and the old one is queued for release', async () => {
|
|
281
|
+
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'typo.example.com' } });
|
|
282
|
+
const res = await post('/provisioning/instances/18/domain', { domain: 'right.example.com' });
|
|
283
|
+
assert.equal(res.status, 200, res.raw);
|
|
284
|
+
assert.equal(res.body.changed, true);
|
|
285
|
+
assert.equal(res.body.queued, true, 'an ACTIVE instance re-points via a domain-attach intent');
|
|
286
|
+
assert.equal(res.body.domain, 'right.example.com');
|
|
287
|
+
assert.equal(res.body.replaced, 'typo.example.com', 'the response NAMES what is being let go');
|
|
288
|
+
assert.deepEqual(updateCalls, [{ id: 18, domain: 'right.example.com', releasing: 'typo.example.com' }],
|
|
289
|
+
'the new address and the release ride ONE write');
|
|
290
|
+
assert.match(res.body.message, /right\.example\.com/);
|
|
291
|
+
assert.match(res.body.message, /typo\.example\.com/, 'the sentence says the old address is going');
|
|
292
|
+
assert.deepEqual(enqueueCalls.map((c) => c.action), ['domain-attach']);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
await ta('a re-point onto a PLATFORM-RESERVED name is refused — the first attach guard, unweakened', async () => {
|
|
296
|
+
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'typo.example.com' } });
|
|
297
|
+
const res = await post('/provisioning/instances/18/domain', { domain: 'cb.test' });
|
|
298
|
+
assert.equal(res.status, 400, res.raw);
|
|
299
|
+
assert.equal(res.body.error.code, 'domain_reserved');
|
|
300
|
+
assert.equal(updateCalls.length, 0, 'nothing was written - the old address still stands');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
await ta('a re-point INSIDE the platform apex is refused as a squat (task 1002704) — except this slug own default', async () => {
|
|
304
|
+
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'typo.example.com' } });
|
|
305
|
+
const squat = await post('/provisioning/instances/18/domain', { domain: 'someone-else.cb.test' });
|
|
306
|
+
assert.equal(squat.status, 400, squat.raw);
|
|
307
|
+
assert.equal(squat.body.error.code, 'domain_reserved');
|
|
308
|
+
assert.equal(updateCalls.length, 0);
|
|
309
|
+
|
|
310
|
+
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'typo.example.com' } });
|
|
311
|
+
const own = await post('/provisioning/instances/18/domain', { domain: 'spike.cb.test' });
|
|
312
|
+
assert.equal(own.status, 200, own.raw);
|
|
313
|
+
assert.deepEqual(updateCalls, [{ id: 18, domain: 'spike.cb.test', releasing: 'typo.example.com' }],
|
|
314
|
+
'falling back to the assigned default still releases the BYO address it left');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
await ta('a re-point onto an address ANOTHER project holds is a truthful 409 domain_taken', async () => {
|
|
318
|
+
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'typo.example.com' },
|
|
319
|
+
holder: { id: 99, slug: 'other' } });
|
|
320
|
+
const res = await post('/provisioning/instances/18/domain', { domain: 'taken.example.com' });
|
|
235
321
|
assert.equal(res.status, 409, res.raw);
|
|
236
|
-
assert.equal(res.body.error.code, '
|
|
237
|
-
assert.equal(res.body.error.details.current, 'old.cloudbongos.com', 'names the current address');
|
|
322
|
+
assert.equal(res.body.error.code, 'domain_taken');
|
|
238
323
|
assert.equal(updateCalls.length, 0);
|
|
239
324
|
});
|
|
240
325
|
|
|
326
|
+
await ta('moving OFF the platform-assigned <slug>.<apex> queues NO release — that name is the platform own (ADR 0181)', async () => {
|
|
327
|
+
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'spike.cb.test' } });
|
|
328
|
+
const res = await post('/provisioning/instances/18/domain', { domain: 'mine.example.com' });
|
|
329
|
+
assert.equal(res.status, 200, res.raw);
|
|
330
|
+
assert.equal(res.body.replaced, null, 'the assigned default is never released - the revive path re-uses it');
|
|
331
|
+
assert.deepEqual(updateCalls, [{ id: 18, domain: 'mine.example.com', releasing: null }]);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
await ta('a re-point on a REQUESTED instance records without an intent, and releases NOTHING — it never stood up', async () => {
|
|
335
|
+
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'requested', domain: 'typo.example.com' } });
|
|
336
|
+
const res = await post('/provisioning/instances/18/domain', { domain: 'right.example.com' });
|
|
337
|
+
assert.equal(res.status, 200, res.raw);
|
|
338
|
+
assert.equal(res.body.queued, false);
|
|
339
|
+
assert.equal(res.body.replaced, null,
|
|
340
|
+
'nothing ever wrote an A record for the address it is leaving - parking that name would CREATE a junk record, not release one');
|
|
341
|
+
assert.deepEqual(updateCalls, [{ id: 18, domain: 'right.example.com', releasing: null }]);
|
|
342
|
+
assert.deepEqual(enqueueCalls, [], 'nothing to re-assert on an instance that has not stood up');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
await ta("an ERROR instance DOES release — a failed standup is frequently a PARTIAL one that already wrote the record (ADR 0181)", async () => {
|
|
346
|
+
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'error', domain: 'typo.example.com' } });
|
|
347
|
+
const res = await post('/provisioning/instances/18/domain', { domain: 'right.example.com' });
|
|
348
|
+
assert.equal(res.status, 200, res.raw);
|
|
349
|
+
assert.equal(res.body.replaced, 'typo.example.com');
|
|
350
|
+
assert.deepEqual(updateCalls, [{ id: 18, domain: 'right.example.com', releasing: 'typo.example.com' }]);
|
|
351
|
+
});
|
|
352
|
+
|
|
241
353
|
await ta('REQUESTED instance: domain records with NO intent — the provision run wires it', async () => {
|
|
242
354
|
reset({ inst: { owner_builder_id: 7, slug: 'fresh', status: 'requested', domain: null } });
|
|
243
355
|
const res = await post('/provisioning/instances/18/domain', { domain: 'fresh.cloudbongos.com' });
|
|
244
356
|
assert.equal(res.status, 200, res.raw);
|
|
245
357
|
assert.equal(res.body.queued, false);
|
|
246
|
-
assert.deepEqual(updateCalls, [{ id: 18, domain: 'fresh.cloudbongos.com' }]);
|
|
358
|
+
assert.deepEqual(updateCalls, [{ id: 18, domain: 'fresh.cloudbongos.com', releasing: null }]);
|
|
247
359
|
assert.equal(enqueueCalls.length, 0, 'provision will wire it — no separate intent');
|
|
248
360
|
});
|
|
249
361
|
|
|
@@ -310,6 +422,43 @@ await ta('a fresh create with a domain is untouched (the domain is stored at cre
|
|
|
310
422
|
assert.equal(res.body.domain_ignored, undefined);
|
|
311
423
|
});
|
|
312
424
|
|
|
425
|
+
console.log('\nPOST /provisioning/instances/:id/domain — the write is throttled per builder:');
|
|
426
|
+
|
|
427
|
+
await ta('the re-point write is rate-limited on the BUILDER, so one owner cannot spend the platform-wide DNS budget', async () => {
|
|
428
|
+
// A re-point QUEUES an address the runner later releases with a call against the
|
|
429
|
+
// SHARED control-plane Cloudflare token, so an unthrottled owner spends a
|
|
430
|
+
// platform resource, not merely their own. Keyed on the builder, not the IP:
|
|
431
|
+
// per-IP starves co-located builders while an attacker rotates IPs.
|
|
432
|
+
limiterMax = null; // the route's own number, whatever it is
|
|
433
|
+
const limited = express();
|
|
434
|
+
limited.use(express.json());
|
|
435
|
+
limited.use((_req, res, next) => {
|
|
436
|
+
res.fail = (code, opts, details) => {
|
|
437
|
+
const status = typeof opts === 'number' ? opts : (opts && opts.status) || 400;
|
|
438
|
+
res.status(status).json({ error: { code, ...(typeof opts === 'object' ? opts : {}), ...(details || {}) } });
|
|
439
|
+
};
|
|
440
|
+
next();
|
|
441
|
+
});
|
|
442
|
+
limited.use('/api/bongos', provisioningRoutes());
|
|
443
|
+
const srv = limited.listen(0);
|
|
444
|
+
await new Promise((r) => srv.once('listening', r));
|
|
445
|
+
const at = `http://127.0.0.1:${srv.address().port}/api/bongos`;
|
|
446
|
+
try {
|
|
447
|
+
reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'typo.example.com' } });
|
|
448
|
+
let sawLimit = false;
|
|
449
|
+
// well past any sane cap for a human correcting one typo
|
|
450
|
+
for (let i = 0; i < 60 && !sawLimit; i++) {
|
|
451
|
+
const res = await postTo(at, `/provisioning/instances/18/domain?i=${i}`, { domain: `h${i}.example.com` });
|
|
452
|
+
if (res.status === 429) {
|
|
453
|
+
sawLimit = true;
|
|
454
|
+
assert.equal(res.body.error.code || res.body.error, 'rate_limited');
|
|
455
|
+
assert.ok(i > 0, 'a first call is never throttled');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
assert.ok(sawLimit, 'the route must shed a flood — an unthrottled owner can queue unbounded DNS work');
|
|
459
|
+
} finally { srv.close(); limiterMax = 10_000; }
|
|
460
|
+
});
|
|
461
|
+
|
|
313
462
|
console.log('\nPOST /provisioning/instances/:id/github-app — accepts the newly-addressed instance:');
|
|
314
463
|
|
|
315
464
|
await ta('domainless instance still 409s domain_required; once addressed, the manifest mints', async () => {
|
|
@@ -95,11 +95,15 @@ test('every truthful refusal the API can give is mapped to human copy, never swa
|
|
|
95
95
|
'not_teardownable', // teardown refused for the real state
|
|
96
96
|
'bad_domain', // malformed hostname
|
|
97
97
|
'domain_taken', // another instance already answers there
|
|
98
|
-
'
|
|
98
|
+
'domain_reserved', // the platform's own name, or a squat inside its apex
|
|
99
99
|
'domain_not_attachable', // mid-flight/torn-down states refuse truthfully
|
|
100
100
|
]) {
|
|
101
101
|
assert.match(body, new RegExp(code), `the page must handle '${code}' explicitly`);
|
|
102
102
|
}
|
|
103
|
+
// retired with the re-point (task 1003237): the server no longer sends it, so a
|
|
104
|
+
// branch for it would be copy the founder can never reach
|
|
105
|
+
assert.doesNotMatch(body, /domain_already_set/,
|
|
106
|
+
'the refusal the re-point removed must not linger as dead copy');
|
|
103
107
|
});
|
|
104
108
|
|
|
105
109
|
test('a domainless instance gets an honest no-address state', async () => {
|
package/tests/ui_design_kit.mjs
CHANGED
|
@@ -85,10 +85,11 @@ test('auditFloors names each floor a render breaks, and says nothing about a cle
|
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
test('expectProblems compares every expectation the vocabulary allows', () => {
|
|
88
|
-
const facts = { ...clean(), expanded: ['projTrig'], dialog: true, sel: { '#a': true, '#b': false }, activeId: 'x', activeText: 'Sky', param: { view: 'mine' }, attr: ['dark'], text: ['Table'], href: ['/docs'], count: [3], checked: false, value: ['demo'], hasClass: [true] };
|
|
89
|
-
assert.deepEqual(lib.expectProblems(facts, { expanded: true, dialog: true, visible: ['#a'], hidden: ['#b'], open: '#a', closed: '#b', activeId: 'x', activeText: 'Sky', expandedIds: ['projTrig'], param: { view: 'mine' }, attr: [['html', 'data-mode', 'dark']], text: [['h1', 'Table']], href: [['a', '/docs']], count: [['li', 3]], value: [['#q', 'demo']], hasClass: [['#w', 'is-open']] }), []);
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
const facts = { ...clean(), expanded: ['projTrig'], dialog: true, sel: { '#a': true, '#b': false }, activeId: 'x', activeText: 'Sky', param: { view: 'mine' }, attr: ['dark'], text: ['Table'], href: ['/docs'], count: [3], checked: false, value: ['demo'], hasClass: [true], top: [64] };
|
|
89
|
+
assert.deepEqual(lib.expectProblems(facts, { expanded: true, dialog: true, visible: ['#a'], hidden: ['#b'], open: '#a', closed: '#b', activeId: 'x', activeText: 'Sky', expandedIds: ['projTrig'], param: { view: 'mine' }, attr: [['html', 'data-mode', 'dark']], text: [['h1', 'Table']], href: [['a', '/docs']], count: [['li', 3]], value: [['#q', 'demo']], hasClass: [['#w', 'is-open']], top: [['main', 64]] }), []);
|
|
90
|
+
// `top` is how a probe proves an overlay did not push the page (task 1003332)
|
|
91
|
+
const p = lib.expectProblems(facts, { expanded: false, dialog: false, visible: ['#b'], hidden: ['#a'], activeText: 'Table', param: { view: null }, count: [['li', 5]], checked: '#c', hasClass: [['#w', 'is-open', false]], noOverflow: true, top: [['main', 185]] });
|
|
92
|
+
for (const needle of ['expected no expanded trigger', 'expected no visible dialog', 'expected visible: #b', 'expected hidden: #a', 'active text "Sky", expected "Table"', '?view=mine, expected null', '3 × li, expected 5', '#c is not checked', 'still has class "is-open"', 'main top 64px, expected 185px']) {
|
|
92
93
|
assert.ok(p.some((x) => x.includes(needle)), `missing: ${needle} in ${p.join(' | ')}`);
|
|
93
94
|
}
|
|
94
95
|
});
|