Oregami
Repositories/oxedyne/daimond

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.
9import os from 'node:os';
10import path from 'node:path';
11import { pathToFileURL } from 'node:url';
12import { 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.
17process.env.PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = '1';
18
19const PW = process.env.DAIMOND_PW || path.join(os.homedir(), '.red-pw/node_modules/playwright-core/index.mjs');
20const { webkit, devices } = await import(pathToFileURL(PW).href);
21
22const ok = [], bad = [];
23const check = (name, pass, detail) => {
24 (pass ? ok : bad).push(name);
25 console.log((pass ? ' ok ' : ' FAIL ') + name + (!pass && detail ? ' — ' + detail : ''));
26};
27const sleep = ms => new Promise(r => setTimeout(r, ms));
28
29// The iPhone descriptor, minus `isMobile` (Chromium-only; WebKit rejects it).
30const 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.
38let ctx;
39try {
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}
54const page = ctx.pages()[0] || await ctx.newPage();
55const errs = [];
56page.on('console', m => { if (m.type() === 'error') errs.push(m.text()); });
57page.on('pageerror', e => errs.push('pageerror: ' + e.message));
58
59const s = { browser: ctx, page, errs, logs: [], name: 'wktester', close: () => ctx.close() };
60await page.goto(APP, { waitUntil: 'domcontentloaded' });
61await 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.)
67const 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 ──────────────────────────────────────────
72check('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.
77const 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});
88console.log(' probe zoom-on-small-input:', JSON.stringify(zoom));
89
90// ── The UX pathways, on Safari's engine ────────────────────
91await signInAs(s, 'wktester');
92await connectMock(s);
93await page.evaluate(() => { try { window.DaimondAdmin.closeModal(); } catch (e) {} });
94await sleep(300);
95
96const 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});
105check('safari: all visible fields ≥16px', (await smallInputs()).length === 0, JSON.stringify(await smallInputs()));
106
107check('safari: shell present', await page.evaluate(() => !!window.DaimondSheet && !!window.DaimondShell));
108
109// Drawer
110await page.evaluate(() => document.getElementById('drawer-btn').click());
111await sleep(300);
112check('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
118await page.evaluate(() => document.getElementById('astat-account').click());
119await 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.
123check('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}));
127check('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}`; }));
131check('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}));
135await page.evaluate(() => document.querySelector('#settings-slot .admin-view-head .admin-back').click());
136await sleep(300);
137
138// A thing rises as a sheet, at a real height (Safari's flexbox/height engine)
139await page.evaluate(() => { window.DaimondPanels.hide('web'); window.DaimondPanels.show('web'); });
140await sleep(400);
141check('safari: sheet raised to a real height', await page.evaluate(() =>
142 document.getElementById('msheet').getBoundingClientRect().height > 120));
143check('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
148check('safari: no console errors', realErrs().length === 0, realErrs().slice(0, 3).join(' | '));
149
150console.log(`\n${ok.length} passed, ${bad.length} failed (engine: WebKit)`);
151if (bad.length) console.log('FAILED: ' + bad.join(', '));
152await ctx.close();
153process.exit(bad.length ? 1 : 0);