Files
gmTouringMiniApp/test/poi-domain.test.ts
T
zuozhou d01525fd51
AI Code Review / review (pull_request) Successful in 2m58s
feat: 30个景点替换为真实照片,添加图片批处理脚本
2026-07-31 18:11:38 +08:00

244 lines
9.3 KiB
TypeScript

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<string, unknown>
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)
})
})