Files
frontend-miniapp/tests/unit/IndexPoiSearchFlow.spec.ts
lyf 267b415673
Some checks failed
CI / verify (push) Has been cancelled
修复微信内嵌点位搜索导航
2026-07-14 10:19:22 +08:00

355 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @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,
searchCollapseAfterDetailCount: 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() {},
returnToCollapsedAfterDetail() {
testState.searchCollapseAfterDetailCount += 1
},
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.searchCollapseAfterDetailCount = 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('同步首页标签时保留内嵌宿主参数', async () => {
window.location.hash = '#/pages/index/index?tab=guide&embedded=wechat-mini-program'
await mountIndex()
expect(window.location.hash).toBe('#/pages/index/index?tab=guide&embedded=wechat-mini-program')
})
it('首页暂不展示来馆入口及其到馆面板', async () => {
const wrapper = await mountIndex()
expect(wrapper.text()).not.toContain('来馆')
expect(wrapper.findAll('.guide-quick-action').map((item) => item.text())).toEqual([
'馆内',
'讲解'
])
})
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('分类结果进入详情时不触发首页地图,返回后才恢复目标焦点与楼层', 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('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('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)
await testState.onShowHandler?.()
await flushPromises()
expect(testState.searchCollapseAfterDetailCount).toBe(1)
expect(testState.searchRestoreCount).toBe(0)
expect(map.props('activeFloor')).toBe('L2')
expect(map.props('visiblePoiIds')).toBeNull()
expect(map.props('targetFocusRequest')).toMatchObject({
poiId: poi.id,
floorId: 'L2'
})
})
it('全屏搜索结果进入详情并返回后同样恢复为收缩态', async () => {
const wrapper = await mountIndex()
const search = wrapper.getComponent(PoiSearchPanelStub)
const map = wrapper.getComponent(GuideMapShellStub)
const fullSearchContext = {
...categoryContext,
categoryId: '' as const,
keyword: '二层卫生间'
}
search.vm.$emit('result-tap', poi, fullSearchContext)
await flushPromises()
await testState.onShowHandler?.()
await flushPromises()
expect(testState.searchCollapseAfterDetailCount).toBe(1)
expect(testState.searchRestoreCount).toBe(0)
expect(map.props('visiblePoiIds')).toBeNull()
expect(map.props('targetFocusRequest')).toMatchObject({ poiId: poi.id })
})
it('页面重新显示时要求搜索面板恢复原列表位置', async () => {
await mountIndex()
await testState.onShowHandler?.()
await flushPromises()
expect(testState.searchRestoreCount).toBe(1)
})
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 })
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(map.props('visiblePoiIds')).toEqual(categoryContext.visiblePoiIds)
expect(map.props('targetFocusRequest')).toBeNull()
})
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')).toBeNull()
search.vm.$emit('cancel')
await wrapper.vm.$nextTick()
expect(map.props('visiblePoiIds')).toBeNull()
expect(map.props('targetFocusRequest')).toBeNull()
})
})