Files
frontend-miniapp/tests/unit/WeakNetworkGuideFallback.spec.ts
lyf 540bb8627f
Some checks failed
CI / verify (push) Has been cancelled
同步移动端源码并修复小程序嵌入标题
2026-07-28 18:37:40 +08:00

972 lines
35 KiB
TypeScript

// @vitest-environment happy-dom
import { flushPromises, mount } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
import WeakNetworkGuideFallback from '@/components/map/WeakNetworkGuideFallback.vue'
import {
GUIDE_FLOOR_BASEMAP_PROJECTION,
GUIDE_FLOOR_BASEMAP_PROJECTION_VERSION,
GUIDE_FLOOR_BASEMAP_SCHEMA_VERSION,
resetGuideFloorBasemapManifestCacheForTests,
type GuideFloorBasemapManifest
} from '@/services/model/GuideFloorBasemap'
const createOrthographicRenderIdentity = () => ({
projection: GUIDE_FLOOR_BASEMAP_PROJECTION,
projectionVersion: GUIDE_FLOOR_BASEMAP_PROJECTION_VERSION
})
const yaw = 54 * Math.PI / 180
const screenRight = { x: Math.cos(yaw), z: -Math.sin(yaw) }
const screenDown = { x: Math.sin(yaw), z: Math.cos(yaw) }
type ProjectedPoint = { x: number; y: number }
const createProjection = (width: number, height: number) => {
const halfX = Math.abs(width / 2 * screenRight.x) + Math.abs(height / 2 * screenDown.x)
const halfZ = Math.abs(width / 2 * screenRight.z) + Math.abs(height / 2 * screenDown.z)
return {
type: 'orthographic-xz' as const,
minX: -halfX,
maxX: halfX,
minZ: -halfZ,
maxZ: halfZ,
planeY: 0,
center: { x: 0, z: 0 },
width,
height,
screenRight,
screenDown
}
}
const l1BasemapManifest: GuideFloorBasemapManifest = {
schemaVersion: GUIDE_FLOOR_BASEMAP_SCHEMA_VERSION,
render: createOrthographicRenderIdentity(),
floors: [{
floorId: 'L1',
floorCode: 'L1',
modelVersion: 'model-v1',
projection: createProjection(80, 40),
initialViewport: {
centerX: 0,
centerZ: 0,
visibleWorldSpan: 40,
maxVisibleWorldSpan: 64
},
image: {
path: '/static/guide-floor-basemaps/L1.model-v1.ortho-yaw54-v2.compact-floor-center-v1.webp',
width: 1600,
height: 800,
format: 'webp'
}
}],
routeAssets: []
}
const exteriorBasemapManifest: GuideFloorBasemapManifest = {
schemaVersion: GUIDE_FLOOR_BASEMAP_SCHEMA_VERSION,
render: createOrthographicRenderIdentity(),
floors: [{
floorId: '2065808919904776194',
floorCode: 'EXTERIOR',
modelVersion: 'model-v1',
projection: createProjection(200, 100),
initialViewport: {
centerX: 0,
centerZ: 0,
visibleWorldSpan: 100,
maxVisibleWorldSpan: 135
},
image: {
path: '/static/guide-floor-basemaps/EXTERIOR.model-v1.ortho-yaw54-v2.compact-floor-center-v1.webp',
width: 1600,
height: 800,
format: 'webp'
}
}],
routeAssets: []
}
const L1_FLOOR_ID = '2065808921272119298'
const EXTERIOR_FLOOR_ID = '2065808919904776194'
const compositeBasemapManifest: GuideFloorBasemapManifest = {
schemaVersion: GUIDE_FLOOR_BASEMAP_SCHEMA_VERSION,
render: createOrthographicRenderIdentity(),
floors: [],
routeAssets: [{
floorId: 'EXTERIOR_L1',
floorCode: 'EXTERIOR_L1',
assetId: '2079549316670373890',
role: 'SAME_GROUND_COMPOSITE',
modelVersion: 'V20260721201311_179f0_SAME_GROUND_NO_BACKGROUND',
modelUrl: '/app-api/composite.glb',
coverageFloorCodes: ['EXTERIOR', 'L1'],
coverageFloorIds: [EXTERIOR_FLOOR_ID, L1_FLOOR_ID],
projection: createProjection(240, 480),
initialViewport: {
centerX: 0,
centerZ: 20,
visibleWorldSpan: 200,
maxVisibleWorldSpan: 270
},
image: {
path: '/static/guide-floor-basemaps/EXTERIOR_L1.composite-v1.ortho-yaw54-v2.compact-floor-center-v1.webp',
width: 750,
height: 1500,
format: 'webp'
}
}]
}
const pois = [
{
id: 'ticket',
name: '售票处',
floorId: 'L1',
primaryCategory: 'ticket_office',
primaryCategoryZh: '售票',
iconType: 'ticket_office',
positionGltf: [0, 0, 0] as [number, number, number]
},
{
id: 'lift',
name: '电梯',
floorId: 'L1',
primaryCategory: 'elevator',
primaryCategoryZh: '电梯',
iconType: 'elevator',
positionGltf: [20, 0, 10] as [number, number, number]
}
]
describe('WeakNetworkGuideFallback', () => {
afterEach(() => {
resetGuideFloorBasemapManifestCacheForTests()
vi.unstubAllGlobals()
})
it('keeps floor POIs and route lines available without a 3D model', async () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'L1',
floorLabel: '1F',
floors: [{ floorId: 'L1', label: '1F' }, { floorId: 'L2', label: '2F' }],
pois,
routePreview: {
id: 'route-1',
start: { poiId: 'ticket', name: '售票处', floorId: 'L1', floorLabel: '1F', position: [0, 0, 0] },
end: { poiId: 'lift', name: '电梯', floorId: 'L1', floorLabel: '1F', position: [20, 0, 10] },
distanceMeters: 25,
nodeIds: [],
points: [],
floorSegments: [{
floorId: 'L1',
floorLabel: '1F',
points: [
{ nodeId: 'a', floorId: 'L1', position: [0, 0, 0] },
{ nodeId: 'b', floorId: 'L1', position: [20, 0, 10] }
]
}],
connectorPoints: []
}
}
})
expect(wrapper.get('[data-testid="weak-network-map"]').text()).toContain('网络较弱')
expect(wrapper.findAll('.weak-network-marker')).toHaveLength(2)
expect(wrapper.findAll('.weak-network-route-line')).toHaveLength(1)
await wrapper.get('[data-poi-id="ticket"]').trigger('tap')
const secondFloor = wrapper.findAll('[data-testid="weak-network-floor-option"]')
.find((item) => item.text() === '2F')
expect(secondFloor).toBeDefined()
await secondFloor!.trigger('tap')
await wrapper.get('[data-testid="weak-network-retry"]').trigger('tap')
expect(wrapper.emitted('poiClick')?.[0][0]).toMatchObject({ id: 'ticket' })
expect(wrapper.emitted('floorChange')?.[0][0]).toBe('L2')
expect(wrapper.emitted('retry')).toHaveLength(1)
})
it('does not reuse indoor POIs or the floor rail over an exterior basemap', () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'EXTERIOR',
floorLabel: '室外导览',
sceneView: 'overview',
floors: [{ floorId: 'L1', label: '1F' }, { floorId: 'L2', label: '2F' }],
pois
}
})
expect(wrapper.find('[data-testid="weak-network-floor"]').exists()).toBe(false)
expect(wrapper.findAll('[data-testid="weak-network-floor-option"]')).toHaveLength(0)
expect(wrapper.findAll('.weak-network-marker')).toHaveLength(2)
expect(wrapper.get('[data-poi-id="overview-parking-entry"]').attributes('data-label'))
.toBe('停车场入口')
expect(wrapper.get('[data-poi-id="overview-parking-exit"]').attributes('data-label'))
.toBe('停车场出口')
})
it('keeps exterior POIs when the fallback uses the real overview floor id', () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: '2065808919904776194',
sceneView: 'overview',
presentation: 'two-dimensional',
basemapManifest: exteriorBasemapManifest,
pois: [
{
...pois[0],
id: 'parking',
name: '停车场入口',
floorId: '2065808919904776194',
primaryCategory: 'entrance',
primaryCategoryZh: '出入口',
iconType: 'entrance'
},
{
...pois[1],
id: 'outdoor-stairs',
name: '楼梯',
floorId: '2065808919904776194',
primaryCategory: 'stairs',
primaryCategoryZh: '楼梯',
iconType: 'stairs'
}
]
}
})
expect(wrapper.get('[data-testid="weak-network-basemap"]').exists()).toBe(true)
expect(wrapper.findAll('.weak-network-marker')).toHaveLength(2)
expect(wrapper.get('[data-poi-id="parking"]').exists()).toBe(true)
expect(wrapper.get('[data-poi-id="parking"]').attributes('data-label'))
.toBe('停车场入口')
expect(wrapper.get('[data-poi-id="overview-parking-exit"]').attributes('data-label'))
.toBe('停车场出口')
expect(wrapper.find('[data-poi-id="outdoor-stairs"]').exists()).toBe(false)
expect(wrapper.get('[data-testid="weak-network-marker-label-parking"]').text())
.toContain('停车场入口')
})
it('keeps selected labels while culling overlapping secondary labels', () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'L1',
floorModelVersion: 'model-v1',
basemapManifest: l1BasemapManifest,
sceneViewport: {
scene: 'floor',
floorId: 'L1',
centerX: 0,
centerZ: 0,
visibleWorldSpan: 16,
revision: 1
},
selectedPoiId: 'selected',
pois: [
{
id: 'landmark',
name: '服务中心',
floorId: 'L1',
primaryCategory: 'business_poi',
primaryCategoryZh: '服务',
iconType: 'service_desk',
positionGltf: [20, 0, 10]
},
{
id: 'secondary',
name: '卫生间',
floorId: 'L1',
primaryCategory: 'basic_service_facility',
primaryCategoryZh: '服务',
iconType: 'restroom',
positionGltf: [0.1, 0, 0.1]
},
{
id: 'selected',
name: '目标地点',
floorId: 'L1',
primaryCategory: 'basic_service_facility',
primaryCategoryZh: '服务',
iconType: 'restroom',
positionGltf: [0.2, 0, 0.2]
}
]
}
})
expect(wrapper.findAll('.weak-network-marker')).toHaveLength(3)
expect(wrapper.get('[data-testid="weak-network-marker-label-landmark"]').text()).toBe('服务中心')
expect(wrapper.find('[data-testid="weak-network-marker-label-secondary"]').exists()).toBe(false)
expect(wrapper.get('[data-testid="weak-network-marker-label-selected"]').text()).toBe('卫生间')
})
it('increases floor markers and labels across tight, balanced, and full tiers', async () => {
const tierPois = [
{
id: 'landmark', name: '展厅地标', floorId: 'L1', primaryCategory: 'space_public_area',
primaryCategoryZh: '空间', iconType: 'space', kind: 'space' as const,
positionGltf: [-7, 0, -12] as [number, number, number]
},
{
id: 'device', name: 'L1_A_导览屏_01', floorId: 'L1', primaryCategory: 'poi',
primaryCategoryZh: '设备', iconType: 'device_terminal', kind: 'facility' as const,
positionGltf: [7, 0, -12] as [number, number, number]
},
{
id: 'elevator', name: 'L1_A电梯', floorId: 'L1', primaryCategory: 'transport_circulation',
primaryCategoryZh: '交通', iconType: 'elevator', kind: 'facility' as const,
positionGltf: [0, 0, 0] as [number, number, number]
},
{
id: 'restroom', name: 'L1_卫生间', floorId: 'L1', primaryCategory: 'basic_service_facility',
primaryCategoryZh: '服务', iconType: 'restroom', kind: 'facility' as const,
positionGltf: [-7, 0, 12] as [number, number, number]
},
{
id: 'stairs', name: 'L1_A楼梯', floorId: 'L1', primaryCategory: 'transport_circulation',
primaryCategoryZh: '交通', iconType: 'stairs', kind: 'facility' as const,
positionGltf: [0, 0, 12] as [number, number, number]
},
{
id: 'locker', name: '寄存柜', floorId: 'L1', primaryCategory: 'basic_service_facility',
primaryCategoryZh: '服务', iconType: 'locker', kind: 'facility' as const,
positionGltf: [7, 0, 12] as [number, number, number]
}
]
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'L1',
sceneView: 'floor',
floorModelVersion: 'model-v1',
basemapManifest: l1BasemapManifest,
presentation: 'two-dimensional',
pois: tierPois
}
})
const renderer = wrapper.vm as unknown as {
zoomCamera: (direction: 'in' | 'out') => void
}
const counts = () => ({
tier: wrapper.get('[data-testid="weak-network-map"]').attributes('data-visibility-tier'),
markers: wrapper.findAll('.weak-network-marker').length,
labels: wrapper.findAll('.weak-network-marker-label').length
})
const tight = counts()
renderer.zoomCamera('in')
renderer.zoomCamera('in')
await wrapper.vm.$nextTick()
const balanced = counts()
renderer.zoomCamera('in')
renderer.zoomCamera('in')
await wrapper.vm.$nextTick()
const full = counts()
expect([tight.tier, balanced.tier, full.tier]).toEqual(['tight', 'balanced', 'full'])
expect(balanced.markers).toBeGreaterThan(tight.markers)
expect(full.markers).toBeGreaterThan(balanced.markers)
expect(balanced.labels).toBeGreaterThan(tight.labels)
expect(full.labels).toBeGreaterThan(balanced.labels)
expect(wrapper.get('[data-poi-id="stairs"]').exists()).toBe(true)
})
it('keeps search, selected, and route endpoint labels visible through collisions', () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'L1',
floorModelVersion: 'model-v1',
basemapManifest: l1BasemapManifest,
visiblePoiIds: ['search'],
selectedPoiId: 'selected',
pois: [
{
id: 'search', name: '查询地点', floorId: 'L1', primaryCategory: 'poi',
primaryCategoryZh: '地点', iconType: 'default', positionGltf: [0, 0, 0]
},
{
id: 'selected', name: '选中地点', floorId: 'L1', primaryCategory: 'poi',
primaryCategoryZh: '地点', iconType: 'default', positionGltf: [0, 0, 0]
}
],
routePreview: {
id: 'forced-route',
start: { poiId: 'route', name: '路线端点', floorId: 'L1', floorLabel: '1F', position: [0, 0, 0] },
end: { poiId: 'other-floor', name: '二层端点', floorId: 'L2', floorLabel: '2F', position: [0, 0, 0] },
distanceMeters: 20,
nodeIds: [],
points: [],
floorSegments: [],
connectorPoints: []
}
}
})
expect(new Set(wrapper.findAll('.weak-network-marker').map((marker) => marker.attributes('data-poi-id'))))
.toEqual(new Set(['search', 'selected', 'route']))
expect(wrapper.findAll('.weak-network-marker-label')).toHaveLength(3)
})
it('uses the shared circular POI icon and label treatment in two-dimensional mode', () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'L1',
presentation: 'two-dimensional',
visiblePoiIds: ['ticket'],
pois
}
})
const marker = wrapper.get('[data-poi-id="ticket"]')
expect(marker.attributes('data-poi-icon')).toBe('ticket-office')
expect(marker.get('.weak-network-marker-label-icon use').attributes('href'))
.toBe('#poi-ticket-office')
expect(marker.get('.weak-network-marker-label').classes()).toContain('weak-network-marker-label')
})
it('treats an explicit empty POI filter as an empty search result', () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'L1',
visiblePoiIds: [],
pois
}
})
expect(wrapper.findAll('.weak-network-marker')).toHaveLength(0)
})
it('keeps a route endpoint visible when it is absent from floor POI data', () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'L1',
pois: [],
routePreview: {
id: 'route-endpoint-only',
start: { poiId: 'origin', name: '主入口', floorId: 'L1', floorLabel: '1F', position: [0, 0, 0] },
end: { poiId: 'destination', name: '二层展厅', floorId: 'L2', floorLabel: '2F', position: [10, 0, 10] },
distanceMeters: 20,
nodeIds: [],
points: [],
floorSegments: [{
floorId: 'L1',
floorLabel: '1F',
points: [{ nodeId: 'origin', floorId: 'L1', position: [0, 0, 0] }]
}],
connectorPoints: []
}
}
})
expect(wrapper.findAll('.weak-network-marker')).toHaveLength(1)
expect(wrapper.get('[data-poi-id="origin"]').exists()).toBe(true)
expect(wrapper.get('[data-testid="weak-network-marker-label-origin"]').text()).toBe('主入口')
})
it('uses a matching WebP basemap without the grid and keeps route endpoints projected with the displayed image frame', () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'L1',
floorModelVersion: 'model-v1',
basemapManifest: l1BasemapManifest,
pois: [],
routePreview: {
id: 'route-on-basemap',
start: { poiId: 'origin', name: '主入口', floorId: 'L1', floorLabel: '1F', position: [-6, 0, -12] },
end: { poiId: 'destination', name: '目的地', floorId: 'L1', floorLabel: '1F', position: [26, 0, 12] },
distanceMeters: 80,
nodeIds: [],
points: [],
floorSegments: [{
floorId: 'L1',
floorLabel: '1F',
points: [
{ nodeId: 'origin', floorId: 'L1', position: [-6, 0, -12] },
{ nodeId: 'destination', floorId: 'L1', position: [26, 0, 12] }
]
}],
connectorPoints: []
}
}
})
expect(wrapper.get('[data-testid="weak-network-basemap"]').attributes('src'))
.toBe('/static/guide-floor-basemaps/L1.model-v1.ortho-yaw54-v2.compact-floor-center-v1.webp')
expect(wrapper.find('[data-testid="weak-network-neutral-grid"]').exists()).toBe(false)
expect(wrapper.get('[data-poi-id="origin"]').attributes('style')).toContain('left:')
expect(wrapper.get('[data-poi-id="origin"]').attributes('style')).toContain('top:')
expect(wrapper.get('[data-poi-id="destination"]').attributes('style')).toContain('left:')
expect(wrapper.get('[data-poi-id="destination"]').attributes('style')).toContain('top:')
expect(wrapper.get('[data-poi-id="destination"]').exists()).toBe(true)
expect(wrapper.get('.weak-network-route-line').attributes('style')).toContain('width:')
})
it('uses multiple 2D zoom levels before reporting a scene-boundary attempt', async () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'L1',
sceneView: 'floor',
floorModelVersion: 'model-v1',
basemapManifest: l1BasemapManifest,
presentation: 'two-dimensional',
pois
}
})
const renderer = wrapper.vm as unknown as {
zoomCamera: (direction: 'in' | 'out') => void
getGuideViewportState: () => { visibleWorldSpan: number; scene: string }
}
const initialSpan = renderer.getGuideViewportState().visibleWorldSpan
renderer.zoomCamera('in')
renderer.zoomCamera('in')
await wrapper.vm.$nextTick()
const intents = wrapper.emitted('zoomIntent') || []
expect(intents).toHaveLength(2)
expect(intents.every(([event]) => !(event as { boundaryAttempt: boolean }).boundaryAttempt)).toBe(true)
expect((intents[0][0] as { referenceVisibleWorldSpan: number }).referenceVisibleWorldSpan).toBe(40)
expect(renderer.getGuideViewportState()).toMatchObject({ scene: 'floor' })
expect(renderer.getGuideViewportState().visibleWorldSpan).toBeLessThan(initialSpan)
})
it('reports the overview threshold on the first 2D zoom step', async () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: '2065808919904776194',
sceneView: 'overview',
floorModelVersion: 'model-v1',
basemapManifest: exteriorBasemapManifest,
presentation: 'two-dimensional',
pois: []
}
})
const renderer = wrapper.vm as unknown as {
zoomCamera: (direction: 'in' | 'out') => void
}
renderer.zoomCamera('in')
await wrapper.vm.$nextTick()
expect(wrapper.emitted('zoomIntent')?.[0]?.[0]).toMatchObject({
direction: 'in',
referenceVisibleWorldSpan: 100,
thresholdReached: true,
viewport: { visibleWorldSpan: 80 }
})
})
it('reserves two exterior zoom-out steps before reporting the Tencent Map handoff boundary', async () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: '2065808919904776194',
sceneView: 'overview',
floorModelVersion: 'model-v1',
basemapManifest: exteriorBasemapManifest,
presentation: 'two-dimensional',
pois: []
}
})
const renderer = wrapper.vm as unknown as {
zoomCamera: (direction: 'in' | 'out') => void
}
renderer.zoomCamera('out')
renderer.zoomCamera('out')
await wrapper.vm.$nextTick()
const intents = wrapper.emitted('zoomIntent') || []
expect(intents).toHaveLength(2)
expect(intents[0][0]).toMatchObject({
direction: 'out',
outdoorExitReached: false,
viewport: { visibleWorldSpan: 125 }
})
expect(intents[1][0]).toMatchObject({
direction: 'out',
outdoorExitReached: true,
viewport: { visibleWorldSpan: 135 }
})
})
it('routes native H5 wheel input through the semantic viewport zoom', async () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'L1',
sceneView: 'floor',
floorModelVersion: 'model-v1',
basemapManifest: l1BasemapManifest,
presentation: 'two-dimensional',
pois
}
})
const renderer = wrapper.vm as unknown as {
getGuideViewportState: () => { visibleWorldSpan: number }
}
const initialSpan = renderer.getGuideViewportState().visibleWorldSpan
const event = new WheelEvent('wheel', {
bubbles: true,
cancelable: true,
clientX: 195,
clientY: 422,
deltaY: -120
})
wrapper.get('[data-testid="weak-network-surface"]').element.dispatchEvent(event)
await wrapper.vm.$nextTick()
expect(event.defaultPrevented).toBe(true)
const wheelIntent = wrapper.emitted('zoomIntent')?.[0]?.[0] as {
direction: string
boundaryAttempt: boolean
viewport: { visibleWorldSpan: number }
}
expect(wheelIntent).toMatchObject({ direction: 'in', boundaryAttempt: false })
expect(wheelIntent.viewport.visibleWorldSpan).toBeLessThan(initialSpan)
})
it('uses the manifest initial viewport instead of fitting the full projection coverage', async () => {
vi.stubGlobal('fetch', vi.fn(async () => ({
ok: true,
json: async () => l1BasemapManifest
})))
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'L1',
sceneView: 'floor',
presentation: 'two-dimensional',
pois
}
})
const renderer = wrapper.vm as unknown as {
getGuideViewportState: () => { visibleWorldSpan: number }
}
expect(wrapper.emitted('viewportChange')).toBeUndefined()
await flushPromises()
await wrapper.vm.$nextTick()
expect(renderer.getGuideViewportState().visibleWorldSpan).toBeCloseTo(40, 8)
expect(wrapper.emitted('viewportChange')).toHaveLength(1)
})
it('keeps selected POI and route state when renderer presentation changes', async () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'L1',
selectedPoiId: 'ticket',
presentation: 'two-dimensional',
pois,
routePreview: {
id: 'preserved-route',
start: { poiId: 'ticket', name: '售票处', floorId: 'L1', floorLabel: '1F', position: [0, 0, 0] },
end: { poiId: 'lift', name: '电梯', floorId: 'L1', floorLabel: '1F', position: [20, 0, 10] },
distanceMeters: 25,
nodeIds: [],
points: [],
floorSegments: [{
floorId: 'L1',
floorLabel: '1F',
points: [
{ nodeId: 'a', floorId: 'L1', position: [0, 0, 0] },
{ nodeId: 'b', floorId: 'L1', position: [20, 0, 10] }
]
}],
connectorPoints: []
}
}
})
await wrapper.setProps({ presentation: 'weak-network' })
expect(wrapper.get('[data-poi-id="ticket"]').classes()).toContain('weak-network-marker--selected')
expect(wrapper.findAll('.weak-network-route-line')).toHaveLength(1)
})
it('keeps the sole valid floor basemap when the model version has advanced', () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'L1',
floorModelVersion: 'model-v2',
basemapManifest: l1BasemapManifest,
pois
}
})
expect(wrapper.get('[data-testid="weak-network-basemap"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="weak-network-neutral-grid"]').exists()).toBe(false)
})
it('keeps map-based route-start selection available in the two-dimensional guide', async () => {
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'L1',
basemapManifest: {
schemaVersion: GUIDE_FLOOR_BASEMAP_SCHEMA_VERSION,
render: createOrthographicRenderIdentity(),
floors: [],
routeAssets: []
},
routeStartSelectionActive: true,
routeSelectablePoints: [{
routeTargetId: 'ticket:entrance',
poiId: 'ticket',
sourceId: 'ticket',
name: '售票处',
floorId: 'L1',
routeNodeId: 'node-ticket',
positionGltf: [0, 0, 0]
}],
pois
}
})
const ticketStyle = wrapper.get('[data-poi-id="ticket"]').attributes('style')
const ticketX = Number(/left:\s*([\d.-]+)/.exec(ticketStyle)?.[1])
const ticketY = Number(/top:\s*([\d.-]+)/.exec(ticketStyle)?.[1])
await wrapper.get('[data-testid="weak-network-surface"]').trigger('tap', {
detail: {
x: ticketX,
y: ticketY
}
})
expect(wrapper.emitted('routeStartCandidate')?.[0][0]).toMatchObject({
routeTargetId: 'ticket:entrance',
poiId: 'ticket',
routeNodeId: 'node-ticket'
})
})
it('renders and roams the real L1 exterior L1 route as one composite projection', async () => {
let animationCallback: FrameRequestCallback | null = null
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
animationCallback = callback
return 1
})
vi.stubGlobal('cancelAnimationFrame', () => undefined)
vi.stubGlobal('performance', { now: () => 0 })
const toPoints = (floorId: string, coordinates: Array<[number, number]>) => coordinates.map(([x, z], index) => ({
nodeId: `${floorId}-${index}`,
floorId,
position: [x, 0, z] as [number, number, number]
}))
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: L1_FLOOR_ID,
floorLabel: '1F',
presentation: 'two-dimensional',
basemapManifest: compositeBasemapManifest,
selectedPoiId: 'selected-exterior',
selectedPoi: {
id: 'selected-exterior',
name: '室外节点',
floorId: EXTERIOR_FLOOR_ID,
primaryCategory: 'target_preview',
primaryCategoryZh: '路线地点',
iconType: 'target_preview',
positionGltf: [-20.16, 0, 8.99]
},
routeNavigationActive: false,
pois: [],
routePreview: {
id: 'real-l1-exterior-l1-route',
start: {
poiId: 'ticket-office',
name: '展厅售票点',
floorId: L1_FLOOR_ID,
floorLabel: '1F',
position: [-59.617386, 0, 16.816814]
},
end: {
poiId: 'stamp-point',
name: '文创盖章打卡',
floorId: L1_FLOOR_ID,
floorLabel: '1F',
position: [61.8, 0, 45.3]
},
distanceMeters: 177.9,
nodeIds: [],
points: [],
floorSegments: [
{
floorId: L1_FLOOR_ID,
floorLabel: '1F',
points: toPoints(L1_FLOOR_ID, [
[-59.617386, 16.816814], [-58.8212, 14.344444], [-58.940376, 6.615569],
[-41.808034, 1.618447], [-33.403716, -6.75775], [-28.536788, -4.662431]
])
},
{
floorId: EXTERIOR_FLOOR_ID,
floorLabel: '室外',
points: toPoints(EXTERIOR_FLOOR_ID, [
[-27.324438, -5.125352], [-20.553785, -3.017094],
[-20.16194, 8.999322], [-10.293926, 11.905159]
])
},
{
floorId: L1_FLOOR_ID,
floorLabel: '1F',
points: toPoints(L1_FLOOR_ID, [
[-8.49571, 11.77092], [-6.183785, 12.700883], [8.789075, 6.158879],
[19.639385, 17.51139], [33.859702, 18.582956], [41.518894, 21.521948],
[49.182552, 26.843338], [65.167565, 33.958185], [67.426422, 41.425653],
[63.238589, 41.268468], [61.8, 45.3]
])
}
],
connectorPoints: [],
transitions: [
{
id: 'exhibition-entrance',
fromFloorId: L1_FLOOR_ID,
toFloorId: EXTERIOR_FLOOR_ID,
fromPosition: [-28.536788, 0, -4.662431],
toPosition: [-27.324438, 0, -5.125352],
transferType: 'ENTRANCE_EXIT',
connectorName: '展览入口'
},
{
id: 'theater-entrance',
fromFloorId: EXTERIOR_FLOOR_ID,
toFloorId: L1_FLOOR_ID,
fromPosition: [-10.293926, 0, 11.905159],
toPosition: [-8.49571, 0, 11.77092],
transferType: 'ENTRANCE_EXIT',
connectorName: '影剧院入口'
}
]
}
}
})
const map = wrapper.get('[data-testid="weak-network-map"]')
expect(map.attributes('data-active-basemap-floor-code')).toBe('EXTERIOR_L1')
expect(map.attributes('data-active-basemap-role')).toBe('SAME_GROUND_COMPOSITE')
expect(map.attributes('data-viewport-scene')).toBe('floor')
expect(map.attributes('data-viewport-floor-id')).toBe('EXTERIOR_L1')
expect(wrapper.get('[data-testid="weak-network-basemap"]').attributes('src')).toContain('EXTERIOR_L1')
expect(wrapper.find('[data-testid="weak-network-floor"]').exists()).toBe(false)
expect(wrapper.findAll('.weak-network-route-line')).toHaveLength(20)
expect(new Set(wrapper.findAll('.weak-network-route-line').map((line) => line.attributes('data-route-floor-ids'))))
.toEqual(new Set([`${L1_FLOOR_ID},${EXTERIOR_FLOOR_ID}`]))
const diagnostics = (wrapper.vm as unknown as {
getCompositeNavigationDiagnostics: () => {
routeSegmentFloorIds: string[]
routeLines: Array<{ start: ProjectedPoint; end: ProjectedPoint }>
markers: Array<{ id: string; point: ProjectedPoint }>
routeProgressPoint: ProjectedPoint | null
}
}).getCompositeNavigationDiagnostics()
expect(diagnostics.routeSegmentFloorIds).toEqual([L1_FLOOR_ID, EXTERIOR_FLOOR_ID, L1_FLOOR_ID])
expect(new Set(diagnostics.markers.map((marker) => marker.id)))
.toEqual(new Set(['ticket-office', 'stamp-point', 'selected-exterior']))
for (const point of [
...diagnostics.routeLines.flatMap((line) => [line.start, line.end]),
...diagnostics.markers.map((marker) => marker.point)
]) {
expect(point.x).toBeGreaterThanOrEqual(0)
expect(point.x).toBeLessThanOrEqual(390)
expect(point.y).toBeGreaterThanOrEqual(0)
expect(point.y).toBeLessThanOrEqual(844)
}
await wrapper.setProps({ routeNavigationActive: true })
await wrapper.vm.$nextTick()
expect(map.attributes('data-viewport-scene')).toBe('floor')
expect(map.attributes('data-viewport-floor-id')).toBe('EXTERIOR_L1')
expect(animationCallback).not.toBeNull()
animationCallback?.(10_000)
await wrapper.vm.$nextTick()
const progress = (wrapper.vm as unknown as {
getCompositeNavigationDiagnostics: () => { routeProgressPoint: ProjectedPoint | null }
}).getCompositeNavigationDiagnostics().routeProgressPoint
expect(progress).not.toBeNull()
expect(progress!.x).toBeGreaterThanOrEqual(0)
expect(progress!.x).toBeLessThanOrEqual(390)
expect(progress!.y).toBeGreaterThanOrEqual(0)
expect(progress!.y).toBeLessThanOrEqual(844)
animationCallback?.(100_000)
await wrapper.vm.$nextTick()
expect(wrapper.emitted('routeRoamingTransfer')).toBeUndefined()
expect(wrapper.emitted('floorChange')).toBeUndefined()
expect(wrapper.emitted('routeRoamingProgress')?.at(-1)?.[0]).toMatchObject({ remainingMeters: 0, progress: 1 })
})
it('keeps route roaming and cross-floor transfer active in two-dimensional mode', async () => {
let animationCallback: FrameRequestCallback | null = null
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => {
animationCallback = callback
return 1
})
vi.stubGlobal('cancelAnimationFrame', () => undefined)
vi.stubGlobal('performance', { now: () => 0 })
const wrapper = mount(WeakNetworkGuideFallback, {
props: {
floorId: 'L1',
presentation: 'two-dimensional',
routeNavigationActive: false,
pois,
routePreview: {
id: 'route-roaming',
start: { poiId: 'ticket', name: '售票处', floorId: 'L1', floorLabel: '1F', position: [0, 0, 0] },
end: { poiId: 'lift', name: '电梯', floorId: 'L2', floorLabel: '2F', position: [20, 0, 10] },
distanceMeters: 30,
nodeIds: [],
points: [],
floorSegments: [
{
floorId: 'L1',
floorLabel: '1F',
points: [
{ nodeId: 'a', floorId: 'L1', position: [0, 0, 0] },
{ nodeId: 'b', floorId: 'L1', position: [10, 0, 0] }
]
},
{
floorId: 'L2',
floorLabel: '2F',
points: [
{ nodeId: 'c', floorId: 'L2', position: [10, 0, 0] },
{ nodeId: 'd', floorId: 'L2', position: [20, 0, 10] }
]
}
],
connectorPoints: [],
transitions: [{
id: 'l1-l2',
fromFloorId: 'L1',
toFloorId: 'L2',
fromPosition: [10, 0, 0],
toPosition: [10, 0, 0],
transferType: 'ELEVATOR',
connectorName: '电梯'
}]
}
}
})
await wrapper.setProps({ routeNavigationActive: true })
await wrapper.vm.$nextTick()
expect(animationCallback).not.toBeNull()
expect(wrapper.get('[data-testid="weak-network-route-progress"]').classes()).not.toContain('is-hidden')
animationCallback?.(10_000)
await wrapper.vm.$nextTick()
animationCallback?.(100_000)
await wrapper.vm.$nextTick()
expect(wrapper.emitted('routeRoamingTransfer')?.[0][0]).toMatchObject({
fromFloorId: 'L1',
toFloorId: 'L2',
transferType: 'ELEVATOR'
})
expect(wrapper.emitted('floorChange')?.[0][0]).toBe('L2')
})
})