同步 sgs-frontend-mobile 源码
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-27 11:26:01 +08:00
parent 575dca430f
commit 9ea1bfab71
181 changed files with 24395 additions and 5212 deletions

View File

@@ -4,6 +4,8 @@ import { defineComponent, onMounted, onUnmounted } from 'vue'
import { flushPromises, mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import IndexPage from '@/pages/index/index.vue'
import type { GuideRouteResult } from '@/domain/museum'
import type { GuideRoutePlanRequest, GuideRoutePlanResult } from '@/usecases/guideRouteUseCase'
const testState = vi.hoisted(() => ({
mapMountCount: 0,
@@ -18,7 +20,6 @@ const testState = vi.hoisted(() => ({
searchRestoreCount: 0,
searchCollapseAfterDetailCount: 0,
searchResetCount: 0,
floorSwitchRequests: [] as string[],
onShowHandler: null as null | (() => Promise<void> | void),
halls: [] as Array<{ id: string; poiId?: string; location?: { poiId?: string }; name: string }>,
hallResolutionPromises: [] as Array<Promise<Array<{
@@ -31,7 +32,21 @@ const testState = vi.hoisted(() => ({
type: 'hall' | 'exhibit'
hallId?: string
exhibitId?: string
}>
}>,
switchFloorCalls: [] as string[],
routeTargets: [] as Array<{
poiId: string
name: string
floorId: string
floorLabel: string
categoryLabel?: string
routeNodeId: string
}>,
routePlanResult: {
route: null,
error: ''
} as GuideRoutePlanResult,
routePlanRequests: [] as GuideRoutePlanRequest[]
}))
const hostEnvironmentMocks = vi.hoisted(() => ({
@@ -72,10 +87,13 @@ vi.mock('@/usecases/guideUseCase', () => ({
vi.mock('@/usecases/guideRouteUseCase', () => ({
guideRouteUseCase: {
listTargets: async () => [],
searchTargets: async () => [],
getRouteReadiness: async () => ({ ready: false, message: '' }),
planRoute: async () => null
listTargets: async () => testState.routeTargets,
searchTargets: async () => testState.routeTargets,
getRouteReadiness: async () => ({ ready: true, message: '' }),
planRoute: async (request: GuideRoutePlanRequest) => {
testState.routePlanRequests.push(request)
return testState.routePlanResult
}
}
}))
@@ -101,9 +119,14 @@ const GuideMapShellStub = defineComponent({
activeFloor: { type: String, default: '' },
indoorView: { type: String, default: '' },
visiblePoiIds: { type: Array, default: null },
targetFocusRequest: { type: Object, default: null }
targetFocusRequest: { type: Object, default: null },
showFloor: { type: Boolean, default: false },
floorBottom: { type: String, default: '' },
disableAutoExit: { type: Boolean, default: false },
routeStartSelectionActive: { type: Boolean, default: false },
routeSelectablePoiIds: { type: Array, default: () => [] }
},
emits: ['floor-change', 'floor-request', 'floor-switch-failed', 'initial-model-ready', 'poi-click', 'selection-clear', 'indoor-view-change'],
emits: ['floor-change', 'poi-click', 'selection-clear', 'indoor-view-change', 'auto-switch', 'route-start-candidate', 'route-start-candidate-rejected', 'route-roaming-progress'],
setup() {
onMounted(() => {
testState.mapMountCount += 1
@@ -114,10 +137,6 @@ const GuideMapShellStub = defineComponent({
return {}
},
methods: {
switchFloor(floorId: string) {
testState.floorSwitchRequests.push(floorId)
return Promise.resolve()
},
resetToViewBaseline(options: {
view: 'overview' | 'floor'
floorId?: string
@@ -125,6 +144,9 @@ const GuideMapShellStub = defineComponent({
}) {
testState.resetToViewBaselineCalls.push(options)
return true
},
switchFloor(floorId: string) {
testState.switchFloorCalls.push(floorId)
}
},
template: '<section data-testid="guide-map"><slot name="overlay" /><slot /></section>'
@@ -167,11 +189,21 @@ const PoiSearchPanelStub = defineComponent({
template: '<div data-testid="poi-search"></div>'
})
const RoutePlannerPanelStub = defineComponent({
name: 'RoutePlannerPanel',
props: {
endPoint: { type: Object, default: null },
startCandidate: { type: Object, default: null }
},
emits: ['confirm-start', 'cancel-start', 'simulate-guide', 'back'],
template: '<section data-testid="route-planner"></section>'
})
const stubs = {
GuidePageFrame: GuidePageFrameStub,
GuideMapShell: GuideMapShellStub,
PoiSearchPanel: PoiSearchPanelStub,
RoutePlannerPanel: true,
RoutePlannerPanel: RoutePlannerPanelStub,
ArrivalPanel: true,
ExplainHallSelect: true
}
@@ -217,16 +249,19 @@ beforeEach(() => {
testState.mapMountCount = 0
testState.mapUnmountCount = 0
testState.resetToViewBaselineCalls = []
testState.switchFloorCalls = []
testState.routePlanResult = { route: null, error: '' }
testState.routePlanRequests = []
testState.searchMountCount = 0
testState.searchUnmountCount = 0
testState.searchRestoreCount = 0
testState.searchCollapseAfterDetailCount = 0
testState.searchResetCount = 0
testState.floorSwitchRequests = []
testState.onShowHandler = null
testState.halls = []
testState.hallResolutionPromises = []
testState.explainResults = []
testState.routeTargets = []
window.history.replaceState({}, '', '/#/pages/index/index?tab=guide')
vi.stubGlobal('uni', {
navigateTo: vi.fn(),
@@ -361,13 +396,14 @@ describe('首页搜索与地图闭环', () => {
expect(wrapper.findAll('.guide-quick-action')[0]?.text()).toBe('导览')
})
it('等待渲染器提交目标楼层后,才将快捷分类结果 ID 传给地图', async () => {
it('向搜索面板传入当前楼层,并将分类结果 ID 精确传给地图', async () => {
const wrapper = await mountIndex()
const search = wrapper.getComponent(PoiSearchPanelStub)
const map = wrapper.getComponent(GuideMapShellStub)
expect(search.props('currentFloorId')).toBe('L1')
expect(search.props('currentFloorLabel')).toBe('1F')
expect(map.props('disableAutoExit')).toBe(false)
search.vm.$emit('category-mode-change', true)
search.vm.$emit('results-change', {
@@ -376,16 +412,16 @@ describe('首页搜索与地图闭环', () => {
})
await wrapper.vm.$nextTick()
expect(testState.floorSwitchRequests).toEqual(['L2'])
expect(map.props('activeFloor')).toBe('L1')
expect(map.props('visiblePoiIds')).toEqual([])
expect(map.props('indoorView')).toBe('overview')
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
expect(map.props('disableAutoExit')).toBe(true)
expect(testState.switchFloorCalls).toEqual(['L1', 'L2'])
map.vm.$emit('floor-change', 'L2')
await wrapper.vm.$nextTick()
expect(map.props('activeFloor')).toBe('L2')
expect(map.props('indoorView')).toBe('floor')
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
search.vm.$emit('results-change', {
...categoryContext,
@@ -398,79 +434,21 @@ describe('首页搜索与地图闭环', () => {
await wrapper.vm.$nextTick()
expect(map.props('activeFloor')).toBe('L2')
expect(map.props('visiblePoiIds')).toEqual([])
})
it('三维模型重新以总览就绪后,不将旧单层状态误作楼层提交', async () => {
const wrapper = await mountIndex()
const search = wrapper.getComponent(PoiSearchPanelStub)
const map = wrapper.getComponent(GuideMapShellStub)
map.vm.$emit('floor-change', 'L1')
await wrapper.vm.$nextTick()
expect(map.props('indoorView')).toBe('floor')
map.vm.$emit('initial-model-ready', { view: 'overview', floorId: 'L1' })
await wrapper.vm.$nextTick()
expect(map.props('indoorView')).toBe('overview')
search.vm.$emit('category-mode-change', true)
search.vm.$emit('results-change', {
...categoryContext,
floorId: 'L1',
floorLabel: '1F',
active: true,
requestId: 20
})
await wrapper.vm.$nextTick()
expect(testState.floorSwitchRequests).toEqual(['L1'])
expect(map.props('visiblePoiIds')).toEqual([])
map.vm.$emit('floor-change', 'L1')
await wrapper.vm.$nextTick()
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
})
it('忽略过期搜索结果,不允许旧楼层覆盖当前待提交地图状态', async () => {
const wrapper = await mountIndex()
const search = wrapper.getComponent(PoiSearchPanelStub)
const map = wrapper.getComponent(GuideMapShellStub)
search.vm.$emit('category-mode-change', true)
search.vm.$emit('results-change', { ...categoryContext, active: true, requestId: 8 })
search.vm.$emit('results-change', {
...categoryContext,
floorId: 'L1',
floorLabel: '1F',
visiblePoiIds: ['stale-l1'],
active: true,
requestId: 7
})
await wrapper.vm.$nextTick()
expect(testState.floorSwitchRequests).toEqual(['L2'])
map.vm.$emit('floor-change', 'L2')
await wrapper.vm.$nextTick()
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
})
it('楼层切换失败时清空快捷分类标记,重试提交后恢复与列表一致的 ID', async () => {
const wrapper = await mountIndex()
const search = wrapper.getComponent(PoiSearchPanelStub)
const map = wrapper.getComponent(GuideMapShellStub)
search.vm.$emit('category-mode-change', true)
search.vm.$emit('results-change', { ...categoryContext, active: true, requestId: 12 })
await wrapper.vm.$nextTick()
map.vm.$emit('floor-switch-failed', { floorId: 'L2', floorLabel: '2F' })
await wrapper.vm.$nextTick()
expect(map.props('activeFloor')).toBe('L1')
expect(map.props('visiblePoiIds')).toEqual([])
expect(map.props('disableAutoExit')).toBe(true)
map.vm.$emit('floor-change', 'L2')
search.vm.$emit('results-change', {
...categoryContext,
active: false,
visiblePoiIds: []
})
search.vm.$emit('category-mode-change', false)
await wrapper.vm.$nextTick()
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
expect(map.props('disableAutoExit')).toBe(false)
})
it('楼层数据按高到低返回时,首次进入仍将 1F 作为自动缩放的默认目标', async () => {
@@ -492,7 +470,7 @@ describe('首页搜索与地图闭环', () => {
}
})
it('分类结果进入详情后恢复目标楼层与搜索列表位置', async () => {
it('分类结果在首页显示统一地点操作卡并定位目标', async () => {
const wrapper = await mountIndex()
const search = wrapper.getComponent(PoiSearchPanelStub)
const map = wrapper.getComponent(GuideMapShellStub)
@@ -502,40 +480,23 @@ describe('首页搜索与地图闭环', () => {
...categoryContext,
active: true
})
map.vm.$emit('floor-change', 'L2')
await wrapper.vm.$nextTick()
search.vm.$emit('result-tap', poi, categoryContext)
await flushPromises()
expect(map.props('targetFocusRequest')).toBeNull()
const navigateTo = vi.mocked(uni.navigateTo)
const detailUrl = navigateTo.mock.calls[0]?.[0]?.url as string
const query = new URLSearchParams(detailUrl.split('?')[1])
expect(query.get('source')).toBe('guide-poi')
expect(query.get('id')).toBe(poi.id)
expect(query.get('floorId')).toBe('L2')
expect(query.has('searchKeyword')).toBe(false)
expect(map.props('targetFocusRequest')).toMatchObject({ poiId: poi.id, floorId: 'L2' })
expect(wrapper.find('.poi-card-title').text()).toBe(poi.name)
expect(wrapper.findAll('.poi-action')).toHaveLength(1)
expect(wrapper.find('[data-action-id="navigate"]').text()).toContain('去这里')
expect(vi.mocked(uni.navigateTo)).not.toHaveBeenCalled()
expect(map.props('activeFloor')).toBe('L1')
map.vm.$emit('floor-change', 'L2')
await wrapper.vm.$nextTick()
expect(map.props('activeFloor')).toBe('L2')
expect(testState.searchMountCount).toBe(1)
expect(testState.searchUnmountCount).toBe(0)
confirmLatestNavigation()
await testState.onShowHandler?.()
await flushPromises()
expect(testState.searchResetCount).toBe(0)
expect(testState.searchRestoreCount).toBeGreaterThan(0)
expect(map.props('activeFloor')).toBe('L2')
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
expect(map.props('targetFocusRequest')).toBeNull()
const resetCount = testState.searchResetCount
await testState.onShowHandler?.()
await flushPromises()
expect(testState.searchResetCount).toBe(resetCount)
})
it('全屏搜索结果进入详情并返回后保留搜索上下文', async () => {
it('全屏搜索结果同样在首页显示地点操作卡', async () => {
const wrapper = await mountIndex()
const search = wrapper.getComponent(PoiSearchPanelStub)
const map = wrapper.getComponent(GuideMapShellStub)
@@ -547,15 +508,43 @@ describe('首页搜索与地图闭环', () => {
search.vm.$emit('result-tap', poi, fullSearchContext)
await flushPromises()
confirmLatestNavigation()
await testState.onShowHandler?.()
expect(wrapper.find('.poi-card-title').text()).toBe(poi.name)
expect(wrapper.find('[data-action-id="navigate"]').exists()).toBe(true)
expect(vi.mocked(uni.navigateTo)).not.toHaveBeenCalled()
expect(map.props('visiblePoiIds')).toBeNull()
expect(map.props('targetFocusRequest')).toMatchObject({ poiId: poi.id })
expect(map.props('activeFloor')).toBe('L1')
map.vm.$emit('floor-change', 'L2')
await wrapper.vm.$nextTick()
expect(map.props('activeFloor')).toBe('L2')
})
it('搜索展开期间若渲染器提交外观,立即重新请求当前楼层并等待提交', async () => {
const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub)
const search = wrapper.getComponent(PoiSearchPanelStub)
map.vm.$emit('floor-change', 'L1')
search.vm.$emit('expanded-change', true)
await flushPromises()
testState.switchFloorCalls = []
map.vm.$emit('auto-switch', {
from: 'floor',
to: 'overview',
trigger: 'zoom-out',
distance: 320
})
await flushPromises()
expect(testState.searchResetCount).toBe(0)
expect(testState.searchRestoreCount).toBeGreaterThan(0)
expect(map.props('visiblePoiIds')).toBeNull()
expect(map.props('targetFocusRequest')).toBeNull()
expect(map.props('activeFloor')).toBe('L2')
expect(map.props('indoorView')).toBe('overview')
expect(map.props('disableAutoExit')).toBe(true)
expect(testState.switchFloorCalls).toEqual(['L1'])
map.vm.$emit('floor-change', 'L1')
await wrapper.vm.$nextTick()
expect(map.props('indoorView')).toBe('floor')
})
it('页面重新显示时要求搜索面板恢复原列表位置', async () => {
@@ -566,27 +555,20 @@ describe('首页搜索与地图闭环', () => {
expect(testState.searchRestoreCount).toBe(1)
})
it('详情打开失败会撤销收缩标记并保持当前搜索状态', async () => {
it('搜索结果不直接跳转详情页,并保持当前搜索状态', async () => {
const wrapper = await mountIndex()
const search = wrapper.getComponent(PoiSearchPanelStub)
const map = wrapper.getComponent(GuideMapShellStub)
search.vm.$emit('category-mode-change', true)
search.vm.$emit('results-change', { ...categoryContext, active: true })
map.vm.$emit('floor-change', 'L2')
await wrapper.vm.$nextTick()
search.vm.$emit('result-tap', poi, categoryContext)
await flushPromises()
const navigateToCall = vi.mocked(uni.navigateTo).mock.calls[0]?.[0]
navigateToCall?.fail?.({ errMsg: 'navigateTo:fail simulated' })
await testState.onShowHandler?.()
await flushPromises()
expect(testState.searchCollapseAfterDetailCount).toBe(0)
expect(testState.searchRestoreCount).toBe(1)
expect(vi.mocked(uni.navigateTo)).not.toHaveBeenCalled()
expect(wrapper.find('.guide-poi-card').exists()).toBe(true)
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
expect(map.props('targetFocusRequest')).toBeNull()
expect(map.props('targetFocusRequest')).toMatchObject({ poiId: poi.id })
})
it('模型点选展厅后查看详情,返回时恢复该楼层视觉基线且不重挂载地图', async () => {
@@ -602,8 +584,9 @@ describe('首页搜索与地图闭环', () => {
})
await flushPromises()
const viewHallAction = wrapper.find('.poi-action.primary')
const viewHallAction = wrapper.find('[data-action-id="detail"]')
expect(viewHallAction.exists()).toBe(true)
expect(viewHallAction.text()).toContain('讲解')
await viewHallAction.trigger('tap')
await flushPromises()
@@ -634,7 +617,7 @@ describe('首页搜索与地图闭环', () => {
expect(testState.resetToViewBaselineCalls).toHaveLength(1)
})
it('模型点选展厅显示一个讲解入口,并在返回时复位原楼层', async () => {
it('模型点选展厅显示去这里和详情,并在详情返回时复位原楼层', async () => {
testState.halls = [{ id: 'hall-l2', poiId: poi.id, name: '二层展厅' }]
const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub)
@@ -648,9 +631,9 @@ describe('首页搜索与地图闭环', () => {
await flushPromises()
const actions = wrapper.findAll('.poi-action')
expect(actions).toHaveLength(1)
expect(actions).toHaveLength(2)
expect(wrapper.text()).not.toContain('相关讲解')
await wrapper.find('.poi-action.primary').trigger('tap')
await wrapper.find('[data-action-id="detail"]').trigger('tap')
await flushPromises()
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/explain/guide-stop-list?hallId=hall-l2')
@@ -684,13 +667,15 @@ describe('首页搜索与地图闭环', () => {
primaryCategoryZh: '展厅'
})
await flushPromises()
await wrapper.find('.poi-action.primary').trigger('tap')
await wrapper.find('[data-action-id="detail"]').trigger('tap')
await flushPromises()
const mapUrl = vi.mocked(uni.navigateTo).mock.calls.at(-1)?.[0]?.url as string
vi.mocked(uni.navigateTo).mock.calls.at(-1)?.[0]?.fail?.({ errMsg: 'navigateTo:fail test cleanup' })
search.vm.$emit('result-tap', hallPoi, categoryContext)
await flushPromises()
await wrapper.find('[data-action-id="detail"]').trigger('tap')
await flushPromises()
const searchUrl = vi.mocked(uni.navigateTo).mock.calls.at(-1)?.[0]?.url as string
const mapRoute = mapUrl.split('?')[0]
const searchRoute = searchUrl.split('?')[0]
@@ -704,7 +689,7 @@ describe('首页搜索与地图闭环', () => {
expect(searchUrl).toBe(mapUrl)
})
it('同一个普通设施从地图和搜索进入时都解析为设施详情', async () => {
it('普通设施从地图和搜索进入时只提供去这里', async () => {
const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub)
const search = wrapper.getComponent(PoiSearchPanelStub)
@@ -715,25 +700,241 @@ describe('首页搜索与地图闭环', () => {
primaryCategoryZh: '卫生间'
})
await flushPromises()
await wrapper.find('.poi-card-collapsed').trigger('tap')
await wrapper.vm.$nextTick()
await wrapper.find('.poi-action.primary').trigger('tap')
expect(wrapper.findAll('.poi-action')).toHaveLength(1)
expect(wrapper.find('[data-action-id="detail"]').exists()).toBe(false)
await wrapper.find('[data-action-id="navigate"]').trigger('tap')
await flushPromises()
const mapUrl = vi.mocked(uni.navigateTo).mock.calls.at(-1)?.[0]?.url as string
vi.mocked(uni.navigateTo).mock.calls.at(-1)?.[0]?.fail?.({ errMsg: 'navigateTo:fail test cleanup' })
expect(vi.mocked(uni.navigateTo)).not.toHaveBeenCalled()
search.vm.$emit('result-tap', poi, categoryContext)
await flushPromises()
const searchUrl = vi.mocked(uni.navigateTo).mock.calls.at(-1)?.[0]?.url as string
expect(mapUrl.split('?')[0]).toBe('/pages/facility/detail')
expect(searchUrl.split('?')[0]).toBe('/pages/facility/detail')
expect(new URLSearchParams(mapUrl.split('?')[1]).get('id')).toBe(poi.id)
expect(new URLSearchParams(searchUrl.split('?')[1]).get('id')).toBe(poi.id)
expect(searchUrl).toBe(mapUrl)
expect(wrapper.findAll('.poi-action')).toHaveLength(1)
expect(wrapper.find('[data-action-id="detail"]').exists()).toBe(false)
expect(wrapper.find('[data-action-id="navigate"]').exists()).toBe(true)
expect(vi.mocked(uni.navigateTo)).not.toHaveBeenCalled()
})
it('展厅类型空间没有讲解展厅关联时只进入普通点位详情,不显示虚假讲解入口', async () => {
it('去这里后仅接受已接入路网的地图点位作为起点,并要求确认', async () => {
const crossFloorStart = {
...poi,
id: 'poi-l1-start',
name: '一层服务台',
floorId: 'L1',
floorLabel: '1F'
}
testState.routeTargets = [{
poiId: poi.id,
name: poi.name,
floorId: poi.floorId,
floorLabel: poi.floorLabel,
categoryLabel: poi.primaryCategory.label,
routeNodeId: 'route-node-restroom-l2'
}, {
routeTargetId: 'anchor-service-l1',
poiId: 'route-node-service-l1',
sourceId: crossFloorStart.id,
name: crossFloorStart.name,
floorId: crossFloorStart.floorId,
floorLabel: crossFloorStart.floorLabel,
categoryLabel: crossFloorStart.primaryCategory.label,
routeNodeId: 'route-node-service-l1'
}]
const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub)
map.vm.$emit('poi-click', {
...poi,
primaryCategory: 'restroom',
primaryCategoryZh: '卫生间'
})
await flushPromises()
await wrapper.find('[data-action-id="navigate"]').trigger('tap')
await flushPromises()
const planner = wrapper.getComponent(RoutePlannerPanelStub)
expect(planner.props('endPoint')).toMatchObject({ poiId: poi.id })
expect(map.props('routeStartSelectionActive')).toBe(true)
expect(map.props('routeSelectablePoiIds')).toEqual([poi.id, 'route-node-service-l1'])
expect(map.props('showFloor')).toBe(true)
expect(map.props('floorBottom')).toBe('193px')
map.vm.$emit('floor-change', crossFloorStart.floorId)
map.vm.$emit('route-start-candidate', {
...crossFloorStart,
primaryCategory: 'restroom',
primaryCategoryZh: '卫生间'
})
await flushPromises()
expect(planner.props('startCandidate')).toMatchObject({
routeTargetId: 'anchor-service-l1',
poiId: 'route-node-service-l1',
floorId: 'L1'
})
expect(map.props('routeStartSelectionActive')).toBe(false)
planner.vm.$emit('cancel-start')
await flushPromises()
expect(planner.props('startCandidate')).toBeNull()
expect(map.props('routeStartSelectionActive')).toBe(true)
// A POI click delivered after a floor switch still belongs to the active
// route-start session and must not reopen its normal "go here" card.
map.vm.$emit('poi-click', crossFloorStart)
await flushPromises()
expect(planner.props('startCandidate')).toMatchObject({
routeTargetId: 'anchor-service-l1',
poiId: 'route-node-service-l1'
})
expect(wrapper.find('.guide-poi-card').exists()).toBe(false)
planner.vm.$emit('cancel-start')
await flushPromises()
// A space can be visible and selectable without being listed as a formal
// navigable place. Its coordinate must survive the delayed poi-click path
// so the backend can snap it to the nearest graph node.
const office = {
...crossFloorStart,
id: 'space-office-5',
name: '办公室',
floorId: 'L5',
floorLabel: '5F',
spaceId: '354513947405226300',
positionGltf: [-119.394396, 32.4643, 39.492556] as [number, number, number]
}
map.vm.$emit('floor-change', office.floorId)
map.vm.$emit('poi-click', office)
await flushPromises()
expect(planner.props('startCandidate')).toMatchObject({
poiId: office.id,
sourceId: office.spaceId,
floorId: office.floorId,
positionGltf: office.positionGltf,
coordinateFallback: true
})
})
it('取消馆内导览后回到发起导览时的当前楼层和地点卡片', async () => {
testState.routeTargets = [{
poiId: poi.id,
name: poi.name,
floorId: poi.floorId,
floorLabel: poi.floorLabel,
categoryLabel: poi.primaryCategory.label,
routeNodeId: 'route-node-restroom-l2'
}]
const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub)
map.vm.$emit('poi-click', {
...poi,
primaryCategory: 'restroom',
primaryCategoryZh: '卫生间'
})
await flushPromises()
await wrapper.find('[data-action-id="navigate"]').trigger('tap')
await flushPromises()
wrapper.getComponent(RoutePlannerPanelStub).vm.$emit('back')
await flushPromises()
expect(wrapper.find('.guide-poi-card').exists()).toBe(true)
expect(wrapper.text()).toContain(poi.name)
expect(map.props('activeFloor')).toBe('L2')
expect(testState.resetToViewBaselineCalls.at(-1)).toMatchObject({
floorId: 'L2',
reason: 'floor-reset'
})
})
it('模拟导览完成后回到发起导览时的当前楼层和地点卡片', async () => {
const startPoint = {
poiId: 'poi-l1-service',
name: '一层服务台',
floorId: 'L1',
floorLabel: '1F',
categoryLabel: '服务设施',
routeNodeId: 'route-node-service-l1',
positionGltf: [1, 0, 1] as [number, number, number]
}
testState.routeTargets = [{
poiId: poi.id,
name: poi.name,
floorId: poi.floorId,
floorLabel: poi.floorLabel,
categoryLabel: poi.primaryCategory.label,
routeNodeId: 'route-node-restroom-l2'
}, startPoint]
testState.routePlanResult = {
route: {
id: 'route-l1-l2',
start: {
poiId: startPoint.poiId,
name: startPoint.name,
floorId: startPoint.floorId,
floorLabel: startPoint.floorLabel,
routeNodeId: startPoint.routeNodeId,
position: [1, 0, 1]
},
end: {
poiId: poi.id,
name: poi.name,
floorId: poi.floorId,
floorLabel: poi.floorLabel,
routeNodeId: 'route-node-restroom-l2',
position: [12, 3, 24]
},
distanceMeters: 38,
nodeIds: ['route-node-service-l1', 'route-node-restroom-l2'],
points: [],
floorSegments: [{
floorId: 'L1',
floorLabel: '1F',
points: [{ nodeId: 'route-node-service-l1', floorId: 'L1', position: [1, 0, 1] }]
}],
connectorPoints: []
} satisfies GuideRouteResult,
error: ''
}
const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub)
map.vm.$emit('poi-click', {
...poi,
primaryCategory: 'restroom',
primaryCategoryZh: '卫生间'
})
await flushPromises()
await wrapper.find('[data-action-id="navigate"]').trigger('tap')
await flushPromises()
const planner = wrapper.getComponent(RoutePlannerPanelStub)
map.vm.$emit('route-start-candidate', startPoint)
await flushPromises()
planner.vm.$emit('confirm-start', startPoint)
await flushPromises()
expect(testState.routePlanRequests.at(-1)).toMatchObject({
startPoiId: startPoint.poiId,
startTarget: {
poiId: startPoint.poiId,
floorId: startPoint.floorId,
positionGltf: startPoint.positionGltf,
routeNodeId: startPoint.routeNodeId
}
})
planner.vm.$emit('simulate-guide')
await flushPromises()
map.vm.$emit('route-roaming-progress', { remainingMeters: 0, progress: 1 })
await flushPromises()
expect(wrapper.find('.guide-poi-card').exists()).toBe(true)
expect(wrapper.text()).toContain(poi.name)
expect(map.props('activeFloor')).toBe('L2')
})
it('非展厅空间没有关联讲解时只保留去这里', async () => {
testState.halls = [{ id: 'hall-biology', poiId: 'another-poi', name: '生物厅' }]
const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub)
@@ -751,12 +952,29 @@ describe('首页搜索与地图闭环', () => {
expect(wrapper.find('.guide-poi-card').exists()).toBe(true)
expect(wrapper.find('.poi-card-actions').exists()).toBe(true)
expect(wrapper.text()).not.toContain('查看展厅')
expect(wrapper.text()).not.toContain('相关讲解')
expect(wrapper.text()).toContain('查看详情')
await wrapper.find('.poi-action.primary').trigger('tap')
expect(wrapper.findAll('.poi-action')).toHaveLength(1)
expect(wrapper.find('[data-action-id="detail"]').exists()).toBe(false)
await wrapper.find('[data-action-id="navigate"]').trigger('tap')
expect(vi.mocked(uni.navigateTo)).not.toHaveBeenCalled()
})
it('没有进入讲解内容库的展厅语义点位也只保留去这里', async () => {
const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub)
map.vm.$emit('poi-click', {
...poi,
id: 'hall-l1-dynamic-cinema',
name: '动感多维影院',
kind: 'hall',
primaryCategory: 'exhibition_hall',
primaryCategoryZh: '展厅'
})
await flushPromises()
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/facility/detail?')
expect(wrapper.findAll('.poi-action')).toHaveLength(1)
expect(wrapper.find('[data-action-id="navigate"]').exists()).toBe(true)
expect(wrapper.find('[data-action-id="detail"]').exists()).toBe(false)
})
it('快速切换点位时忽略上一个点位较晚返回的展厅关联结果', async () => {
@@ -793,7 +1011,7 @@ describe('首页搜索与地图闭环', () => {
await flushPromises()
expect(wrapper.text()).toContain(secondPoi.name)
await wrapper.find('.poi-action.primary').trigger('tap')
await wrapper.find('[data-action-id="detail"]').trigger('tap')
await flushPromises()
const url = vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url as string
expect(new URLSearchParams(url.split('?')[1]).get('hallId')).toBe('hall-second')
@@ -816,15 +1034,15 @@ describe('首页搜索与地图闭环', () => {
await flushPromises()
const actions = wrapper.findAll('.poi-action')
expect(actions).toHaveLength(1)
await actions[0]!.trigger('tap')
expect(actions).toHaveLength(2)
await wrapper.find('[data-action-id="detail"]').trigger('tap')
await flushPromises()
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain(
'/pages/explain/guide-stop-list?hallId=hall-biology'
)
})
it('模型点选展厅的唯一讲解入口进入讲解对象列表并保留一次性返回上下文', async () => {
it('模型点选展厅的详情入口进入讲解对象列表并保留一次性返回上下文', async () => {
testState.halls = [{ id: 'hall-l2', poiId: poi.id, name: '二层展厅' }]
const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub)
@@ -836,7 +1054,7 @@ describe('首页搜索与地图闭环', () => {
primaryCategoryZh: '展厅'
})
await flushPromises()
await wrapper.find('.poi-action.primary').trigger('tap')
await wrapper.find('[data-action-id="detail"]').trigger('tap')
await flushPromises()
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/explain/guide-stop-list?hallId=hall-l2')
@@ -858,7 +1076,7 @@ describe('首页搜索与地图闭环', () => {
primaryCategoryZh: '展厅'
})
await flushPromises()
await wrapper.find('.poi-action.primary').trigger('tap')
await wrapper.find('[data-action-id="detail"]').trigger('tap')
await flushPromises()
const navigation = vi.mocked(uni.navigateTo).mock.calls[0]?.[0]
@@ -896,7 +1114,7 @@ describe('首页搜索与地图闭环', () => {
expect(testState.resetToViewBaselineCalls).toHaveLength(0)
})
it('直接点选普通设施后展开并关闭卡片,恢复该楼层视觉基线且不重挂载地图', async () => {
it('直接点选普通设施后显示统一名称和楼层卡片,关闭后恢复该楼层视觉基线且不重挂载地图', async () => {
const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub)
@@ -907,9 +1125,9 @@ describe('首页搜索与地图闭环', () => {
})
await wrapper.vm.$nextTick()
expect(wrapper.find('.poi-card-collapsed').exists()).toBe(true)
await wrapper.find('.poi-card-collapsed').trigger('tap')
await wrapper.vm.$nextTick()
expect(wrapper.find('.poi-card-collapsed').exists()).toBe(false)
expect(wrapper.find('.poi-card-title').text()).toBe(poi.name)
expect(wrapper.find('.poi-card-subtitle').text()).toBe('2F')
await wrapper.find('.poi-card-close').trigger('tap')
await flushPromises()
@@ -963,7 +1181,23 @@ describe('首页搜索与地图闭环', () => {
expect(testState.resetToViewBaselineCalls).toHaveLength(0)
})
it('单结果分类使用统一详情目标,主动取消恢复默认标记并清除目标', async () => {
it('导览地图不渲染任何遮挡模型的临时提示条', async () => {
const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub)
expect(wrapper.find('.indoor-gesture-hint').exists()).toBe(false)
map.vm.$emit('auto-switch', {
from: 'floor',
to: 'overview',
trigger: 'zoom-out',
distance: 120
})
await wrapper.vm.$nextTick()
expect(wrapper.find('.indoor-gesture-hint').exists()).toBe(false)
})
it('单结果分类显示地点操作卡,取消后清除定位目标', async () => {
const wrapper = await mountIndex()
const search = wrapper.getComponent(PoiSearchPanelStub)
const map = wrapper.getComponent(GuideMapShellStub)
@@ -978,9 +1212,9 @@ describe('首页搜索与地图闭环', () => {
search.vm.$emit('result-tap', poi, singleContext)
await flushPromises()
const detailUrl = vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url as string
expect(new URLSearchParams(detailUrl.split('?')[1]).get('source')).toBe('guide-poi')
expect(map.props('targetFocusRequest')).toBeNull()
expect(wrapper.find('.guide-poi-card').exists()).toBe(true)
expect(map.props('targetFocusRequest')).toMatchObject({ poiId: poi.id })
expect(vi.mocked(uni.navigateTo)).not.toHaveBeenCalled()
search.vm.$emit('cancel')
await wrapper.vm.$nextTick()