import { render, screen, fireEvent } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import React from 'react'; // Use doMock to avoid hoisting issues and potential caching conflicts vi.doMock('../google-maps-loader', () => ({ useGoogleMapsLoader: () => ({ isLoaded: true, loadError: undefined, load: async () => {}, }), })); describe('Map', () => { let Map: any; const mockMap = { setCenter: vi.fn(), setZoom: vi.fn(), getZoom: vi.fn(() => 10), fitBounds: vi.fn(), addListener: vi.fn(() => ({ remove: vi.fn() })), }; const mockInfoWindow = { open: vi.fn(), close: vi.fn(), setContent: vi.fn(), addListener: vi.fn(() => ({ remove: vi.fn() })), }; beforeEach(async () => { vi.clearAllMocks(); // Dynamically import Map after doMock const module = await import('./map'); Map = module.Map; // Mock Google Maps Globals global.google = { maps: { Map: vi.fn(function () { return mockMap; }), InfoWindow: vi.fn(function () { return mockInfoWindow; }), Circle: vi.fn(function () { return { setMap: vi.fn() }; }), Polygon: vi.fn(function () { return { setMap: vi.fn() }; }), LatLngBounds: vi.fn(function () { return { extend: vi.fn() }; }), event: { addListener: vi.fn(() => ({ remove: vi.fn() })), addListenerOnce: vi.fn(() => ({ remove: vi.fn() })), }, marker: { AdvancedMarkerElement: vi.fn(), }, }, } as any; }); afterEach(() => { delete (global as any).google; vi.resetModules(); }); it('renders correctly when loaded', () => { render(); const mapElement = document.querySelector('gmp-map'); expect(mapElement).toBeInTheDocument(); }); it('renders markers correctly', () => { const markers = [ { position: { lat: 10, lng: 20 }, title: 'Marker 1' }, { position: { lat: 30, lng: 40 }, title: 'Marker 2' }, ]; render(); const advancedMarkers = document.querySelectorAll('gmp-advanced-marker'); expect(advancedMarkers.length).toBe(2); expect(advancedMarkers[0]).toHaveAttribute('title', 'Marker 1'); }); /* --- Backwards compatibility ------------------------------------------- */ describe('backwards compatibility', () => { it('still renders the teardrop pin when no shape is given', () => { render(); const pin = screen.getByRole('button'); expect(pin).toHaveStyle({ borderRadius: '50% 50% 50% 0' }); expect(pin).toHaveTextContent('A'); }); it('honors the literal customColor prop', () => { render( ); expect(screen.getByRole('button')).toHaveStyle({ backgroundColor: '#ff0000' }); }); it('falls back to the demo map id when none is supplied', () => { render(); expect(document.querySelector('gmp-map')).toHaveAttribute('map-id', 'DEMO_MAP_ID'); }); it('uses a supplied mapId', () => { render(); expect(document.querySelector('gmp-map')).toHaveAttribute('map-id', 'my-cloud-style'); }); }); /* --- B1 proportional symbology ------------------------------------------ */ describe('proportional markers', () => { it('sizes a circle marker from its scale', () => { render( ); // sqrt(1) = 1 -> the top of the range -> 20px radius -> 40px box. expect(screen.getByRole('button')).toHaveStyle({ width: '40px', height: '40px' }); }); it('honors a fixed radiusPx', () => { render( ); expect(screen.getByRole('button')).toHaveStyle({ width: '12px', height: '12px' }); }); it('renders a square shape without a circular radius', () => { render( ); expect(screen.getByRole('button')).toHaveAttribute('data-shape', 'square'); }); }); /* --- B5 selection and events -------------------------------------------- */ describe('selection and events', () => { const markers = [ { id: 'porto-alegre', position: { lat: -30.03, lng: -51.23 }, title: 'Porto Alegre' }, { id: 'gravatai', position: { lat: -29.94, lng: -50.99 }, title: 'Gravataí' }, ]; it('exposes every marker as a labelled button', () => { render(); expect(screen.getByRole('button', { name: 'Porto Alegre' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Gravataí' })).toBeInTheDocument(); }); it('makes markers reachable by keyboard', () => { render(); expect(screen.getByRole('button', { name: 'Gravataí' })).toHaveAttribute('tabindex', '0'); }); it('reports the selected feature on click', () => { const onFeatureSelect = vi.fn(); render(); fireEvent.click(screen.getByRole('button', { name: 'Gravataí' })); expect(onFeatureSelect).toHaveBeenCalledWith( 'gravatai', expect.objectContaining({ id: 'gravatai', kind: 'marker', index: 1 }) ); }); it('activates on Enter and on Space', () => { const onFeatureSelect = vi.fn(); render(); const target = screen.getByRole('button', { name: 'Porto Alegre' }); fireEvent.keyDown(target, { key: 'Enter' }); expect(onFeatureSelect).toHaveBeenCalledTimes(1); // The activation guard collapses repeats inside one tick, so advance time. vi.setSystemTime(Date.now() + 100); fireEvent.keyDown(target, { key: ' ' }); expect(onFeatureSelect).toHaveBeenCalledTimes(2); vi.useRealTimers(); }); it('reflects a controlled selectedId with aria-pressed', () => { render( {}} /> ); expect(screen.getByRole('button', { name: 'Gravataí' })).toHaveAttribute( 'aria-pressed', 'true' ); expect(screen.getByRole('button', { name: 'Porto Alegre' })).toHaveAttribute( 'aria-pressed', 'false' ); }); it('omits aria-pressed when selection is not wired up', () => { render(); expect(screen.getByRole('button', { name: 'Gravataí' })).not.toHaveAttribute('aria-pressed'); }); it('reports hover in and hover out', () => { const onFeatureHover = vi.fn(); render(); const target = screen.getByRole('button', { name: 'Gravataí' }); fireEvent.mouseEnter(target); expect(onFeatureHover).toHaveBeenLastCalledWith( 'gravatai', expect.objectContaining({ id: 'gravatai' }) ); fireEvent.mouseLeave(target); expect(onFeatureHover).toHaveBeenLastCalledWith(null, null); }); it('does not accumulate click listeners across re-renders', () => { const onFeatureSelect = vi.fn(); const { rerender } = render( ); // The old implementation added a fresh gmp-click listener on every render. for (let i = 0; i < 5; i += 1) { rerender(); } const element = document.querySelectorAll('gmp-advanced-marker')[1]; element.dispatchEvent(new Event('gmp-click')); expect(onFeatureSelect).toHaveBeenCalledTimes(1); }); }); /* --- F2 tooltip ---------------------------------------------------------- */ it('renders a tooltip trigger when a marker declares one', () => { render( Gravataí — 4 h/dia, }, ]} /> ); expect(screen.getByRole('button', { name: 'Gravataí' })).toHaveAttribute('data-state'); }); /* --- A3 container states -------------------------------------------------- */ describe('container states', () => { it('renders the empty state instead of the map', () => { render(); expect(screen.getByText('No results for the current filters')).toBeInTheDocument(); expect(document.querySelector('gmp-map')).not.toBeInTheDocument(); }); it('accepts a custom empty state', () => { render(Nenhum município no filtro

} />); expect(screen.getByText('Nenhum município no filtro')).toBeInTheDocument(); }); it('renders the loading skeleton when loading is forced', () => { const { container } = render(); expect(container.querySelector('.animate-pulse')).toBeInTheDocument(); expect(document.querySelector('gmp-map')).not.toBeInTheDocument(); }); it('lets the denied state outrank every other state', () => { render( data error

} deniedState={

Sem permissão para estes dados

} /> ); expect(screen.getByText('Sem permissão para estes dados')).toBeInTheDocument(); expect(screen.queryByText('data error')).not.toBeInTheDocument(); }); it('lets the error state outrank loading and empty', () => { render(Falha ao carregar

} />); expect(screen.getByText('Falha ao carregar')).toBeInTheDocument(); }); it('keeps the container height across states', () => { const { container } = render(); expect(container.firstChild).toHaveStyle({ height: '520px' }); }); }); /* --- F4 attribution ------------------------------------------------------- */ it('renders the cartographic attribution over the map', () => { render(); expect(screen.getByText('© OpenStreetMap contributors')).toBeInTheDocument(); }); /* --- Keyless basemap ------------------------------------------------------ */ describe('basemap="none"', () => { it('skips the Google basemap even when the script is loaded and keyed', () => { render( ); expect(document.querySelector('gmp-map')).not.toBeInTheDocument(); expect(screen.getByRole('status')).toHaveTextContent('Base cartográfica indisponível'); }); it('keeps the markers interactive', () => { const onFeatureSelect = vi.fn(); Object.defineProperty(HTMLElement.prototype, 'clientWidth', { configurable: true, value: 800, }); Object.defineProperty(HTMLElement.prototype, 'clientHeight', { configurable: true, value: 600, }); render( ); fireEvent.click(screen.getByRole('button', { name: 'Porto Alegre' })); expect(onFeatureSelect).toHaveBeenCalledWith('a', expect.objectContaining({ id: 'a' })); }); }); /* --- Accessibility -------------------------------------------------------- */ it('labels the map region', () => { render(); expect( screen.getByRole('region', { name: 'Mapa de unidades por município' }) ).toBeInTheDocument(); }); });