Files
geointel/frontend/src/hooks/useMapRectangleSelection.test.tsx
T
JensandClaude Opus 5 6177eecef5 extract and test the map rectangle interaction state
Corner tracking for a two-click rectangle lived inline in the component and
was never exercised. The rule worth pinning is that a first corner yields no
rectangle at all: acting on it would analyse a zero-width area.

The hook holds only interaction state — drawing mode, placed corner, the
coordinate fields. What happens with a finished rectangle stays in the
workspace, which owns retiring stale results and starting the analysis.
Injecting that would have made the hook depend on values declared after it,
which is what a first attempt at a wider extraction ran into.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 19:01:45 +02:00

134 lines
4.0 KiB
TypeScript

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)
})
})