/** * Dev-only: repro + regression check for the MAP console's object click. * * Scenario under test: * 1. open the map console (deck MAP button) * 2. hover a discovered object on the chart → the tooltip * (name + "TAP TO PLOT COURSE") must be part of the window * 3. click it → the ENGAGE AUTOPILOT confirm opens (autopilot is a * commitment, so it's confirmed before it engages): * - CANCEL / scrim / ESC → map stays open, no course * - ENGAGE → course plotted (ship.target) AND the map closes * While the confirm is up the plate is inert (no zoom/hover/click). * 4. close the map → NOTHING of the tooltip may linger on screen * * The known bug: ttPlate/ttName/ttSub/ttHint are created with * scene.add() but never added to the MapWindow container (only the * EMPTY ttCont is) — so they paint at depth 5, BEHIND the window * (depth 80), and survive close() (nothing ever hides/clears them). * * node dev/server.mjs 8091 * node dev/shot-firefox.mjs \ * "http://127.0.0.1:8091/dev/map-click.html" \ * "window.__MAPCLICK ? window.__MAPCLICK.ready : null" \ * /tmp/map-click.png 30000 */ import Phaser from '../js/vendor/phaser.js'; import { config } from '../js/config/Config.js'; import { ConfigLoader } from '../js/config/ConfigLoader.js'; import { createGameConfig } from '../js/config/GameConfig.js'; import { GameScene } from '../js/scenes/GameScene.js'; const data = await ConfigLoader.load(); config.init(data); const errors = []; const origErr = console.error.bind(console); console.error = (...a) => { errors.push(a.map(String).join(' ')); origErr(...a); }; window.addEventListener('error', (e) => errors.push(String(e.message))); window.addEventListener('unhandledrejection', (e) => errors.push(`rejection: ${e.reason}`)); const gameConfig = createGameConfig(); gameConfig.scene = [GameScene]; const game = new Phaser.Game(gameConfig); window.game = game; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function waitScene() { for (let i = 0; i < 100; i++) { const s = game.scene.getScene('GameScene'); if (s && s.ship && s.mapWindow) return s; await sleep(100); } throw new Error('GameScene never booted'); } /** Walk up the container parents to the object's top-level display-list ancestor. */ function topLevel(obj) { let top = obj; let c = obj.parentContainer; // Phaser 4: container children track their container here while (c) { top = c; c = c.parentContainer; } return top; } /** Effective (chained) visibility down to the top-level object. */ function effVisible(obj) { let v = true; let o = obj; while (o) { v = v && o.visible !== false; o = o.parentContainer; } return v; } try { const s = await waitScene(); await sleep(800); // Discover the central body + first planet + first station so the // chart (and _hits) has something to point at. const sysId = s.systemRecord.id; let known = s.discovery.bySystem.get(sysId); if (!known) { known = new Set(); s.discovery.bySystem.set(sysId, known); } const push = (obj) => { if (obj && !known.has(obj.id)) s.discovery.check(sysId, obj.x, obj.y, [obj]); }; if (s.isHomeSystem && s.planet) push({ id: 'home', x: 0, y: 0, radius: s.planet.radius }); if (s.systemPlanets[0]) push({ id: s.systemPlanets[0].discoveryId, x: s.systemPlanets[0].x, y: s.systemPlanets[0].y, radius: s.systemPlanets[0].radius }); if (s.systemStations[0]) push({ id: s.systemStations[0].discoveryId, x: s.systemStations[0].x, y: s.systemStations[0].y, radius: s.systemStations[0].bound ?? 60 }); // Open the console, wait for the reveal + first paint. s.deckAction('map'); await sleep(1800); const win = s.mapWindow; if (!win.isOpen) throw new Error('map window did not open'); // Force a fresh snapshot so _hits is populated. const snap = win._snapOf(); if (snap && win._fp !== win._fpOf(snap)) win._applySnap(snap); if (!win._hits || win._hits.length === 0) throw new Error('no hits on the chart'); const hit = win._hits[0]; const hitLabel = String(hit.label ?? hit.id).toUpperCase(); const sx = win.geo.mapX + hit.x; const sy = win.geo.mapY + hit.y; const dl = s.sys.displayList.getChildren(); const iWin = dl.indexOf(win); const iMapImg = dl.indexOf(win.mapImg); const describe = (o, label) => { const top = topLevel(o); const iTop = dl.indexOf(top); return { label, visible: o.visible, effVisible: effVisible(o), inWindow: top === win, topInDisplayList: top === win ? 'the map window' : (iTop >= 0 ? `${top.type || top.constructor.name} @ dl[${iTop}] (win=${iWin})` : 'NOT in display list'), paintsAboveMap: top === win || (iTop > iWin), }; }; // Real-input helpers (shared by the dialog + zoom + pan sections) const frames = () => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))); const sm = win.scene.scale; const cb = sm.canvasBounds; const cbL = cb.left ?? cb.x, cbT = cb.top ?? cb.y; const toClient = (gx, gy) => [cbL + gx / sm.displayScale.x, cbT + gy / sm.displayScale.y]; const canvas = win.scene.game.canvas; const realPress = async (gx, gy) => { const [cx, cy] = toClient(gx, gy); for (const type of ['mousedown', 'mouseup']) { canvas.dispatchEvent(new MouseEvent(type, { clientX: cx, clientY: cy, bubbles: true, cancelable: true, button: 0 })); await frames(); } }; // ---- 1: hover the object ------------------------------------------------ win._setHover({ x: sx, y: sy }); await sleep(120); const before = { ttPlate: describe(win.ttPlate, 'ttPlate'), ttName: describe(win.ttName, 'ttName'), ttHint: describe(win.ttHint, 'ttHint'), ttCont: describe(win.ttCont, 'ttCont'), hoverG: describe(win.hoverG, 'hoverG'), hover: !!win._hover, hoverId: win._hover?.id ?? null, ttNameText: win.ttName.text, mapImg: { inDL: iMapImg >= 0, interactive: !!(win.mapImg.input && win.mapImg.input.enabled) }, }; // ---- 1b: hover decor is drawn in WORLD coordinates --------------------- // hits are PLATE-LOCAL (plate top-left = 0,0), hoverG draws in world // space, and the plate sits at geo.mapX/mapY — so the ring, the course // line's destination end and the dot must all be offset to world. (This // used to be forgotten for the ring/line/dot: the ship end was correct, // the destination end landed at plate-local coords, off by mapX/mapY.) const hoverDraw = (() => { const g = win.hoverG; const circles = []; const lines = []; const origCircle = g.strokeCircle.bind(g); const origLine = g.strokeLineShape.bind(g); g.strokeCircle = (x, y, r) => { circles.push([x, y, r]); return origCircle(x, y, r); }; g.strokeLineShape = (shape) => { lines.push(shape); return origLine(shape); }; g.clear(); win._paintHover(); g.strokeCircle = origCircle; g.strokeLineShape = origLine; const ex = win.geo.mapX + hit.x; const ey = win.geo.mapY + hit.y; const ring = circles.find((c) => Math.abs(c[2] - (hit.r + 5)) < 1e-6); const ln = lines[0]; return { ex, ey, ringAtDest: !!ring && Math.abs(ring[0] - ex) < 0.5 && Math.abs(ring[1] - ey) < 0.5, lineEndsAtDest: !!ln && Math.abs(ln.x2 - ex) < 0.5 && Math.abs(ln.y2 - ey) < 0.5, lineShipEnd: ln ? [ln.x1, ln.y1] : null, ring: ring ?? null, }; })(); // ---- 2: click the object → the ENGAGE AUTOPILOT confirm opens --------- // (autopilot is a commitment — the dialog asks BEFORE onSelect fires) const dlg = win.dialog; const targetBefore = s.ship.target; win.mapImg.emit('pointerdown', { x: sx, y: sy }); win.mapImg.emit('pointerup', {}); await sleep(700); // open anim (~170ms) + title decode (420ms) const zDlg = win._view.z; const [iw, iwY] = toClient(win.geo.mapX + win.geo.mapW / 2, win.geo.mapY + win.geo.mapH / 2); const evDlg = new WheelEvent('wheel', { clientX: iw, clientY: iwY, deltaY: -300, bubbles: true, cancelable: true }); canvas.dispatchEvent(evDlg); // plate-inert probe: this must NOT zoom await frames(); const dialog = { isOpen: dlg.isOpen, title: dlg.title.text, body: dlg.bodyTexts.map((t) => t.text), confirmLabel: dlg.confirmBtn.labelText.text, targetUnchanged: s.ship.target === targetBefore, plateInertWheel: win._view.z === zDlg, }; // ---- 2b: CANCEL keeps the map open, plots no course --------------------- dlg.cancel(); await sleep(300); // close anim (~130ms) const dialogCancelled = { hidden: dlg.state === 'hidden', mapStillOpen: win.isOpen, targetUnchanged: s.ship.target === targetBefore, }; // ---- 2c: ESC cancels the confirm (map stays open) ----------------------- win._setHover({ x: sx, y: sy }); win.mapImg.emit('pointerdown', { x: sx, y: sy }); win.mapImg.emit('pointerup', {}); await sleep(250); // enough for state === 'opening' (isOpen true) const escSeen = []; if (s.input.keyboard) { for (const name of ['keydown-ESC', 'keydown-Escape', 'keydown-ESCAPE']) { s.input.keyboard.once(name, () => escSeen.push(name)); } } window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true, cancelable: true })); await sleep(300); const dialogEsc = { hidden: dlg.state === 'hidden', mapStillOpen: win.isOpen, targetUnchanged: s.ship.target === targetBefore, escEventSeen: escSeen, }; // a click on EMPTY plate must not retarget (and opens no dialog) s.ship.target = null; win.mapImg.emit('pointerdown', { x: win.geo.mapX + 4, y: win.geo.mapY + 4 }); win.mapImg.emit('pointerup', {}); await sleep(120); const missClick = { targetStayedNull: s.ship.target === null, noDialog: !dlg.isOpen }; // ---- 2d: REAL pipeline: canvas click over the object opens the confirm; // a real click on ENGAGE confirms (course + map closes) --------- // The v3→v4 port bug lived here: mapImg's hit rect was in world-plate // coords, but v4 hit tests in the image's UNSCALED frame space — so // only the plate's top-left quarter was actually clickable. const hitPl = s.sys.input.hitTestPointer({ x: sx, y: sy }); const realHit = hitPl.includes(win.mapImg); s.ship.target = null; await realPress(sx, sy); await sleep(250); const realDialog = dlg.isOpen; // ENGAGE button (dialog-local w/2-84, h/2-24 — real click on it) const bx = dlg.x + dlg.confirmBtn.x; const by = dlg.y + dlg.confirmBtn.y; await realPress(bx, by); await sleep(600); // confirm close (~130ms) + map close (~150ms) const realConfirm = { targetWasSet: s.ship.target !== null && s.ship.target !== targetBefore, target: s.ship.target ? { x: Math.round(s.ship.target.x), y: Math.round(s.ship.target.y) } : null, mapClosed: win.openState === 'closed', dialogHidden: dlg.state === 'hidden', }; // re-open the console for the zoom / pan sections win.open(); await sleep(1800); // ---- 2e: wheel zoom ---------------------------------------------------- const geo = win.geo; const center = { x: geo.mapW / 2, y: geo.mapH / 2 }; const baseScale = win._tf.scale; const worldAt = (pt) => ({ x: (pt.x - win._tf.ox) / win._tf.scale, y: (pt.y - win._tf.oy) / win._tf.scale }); // (a) 2× about the plate centre — crisp repaint, cursor point anchored const wAnchor = worldAt(center); win._zoomAt(center.x, center.y, 2); await frames(); const zoomIn = { z: win._view.z, scale: win._tf.scale, anchor: worldAt(center) }; // (b)/(c) clamping: below min → 1×, above max → 8× win._zoomAt(center.x, center.y, 0.001); await frames(); const clampedMin = win._view.z; win._zoomAt(center.x, center.y, 999); await frames(); const clampedMax = win._view.z; // (d) hit accuracy at 4×: the object nearest the anchor, hover + click it win._view = { z: 4, cx: wAnchor.x, cy: wAnchor.y }; win._saveView(); win._queueRedraw(); await frames(); let zHit = null, zBest = Infinity; for (const h of win._hits) { const w = worldAt({ x: h.x, y: h.y }); const d = Math.hypot(w.x - wAnchor.x, w.y - wAnchor.y); if (d < zBest) { zBest = d; zHit = h; } } const zoomHit = { id: zHit?.id ?? null, objectAt: win._objectAt(win._platePoint({ x: geo.mapX + zHit?.x, y: geo.mapY + zHit?.y }))?.id === zHit?.id, }; s.ship.target = null; win.mapImg.emit('pointerdown', { x: geo.mapX + zHit.x, y: geo.mapY + zHit.y }); win.mapImg.emit('pointerup', {}); await sleep(200); zoomHit.clickOpensDialog = dlg.isOpen; dlg.cancel(); // cancel to restore the plate (the confirm is topmost while up) await sleep(300); // (e) double-tap EMPTY plate → reset to 1× let dblTap = { reset: false, tried: false }; for (const c of [{ x: 6, y: 6 }, { x: geo.mapW - 6, y: 6 }, { x: 6, y: geo.mapH - 6 }, { x: geo.mapW - 6, y: geo.mapH - 6 }]) { if (win._objectAt(c)) continue; // find a truly empty corner dblTap.tried = true; s.ship.target = null; win.mapImg.emit('pointerdown', { x: geo.mapX + c.x, y: geo.mapY + c.y }); win.mapImg.emit('pointerup', {}); await sleep(30); win.mapImg.emit('pointerdown', { x: geo.mapX + c.x, y: geo.mapY + c.y }); win.mapImg.emit('pointerup', {}); await frames(); dblTap.reset = win._view.z === 1; break; } // (f) the REAL wheel path (DOM WheelEvent → Phaser input plugin → // the plate's `wheel` event): over the plate zooms in; over the // chrome neither zooms nor targets the plate. const zBeforeWheel = win._view.z; const [wx1, wy1] = toClient(geo.mapX + geo.mapW / 2, geo.mapY + geo.mapH / 2); const evIn = new WheelEvent('wheel', { clientX: wx1, clientY: wy1, deltaY: -300, bubbles: true, cancelable: true }); canvas.dispatchEvent(evIn); await frames(); const wheelIn = { prevented: evIn.defaultPrevented, z: win._view.z, moved: win._view.z !== zBeforeWheel }; const [wx2, wy2] = toClient(30, 20); // title-bar area, off the plate const evOut = new WheelEvent('wheel', { clientX: wx2, clientY: wy2, deltaY: -300, bubbles: true, cancelable: true }); canvas.dispatchEvent(evOut); await frames(); const wheelOut = { prevented: evOut.defaultPrevented, z: win._view.z }; // (f2) DRAG PAN (the click now fires on release, so a press that moves // past the slop pans instead of clicking). const vSave = { ...win._view }; const restoreView = async () => { win._view = { ...vSave }; win._saveView(); win._queueRedraw(); await frames(); }; const pan = {}; { // set the view EXACTLY (a direct 2× about the current centre), not via // the relative factor, so the assertions are exact const wC = worldAt(center); win._view = { z: 2, cx: wC.x, cy: wC.y }; win._saveView(); win._queueRedraw(); await frames(); const c0 = { ...win._view }; const sc = win._tf.scale; const A = { x: center.x - 60, y: center.y - 40 }; win.mapImg.emit('pointerdown', { x: A.x, y: A.y }); win.mapImg.emit('pointermove', { x: A.x + 30, y: A.y + 20 }); win.mapImg.emit('pointerup', {}); await frames(); pan.z = win._view.z; pan.dx = Math.abs(win._view.cx - (c0.cx - 30 / sc)) * sc; pan.dy = Math.abs(win._view.cy - (c0.cy - 20 / sc)) * sc; pan.noTarget = s.ship.target === null; pan.hoverCleared = !win._hover; // hard drag left (2000px — past the frame edge, whatever the size) → // the centre clamps to the frame's right edge const B = { x: center.x, y: center.y }; win.mapImg.emit('pointerdown', { x: B.x, y: B.y }); win.mapImg.emit('pointermove', { x: B.x - 2000, y: B.y }); win.mapImg.emit('pointerup', {}); await frames(); const full = win._full; const vw = full.w / win._view.z; const edge = full.cx + (full.w - vw) / 2; pan.clampedPx = Math.abs(win._view.cx - edge) * sc; pan.clamped = pan.clampedPx < 1e-3; pan.edgeSlip = Math.abs(win._view.cx - edge); // world units beyond the edge (want 0) // a drag that STARTS on an object must not select it (or confirm it) s.ship.target = null; const h0 = win._hits[0]; win.mapImg.emit('pointerdown', { x: geo.mapX + h0.x, y: geo.mapY + h0.y }); win.mapImg.emit('pointermove', { x: geo.mapX + h0.x + 24, y: geo.mapY + h0.y }); win.mapImg.emit('pointerup', {}); await frames(); pan.dragNoSelect = s.ship.target === null && !dlg.isOpen; // the REAL input pipeline: mousedown → mousemove → mouseup const R = { x: center.x + 40, y: center.y }; const [r0x, r0y] = toClient(R.x, R.y); const kx = 20 / sm.displayScale.x, ky = 12 / sm.displayScale.y; const cB = { ...win._view }; const scB = win._tf.scale; canvas.dispatchEvent(new MouseEvent('mousedown', { clientX: r0x, clientY: r0y, bubbles: true, cancelable: true, button: 0 })); await frames(); canvas.dispatchEvent(new MouseEvent('mousemove', { clientX: r0x + kx, clientY: r0y + ky, bubbles: true, cancelable: true })); await frames(); canvas.dispatchEvent(new MouseEvent('mouseup', { clientX: r0x + kx, clientY: r0y + ky, bubbles: true, cancelable: true })); await frames(); pan.realDx = Math.abs(win._view.cx - (cB.cx - 20 / scB)) * scB; pan.realDy = Math.abs(win._view.cy - (cB.cy - 12 / scB)) * scB; await restoreView(); } // (g) per-system memory const viewMemory = win._views.has(win._snap?.systemId); // ?keepOpen=1 — stop here (map still open, tooltip up) so a screenshot // harness can capture the chart + tooltip on screen. const keepOpen = new URLSearchParams(location.search).has('keepOpen'); let after = null, reopen = null, after2 = null; if (keepOpen) { win._resetZoom(); // the screenshot wants the full-frame chart await frames(); const hk = win._hits[0]; win._setHover({ x: geo.mapX + hk.x, y: geo.mapY + hk.y }); await sleep(150); const pass = dialog.isOpen && dialogCancelled.mapStillOpen && dialogEsc.mapStillOpen && missClick.targetStayedNull && realDialog && realConfirm.targetWasSet; window.__MAPCLICK = { ready: true, pass, keepOpen: true, before, dialog, dialogCancelled, dialogEsc, missClick, realDialog, realConfirm, realHit, errors, }; console.log('MAPCLICK (keepOpen) ready'); } else { // ---- 3: close the map, then inspect what lingers ------------------------ win.close(); await sleep(700); // close tween is 150 ms after = { openState: win.openState, ttPlate: describe(win.ttPlate, 'ttPlate'), ttName: describe(win.ttName, 'ttName'), ttHint: describe(win.ttHint, 'ttHint'), ttCont: describe(win.ttCont, 'ttCont'), hoverG: describe(win.hoverG, 'hoverG'), }; // ---- 4: re-open, hover, close again (no leftovers, no crash) ----------- win.open(); await sleep(1800); const h2 = win._hits[0]; // live hit list for the (possibly zoomed) view win._setHover({ x: geo.mapX + h2.x, y: geo.mapY + h2.y }); await sleep(120); reopen = { open: win.isOpen, hoverVisible: effVisible(win.ttName), hoverId: win._hover?.id ?? null, zoomKept: win._view.z === wheelIn.z, }; win.close(); await sleep(700); after2 = { openState: win.openState, ttContHidden: !effVisible(win.ttCont), ttPlateHidden: !effVisible(win.ttPlate), ttNameHidden: !effVisible(win.ttName), ttHintHidden: !effVisible(win.ttHint), }; } const results = []; const check = (label, cond) => results.push({ label, pass: !!cond }); check('tooltip (ttPlate) belongs to the map window (paints with it)', topLevel(win.ttPlate) === win); check('tooltip name text belongs to the map window', topLevel(win.ttName) === win); check('tooltip hint ("TAP TO PLOT COURSE") belongs to the map window', topLevel(win.ttHint) === win); check('while open: tooltip effectively visible on the chart', before.ttPlate.effVisible && before.ttName.effVisible); check('while open: tooltip paints ABOVE the map surface (inside the window)', before.ttPlate.paintsAboveMap && before.ttName.paintsAboveMap); check('hover ring is drawn at the destination\u2019s WORLD position (offset by the plate origin)', hoverDraw.ringAtDest); check('course line ends at the destination\u2019s WORLD position (ship→destination)', hoverDraw.lineEndsAtDest); check('clicking a discovered object opens the ENGAGE AUTOPILOT confirm', dialog.isOpen); check('the confirm is titled ENGAGE AUTOPILOT', dialog.title.trim() === 'ENGAGE AUTOPILOT'); check('the confirm names the destination', dialog.body.join(' ').toUpperCase().includes(hitLabel)); check('the confirm button reads ENGAGE', dialog.confirmLabel === 'ENGAGE'); check('confirming is deferred (no target until ENGAGE)', dialog.targetUnchanged); check('while the confirm is up the plate is inert (wheel does not zoom)', dialog.plateInertWheel); check('CANCEL closes the dialog, keeps the map open, plots no course', dialogCancelled.hidden && dialogCancelled.mapStillOpen && dialogCancelled.targetUnchanged); check('ESC cancels the confirm (map stays open, no course)', dialogEsc.hidden && dialogEsc.mapStillOpen && dialogEsc.targetUnchanged); check('clicking empty plate does NOT retarget (and opens no dialog)', missClick.targetStayedNull && missClick.noDialog); check('real hit test: a point over the object resolves the chart plate (mapImg)', realHit); check('REAL canvas click over the object opens the confirm (full input pipeline)', realDialog); check('REAL click on ENGAGE: course plotted AND the map closes', realConfirm.targetWasSet && realConfirm.mapClosed && realConfirm.dialogHidden); check('zoom in 2×: chart re-rendered at double scale (crisp, not stretched)', zoomIn.z === 2 && Math.abs(zoomIn.scale - baseScale * 2) / baseScale < 0.01); check('zoom in 2×: the point under the cursor stays under the cursor', Math.abs(zoomIn.anchor.x - wAnchor.x) * baseScale < 0.5 && Math.abs(zoomIn.anchor.y - wAnchor.y) * baseScale < 0.5); check('zoom out clamps at 1×', clampedMin === 1); check('zoom in clamps at 8×', clampedMax === 8); check('at 4× zoom: the object under the pointer still resolves (hover target)', zoomHit.objectAt); check('at 4× zoom: clicking it opens the confirm', zoomHit.clickOpensDialog); check('double-tap empty plate resets zoom to 1×', dblTap.tried && dblTap.reset); check('wheel over the plate zooms in', wheelIn.moved && wheelIn.z > 1); check('wheel over the chrome does NOT zoom the plate', wheelOut.z === wheelIn.z); check('drag over the plate pans the zoomed view (exact dx/scale, dy/scale)', pan.dx < 0.5 && pan.dy < 0.5); check('drag keeps the zoom level', pan.z === 2); check('drag pan clamps at the frame edge (and never overshoots it)', pan.clamped && pan.edgeSlip < 1e-6); check('a drag does not select / does not confirm', pan.noTarget && pan.dragNoSelect); check('drag clears the hover', pan.hoverCleared); check('REAL mousedown→mousemove→mouseup pans through the input pipeline', pan.realDx < 0.5 && pan.realDy < 0.5); check('the system\'s zoom view is remembered (per-system memory)', viewMemory); if (!keepOpen) { check('after close: tooltip plate hidden (no lingering button)', !after.ttPlate.effVisible); check('after close: tooltip name hidden', !after.ttName.effVisible); check('after close: tooltip hint hidden', !after.ttHint.effVisible); check('after close: hover ring cleared', !after.hoverG.effVisible || win.hoverG.commandBuffer.length === 0); check('re-open: hover tooltip visible on the chart again', reopen.open && reopen.hoverVisible); check('re-open: the system\'s zoom view persisted (per-system memory)', reopen.zoomKept === true); check('second close: tooltip fully hidden (no leftovers)', after2.ttContHidden && after2.ttPlateHidden && after2.ttNameHidden && after2.ttHintHidden); } check('no console errors', errors.length === 0); const pass = results.every((r) => r.pass); window.__MAPCLICK = { ready: true, pass, keepOpen, results, before, hoverDraw, dialog, dialogCancelled, dialogEsc, missClick, realDialog, realConfirm, realHit, zoom: { zoomIn, clampedMin, clampedMax, zoomHit, dblTap, wheelIn, wheelOut, pan, viewMemory }, after, reopen, after2, errors }; console.log(pass ? 'MAPCLICK PASS' : 'MAPCLICK FAIL'); console.log(JSON.stringify({ results, dialog, dialogCancelled, dialogEsc, realConfirm }, null, 1)); } catch (err) { const results = [{ label: `THREW: ${err.message}`, pass: false }]; window.__MAPCLICK = { ready: true, pass: false, results, errors }; console.error('MAPCLICK FAIL (threw)', err); }