#!/usr/bin/env node /** Location UI acceptance; controlled search results isolate UI races from geocoder availability. */ import fs from 'node:fs'; import puppeteer from 'puppeteer'; const browser = await puppeteer.launch({ headless: true, args: [ '--no-sandbox', ...(process.platform === 'darwin' ? ['--use-angle=metal', '--enable-gpu'] : ['--use-gl=angle', '--use-angle=swiftshader']), ], }); const page = await browser.newPage(); const errors = []; page.on('pageerror', (e) => errors.push(e.message)); let failures = 0; const check = (name, ok) => { console.log(`[${ok ? 'PASS' : 'FAIL'}] ${name}`); if (!ok) failures++; }; try { await page.setViewport({ width: 1440, height: 900 }); await page.goto( `${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`, { waitUntil: 'domcontentloaded' }, ); await page.waitForFunction( () => window.__godsEyeView?.styleManager?._locationControls && document.getElementById('loading-screen')?.classList.contains('hidden'), { timeout: 60000 }, ); await page.click('#location-bar-toggle'); await page.click('[data-pin-target="location-bar"]'); const cityId = await page.$eval( '.location-pill', (node) => node.dataset.locationId, ); await page.click('.location-pill'); await page.waitForFunction(() => document.getElementById('poi-row').classList.contains('expanded'), ); check( 'native city selection highlights and expands its POIs', await page.evaluate( (id) => window.__godsEyeView.styleManager._activeLocationId === id && document.querySelector('.location-pill.active')?.dataset.locationId === id, cityId, ), ); await page.keyboard.press('w'); check( 'POI keyboard action selects the second landmark', await page.evaluate( () => window.__godsEyeView.styleManager._activePoiIndex === 1 && document.querySelector('.poi-pill.active')?.dataset.poiIndex === '1', ), ); check( 'orbit controls and indicator agree', await page.evaluate(async () => { const ui = window.__godsEyeView.styleManager; // Vue renders on a microtask; a task later the indicator shows it. const rendered = () => new Promise((resolve) => setTimeout(resolve)); ui.setOrbit(true); await rendered(); const active = ui.orbitController.active && document.getElementById('orbit-indicator').classList.contains('active'); ui.setOrbit(false); await rendered(); return ( active && !ui.orbitController.active && !document.getElementById('orbit-indicator').classList.contains('active') ); }), ); await page.click('#search-toggle'); await page.type('#location-search', 'qwerty'); check( 'typing in search leaves POI selection unchanged', await page.evaluate( () => window.__godsEyeView.styleManager._activePoiIndex === 1, ), ); const geocoderRequests = []; const recordGeocoderRequest = (request) => { const url = new URL(request.url()); if ( url.pathname === '/api/geocode' || url.pathname === '/api/google/geocode' || (url.hostname === 'photon.komoot.io' && url.pathname === '/api/') ) geocoderRequests.push(request.url()); }; page.on('request', recordGeocoderRequest); await page.$eval('#location-search', (input) => { input.value = 'geo:39.7392,-104.9903'; input.focus(); }); await page.keyboard.press('Enter'); await page.waitForFunction( () => { const state = window.__godsEyeView.styleManager._locationLookup.getState(); return ( state.query === 'geo:39.7392,-104.9903' && state.status === 'found' ); }, { timeout: 30000 }, ); await page.waitForFunction( () => !window.__godsEyeView.viewer.camera._currentFlight, { timeout: 10000 }, ); check( 'native geo URI search shows the decimal coordinate readout', await page.evaluate(() => { const ui = window.__godsEyeView.styleManager; return ( ui._searchedLocationLabel === '39.7392° N, 104.9903° W' && document .getElementById('location-mini-city') .textContent.includes('39.7392° N') && document.getElementById('location-mini-poi').textContent === '104.9903° W' ); }), ); check( 'geo URI navigation settles near the requested coordinate', await page.evaluate(() => { const position = window.__godsEyeView.viewer.camera.positionCartographic; const degrees = 180 / Math.PI; return ( Math.abs(position.latitude * degrees - 39.7392) < 0.05 && Math.abs(position.longitude * degrees + 104.9903) < 0.05 ); }), ); check( 'geo URI search makes no geocoder request', geocoderRequests.length === 0, ); page.off('request', recordGeocoderRequest); await page.evaluate(() => { const lookup = window.__godsEyeView.styleManager._locationLookup; window.__qaSearchRequests = []; window.__qaLocationState = []; window.__godsEyeView.styleManager.subscribeLocationSearch((notification) => window.__qaLocationState.push(notification), ); lookup.search = (query, options) => new Promise((resolve) => window.__qaSearchRequests.push({ query, options, resolve }), ); document.getElementById('location-search').value = 'First'; document.getElementById('location-search').focus(); }); await page.keyboard.press('Enter'); await page.waitForFunction(() => window.__qaSearchRequests.length === 1); await page.evaluate(() => { document.getElementById('location-search').value = 'Second'; document.getElementById('location-search').focus(); }); await page.keyboard.press('Enter'); await page.waitForFunction(() => window.__qaSearchRequests.length === 2); check( 'second native submit aborts the first request and keeps current busy state', await page.evaluate( () => window.__qaSearchRequests[0].options.signal.aborted && document .getElementById('location-search') .classList.contains('searching'), ), ); await page.evaluate(() => window.__qaSearchRequests[1].resolve({ label: 'Second landmark, Test city', }), ); await page.waitForFunction( () => window.__godsEyeView.styleManager._searchedLocationLabel === 'Second landmark, Test city', ); await page.evaluate(() => window.__qaSearchRequests[0].resolve({ label: 'First landmark, Test city', }), ); check( 'late result cannot overwrite the current location readout or style', await page.evaluate(() => { const ui = window.__godsEyeView.styleManager; return ( ui._searchedLocationLabel === 'Second landmark, Test city' && document .getElementById('location-mini-city') .textContent.includes('Second landmark') && document.getElementById('location-mini-poi').textContent === 'Test city' && ui.activeStyle === 'normal' && !ui._locationSearch.classList.contains('searching') ); }), ); check( 'Location subscriptions accept only the current result and retain request identities', await page.evaluate(() => { const seen = window.__qaLocationState; const started = seen.filter(({ change }) => change?.type === 'started'); const found = seen.filter(({ change }) => change?.type === 'found'); return ( seen[0].initial && started.length === 2 && found.length === 1 && found[0].change.requestId === started[1].change.requestId && found[0].state.destination.label === 'Second landmark, Test city' && Object.isFrozen(found[0].state.destination) ); }), ); check( 'cancelled POI expansion stays closed after a frame', await page.evaluate(async (id) => { const ui = window.__godsEyeView.styleManager; ui._expandPOIRow(id); ui._collapsePOIRow(); await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)), ); return ( !document.getElementById('poi-row').classList.contains('expanded') && !document .getElementById('location-bar-divider') .classList.contains('visible') ); }, cityId), ); await page.evaluate( (id) => window.__godsEyeView.styleManager._onCityPillClick(id), cityId, ); await page.waitForFunction(() => document.getElementById('poi-row').classList.contains('expanded'), ); // Finish the camera's existing city flight before inspecting both layouts. await page.waitForFunction( () => !window.__godsEyeView.viewer.camera._currentFlight, { timeout: 10000 }, ); await page.evaluate( () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)), ), ); const settled = await page .waitForFunction( () => { const scene = window.__godsEyeView.viewer.scene; for (let index = 0; index < scene.primitives.length; index++) { const primitive = scene.primitives.get(index); if ( typeof primitive.tilesLoaded === 'boolean' && !primitive.tilesLoaded ) return false; } return true; }, { timeout: 60000 }, ) .then( () => true, () => false, ); check('visible city tiles settle before screenshots', settled); fs.mkdirSync('qa-shots/location-controls', { recursive: true }); await page.screenshot({ path: 'qa-shots/location-controls/desktop.png' }); await page.setViewport({ width: 620, height: 900 }); await page.waitForFunction( () => document.getElementById('left-panel-stack')?.dataset.layoutMode === 'mobile', ); await page.screenshot({ path: 'qa-shots/location-controls/narrow.png' }); check( 'native reset returns through the existing camera handoff', await page.evaluate(async () => { const ui = window.__godsEyeView.styleManager; ui._resetGlobeBtn.click(); const pending = ui._globeResetPromise; if (!pending) return false; await pending; return ( !ui.orbitController.active && !window.__godsEyeView.viewer.trackedEntity ); }), ); check( 'destroyed controls cannot navigate or submit another lookup', await page.evaluate(() => { const ui = window.__godsEyeView.styleManager; const count = window.__qaSearchRequests.length; ui._locationControls.destroy(); ui._locationLookup.destroy(); const before = ui._navigationGeneration; document .getElementById('location-pills') .querySelector('button') ?.click(); ui._resetGlobeBtn.click(); ui._locationSearch.dispatchEvent( new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }), ); return ( before === ui._navigationGeneration && count === window.__qaSearchRequests.length ); }), ); check('no uncaught browser errors', errors.length === 0); } finally { await browser.close(); } console.log(`RESULT: ${failures} failures`); process.exitCode = failures ? 1 : 0;