283
tests/unit/IndexPoiSearchFlow.spec.ts
Normal file
283
tests/unit/IndexPoiSearchFlow.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user