@@ -42,3 +42,81 @@ test("community events page map loads and Zurich meetup link works", async ({
4242 await newPage . waitForLoadState ( "domcontentloaded" , { timeout : 10000 } )
4343 expect ( newPage . url ( ) ) . toContain ( "meetup.com/graphql-zurich" )
4444} )
45+
46+ test ( "community events map popover appears on marker hover" , async ( { page } ) => {
47+ await page . goto ( "/community/events" )
48+ const mapCanvas = page . locator ( "canvas" ) . first ( )
49+ await expect ( mapCanvas ) . toBeVisible ( { timeout : 10000 } )
50+ await expect
51+ . poll ( async ( ) => {
52+ const box = await mapCanvas . boundingBox ( )
53+ return Boolean ( box && box . width > 100 && box . height > 100 )
54+ } )
55+ . toBe ( true )
56+ const popover = page . locator ( '[data-testid="meetup-map-popover"]' )
57+ await expect ( popover ) . toHaveCount ( 0 )
58+ await mapCanvas . hover ( )
59+ const { clientX, clientY } = await page . evaluate ( ( ) => {
60+ const canvas = document . querySelector ( "canvas" ) as HTMLCanvasElement | null
61+ if ( ! canvas ) throw new Error ( "Canvas not found" )
62+ const targetLat = 51.51
63+ const targetLon = - 0.12
64+ const aspectRatio = 1.65
65+ const cellSize = 8
66+ const mercatorLimit = 85.05112878
67+ const minDisplayedLatitude = - 60
68+ const baseLatitudeOffset = 4
69+ const baseLongitudeOffset = 0.1
70+ const clamp01 = ( value : number ) => {
71+ if ( value <= 0 ) return 0
72+ if ( value >= 1 ) return 1
73+ return value
74+ }
75+ const normalizeLongitude = ( value : number ) => {
76+ let lon = value
77+ while ( lon <= - 180 ) lon += 360
78+ while ( lon > 180 ) lon -= 360
79+ return lon
80+ }
81+ const latToRawV = ( lat : number ) => {
82+ const clampedLat = Math . max ( - mercatorLimit , Math . min ( mercatorLimit , lat ) )
83+ const rad = ( clampedLat * Math . PI ) / 180
84+ return 0.5 - Math . log ( Math . tan ( Math . PI * 0.25 + rad * 0.5 ) ) / ( 2 * Math . PI )
85+ }
86+ const maxProjectedV = latToRawV ( mercatorLimit )
87+ const minProjectedV = latToRawV ( minDisplayedLatitude )
88+ const lonLatToUV = ( lon : number , lat : number ) => {
89+ const adjustedLon = normalizeLongitude ( lon + baseLongitudeOffset )
90+ const u = ( adjustedLon + 180 ) / 360
91+ const adjustedLat = Math . max (
92+ minDisplayedLatitude ,
93+ Math . min ( mercatorLimit , lat + baseLatitudeOffset ) ,
94+ )
95+ const rawV = latToRawV ( adjustedLat )
96+ const normalizedV = clamp01 ( ( rawV - maxProjectedV ) / ( minProjectedV - maxProjectedV ) )
97+ return [ u , normalizedV ] as const
98+ }
99+ const { width, height } = canvas
100+ const pixelRatio = window . devicePixelRatio || 1
101+ const worldHeight = Math . min ( width / aspectRatio , height )
102+ const worldWidth = worldHeight * aspectRatio
103+ const panX = width * 0.5 - worldWidth * 0.5
104+ const panY = height * 0.5 - worldHeight * 0.5
105+ const [ u , v ] = lonLatToUV ( targetLon , targetLat )
106+ const markerY = 1 - v
107+ const screenX = panX + u * worldWidth
108+ const screenY = panY + markerY * worldHeight
109+ const deviceCell = cellSize * pixelRatio
110+ const cellX = Math . floor ( screenX / deviceCell )
111+ const cellY = Math . floor ( screenY / deviceCell )
112+ const centerX = ( cellX + 0.5 ) * deviceCell
113+ const centerY = ( cellY + 0.5 ) * deviceCell
114+ const rect = canvas . getBoundingClientRect ( )
115+ const clientX = rect . left + centerX / pixelRatio
116+ const clientY = rect . bottom - centerY / pixelRatio
117+ return { clientX, clientY }
118+ } )
119+ await page . mouse . move ( clientX , clientY )
120+ await expect ( popover ) . toHaveText ( "London GraphQL" , { timeout : 5000 } )
121+ await expect ( popover ) . toBeVisible ( )
122+ } )
0 commit comments