Automate RC8 release journeys
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
bboxesEqual,
|
||||
normalizeBboxFromCorners,
|
||||
parseBboxInput,
|
||||
resultMetricLabel,
|
||||
selectionAreaSquareMetres,
|
||||
} from './mapWorkspaceUtils'
|
||||
import type { VectorSelectionResponse } from '../../types'
|
||||
|
||||
describe('map workspace selection guards', () => {
|
||||
it('normalizes drag corners into an EPSG:4326 bbox', () => {
|
||||
expect(normalizeBboxFromCorners([5.2, 51.3], [4.8, 50.9])).toEqual({
|
||||
min_x: 4.8,
|
||||
min_y: 50.9,
|
||||
max_x: 5.2,
|
||||
max_y: 51.3,
|
||||
crs: 'EPSG:4326',
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects empty, inverted and degenerate manual selections', () => {
|
||||
expect(parseBboxInput({ min_x: '', min_y: '', max_x: '', max_y: '' })).toBeNull()
|
||||
expect(parseBboxInput({ min_x: '5', min_y: '51', max_x: '4', max_y: '52' })).toBeNull()
|
||||
expect(parseBboxInput({ min_x: '4', min_y: '51', max_x: '4', max_y: '52' })).toBeNull()
|
||||
})
|
||||
|
||||
it('treats sub-nanodegree bbox drift as the same persisted selection', () => {
|
||||
const bbox = normalizeBboxFromCorners([4.98, 51.15], [5.02, 51.19])
|
||||
expect(bboxesEqual(bbox, { ...bbox, max_x: bbox.max_x + 1e-10 })).toBe(true)
|
||||
expect(bboxesEqual(bbox, { ...bbox, max_x: bbox.max_x + 1e-5 })).toBe(false)
|
||||
})
|
||||
|
||||
it('computes a positive bounded selection area and renders governed metrics', () => {
|
||||
const bbox = normalizeBboxFromCorners([5.0, 51.0], [5.01, 51.01])
|
||||
expect(selectionAreaSquareMetres(bbox)).toBeGreaterThan(700_000)
|
||||
expect(selectionAreaSquareMetres(bbox)).toBeLessThan(900_000)
|
||||
|
||||
const result = {
|
||||
feature_count: 12,
|
||||
truncated: false,
|
||||
summary: {
|
||||
metric_key: 'forest_area',
|
||||
metric_value: 14.236,
|
||||
metric_unit: 'ha',
|
||||
},
|
||||
} as unknown as VectorSelectionResponse
|
||||
expect(resultMetricLabel(result)).toBe('14,24 ha')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user