import { describe, it, expect } from 'vitest'; import { buildCategoryNameLookup, decodeUtf8WithWarning, extractKmlPlacemarkNodes, parseKmlPointCoordinates, parsePlacemarkNode, resolveCategoryIdForFolder, sanitizeKmlDescription, } from '../../../src/services/kmlImport'; describe('kmlImportUtils', () => { it('sanitizes HTML descriptions with br to newline', () => { const input = 'Line 1
Line 2 & more'; const output = sanitizeKmlDescription(input); expect(output).toBe('Line 1\nLine 2 & more'); }); it('unwraps CDATA sections before stripping tags', () => { const input = 'for photos and skyline.]]>'; expect(sanitizeKmlDescription(input)).toBe('Great spot\nfor photos and skyline.'); }); it('parses KML coordinate order lng,lat,alt', () => { const parsed = parseKmlPointCoordinates('13.4050,52.5200,15'); expect(parsed).toEqual({ lat: 52.52, lng: 13.405 }); }); it('extracts placemarks from nested folders', () => { const root = { Document: { Folder: { name: 'Parent', Folder: { name: 'Child', Placemark: { name: 'Nested', Point: { coordinates: '13.4,52.5,0' } }, }, }, }, }; const nodes = extractKmlPlacemarkNodes(root); expect(nodes).toHaveLength(1); expect(nodes[0].folderName).toBe('Child'); const parsed = parsePlacemarkNode(nodes[0]); expect(parsed.name).toBe('Nested'); expect(parsed.lat).toBe(52.5); expect(parsed.lng).toBe(13.4); }); it('builds exact case-insensitive category lookup', () => { const lookup = buildCategoryNameLookup([ { id: 3, name: 'Museums' }, { id: 4, name: 'Parks' }, ]); expect(resolveCategoryIdForFolder('museums', lookup)).toBe(3); expect(resolveCategoryIdForFolder('Museum', lookup)).toBeNull(); expect(resolveCategoryIdForFolder('parks', lookup)).toBe(4); }); it('decodes non-BMP decimal HTML entities (emoji)', () => { // 😀 = U+1F600 = 😀 — requires String.fromCodePoint, not fromCharCode expect(sanitizeKmlDescription('😀')).toBe('😀'); }); it('decodes non-BMP hex HTML entities (emoji)', () => { // 😀 = U+1F600 = 😀 expect(sanitizeKmlDescription('😀')).toBe('😀'); }); it('does not produce [object Object] when description is a parsed object with #text', () => { // fast-xml-parser can return an object for mixed-content nodes when stopNodes // is not configured; the fallback in asTrimmedString must extract #text. const result = sanitizeKmlDescription({ '#text': 'Hello world' } as any); expect(result).not.toBe('[object Object]'); expect(result).toBe('Hello world'); }); it('returns null when description object has no #text', () => { expect(sanitizeKmlDescription({ i: 'bold' } as any)).toBeNull(); }); it('returns warning for non-UTF8 payload', () => { const buffer = Buffer.concat([ Buffer.from('Caf'), Buffer.from([0xe9]), Buffer.from(''), ]); const decoded = decodeUtf8WithWarning(buffer); expect(decoded.warning).toContain('not valid UTF-8'); expect(decoded.text).toContain(''); }); });