import { act, renderHook } from '@testing-library/react' import { describe, expect, it } from 'vitest' import { useMapRectangleSelection } from './useMapRectangleSelection' import type { VectorSelectionBBox } from '../types' /** * Corner tracking lived inline in a 3.300-line component and was never * exercised. The rule worth pinning is that a first corner produces no * rectangle at all: acting on it would analyse a zero-width area. */ const BBOX: VectorSelectionBBox = { min_x: 5.0, min_y: 51.1, max_x: 5.2, max_y: 51.3, crs: 'EPSG:4326' } function setup(initial: VectorSelectionBBox | null = null) { return renderHook(() => useMapRectangleSelection(initial)) } describe('two-click rectangle', () => { it('yields no rectangle from the first corner', () => { const view = setup() let result: VectorSelectionBBox | null = BBOX act(() => { result = view.result.current.placeCorner([5.0, 51.1]) }) expect(result).toBeNull() expect(view.result.current.firstCorner).toEqual([5.0, 51.1]) }) it('yields the normalised rectangle from the second corner', () => { const view = setup() act(() => void view.result.current.placeCorner([5.2, 51.3])) let result: VectorSelectionBBox | null = null act(() => { result = view.result.current.placeCorner([5.0, 51.1]) }) expect(result).not.toBeNull() expect(result!.min_x).toBeCloseTo(5.0) expect(result!.min_y).toBeCloseTo(51.1) expect(result!.max_x).toBeCloseTo(5.2) expect(result!.max_y).toBeCloseTo(51.3) }) it('leaves no half-finished gesture behind', () => { const view = setup() act(() => void view.result.current.setDrawing(true)) act(() => void view.result.current.placeCorner([5.2, 51.3])) act(() => void view.result.current.placeCorner([5.0, 51.1])) expect(view.result.current.firstCorner).toBeNull() expect(view.result.current.drawing).toBe(false) }) it('normalises corners placed in any order', () => { const view = setup() act(() => void view.result.current.placeCorner([5.0, 51.3])) let result: VectorSelectionBBox | null = null act(() => { result = view.result.current.placeCorner([5.2, 51.1]) }) expect(result!.min_x).toBeLessThan(result!.max_x) expect(result!.min_y).toBeLessThan(result!.max_y) }) }) describe('coordinate fields', () => { it('starts from the rectangle already on the map', () => { const view = setup(BBOX) expect(view.result.current.typedBbox).toMatchObject({ min_x: 5.0, max_y: 51.3 }) }) it('parses a complete rectangle', () => { const view = setup() act(() => view.result.current.setBboxInput({ min_x: '5.0', min_y: '51.1', max_x: '5.2', max_y: '51.3' }), ) expect(view.result.current.typedBbox).toMatchObject({ min_x: 5.0, max_y: 51.3 }) }) it('reports no rectangle while the fields are incomplete', () => { const view = setup() act(() => view.result.current.setBboxInput({ min_x: '5.0', min_y: '', max_x: '', max_y: '' })) expect(view.result.current.typedBbox).toBeNull() }) it('supports an updater so one field can change at a time', () => { const view = setup(BBOX) act(() => view.result.current.setBboxInput((previous) => ({ ...previous, max_x: '5.4' }))) expect(view.result.current.typedBbox).toMatchObject({ min_x: 5.0, max_x: 5.4 }) }) it('shows a rectangle that was set elsewhere', () => { const view = setup() act(() => view.result.current.showBbox(BBOX)) expect(view.result.current.typedBbox).toMatchObject({ min_x: 5.0, max_y: 51.3 }) }) it('clears the fields when the selection is cleared', () => { const view = setup(BBOX) act(() => view.result.current.showBbox(null)) expect(view.result.current.typedBbox).toBeNull() }) }) describe('reset', () => { it('abandons a half-drawn rectangle', () => { const view = setup() act(() => void view.result.current.placeCorner([5.0, 51.1])) act(() => view.result.current.reset()) expect(view.result.current.firstCorner).toBeNull() expect(view.result.current.drawing).toBe(false) }) })