Files
home-automation/frontend/src/map/mapUtils.test.ts
T
tliu93 32d93bba2a M2-T09: build data visualization UI (heatmap map as home page)
- self-contained RecordsMap (only module importing leaflet/react-leaflet/
  leaflet.heat/leaflet.markercluster); OSM tiles, swappable behind clean props
- heatmap layers for location + poo (primary); time-range selector fetches
  only the window (locations server-filtered; poo client-filtered)
- toggleable scatter layer with marker clustering; point-select reuses T10's
  edit/delete modals + hooks; query-key prefixes refresh map on mutation
- pure map logic isolated + unit-tested; leaflet mocked in component tests
- responsive layout; typed client only
2026-06-13 15:20:50 +02:00

197 lines
6.0 KiB
TypeScript

/**
* Unit tests for mapUtils.ts — pure logic, no leaflet, runs in jsdom.
*/
import { describe, it, expect } from 'vitest'
import {
locationsToHeatPoints,
pooToHeatPoints,
locationsToMapPoints,
pooToMapPoints,
filterPooByTimeWindow,
computeCenter,
daysAgoISO,
} from './mapUtils'
import type { LocationRecord, PooRecord } from '../records'
// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------
const loc1: LocationRecord = {
person: 'alice',
datetime: '2026-01-15T10:00:00Z',
latitude: 39.9,
longitude: 116.4,
altitude: 50,
}
const loc2: LocationRecord = {
person: 'alice',
datetime: '2026-01-20T12:00:00Z',
latitude: 39.95,
longitude: 116.45,
altitude: null,
}
const poo1: PooRecord = {
timestamp: '2026-01-10T08:00:00Z',
status: 'done',
latitude: 39.91,
longitude: 116.41,
}
const poo2: PooRecord = {
timestamp: '2026-01-20T09:00:00Z',
status: 'done',
latitude: 39.92,
longitude: 116.42,
}
const poo3: PooRecord = {
timestamp: '2026-02-01T09:00:00Z',
status: 'done',
latitude: 39.93,
longitude: 116.43,
}
// ---------------------------------------------------------------------------
// locationsToHeatPoints
// ---------------------------------------------------------------------------
describe('locationsToHeatPoints', () => {
it('converts records to [lat, lng, 1] tuples', () => {
const pts = locationsToHeatPoints([loc1, loc2])
expect(pts).toHaveLength(2)
expect(pts[0]).toEqual([39.9, 116.4, 1])
expect(pts[1]).toEqual([39.95, 116.45, 1])
})
it('returns empty array for empty input', () => {
expect(locationsToHeatPoints([])).toEqual([])
})
})
// ---------------------------------------------------------------------------
// pooToHeatPoints
// ---------------------------------------------------------------------------
describe('pooToHeatPoints', () => {
it('converts poo records to heat points', () => {
const pts = pooToHeatPoints([poo1])
expect(pts).toHaveLength(1)
expect(pts[0]).toEqual([39.91, 116.41, 1])
})
})
// ---------------------------------------------------------------------------
// locationsToMapPoints
// ---------------------------------------------------------------------------
describe('locationsToMapPoints', () => {
it('attaches original record to each point', () => {
const pts = locationsToMapPoints([loc1])
expect(pts).toHaveLength(1)
expect(pts[0].lat).toBe(39.9)
expect(pts[0].lng).toBe(116.4)
expect(pts[0].record).toBe(loc1)
})
})
// ---------------------------------------------------------------------------
// pooToMapPoints
// ---------------------------------------------------------------------------
describe('pooToMapPoints', () => {
it('attaches original poo record to each point', () => {
const pts = pooToMapPoints([poo1])
expect(pts[0].record).toBe(poo1)
})
})
// ---------------------------------------------------------------------------
// filterPooByTimeWindow — client-side time filter
// ---------------------------------------------------------------------------
describe('filterPooByTimeWindow', () => {
const records = [poo1, poo2, poo3]
// timestamps: 2026-01-10, 2026-01-20, 2026-02-01
it('returns all records when start and end are both null', () => {
expect(filterPooByTimeWindow(records, null, null)).toHaveLength(3)
})
it('filters by start (inclusive)', () => {
const result = filterPooByTimeWindow(records, '2026-01-15T00:00:00Z', null)
expect(result).toHaveLength(2)
expect(result.map((r) => r.timestamp)).toContain('2026-01-20T09:00:00Z')
expect(result.map((r) => r.timestamp)).toContain('2026-02-01T09:00:00Z')
})
it('filters by end (inclusive)', () => {
const result = filterPooByTimeWindow(records, null, '2026-01-20T09:00:00Z')
expect(result).toHaveLength(2)
expect(result.map((r) => r.timestamp)).toContain('2026-01-10T08:00:00Z')
expect(result.map((r) => r.timestamp)).toContain('2026-01-20T09:00:00Z')
})
it('filters by both start and end', () => {
const result = filterPooByTimeWindow(
records,
'2026-01-15T00:00:00Z',
'2026-01-25T00:00:00Z',
)
expect(result).toHaveLength(1)
expect(result[0].timestamp).toBe('2026-01-20T09:00:00Z')
})
it('returns empty when no records match', () => {
const result = filterPooByTimeWindow(records, '2027-01-01T00:00:00Z', null)
expect(result).toHaveLength(0)
})
it('includes records exactly at start boundary', () => {
const result = filterPooByTimeWindow(records, '2026-01-10T08:00:00Z', null)
expect(result.map((r) => r.timestamp)).toContain('2026-01-10T08:00:00Z')
})
it('includes records exactly at end boundary', () => {
const result = filterPooByTimeWindow(records, null, '2026-02-01T09:00:00Z')
expect(result.map((r) => r.timestamp)).toContain('2026-02-01T09:00:00Z')
})
})
// ---------------------------------------------------------------------------
// computeCenter
// ---------------------------------------------------------------------------
describe('computeCenter', () => {
it('returns null for empty array', () => {
expect(computeCenter([])).toBeNull()
})
it('returns the point for a single-element array', () => {
const result = computeCenter([{ lat: 10, lng: 20 }])
expect(result).toEqual([10, 20])
})
it('returns the average of multiple points', () => {
const result = computeCenter([
{ lat: 0, lng: 0 },
{ lat: 4, lng: 6 },
])
expect(result).toEqual([2, 3])
})
})
// ---------------------------------------------------------------------------
// daysAgoISO
// ---------------------------------------------------------------------------
describe('daysAgoISO', () => {
it('returns a valid ISO string in the past', () => {
const result = daysAgoISO(7)
expect(typeof result).toBe('string')
const d = new Date(result)
expect(isNaN(d.getTime())).toBe(false)
expect(d.getTime()).toBeLessThan(Date.now())
})
})