import { describe, expect, it } from 'vitest' import { poiDataset } from '../src/data/poi/dataset' import { createPoiRepository } from '../src/data/poi/repository' import { filterPois, findPoiById, formatOpeningHours, formatRecommendationLabel, type PoiDataset, validatePoiDataset, } from '../src/domain/poi' function cloneDataset(): PoiDataset { return JSON.parse(JSON.stringify(poiDataset)) as PoiDataset } describe('poi POC dataset', () => { it('is usable while explicitly limiting its claim to a reviewed sample', () => { const result = validatePoiDataset(poiDataset) expect(result.isDatasetUsable).toBe(true) expect(result.isValid).toBe(true) expect(result.invalidPoiIds).toEqual([]) expect(result.validPois).toHaveLength(poiDataset.pois.length) expect(result.validPois.length).toBeGreaterThanOrEqual(20) expect(poiDataset.meta.coverageScope).toBe('reviewed_sample') expect(poiDataset.meta.authoritativeBaselineProvided).toBe(false) expect(poiDataset.meta.canClaimFullCoverage).toBe(false) expect(poiDataset.meta.coverageLabel).toContain('非全域权威清单') // All 30 POIs have real photos, so no placeholder warnings remain expect(result.issues.filter(issue => issue.code === 'POC_PLACEHOLDER_ASSET')).toHaveLength(0) }) it('provides the seven requested presentation fields for every sample POI', () => { for (const poi of poiDataset.pois) { expect(poi.name.trim()).not.toBe('') expect(poi.coverImageId.trim()).not.toBe('') expect(Number.isFinite(poi.longitude)).toBe(true) expect(Number.isFinite(poi.latitude)).toBe(true) expect(poi.coordinateSystem).toBe('GCJ02') expect(poi.description.trim()).not.toBe('') expect(poi.openingHours).toBeDefined() expect(poi.recommendationIndex).toBeGreaterThanOrEqual(1) expect(poi.recommendationIndex).toBeLessThanOrEqual(5) expect(poi.recommendationSource).toBe('poc_default') expect(poi.tagCodes.length).toBeGreaterThanOrEqual(1) expect(poi.tagCodes.length).toBeLessThanOrEqual(5) expect(poi.sourceReference).toMatch(/^amap:[A-Z0-9]+$/) } }) }) describe('poi dataset validation', () => { it('rejects a structurally invalid dataset as a whole', () => { const invalid = cloneDataset() invalid.categories[1].code = invalid.categories[0].code const result = validatePoiDataset(invalid) expect(result.isDatasetUsable).toBe(false) expect(result.validPois).toEqual([]) expect(result.issues.some(issue => issue.code === 'DATASET_STRUCTURE_INVALID')).toBe(true) }) it('returns a structural issue instead of throwing for malformed array members', () => { const malformed = cloneDataset() as unknown as Record malformed.categories = [null] expect(() => validatePoiDataset(malformed)).not.toThrow() const result = validatePoiDataset(malformed) expect(result.isDatasetUsable).toBe(false) expect(result.issues).toEqual(expect.arrayContaining([ expect.objectContaining({ code: 'DATASET_STRUCTURE_INVALID', path: '$' }), ])) }) it('isolates a POI with invalid fields without hiding valid records', () => { const invalid = cloneDataset() invalid.pois[0].description = '' invalid.pois[0].tagCodes = [] const result = validatePoiDataset(invalid) expect(result.isDatasetUsable).toBe(true) expect(result.isValid).toBe(false) expect(result.invalidPoiIds).toEqual(['poi_gm_culture_center']) expect(result.validPois).toHaveLength(poiDataset.pois.length - 1) expect(result.issues.some(issue => issue.code === 'REQUIRED_FIELD_MISSING')).toBe(true) expect(result.issues.some(issue => issue.code === 'TAG_INVALID')).toBe(true) }) it('blocks invalid coordinate systems, out-of-area points and fake scores', () => { const invalid = cloneDataset() const target = invalid.pois[0] as unknown as { id: string coordinateSystem: string longitude: number latitude: number recommendationIndex: number } target.coordinateSystem = 'WGS84' target.longitude = 120 target.latitude = 30 target.recommendationIndex = 4.5 const result = validatePoiDataset(invalid) const targetCodes = result.issues .filter(issue => issue.poiId === target.id) .map(issue => issue.code) expect(targetCodes).toContain('COORDINATE_SYSTEM_INVALID') expect(targetCodes).toContain('OUTSIDE_TARGET_AREA') expect(targetCodes).toContain('RECOMMENDATION_INVALID') }) it('accepts both controlled recommendation sources and rejects unknown sources', () => { const pocDataset = cloneDataset() pocDataset.pois[0].recommendationSource = 'poc_default' const pocResult = validatePoiDataset(pocDataset) expect(pocResult.invalidPoiIds).not.toContain(pocDataset.pois[0].id) expect(pocResult.issues.some(issue => issue.code === 'RECOMMENDATION_INVALID')).toBe(false) const invalidDataset = cloneDataset() const invalidPoi = invalidDataset.pois[0] as unknown as { id: string, recommendationSource: string } invalidPoi.recommendationSource = 'amap_rating' const invalidResult = validatePoiDataset(invalidDataset) expect(invalidResult.invalidPoiIds).toContain(invalidPoi.id) expect(invalidResult.issues).toEqual(expect.arrayContaining([ expect.objectContaining({ code: 'RECOMMENDATION_INVALID', poiId: invalidPoi.id, }), ])) }) it('does not allow a sample package to claim authoritative full coverage', () => { const invalid = cloneDataset() invalid.meta.canClaimFullCoverage = true const result = validatePoiDataset(invalid) expect(result.isDatasetUsable).toBe(false) expect(result.issues.some(issue => issue.path === 'meta.canClaimFullCoverage')).toBe(true) }) it('warns about near-identical marker coordinates without blocking the dataset', () => { const nearby = cloneDataset() nearby.pois[1].longitude = nearby.pois[0].longitude nearby.pois[1].latitude = nearby.pois[0].latitude const result = validatePoiDataset(nearby) expect(result.isDatasetUsable).toBe(true) expect(result.invalidPoiIds).not.toContain(nearby.pois[1].id) expect(result.issues).toEqual(expect.arrayContaining([ expect.objectContaining({ code: 'POTENTIAL_COORDINATE_OVERLAP', severity: 'warning', poiId: nearby.pois[1].id, }), ])) }) }) describe('poi selection and repository', () => { it('filters by one category and an AND-combined set of controlled tags', () => { const pastoral = filterPois(poiDataset.pois, { categoryCode: 'pastoral_leisure' }) const familyPhoto = filterPois(poiDataset.pois, { tagCodes: ['family_friendly', 'photo_spot'] }) expect(pastoral.map(poi => poi.id)).toEqual([ 'poi_farm_grand_view', 'poi_happy_pastoral', 'poi_amap_b0ffkkmzsc', ]) expect(familyPhoto.map(poi => poi.id)).toEqual(['poi_hongqiao_park', 'poi_happy_pastoral']) }) it('finds a POI only by its stable ID', () => { expect(findPoiById(poiDataset.pois, 'poi_hongqiao_park')?.name).toBe('虹桥公园') expect(findPoiById(poiDataset.pois, '虹桥公园')).toBeNull() expect(findPoiById(poiDataset.pois, ' ')).toBeNull() }) it('uses the mandated fallback for unknown opening hours', () => { expect(formatOpeningHours({ status: 'unknown', displayText: null, note: null, sourceUpdatedAt: null, })).toBe('开放时间待确认') }) it('formats recommendation labels consistently for all sources', () => { expect(formatRecommendationLabel('official_editorial', 5)).toBe('推荐指数 5/5') expect(formatRecommendationLabel('poc_default', 4)).toBe('推荐指数 4/5') }) it('returns resolved summaries and details from one dataset version', () => { const repository = createPoiRepository() const summaries = repository.getPoiSummaries({ categoryCode: 'urban_park' }) const detail = repository.getPoiById(summaries[0].id) expect(repository.getDatasetMeta().datasetVersion).toBe(poiDataset.meta.datasetVersion) expect(summaries.length).toBeGreaterThanOrEqual(10) expect(detail?.id).toBe(summaries[0].id) expect(detail?.category.code).toBe('urban_park') expect(detail?.coverImage.id).toBe(detail?.coverImageId) expect(detail?.openingHoursText).toBe('开放时间待确认') expect(summaries[0].recommendationSource).toBe(detail?.recommendationSource) expect(repository.getPoiById('missing')).toBeNull() }) it('does not expose categories without published valid POIs', () => { const dataset = cloneDataset() dataset.categories.push({ code: 'empty_category', name: '暂无点位', markerIcon: '/static/logo.png', selectedMarkerIcon: '/static/logo.png', sortOrder: 999, status: 'enabled', }) const repository = createPoiRepository(dataset) expect(repository.getCategories().some(category => category.code === 'empty_category')).toBe(false) }) it('returns defensive copies so page state cannot mutate the source package', () => { const repository = createPoiRepository() const firstRead = repository.getPoiById('poi_hongqiao_park') if (!firstRead) throw new Error('test fixture missing') firstRead.name = '被页面篡改' firstRead.tags.splice(0) const secondRead = repository.getPoiById('poi_hongqiao_park') expect(secondRead?.name).toBe('虹桥公园') expect(secondRead?.tags.length).toBeGreaterThan(0) }) })