@bongos/core 1.21.74 → 1.21.76
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 +78 -48
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0343-a-module-score-is-a-security-gate-then-an-average-of-visible-parts.md +12 -0
- package/docs/api/openapi.json +81 -3
- package/docs/api-reference.md +5 -3
- package/docs/copy-inventory.md +45 -42
- package/docs/copy-registry.json +76 -49
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-inventory.json +16 -8
- package/docs/page-readings.json +1542 -1522
- package/modules/hall-ui/public/craft-mode.js +3 -1
- package/modules/hall-ui/public/page-notes.css +150 -0
- package/modules/hall-ui/public/page-notes.js +30 -5
- package/modules/hall-ui/public/studio.css +15 -0
- package/modules/hall-ui/public/studio.html +6 -0
- package/modules/hall-ui/public/studio.js +38 -4
- package/modules/hall-ui/public/style.css +1 -116
- package/modules/page-notes/db.js +19 -0
- package/modules/page-notes/migrations/page_notes_002_surface_index.sql +12 -0
- package/modules/page-notes/notes.js +25 -2
- package/modules/page-notes/routes/page-notes.js +9 -0
- package/modules/public-landing/public/account.html +2 -0
- package/modules/public-landing/public/assets/artist-review.js +107 -0
- package/modules/public-landing/public/contact.html +2 -0
- package/modules/public-landing/public/guild.html +2 -0
- package/modules/public-landing/public/index.html +2 -0
- package/modules/public-landing/public/invitations.html +2 -0
- package/modules/public-landing/public/privacy.html +2 -0
- package/modules/public-landing/public/projects.html +2 -0
- package/modules/public-landing/public/terms.html +2 -0
- package/modules/ui-design/kit/serve.js +32 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/module-listing.js +85 -0
- package/src/bongos/routes/modules.js +15 -0
- package/src/module-api.js +1 -1
- package/tests/artist_review.mjs +231 -0
- package/tests/hall_studio_home.mjs +39 -1
- package/tests/landing_legal_pages.mjs +2 -1
- package/tests/module_store_listing.mjs +156 -0
- package/tests/page_notes_db.mjs +28 -1
- package/tests/page_notes_panel.mjs +9 -3
|
@@ -418,12 +418,50 @@ try {
|
|
|
418
418
|
assert.equal(none.el('st-older').hidden, true, 'no review waiting: no door');
|
|
419
419
|
});
|
|
420
420
|
|
|
421
|
+
await test('From the live site (task 1004499): the public pages\' reviews, each opening its doc here', async () => {
|
|
422
|
+
const N = require('../modules/page-notes/notes.js');
|
|
423
|
+
// the route's own mapper over two listForSurface rows
|
|
424
|
+
const rows = [
|
|
425
|
+
{ id: 31, page_id: 'site:projects', page_path: '/projects', page_title: 'Projects', status: 'filed', submitted_at: day(1), idea_ids: [1, 2], author_name: 'Ana', excerpt: 'The map labels <b>overlap</b>.\n' },
|
|
426
|
+
{ id: 32, page_id: 'site:home', page_path: '/', page_title: '', status: 'splitting', submitted_at: day(0), idea_ids: [], author_name: 'Bo', excerpt: 'Hero is too dark.' },
|
|
427
|
+
].map(N.siteListItem);
|
|
428
|
+
const s = bootStudio({ answers: { ...HOME, 'GET /page-notes/site': ok({ notes: rows }) } });
|
|
429
|
+
await s.settle();
|
|
430
|
+
assert.equal(s.el('st-site-detail').textContent, '2 reviews from the live site');
|
|
431
|
+
await s.click('st-site');
|
|
432
|
+
assert.equal(s.el('st-panel-title').textContent, 'From the live site');
|
|
433
|
+
const body = s.el('st-panel-body').innerHTML;
|
|
434
|
+
assert.deepEqual([...body.matchAll(/class="st-site" href="([^"]*)"/g)].map((m) => m[1]), ['/builders/studio?note=31', '/builders/studio?note=32'], 'each row opens its doc in the Studio');
|
|
435
|
+
assert.match(body, /st-site__page">Projects</);
|
|
436
|
+
assert.match(body, /st-site__page">\/</, 'an untitled page falls back to its path');
|
|
437
|
+
assert.match(body, /Ana · [^<]* · 2 ideas/);
|
|
438
|
+
assert.match(body, /Bo · [^<]* · splitting/);
|
|
439
|
+
assert.match(body, /The map labels <b>overlap<\/b>\. \[image\]</, 'the excerpt is escaped and its picture named, not shown');
|
|
440
|
+
const long = bootStudio({ answers: { ...HOME, 'GET /page-notes/site': ok({ notes: [{ ...rows[1], excerpt: 'word '.repeat(60) }] }) } });
|
|
441
|
+
await long.settle();
|
|
442
|
+
await long.click('st-site');
|
|
443
|
+
const cut = /st-site__text">([^<]*)</.exec(long.el('st-panel-body').innerHTML)[1];
|
|
444
|
+
assert.ok(cut.length <= 140 && /word…$/.test(cut), `a long excerpt ends on a whole word and says it goes on: ${cut}`);
|
|
445
|
+
await s.clickIn('st-panel', '[data-close-panel]');
|
|
446
|
+
|
|
447
|
+
const none = bootStudio({ answers: { ...HOME, 'GET /page-notes/site': ok({ notes: [] }) } });
|
|
448
|
+
await none.settle();
|
|
449
|
+
assert.equal(none.el('st-site-detail').textContent, 'no reviews from the live site yet');
|
|
450
|
+
await none.click('st-site');
|
|
451
|
+
assert.match(none.el('st-panel-body').innerHTML, /None yet\./);
|
|
452
|
+
|
|
453
|
+
const down = bootStudio({ answers: HOME });
|
|
454
|
+
await down.settle();
|
|
455
|
+
await down.click('st-site');
|
|
456
|
+
assert.match(down.el('st-panel-body').innerHTML, /could not be read just now/);
|
|
457
|
+
});
|
|
458
|
+
|
|
421
459
|
await test('the markup: one h1 (the greeting), the tally and four doors, the libs before the page, the full-screen switch', () => {
|
|
422
460
|
const nc = HTML.replace(/<!--[\s\S]*?-->/g, '');
|
|
423
461
|
assert.equal((nc.match(/<h1\b/g) || []).length, 1);
|
|
424
462
|
assert.match(nc, /<h1[^>]*id="studio-greet"[^>]*data-words/);
|
|
425
463
|
assert.match(nc, /<p[^>]*id="studio-sub"[^>]*data-words/);
|
|
426
|
-
for (const id of ['st-tally', 'st-resume', 'st-flow', 'studio-approvals', 'st-status', 'st-older']) assert.match(nc, new RegExp(`id="${id}"`));
|
|
464
|
+
for (const id of ['st-tally', 'st-resume', 'st-flow', 'studio-approvals', 'st-status', 'st-site', 'st-older']) assert.match(nc, new RegExp(`id="${id}"`));
|
|
427
465
|
assert.match(nc, /<body[^>]*data-fullscreen-switch/);
|
|
428
466
|
const at = (f) => nc.indexOf(f);
|
|
429
467
|
assert.ok(at('tweak-editor-lib.js') < at('studio-lib.js') && at('studio-lib.js') < at('/studio.js'), 'the libs, then the page');
|
|
@@ -206,7 +206,8 @@ for (const p of PAGES) {
|
|
|
206
206
|
const html = read(p.file);
|
|
207
207
|
const styles = [...html.matchAll(/<style>([\s\S]*?)<\/style>/g)].map((m) => m[1]).join('\n');
|
|
208
208
|
assert.doesNotMatch(styles, /\.(shell|bar|barMark|barLeft|barRight|trig|who|signinDoor|menuBand|band[A-Z]\w*)\b[^{}]*\{/, 'no bar or band rule in the page');
|
|
209
|
-
|
|
209
|
+
// then the Artist review button's loader (task 1004499), the page's last tag
|
|
210
|
+
assert.match(html, /<\/footer>\n<script src="\/assets\/hub-bar\.js"><\/script>\n<!-- [^\n]*-->\n<script src="\/assets\/artist-review\.js" data-hall="\{\{buildersOrigin\}\}"><\/script>\n<\/body>/, 'one shared script, after the markup it drives');
|
|
210
211
|
assert.doesNotMatch(html, /<script>/, 'and no inline copy of the bands');
|
|
211
212
|
assert.match(html, /<a data-link-hall href="\{\{buildersOrigin\}\}">Builders' hall<\/a><\/nav><\/footer>/, 'the hall keeps a door in the footer row now the bar has no Hall pill, as on /account');
|
|
212
213
|
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// tests/module_store_listing.mjs — the store catalog under the quality floor (task
|
|
2
|
+
// 1003807, criterion wa5-curated-listing, ADR 0343 D2/D5). Two halves, no port and
|
|
3
|
+
// no real DB:
|
|
4
|
+
// - the rule: placeVersion, pure — the Security gate excludes, "New" is listed
|
|
5
|
+
// unranked, a real score must reach the floor;
|
|
6
|
+
// - the route: the real GET /store/modules handler off the real router, with
|
|
7
|
+
// pool.js replaced in the require cache (the module_upgrade_updates.mjs harness).
|
|
8
|
+
//
|
|
9
|
+
// Run: node tests/module_store_listing.mjs
|
|
10
|
+
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
// ---- the fake registry the route reads ----
|
|
18
|
+
const state = { modules: {}, versions: [], scores: [] };
|
|
19
|
+
const pool = {
|
|
20
|
+
async query(sql, params = []) {
|
|
21
|
+
if (/FROM store_modules m JOIN store_module_versions v/.test(sql)) {
|
|
22
|
+
return {
|
|
23
|
+
rows: state.versions
|
|
24
|
+
.filter((v) => state.modules[v.module_key] && state.modules[v.module_key].status === 'listed')
|
|
25
|
+
.map((v) => ({ ...v, version_id: v.id, title: state.modules[v.module_key].title })),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (/FROM module_assessment_scores/.test(sql)) {
|
|
29
|
+
const ids = params[0].map(String);
|
|
30
|
+
const newest = new Map();
|
|
31
|
+
for (const s of state.scores) {
|
|
32
|
+
if (!ids.includes(String(s.version_id))) continue;
|
|
33
|
+
const have = newest.get(s.version_id);
|
|
34
|
+
if (!have || s.computed_at > have.computed_at || (s.computed_at === have.computed_at && s.id > have.id)) newest.set(s.version_id, s);
|
|
35
|
+
}
|
|
36
|
+
return { rows: [...newest.values()] };
|
|
37
|
+
}
|
|
38
|
+
return { rows: [] };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const poolPath = require.resolve('../src/bongos/pool.js');
|
|
42
|
+
require.cache[poolPath] = { id: poolPath, filename: poolPath, loaded: true, exports: { pool: { connect: async () => ({ ...pool, release() {} }), query: pool.query } } };
|
|
43
|
+
|
|
44
|
+
const buildModulesRouter = require('../src/bongos/routes/modules.js');
|
|
45
|
+
const { QUALITY_FLOOR, placeVersion } = require('../src/bongos/module-listing.js');
|
|
46
|
+
|
|
47
|
+
function handlerFor(router, method, p) {
|
|
48
|
+
const layer = router.stack.find((l) => l.route && l.route.path === p && l.route.methods[method]);
|
|
49
|
+
assert.ok(layer, `no ${method.toUpperCase()} ${p} on the router`);
|
|
50
|
+
const stack = layer.route.stack;
|
|
51
|
+
return { handle: stack[stack.length - 1].handle, names: stack.map((s) => s.name) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function invoke(handle, req) {
|
|
55
|
+
const res = {
|
|
56
|
+
statusCode: 200, body: undefined, headers: {},
|
|
57
|
+
json(b) { this.body = b; return this; },
|
|
58
|
+
status(c) { this.statusCode = c; return this; },
|
|
59
|
+
set(k, v) { this.headers[k] = v; return this; },
|
|
60
|
+
fail(code, opts) { this.statusCode = opts.status; this.body = { error: code, ...opts }; return this; },
|
|
61
|
+
};
|
|
62
|
+
let nextErr = null;
|
|
63
|
+
await handle(req, res, (err) => { nextErr = err; });
|
|
64
|
+
if (nextErr) throw nextErr;
|
|
65
|
+
return res;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const score = (extra = {}) => ({ overall: 70, security_passed: true, is_new: false, ...extra });
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// the rule (pure)
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
test('the floor is 50 out of 100 (owner decision, 2026-10-01)', () => {
|
|
75
|
+
assert.equal(QUALITY_FLOOR, 50);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('placeVersion: the Security gate must have PASSED — false or not run yet excludes', () => {
|
|
79
|
+
assert.equal(placeVersion(score({ security_passed: false })).placement, 'excluded');
|
|
80
|
+
assert.equal(placeVersion(score({ security_passed: null })).placement, 'excluded');
|
|
81
|
+
assert.equal(placeVersion(score({ security_passed: false, is_new: true })).placement, 'excluded');
|
|
82
|
+
assert.equal(placeVersion(score({ security_passed: false })).reason, 'security_not_passed');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('placeVersion: no score row at all is excluded, not failed silently', () => {
|
|
86
|
+
assert.deepEqual(placeVersion(null), { placement: 'excluded', reason: 'not_assessed' });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('placeVersion: a New version that passed the gate is listed as new, whatever its number', () => {
|
|
90
|
+
assert.equal(placeVersion(score({ is_new: true, overall: 10 })).placement, 'new');
|
|
91
|
+
assert.equal(placeVersion(score({ is_new: true, overall: null })).placement, 'new');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('placeVersion: a scored version must reach the floor — at the floor is in, one below is out', () => {
|
|
95
|
+
assert.equal(placeVersion(score({ overall: 50 })).placement, 'listed');
|
|
96
|
+
assert.equal(placeVersion(score({ overall: 100 })).placement, 'listed');
|
|
97
|
+
assert.deepEqual(placeVersion(score({ overall: 49 })), { placement: 'excluded', reason: 'below_floor' });
|
|
98
|
+
assert.equal(placeVersion(score({ overall: null })).placement, 'excluded');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// the route
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
test('GET /store/modules: any signed-in builder, no extra permission', () => {
|
|
106
|
+
const { names } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
|
|
107
|
+
assert.equal(names.length, 2, `expected requireBuilder + handler, got ${names.join(', ')}`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('GET /store/modules: lists by the newest version\'s current score, drops what falls short', async () => {
|
|
111
|
+
state.modules = {
|
|
112
|
+
good: { status: 'listed', title: 'Good' },
|
|
113
|
+
fresh: { status: 'listed', title: 'Fresh' },
|
|
114
|
+
low: { status: 'listed', title: 'Low' },
|
|
115
|
+
unsafe: { status: 'listed', title: 'Unsafe' },
|
|
116
|
+
unscored: { status: 'listed', title: 'Unscored' },
|
|
117
|
+
gone: { status: 'delisted', title: 'Gone' },
|
|
118
|
+
fixed: { status: 'listed', title: 'Fixed' },
|
|
119
|
+
};
|
|
120
|
+
state.versions = [
|
|
121
|
+
{ id: 1, module_key: 'good', version: '1.9.0', published_at: 't1' },
|
|
122
|
+
{ id: 2, module_key: 'good', version: '1.10.0', published_at: 't2' },
|
|
123
|
+
{ id: 3, module_key: 'fresh', version: '0.1.0', published_at: 't3' },
|
|
124
|
+
{ id: 4, module_key: 'low', version: '1.0.0', published_at: 't4' },
|
|
125
|
+
{ id: 5, module_key: 'unsafe', version: '1.0.0', published_at: 't5' },
|
|
126
|
+
{ id: 6, module_key: 'unscored', version: '1.0.0', published_at: 't6' },
|
|
127
|
+
{ id: 7, module_key: 'gone', version: '1.0.0', published_at: 't7' },
|
|
128
|
+
{ id: 8, module_key: 'fixed', version: '2.0.0', published_at: 't8' },
|
|
129
|
+
];
|
|
130
|
+
state.scores = [
|
|
131
|
+
{ id: 1, version_id: 1, computed_at: 1, ...score({ overall: 20 }) }, // an OLDER version scored low
|
|
132
|
+
{ id: 2, version_id: 2, computed_at: 1, ...score({ overall: 80 }) },
|
|
133
|
+
{ id: 3, version_id: 3, computed_at: 1, ...score({ is_new: true, overall: 30 }) },
|
|
134
|
+
{ id: 4, version_id: 4, computed_at: 1, ...score({ overall: 49 }) },
|
|
135
|
+
{ id: 5, version_id: 5, computed_at: 1, ...score({ overall: 95, security_passed: false }) },
|
|
136
|
+
{ id: 6, version_id: 7, computed_at: 1, ...score({ overall: 99 }) },
|
|
137
|
+
{ id: 7, version_id: 8, computed_at: 1, ...score({ overall: 40 }) }, // superseded by the newer row
|
|
138
|
+
{ id: 8, version_id: 8, computed_at: 2, ...score({ overall: 60 }) },
|
|
139
|
+
];
|
|
140
|
+
const { handle } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
|
|
141
|
+
const res = await invoke(handle, {});
|
|
142
|
+
assert.equal(res.statusCode, 200);
|
|
143
|
+
assert.equal(res.headers['Cache-Control'], 'no-store');
|
|
144
|
+
assert.equal(res.body.floor, 50);
|
|
145
|
+
assert.deepEqual(res.body.listed.map((m) => [m.key, m.version, m.overall]), [['fixed', '2.0.0', 60], ['good', '1.10.0', 80]]);
|
|
146
|
+
assert.deepEqual(res.body.new.map((m) => [m.key, m.is_new, m.overall]), [['fresh', true, null]]);
|
|
147
|
+
const all = [...res.body.listed, ...res.body.new].map((m) => m.key);
|
|
148
|
+
for (const k of ['low', 'unsafe', 'unscored', 'gone']) assert.ok(!all.includes(k), `${k} must not be listed`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('GET /store/modules: an empty store answers empty lists and the floor', async () => {
|
|
152
|
+
state.modules = {}; state.versions = []; state.scores = [];
|
|
153
|
+
const { handle } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
|
|
154
|
+
const res = await invoke(handle, {});
|
|
155
|
+
assert.deepEqual(res.body, { floor: 50, listed: [], new: [] });
|
|
156
|
+
});
|
package/tests/page_notes_db.mjs
CHANGED
|
@@ -54,8 +54,11 @@ await pool.query('INSERT INTO builders (id, github_login, display_name) VALUES (
|
|
|
54
54
|
|
|
55
55
|
const MIGRATION = fs.readFileSync(path.join(ROOT, 'modules/page-notes/migrations/page_notes_001_notes.sql'), 'utf8');
|
|
56
56
|
await pool.query(MIGRATION);
|
|
57
|
+
// task 1004499: the Studio's site list reads by page_id prefix
|
|
58
|
+
const MIGRATION_2 = fs.readFileSync(path.join(ROOT, 'modules/page-notes/migrations/page_notes_002_surface_index.sql'), 'utf8');
|
|
59
|
+
await pool.query(MIGRATION_2);
|
|
57
60
|
const PAGE = 'builders:pn-test-page';
|
|
58
|
-
const clean = () => pool.query('DELETE FROM page_notes_docs WHERE page_id LIKE $1', ['builders:pn-test-%']);
|
|
61
|
+
const clean = () => pool.query('DELETE FROM page_notes_docs WHERE page_id LIKE $1 OR page_id LIKE $2', ['builders:pn-test-%', 'pntest:%']);
|
|
59
62
|
await clean();
|
|
60
63
|
|
|
61
64
|
const PNG = Buffer.from('89504e470d0a1a0a0000000d4948445200000001000000010806000000', 'hex');
|
|
@@ -135,6 +138,30 @@ await test('deleting a doc takes its images with it', async () => {
|
|
|
135
138
|
|
|
136
139
|
await clean();
|
|
137
140
|
await pool.query('DELETE FROM builders WHERE id = ANY($1::bigint[])', [[A, B]]).catch(() => {});
|
|
141
|
+
await test('THE SITE LIST (task 1004499): one surface, submitted only, newest first, read through its own index', async () => {
|
|
142
|
+
await pool.query(MIGRATION_2);
|
|
143
|
+
const mk = async (author, page, at) => {
|
|
144
|
+
const n = await db.openDraft({ authorId: author, pageId: page, pagePath: '/', pageTitle: page }, q);
|
|
145
|
+
await pool.query("UPDATE page_notes_docs SET status = 'filed', submitted_at = $2, body_md = 'words' WHERE id = $1", [n.id, at]);
|
|
146
|
+
return String(n.id);
|
|
147
|
+
};
|
|
148
|
+
const older = await mk(A, 'pntest:home', '2026-10-01T00:00:00Z');
|
|
149
|
+
const newer = await mk(B, 'pntest:projects', '2026-10-02T00:00:00Z');
|
|
150
|
+
await db.openDraft({ authorId: A, pageId: 'pntest:terms', pagePath: '/terms', pageTitle: 'Terms' }, q);
|
|
151
|
+
await mk(A, 'builders:pn-test-other', '2026-10-03T00:00:00Z');
|
|
152
|
+
const rows = await db.listForSurface('pntest', { limit: 20 }, q);
|
|
153
|
+
assert.deepEqual(rows.map((r) => String(r.id)), [newer, older], 'the surface only, drafts left out, newest first');
|
|
154
|
+
const c = await pool.connect();
|
|
155
|
+
try {
|
|
156
|
+
await c.query('BEGIN');
|
|
157
|
+
await c.query('SET LOCAL enable_seqscan = off');
|
|
158
|
+
const plan = (await c.query("EXPLAIN SELECT id FROM page_notes_docs n WHERE n.page_id LIKE 'site:%' AND n.status <> 'draft'")).rows.map((r) => r['QUERY PLAN']).join('\n');
|
|
159
|
+
assert.match(plan, /page_notes_by_surface/, plan);
|
|
160
|
+
await c.query('ROLLBACK');
|
|
161
|
+
} finally { c.release(); }
|
|
162
|
+
await clean();
|
|
163
|
+
});
|
|
164
|
+
|
|
138
165
|
await pool.end();
|
|
139
166
|
console.log(`page_notes_db: ${passed} passed, ${failed} failed`);
|
|
140
167
|
process.exit(failed ? 1 : 0);
|
|
@@ -79,7 +79,7 @@ test('THE PALETTE: in the top bar in Artistry alone, right before the switch, an
|
|
|
79
79
|
assert.deepEqual(p.tools().map((t) => t.id), ['craft-tool-palette', 'craft-tool-paintbrush'], 'the palette, then the paintbrush (task 1004497)');
|
|
80
80
|
assert.equal(p.tools()[0].nextSibling, p.tools()[1], 'the palette comes first');
|
|
81
81
|
assert.equal(p.tools()[1].nextSibling, p.sw, 'and the pair sits right before the mode switch');
|
|
82
|
-
assert.equal(p.tools()[0].getAttribute('aria-label'), '
|
|
82
|
+
assert.equal(p.tools()[0].getAttribute('aria-label'), 'Artist review: write feedback on this page', 'one name with the public site\'s button (task 1004499)');
|
|
83
83
|
for (const m of ['building', 'governing']) {
|
|
84
84
|
p.craft.choose(m);
|
|
85
85
|
assert.equal(p.tools().length, 0, `no palette in ${m}`);
|
|
@@ -141,8 +141,14 @@ test('the doc names its page the way the page inventory does', () => {
|
|
|
141
141
|
assert.deepEqual(JSON.parse(JSON.stringify(R.page())), { id: 'builders:studio', path: '/studio', title: 'Studio' });
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
-
test('colours live in
|
|
144
|
+
test('colours live in the sheets, and the panel and tool are styled there', () => {
|
|
145
145
|
assert.doesNotMatch(NOTES, /#[0-9a-f]{3,6}\b|rgba?\(/i, 'no colour literal in the panel script');
|
|
146
146
|
const css = fs.readFileSync(path.join(HALL, 'style.css'), 'utf8');
|
|
147
|
-
|
|
147
|
+
assert.ok(css.includes('.craft-tool {'), '.craft-tool {');
|
|
148
|
+
// Task 1004499: the panel has its own sheet so the public site can wear it;
|
|
149
|
+
// the hall pulls it in through style.css, ahead of every rule.
|
|
150
|
+
const pn = fs.readFileSync(path.join(HALL, 'page-notes.css'), 'utf8');
|
|
151
|
+
for (const sel of ['.pn {', '.pn__panel {', '.pn__text {', '.pn-doc__img {']) assert.ok(pn.includes(sel) && !css.includes(sel), sel);
|
|
152
|
+
const at = css.indexOf('@import url("page-notes.css");');
|
|
153
|
+
assert.ok(at !== -1 && at < css.indexOf('{'), 'style.css imports the sheet before any rule');
|
|
148
154
|
});
|