/** * 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()) }) })