oxedyne/daimond/dev/verify_frame.mjs
22.1 KiB, 1 run
created by r2519314175:425, 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_frame.mjs — A TRANSIENT ELEMENT MUST NEVER SIT IN THE FLOW OF CLICKABLE |
| 2 | // TARGETS. Either it lives outside that flow, or the space it occupies is |
| 3 | // reserved whether or not it is showing. |
| 4 | // |
| 5 | // The top bar is where that rule is hardest to keep, because five things in it |
| 6 | // come and go on their own timing: a sync round starting and stopping, a held |
| 7 | // cloud sweep, the pairing button arriving with a session, the update chip going |
| 8 | // quiet, and an unseen count landing on a chip. Until 2026-08-28 the bar's |
| 9 | // right-hand group shrank to its contents inside a `space-between` row, so every |
| 10 | // one of those moved the whole chip row sideways -- 86px for a sync round, 98 for |
| 11 | // a held sweep, 122 for the pairing button, 21 for a mail badge -- and the owner |
| 12 | // reported it as what it is: "it makes it impossible to click moving targets |
| 13 | // during the process." |
| 14 | // |
| 15 | // So this measures rather than inspects. Each transient is made to appear |
| 16 | // through the app's own path, and the x of every chip and every icon button in |
| 17 | // the bar is compared across the change. Nothing may move by a pixel. |
| 18 | // |
| 19 | // EVERY CHECK BELOW WAS SHOWN GOING RED, 2026-08-28, by putting each mechanism |
| 20 | // back the way it was: the badge in the chip's flow (4 chips moved 22px), the |
| 21 | // update chip's `[hidden]` rule taken out (visible while hidden), the pairing |
| 22 | // button hidden with `display` (its arrival caught), the sync chip mounted back |
| 23 | // in `.top-actions` (18 targets moved, worst 77px, and 9 rail rows with it), and |
| 24 | // `.top-actions` back to `flex: 0 1 auto` (a longer label moved all 12 chips to |
| 25 | // its left). That last one caught a real mistake in the very change this file |
| 26 | // was written for: the property had been reverted while the comment above it |
| 27 | // still described the fix, and the four transient checks all passed anyway -- |
| 28 | // their space is reserved, so they never needed the origin to be nailed down. |
| 29 | // |
| 30 | // WHAT THIS CANNOT SEE. It measures one window width and one language: a rule |
| 31 | // that held at 1440px in English and broke at 900px in German would pass here. |
| 32 | // The widths are `verify_workspace`'s subject and the languages are |
| 33 | // `verify_i18n`'s; this one owns the question of whether anything MOVES. |
| 34 | // And the pairing section is the weakest of the five, because the button's |
| 35 | // hidden form has to be posed by this file rather than read off the app: it |
| 36 | // catches a change to how the button is revealed, not every way its slot could |
| 37 | // stop being reserved. |
| 38 | import { open, errors } from './harness.mjs'; |
| 39 | |
| 40 | const ok = [], bad = []; |
| 41 | const check = (name, pass, detail) => { |
| 42 | (pass ? ok : bad).push(name); |
| 43 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 44 | }; |
| 45 | const sleep = ms => new Promise(r => setTimeout(r, ms)); |
| 46 | |
| 47 | const s = await open({ signIn: true, connect: true, name: 'frame' }); |
| 48 | const { page } = s; |
| 49 | await page.route('**/*.{woff,woff2,ttf,otf}', r => r.abort()); |
| 50 | await page.evaluate(() => { try { window.DaimondAdmin.closeModal(); } catch (e) {} const m = document.getElementById('settings-modal'); if (m) m.style.display = 'none'; }); |
| 51 | await page.setViewportSize({ width: 1440, height: 900 }); |
| 52 | await page.addStyleTag({ content: '*,*::before,*::after{transition:none!important;animation:none!important}' }); |
| 53 | await sleep(700); |
| 54 | |
| 55 | /// Every target in the top bar, by its left edge. |
| 56 | const bar = () => page.evaluate(() => { |
| 57 | const o = {}; |
| 58 | document.querySelectorAll('#panel-tags .ptag').forEach(c => { |
| 59 | o['chip:' + (c.dataset.panel || c.id)] = Math.round(c.getBoundingClientRect().x); |
| 60 | }); |
| 61 | document.querySelectorAll('.top-actions > button').forEach(b => { |
| 62 | o['btn:' + (b.id || b.className)] = Math.round(b.getBoundingClientRect().x); |
| 63 | }); |
| 64 | return o; |
| 65 | }); |
| 66 | |
| 67 | /// Compare two readings and report the worst movement. |
| 68 | const still = async (what, before) => { |
| 69 | const after = await bar(); |
| 70 | const shared = Object.keys(before).filter(k => after[k] !== undefined); |
| 71 | const moved = shared.filter(k => after[k] !== before[k]); |
| 72 | const worst = moved.length ? Math.max(...moved.map(k => Math.abs(after[k] - before[k]))) : 0; |
| 73 | check(what, moved.length === 0 && shared.length > 6, |
| 74 | `${shared.length} targets` + (moved.length |
| 75 | ? `, ${moved.length} moved, worst ${worst}px: ` + moved.slice(0, 4).join(' ') |
| 76 | : ', none moved')); |
| 77 | }; |
| 78 | |
| 79 | // ── 1. A sync round ──────────────────────────────────────────────── |
| 80 | // Through sync.js's own `pullOnce`, with the gateway stubbed so the request |
| 81 | // hangs: that is the state being complained about, a round in flight while a |
| 82 | // thumb is on its way to a chip. |
| 83 | { |
| 84 | const before = await bar(); |
| 85 | check('the bar holds chips and icon buttons to measure', |
| 86 | Object.keys(before).filter(k => k.startsWith('chip:')).length > 3 |
| 87 | && Object.keys(before).filter(k => k.startsWith('btn:')).length > 2, |
| 88 | Object.keys(before).length + ' targets'); |
| 89 | await page.evaluate(() => { |
| 90 | window.DaimondGateway.state = () => ({ authed: true }); |
| 91 | window.DaimondGateway.clientApi = () => 1; |
| 92 | window.DaimondGateway.gwFetch = () => new Promise(r => setTimeout( |
| 93 | () => r({ status: 200, json: async () => ({ present: false }) }), 5000)); |
| 94 | }); |
| 95 | page.evaluate(() => window.DaimondSync.pull()).catch(() => {}); |
| 96 | await sleep(800); |
| 97 | const chip = await page.evaluate(() => { |
| 98 | const c = document.getElementById('sync-chip'); |
| 99 | return c ? { shown: getComputedStyle(c).display !== 'none', text: (c.textContent || '').trim(), where: c.parentNode.id } : null; |
| 100 | }); |
| 101 | check('a round in flight says so somewhere the user can see it', |
| 102 | !!(chip && chip.shown && /sync/i.test(chip.text)), JSON.stringify(chip)); |
| 103 | // AND NOT IN THE ROW OF TARGETS. The chip is the reason this file exists; a |
| 104 | // later change that moved it back into `.top-actions` would pass every |
| 105 | // measurement below only until the bar ran short of room. |
| 106 | check('and it says so OUT of the bar, in the rail\'s status strip', |
| 107 | !!(chip && chip.where === 'astat-sync'), chip && chip.where); |
| 108 | await still('a sync round moves nothing in the bar', before); |
| 109 | await sleep(5000); |
| 110 | } |
| 111 | |
| 112 | // ── 2. The pairing button, revealed by a session ─────────────────── |
| 113 | { |
| 114 | await page.evaluate(() => { const b = document.getElementById('pair-link-btn'); if (b) b.style.visibility = 'hidden'; }); |
| 115 | await sleep(250); |
| 116 | const before = await bar(); |
| 117 | await page.evaluate(() => window.dispatchEvent(new Event('daimond:authed'))); |
| 118 | await sleep(350); |
| 119 | check('the pairing button is showing once a session exists', await page.evaluate(() => { |
| 120 | const b = document.getElementById('pair-link-btn'); |
| 121 | return !!b && getComputedStyle(b).visibility === 'visible'; |
| 122 | })); |
| 123 | await still('and its slot was already there, so nothing moved when it arrived', before); |
| 124 | } |
| 125 | |
| 126 | // ── 3. The update chip going quiet ───────────────────────────────── |
| 127 | // `hidden` used to do nothing at all here: an author rule setting `display: |
| 128 | // flex` beats the browser's `[hidden] { display: none }`, so the one state that |
| 129 | // asks for silence -- no version stamp deployed -- drew a faint mark anyway. |
| 130 | { |
| 131 | await page.evaluate(() => { document.getElementById('update-chip').hidden = true; }); |
| 132 | await sleep(250); |
| 133 | const quiet = await page.evaluate(() => { |
| 134 | const c = getComputedStyle(document.getElementById('update-chip')); |
| 135 | return { display: c.display, visibility: c.visibility, w: Math.round(document.getElementById('update-chip').getBoundingClientRect().width) }; |
| 136 | }); |
| 137 | check('a hidden update chip is actually invisible', quiet.visibility === 'hidden', JSON.stringify(quiet)); |
| 138 | check('and still holds its place in the row', quiet.w > 20, quiet.w + 'px'); |
| 139 | const before = await bar(); |
| 140 | await page.evaluate(() => { document.getElementById('update-chip').hidden = false; }); |
| 141 | await sleep(250); |
| 142 | await still('the update chip coming back moves nothing', before); |
| 143 | } |
| 144 | |
| 145 | // ── 4. An unseen count landing on a chip ─────────────────────────── |
| 146 | // Through the event mail.js dispatches, so what is measured is the badge the |
| 147 | // app really draws. |
| 148 | { |
| 149 | const before = await bar(); |
| 150 | await page.evaluate(() => window.dispatchEvent(new CustomEvent('daimond:mail-arrived', { detail: { count: 3 } }))); |
| 151 | await sleep(450); |
| 152 | check('three messages arriving draws a count on the Email chip', await page.evaluate(() => { |
| 153 | const b = document.querySelector('#panel-tags .ptag[data-panel="mail"] .dock-count'); |
| 154 | return !!b && (b.textContent || '').trim() === '3'; |
| 155 | })); |
| 156 | await still('a count arriving moves nothing — not even the chip it is on', before); |
| 157 | } |
| 158 | |
| 159 | // ── 5. A chip's own label getting longer ─────────────────────────── |
| 160 | // The row's LEFT EDGE has to be fixed too, not merely undisturbed by what is to |
| 161 | // its right. A language change, a renamed panel, or the `... N` chip going from |
| 162 | // one digit to two all change how much room the row wants; if the row shrank to |
| 163 | // its contents inside a right-anchored group, every one of those would slide the |
| 164 | // whole row sideways. This is the check that says the origin is nailed down -- |
| 165 | // and it is here because the rule was written into the comment above |
| 166 | // `.top-actions` before it was written into the property, and nothing else |
| 167 | // noticed. |
| 168 | { |
| 169 | // Through `relabel`, which is what a language change calls: writing a longer |
| 170 | // word into the chip by hand does not survive, because the row's own |
| 171 | // ResizeObserver rebuilds it from the registry within the frame. |
| 172 | const before = await bar(); |
| 173 | await page.evaluate(() => { |
| 174 | const el = document.getElementById('panel-social'); |
| 175 | el.dataset.was = el.dataset.label; |
| 176 | el.dataset.label = el.dataset.label + ' and then some'; |
| 177 | window.DaimondPanels.relabel(); |
| 178 | }); |
| 179 | await sleep(400); |
| 180 | const after = await bar(); |
| 181 | const grew = await page.evaluate(() => { |
| 182 | const c = document.querySelector('#panel-tags .ptag[data-panel="social"]'); |
| 183 | return c ? Math.round(c.getBoundingClientRect().width) : 0; |
| 184 | }); |
| 185 | const left = Object.keys(before).filter(k => k.startsWith('chip:') && k !== 'chip:social' |
| 186 | && after[k] !== undefined && before[k] < before['chip:social']); |
| 187 | const movedLeft = left.filter(k => after[k] !== before[k]); |
| 188 | check('a longer label moves no chip to its left', |
| 189 | left.length > 3 && movedLeft.length === 0 && grew > 90, |
| 190 | `the chip grew to ${grew}px; ${left.length} chips before it, ${movedLeft.length} moved`); |
| 191 | await page.evaluate(() => { |
| 192 | const el = document.getElementById('panel-social'); |
| 193 | if (el.dataset.was) { el.dataset.label = el.dataset.was; delete el.dataset.was; } |
| 194 | window.DaimondPanels.relabel(); |
| 195 | }); |
| 196 | await sleep(300); |
| 197 | } |
| 198 | |
| 199 | // ── 6. A held-back cloud sweep ───────────────────────────────────── |
| 200 | { |
| 201 | const before = await bar(); |
| 202 | const drawn = await page.evaluate(() => { |
| 203 | const c = document.getElementById('chunk-chip'); |
| 204 | if (!c) return null; |
| 205 | c.querySelector('.ctext').textContent = 'Cleanup paused'; |
| 206 | c.style.display = 'flex'; |
| 207 | return c.parentNode.id; |
| 208 | }); |
| 209 | await sleep(300); |
| 210 | // IN THE SYNC ROW, sharing it. Its own row at the foot of the strip was the |
| 211 | // first answer and it was the same fault one surface over -- the strip grew |
| 212 | // and the rail's controls moved with it (`dev/verify_sweep_seen.mjs`, ANCHOR). |
| 213 | check('a held sweep says so in the rail rather than in the bar', |
| 214 | drawn === 'astat-sync', String(drawn)); |
| 215 | await still('a held sweep moves nothing in the bar', before); |
| 216 | } |
| 217 | |
| 218 | // ── 7. And the strip it all moved into does not move either ──────── |
| 219 | // A status that appears among the rail's rows would be the same fault one panel |
| 220 | // over: those rows are buttons, and the two above the sync row open Credits and |
| 221 | // the Pro offer. |
| 222 | { |
| 223 | const rows = () => page.evaluate(() => { |
| 224 | const o = {}; |
| 225 | document.querySelectorAll('#admin-status > *').forEach(r => { o[r.id || r.className] = Math.round(r.getBoundingClientRect().y); }); |
| 226 | return o; |
| 227 | }); |
| 228 | const quiet = await rows(); |
| 229 | page.evaluate(() => window.DaimondSync.pull()).catch(() => {}); |
| 230 | await sleep(800); |
| 231 | const busy = await rows(); |
| 232 | const moved = Object.keys(quiet).filter(k => busy[k] !== undefined && busy[k] !== quiet[k]); |
| 233 | check('a sync round moves no row in the rail\'s status strip either', |
| 234 | moved.length === 0 && Object.keys(quiet).length > 6, |
| 235 | Object.keys(quiet).length + ' rows, ' + moved.length + ' moved' + (moved.length ? ': ' + moved.join(', ') : '')); |
| 236 | // The row cannot fade the way the old pill did -- it would take its |
| 237 | // neighbours up the strip with it -- so what it does instead is say the one |
| 238 | // thing that is always true and was never on screen anywhere before: a phone |
| 239 | // has no hover, and this sentence lived only in the pill's `title`. |
| 240 | // |
| 241 | // THE TIER HAS TO BE HELD FOR THE RESTING STATE TO EXIST AT ALL. Without it |
| 242 | // "Sync off" stands permanently and correctly, and there is nothing to rest |
| 243 | // to; `recheck` is the app's own door to the state a Pro purchase leaves |
| 244 | // behind. Then a round that lands leaves "Synced" for 1.8s and rests. |
| 245 | await sleep(5500); |
| 246 | await page.evaluate(async () => { |
| 247 | window.DaimondGateway.gwFetch = () => Promise.resolve({ status: 200, json: async () => ({ present: false }) }); |
| 248 | window.DaimondSync.recheck(); |
| 249 | await window.DaimondSync.pull(); |
| 250 | }); |
| 251 | await sleep(2400); |
| 252 | const rest = await page.evaluate(() => { |
| 253 | const c = document.getElementById('sync-chip'), r = document.getElementById('sync-rest'); |
| 254 | return { chipShown: c ? c.style.display !== 'none' : null, |
| 255 | restShown: getComputedStyle(r).display !== 'none', rest: (r.textContent || '').trim() }; |
| 256 | }); |
| 257 | check('with nothing in flight the row says when a sync last worked', |
| 258 | rest.restShown && /sync/i.test(rest.rest), JSON.stringify(rest)); |
| 259 | // SIX VERIFIERS READ `#sync-chip`'s `style.display` to mean "is it saying |
| 260 | // anything", verify_sessionrenew's `state === 'synced' || !shown` among them. |
| 261 | // The resting line is a SIBLING for exactly that reason. |
| 262 | check('and the chip itself is still hidden when it has nothing to say', |
| 263 | rest.chipShown === false, JSON.stringify(rest)); |
| 264 | } |
| 265 | |
| 266 | // A world with no gateway of its own answers `/api/*` with a 502, deliberately |
| 267 | // (dev/world.sh), and nothing in this file is about the gateway. |
| 268 | // ── 8. The Admin drawer, bounded by the rail ─────────────────────── |
| 269 | // The strip's own height is only safe to change because the drawer gives the |
| 270 | // height back instead of rising into the rail's head. HIT-TESTED, not measured: |
| 271 | // the failure here is a control that is present, positioned and covered, which |
| 272 | // is the one case a geometric check passes and a user fails. |
| 273 | { |
| 274 | const seen = (sel) => page.evaluate((q) => { |
| 275 | const e = document.querySelector(q); |
| 276 | if (!e) return 'absent'; |
| 277 | const r = e.getBoundingClientRect(); |
| 278 | if (r.width === 0 || r.height === 0) return 'not drawn'; |
| 279 | const own = (x, y) => { const el = document.elementFromPoint(Math.round(x), Math.round(y)); return !!(el && (el === e || e.contains(el))); }; |
| 280 | const c = own(r.x + r.width / 2, r.y + r.height / 2); |
| 281 | const t = own(r.x + r.width / 2, r.y + 2); |
| 282 | const b = own(r.x + r.width / 2, r.bottom - 2); |
| 283 | return c && t && b ? 'whole' : (c ? 'part' : (t || b ? 'half-live' : 'covered')); |
| 284 | }, sel); |
| 285 | const spill = () => page.evaluate(() => { |
| 286 | const b = document.querySelector('.admin-body').getBoundingClientRect(); |
| 287 | const r = document.querySelector('.panel.rail').getBoundingClientRect(); |
| 288 | return Math.round(r.y - b.y); |
| 289 | }); |
| 290 | // Three window heights, the middle one being where the old 540px cap put the |
| 291 | // drawer's top edge inside `#new-diamond-btn`. |
| 292 | const bad = []; |
| 293 | for (const h of [700, 940, 1100]) { |
| 294 | await page.setViewportSize({ width: 1440, height: h }); |
| 295 | await sleep(500); |
| 296 | await page.evaluate(() => { const b = document.getElementById('settings-btn'); if (b && !document.getElementById('admin').classList.contains('admin-open')) b.click(); }); |
| 297 | await sleep(500); |
| 298 | const said = { h, spill: await spill(), close: await seen('#admin-close'), plus: await seen('#new-diamond-btn') }; |
| 299 | if (said.spill > 0 || said.close !== 'whole' || said.plus !== 'whole') bad.push(JSON.stringify(said)); |
| 300 | await page.evaluate(() => { try { DaimondAdmin.close(); } catch (e) {} }); |
| 301 | await sleep(250); |
| 302 | } |
| 303 | check('the open Admin drawer never leaves the rail, and its own × is always pressable', |
| 304 | bad.length === 0, bad.join(' | ')); |
| 305 | check('and the rail\'s own head is whole above it, at every window height', |
| 306 | bad.length === 0, bad.length ? bad.join(' | ') : '700, 940 and 1100px'); |
| 307 | await page.setViewportSize({ width: 1440, height: 900 }); |
| 308 | await sleep(400); |
| 309 | } |
| 310 | |
| 311 | // ── 9. The strip may change height by ANY amount ─────────────────── |
| 312 | // Section 7 says no row inside the strip moves when a row's contents change. |
| 313 | // This says the thing underneath it: the strip may change HEIGHT, by any amount |
| 314 | // and for any reason, and the divider the user set in the rail above it does not |
| 315 | // move. A different claim, and until 2026-08-28 it was false. The Diamonds list |
| 316 | // held a SHARE of what the furniture left over, so a taller strip shrank |
| 317 | // `#rail-top`, `applyRailSplit` recomputed the list, and the Chats head went half |
| 318 | // as far again as the strip's own rows did. Measured on this file's world before |
| 319 | // the change, at 1440x900: a strip 37px taller moved the Chats head 16px, one |
| 320 | // 113px taller moved it 54. |
| 321 | // |
| 322 | // GROWN WITH PADDING rather than by showing one of the rows that happen to exist |
| 323 | // today. `astat-byok`, `astat-pro` and `astat-store-native` are all still |
| 324 | // transient, by decision or by circumstance, and more rows will be written after |
| 325 | // this file is: a check that names one of them proves nothing about the next. |
| 326 | // Padding is any reason at all, which is the whole claim. |
| 327 | // |
| 328 | // THE DIVIDER IS DRAGGED FIRST, and it has to be. A divider nobody has touched is |
| 329 | // an even split and is still cut from the room every time, deliberately -- see |
| 330 | // `applyRailSplit`. What is fixed here is that a position somebody CHOSE is never |
| 331 | // recomputed out from under them, whatever the strip below does. |
| 332 | { |
| 333 | const MIN_LIST = 72; // MIN_H.list in js/daimond.js -- neither list is crushed |
| 334 | const rail = () => page.evaluate(() => { |
| 335 | const heads = [...document.querySelectorAll('#rail-top .railhead')]; |
| 336 | const chats = heads[heads.length - 1]; |
| 337 | const list = document.getElementById('diamond-list'); |
| 338 | const sess = document.getElementById('session-list'); |
| 339 | return { |
| 340 | chats: chats ? Math.round(chats.getBoundingClientRect().y) : null, |
| 341 | list: Math.round(list.getBoundingClientRect().height), |
| 342 | sess: Math.round(sess.getBoundingClientRect().height), |
| 343 | strip: Math.round(document.getElementById('admin-status').getBoundingClientRect().height), |
| 344 | }; |
| 345 | }); |
| 346 | // Its BOTTOM padding, which is nought in the stylesheet, so the strip grows by |
| 347 | // exactly the number asked for. The top padding is 6px, and setting that grew |
| 348 | // the strip by px-6 -- caught by the premise check below, which is the argument |
| 349 | // for having one. |
| 350 | const grow = (px) => page.evaluate((px) => { |
| 351 | document.getElementById('admin-status').style.paddingBottom = px ? px + 'px' : ''; |
| 352 | }, px); |
| 353 | const drag = async (dy) => { |
| 354 | const b = await (await page.$('#handle-rail-split')).boundingBox(); |
| 355 | await page.mouse.move(b.x + b.width / 2, b.y + b.height / 2); |
| 356 | await page.mouse.down(); |
| 357 | await page.mouse.move(b.x + b.width / 2, b.y + b.height / 2 + dy, { steps: 12 }); |
| 358 | await page.mouse.up(); |
| 359 | await sleep(400); |
| 360 | }; |
| 361 | await page.evaluate(() => { try { DaimondAdmin.close(); } catch (e) {} }); |
| 362 | await page.setViewportSize({ width: 1440, height: 900 }); |
| 363 | await sleep(500); |
| 364 | const even = await rail(); |
| 365 | await drag(-60); |
| 366 | const set = await rail(); |
| 367 | check('the rail\'s divider can be put somewhere the user chose', |
| 368 | set.list < even.list - 40 && set.sess > even.sess + 40, |
| 369 | `diamonds ${even.list}->${set.list}px, chats ${even.sess}->${set.sess}px`); |
| 370 | |
| 371 | const missed = [], moved9 = []; |
| 372 | for (const h of [900, 1100]) { |
| 373 | await page.setViewportSize({ width: 1440, height: h }); |
| 374 | await sleep(500); |
| 375 | for (const px of [37, 54, 97]) { |
| 376 | const a = await rail(); |
| 377 | await grow(px); |
| 378 | await sleep(350); |
| 379 | const b = await rail(); |
| 380 | await grow(0); |
| 381 | await sleep(300); |
| 382 | const c = await rail(); |
| 383 | const said = `${h}/+${px}: chats ${a.chats}->${b.chats}, diamonds ${a.list}->${b.list}->${c.list}, chats list ${a.sess}->${b.sess}, strip ${a.strip}->${b.strip}`; |
| 384 | // The growth has to have reached the rail and left the Chats list room |
| 385 | // to give, or the clamp is entitled to move the divider and this is not |
| 386 | // the case being made. Reported apart from a failure, because a check |
| 387 | // whose premise did not hold has proved nothing rather than passed. |
| 388 | if (b.strip - a.strip < px - 1 || b.sess >= a.sess || b.sess < MIN_LIST) { missed.push(said); continue; } |
| 389 | if (b.chats !== a.chats || b.list !== a.list || c.list !== a.list) moved9.push(said); |
| 390 | } |
| 391 | } |
| 392 | check('the strip really grew under the rail, six times over', missed.length === 0, |
| 393 | missed.length ? missed.join(' | ') : '37, 54 and 97px at 900 and 1100'); |
| 394 | check('a strip taller by any amount leaves a divider the user set exactly where it was', |
| 395 | moved9.length === 0 && missed.length === 0, moved9.join(' | ')); |
| 396 | |
| 397 | // And when the room really does run out, the Chats list keeps its floor and |
| 398 | // the STORED height is not quietly rewritten to fit: the clamp is on read. |
| 399 | await page.setViewportSize({ width: 1440, height: 900 }); |
| 400 | await sleep(500); |
| 401 | await drag(160); // the divider low, with little left to give |
| 402 | const low = await rail(); |
| 403 | await grow(60); |
| 404 | await sleep(400); |
| 405 | const squeezed = await rail(); |
| 406 | await grow(0); |
| 407 | await sleep(400); |
| 408 | const back = await rail(); |
| 409 | check('squeezed past what it can give, the Chats list keeps a usable height', |
| 410 | squeezed.sess >= MIN_LIST - 1 && squeezed.list < low.list, |
| 411 | `diamonds ${low.list}->${squeezed.list}, chats ${low.sess}->${squeezed.sess}`); |
| 412 | check('and the divider returns to the pixel it was left at, nothing having been rewritten', |
| 413 | back.list === low.list && back.chats === low.chats, |
| 414 | `diamonds ${low.list}->${squeezed.list}->${back.list}`); |
| 415 | await page.setViewportSize({ width: 1440, height: 900 }); |
| 416 | await sleep(400); |
| 417 | } |
| 418 | |
| 419 | const errs = errors(s).filter(e => !/Failed to load resource|status of \d\d\d|favicon|net::ERR_ABORTED/.test(e)); |
| 420 | check('no console errors while the bar was measured', errs.length === 0, errs.slice(0, 3).join(' | ')); |
| 421 | |
| 422 | await s.close(); |
| 423 | console.log(''); |
| 424 | console.log(bad.length ? `${ok.length} passed, ${bad.length} failed` : `ALL ${ok.length} CHECKS PASSED`); |
| 425 | process.exit(bad.length ? 1 : 0); |