修复搜索与点位定位闭环
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-12 03:07:49 +08:00
parent e006333c0a
commit d8420fc7c2
18 changed files with 2961 additions and 621 deletions

View File

@@ -0,0 +1,283 @@
// @vitest-environment happy-dom
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'
const testState = vi.hoisted(() => ({
searchMountCount: 0,
searchUnmountCount: 0,
searchRestoreCount: 0,
onShowHandler: null as null | (() => Promise<void> | void)
}))
vi.mock('@dcloudio/uni-app', () => ({
onLoad: vi.fn(),
onShow: (handler: () => Promise<void> | void) => {
testState.onShowHandler = handler
}
}))
const floors = [
{ id: 'L1', label: '1F', order: 1 },
{ id: 'L2', label: '2F', order: 2 }
]
const modelSource = {
loadPackage: async () => ({ overviewModelUrl: '', floors: [] }),
loadFloorPois: async () => []
}
vi.mock('@/usecases/guideUseCase', () => ({
guideUseCase: {
getAssetBaseUrl: () => '/static/nav-assets',
getModelSource: () => modelSource,
normalizeFloorId: (value: string) => (
value === '1F' ? 'L1' : value === '2F' ? 'L2' : value
),
getFloors: async () => floors
}
}))
vi.mock('@/usecases/guideRouteUseCase', () => ({
guideRouteUseCase: {
listTargets: async () => [],
searchTargets: async () => [],
getRouteReadiness: async () => ({ ready: false, message: '' }),
planRoute: async () => null
}
}))
vi.mock('@/usecases/explainUseCase', () => ({
explainUseCase: {
loadExplainHalls: async () => [],
loadExplainHallSummaries: async () => ({}),
listHalls: async () => [],
searchExplain: async () => []
}
}))
const GuidePageFrameStub = defineComponent({
name: 'GuidePageFrame',
template: '<main><slot /></main>'
})
const GuideMapShellStub = defineComponent({
name: 'GuideMapShell',
props: {
activeFloor: { type: String, default: '' },
indoorView: { type: String, default: '' },
visiblePoiIds: { type: Array, default: null },
targetFocusRequest: { type: Object, default: null }
},
emits: ['floor-change', 'poi-click', 'selection-clear'],
template: '<section data-testid="guide-map"><slot name="overlay" /><slot /></section>'
})
const PoiSearchPanelStub = defineComponent({
name: 'PoiSearchPanel',
props: {
currentFloorId: { type: String, default: '' },
currentFloorLabel: { type: String, default: '' }
},
emits: [
'expanded-change',
'category-mode-change',
'results-change',
'result-tap',
'cancel'
],
setup() {
onMounted(() => {
testState.searchMountCount += 1
})
onUnmounted(() => {
testState.searchUnmountCount += 1
})
return {}
},
methods: {
collapseHomePanel() {},
resetSearchState() {},
restoreResultListScroll() {
testState.searchRestoreCount += 1
}
},
template: '<div data-testid="poi-search"></div>'
})
const stubs = {
GuidePageFrame: GuidePageFrameStub,
GuideMapShell: GuideMapShellStub,
PoiSearchPanel: PoiSearchPanelStub,
RoutePlannerPanel: true,
ArrivalPanel: true,
ExplainHallSelect: true
}
const poi = {
id: 'poi-l2-restroom',
name: '二层卫生间',
floorId: 'L2',
floorLabel: '2F',
primaryCategory: { id: 'restroom', label: '卫生间' },
categories: [{ id: 'restroom', label: '卫生间' }],
positionGltf: [12, 3, 24] as [number, number, number],
accessible: true
}
const categoryContext = {
origin: 'home' as const,
keyword: '卫生间',
categoryId: 'restroom' as const,
floorId: 'L2',
floorLabel: '2F',
resultCount: 5,
floorResultCount: 2,
visiblePoiIds: ['poi-l2-restroom', 'poi-l2-restroom-2'],
listScrollTop: 126
}
const mountIndex = async () => {
const wrapper = mount(IndexPage, {
global: { stubs }
})
await flushPromises()
return wrapper
}
beforeEach(() => {
testState.searchMountCount = 0
testState.searchUnmountCount = 0
testState.searchRestoreCount = 0
testState.onShowHandler = null
window.history.replaceState({}, '', '/#/pages/index/index?tab=guide')
vi.stubGlobal('uni', {
navigateTo: vi.fn(),
navigateBack: vi.fn(),
reLaunch: vi.fn(),
showToast: vi.fn(),
hideKeyboard: vi.fn()
})
})
afterEach(() => {
vi.unstubAllGlobals()
})
describe('首页搜索与地图闭环', () => {
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')
search.vm.$emit('category-mode-change', true)
search.vm.$emit('results-change', {
...categoryContext,
active: true
})
await wrapper.vm.$nextTick()
expect(map.props('activeFloor')).toBe('L2')
expect(map.props('indoorView')).toBe('floor')
expect(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
})
it('跨楼层结果使用既有 target-focus并携带返回上下文打开详情', 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
})
search.vm.$emit('result-tap', poi, categoryContext)
await flushPromises()
expect(map.props('activeFloor')).toBe('L2')
expect(map.props('targetFocusRequest')).toMatchObject({
poiId: poi.id,
floorId: 'L2',
floorLabel: '2F',
positionGltf: poi.positionGltf
})
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('search')
expect(query.get('searchOrigin')).toBe('home')
expect(query.get('searchKeyword')).toBe('卫生间')
expect(query.get('searchCategoryId')).toBe('restroom')
expect(query.get('searchFloorId')).toBe('L2')
expect(query.get('resultCount')).toBe('2')
expect(query.get('floorResultCount')).toBe('2')
expect(query.get('visiblePoiIds')).toBe(categoryContext.visiblePoiIds.join(','))
expect(query.get('listScrollTop')).toBe('126')
expect(testState.searchMountCount).toBe(1)
expect(testState.searchUnmountCount).toBe(0)
})
it('页面重新显示时要求搜索面板恢复原列表位置', async () => {
await mountIndex()
await testState.onShowHandler?.()
await flushPromises()
expect(testState.searchRestoreCount).toBe(1)
})
it('地图点位卡隐藏首页 dock 时仍保留搜索组件,关闭后可继续使用原列表', async () => {
const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub)
map.vm.$emit('poi-click', {
id: poi.id,
name: poi.name,
floorId: poi.floorId,
primaryCategory: 'restroom',
primaryCategoryZh: '卫生间',
iconType: 'restroom',
positionGltf: poi.positionGltf
})
await wrapper.vm.$nextTick()
expect(wrapper.get('[data-testid="poi-search"]').exists()).toBe(true)
expect(testState.searchUnmountCount).toBe(0)
map.vm.$emit('selection-clear')
await wrapper.vm.$nextTick()
expect(wrapper.getComponent(PoiSearchPanelStub).exists()).toBe(true)
expect(testState.searchMountCount).toBe(1)
})
it('单结果分类写入 resultCount=1主动取消恢复默认标记并清除目标', async () => {
const wrapper = await mountIndex()
const search = wrapper.getComponent(PoiSearchPanelStub)
const map = wrapper.getComponent(GuideMapShellStub)
const singleContext = {
...categoryContext,
floorResultCount: 1,
visiblePoiIds: [poi.id]
}
search.vm.$emit('category-mode-change', true)
search.vm.$emit('results-change', { ...singleContext, active: true })
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('resultCount')).toBe('1')
expect(map.props('targetFocusRequest')).not.toBeNull()
search.vm.$emit('cancel')
await wrapper.vm.$nextTick()
expect(map.props('visiblePoiIds')).toBeNull()
expect(map.props('targetFocusRequest')).toBeNull()
})
})