Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_nogutter.mjs

10.5 KiB, 1 run

created by r2519314175:549, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// verify_nogutter.mjs — no zone reserves room for nothing.
2//
3// The fifth property search, after reversible, escapable, focus and errnames.
4// The property is one sentence: A ZONE WITH WIDTH HAS SOMETHING IN IT. If the
5// dock is 300px wide, at least one panel must actually be rendering inside it;
6// the same for the stage. A zone that holds width for a panel nobody can see is
7// a dead gutter — a strip of nothing beside the chat, taking space from the
8// work.
9//
10// It is a property rather than a case because of how the real one arose. Panels
11// can be held back until they have earned their place (`WAITS_FOR`): the Agents
12// panel stays behind `body.agents-hidden`, Message and Compose wait for a
13// mailbox. The chip row's own path knew to let a panel out before opening it —
14// but `show()`, which is what the API, the mobile nav, the guide button and a
15// saved layout all call, did not. So a panel the stylesheet suppresses could be
16// SEATED: `display:none !important` beat the engine's `display:''`, the dock
17// reserved its column, and nothing appeared in it. Worse, an unrevealed panel
18// draws no chip and its own closer is inside the hidden element, so there was
19// no way to shut it again — the gutter stayed for the rest of the session and
20// was written to the saved layout for the next one.
21//
22// Nobody would have written a test for "the Agents panel, on an account that
23// has not met agents yet, opened through the API rather than the chip". The
24// property covers it without anyone naming it.
25//
26// node dev/verify_nogutter.mjs
27//
28// Needs dev/serve.mjs (DAIMOND_PORT, default 8777). No gateway.
29
30import fs from 'node:fs';
31import { open, scratch, shot } from './harness.mjs';
32
33let failures = 0, skips = 0;
34const skipped = [];
35const check = (cond, msg, detail) => {
36 console.log((cond ? ' ok ' : ' FAIL ') + msg + (detail != null ? ' — ' + detail : ''));
37 if (!cond) failures++;
38};
39const skip = (name, why) => {
40 console.log(' SKIP ' + name + ' — ' + why);
41 skipped.push(name + ': ' + why);
42 skips++;
43};
44
45/// What each zone is holding, and what is actually drawn inside it.
46///
47/// `getClientRects()`, not the inline style: the whole defect was a stylesheet
48/// rule beating an inline `display:''`, so anything that reads what the engine
49/// INTENDED rather than what the browser DID would have reported the broken
50/// layout as healthy. (An `el.style.display || 'unset'` probe is worse still —
51/// it prints "unset" for the empty string the engine legitimately sets.)
52const ZONES = () => {
53 const seen = (el) => el.getClientRects().length > 0;
54 const out = {};
55 [['dock', 'dock'], ['stage', 'stage']].forEach(([key, id]) => {
56 const z = document.getElementById(id);
57 if (!z) { out[key] = null; return; }
58 out[key] = {
59 width: Math.round(z.getBoundingClientRect().width),
60 visible: [...z.querySelectorAll('.panel')].filter(seen).map((p) => p.dataset.panel),
61 seated: [...z.querySelectorAll('.panel')].map((p) => p.dataset.panel),
62 };
63 });
64 out.open = (window.DaimondPanels ? DaimondPanels.panels() : [])
65 .map((p) => p.id).filter((id) => DaimondPanels.isOpen(id));
66 // The same question one level down. The dock's columns SHARE its width, so a
67 // column the grid has finished with but the browser is still drawing takes
68 // its share from the columns that have something in them -- a gutter that
69 // does not show up in the dock's own width at all.
70 out.cols = [...document.querySelectorAll('#dock .pcol')].map((c) => ({
71 id: c.id,
72 width: Math.round(c.getBoundingClientRect().width),
73 visible: [...c.children].filter(seen).map((k) => k.dataset.panel),
74 }));
75 return out;
76};
77
78/// The complaint, or null when the zones are honest.
79function gutterIn(z) {
80 for (const key of ['dock', 'stage']) {
81 const zone = z[key];
82 if (!zone) continue;
83 if (zone.width > 0 && zone.visible.length === 0) {
84 return `${key} is ${zone.width}px wide with nothing rendered in it`
85 + (zone.seated.length ? ` (seated but invisible: ${zone.seated.join(', ')})` : '');
86 }
87 }
88 const dead = (z.cols || []).filter((c) => c.width > 0 && c.visible.length === 0);
89 if (dead.length) {
90 return dead.map((c) => `dock column ${c.id} is ${c.width}px wide with nothing rendered in it`)
91 .join('; ');
92 }
93 return null;
94}
95
96/// Every panel the engine knows about, asked of the engine rather than listed
97/// here — a panel added later is covered without this file being edited.
98const PANEL_IDS = () => (window.DaimondPanels ? DaimondPanels.panels() : []).map((p) => p.id);
99
100const start = async (name) => {
101 const dir = scratch('pw', 'gut-' + Math.random().toString(36).slice(2, 10));
102 const s = await open({ connect: false, name: name || 'nogutter', profile: dir });
103 const inner = s.close;
104 s.close = async () => {
105 await inner();
106 try { fs.rmSync(dir, { recursive: true, force: true }); } catch (e) { /* gone */ }
107 };
108 await s.page.waitForTimeout(600);
109 return s;
110};
111
112// ── 1. Every panel, opened and closed on its own, in a fresh session ────
113//
114// Fresh each time, and deliberately on a NEW account: `agents-hidden` is on
115// until an account has met the agents, which is exactly the state the defect
116// needed. A verifier that reused one signed-in profile would reveal the panel
117// on its first probe and be blind for the rest of the run.
118console.log('\n── Each panel, shown then hidden, on a fresh account');
119let ids = [];
120try {
121 const s0 = await start();
122 ids = await s0.page.evaluate(PANEL_IDS);
123 await s0.close();
124} catch (e) {
125 skip('panel list', String(e && e.message ? e.message : e).split('\n')[0]);
126}
127
128for (const id of ids) {
129 if (id === 'rail' || id === 'ai') continue; // permanent furniture, not guests
130 let s = null;
131 try {
132 s = await start();
133 const { page } = s;
134 await page.evaluate((i) => DaimondPanels.show(i), id);
135 await page.waitForTimeout(500);
136 const shown = await page.evaluate(ZONES);
137 const bad1 = gutterIn(shown);
138
139 // And the other side of the same coin: show() has to SHOW. Refusing to
140 // seat a panel that cannot render would collapse the gutter honestly
141 // enough, and leave "open the Agents panel" as a silent no-op — the
142 // asking is what earns a held-back panel its place, so the answer to
143 // being asked is to let it out, not to swallow the request.
144 const up = await page.evaluate((i) => {
145 const el = document.querySelector('.panel[data-panel="' + i + '"]');
146 return !!(el && el.getClientRects().length);
147 }, id);
148
149 await page.evaluate((i) => DaimondPanels.hide(i), id);
150 await page.waitForTimeout(500);
151 const hidden = await page.evaluate(ZONES);
152 const bad2 = gutterIn(hidden);
153
154 check(!bad1 && !bad2, `show('${id}') then hide('${id}') leaves no dead space`,
155 bad1 ? 'after show: ' + bad1 : (bad2 ? 'after hide: ' + bad2 : null));
156 check(up, `show('${id}') actually puts it on screen`,
157 up ? null : 'the panel never rendered — the request was swallowed');
158 if (bad1 || bad2 || !up) await shot(s, 'nogutter-' + id);
159 } catch (e) {
160 skip(`show/hide ${id}`, String(e && e.message ? e.message : e).split('\n')[0]);
161 } finally {
162 if (s) { try { await s.close(); } catch (e) { /* gone */ } }
163 }
164}
165
166// ── 2. The whole round, in one session ─────────────────────────────────
167//
168// One session ON PURPOSE, unlike every other probe here: the defect's second
169// half was that it PERSISTED — the gutter outlived the panel that caused it and
170// followed the saved layout into the next session. Only a sequence can catch
171// that, so this walk opens everything in turn and checks after every step.
172console.log('\n── One session, opening each panel in turn and closing it again');
173{
174 let s = null;
175 try {
176 s = await start('nogutter-walk');
177 const { page } = s;
178 let worst = null;
179 for (const id of ids) {
180 if (id === 'rail' || id === 'ai') continue;
181 await page.evaluate((i) => DaimondPanels.show(i), id);
182 await page.waitForTimeout(350);
183 let bad = gutterIn(await page.evaluate(ZONES));
184 if (bad && !worst) worst = `after show('${id}'): ${bad}`;
185 await page.evaluate((i) => DaimondPanels.hide(i), id);
186 await page.waitForTimeout(350);
187 bad = gutterIn(await page.evaluate(ZONES));
188 if (bad && !worst) worst = `after hide('${id}'): ${bad}`;
189 }
190 check(worst === null, 'the whole round leaves no dead space', worst);
191
192 // And the state it leaves behind: reload, and see what the saved layout
193 // brings back. A gutter that survives a reload is one the user is stuck
194 // with for good.
195 await page.reload({ waitUntil: 'domcontentloaded' });
196 await page.waitForTimeout(2500);
197 const after = await page.evaluate(ZONES);
198 const bad = gutterIn(after);
199 check(!bad, 'and nothing dead is restored from the saved layout', bad);
200 if (bad) await shot(s, 'nogutter-after-reload');
201 } catch (e) {
202 skip('the walk', String(e && e.message ? e.message : e).split('\n')[0]);
203 } finally {
204 if (s) { try { await s.close(); } catch (e) { /* gone */ } }
205 }
206}
207
208// ── 3. A layout that arrives already broken ────────────────────────────
209//
210// Anyone who met the defect has it written into `daimond-layout` already, and
211// boot calls apply() rather than show() — so unless apply() itself heals it,
212// the fix would never reach the people who need it.
213console.log('\n── A saved layout that already carries the state');
214{
215 let s = null;
216 try {
217 s = await start('nogutter-heal');
218 const { page } = s;
219 await page.evaluate(() => {
220 localStorage.removeItem('daimond-agents-revealed');
221 localStorage.removeItem('daimond-used-panels');
222 const l = JSON.parse(localStorage.getItem('daimond-layout') || '{}');
223 l.open = Object.assign({}, l.open, { rail: true, ai: true, web: false, work: false, agents: true });
224 l.stage = ['ai'];
225 l.dock = ['agents'];
226 localStorage.setItem('daimond-layout', JSON.stringify(l));
227 });
228 await page.reload({ waitUntil: 'domcontentloaded' });
229 await page.waitForTimeout(2500);
230 const z = await page.evaluate(ZONES);
231 const bad = gutterIn(z);
232 check(!bad, 'a layout saved with an unshowable panel heals on load',
233 bad || `dock ${z.dock.width}px, open: ${z.open.join(',')}`);
234 if (bad) await shot(s, 'nogutter-heal');
235 } catch (e) {
236 skip('the heal', String(e && e.message ? e.message : e).split('\n')[0]);
237 } finally {
238 if (s) { try { await s.close(); } catch (e) { /* gone */ } }
239 }
240}
241
242if (skipped.length) console.log('\nSKIPPED: ' + skipped.join('; '));
243console.log(failures === 0
244 ? `\nnogutter: every zone with width has something in it${skips ? ` (${skips} SKIPPED)` : ''}.`
245 : `\nnogutter: ${failures} zone(s) holding room for nothing${skips ? `, ${skips} SKIPPED` : ''}.`);
246process.exit(failures === 0 && skips === 0 ? 0 : 1);