// 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=
(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 }));` },
];