forked from zhouruizhe/gmTouringMiniApp
Initial commit: gmTouringMiniApp project
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
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('非全域权威清单')
|
||||
expect(result.issues.filter(issue => issue.code === 'POC_PLACEHOLDER_ASSET')).toHaveLength(poiDataset.images.length)
|
||||
})
|
||||
|
||||
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('distinguishes official and unreviewed POC recommendation labels', () => {
|
||||
expect(formatRecommendationLabel('official_editorial', 5)).toBe('官方推荐指数 5/5')
|
||||
expect(formatRecommendationLabel('poc_default', 4)).toBe('POC 推荐指数(待审核) 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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user