import { describe, expect, it } from 'vitest' import type { CaseState, Evidence, WidgetRelation } from './types' import { MAX_BOARD_ZOOM, MIN_BOARD_ZOOM, clampBoardZoom, containedIds, folderIsOpen, moveBoardPoint, nextOpenBoardPosition, 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 = { brief: { body: '', concepts: [] }, 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) }) it('places new exhibits in deterministic open slots', () => { const preferred = { x: 500, y: 400 } expect(nextOpenBoardPosition([], preferred, { width: 280 })).toEqual(preferred) expect(nextOpenBoardPosition([{ x: 500, y: 400, width: 280 }], preferred, { width: 280 })).toEqual({ x: 826, y: 400 }) expect(nextOpenBoardPosition([ { x: 500, y: 400, width: 280 }, { x: 826, y: 400, width: 280 }, ], preferred, { width: 280 })).toEqual({ x: 174, y: 400 }) }) }) 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) }) it('uses a valid configured date range without automatic year padding', () => { const range = timelineRange(['1987-01-08', '1987-10-24'], 2026, { start: '1987-10-01', end: '1987-10-31' }) expect(new Date(range.start).toISOString()).toBe('1987-10-01T00:00:00.000Z') expect(new Date(range.end).toISOString()).toBe('1987-10-31T23:59:59.999Z') expect(timelinePositionPercent('1987-10-16', range)).toBeGreaterThan(48) expect(timelinePositionPercent('1987-10-16', range)).toBeLessThan(52) }) }) 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) }) })