forked from zhouruizhe/gmTouringMiniApp
244 lines
9.3 KiB
TypeScript
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)
|
|
})
|
|
})
|