| import { devices, expect, test } from '@playwright/test'; |
|
|
| type HarnessWindow = Window & { |
| __mobileMapHarness?: { |
| ready: boolean; |
| getPopupRect: () => { |
| left: number; |
| top: number; |
| right: number; |
| bottom: number; |
| width: number; |
| height: number; |
| viewportWidth: number; |
| viewportHeight: number; |
| } | null; |
| getFirstHotspotRect: () => { |
| width: number; |
| height: number; |
| } | null; |
| }; |
| __mobileMapIntegrationHarness?: { |
| ready: boolean; |
| pendingDeferredCallbacks: number; |
| flushDeferredCallbacks: () => void; |
| destroyMap: () => void; |
| getDynamicLayerChildCount: () => number; |
| getInitialDynamicRendered: () => boolean; |
| getWrapperTransform: () => string; |
| getPopupRect: () => { |
| left: number; |
| top: number; |
| right: number; |
| bottom: number; |
| width: number; |
| height: number; |
| viewportWidth: number; |
| viewportHeight: number; |
| } | null; |
| }; |
| }; |
|
|
| const MOBILE_DEVICE_MATRIX = [ |
| { label: 'iPhone SE', use: devices['iPhone SE'] }, |
| { label: 'iPhone 14 Pro Max', use: devices['iPhone 14 Pro Max'] }, |
| { label: 'Pixel 5', use: devices['Pixel 5'] }, |
| { label: 'Galaxy S9+', use: devices['Galaxy S9+'] }, |
| ] as const; |
|
|
| for (const deviceConfig of MOBILE_DEVICE_MATRIX) { |
| test.describe(`Mobile SVG popup QA (${deviceConfig.label})`, () => { |
| const { defaultBrowserType: _defaultBrowserType, ...contextOptions } = deviceConfig.use; |
| test.use(contextOptions); |
|
|
| test('keeps popup in viewport and supports dismissal patterns', async ({ page }) => { |
| const pageErrors: string[] = []; |
| page.on('pageerror', (error) => pageErrors.push(error.message)); |
|
|
| await page.goto('/tests/mobile-map-harness.html'); |
|
|
| await expect |
| .poll(async () => { |
| return await page.evaluate(() => { |
| const w = window as HarnessWindow; |
| return Boolean(w.__mobileMapHarness?.ready); |
| }); |
| }, { timeout: 20000 }) |
| .toBe(true); |
|
|
| const hotspotRect = await page.evaluate(() => { |
| const w = window as HarnessWindow; |
| return w.__mobileMapHarness?.getFirstHotspotRect() ?? null; |
| }); |
|
|
| expect(hotspotRect).not.toBeNull(); |
| expect(hotspotRect?.width ?? 0).toBeGreaterThanOrEqual(44); |
| expect(hotspotRect?.height ?? 0).toBeGreaterThanOrEqual(44); |
|
|
| const hotspot = page.locator('.hotspot').first(); |
| await expect(hotspot).toBeVisible(); |
| await hotspot.tap(); |
|
|
| const popup = page.locator('.map-popup.map-popup-sheet'); |
| await expect(popup).toBeVisible(); |
|
|
| await expect |
| .poll(async () => { |
| return await page.evaluate(() => { |
| const w = window as HarnessWindow; |
| const rect = w.__mobileMapHarness?.getPopupRect(); |
| if (!rect) return false; |
| return ( |
| rect.left >= 0 && |
| rect.top >= 0 && |
| rect.right <= rect.viewportWidth + 1 && |
| rect.bottom <= rect.viewportHeight + 1 |
| ); |
| }); |
| }, { timeout: 5000 }) |
| .toBe(true); |
|
|
| const popupRect = await page.evaluate(() => { |
| const w = window as HarnessWindow; |
| return w.__mobileMapHarness?.getPopupRect() ?? null; |
| }); |
|
|
| expect(popupRect).not.toBeNull(); |
| expect(popupRect?.left ?? -1).toBeGreaterThanOrEqual(0); |
| expect(popupRect?.top ?? -1).toBeGreaterThanOrEqual(0); |
| expect((popupRect?.right ?? 0) - (popupRect?.viewportWidth ?? 0)).toBeLessThanOrEqual(1); |
| expect((popupRect?.bottom ?? 0) - (popupRect?.viewportHeight ?? 0)).toBeLessThanOrEqual(1); |
|
|
| const dragPopupBy = async (distance: number): Promise<void> => { |
| await page.evaluate((dragDistance) => { |
| const popupEl = document.querySelector('.map-popup.map-popup-sheet') as HTMLElement | null; |
| const handle = document.querySelector('.map-popup-sheet-handle') as HTMLElement | null; |
| if (!popupEl || !handle || typeof Touch === 'undefined') return; |
|
|
| const rect = handle.getBoundingClientRect(); |
| const x = rect.left + rect.width / 2; |
| const startY = rect.top + rect.height / 2; |
| const endY = startY + dragDistance; |
| const target = handle; |
|
|
| const makeTouch = (y: number): Touch => |
| new Touch({ |
| identifier: 42, |
| target, |
| clientX: x, |
| clientY: y, |
| pageX: x, |
| pageY: y, |
| screenX: x, |
| screenY: y, |
| radiusX: 2, |
| radiusY: 2, |
| rotationAngle: 0, |
| force: 0.5, |
| }); |
|
|
| const startTouch = makeTouch(startY); |
| target.dispatchEvent( |
| new TouchEvent('touchstart', { |
| bubbles: true, |
| cancelable: true, |
| touches: [startTouch], |
| targetTouches: [startTouch], |
| changedTouches: [startTouch], |
| }) |
| ); |
|
|
| const moveTouch = makeTouch(endY); |
| target.dispatchEvent( |
| new TouchEvent('touchmove', { |
| bubbles: true, |
| cancelable: true, |
| touches: [moveTouch], |
| targetTouches: [moveTouch], |
| changedTouches: [moveTouch], |
| }) |
| ); |
|
|
| target.dispatchEvent( |
| new TouchEvent('touchend', { |
| bubbles: true, |
| cancelable: true, |
| touches: [], |
| targetTouches: [], |
| changedTouches: [moveTouch], |
| }) |
| ); |
| }, distance); |
| }; |
|
|
| await dragPopupBy(48); |
| await expect(page.locator('.map-popup.map-popup-sheet')).toBeVisible(); |
| await expect |
| .poll(async () => { |
| return await page.evaluate(() => { |
| const popupEl = document.querySelector('.map-popup.map-popup-sheet') as HTMLElement | null; |
| return popupEl?.style.transform ?? null; |
| }); |
| }, { timeout: 2000 }) |
| .toBe(''); |
|
|
| await dragPopupBy(150); |
| await expect(page.locator('.map-popup')).toHaveCount(0); |
|
|
| await hotspot.tap(); |
| await expect(page.locator('.map-popup.map-popup-sheet')).toBeVisible(); |
| await page.locator('.popup-close').first().tap(); |
| await expect(page.locator('.map-popup')).toHaveCount(0); |
|
|
| await hotspot.tap(); |
| await expect(page.locator('.map-popup.map-popup-sheet')).toBeVisible(); |
|
|
| await page.touchscreen.tap(6, 6); |
| await expect(page.locator('.map-popup')).toHaveCount(0); |
|
|
| expect(pageErrors).toEqual([]); |
| }); |
| }); |
| } |
|
|
| test.describe('Mobile SVG popup integration path', () => { |
| const { defaultBrowserType: _defaultBrowserType, ...iphoneSE } = devices['iPhone SE']; |
| test.use(iphoneSE); |
|
|
| test('opens popup through MapComponent hotspot marker tap', async ({ page }) => { |
| const pageErrors: string[] = []; |
| page.on('pageerror', (error) => pageErrors.push(error.message)); |
|
|
| await page.goto('/tests/mobile-map-integration-harness.html'); |
|
|
| await expect |
| .poll(async () => { |
| return await page.evaluate(() => { |
| const w = window as HarnessWindow; |
| return Boolean(w.__mobileMapIntegrationHarness?.ready); |
| }); |
| }, { timeout: 30000 }) |
| .toBe(true); |
|
|
| const timeSlider = page.locator('.time-slider'); |
| const mapControls = page.locator('.map-controls'); |
| await expect(timeSlider).toBeVisible(); |
| await expect(mapControls).toBeVisible(); |
| const controlsDoNotOverlap = await page.evaluate(() => { |
| const slider = document.querySelector('.time-slider') as HTMLElement | null; |
| const controls = document.querySelector('.map-controls') as HTMLElement | null; |
| if (!slider || !controls) return false; |
| const sliderRect = slider.getBoundingClientRect(); |
| const controlsRect = controls.getBoundingClientRect(); |
| return sliderRect.right <= controlsRect.left + 1; |
| }); |
| expect(controlsDoNotOverlap).toBe(true); |
|
|
| const hotspot = page.locator('.hotspot').first(); |
| await expect(hotspot).toBeVisible(); |
| await hotspot.tap(); |
|
|
| const popup = page.locator('.map-popup.map-popup-sheet'); |
| await expect(popup).toBeVisible(); |
|
|
| await expect |
| .poll(async () => { |
| return await page.evaluate(() => { |
| const w = window as HarnessWindow; |
| const rect = w.__mobileMapIntegrationHarness?.getPopupRect(); |
| if (!rect) return false; |
| return ( |
| rect.left >= 0 && |
| rect.top >= 0 && |
| rect.right <= rect.viewportWidth + 1 && |
| rect.bottom <= rect.viewportHeight + 1 |
| ); |
| }); |
| }, { timeout: 5000 }) |
| .toBe(true); |
|
|
| const popupRect = await page.evaluate(() => { |
| const w = window as HarnessWindow; |
| return w.__mobileMapIntegrationHarness?.getPopupRect() ?? null; |
| }); |
|
|
| expect(popupRect).not.toBeNull(); |
| expect(popupRect?.left ?? -1).toBeGreaterThanOrEqual(0); |
| expect(popupRect?.top ?? -1).toBeGreaterThanOrEqual(0); |
| expect((popupRect?.right ?? 0) - (popupRect?.viewportWidth ?? 0)).toBeLessThanOrEqual(1); |
| expect((popupRect?.bottom ?? 0) - (popupRect?.viewportHeight ?? 0)).toBeLessThanOrEqual(1); |
|
|
| await popup.locator('.popup-close').first().tap(); |
| await expect(page.locator('.map-popup')).toHaveCount(0); |
|
|
| expect(pageErrors).toEqual([]); |
| }); |
|
|
| test('defers overlays while preserving transform and guarding destroy', async ({ page }) => { |
| const pageErrors: string[] = []; |
| page.on('pageerror', (error) => pageErrors.push(error.message)); |
|
|
| await page.goto('/tests/mobile-map-integration-harness.html?defer-control=1'); |
|
|
| await expect |
| .poll(async () => { |
| return await page.evaluate(() => document.querySelectorAll('.country').length); |
| }, { timeout: 30000 }) |
| .toBeGreaterThan(0); |
|
|
| await expect |
| .poll(async () => { |
| return await page.evaluate(() => { |
| const w = window as HarnessWindow; |
| return w.__mobileMapIntegrationHarness?.pendingDeferredCallbacks ?? 0; |
| }); |
| }, { timeout: 10000 }) |
| .toBeGreaterThan(0); |
|
|
| const transformBeforeFlush = await page.evaluate(() => { |
| const w = window as HarnessWindow; |
| return w.__mobileMapIntegrationHarness?.getWrapperTransform() ?? ''; |
| }); |
| expect(transformBeforeFlush).toContain('scale(2.7)'); |
|
|
| const dynamicBeforeFlush = await page.evaluate(() => { |
| const w = window as HarnessWindow; |
| return w.__mobileMapIntegrationHarness?.getDynamicLayerChildCount() ?? -1; |
| }); |
| expect(dynamicBeforeFlush).toBe(0); |
|
|
| await page.evaluate(() => { |
| const w = window as HarnessWindow; |
| w.__mobileMapIntegrationHarness?.destroyMap(); |
| w.__mobileMapIntegrationHarness?.flushDeferredCallbacks(); |
| }); |
|
|
| const initialDynamicRendered = await page.evaluate(() => { |
| const w = window as HarnessWindow; |
| return w.__mobileMapIntegrationHarness?.getInitialDynamicRendered() ?? true; |
| }); |
| expect(initialDynamicRendered).toBe(false); |
| expect(pageErrors).toEqual([]); |
| }); |
| }); |
|
|