From 16e0f35c74f2ff672eefac1eff05f1bd13d719fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20K=C3=A9kesi?= <64812024+KristofKekesi@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:21:23 +0200 Subject: [PATCH 1/3] Feat: GBFS v3 support for station names GBFS v3 uses `LocalizedString` for `name` and `short_name` of the stations. This caused stations from v3 to have names like `[object Object]`. This commit makes the code handle v3 names. It default to locale 'en' then fallbacks to the first option in the array. Documentation for v3: https://gbfs.org/documentation/reference/#station_informationjson --- src/sources/gbfsStations.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/sources/gbfsStations.js b/src/sources/gbfsStations.js index 9c5b93528..cd90b12dd 100644 --- a/src/sources/gbfsStations.js +++ b/src/sources/gbfsStations.js @@ -63,15 +63,27 @@ export function parseStationInformation(payload) { const stationMap = new Map(); for (const raw of stations) { - // Accept both GBFS 2.x (station_id) and alternate (id) field names + // Accept both GBFS (station_id) and alternate (id) field names const stationId = String(raw?.station_id ?? raw?.id ?? '').trim(); const lat = Number(raw?.lat ?? raw?.latitude); const lon = Number(raw?.lon ?? raw?.longitude); if (!stationId || !Number.isFinite(lat) || !Number.isFinite(lon)) continue; + let name; + if (payload?.version >= 3) { + const handleLocalizedString = (array) => + array.find((localizedString) => + localizedString.language.toLowerCase() === 'en') || array[0]; + name = String( + handleLocalizedString(raw?.name).text || + handleLocalizedString(raw?.short_name).text || '').trim(); + } else { + name = String(raw?.name || raw?.short_name || '').trim(); + } + stationMap.set(stationId, { stationId, - name: String(raw?.name || raw?.short_name || '').trim(), + name, lat, lon, capacity: toNonNegativeInteger(raw?.capacity), From ddd932f0a7e287da7297fed3d02c03834d5039b0 Mon Sep 17 00:00:00 2001 From: Mazeyar Moeini Feizabadi Date: Tue, 13 Oct 2026 23:46:51 +0200 Subject: [PATCH 2/3] Harden shared GBFS station-name parsing and add regression coverage --- CHANGELOG.md | 6 + docs/CURRENT-STATE.md | 13 ++ scripts/format-scope.json | 4 +- scripts/qa-gbfs-names.mjs | 335 ++++++++++++++++++++++++++++ src/sources/gbfsStations.js | 34 +-- src/sources/gbfsStations.test.mjs | 199 +++++++++++++++++ src/tools/queries/mobility.test.mjs | 28 +++ 7 files changed, 605 insertions(+), 14 deletions(-) create mode 100644 scripts/qa-gbfs-names.mjs create mode 100644 src/sources/gbfsStations.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d15d897..032c6fe2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +- GBFS v3 station names use English translations when available, then the first + usable translation or short name. Empty or malformed names no longer appear as + `[object Object]` or interrupt station loading; legacy string names remain + supported in the globe and bike-share tools. Thanks to + [Kristóf Kékesi](https://github.com/KristofKekesi) (#61). + - Treat explicitly current, complete empty vessel coverage as a successful update, clearing obsolete contacts while retaining stale or incomplete snapshots. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index a9aeeddbc..0d3ef31e0 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1092,6 +1092,19 @@ normal fallback chain. The existing declared media size ceiling remains 64 MiB. ## GBFS upstream bounds +Station information is normalized in `src/sources/gbfsStations.js` for both the +globe layer and `get_bike_share`. Names accept legacy strings or GBFS v3 Localized +String arrays regardless of the reported feed version. Nonempty English text +(including regional `en-*` tags, case-insensitive) takes precedence, then the first +usable translation. An unusable `name` falls back to `short_name` with the same +rules; if neither supplies text, the station remains available with an empty name +(the globe uses its station-ID label and the tool returns `null`). Malformed +translation entries do not interrupt loading of the other stations. This covers +station names, not complete GBFS v3 support. +`scripts/qa-gbfs-names.mjs` checks synthetic localized names through the real layer, +station selection, keyboard dismissal, mobile cards and bike-share tool against +either a dev or preview server (`--url` selects the target). + The station reader addresses the proxy as `/api/gbfs/`; the feed address is read from the path, and a query-string form is refused with a 400. GBFS refuses upstream redirects and enforces its 5 MiB response cap while streaming. The 12-second deadline includes reading the body, and rejected or diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 4973bd484..662d33a6b 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -979,5 +979,7 @@ "src/tools/views.test.mjs", "src/app/embed.test.mjs", "src/tools/globePanel.test.mjs", - "src/costEndpointThrottle.test.mjs" + "src/costEndpointThrottle.test.mjs", + "src/sources/gbfsStations.test.mjs", + "scripts/qa-gbfs-names.mjs" ] diff --git a/scripts/qa-gbfs-names.mjs b/scripts/qa-gbfs-names.mjs new file mode 100644 index 000000000..81dc5eb08 --- /dev/null +++ b/scripts/qa-gbfs-names.mjs @@ -0,0 +1,335 @@ +#!/usr/bin/env node +/** + * Keyless station-name regression against the real globe and bike-share tool. + * Run with a dev or preview server: node scripts/qa-gbfs-names.mjs --url http://localhost:4173 + * GBFS documents are synthetic; no operator feed or provider key is required. + */ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import puppeteer from 'puppeteer'; + +const urlIndex = process.argv.indexOf('--url'); +const appUrl = + urlIndex < 0 ? 'http://localhost:4173' : process.argv[urlIndex + 1]; +const shots = path.resolve('qa-shots/gbfs-names'); +fs.mkdirSync(shots, { recursive: true }); +const information = { + version: '3.0', + data: { + stations: [ + { + station_id: 'english', + name: [ + { language: 'hu', text: 'Magyar tér' }, + { language: 'en', text: 'QA English Square' }, + ], + }, + { + station_id: 'short', + name: [null, { language: 'en', text: ' ' }], + short_name: [{ language: 'en-GB', text: 'QA Short Name' }], + }, + { station_id: 'unnamed', name: [null, {}, { language: 42, text: [] }] }, + { station_id: 'legacy', name: ' QA Legacy Station ' }, + { + station_id: 'fallback', + name: [null, { language: 'hu', text: 'QA Magyar Station' }], + }, + ].map((station, index) => ({ + ...station, + lat: 30.2681 + index * 0.001, + lon: -97.7469 + index * 0.001, + capacity: 13, + })), + }, +}; +const status = { + data: { + stations: information.data.stations.map(({ station_id }) => ({ + station_id, + num_bikes_available: 4, + num_docks_available: 9, + is_renting: true, + is_returning: true, + })), + }, +}; +const browser = await puppeteer.launch({ + headless: 'new', + ...(process.env.PUPPETEER_EXECUTABLE_PATH + ? { executablePath: process.env.PUPPETEER_EXECUTABLE_PATH } + : {}), + args: + process.platform === 'darwin' + ? ['--use-angle=metal'] + : ['--use-gl=angle', '--use-angle=swiftshader'], +}); +let checks = 0; +const check = (name, actual, expected) => { + assert.deepEqual(actual, expected, name); + checks++; + console.log(`[PASS] ${name}`); +}; +async function open(mode = 'normal') { + const page = await browser.newPage(); + await page.bringToFront(); + const errors = []; + page.on('pageerror', (error) => errors.push(error.message)); + await page.setViewport({ width: 1440, height: 900 }); + await page.evaluateOnNewDocument( + ({ information, status, mode }) => { + localStorage.clear(); + localStorage.setItem('gev:first-run-mission:v1', 'suppressed'); + window.__qaPainted = []; + const paint = CanvasRenderingContext2D.prototype.fillText; + CanvasRenderingContext2D.prototype.fillText = function (text, ...args) { + if ( + String(text).startsWith('QA ') || + String(text) === 'Station unnamed' + ) { + window.__qaPainted.push(String(text)); + if (window.__qaPainted.length > 100) window.__qaPainted.shift(); + } + return paint.call(this, text, ...args); + }; + const original = window.fetch.bind(window); + window.fetch = async (input, init) => { + const url = String( + typeof input === 'string' || input instanceof URL ? input : input.url, + ); + if (url.includes('/api/gbfs/')) { + if (mode === 'failure') return new Response('{}', { status: 503 }); + const payload = + mode === 'empty' + ? { data: { stations: [] } } + : decodeURIComponent(url).includes('station_information') + ? information + : status; + return new Response(JSON.stringify(payload), { + headers: { 'Content-Type': 'application/json' }, + }); + } + if (url.includes('/api/flights')) return new Response('{"states":[]}'); + if (url.includes('/api/military')) return new Response('{"ac":[]}'); + if (url.includes('/api/vessels')) + return new Response('{"connected":false,"vessels":[]}'); + return original(input, init); + }; + }, + { information, status, mode }, + ); + await page.goto(appUrl, { waitUntil: 'domcontentloaded' }); + await page.waitForFunction( + () => + window.__godsEyeView?.dataManager && window.__godsEyeView?.voiceCommands, + { timeout: 60000 }, + ); + await page.waitForFunction( + () => + !document.getElementById('loading-screen') || + (document.getElementById('loading-screen').classList.contains('hidden') && + getComputedStyle(document.getElementById('loading-screen')).opacity === + '0'), + { timeout: 60000 }, + ); + await page.evaluate(async () => { + const { dataManager, viewer } = window.__godsEyeView; + for (const [id, layer] of dataManager.layers) { + if (layer.enabled) + await dataManager.setEnabled(id, false, { origin: 'user' }); + } + const Cartesian3 = viewer.camera.position.constructor; + viewer.camera.cancelFlight(); + viewer.camera.setView({ + destination: Cartesian3.fromDegrees(-97.7449, 30.2701, 3000), + orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 }, + }); + await dataManager.setEnabled('bikeshare', true, { origin: 'user' }); + window.__godsEyeView.requestRender('gbfs-name-qa'); + }); + return { page, errors }; +} +try { + const { page, errors } = await open(); + await page.waitForFunction( + () => + window.__godsEyeView.dataManager.layers.get('bikeshare').module.getStats() + .count === 5, + { timeout: 15000 }, + ); + const records = await page.evaluate(() => + window.__godsEyeView.dataManager.layers + .get('bikeshare') + .module.getAnalystRecords(), + ); + check( + 'the layer retains all five stations and normalized names', + records.map((r) => r.name), + [ + 'QA English Square', + 'QA Short Name', + null, + 'QA Legacy Station', + 'QA Magyar Station', + ], + ); + const tool = await page.evaluate(() => + window.__godsEyeView.voiceCommands.runner('get_bike_share', { + area: { lat: 30.2701, lon: -97.7449, radius_km: 2 }, + }), + ); + check('the real bike-share tool succeeds', tool.ok, true); + check( + 'the tool preserves localized, short, unnamed, legacy and non-English names', + tool.data.rows.map((r) => r.name).sort(), + [ + null, + 'QA English Square', + 'QA Short Name', + 'QA Legacy Station', + 'QA Magyar Station', + ].sort(), + ); + check( + 'availability remains attached to every station', + tool.data.rows.every( + (r) => r.bikes_available === 4 && r.docks_available === 9, + ), + true, + ); + const clickPoint = await page.evaluate(() => { + window.__qaPainted = []; + const { viewer } = window.__godsEyeView; + const Cartesian3 = viewer.camera.position.constructor; + const point = viewer.scene.cartesianToCanvasCoordinates( + Cartesian3.fromDegrees(-97.7469, 30.2681), + ); + const rect = viewer.scene.canvas.getBoundingClientRect(); + return { x: rect.left + point.x, y: rect.top + point.y }; + }); + await page.mouse.click(clickPoint.x, clickPoint.y); + await page.waitForFunction( + () => window.__qaPainted.includes('QA English Square'), + { timeout: 10000 }, + ); + check( + 'a real pointer click selects the station with its localized name', + await page.evaluate(() => window.__qaPainted.includes('QA English Square')), + true, + ); + await page.keyboard.press('Escape'); + for (const [id, title] of [ + ['english', 'QA English Square'], + ['short', 'QA Short Name'], + ['unnamed', 'Station unnamed'], + ]) { + await page.evaluate((id) => { + window.__qaPainted = []; + const app = window.__godsEyeView; + app.dataManager.layers + .get('bikeshare') + .module._selectBikeshareStationForTest(`austin-capmetro:${id}`); + app.requestRender('gbfs-name-selection-qa'); + }, id); + await page.waitForFunction( + (title) => window.__qaPainted.includes(title), + { timeout: 10000 }, + title, + ); + check( + `the selected ${id} card paints the expected title`, + await page.evaluate((title) => window.__qaPainted.includes(title), title), + true, + ); + await page.screenshot({ path: path.join(shots, `${id}.png`) }); + await page.keyboard.press('Escape'); + await page.waitForFunction(() => + window.__godsEyeView.dataManager.layers + .get('bikeshare') + .module.getDetectableObjects() + .every((record) => !record.skipLabel), + ); + check( + `Escape clears the ${id} card`, + await page.evaluate( + () => + window.__godsEyeView.dataManager.layers + .get('bikeshare') + .module.getDetectableObjects() + .filter((record) => record.skipLabel).length, + ), + 0, + ); + } + await page.setViewport({ width: 390, height: 844 }); + await page.evaluate(() => { + window.__qaPainted = []; + const app = window.__godsEyeView; + // Place the station card below the mobile panel headers. + const Cartesian3 = app.viewer.camera.position.constructor; + app.viewer.camera.cancelFlight(); + app.viewer.camera.setView({ + destination: Cartesian3.fromDegrees(-97.7449, 30.2771, 3000), + orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 }, + }); + app.dataManager.layers + .get('bikeshare') + .module._selectBikeshareStationForTest('austin-capmetro:english'); + app.requestRender('gbfs-name-mobile-qa'); + }); + await page.waitForFunction( + () => window.__qaPainted.includes('QA English Square'), + { timeout: 10000 }, + ); + check( + 'the mobile selected card paints its normalized name', + await page.evaluate(() => window.__qaPainted.includes('QA English Square')), + true, + ); + await page.screenshot({ path: path.join(shots, 'mobile.png') }); + await page.evaluate(() => + window.__godsEyeView.dataManager.setEnabled('bikeshare', false, { + origin: 'user', + }), + ); + check( + 'disabling clears selected cards and analyst records', + await page.evaluate(() => ({ + count: window.__godsEyeView.dataManager.layers + .get('bikeshare') + .module.getAnalystRecords().length, + selected: window.__godsEyeView.dataManager.layers + .get('bikeshare') + .module.getDetectableObjects() + .filter((record) => record.skipLabel).length, + })), + { count: 0, selected: 0 }, + ); + check('normal fixtures produce no page exceptions', errors, []); + await page.close(); + for (const mode of ['empty', 'failure']) { + const { page, errors } = await open(mode); + await page.waitForFunction( + () => + !window.__godsEyeView.dataManager.layers + .get('bikeshare') + .module.getStats().loading, + ); + check( + `${mode} leaves no rendered stations`, + await page.evaluate( + () => + window.__godsEyeView.dataManager.layers + .get('bikeshare') + .module.getStats().count, + ), + 0, + ); + check(`${mode} produces no page exceptions`, errors, []); + await page.close(); + } + console.log(`GBFS names: ${checks}/${checks} checks passed; ${appUrl}`); +} finally { + await browser.close(); +} diff --git a/src/sources/gbfsStations.js b/src/sources/gbfsStations.js index cd90b12dd..efa1e6169 100644 --- a/src/sources/gbfsStations.js +++ b/src/sources/gbfsStations.js @@ -50,6 +50,26 @@ export function extractStationsArray(payload) { return []; } +/** Read legacy text or a GBFS v3 Localized String, preferring usable English text. */ +function stationName(value) { + if (typeof value === 'string') return value.trim(); + if (!Array.isArray(value)) return ''; + + let fallback = ''; + for (const translation of value) { + if (typeof translation?.text !== 'string') continue; + const text = translation.text.trim(); + if (!text) continue; + if (!fallback) fallback = text; + if ( + typeof translation.language === 'string' && + /^en(?:-|$)/i.test(translation.language.trim()) + ) + return text; + } + return fallback; +} + /** * Parse a GBFS station_information payload into a Map of station metadata. * Handles field-name variations across different GBFS providers @@ -69,21 +89,9 @@ export function parseStationInformation(payload) { const lon = Number(raw?.lon ?? raw?.longitude); if (!stationId || !Number.isFinite(lat) || !Number.isFinite(lon)) continue; - let name; - if (payload?.version >= 3) { - const handleLocalizedString = (array) => - array.find((localizedString) => - localizedString.language.toLowerCase() === 'en') || array[0]; - name = String( - handleLocalizedString(raw?.name).text || - handleLocalizedString(raw?.short_name).text || '').trim(); - } else { - name = String(raw?.name || raw?.short_name || '').trim(); - } - stationMap.set(stationId, { stationId, - name, + name: stationName(raw?.name) || stationName(raw?.short_name), lat, lon, capacity: toNonNegativeInteger(raw?.capacity), diff --git a/src/sources/gbfsStations.test.mjs b/src/sources/gbfsStations.test.mjs new file mode 100644 index 000000000..8c60da90c --- /dev/null +++ b/src/sources/gbfsStations.test.mjs @@ -0,0 +1,199 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { parseStationInformation } from './gbfsStations.js'; + +const station = (fields = {}) => ({ + station_id: 's1', + lat: 30.2681, + lon: -97.7469, + capacity: 13, + ...fields, +}); +const parseName = (fields, version) => + parseStationInformation({ + version, + data: { stations: [station(fields)] }, + }).get('s1').name; + +test('GBFS v2 station names remain trimmed strings', () => { + assert.equal( + parseName({ name: ' Republic Square ', short_name: 'Short' }, '2.3'), + 'Republic Square', + ); +}); + +test('GBFS v3 station names prefer English over the first translation', () => { + assert.equal( + parseName( + { + name: [ + { language: 'hu', text: 'Köztársaság tér' }, + { language: 'en', text: ' Republic Square ' }, + ], + }, + '3.0', + ), + 'Republic Square', + ); +}); + +test('English preference accepts case differences and regional language tags', () => { + for (const language of ['EN', 'en-GB', 'En-US']) { + assert.equal( + parseName({ + name: [ + { language: 'hu', text: 'Hungarian' }, + { language, text: 'English' }, + ], + }), + 'English', + ); + } +}); + +test('without usable English text the first usable translation wins', () => { + assert.equal( + parseName({ + name: [ + null, + { language: 'en', text: ' ' }, + { language: 'en', text: 42 }, + { language: 'hu', text: ' Első ' }, + { language: 'fr', text: 'Deuxième' }, + ], + }), + 'Első', + ); +}); + +test('malformed language fields cannot abort translation selection', () => { + const name = [ + null, + false, + 'bad', + {}, + { language: 42, text: 'Fallback' }, + { language: { unexpected: true }, text: 'Other' }, + { language: 'en', text: 'English' }, + ]; + assert.equal(parseName({ name }), 'English'); + assert.equal( + parseName({ name: [{ text: ' Usable text without a locale ' }] }), + 'Usable text without a locale', + ); +}); + +test('a usable name takes precedence over an English short name', () => { + assert.equal( + parseName({ + name: [{ language: 'hu', text: 'Full name' }], + short_name: [{ language: 'en', text: 'Short name' }], + }), + 'Full name', + ); +}); + +test('missing, empty and malformed names fall back to localized short_name', () => { + for (const name of [ + undefined, + null, + '', + ' ', + [], + {}, + 0, + 42, + false, + [null, {}, { language: 'en', text: [] }, { language: 'hu', text: ' ' }], + ]) { + assert.equal( + parseName({ + name, + short_name: [ + { language: 'hu', text: 'Rövid' }, + { language: 'en', text: ' Short ' }, + ], + }), + 'Short', + ); + } +}); + +test('localized names can fall back to legacy short_name strings', () => { + assert.equal(parseName({ name: [], short_name: ' Short ' }), 'Short'); +}); + +test('no usable name leaves the station available with an empty name', () => { + for (const value of [ + undefined, + null, + '', + [], + {}, + 42, + true, + [null, { text: false }, { language: 'en', text: ' ' }], + ]) { + assert.equal(parseName({ name: value, short_name: value }), ''); + } +}); + +test('name shape is normalized even when a feed omits or misstates its version', () => { + const name = [{ language: 'en', text: 'Republic Square' }]; + for (const version of [undefined, '2.3', '3.0', '3.1', 'unknown']) { + assert.equal(parseName({ name }, version), 'Republic Square'); + assert.equal(parseName({ name: 'Legacy name' }, version), 'Legacy name'); + } +}); + +test('bad names do not lose valid stations or their metadata', () => { + const information = parseStationInformation({ + version: '3.0', + data: { + stations: [ + station({ station_id: 'bad', name: [null, {}] }), + station({ + station_id: 'short', + name: [], + short_name: [{ language: 'en', text: 'Short' }], + }), + station({ + station_id: 'good', + name: [{ language: 'en', text: 'Republic Square' }], + }), + ], + }, + }); + assert.equal(information.size, 3); + assert.equal(information.get('bad').name, ''); + assert.equal(information.get('short').name, 'Short'); + assert.deepEqual(information.get('good'), { + stationId: 'good', + name: 'Republic Square', + lat: 30.2681, + lon: -97.7469, + capacity: 13, + isInstalled: true, + isRenting: true, + isReturning: true, + }); +}); + +test('normalizing a localized name leaves the feed document unchanged', () => { + const payload = { + version: '3.0', + data: { + stations: [ + station({ + name: [ + { language: 'hu', text: 'Magyar' }, + { language: 'en', text: ' English ' }, + ], + }), + ], + }, + }; + const original = structuredClone(payload); + parseStationInformation(payload); + assert.deepEqual(payload, original); +}); diff --git a/src/tools/queries/mobility.test.mjs b/src/tools/queries/mobility.test.mjs index af8142305..7c7050e88 100644 --- a/src/tools/queries/mobility.test.mjs +++ b/src/tools/queries/mobility.test.mjs @@ -106,6 +106,34 @@ test('bike share joins station information with live status inside the area', as assert.equal(result.data.rows[1].capacity, 10); }); +test('bike share returns localized names without losing stations with malformed names', async () => { + const informationUrl = 'https://gbfs.example/austin/station_information.json'; + const information = structuredClone(documents[informationUrl]); + information.version = '3.0'; + information.data.stations[0].name = [ + { language: 'hu', text: 'Magyar név' }, + { language: 'en', text: 'Congress & 2nd' }, + ]; + information.data.stations[2].name = [null, { language: 42, text: [] }]; + const source = bikeshare([system('austin', 'Austin, TX', 30.27, -97.74)]); + const catalog = composeCatalog({ + tools: coreTools, + services: { + bikeshare: { + ...source, + getStations: (url) => + url === informationUrl ? information : source.getStations(url), + }, + }, + }); + const result = await catalog.call('get_bike_share', { area: downtown }); + assert.equal(result.data.total, 2); + assert.equal(result.data.rows[0].name, 'Congress & 2nd'); + assert.equal(result.data.rows[0].bikes_available, 4); + assert.equal(result.data.rows[1].name, null); + assert.doesNotMatch(result.summary, /unavailable/); +}); + test('bike share reports uncovered areas and failed systems honestly', async () => { const none = composeCatalog({ tools: coreTools, From 82b8c0c850665245c5149279387a6dac2632656e Mon Sep 17 00:00:00 2001 From: Mazeyar Moeini Feizabadi Date: Tue, 13 Oct 2026 23:47:48 +0200 Subject: [PATCH 3/3] Frame mobile GBFS QA in supported clean view --- scripts/qa-gbfs-names.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/qa-gbfs-names.mjs b/scripts/qa-gbfs-names.mjs index 81dc5eb08..3a367abe4 100644 --- a/scripts/qa-gbfs-names.mjs +++ b/scripts/qa-gbfs-names.mjs @@ -266,11 +266,12 @@ try { await page.evaluate(() => { window.__qaPainted = []; const app = window.__godsEyeView; - // Place the station card below the mobile panel headers. + // Use the supported clean view to inspect the mobile card clear of panels. + app.styleManager.toggleCleanView(true); const Cartesian3 = app.viewer.camera.position.constructor; app.viewer.camera.cancelFlight(); app.viewer.camera.setView({ - destination: Cartesian3.fromDegrees(-97.7449, 30.2771, 3000), + destination: Cartesian3.fromDegrees(-97.7449, 30.2701, 3000), orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 }, }); app.dataManager.layers