test: add board and postgres safety net
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { CaseState, Evidence, WidgetRelation } from './types'
|
||||
import {
|
||||
MAX_BOARD_ZOOM,
|
||||
MIN_BOARD_ZOOM,
|
||||
clampBoardZoom,
|
||||
containedIds,
|
||||
folderIsOpen,
|
||||
moveBoardPoint,
|
||||
normalizeCase,
|
||||
panViewport,
|
||||
relationPosition,
|
||||
timelinePositionPercent,
|
||||
timelineRange,
|
||||
zoomFromWheel,
|
||||
} from './boardDomain'
|
||||
|
||||
const folder: Evidence = {
|
||||
id: 'folder-1',
|
||||
type: 'folder',
|
||||
title: 'Folder',
|
||||
content: '',
|
||||
x: 200,
|
||||
y: 300,
|
||||
width: 260,
|
||||
config: { open: false },
|
||||
}
|
||||
|
||||
const state: CaseState = {
|
||||
id: 'test-level',
|
||||
title: 'Test',
|
||||
subtitle: '',
|
||||
documents: [],
|
||||
evidence: [folder],
|
||||
relations: [],
|
||||
connections: [],
|
||||
viewport: { x: 10, y: 20, zoom: 0.5 },
|
||||
}
|
||||
|
||||
describe('board coordinate math', () => {
|
||||
it('moves board objects by screen distance divided by zoom', () => {
|
||||
expect(moveBoardPoint({ x: 100, y: 80 }, { x: 50, y: -25 }, 0.5)).toEqual({ x: 200, y: 30 })
|
||||
expect(moveBoardPoint({ x: 100, y: 80 }, { x: 50, y: -25 }, 1.25)).toEqual({ x: 140, y: 60 })
|
||||
})
|
||||
|
||||
it('rejects invalid zoom during coordinate conversion', () => {
|
||||
expect(() => moveBoardPoint({ x: 0, y: 0 }, { x: 10, y: 10 }, 0)).toThrow(RangeError)
|
||||
})
|
||||
|
||||
it('pans in screen coordinates without scaling the delta', () => {
|
||||
expect(panViewport(state.viewport, { x: 25, y: -10 })).toEqual({ x: 35, y: 10, zoom: 0.5 })
|
||||
})
|
||||
|
||||
it('clamps toolbar and pinch zoom to the same limits', () => {
|
||||
expect(clampBoardZoom(-10)).toBe(MIN_BOARD_ZOOM)
|
||||
expect(clampBoardZoom(10)).toBe(MAX_BOARD_ZOOM)
|
||||
expect(zoomFromWheel(1, 10_000)).toBe(MIN_BOARD_ZOOM)
|
||||
expect(zoomFromWheel(1, -10_000)).toBe(MAX_BOARD_ZOOM)
|
||||
})
|
||||
})
|
||||
|
||||
describe('timeline projection', () => {
|
||||
it('creates at least a four-year range around a single year', () => {
|
||||
const range = timelineRange(['2021-03-12'])
|
||||
expect(range.endYear - range.startYear).toBe(4)
|
||||
expect(range.startYear).toBeLessThanOrEqual(2021)
|
||||
expect(range.endYear).toBeGreaterThanOrEqual(2021)
|
||||
})
|
||||
|
||||
it('uses all supplied years when the evidence spans a wider range', () => {
|
||||
expect(timelineRange(['2012-01-01', '2024-06-01'])).toMatchObject({ startYear: 2012, endYear: 2024 })
|
||||
})
|
||||
|
||||
it('projects dates proportionally and clamps dates outside the range', () => {
|
||||
const range = timelineRange(['2020-01-01', '2024-12-31'])
|
||||
expect(timelinePositionPercent('2010-01-01', range)).toBe(0)
|
||||
expect(timelinePositionPercent('2030-01-01', range)).toBe(100)
|
||||
expect(timelinePositionPercent('2022-07-02', range)).toBeGreaterThan(49)
|
||||
expect(timelinePositionPercent('2022-07-02', range)).toBeLessThan(51)
|
||||
})
|
||||
})
|
||||
|
||||
describe('folder domain behavior', () => {
|
||||
const relations: WidgetRelation[] = [
|
||||
{ id: 'later', fromWidgetId: folder.id, toWidgetId: 'doc-2', type: 'contains', sortOrder: 2 },
|
||||
{ id: 'other', fromWidgetId: 'folder-2', toWidgetId: 'doc-x', type: 'contains', sortOrder: 0 },
|
||||
{ id: 'first', fromWidgetId: folder.id, toWidgetId: 'doc-1', type: 'contains', sortOrder: 0 },
|
||||
]
|
||||
|
||||
it('orders and scopes contained documents by their normalized relations', () => {
|
||||
expect(containedIds({ ...state, relations }, folder.id)).toEqual(['doc-1', 'doc-2'])
|
||||
})
|
||||
|
||||
it('only treats an explicit boolean true as open', () => {
|
||||
expect(folderIsOpen(folder)).toBe(false)
|
||||
expect(folderIsOpen({ ...folder, config: { open: true } })).toBe(true)
|
||||
expect(folderIsOpen({ ...folder, config: { open: 'true' } })).toBe(false)
|
||||
})
|
||||
|
||||
it('retains a configured expanded file position', () => {
|
||||
const relation = { ...relations[0], config: { x: 720, y: 415 } }
|
||||
expect(relationPosition({ ...state, relations }, relation)).toEqual({ x: 720, y: 415 })
|
||||
})
|
||||
|
||||
it('derives a deterministic position when a relation has not been moved', () => {
|
||||
expect(relationPosition({ ...state, relations }, relations[2])).toEqual({ x: 550, y: 270 })
|
||||
expect(relationPosition({ ...state, relations }, relations[0])).toEqual({ x: 960, y: 270 })
|
||||
})
|
||||
|
||||
it('normalizes legacy containment without changing source ownership', () => {
|
||||
const legacy = {
|
||||
...state,
|
||||
documents: [{ id: 'doc-1', title: 'Image', kind: 'IMAGE', date: '', body: [], regions: [], mimeType: 'image/png' }],
|
||||
evidence: [{ ...folder, type: 'evidence', sourceDocumentId: 'doc-1', containedDocumentIds: ['doc-1'], config: undefined }],
|
||||
relations: undefined,
|
||||
} as unknown as CaseState
|
||||
const normalized = normalizeCase(legacy)
|
||||
expect(normalized.evidence[0]).toMatchObject({ type: 'folder', config: {}, containedDocumentIds: ['doc-1'] })
|
||||
expect(normalized.documents[0]).toMatchObject({ fileType: 'image', metadata: {} })
|
||||
expect(normalized.relations).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user