aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/viewer/tools/smoke/mobile.mjs
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
committergodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
commit3443c1c65e9f1753e1e656b35d08416c1fa298f2 (patch)
tree4e43236f460145a4d75d1b4616dcb7aa6ef08f51 /viewer/tools/smoke/mobile.mjs
downloadworldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.tar.gz
worldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.zip
worldmap-viewer: initial public history
Diffstat (limited to 'viewer/tools/smoke/mobile.mjs')
-rw-r--r--viewer/tools/smoke/mobile.mjs48
1 files changed, 48 insertions, 0 deletions
diff --git a/viewer/tools/smoke/mobile.mjs b/viewer/tools/smoke/mobile.mjs
new file mode 100644
index 0000000..6c94dee
--- /dev/null
+++ b/viewer/tools/smoke/mobile.mjs
@@ -0,0 +1,48 @@
+// mobile.mjs — the viewer on a phone screen: controls fit without overlapping, a tap opens the inspector sheet,
+// the ☰ menu holds the layer picker, map buttons are finger-sized. SHOTS=<dir> (default /tmp): screenshots.
+// LANDSCAPE=1: the phone on its side.
+const dir = process.env.SHOTS || '/tmp';
+const land = !!process.env.LANDSCAPE;
+const tag = land ? 'landscape' : 'portrait';
+export const viewport = land ? { width: 844, height: 390 } : { width: 390, height: 844 };
+const overlap = `const vis = id => { const e = document.getElementById(id); if (!e || e.hidden) return null;
+ const b = e.getBoundingClientRect(); return b.width && b.height && getComputedStyle(e).display !== 'none' ? b : null; };
+ const ids = ['toolbar', 'panel', 'nav', 'status', 'legend', 'exag-box', 'skybar'].filter(vis);
+ const bad = [];
+ for (let i = 0; i < ids.length; i++) for (let j = i + 1; j < ids.length; j++) {
+ const a = vis(ids[i]), b = vis(ids[j]);
+ if (a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom) bad.push(ids[i] + 'x' + ids[j]);
+ }
+ const off = ids.filter(id => { const b = vis(id); return b.left < 0 || b.right > innerWidth + 1 || b.top < 0 || b.bottom > innerHeight + 1; });
+ const covered = ids.reduce((s, id) => { const b = vis(id); return s + b.width * b.height; }, 0) / (innerWidth * innerHeight);
+ return { bad, off, covered: +covered.toFixed(2) };`;
+const tap = `const c = document.querySelector('#map canvas'), b = c.getBoundingClientRect(), x = b.left + b.width * 0.3, y = b.top + b.height * 0.35;
+ for (const t of ['pointerdown', 'pointerup']) c.dispatchEvent(new PointerEvent(t, { clientX: x, clientY: y, bubbles: true, pointerId: 1, pointerType: 'touch', isPrimary: true, button: 0 }));
+ c.dispatchEvent(new MouseEvent('click', { clientX: x, clientY: y, bubbles: true }));
+ await new Promise(r => setTimeout(r, 1500));`;
+export const steps = [
+ { name: 'boots on a phone', wait: 5000, expect: v => v.ok && !v.toast && v.compact,
+ js: `return { ok: !!window.mapViewer?.texture, toast: document.getElementById('toast').textContent,
+ compact: document.body.classList.contains('compact') };` },
+ { name: `${tag}: controls do not overlap or leave the screen, the map stays visible`, shot: `${dir}/phone-${tag}.png`,
+ expect: v => !v.bad.length && !v.off.length && v.covered < 0.35, js: overlap },
+ { name: 'tap the map: the inspector shows', shot: `${dir}/phone-${tag}-inspect.png`, expect: v => v.visible,
+ js: `${tap}
+ const s = document.getElementById('tab-inspector'), r = s.getBoundingClientRect();
+ return { visible: !s.hidden && r.height > 40 && r.top < innerHeight - 40, text: s.textContent.slice(0, 40) };` },
+ { name: 'panel open: still no overlaps', expect: v => !v.bad.length && !v.off.length, js: overlap },
+ { name: 'the menu holds the layer picker and fits the screen', shot: `${dir}/phone-${tag}-menu.png`,
+ expect: v => v.layer && v.fits,
+ js: `document.getElementById('menu-toggle').click(); await new Promise(r => setTimeout(r, 300));
+ const l = document.getElementById('layer').getBoundingClientRect(), t = document.getElementById('toolbar').getBoundingClientRect();
+ return { layer: l.width > 150 && l.height >= 36, fits: t.bottom <= innerHeight };` },
+ { name: 'a tap on the map closes the menu', expect: v => v === false,
+ js: `const c = document.querySelector('#map canvas');
+ c.dispatchEvent(new PointerEvent('pointerdown', { clientX: 200, clientY: 300, bubbles: true, pointerId: 2, pointerType: 'touch' }));
+ c.dispatchEvent(new PointerEvent('pointerup', { clientX: 200, clientY: 300, bubbles: true, pointerId: 2, pointerType: 'touch' }));
+ return document.body.classList.contains('menu-open');` },
+ { name: 'layer textures fit a phone GPU (≤ 4096 px wide)', expect: v => v > 0 && v <= 4096,
+ js: `return window.mapViewer.texture?.image?.width ?? 0;` },
+ { name: 'map buttons are finger-sized', expect: v => v.length >= 4 && v.every(b => b.w >= 40 && b.h >= 40),
+ js: `return [...document.querySelectorAll('#nav button')].filter(b => b.offsetParent).map(b => ({ w: b.offsetWidth, h: b.offsetHeight }));` },
+];