Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_queuebadge.mjs

12.1 KiB, 1 run

created by r2519314175:619, 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_queuebadge.mjs — the queue badge on a DIAMOND tile: that it is drawn,
2// that it updates in place, and that a message typed into a busy daimon is held
3// rather than swallowed.
4//
5// `queueBadge` was called from exactly one place, the ordinary-chat tile, so a
6// Diamond's tile said nothing about what was waiting on its daimon -- though
7// `app.css` states as settled fact that Simple keeps "the pause light, the name, the
8// queue badge, the pending-fold badge and the tags", and every other item in that
9// list is a Diamond-tile thing.
10//
11// TWO TRAPS, AND THEY ARE THE POINT OF THE FILE.
12//
13// 1. `updateQueueBadges` scanned `sessionList` alone, wanted a `.tile-active-top`
14// container a Diamond tile does not have, and looked the record up by
15// `byId[box.dataset.id]` -- but a Diamond tile's `dataset.id` is the DIAMOND'S
16// id, not its daimon conversation's. Any of the three wrong and the badge is
17// drawn once by `diamondBox` and then either frozen or stripped on the next
18// pass: correct-looking the moment it appears and wrong for ever after. So the
19// badge is asserted to CHANGE without a rail redraw, not merely to exist.
20//
21// 2. Nothing could put anything in a daimon's queue. `sendUserMessage`'s Diamond
22// branch read `if (crystalBusy) return;` and did nothing else, while the Send
23// button stayed enabled and read "Send" -- `sendMode` asks `curGen`, which reads
24// `_generating`, and a steer sets `crystalBusy` instead. The press was swallowed
25// whole. A badge over a queue that can never fill is decoration, so the queue is
26// exercised through the composer here, the way a person fills it.
27//
28// node dev/verify_queuebadge.mjs --break railonly # updateQueueBadges scans one rail again
29// node dev/verify_queuebadge.mjs --break byid # the daimon looked up by the Diamond's id
30// node dev/verify_queuebadge.mjs --break swallow # a busy daimon drops what you type
31// node dev/verify_queuebadge.mjs --break meterrow # the badge on the row Simple hides
32// node dev/verify_queuebadge.mjs # and then, clean
33import fs from 'node:fs';
34import path from 'node:path';
35import { fileURLToPath } from 'node:url';
36import { open, shot, errors } from './harness.mjs';
37
38const HERE = path.dirname(fileURLToPath(import.meta.url));
39const WWW = path.join(HERE, '..', 'www');
40
41const ok = [], bad = [];
42const check = (name, pass, detail) => {
43 (pass ? ok : bad).push(name);
44 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
45};
46const die = (why) => { console.error('ABORT: ' + why); process.exit(2); };
47
48const BREAK = (() => {
49 const i = process.argv.indexOf('--break');
50 return i > 0 ? String(process.argv[i + 1] || '') : '';
51})();
52const BREAKS = {
53 railonly: {
54 what: 'updateQueueBadges scans the chats rail alone, as it always did',
55 edit: (src) => src.replace(
56 '\t\tif (diamondList) boxes = boxes.concat(\n'
57 + '\t\t\tArray.prototype.slice.call(diamondList.querySelectorAll(\'.diamond-box\')));',
58 '\t\t/* railonly: the Diamonds rail is not looked at */'),
59 },
60 byid: {
61 what: 'a Diamond tile\'s conversation looked up by the DIAMOND\'s id, in the chats map',
62 edit: (src) => src.replace(
63 '\t\tif (box.classList.contains(\'diamond-box\')) {',
64 '\t\tif (false) {'),
65 },
66 swallow: {
67 what: 'a message typed into a busy daimon is dropped, as it was before',
68 edit: (src) => src.replace(
69 '\t\t\tif (diamondBusy(current.diamondId)) { enqueueMessage(current, text); return; }',
70 '\t\t\tif (diamondBusy(current.diamondId)) return;'),
71 },
72 meterrow: {
73 what: 'the badge kept on the meter row, which Simple hides outright',
74 edit: (src) => src.replace(
75 '\t\tif (!box.classList.contains(\'diamond-box\')) return box.querySelector(\'.tile-active-top\');\n'
76 + '\t\tvar meta = box.querySelector(\'.session-box-meta\');',
77 '\t\tif (!box.classList.contains(\'diamond-box\')) return box.querySelector(\'.tile-active-top\');\n'
78 + '\t\tvar meta = box.querySelector(\'.diamond-meter\');'),
79 },
80};
81if (BREAK && !BREAKS[BREAK]) die(`no break called "${BREAK}"`);
82if (BREAK) console.log(`\n*** BREAK ${BREAK}: ${BREAKS[BREAK].what} — failures below are the point ***\n`);
83
84const SRC = fs.readFileSync(path.join(WWW, 'js/daimond.js'), 'utf8');
85const BROKEN = BREAKS[BREAK] ? BREAKS[BREAK].edit(SRC) : SRC;
86if (BREAK && BROKEN === SRC) die(`the "${BREAK}" break no longer matches www/js/daimond.js`);
87
88const s = await open({
89 name: 'queuebadge' + (BREAK ? '-' + BREAK : ''),
90 route: BREAK ? (async (page) => {
91 await page.route('**/js/daimond.js', (r) => r.fulfill({
92 status: 200, contentType: 'application/javascript', body: BROKEN,
93 }));
94 }) : null,
95});
96const p = s.page;
97await p.waitForFunction(() => !!window.DaimondDiamond && !!window.DaimondModels, null, { timeout: 20000 });
98
99const [A, B] = await p.evaluate(() =>
100 [...document.querySelectorAll('#diamond-list .diamond-box')].map(b => b.dataset.id));
101if (!A || !B) die('this world does not hold the two seeded Diamonds');
102
103/// What the tile SAYS, measured as ink on the rail rather than as a computed
104/// `display`: a badge inside a hidden row still answers `display: inline` for
105/// itself, so only geometry can say whether anybody can read it.
106const badgeOn = (id) => p.evaluate((i) => {
107 const box = document.querySelector(`#diamond-list .diamond-box[data-id="${i}"]`);
108 if (!box) return { tile: false };
109 const b = box.querySelector('.queue-badge');
110 if (!b) return { tile: true, badge: false };
111 const r = b.getBoundingClientRect();
112 const t = box.getBoundingClientRect();
113 return {
114 tile: true, badge: true,
115 text: b.textContent || '',
116 // Drawn, and drawn INSIDE its own tile: a badge with area somewhere off the
117 // rail is not a badge the reader of that tile can see.
118 ink: r.width > 0 && r.height > 0,
119 within: r.top >= t.top - 1 && r.bottom <= t.bottom + 1,
120 // Which row it landed in, since that is what decides whether Simple keeps it.
121 row: (b.parentElement || {}).className || '',
122 };
123}, id);
124
125const goDiamond = (id) => p.evaluate((i) => {
126 document.querySelector(`#diamond-list .diamond-box[data-id="${i}"]`).click();
127}, id);
128const say = (text) => p.evaluate((t) => {
129 const el = document.getElementById('chat-input');
130 el.value = t;
131 el.dispatchEvent(new Event('input', { bubbles: true }));
132 document.getElementById('chat-send').click();
133}, text);
134const queued = () => p.evaluate((i) =>
135 (window.DaimondDiamond.conversation(i)._queue || []).slice(), A);
136
137// ── 1. A busy daimon HOLDS what is typed into it ───────────────────
138console.log('\n1. a message typed into a busy daimon\n');
139await goDiamond(A);
140await p.waitForTimeout(500);
141await say('@slow 6000'); // the turn this queues behind
142await p.waitForTimeout(900);
143// THE CONTROL for everything below: nothing here means anything unless the daimon
144// really was busy when the second message was typed. `_generating` is set on the
145// daimon's own record for the length of the steer.
146check('the control: the daimon was mid-turn when the next message was typed',
147 await p.evaluate((i) => !!window.DaimondDiamond.conversation(i)._generating, A));
148await say('held one');
149await p.waitForTimeout(300);
150check('what was typed while it ran is held, not dropped',
151 (await queued()).length === 1, JSON.stringify(await queued()));
152check('and the composer was cleared, so the press did something visible',
153 (await p.evaluate(() => document.getElementById('chat-input').value)) === '');
154
155// ── 2. The badge is on the Diamond's tile, in the row Simple keeps ──
156console.log('\n2. the badge on the tile\n');
157{
158 const b = await badgeOn(A);
159 check('the Diamond\'s tile carries a queue badge', !!b.badge, JSON.stringify(b));
160 check('the badge is ink on the tile, not a node with no area',
161 !!(b.badge && b.ink && b.within), JSON.stringify(b));
162 // `app.css` keeps a meta row alive in Simple exactly when it holds a
163 // `.diamond-pending`; `.diamond-meter` it hides outright. So the row the badge
164 // lands in decides whether the stylesheet's own sentence is true.
165 check('and it is in the meta row, the one Simple keeps for a pending badge',
166 /session-box-meta/.test(b.row || ''), JSON.stringify(b.row));
167 check('no other Diamond\'s tile grew one',
168 (await badgeOn(B)).badge === false);
169}
170
171// ── 3. Simple keeps it ─────────────────────────────────────────────
172console.log('\n3. Simple keeps the badge, as app.css says it does\n');
173{
174 const was = await p.evaluate(() => window.DaimondView.get && window.DaimondView.get());
175 await p.evaluate(() => window.DaimondView.set('simple'));
176 await p.waitForTimeout(400);
177 const b = await badgeOn(A);
178 check('the badge survives Simple', !!(b.badge && b.ink && b.within), JSON.stringify(b));
179 await shot(s, 'queuebadge-simple');
180 await p.evaluate((w) => window.DaimondView.set(w || 'max'), was);
181 await p.waitForTimeout(400);
182}
183
184// ── 4. IT UPDATES IN PLACE ─────────────────────────────────────────
185//
186// The half that a badge drawn only by `diamondBox` would pass on and then fail for
187// ever. The rail is NOT redrawn here: the count is changed and `updateQueueBadges`
188// is left to do its work, which is what it exists for.
189console.log('\n4. the badge follows the queue without a rail redraw\n');
190{
191 const before = await badgeOn(A);
192 // A second message, through the composer again.
193 await say('held two');
194 await p.waitForTimeout(300);
195 const after = await badgeOn(A);
196 check('two are queued now', (await queued()).length === 2, JSON.stringify(await queued()));
197 check('the badge text changed with the queue, in place',
198 !!(after.badge && after.text && after.text !== before.text),
199 `${JSON.stringify(before.text)} -> ${JSON.stringify(after.text)}`);
200
201 // And back down: emptying the queue must take the badge away, on the same path.
202 await p.evaluate((i) => {
203 window.DaimondDiamond.conversation(i)._queue = [];
204 // The app's own refresh, not a rail rebuild -- see `renderQueue`.
205 document.getElementById('chat-input').dispatchEvent(new Event('input', { bubbles: true }));
206 }, A);
207 await p.evaluate(() => window.dispatchEvent(new Event('resize')));
208 // `renderQueue` is what calls `updateQueueBadges`, and it is reached by the
209 // composer's own path: taking a queued bubble back withdraws it.
210 await p.evaluate(() => {
211 const x = document.querySelector('#chat-queued .queue-x');
212 if (x) x.click();
213 });
214 await p.waitForTimeout(300);
215 const gone = await badgeOn(A);
216 check('an emptied queue takes the badge away again', gone.badge === false, JSON.stringify(gone));
217 check('and the empty meta row goes with it, rather than costing its margin',
218 await p.evaluate((i) => {
219 const box = document.querySelector(`#diamond-list .diamond-box[data-id="${i}"]`);
220 const m = box.querySelector('.session-box-meta');
221 return !m || !!m.firstChild;
222 }, A));
223}
224
225// ── 5. The queue drains when the turn is over ──────────────────────
226console.log('\n5. what was held is sent, once the daimon is free\n');
227{
228 await p.evaluate((i) => { window.DaimondDiamond.conversation(i)._queue = ['@text drained']; }, A);
229 await p.evaluate(() => {
230 const b = document.getElementById('chat-input');
231 b.dispatchEvent(new Event('input', { bubbles: true }));
232 });
233 // Wait out the steer that is still running, then a moment for the drain.
234 const t0 = Date.now();
235 while (Date.now() - t0 < 40000) {
236 if ((await queued()).length === 0) break;
237 await p.waitForTimeout(400);
238 }
239 check('the held message was sent rather than left waiting for ever',
240 (await queued()).length === 0, JSON.stringify(await queued()));
241}
242
243const errs = errors(s).filter(e => !/favicon|401|402|502|Unauthorized|Payment|Bad Gateway/i.test(e));
244check('no console errors', errs.length === 0, errs.slice(0, 3).join(' | '));
245
246await s.close();
247
248console.log(`\n${ok.length} ok, ${bad.length} failed`);
249if (bad.length) console.log(' ' + bad.join('\n '));
250if (BREAK) {
251 console.log(bad.length
252 ? '\nTHE BREAK WAS CAUGHT.'
253 : '\nTHE BREAK WAS NOT CAUGHT: this check proves nothing');
254 process.exit(bad.length ? 0 : 1);
255}
256process.exit(bad.length ? 1 : 0);