Automate RC8 release journeys
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-18 06:07:44 +02:00
parent 0fae53a7de
commit 55685ba1bd
22 changed files with 2665 additions and 11 deletions
@@ -0,0 +1,98 @@
import { act, renderHook } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { CoverageResolveResponse, VectorSelectionBBox } from '../types'
const mocks = vi.hoisted(() => ({
resolveCoverage: vi.fn(),
}))
vi.mock('../services/api', () => ({
externalApi: {
resolveCoverage: mocks.resolveCoverage,
},
}))
import { useCoverageResolver } from './useCoverageResolver'
const bbox: VectorSelectionBBox = {
min_x: 4.98,
min_y: 51.15,
max_x: 5.02,
max_y: 51.19,
crs: 'EPSG:4326',
}
const coverageResult = {
intersected_zones: ['flanders'],
outside_supported_scope: false,
items: [],
warnings: [],
} as unknown as CoverageResolveResponse
describe('useCoverageResolver', () => {
beforeEach(() => {
vi.useFakeTimers()
mocks.resolveCoverage.mockResolvedValue(coverageResult)
})
afterEach(() => {
vi.useRealTimers()
})
it('does not query until both project and bbox exist', async () => {
const { result, rerender } = renderHook(
({ projectId, selection }) => useCoverageResolver({ projectId, bbox: selection }),
{ initialProps: { projectId: 'project-1' as string | null, selection: null as VectorSelectionBBox | null } },
)
await act(async () => {
await vi.advanceTimersByTimeAsync(500)
})
expect(mocks.resolveCoverage).not.toHaveBeenCalled()
expect(result.current.loadingCoverage).toBe(false)
rerender({ projectId: 'project-1', selection: bbox })
await act(async () => {
await vi.advanceTimersByTimeAsync(249)
})
expect(mocks.resolveCoverage).not.toHaveBeenCalled()
await act(async () => {
await vi.advanceTimersByTimeAsync(1)
})
expect(result.current.coverage).toEqual(coverageResult)
expect(mocks.resolveCoverage).toHaveBeenCalledWith({
projectId: 'project-1',
bbox: { minx: 4.98, miny: 51.15, maxx: 5.02, maxy: 51.19 },
})
})
it('clears stale coverage when the selection is removed', async () => {
const { result, rerender } = renderHook(
({ selection }) => useCoverageResolver({ projectId: 'project-1', bbox: selection }),
{ initialProps: { selection: bbox as VectorSelectionBBox | null } },
)
await act(async () => {
await vi.advanceTimersByTimeAsync(250)
})
expect(result.current.coverage).toEqual(coverageResult)
rerender({ selection: null })
expect(result.current.coverage).toBeNull()
expect(result.current.coverageError).toBeNull()
expect(result.current.loadingCoverage).toBe(false)
})
it('exposes provider failures without retaining stale results', async () => {
mocks.resolveCoverage.mockRejectedValueOnce(new Error('provider unavailable'))
const { result } = renderHook(() => useCoverageResolver({ projectId: 'project-1', bbox }))
await act(async () => {
await vi.advanceTimersByTimeAsync(250)
})
expect(result.current.coverageError).toBe('provider unavailable')
expect(result.current.coverage).toBeNull()
expect(result.current.loadingCoverage).toBe(false)
})
})
@@ -0,0 +1,72 @@
import { act, renderHook } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import type { TemporalComparisonResponse, VectorSelectionBBox } from '../types'
const mocks = vi.hoisted(() => ({
compare: vi.fn(),
}))
vi.mock('../services/api/temporal', () => ({
temporalApi: {
compare: mocks.compare,
},
}))
import { useTemporalComparison } from './useTemporalComparison'
const bbox: VectorSelectionBBox = {
min_x: 4.98,
min_y: 51.15,
max_x: 5.02,
max_y: 51.19,
crs: 'EPSG:4326',
}
describe('useTemporalComparison', () => {
it('fails locally when no project is active', async () => {
const { result } = renderHook(() => useTemporalComparison(null))
let response: TemporalComparisonResponse | null = null
await act(async () => {
response = await result.current.compareTemporalSnapshots('earlier', 'later', bbox)
})
expect(response).toBeNull()
expect(result.current.temporalComparisonError).toContain('project')
expect(mocks.compare).not.toHaveBeenCalled()
})
it('requires two explicit snapshots before calling the API', async () => {
const { result } = renderHook(() => useTemporalComparison('project-1'))
await act(async () => {
await result.current.compareTemporalSnapshots('', 'later', bbox)
})
expect(result.current.temporalComparisonError).toContain('twee meetmomenten')
expect(mocks.compare).not.toHaveBeenCalled()
})
it('persists the compatible comparison result in hook state', async () => {
const comparison = {
project_id: 'project-1',
earlier_dataset_id: 'earlier',
later_dataset_id: 'later',
warnings: [],
} as unknown as TemporalComparisonResponse
mocks.compare.mockResolvedValueOnce(comparison)
const { result } = renderHook(() => useTemporalComparison('project-1'))
let response: TemporalComparisonResponse | null = null
await act(async () => {
response = await result.current.compareTemporalSnapshots('earlier', 'later', bbox, 'area-1')
})
expect(response).toEqual(comparison)
expect(result.current.temporalComparison).toEqual(comparison)
expect(result.current.temporalComparisonError).toBeNull()
expect(mocks.compare).toHaveBeenCalledWith('project-1', {
earlier_dataset_id: 'earlier',
later_dataset_id: 'later',
bbox,
area_id: 'area-1',
preview_limit: 500,
})
})
})
@@ -0,0 +1,66 @@
import { renderHook, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { useWorkbenchBootstrap } from './useWorkbenchBootstrap'
function action() {
return vi.fn().mockResolvedValue(undefined)
}
function options(selectedProjectId: string | null) {
return {
selectedProjectId,
selectedDetectionRunId: '',
detectionClassFilter: '',
detectionMinConfidenceFilter: 0,
selectedSegmentationRunId: '',
segmentationClassFilter: '',
segmentationMinConfidenceFilter: 0,
loadProjects: action(),
loadCapabilities: action(),
loadDetectionModels: action(),
loadSegmentationModels: action(),
loadProjectData: action(),
loadDetectionRuns: action(),
loadSegmentationRuns: action(),
loadQualityChecks: action(),
loadExports: action(),
loadDetectionResults: action(),
loadSegmentationResults: action(),
resetProjectData: vi.fn(),
resetDatasetForProject: vi.fn(),
resetDetectionForProject: vi.fn(),
resetSegmentationForProject: vi.fn(),
resetExportsForProject: vi.fn(),
}
}
describe('useWorkbenchBootstrap', () => {
it('loads global capabilities and clears project-owned state without a project', async () => {
const state = options(null)
renderHook(() => useWorkbenchBootstrap(state))
await waitFor(() => expect(state.loadProjects).toHaveBeenCalledOnce())
expect(state.loadCapabilities).toHaveBeenCalledOnce()
expect(state.loadDetectionModels).toHaveBeenCalledOnce()
expect(state.loadSegmentationModels).toHaveBeenCalledOnce()
expect(state.resetProjectData).toHaveBeenCalledOnce()
expect(state.resetDatasetForProject).toHaveBeenCalledOnce()
expect(state.loadProjectData).not.toHaveBeenCalled()
})
it('resets stale state before loading every project-owned collection', async () => {
const state = options('project-1')
renderHook(() => useWorkbenchBootstrap(state))
await waitFor(() => expect(state.loadProjectData).toHaveBeenCalledWith('project-1'))
expect(state.resetProjectData).toHaveBeenCalledOnce()
expect(state.resetDatasetForProject).toHaveBeenCalledOnce()
expect(state.resetDetectionForProject).toHaveBeenCalledOnce()
expect(state.resetSegmentationForProject).toHaveBeenCalledOnce()
expect(state.resetExportsForProject).toHaveBeenCalledOnce()
expect(state.loadDetectionRuns).toHaveBeenCalledWith('project-1')
expect(state.loadSegmentationRuns).toHaveBeenCalledWith('project-1')
expect(state.loadQualityChecks).toHaveBeenCalledWith('project-1')
expect(state.loadExports).toHaveBeenCalledWith('project-1')
})
})