oxedyne/daimond/dev/verify_identityfold.mjs
6.5 KiB, 1 run
created by r2519314175:483, 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 | // Can a first-time visitor SEE the button they came here to press? |
| 2 | // |
| 3 | // The question is asked the way a person meets the screen: the card at the top |
| 4 | // of its own scroll, nothing scrolled into view first, and `elementFromPoint` |
| 5 | // over the middle of the button rather than its bounding box. A box is returned |
| 6 | // for an element nobody can see, which is how the identity card came to hold a |
| 7 | // "Create account" button 25px below its own bottom edge on 2026-08-14 with |
| 8 | // every check still green. |
| 9 | // |
| 10 | // It is NOT enough to sample near the button's top edge. A card that overflows |
| 11 | // by less than the button's height leaves the top of it showing, and a check |
| 12 | // that photographs a few pixels there passes on a button whose label is cut in |
| 13 | // half. `verify_contrast_ui` samples at `top + 4` and scrolls the button into |
| 14 | // view before it does -- it is asking "is this pixel really the button", which |
| 15 | // is a fair question about a photograph and a poor one about a fold. Hence this. |
| 16 | // |
| 17 | // The header used to say "L11 scratch ... Not part of the suite. `node |
| 18 | // dev/l11_fold.mjs`". It is `dev/verify_identityfold.mjs`, `run_all.sh` takes |
| 19 | // every `dev/verify_*.mjs` there is, and it has been in the suite since it was |
| 20 | // renamed. It counted its own reds and then exited 0, so the count reached |
| 21 | // nobody. It exits on the count now. |
| 22 | // |
| 23 | // PROVED AGAINST A CARD THAT CANNOT SHOW ITS OWN FOOT FIRST. `--break shortcard` |
| 24 | // caps the card's height, which puts the primary button under the fold at every |
| 25 | // size, and the run is expected to FAIL. |
| 26 | // |
| 27 | // node dev/verify_identityfold.mjs --break shortcard # expected to FAIL |
| 28 | // node dev/verify_identityfold.mjs # and then, clean |
| 29 | import { open } from './harness.mjs'; |
| 30 | |
| 31 | const ok = [], bad = []; |
| 32 | const check = (name, pass, detail) => { |
| 33 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 34 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 35 | return pass; |
| 36 | }; |
| 37 | |
| 38 | const BREAK = (() => { |
| 39 | const i = process.argv.indexOf('--break'); |
| 40 | return i > 0 ? String(process.argv[i + 1] || '') : ''; |
| 41 | })(); |
| 42 | if (BREAK && BREAK !== 'shortcard') { |
| 43 | console.error(`unknown break '${BREAK}'; known: shortcard`); |
| 44 | process.exit(2); |
| 45 | } |
| 46 | |
| 47 | /// The break is injected on EVERY load, because this file reloads the page |
| 48 | /// half way through to meet the returning visitor. |
| 49 | const breakInto = (page) => page.addInitScript(() => { |
| 50 | const put = () => { |
| 51 | const st = document.createElement('style'); |
| 52 | st.textContent = '#identity-modal .modal-card { max-height: 300px !important; overflow-y: auto !important; }'; |
| 53 | document.head.appendChild(st); |
| 54 | }; |
| 55 | if (document.head) put(); else document.addEventListener('DOMContentLoaded', put); |
| 56 | }); |
| 57 | |
| 58 | /// Where the button is, and whether the button is what is painted over its |
| 59 | /// middle -- with the card put back to the top first, because a fold is about |
| 60 | /// what greets somebody, not about what they can reach. |
| 61 | const look = (page) => page.evaluate(() => { |
| 62 | const card = document.querySelector('#identity-modal .modal-card'); |
| 63 | const btn = document.getElementById('id-primary'); |
| 64 | if (!card || !btn) return null; |
| 65 | card.scrollTop = 0; |
| 66 | const cr = card.getBoundingClientRect(); |
| 67 | const br = btn.getBoundingClientRect(); |
| 68 | const hit = document.elementFromPoint(br.left + br.width / 2, br.top + br.height / 2); |
| 69 | return { |
| 70 | card: { top: +cr.top.toFixed(1), bottom: +cr.bottom.toFixed(1) }, |
| 71 | btn: { top: +br.top.toFixed(1), bottom: +br.bottom.toFixed(1), label: (btn.textContent || '').trim() }, |
| 72 | over: card.scrollHeight - card.clientHeight, |
| 73 | seen: !!(hit && (hit === btn || btn.contains(hit))), |
| 74 | by: hit ? hit.tagName.toLowerCase() + (hit.id ? '#' + hit.id : '') : 'nothing', |
| 75 | }; |
| 76 | }); |
| 77 | |
| 78 | const at = async (page, skin, w, h, what) => { |
| 79 | await page.evaluate((sk) => document.documentElement.setAttribute('data-skin', sk), skin); |
| 80 | await page.setViewportSize({ width: w, height: h }); |
| 81 | await page.waitForTimeout(350); |
| 82 | const m = await look(page); |
| 83 | if (!m) return check(`${what}, ${skin} at ${w}x${h}: the card is on screen`, false, 'no card and no #id-primary'); |
| 84 | return check( |
| 85 | `${what}, ${skin} at ${w}x${h}: "${m.btn.label}" is on screen unscrolled`, |
| 86 | m.seen, |
| 87 | `button ${m.btn.top}–${m.btn.bottom}, card edge ${m.card.bottom}, ${m.over}px under the fold` |
| 88 | + (m.seen ? '' : `, ${m.by} is drawn over it`)); |
| 89 | }; |
| 90 | |
| 91 | const s = await open({ name: 'identityfold', signIn: false, defaults: false, route: BREAK ? breakInto : null }); |
| 92 | const { page } = s; |
| 93 | if (BREAK) console.log(`\n*** RUNNING UNDER --break ${BREAK}: failures below are the point ***\n`); |
| 94 | await page.waitForSelector('#id-primary', { timeout: 15000 }); |
| 95 | await page.waitForTimeout(600); |
| 96 | |
| 97 | // ── A stranger, who is shown the front door and the tallest form ── |
| 98 | check('the stranger strip is on the screen being measured', |
| 99 | await page.evaluate(() => !!document.getElementById('id-doors').getClientRects().length), '#id-doors'); |
| 100 | for (const skin of ['sharp', 'warm']) { |
| 101 | await at(page, skin, 1500, 950, 'create'); // the laptop of the report |
| 102 | await at(page, skin, 1280, 800, 'create'); // a smaller one |
| 103 | await at(page, skin, 375, 812, 'create'); // a phone |
| 104 | await at(page, skin, 375, 667, 'create'); // the shortest phone still current |
| 105 | } |
| 106 | |
| 107 | // ── And somebody coming back, who must be no worse off ──────────── |
| 108 | await page.evaluate(() => document.documentElement.setAttribute('data-skin', 'warm')); |
| 109 | await page.setViewportSize({ width: 1500, height: 950 }); |
| 110 | await page.fill('#id-name', 'Returning'); |
| 111 | await page.evaluate(() => { const w = document.getElementById('id-wrote'); if (w && !w.checked) w.click(); }); |
| 112 | await page.waitForTimeout(150); |
| 113 | await page.locator('#id-primary').click(); |
| 114 | await page.waitForTimeout(4000); |
| 115 | await page.keyboard.press('Escape'); |
| 116 | await page.waitForTimeout(300); |
| 117 | await page.reload(); |
| 118 | await page.waitForSelector('#id-primary', { timeout: 15000 }); |
| 119 | await page.waitForTimeout(2000); |
| 120 | const mode = await page.evaluate(() => document.getElementById('identity-modal').dataset.mode); |
| 121 | check('the return visit is an unlock screen', mode === 'unlock', `mode=${mode}`); |
| 122 | for (const skin of ['sharp', 'warm']) { |
| 123 | await at(page, skin, 1500, 950, 'unlock'); |
| 124 | await at(page, skin, 375, 667, 'unlock'); |
| 125 | } |
| 126 | // The unlock card is short enough not to scroll at all, which is what makes the |
| 127 | // sticky foot inert there rather than merely harmless. |
| 128 | const un = await look(page); |
| 129 | check('and it does not scroll, so the foot never leaves its place', |
| 130 | !!un && un.over === 0, un ? `${un.over}px under the fold` : 'no card'); |
| 131 | |
| 132 | await s.close(); |
| 133 | console.log(`\n${ok.length} passed, ${bad.length} failed`); |
| 134 | if (bad.length) { bad.forEach(x => console.log(' FAILED: ' + x)); process.exit(1); } |