@@ -10,6 +10,19 @@ const JSON_SEMANTIC_LOC_FIXTURE = path.resolve(__dirname, 'fixtures/google-seman
1010const JSON_TIMELINE_EDITS_FIXTURE = path . resolve ( __dirname , 'fixtures/google-timeline-edits.json' )
1111const JSON_SEMANTIC_SEG_FIXTURE = path . resolve ( __dirname , 'fixtures/google-semantic-segments.json' )
1212
13+ function boxesOverlap ( a : { x : number ; y : number ; width : number ; height : number } , b : { x : number ; y : number ; width : number ; height : number } ) {
14+ return ! (
15+ a . x + a . width <= b . x ||
16+ b . x + b . width <= a . x ||
17+ a . y + a . height <= b . y ||
18+ b . y + b . height <= a . y
19+ )
20+ }
21+
22+ function shortestAngleDelta ( from : number , to : number ) {
23+ return Math . abs ( ( ( to - from + 540 ) % 360 ) - 180 )
24+ }
25+
1326/** Helper: wait for the app to be ready (map container rendered, with or without WebGL) */
1427async function waitForApp ( page : Page ) {
1528 // Wait for the heading to be visible (confirms React rendered)
@@ -18,11 +31,6 @@ async function waitForApp(page: Page) {
1831 await page . waitForTimeout ( 500 )
1932}
2033
21- /** Helper: check if the map canvas loaded (WebGL worked) */
22- async function hasMapCanvas ( page : Page ) : Promise < boolean > {
23- return await page . locator ( 'canvas' ) . count ( ) > 0
24- }
25-
2634/** Helper: upload a GPX file and wait for the track to load */
2735async function uploadGpx ( page : Page ) {
2836 const fileInput = page . locator ( 'input[type="file"]' )
@@ -101,6 +109,98 @@ test.describe('Travelback App', () => {
101109 await expect ( page . getByRole ( 'button' , { name : / c a m e r a t r a c k i n g / i } ) ) . toBeVisible ( )
102110 } )
103111
112+ test ( 'map zoom controls do not overlap top toolbars' , async ( { page } ) => {
113+ await uploadGpx ( page )
114+
115+ const zoomControls = page . locator ( '.maplibregl-ctrl-top-left .maplibregl-ctrl-group' ) . first ( )
116+ await expect ( zoomControls ) . toBeVisible ( { timeout : 10_000 } )
117+ await expect ( page . getByTestId ( 'global-toolbar' ) ) . toBeVisible ( { timeout : 10_000 } )
118+ await expect ( page . getByTestId ( 'track-toolbar' ) ) . toBeVisible ( { timeout : 10_000 } )
119+
120+ await expect . poll ( async ( ) => {
121+ const [ zoomBox , globalToolbarBox , trackToolbarBox ] = await Promise . all ( [
122+ zoomControls . boundingBox ( ) ,
123+ page . getByTestId ( 'global-toolbar' ) . boundingBox ( ) ,
124+ page . getByTestId ( 'track-toolbar' ) . boundingBox ( ) ,
125+ ] )
126+
127+ if ( ! zoomBox || ! globalToolbarBox || ! trackToolbarBox ) {
128+ return true
129+ }
130+
131+ return boxesOverlap ( zoomBox , globalToolbarBox ) || boxesOverlap ( zoomBox , trackToolbarBox )
132+ } , { timeout : 5_000 , intervals : [ 120 , 200 , 300 ] } ) . toBeFalsy ( )
133+ } )
134+
135+ test ( 'map camera movement stays stable during playback' , async ( { page } ) => {
136+ await uploadGpx ( page )
137+
138+ const playBtn = page . getByRole ( 'button' , { name : 'Play' } )
139+ await expect ( playBtn ) . toBeVisible ( { timeout : 10_000 } )
140+ await playBtn . click ( { force : true } )
141+
142+ const samples = await page . evaluate ( async ( ) => {
143+ type CameraSample = { center : [ number , number ] ; bearing : number }
144+ type DebugWindow = Window & {
145+ __travelbackDebug ?: {
146+ getCamera : ( ) => CameraSample | null
147+ }
148+ }
149+
150+ const debugWindow = window as DebugWindow
151+ const points : CameraSample [ ] = [ ]
152+
153+ for ( let i = 0 ; i < 16 ; i ++ ) {
154+ const camera = debugWindow . __travelbackDebug ?. getCamera ( )
155+ if ( camera ) {
156+ points . push ( { center : [ ...camera . center ] as [ number , number ] , bearing : camera . bearing } )
157+ }
158+ await new Promise ( resolve => setTimeout ( resolve , 120 ) )
159+ }
160+
161+ return points
162+ } )
163+
164+ expect ( samples . length ) . toBeGreaterThanOrEqual ( 8 )
165+
166+ const centerJumpsMeters : number [ ] = [ ]
167+ const bearingJumps : number [ ] = [ ]
168+
169+ for ( let i = 1 ; i < samples . length ; i ++ ) {
170+ const prev = samples [ i - 1 ]
171+ const next = samples [ i ]
172+ const avgLatRad = ( ( prev . center [ 1 ] + next . center [ 1 ] ) / 2 ) * ( Math . PI / 180 )
173+ const dLngMeters = ( next . center [ 0 ] - prev . center [ 0 ] ) * 111320 * Math . cos ( avgLatRad )
174+ const dLatMeters = ( next . center [ 1 ] - prev . center [ 1 ] ) * 110540
175+ centerJumpsMeters . push ( Math . hypot ( dLngMeters , dLatMeters ) )
176+ bearingJumps . push ( shortestAngleDelta ( prev . bearing , next . bearing ) )
177+ }
178+
179+ const steadyCenterJumps = centerJumpsMeters . slice ( 4 )
180+ const steadyBearingJumps = bearingJumps . slice ( 4 )
181+
182+ expect ( steadyCenterJumps . length ) . toBeGreaterThanOrEqual ( 4 )
183+ expect ( steadyBearingJumps . length ) . toBeGreaterThanOrEqual ( 4 )
184+
185+ const sortedCenterJumps = [ ...steadyCenterJumps ] . sort ( ( a , b ) => a - b )
186+ const sortedBearingJumps = [ ...steadyBearingJumps ] . sort ( ( a , b ) => a - b )
187+ const centerMedian = sortedCenterJumps [ Math . floor ( sortedCenterJumps . length / 2 ) ]
188+ const bearingMedian = sortedBearingJumps [ Math . floor ( sortedBearingJumps . length / 2 ) ]
189+ const centerP95 = sortedCenterJumps [ Math . floor ( ( sortedCenterJumps . length - 1 ) * 0.95 ) ]
190+ const bearingP95 = sortedBearingJumps [ Math . floor ( ( sortedBearingJumps . length - 1 ) * 0.95 ) ]
191+
192+ const firstSample = samples [ 0 ]
193+ const lastSample = samples [ samples . length - 1 ]
194+ const avgLatRad = ( ( firstSample . center [ 1 ] + lastSample . center [ 1 ] ) / 2 ) * ( Math . PI / 180 )
195+ const totalLngMeters = ( lastSample . center [ 0 ] - firstSample . center [ 0 ] ) * 111320 * Math . cos ( avgLatRad )
196+ const totalLatMeters = ( lastSample . center [ 1 ] - firstSample . center [ 1 ] ) * 110540
197+ const totalDisplacementMeters = Math . hypot ( totalLngMeters , totalLatMeters )
198+
199+ expect ( totalDisplacementMeters ) . toBeGreaterThan ( 25 )
200+ expect ( centerP95 ) . toBeLessThan ( Math . max ( 600 , centerMedian * 8 ) )
201+ expect ( bearingP95 ) . toBeLessThan ( Math . max ( 150 , bearingMedian * 8 ) )
202+ } )
203+
104204 test ( 'scene editor opens and allows adding scenes' , async ( { page } ) => {
105205 await uploadGpx ( page )
106206
0 commit comments