@bongos/core 1.20.59 → 1.20.60
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 +136 -86
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +10 -0
- package/clients/bongos-client/index.cjs +10 -0
- package/clients/bongos-client/index.d.ts +13 -0
- package/clients/bongos-client/index.mjs +10 -0
- package/docs/adr/0358-a-hall-wears-a-look-and-its-night-takes-a-tint.md +52 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +308 -3
- package/docs/api-reference.md +7 -2
- package/docs/copy-inventory.md +338 -329
- package/docs/copy-registry.json +515 -430
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +757 -706
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +5 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +8 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/provisioning/look.js +169 -0
- package/modules/provisioning/migrations/provisioning_034_project_logo.sql +35 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/provisioning.js +4 -3
- package/modules/provisioning/routes/look.js +204 -0
- package/modules/provisioning/routes/provisioning.js +5 -59
- package/modules/provisioning/settings-push.js +76 -0
- package/modules/public-landing/public/assets/cosmos.css +1 -1
- package/modules/public-landing/public/projects.html +499 -28
- package/modules/public-landing/public/projects.probes.json +2 -2
- package/modules/public-landing/public/projects.states.json +6 -3
- package/modules/ui-design/kit/serve.js +50 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/gds/fitness.js +4 -0
- package/src/bongos/routes.js +4 -1
- package/src/bongos/serve-internal.js +6 -0
- package/src/branding.js +26 -1
- package/src/hall-look.js +99 -0
- package/src/module-api.js +1 -1
- package/src/night-tint.js +26 -0
- package/tests/currency_label.mjs +7 -4
- package/tests/hall_tokens.mjs +10 -1
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/look_night_tint.mjs +169 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/projects_hub.mjs +5 -4
- package/tests/projects_hub_app_status.mjs +1 -1
- package/tests/projects_hub_app_step.mjs +8 -7
- package/tests/projects_hub_look.mjs +255 -0
- package/tests/projects_hub_module_picker.mjs +2 -2
- package/tests/provision_settings_apply.mjs +12 -4
- package/tests/provisioning_look.mjs +376 -0
- package/tests/provisioning_settings.mjs +10 -7
- package/tests/provisioning_settings_apply.mjs +4 -4
- package/tests/provisioning_settings_env.mjs +4 -0
- package/tests/wizard_draft_resume.mjs +2 -2
- package/tests/wizard_front_door.mjs +2 -2
- package/tests/wizard_intent_resume.mjs +16 -15
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// tests/look_night_tint.mjs — the look a hall wears, its night tint and its logo, hall
|
|
2
|
+
// side (task 1004421, BV2.PS06; ADR 0358).
|
|
3
|
+
//
|
|
4
|
+
// The load-bearing assertions:
|
|
5
|
+
// • THE BLEND RULE: a hall that never chose a look is exactly today's hall — the
|
|
6
|
+
// same theme, the same markup, pure black at night — whatever its tint setting;
|
|
7
|
+
// • a pushed look name becomes that look's fifteen on the instance, read from the
|
|
8
|
+
// style library the core ships; an unknown name or a malformed accent leaves the
|
|
9
|
+
// hall on its own pack rather than half-applying;
|
|
10
|
+
// • the night tint is emitted only with a look, as both dark twins, after the sheet;
|
|
11
|
+
// • CONTRAST: the hall's own token suite (tests/hall_tokens.mjs) passes under every
|
|
12
|
+
// library look with the tint on, and under custom accents round the hue wheel
|
|
13
|
+
// after the platform's nudge — light, dark and tinted dark;
|
|
14
|
+
// • a logo address reaches the hall mark and the favicon only when it is a plain
|
|
15
|
+
// https URL.
|
|
16
|
+
//
|
|
17
|
+
// Run: node --test tests/look_night_tint.mjs
|
|
18
|
+
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import os from 'node:os';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
|
+
const { resolveBranding, clientBranding } = require('../src/branding.js');
|
|
31
|
+
const { themeMarkup } = require('../src/bongos/serve-internal.js');
|
|
32
|
+
const { NIGHT_TINT, nightTintCss } = require('../src/night-tint.js');
|
|
33
|
+
const { lookAdjustAccent, LIBRARY_LOOKS } = require('../modules/provisioning/look.js');
|
|
34
|
+
|
|
35
|
+
const neutral = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'branding.neutral.json'), 'utf8'));
|
|
36
|
+
const brand = (env = {}) => clientBranding(resolveBranding({ neutral, env }));
|
|
37
|
+
const packOf = (name) => JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'styles', name, 'pack.json'), 'utf8')).theme.ui;
|
|
38
|
+
|
|
39
|
+
test('the blend rule: no look means today\'s hall, whatever the tint setting says', () => {
|
|
40
|
+
const today = brand();
|
|
41
|
+
for (const env of [{ BONGOS_LOOK: 'own' }, { BONGOS_NIGHT_TINT: 'on' }, { BONGOS_LOOK: 'own', BONGOS_NIGHT_TINT: 'on' }]) {
|
|
42
|
+
const b = brand(env);
|
|
43
|
+
assert.deepEqual(b.theme, today.theme, JSON.stringify(env));
|
|
44
|
+
assert.equal(themeMarkup(b.theme), themeMarkup(today.theme));
|
|
45
|
+
}
|
|
46
|
+
assert.equal(today.theme.nightTint, false);
|
|
47
|
+
assert.equal(today.theme.look, '');
|
|
48
|
+
assert.ok(!themeMarkup(today.theme).includes('data-theme'), 'no dark block is injected for a hall without a look');
|
|
49
|
+
assert.deepEqual(today.theme.colors, neutral.theme.ui.colors);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('a library look becomes its fifteen, and the manifest reads back what was pushed', () => {
|
|
53
|
+
for (const name of LIBRARY_LOOKS) {
|
|
54
|
+
const b = brand({ BONGOS_LOOK: name });
|
|
55
|
+
const ui = packOf(name);
|
|
56
|
+
assert.deepEqual(b.theme.colors, ui.colors, name);
|
|
57
|
+
assert.deepEqual(b.theme.fonts, { display: ui.fonts.display, body: ui.fonts.body, links: ui.fonts.links }, name);
|
|
58
|
+
assert.equal(b.theme.look, name);
|
|
59
|
+
assert.equal(b.project.look, name, 'the settings read-back (settingsConsumed) sees the pushed name');
|
|
60
|
+
assert.equal(b.theme.nightTint, true, 'the tint is on by default with a look');
|
|
61
|
+
assert.match(b.theme.favicon, new RegExp(encodeURIComponent(ui.colors.accent)), 'the tab icon is drawn in the look\'s accent');
|
|
62
|
+
}
|
|
63
|
+
assert.equal(brand({ BONGOS_LOOK: 'grove', BONGOS_NIGHT_TINT: 'off' }).theme.nightTint, false, 'the founder can switch back to pure black');
|
|
64
|
+
assert.equal(brand({ BONGOS_LOOK: 'grove', BONGOS_NIGHT_TINT: 'off' }).project.nightTint, 'off');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('a custom look is the chrome world with one accent; an unusable accent or an unknown look leaves the hall alone', () => {
|
|
68
|
+
const b = brand({ BONGOS_LOOK: 'custom', BONGOS_LOOK_ACCENT: '#B48CF2' });
|
|
69
|
+
assert.deepEqual(b.theme.colors, { ...packOf('chrome-world').colors, accent: '#b48cf2' });
|
|
70
|
+
assert.equal(b.theme.look, 'custom');
|
|
71
|
+
const today = brand();
|
|
72
|
+
for (const env of [{ BONGOS_LOOK: 'custom' }, { BONGOS_LOOK: 'custom', BONGOS_LOOK_ACCENT: 'red;}body{x' }, { BONGOS_LOOK: 'neon' }, { BONGOS_LOOK: '../../etc' }]) {
|
|
73
|
+
assert.deepEqual(brand(env).theme, today.theme, JSON.stringify(env));
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('the night tint is emitted with a look, as both dark twins, and nowhere else', () => {
|
|
78
|
+
const on = themeMarkup(brand({ BONGOS_LOOK: 'expedition' }).theme);
|
|
79
|
+
assert.ok(on.includes(nightTintCss()), 'the tint block is in the markup');
|
|
80
|
+
assert.ok(on.indexOf(':root{') < on.indexOf(nightTintCss()), 'after the palette, so the dark grounds win on order');
|
|
81
|
+
assert.equal(nightTintCss().split('--bg:').length - 1, 2, 'written twice: the [data-theme] switch and the media twin');
|
|
82
|
+
assert.match(nightTintCss(), /html\[data-theme="dark"\]\{/);
|
|
83
|
+
assert.match(nightTintCss(), /@media \(prefers-color-scheme: dark\)\{html:not\(\[data-theme\]\)\{/);
|
|
84
|
+
for (const v of Object.values(NIGHT_TINT)) assert.match(v, /^color-mix\(in srgb, var\(--accent\) \d+%, #[0-9a-f]{6}\)$/, 'derived from the look\'s own accent, never a literal colour');
|
|
85
|
+
assert.ok(!themeMarkup(brand({ BONGOS_LOOK: 'expedition', BONGOS_NIGHT_TINT: 'off' }).theme).includes('data-theme'));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('a logo reaches the hall only as a plain https address, and becomes its favicon', () => {
|
|
89
|
+
const url = 'https://platform.test/api/bongos/provisioning/logos/' + 'a'.repeat(64) + '.png';
|
|
90
|
+
const b = brand({ BONGOS_LOGO_URL: url });
|
|
91
|
+
assert.equal(b.theme.logo, url);
|
|
92
|
+
assert.equal(b.theme.favicon, url);
|
|
93
|
+
assert.equal(b.theme.nightTint, false, 'a logo alone tints nothing');
|
|
94
|
+
assert.deepEqual(b.theme.colors, neutral.theme.ui.colors, 'a logo alone changes no colour');
|
|
95
|
+
for (const bad of ['http://x.test/a.png', 'javascript:alert(1)', 'https://x.test/a.png"><script>', 'https://x.test/a b.png', 'https://x.test/a.png)']) {
|
|
96
|
+
assert.equal(brand({ BONGOS_LOGO_URL: bad }).theme.logo, '', bad);
|
|
97
|
+
}
|
|
98
|
+
const shell = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'shell.js'), 'utf8');
|
|
99
|
+
assert.match(shell, /brand\.theme && brand\.theme\.logo\s*\?\s*'<img class="brand-mark__logo" src="' \+ esc\(brand\.theme\.logo\)/, 'the hall mark shows the logo, escaped, in the dot\'s place');
|
|
100
|
+
assert.match(fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'style.css'), 'utf8'), /\.brand-mark__logo \{/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── contrast: the hall's own suite, under each look, with the tint on ──────────────
|
|
104
|
+
const childEnv = (extra) => {
|
|
105
|
+
const env = { ...process.env, ...extra };
|
|
106
|
+
delete env.NODE_TEST_CONTEXT; // a nested node --test must run its own tests, not report to ours
|
|
107
|
+
return env;
|
|
108
|
+
};
|
|
109
|
+
function hallSuite(extra) {
|
|
110
|
+
const r = spawnSync(process.execPath, ['--test', '--test-reporter=tap', 'tests/hall_tokens.mjs'], {
|
|
111
|
+
cwd: ROOT, encoding: 'utf8', env: childEnv({ UI_DESIGN_TINT: '1', ...extra }), timeout: 120000,
|
|
112
|
+
});
|
|
113
|
+
const fails = (r.stdout || '').split('\n').filter((l) => /= \d+\.\d+:1/.test(l)).map((l) => l.trim());
|
|
114
|
+
return { status: r.status, fails };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
test('the hall clears AA under every library look with the night tint on', () => {
|
|
118
|
+
for (const name of LIBRARY_LOOKS) {
|
|
119
|
+
const r = hallSuite({ UI_DESIGN_PACK: path.join(ROOT, 'modules', 'ui-design', 'styles', name, 'pack.json') });
|
|
120
|
+
assert.equal(r.status, 0, `${name}, tinted night:\n${r.fails.join('\n')}`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('the hall clears AA under custom accents round the hue wheel, once the platform has nudged them', () => {
|
|
125
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'look-tint-'));
|
|
126
|
+
const chrome = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'styles', 'chrome-world', 'pack.json'), 'utf8'));
|
|
127
|
+
const hsl = (h, s, l) => {
|
|
128
|
+
const a = s * Math.min(l, 1 - l), k = (n) => (n + h / 30) % 12;
|
|
129
|
+
const f = (n) => Math.round(255 * (l - a * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1))));
|
|
130
|
+
return '#' + [f(0), f(8), f(4)].map((x) => x.toString(16).padStart(2, '0')).join('');
|
|
131
|
+
};
|
|
132
|
+
// Saturated hues at three lightnesses, plus the greys — the corners of the space.
|
|
133
|
+
const raw = [];
|
|
134
|
+
for (let h = 0; h < 360; h += 45) for (const l of [0.25, 0.5, 0.8]) raw.push(hsl(h, 1, l));
|
|
135
|
+
raw.push('#000000', '#808080', '#ffffff');
|
|
136
|
+
const failures = [];
|
|
137
|
+
try {
|
|
138
|
+
for (const hex of raw) {
|
|
139
|
+
const kept = lookAdjustAccent(hex).hex;
|
|
140
|
+
const pack = structuredClone(chrome);
|
|
141
|
+
pack.theme.ui.colors.accent = kept;
|
|
142
|
+
const file = path.join(tmp, `${hex.slice(1)}.json`);
|
|
143
|
+
fs.writeFileSync(file, JSON.stringify(pack));
|
|
144
|
+
for (const tint of ['0', '1']) {
|
|
145
|
+
const r = hallSuite({ UI_DESIGN_PACK: file, UI_DESIGN_TINT: tint });
|
|
146
|
+
if (r.status !== 0) failures.push(`${hex} → ${kept} (${tint === '1' ? 'tinted' : 'black'} night): ${r.fails.join('; ') || 'suite failed'}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
|
|
150
|
+
assert.deepEqual(failures, []);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('UI_DESIGN_TINT is honoured, not decorative: a tint that is too strong fails the hall suite', () => {
|
|
154
|
+
// Proves the knob reaches the resolver: the same suite, with the card tinted far past
|
|
155
|
+
// the shipped share, must go red on the pair the 8% draft already failed.
|
|
156
|
+
const src = fs.readFileSync(path.join(ROOT, 'tests', 'hall_tokens.mjs'), 'utf8');
|
|
157
|
+
assert.match(src, /process\.env\.UI_DESIGN_TINT === '1'/);
|
|
158
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'look-tint-strong-'));
|
|
159
|
+
try {
|
|
160
|
+
const strong = path.join(tmp, 'night-tint.js');
|
|
161
|
+
fs.writeFileSync(strong, fs.readFileSync(path.join(ROOT, 'src', 'night-tint.js'), 'utf8').replace(/var\(--accent\) 5%, #101012/, 'var(--accent) 30%, #101012'));
|
|
162
|
+
const probe = path.join(tmp, 'hall_tokens.mjs');
|
|
163
|
+
fs.writeFileSync(probe, src.replace("'../src/night-tint.js'", JSON.stringify(strong)).replace(
|
|
164
|
+
"path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')", JSON.stringify(ROOT)));
|
|
165
|
+
const r = spawnSync(process.execPath, ['--test', '--test-reporter=tap', probe], { cwd: ROOT, encoding: 'utf8', env: childEnv({ UI_DESIGN_TINT: '1' }), timeout: 120000 });
|
|
166
|
+
assert.notEqual(r.status, 0, 'a 30% card tint must fail the hall suite');
|
|
167
|
+
assert.match(r.stdout, /dark --[\w-]+ on --[\w-]+ = \d/);
|
|
168
|
+
} finally { fs.rmSync(tmp, { recursive: true, force: true }); }
|
|
169
|
+
});
|
|
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
87
87
|
// the file and left the query alone, so a returning browser kept the cached copy —
|
|
88
88
|
// a fix that reaches nobody looks identical to a fix that does not work.
|
|
89
89
|
|
|
90
|
-
const SHELL_SHA = '
|
|
90
|
+
const SHELL_SHA = '51a13ea563542113';
|
|
91
91
|
|
|
92
92
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
93
93
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -715,8 +715,9 @@ test('a renumbering of the wizard invalidates old drafts — the key carries the
|
|
|
715
715
|
const panels = (SCRIPT.match(/var panels = \{([^}]*)\}/) || [, ''])[1];
|
|
716
716
|
const count = [...panels.matchAll(/\d+:\s*'panel-/g)].length;
|
|
717
717
|
// task 1004417 put the front door's two screens (terms, fork) in front: the sixth renumbering
|
|
718
|
-
|
|
719
|
-
assert.equal(
|
|
718
|
+
// task 1004421 put the Look step between App and Review: the seventh
|
|
719
|
+
assert.equal(count, 13, 'gate + the front door\'s two + nine numbered steps + done — bump DRAFT_KEY when this changes');
|
|
720
|
+
assert.equal(key, 'cb-create-draft-v7', 'and the key moved with the Look step');
|
|
720
721
|
});
|
|
721
722
|
|
|
722
723
|
test('every row lays the same five parts — the ledger has no ragged row', () => {
|
|
@@ -2137,7 +2138,7 @@ test('the wizard takes the world by scope: no card ground, the six-step strip as
|
|
|
2137
2138
|
assert.match(block, /#view-new \.card\{background:none;border-radius:0;box-shadow:none;/, 'no card ground');
|
|
2138
2139
|
// eight tracks since task 1004351 added the App step (seven since 1002339); the count is the
|
|
2139
2140
|
// rail's own, so it moves with the panels map rather than being a magic number
|
|
2140
|
-
assert.match(block, /#view-new \.steps\{display:grid;grid-template-columns:repeat\(
|
|
2141
|
+
assert.match(block, /#view-new \.steps\{display:grid;grid-template-columns:repeat\(9,1fr\);/, 'the strip lays a track per numbered step');
|
|
2141
2142
|
assert.match(block, /#view-new \.steps li::before\{content:"";position:absolute;top:-4px;left:0;right:auto;width:7px;height:7px;/, 'the connector becomes the 7px dot');
|
|
2142
2143
|
assert.match(block, /#view-new \.steps li\.now::before\{background:var\(--accent\);border-color:var\(--accent\);outline:4px solid/, 'the lit step carries the accent and an outline halo');
|
|
2143
2144
|
assert.match(block, /#view-new \.panel h1\{font-weight:300;[^}]*text-transform:lowercase;/, 'the step title in the display voice, lowercase');
|
|
@@ -2184,7 +2185,7 @@ test('the wizard\'s own structures wear the world: hairline fields, cells of one
|
|
|
2184
2185
|
assert.match(block, /\.addrBig code\{display:block;font-family:var\(--font-display\);/, 'the address stands in the display voice');
|
|
2185
2186
|
assert.doesNotMatch(block, /linear-gradient/, 'no tinted plates');
|
|
2186
2187
|
assert.doesNotMatch(block, /box-shadow:(?!none)/, 'no box shadow in the wizard block');
|
|
2187
|
-
assert.match(HUB, /<div class="nav">\s*<button type="button" class="btn ghost" data-back="
|
|
2188
|
+
assert.match(HUB, /<div class="nav">\s*<button type="button" class="btn ghost" data-back="10">← Back<\/button>\s*<button type="button" class="btn big" id="doCreate">Create my project<\/button>/, 'the review\'s doors sit on one row, back then the primary');
|
|
2188
2189
|
assert.match(HUB, /<h2 class="stepsHead" id="doneStepsHead">Your steps — the parts only you can play<\/h2>/, 'the steps heading is the label face by class, not an inline style (the id is where a skipped invite step hands focus on, task 1002346)');
|
|
2189
2190
|
// what is enqueued is unchanged (ADR 0111): the filing body the control plane reads
|
|
2190
2191
|
assert.match(SCRIPT, /body: JSON\.stringify\(\{\s*slug: state\.slug,/, 'the filing still carries the slug first');
|
|
@@ -240,7 +240,7 @@ test('a repaint never lands on top of an open Retry — from this watch or a fre
|
|
|
240
240
|
});
|
|
241
241
|
|
|
242
242
|
test('both surfaces use it: the done panel and the project page', () => {
|
|
243
|
-
assert.match(HUB, /if \(box\) watchApp\(instanceId, box, function \(\) \{ return state\.step ===
|
|
243
|
+
assert.match(HUB, /if \(box\) watchApp\(instanceId, box, function \(\) \{ return state\.step === 12; \}, 5000\);/);
|
|
244
244
|
assert.match(HUB, /<div id="doneApp" hidden><\/div>/);
|
|
245
245
|
assert.match(HUB, /<div class="card mSec" id="mAppCard" hidden>\n\s*<h2>Where its app runs<\/h2>\n\s*<div id="mApp"><\/div>/);
|
|
246
246
|
assert.match(HUB, /loadAppStatus\(inst\.id \|\| asked, inst\);/);
|
|
@@ -142,16 +142,17 @@ test('the panel is real markup: Render and Decide later, Decide later the defaul
|
|
|
142
142
|
assert.doesNotMatch(PANEL, /Coming soon/, 'and is no longer marked soon');
|
|
143
143
|
assert.match(PANEL, /<input type="radio" name="appPlatform" value="later" checked>/, 'Decide later is the default');
|
|
144
144
|
assert.match(PANEL, /We host your Builders' hall/, 'the step says the hall is ours to host (ADR 0345 D1), in the page\'s one spelling of it');
|
|
145
|
-
assert.match(PANEL, /<form data-next="10">/, 'Next leads to the review');
|
|
145
|
+
assert.match(PANEL, /<form data-next="10">/, 'Next leads to the Look step (task 1004421), then the review');
|
|
146
146
|
assert.match(PANEL, /data-back="8"/, 'Back returns to the address step');
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
test('the step sits between Address and
|
|
150
|
-
assert.match(HUB, /8: 'panel-address', 9: 'panel-app', 10: 'panel-
|
|
149
|
+
test('the step sits between Address and the Look step in the panels map and on the rail', () => {
|
|
150
|
+
assert.match(HUB, /8: 'panel-address', 9: 'panel-app', 10: 'panel-look', 11: 'panel-review', 12: 'panel-done'/);
|
|
151
151
|
assert.match(HUB, /<li data-step="9"><span class="snum">7<\/span><span class="slbl">App<\/span><\/li>/);
|
|
152
|
-
assert.match(HUB, /<li data-step="10"><span class="snum">8<\/span><span class="slbl">
|
|
153
|
-
assert.match(HUB,
|
|
154
|
-
assert.match(HUB, /
|
|
152
|
+
assert.match(HUB, /<li data-step="10"><span class="snum">8<\/span><span class="slbl">Look<\/span><\/li>/);
|
|
153
|
+
assert.match(HUB, /<li data-step="11"><span class="snum">9<\/span><span class="slbl">Create<\/span><\/li>/);
|
|
154
|
+
assert.match(HUB, /data-back="10">← Back<\/button>\n\s*<button type="button" class="btn big" id="doCreate">/, "the review's Back returns to the Look step");
|
|
155
|
+
assert.match(HUB, /if \(step === 9\) \{ syncAppStep\(\); syncRenderSetup\(\); \}\n\s*if \(step === 10\) renderLook\(\);\n\s*if \(step === 11\) renderReview\(\);/, 'entering the step repaints it, Render setup included');
|
|
155
156
|
assert.match(HUB, /if \(step === 9\) return validateRender\(\);/, 'Next on the step runs the Render check');
|
|
156
157
|
});
|
|
157
158
|
|
|
@@ -455,5 +456,5 @@ test('the review names exactly what was chosen, prices included', () => {
|
|
|
455
456
|
});
|
|
456
457
|
|
|
457
458
|
test('Create hands the app to Render straight after the project is filed', () => {
|
|
458
|
-
assert.match(HUB, /show\(
|
|
459
|
+
assert.match(HUB, /show\(12, true\);\n\s*sendRenderSetup\(data\.instance && data\.instance\.id\);/);
|
|
459
460
|
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// tests/projects_hub_look.mjs — the Look step in the create wizard and "Its look" on the
|
|
2
|
+
// manage page (task 1004421, BV2.PS06; ADR 0358; spec D2 + D8).
|
|
3
|
+
//
|
|
4
|
+
// The load-bearing assertions:
|
|
5
|
+
// • the previews paint the REAL looks: every colour lookKit carries equals its
|
|
6
|
+
// pack.json, and the night shares equal src/night-tint.js — a preview that drifted
|
|
7
|
+
// from the hall would promise a look the hall does not wear;
|
|
8
|
+
// • the step sits between App and Review (look 10, review 11, done 12, draft v7), on
|
|
9
|
+
// the rail as a beat of its own, and every kit walk past App passes it;
|
|
10
|
+
// • EXECUTED by the vm-slice idiom: the picker offers the four looks and "make your
|
|
11
|
+
// own", shows the accent only for your own, shows the ADJUSTED colour in the
|
|
12
|
+
// preview and says so, paints a black night when the tint is off, refuses an SVG
|
|
13
|
+
// or oversized logo before reading it, and sends the look then the logo;
|
|
14
|
+
// • Create sends the look right after the project is filed, and the review names it;
|
|
15
|
+
// • the manage page reads the look once per project, never on the minute tick.
|
|
16
|
+
//
|
|
17
|
+
// Run: node --test tests/projects_hub_look.mjs
|
|
18
|
+
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import vm from 'node:vm';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
29
|
+
const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
|
|
30
|
+
const HUB = fs.readFileSync(path.join(LANDING, 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
|
|
31
|
+
const { NIGHT_TINT } = require('../src/night-tint.js');
|
|
32
|
+
const { LIBRARY_LOOKS, lookAdjustAccent } = require('../modules/provisioning/look.js');
|
|
33
|
+
|
|
34
|
+
// ── the lookKit slice, run in a vm under a small DOM stub ───────────────────────
|
|
35
|
+
const KIT_START = ' var lookKit = (function () {';
|
|
36
|
+
const KIT_END = '\n })();\n';
|
|
37
|
+
const kitStart = HUB.indexOf(KIT_START);
|
|
38
|
+
const kitEnd = HUB.indexOf(KIT_END, kitStart);
|
|
39
|
+
const KIT_SRC = HUB.slice(kitStart, kitEnd + KIT_END.length);
|
|
40
|
+
|
|
41
|
+
function makeDom() {
|
|
42
|
+
const all = [];
|
|
43
|
+
function node(tag) {
|
|
44
|
+
const n = {
|
|
45
|
+
tagName: String(tag).toUpperCase(), children: [], parentNode: null, style: { props: {}, setProperty(k, v) { this.props[k] = v; } },
|
|
46
|
+
attrs: {}, listeners: {}, hidden: false, value: '', checked: false, textContent: '', className: '', id: '', type: '', name: '', src: '', files: null,
|
|
47
|
+
get firstChild() { return this.children[0] || null; },
|
|
48
|
+
classList: null,
|
|
49
|
+
appendChild(c) { c.parentNode = this; this.children.push(c); return c; },
|
|
50
|
+
removeChild(c) { this.children = this.children.filter((x) => x !== c); c.parentNode = null; return c; },
|
|
51
|
+
setAttribute(k, v) { this.attrs[k] = String(v); },
|
|
52
|
+
addEventListener(ev, fn) { (this.listeners[ev] ||= []).push(fn); },
|
|
53
|
+
fire(ev) { (this.listeners[ev] || []).forEach((fn) => fn.call(this, { target: this })); },
|
|
54
|
+
querySelector(sel) { return find(this, (x) => x.className.split(' ').includes(sel.replace(/^\./, ''))); },
|
|
55
|
+
};
|
|
56
|
+
n.classList = {
|
|
57
|
+
add(c) { const s = new Set(n.className.split(' ').filter(Boolean)); s.add(c); n.className = [...s].join(' '); },
|
|
58
|
+
remove(c) { n.className = n.className.split(' ').filter((x) => x && x !== c).join(' '); },
|
|
59
|
+
toggle(c, on) { if (on) this.add(c); else this.remove(c); },
|
|
60
|
+
contains(c) { return n.className.split(' ').includes(c); },
|
|
61
|
+
};
|
|
62
|
+
all.push(n);
|
|
63
|
+
return n;
|
|
64
|
+
}
|
|
65
|
+
function find(root, pred) {
|
|
66
|
+
for (const c of root.children) { if (pred(c)) return c; const d = find(c, pred); if (d) return d; }
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const document = { createElement: node };
|
|
70
|
+
const byId = (root, id) => find(root, (x) => x.id === id);
|
|
71
|
+
return { document, node, byId, find };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function bootKit({ fetchImpl, reader } = {}) {
|
|
75
|
+
const dom = makeDom();
|
|
76
|
+
const sandbox = {
|
|
77
|
+
document: dom.document, API: '/api/bongos', JSON, Math, String, Number, Object, Array, Promise, parseInt,
|
|
78
|
+
fetch: fetchImpl || (() => Promise.reject(new Error('no fetch'))),
|
|
79
|
+
FileReader: reader || function FileReader() { this.readAsDataURL = (f) => { this.result = 'data:' + f.type + ';base64,QUJD'; this.onload(); }; },
|
|
80
|
+
};
|
|
81
|
+
vm.createContext(sandbox);
|
|
82
|
+
vm.runInContext(KIT_SRC.replace('var lookKit =', 'this.lookKit ='), sandbox);
|
|
83
|
+
return { kit: sandbox.lookKit, dom };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test('the slice is findable: a rename must fail here, not silently test nothing', () => {
|
|
87
|
+
assert.ok(kitStart > -1, 'var lookKit = (function () { is in the page');
|
|
88
|
+
assert.ok(kitEnd > kitStart, 'and it closes');
|
|
89
|
+
assert.match(KIT_SRC, /function buildLookPicker\(host, model, onChange, idp, projectName\)/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('every colour the previews paint is its pack\'s, and the night shares are the hall\'s', () => {
|
|
93
|
+
const { kit } = bootKit();
|
|
94
|
+
assert.deepEqual(Object.keys(kit.LOOKS).sort(), [...LIBRARY_LOOKS].sort(), 'one preview per library look');
|
|
95
|
+
for (const name of LIBRARY_LOOKS) {
|
|
96
|
+
const pack = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'styles', name, 'pack.json'), 'utf8')).theme.ui.colors;
|
|
97
|
+
assert.equal(JSON.stringify(kit.LOOKS[name].colors), JSON.stringify(pack), `${name}: the client copy equals pack.json, key for key`);
|
|
98
|
+
}
|
|
99
|
+
const share = (k) => Number(/var\(--accent\) (\d+)%/.exec(NIGHT_TINT[k])[1]);
|
|
100
|
+
assert.equal(kit.NIGHT.bgShare, share('--bg'));
|
|
101
|
+
assert.equal(kit.NIGHT.cardShare, share('--bg-card'));
|
|
102
|
+
assert.match(NIGHT_TINT['--bg'], /#000000\)$/);
|
|
103
|
+
assert.match(NIGHT_TINT['--bg-card'], /#101012\)$/);
|
|
104
|
+
assert.equal(kit.NIGHT.bg, '#000000');
|
|
105
|
+
assert.equal(kit.NIGHT.card, '#101012');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('the step sits between App and Review: look 10, review 11, done 12, draft v7, a beat on the rail', () => {
|
|
109
|
+
assert.match(HUB, /9: 'panel-app', 10: 'panel-look', 11: 'panel-review', 12: 'panel-done' \};/);
|
|
110
|
+
assert.match(HUB, /var DRAFT_KEY = 'cb-create-draft-v7';/);
|
|
111
|
+
assert.match(HUB, /<li data-step="10"><span class="snum">8<\/span><span class="slbl">Look<\/span><\/li>\n\s*<li data-step="11"><span class="snum">9<\/span><span class="slbl">Create<\/span><\/li>/);
|
|
112
|
+
const panel = (HUB.match(/<section class="panel" id="panel-look"[\s\S]*?<\/section>/) || [''])[0];
|
|
113
|
+
assert.match(panel, /<h1 id="look-h">Give it a look<\/h1>/);
|
|
114
|
+
assert.match(panel, /<form data-next="11">/);
|
|
115
|
+
assert.match(panel, /data-back="9"/);
|
|
116
|
+
assert.match(panel, /<div class="lookPick" id="wizLook"><\/div>/);
|
|
117
|
+
assert.ok(HUB.indexOf('id="panel-app"') < HUB.indexOf('id="panel-look"') && HUB.indexOf('id="panel-look"') < HUB.indexOf('id="panel-review"'), 'in document order too');
|
|
118
|
+
assert.match(HUB, /if \(step === 10\) renderLook\(\);\n\s*if \(step === 11\) renderReview\(\);/);
|
|
119
|
+
assert.match(fs.readFileSync(path.join(LANDING, 'assets', 'cosmos.css'), 'utf8'), /#view-new \.steps\{display:grid;grid-template-columns:repeat\(9,1fr\);/, 'a track per numbered step');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('every kit walk past the App step passes the Look step, and the kit has a state on it', () => {
|
|
123
|
+
const states = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.states.json'), 'utf8')).states;
|
|
124
|
+
const probes = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.probes.json'), 'utf8')).probes;
|
|
125
|
+
const walks = [...Object.entries(states).map(([k, v]) => [k, v.actions]), ...probes.map((p) => [p.name, p.steps])];
|
|
126
|
+
let past = 0;
|
|
127
|
+
for (const [name, steps] of walks) {
|
|
128
|
+
const at = (steps || []).findIndex((a) => a[1] === '#panel-app button[type=submit]');
|
|
129
|
+
if (at < 0 || /^in-new-look/.test(name)) continue; // the Look states stop ON the step
|
|
130
|
+
past++;
|
|
131
|
+
const look = steps.findIndex((a) => a[1] === '#panel-look button[type=submit]');
|
|
132
|
+
assert.ok(look > at, `${name}: submits the Look step after the App step`);
|
|
133
|
+
}
|
|
134
|
+
assert.ok(past >= 5, `sanity: the walks that reach the review were found (${past})`);
|
|
135
|
+
assert.deepEqual(states['in-new-look'].expect.visible.slice(0, 2), ['#panel-look', '#wizLook']);
|
|
136
|
+
assert.ok(states['in-new-look-custom'].actions.some((a) => a[1] === '#wizLook-custom'), 'a state pictures "make your own"');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('executed: the picker offers the four looks and your own, and the accent shows only for your own', () => {
|
|
140
|
+
const { kit, dom } = bootKit();
|
|
141
|
+
const host = dom.node('div');
|
|
142
|
+
const model = { look: 'chrome-world', nightTint: true, accent: '#b48cf2', logo: null };
|
|
143
|
+
let changes = 0;
|
|
144
|
+
kit.buildLookPicker(host, model, () => { changes++; }, 'wiz', () => 'Kestrel');
|
|
145
|
+
const radios = ['chrome-world', 'expedition', 'grove', 'blueprint', 'custom'].map((k) => dom.byId(host, 'wizLook-' + k));
|
|
146
|
+
assert.ok(radios.every(Boolean), 'five radios, one per choice');
|
|
147
|
+
assert.equal(radios[0].checked, true, 'the current look is ticked');
|
|
148
|
+
const accentWrap = dom.byId(host, 'wizAccent').parentNode.parentNode;
|
|
149
|
+
assert.equal(accentWrap.hidden, true, 'no accent for a library look');
|
|
150
|
+
radios[4].checked = true; radios[4].fire('change');
|
|
151
|
+
assert.equal(model.look, 'custom');
|
|
152
|
+
assert.equal(accentWrap.hidden, false, 'the accent appears for your own');
|
|
153
|
+
assert.equal(changes, 1, 'onChange ran');
|
|
154
|
+
const name = dom.find(host, (x) => x.className === 'mhName');
|
|
155
|
+
assert.equal(name.textContent, 'Kestrel', 'the preview names the project');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('executed: an unreadable accent is shown ADJUSTED in the preview, with a sentence saying so', () => {
|
|
159
|
+
const { kit, dom } = bootKit();
|
|
160
|
+
const host = dom.node('div');
|
|
161
|
+
const model = { look: 'custom', nightTint: true, accent: '#112233', logo: null };
|
|
162
|
+
kit.buildLookPicker(host, model, null, 'wiz');
|
|
163
|
+
const kept = lookAdjustAccent('#112233').hex;
|
|
164
|
+
const note = dom.byId(host, 'wizAccentNote');
|
|
165
|
+
assert.match(note.textContent, new RegExp(`Adjusted to ${kept} so text stays readable in both modes`));
|
|
166
|
+
const light = dom.find(host, (x) => x.className === 'mh light');
|
|
167
|
+
assert.equal(light.style.props['--mh-accent'], kept, 'the preview paints the colour the server will keep');
|
|
168
|
+
model.accent = '#b48cf2';
|
|
169
|
+
const picker = kit.buildLookPicker(host, model, null, 'wiz');
|
|
170
|
+
picker.repaint();
|
|
171
|
+
assert.equal(dom.byId(host, 'wizAccentNote').textContent, ' ', 'a readable colour needs no sentence');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('executed: the dark preview is the look\'s tinted night, or pure black with the tint off', () => {
|
|
175
|
+
const { kit, dom } = bootKit();
|
|
176
|
+
const host = dom.node('div');
|
|
177
|
+
const model = { look: 'grove', nightTint: true, accent: '#b48cf2', logo: null };
|
|
178
|
+
kit.buildLookPicker(host, model, null, 'wiz');
|
|
179
|
+
const dark = () => dom.find(host, (x) => x.className === 'mh dark');
|
|
180
|
+
const accent = kit.LOOKS.grove.colors.accent;
|
|
181
|
+
const mix = (hex, p, base) => '#' + [16, 8, 0].map((sh) => Math.round(((parseInt(hex.slice(1), 16) >> sh) & 255) * p / 100 + ((parseInt(base.slice(1), 16) >> sh) & 255) * (100 - p) / 100).toString(16).padStart(2, '0')).join('');
|
|
182
|
+
assert.equal(dark().style.props['--mh-bg'], mix(accent, 6, '#000000'));
|
|
183
|
+
assert.equal(dark().style.props['--mh-card'], mix(accent, 5, '#101012'));
|
|
184
|
+
const tint = dom.byId(host, 'wizTint');
|
|
185
|
+
tint.checked = false; tint.fire('change');
|
|
186
|
+
assert.equal(model.nightTint, false);
|
|
187
|
+
assert.equal(dark().style.props['--mh-bg'], '#000000');
|
|
188
|
+
assert.equal(dark().style.props['--mh-card'], '#101012');
|
|
189
|
+
// a project on its own colours (manage page) never shows a tinted night
|
|
190
|
+
const own = { look: 'own', nightTint: true, accent: '#b48cf2', logo: null };
|
|
191
|
+
const h2 = dom.node('div');
|
|
192
|
+
kit.buildLookPicker(h2, own, null, 'mLk');
|
|
193
|
+
assert.equal(dom.find(h2, (x) => x.className === 'mh dark').style.props['--mh-bg'], '#000000');
|
|
194
|
+
assert.ok(['chrome-world', 'expedition', 'grove', 'blueprint', 'custom'].every((k) => !dom.byId(h2, 'mLkLook-' + k).checked), 'no look is ticked for a project that never chose');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('executed: a logo is refused before it is read unless it is a small PNG, JPEG or WebP', () => {
|
|
198
|
+
let reads = 0;
|
|
199
|
+
const reader = function FileReader() { this.readAsDataURL = (f) => { reads++; this.result = 'data:' + f.type + ';base64,QUJD'; this.onload(); }; };
|
|
200
|
+
const { kit, dom } = bootKit({ reader });
|
|
201
|
+
const host = dom.node('div');
|
|
202
|
+
const model = { look: 'grove', nightTint: true, accent: '#b48cf2', logo: null };
|
|
203
|
+
kit.buildLookPicker(host, model, null, 'wiz');
|
|
204
|
+
const file = dom.byId(host, 'wizLogo');
|
|
205
|
+
const note = file.parentNode.parentNode.children.find((c) => c.attrs.role === 'status');
|
|
206
|
+
file.files = [{ type: 'image/svg+xml', size: 100, name: 'k.svg' }]; file.fire('change');
|
|
207
|
+
assert.match(note.textContent, /PNG, JPEG or WebP/);
|
|
208
|
+
file.files = [{ type: 'image/png', size: 300 * 1024, name: 'k.png' }]; file.fire('change');
|
|
209
|
+
assert.match(note.textContent, /at most 256 KB/);
|
|
210
|
+
assert.equal(reads, 0, 'neither was read');
|
|
211
|
+
assert.equal(model.logo, null);
|
|
212
|
+
file.files = [{ type: 'image/png', size: 2000, name: 'k.png' }]; file.fire('change');
|
|
213
|
+
assert.equal(reads, 1);
|
|
214
|
+
assert.deepEqual(JSON.parse(JSON.stringify(model.logo)), { type: 'image/png', b64: 'QUJD', name: 'k.png' });
|
|
215
|
+
const mark = dom.find(host, (x) => x.className === 'mhMark logo');
|
|
216
|
+
assert.ok(mark && mark.children[0].src.startsWith('data:image/png;base64,'), 'the preview\'s hall mark shows the logo');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('executed: sendLook sends the look, then the logo — and a removed logo is deleted', async () => {
|
|
220
|
+
const calls = [];
|
|
221
|
+
const fetchImpl = (url, opts) => {
|
|
222
|
+
calls.push({ url, method: opts.method, body: opts.body ? JSON.parse(opts.body) : null });
|
|
223
|
+
return Promise.resolve({ ok: true, json: () => Promise.resolve({ ok: true, look: { look: 'custom' }, push: { message: 'Saved.' }, adjusted: null }) });
|
|
224
|
+
};
|
|
225
|
+
const { kit } = bootKit({ fetchImpl });
|
|
226
|
+
await kit.sendLook(42, { look: 'custom', nightTint: false, accent: '#b48cf2', logo: { type: 'image/webp', b64: 'QUJD', name: 'k.webp' } });
|
|
227
|
+
assert.deepEqual(JSON.parse(JSON.stringify(calls)), [
|
|
228
|
+
{ url: '/api/bongos/provisioning/instances/42/look', method: 'PATCH', body: { look: 'custom', night_tint: 'off', accent: '#b48cf2' } },
|
|
229
|
+
{ url: '/api/bongos/provisioning/instances/42/logo', method: 'PUT', body: { image_b64: 'QUJD', content_type: 'image/webp' } },
|
|
230
|
+
]);
|
|
231
|
+
calls.length = 0;
|
|
232
|
+
await kit.sendLook(42, { look: 'grove', nightTint: true, accent: '#b48cf2', logo: null, logoDropped: true });
|
|
233
|
+
assert.deepEqual(calls.map((c) => [c.method, c.url.split('/').pop(), c.body]), [['PATCH', 'look', { look: 'grove', night_tint: 'on' }], ['DELETE', 'logo', null]]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('Create sends the look right after the project is filed, and the review names it', () => {
|
|
237
|
+
assert.match(HUB, /sendRenderSetup\(data\.instance && data\.instance\.id\);[^\n]*\n\s*sendLookSetup\(data\.instance && data\.instance\.id\);/);
|
|
238
|
+
assert.match(HUB, /lookKit\.sendLook\(instanceId, state\.look \|\| defaultLook\(\)\)/);
|
|
239
|
+
assert.match(HUB, /function defaultLook\(\) \{ return \{ look: 'chrome-world', nightTint: true, accent: '#b48cf2', logo: null \}; \}/, 'the default is the platform\'s look with the tint ON (owner D2)');
|
|
240
|
+
assert.match(HUB, /\['Look', lookRow\(\)\]/);
|
|
241
|
+
assert.match(HUB, /<p class="hint" id="doneLook" role="status" hidden><\/p>/, 'a refused look is said on the done panel');
|
|
242
|
+
assert.match(HUB, /look: null, termsAck: false \};/, 'the draft carries the look (and its reset clears it)');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('the manage page has "Its look", read once per project — never on the minute tick', () => {
|
|
246
|
+
const sec = (HUB.match(/<div class="card mSec" id="mLook">[\s\S]*?\n {6}<\/div>/) || [''])[0];
|
|
247
|
+
assert.match(sec, /<h2>Its look<\/h2>/);
|
|
248
|
+
assert.match(sec, /<div id="mLookPick">/);
|
|
249
|
+
assert.match(sec, /id="mLookSave">Save the look<\/button>/);
|
|
250
|
+
assert.match(sec, /Saving restarts your project once/);
|
|
251
|
+
assert.match(HUB, /renderCardPick\(inst\);\n\s*renderLookCard\(inst\);/);
|
|
252
|
+
assert.match(HUB, /function renderLookCard\(inst\) \{\n\s*if \(mLookFor === inst\.id\) return;/, 'the tick does not re-read or rebuild it');
|
|
253
|
+
assert.match(HUB, /lookKit\.buildLookPicker\(host, mLookModel,/, 'the wizard\'s own picker');
|
|
254
|
+
assert.match(HUB, /lookKit\.sendLook\(manageId, mLookModel\)/, 'and its own send');
|
|
255
|
+
});
|
|
@@ -415,7 +415,7 @@ test('the review names the set even when the picker was never opened (task 10023
|
|
|
415
415
|
// and the module NAMES are not known. "None — just the core" would be a false
|
|
416
416
|
// promise on the last screen before Create: the server is about to apply the
|
|
417
417
|
// type's bundle. The row says that instead, then repaints when the read lands.
|
|
418
|
-
const h = boot({ state: { projType: 'game', step:
|
|
418
|
+
const h = boot({ state: { projType: 'game', step: 11 } }); // 11 = the review since task 1004421 (10 since 1004417, 8 since 1004351)
|
|
419
419
|
|
|
420
420
|
// Arrange + Act — the review asks for the value before the read can answer
|
|
421
421
|
const first = h.extrasRow();
|
|
@@ -821,7 +821,7 @@ test('with nothing switched on the count says only what is shown', async () => {
|
|
|
821
821
|
});
|
|
822
822
|
|
|
823
823
|
test('the review names the hidden picks too — a filter never reaches the create request', async () => {
|
|
824
|
-
const h = boot({ state: { projType: 'game', step:
|
|
824
|
+
const h = boot({ state: { projType: 'game', step: 11 } }); // 11 = the review since task 1004421 (10 since 1004417, 8 since 1004351)
|
|
825
825
|
await h.open();
|
|
826
826
|
h.click('agents');
|
|
827
827
|
assert.deepEqual(h.forCreate(), ['agents', 'npm-release', 'discord']);
|
|
@@ -70,7 +70,11 @@ ${NEUTRAL_PREFIX}_VISIBILITY=public
|
|
|
70
70
|
${NEUTRAL_PREFIX}_JOIN_GRANT=full
|
|
71
71
|
${NEUTRAL_PREFIX}_ARTIST_GATE=strict
|
|
72
72
|
${NEUTRAL_PREFIX}_SPEND_PAYER=none
|
|
73
|
+
${NEUTRAL_PREFIX}_LOOK=own
|
|
74
|
+
${NEUTRAL_PREFIX}_NIGHT_TINT=on
|
|
73
75
|
${NEUTRAL_PREFIX}_ARTIST_GATE_SINCE=
|
|
76
|
+
${NEUTRAL_PREFIX}_LOOK_ACCENT=
|
|
77
|
+
${NEUTRAL_PREFIX}_LOGO_URL=
|
|
74
78
|
`;
|
|
75
79
|
|
|
76
80
|
t('replaces an existing KEY= line where it stands; every other line survives byte-for-byte', () => {
|
|
@@ -134,6 +138,10 @@ t('settingsEnvVarsFor: the runner pushes the SAME pair webEnvBody writes at stan
|
|
|
134
138
|
// must already hold it in web.env before its first require
|
|
135
139
|
[`${NEUTRAL_PREFIX}_SPEND_PAYER`]: 'none',
|
|
136
140
|
[`${NEUTRAL_PREFIX}_ARTIST_GATE_SINCE`]: '',
|
|
141
|
+
// the look a hall wears and its night tint (task 1004421, ADR 0358), with the
|
|
142
|
+
// custom accent and the logo address as companions, empty when there is none
|
|
143
|
+
[`${NEUTRAL_PREFIX}_LOOK`]: 'own', [`${NEUTRAL_PREFIX}_NIGHT_TINT`]: 'on',
|
|
144
|
+
[`${NEUTRAL_PREFIX}_LOOK_ACCENT`]: '', [`${NEUTRAL_PREFIX}_LOGO_URL`]: '',
|
|
137
145
|
});
|
|
138
146
|
const body = P.webEnvBody(inst, {});
|
|
139
147
|
for (const [k, v] of Object.entries(vars)) assert.match(body, new RegExp(`^${k}=${v}$`, 'm'), `${k} at standup`);
|
|
@@ -145,7 +153,7 @@ const GATED = { settings: { platform_visibility: 'gated', joinability: 'invite_o
|
|
|
145
153
|
const manifestWith = (project) => JSON.stringify({ schema: 'instance-manifest/v1', brand: project === undefined ? {} : { project } });
|
|
146
154
|
|
|
147
155
|
t('true when every pushed key matches brand.project (camelCase, the ENV_OVERRIDES targets)', () => {
|
|
148
|
-
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' }), GATED), true);
|
|
156
|
+
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none', look: 'own', nightTint: 'on' }), GATED), true);
|
|
149
157
|
});
|
|
150
158
|
t('false when the instance still reports a different value', () => {
|
|
151
159
|
assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'public', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' }), GATED), false);
|
|
@@ -166,7 +174,7 @@ t('instanceManifestCmd asks the instance on loopback, like healthzCmd', () => {
|
|
|
166
174
|
function recorder({ apply = false, envBody = LIVE_ENV, readFails = false, healthzFails = false, privileged = false,
|
|
167
175
|
manifest = 'match', restartThrows = false, writeThrows = false } = {}) {
|
|
168
176
|
const cmds = [], writes = [], events = [], probes = [], cleared = [], logs = [];
|
|
169
|
-
const manifestOut = manifest === 'match' ? manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' })
|
|
177
|
+
const manifestOut = manifest === 'match' ? manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none', look: 'own', nightTint: 'on' })
|
|
170
178
|
: manifest === 'mismatch' ? manifestWith({ platformVisibility: 'public', joinability: 'apply', visibility: 'public' })
|
|
171
179
|
: manifest === 'none' ? manifestWith(undefined) : 'nope';
|
|
172
180
|
const exec = (cmd, opts = {}) => {
|
|
@@ -234,7 +242,7 @@ await ta('dry-run PLANS the patch: reads nothing real, writes NOTHING (no synthe
|
|
|
234
242
|
assert.equal(r.ok, true);
|
|
235
243
|
assert.equal(r.healthy, null, 'nothing ran, nothing observed');
|
|
236
244
|
assert.equal(writes.length, 0, 'a dry-run must not plan a two-line overwrite of web.env');
|
|
237
|
-
assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY, .*_VISIBILITY, .*_JOIN_GRANT, .*_ARTIST_GATE, .*_SPEND_PAYER, .*_ARTIST_GATE_SINCE in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
|
|
245
|
+
assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY, .*_VISIBILITY, .*_JOIN_GRANT, .*_ARTIST_GATE, .*_SPEND_PAYER, .*_LOOK, .*_NIGHT_TINT, .*_ARTIST_GATE_SINCE, .*_LOOK_ACCENT, .*_LOGO_URL in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
|
|
238
246
|
assert.ok(cmds.some((c) => /restart spike/.test(c)), 'the restart is still in the plan');
|
|
239
247
|
assert.ok(logs.some((l) => /dry-run — nothing ran/.test(l)), 'the closing line does not claim a restart happened');
|
|
240
248
|
});
|
|
@@ -354,7 +362,7 @@ await ta('--apply, the control-plane row: its OWN /etc/<slug>/web.env on the con
|
|
|
354
362
|
const mk = (bucket) => (cmd) => {
|
|
355
363
|
seen[bucket].push(cmd);
|
|
356
364
|
if (/\bcat /.test(cmd)) return { ok: true, stdout: selfEnv };
|
|
357
|
-
if (/api\/bongos\/instance/.test(cmd)) return { ok: true, stdout: manifestWith({ platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'off', spendPayer: 'none' }) };
|
|
365
|
+
if (/api\/bongos\/instance/.test(cmd)) return { ok: true, stdout: manifestWith({ platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'off', spendPayer: 'none', look: 'own', nightTint: 'on' }) };
|
|
358
366
|
return { ok: true, stdout: '{"ok":true}' };
|
|
359
367
|
};
|
|
360
368
|
try {
|