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 |
| 33 | import fs from 'node:fs'; |
| 34 | import path from 'node:path'; |
| 35 | import { fileURLToPath } from 'node:url'; |
| 36 | import { open, shot, errors } from './harness.mjs'; |
| 37 | |
| 38 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 39 | const WWW = path.join(HERE, '..', 'www'); |
| 40 | |
| 41 | const ok = [], bad = []; |
| 42 | const check = (name, pass, detail) => { |
| 43 | (pass ? ok : bad).push(name); |
| 44 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 45 | }; |
| 46 | const die = (why) => { console.error('ABORT: ' + why); process.exit(2); }; |
| 47 | |
| 48 | const BREAK = (() => { |
| 49 | const i = process.argv.indexOf('--break'); |
| 50 | return i > 0 ? String(process.argv[i + 1] || '') : ''; |
| 51 | })(); |
| 52 | const 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 | }; |
| 81 | if (BREAK && !BREAKS[BREAK]) die(`no break called "${BREAK}"`); |
| 82 | if (BREAK) console.log(`\n*** BREAK ${BREAK}: ${BREAKS[BREAK].what} — failures below are the point ***\n`); |
| 83 | |
| 84 | const SRC = fs.readFileSync(path.join(WWW, 'js/daimond.js'), 'utf8'); |
| 85 | const BROKEN = BREAKS[BREAK] ? BREAKS[BREAK].edit(SRC) : SRC; |
| 86 | if (BREAK && BROKEN === SRC) die(`the "${BREAK}" break no longer matches www/js/daimond.js`); |
| 87 | |
| 88 | const 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 | }); |
| 96 | const p = s.page; |
| 97 | await p.waitForFunction(() => !!window.DaimondDiamond && !!window.DaimondModels, null, { timeout: 20000 }); |
| 98 | |
| 99 | const [A, B] = await p.evaluate(() => |
| 100 | [...document.querySelectorAll('#diamond-list .diamond-box')].map(b => b.dataset.id)); |
| 101 | if (!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. |
| 106 | const 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 | |
| 125 | const goDiamond = (id) => p.evaluate((i) => { |
| 126 | document.querySelector(`#diamond-list .diamond-box[data-id="${i}"]`).click(); |
| 127 | }, id); |
| 128 | const 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); |
| 134 | const queued = () => p.evaluate((i) => |
| 135 | (window.DaimondDiamond.conversation(i)._queue || []).slice(), A); |
| 136 | |
| 137 | // ── 1. A busy daimon HOLDS what is typed into it ─────────────────── |
| 138 | console.log('\n1. a message typed into a busy daimon\n'); |
| 139 | await goDiamond(A); |
| 140 | await p.waitForTimeout(500); |
| 141 | await say('@slow 6000'); // the turn this queues behind |
| 142 | await 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. |
| 146 | check('the control: the daimon was mid-turn when the next message was typed', |
| 147 | await p.evaluate((i) => !!window.DaimondDiamond.conversation(i)._generating, A)); |
| 148 | await say('held one'); |
| 149 | await p.waitForTimeout(300); |
| 150 | check('what was typed while it ran is held, not dropped', |
| 151 | (await queued()).length === 1, JSON.stringify(await queued())); |
| 152 | check('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 ── |
| 156 | console.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 ───────────────────────────────────────────── |
| 172 | console.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. |
| 189 | console.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 ────────────────────── |
| 226 | console.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 | |
| 243 | const errs = errors(s).filter(e => !/favicon|401|402|502|Unauthorized|Payment|Bad Gateway/i.test(e)); |
| 244 | check('no console errors', errs.length === 0, errs.slice(0, 3).join(' | ')); |
| 245 | |
| 246 | await s.close(); |
| 247 | |
| 248 | console.log(`\n${ok.length} ok, ${bad.length} failed`); |
| 249 | if (bad.length) console.log(' ' + bad.join('\n ')); |
| 250 | if (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 | } |
| 256 | process.exit(bad.length ? 1 : 0); |