oxedyne/daimond/dev/verify_webkit.mjs
7.7 KiB, 1 run
created by r2519314175:805, 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_webkit.mjs — the mobile UX walk, on the real Safari (WebKit) engine. |
| 2 | // |
| 3 | // iPhones run WebKit, not Chromium; "Chrome for iOS" is Safari underneath. This |
| 4 | // drives the same pathways as verify_mobile_ux.mjs but on Playwright's WebKit |
| 5 | // build with an iPhone viewport, so Safari-only rendering/JS quirks that Chromium |
| 6 | // tolerates get caught. It is DESKTOP WebKit on Linux, not iOS, so it may not |
| 7 | // reproduce iOS-only behaviour (auto-zoom, address-bar vh) — the zoom PROBE below |
| 8 | // reports whether this build does; treat that line as a diagnostic, not a gate. |
| 9 | import os from 'node:os'; |
| 10 | import path from 'node:path'; |
| 11 | import { pathToFileURL } from 'node:url'; |
| 12 | import { signInAs, connectMock, APP, scratch } from './harness.mjs'; |
| 13 | |
| 14 | // This host (Ubuntu 25.10) is newer than Playwright's WebKit build targets, so |
| 15 | // its dependency preflight refuses to launch; the real runtime libs are supplied |
| 16 | // out-of-band (see dev/setup-webkit-libs.sh). Skip the preflight. |
| 17 | process.env.PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = '1'; |
| 18 | |
| 19 | const PW = process.env.DAIMOND_PW || path.join(os.homedir(), '.red-pw/node_modules/playwright-core/index.mjs'); |
| 20 | const { webkit, devices } = await import(pathToFileURL(PW).href); |
| 21 | |
| 22 | const ok = [], bad = []; |
| 23 | const check = (name, pass, detail) => { |
| 24 | (pass ? ok : bad).push(name); |
| 25 | console.log((pass ? ' ok ' : ' FAIL ') + name + (!pass && detail ? ' — ' + detail : '')); |
| 26 | }; |
| 27 | const sleep = ms => new Promise(r => setTimeout(r, ms)); |
| 28 | |
| 29 | // The iPhone descriptor, minus `isMobile` (Chromium-only; WebKit rejects it). |
| 30 | const iph = devices['iPhone 13']; |
| 31 | // WebKit is the one engine this host cannot be relied on to run: the WPE build |
| 32 | // is compiled for an older Ubuntu, and even with its runtime libraries supplied |
| 33 | // out-of-band the MiniBrowser sometimes launches and never completes |
| 34 | // Playwright's handshake. That is a missing ENGINE, not a failing product, so it |
| 35 | // SKIPS rather than fails -- a red suite that means "the host lacks a browser" |
| 36 | // teaches everyone to ignore red. `verify_style` reports the same absence the |
| 37 | // same way. |
| 38 | let ctx; |
| 39 | try { |
| 40 | ctx = await webkit.launchPersistentContext(scratch(`wk-${process.pid}`), { |
| 41 | viewport: iph.viewport, |
| 42 | deviceScaleFactor: iph.deviceScaleFactor, |
| 43 | userAgent: iph.userAgent, |
| 44 | hasTouch: iph.hasTouch, |
| 45 | timeout: 45000, |
| 46 | }); |
| 47 | } catch (e) { |
| 48 | console.log('SKIPPED: WebKit could not be launched on this host — ' |
| 49 | + (e && e.message ? e.message.split('\n')[0] : e)); |
| 50 | console.log(' (run dev/setup-webkit-libs.sh after any `playwright install webkit`;' |
| 51 | + ' Safari coverage is unverified until this launches)'); |
| 52 | process.exit(0); |
| 53 | } |
| 54 | const page = ctx.pages()[0] || await ctx.newPage(); |
| 55 | const errs = []; |
| 56 | page.on('console', m => { if (m.type() === 'error') errs.push(m.text()); }); |
| 57 | page.on('pageerror', e => errs.push('pageerror: ' + e.message)); |
| 58 | |
| 59 | const s = { browser: ctx, page, errs, logs: [], name: 'wktester', close: () => ctx.close() }; |
| 60 | await page.goto(APP, { waitUntil: 'domcontentloaded' }); |
| 61 | await sleep(400); |
| 62 | |
| 63 | // Playwright's WebKitGTK/WPE build ships WITHOUT `navigator.storage` (OPFS), so |
| 64 | // the wasm's getDirectory() call throws here. Real iOS Safari 15.2+ HAS OPFS, so |
| 65 | // this is a test-engine gap, not a device bug — filter it. (The wasm could still |
| 66 | // feature-detect OPFS defensively; tracked separately.) |
| 67 | const realErrs = () => errs.filter(e => |
| 68 | !/Failed to load resource|status of 4\d\d/.test(e) && |
| 69 | !/getDirectory|navigator\.storage/.test(e)); |
| 70 | |
| 71 | // ── Engine sanity ────────────────────────────────────────── |
| 72 | check('engine: is WebKit', await page.evaluate(() => /WebKit/.test(navigator.userAgent) && !/Chrome\//.test(navigator.userAgent)), await page.evaluate(() => navigator.userAgent)); |
| 73 | |
| 74 | // ── The zoom PROBE (diagnostic) ──────────────────────────── |
| 75 | // Does this WebKit reproduce iOS auto-zoom on a sub-16px focus? Shrink the |
| 76 | // passphrase box, focus it, and read the visual-viewport scale. |
| 77 | const zoom = await page.evaluate(async () => { |
| 78 | const el = document.getElementById('id-pass') || document.querySelector('input'); |
| 79 | if (!el) return { probed: false }; |
| 80 | el.style.fontSize = '11px'; |
| 81 | const before = window.visualViewport ? window.visualViewport.scale : 1; |
| 82 | el.focus(); |
| 83 | await new Promise(r => setTimeout(r, 300)); |
| 84 | const after = window.visualViewport ? window.visualViewport.scale : 1; |
| 85 | el.blur(); el.style.fontSize = ''; |
| 86 | return { probed: true, before, after, zoomed: after > before + 0.01 }; |
| 87 | }); |
| 88 | console.log(' probe zoom-on-small-input:', JSON.stringify(zoom)); |
| 89 | |
| 90 | // ── The UX pathways, on Safari's engine ──────────────────── |
| 91 | await signInAs(s, 'wktester'); |
| 92 | await connectMock(s); |
| 93 | await page.evaluate(() => { try { window.DaimondAdmin.closeModal(); } catch (e) {} }); |
| 94 | await sleep(300); |
| 95 | |
| 96 | const smallInputs = async () => page.evaluate(() => { |
| 97 | const out = []; |
| 98 | document.querySelectorAll('input, textarea, select').forEach(el => { |
| 99 | if (el.offsetParent === null) return; |
| 100 | const fs = parseFloat(getComputedStyle(el).fontSize); |
| 101 | if (fs < 16) out.push((el.id || el.className) + '=' + fs); |
| 102 | }); |
| 103 | return out; |
| 104 | }); |
| 105 | check('safari: all visible fields ≥16px', (await smallInputs()).length === 0, JSON.stringify(await smallInputs())); |
| 106 | |
| 107 | check('safari: shell present', await page.evaluate(() => !!window.DaimondSheet && !!window.DaimondShell)); |
| 108 | |
| 109 | // Drawer |
| 110 | await page.evaluate(() => document.getElementById('drawer-btn').click()); |
| 111 | await sleep(300); |
| 112 | check('safari: drawer opaque', await page.evaluate(() => { |
| 113 | const bg = getComputedStyle(document.getElementById('panel-rail')).backgroundColor; |
| 114 | return bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent'; |
| 115 | })); |
| 116 | |
| 117 | // Credits modal |
| 118 | await page.evaluate(() => document.getElementById('astat-account').click()); |
| 119 | await sleep(400); |
| 120 | // The title and the × belong to the modal's own head, built into |
| 121 | // #settings-slot when a view is hosted there -- the views stopped carrying |
| 122 | // heads of their own when the rail split into a Status strip and one drawer head. |
| 123 | check('safari: credits titled "Credits"', await page.evaluate(() => { |
| 124 | const t = document.querySelector('#settings-slot .admin-view-head .admin-title'); |
| 125 | return t && /credit/i.test(t.textContent) && t.offsetParent !== null; |
| 126 | })); |
| 127 | check('safari: credits card fits viewport', await page.evaluate(() => { |
| 128 | const r = document.querySelector('#settings-modal .modal-card').getBoundingClientRect(); |
| 129 | return r.top >= -1 && r.bottom <= window.innerHeight + 1; |
| 130 | }), await page.evaluate(() => { const r = document.querySelector('#settings-modal .modal-card').getBoundingClientRect(); return `bottom=${Math.round(r.bottom)} vh=${window.innerHeight}`; })); |
| 131 | check('safari: credits has top-right ×', await page.evaluate(() => { |
| 132 | const x = document.querySelector('#settings-slot .admin-view-head .admin-back'); |
| 133 | return x && x.offsetParent !== null; |
| 134 | })); |
| 135 | await page.evaluate(() => document.querySelector('#settings-slot .admin-view-head .admin-back').click()); |
| 136 | await sleep(300); |
| 137 | |
| 138 | // A thing rises as a sheet, at a real height (Safari's flexbox/height engine) |
| 139 | await page.evaluate(() => { window.DaimondPanels.hide('web'); window.DaimondPanels.show('web'); }); |
| 140 | await sleep(400); |
| 141 | check('safari: sheet raised to a real height', await page.evaluate(() => |
| 142 | document.getElementById('msheet').getBoundingClientRect().height > 120)); |
| 143 | check('safari: ask pill on screen', await page.evaluate(() => { |
| 144 | const r = document.getElementById('msheet-ask').getBoundingClientRect(); |
| 145 | return r.height > 0 && r.bottom <= window.innerHeight + 1; |
| 146 | })); |
| 147 | |
| 148 | check('safari: no console errors', realErrs().length === 0, realErrs().slice(0, 3).join(' | ')); |
| 149 | |
| 150 | console.log(`\n${ok.length} passed, ${bad.length} failed (engine: WebKit)`); |
| 151 | if (bad.length) console.log('FAILED: ' + bad.join(', ')); |
| 152 | await ctx.close(); |
| 153 | process.exit(bad.length ? 1 : 0); |