774 lines
26 KiB
TypeScript
774 lines
26 KiB
TypeScript
// @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(() => ({
|
||
mapMountCount: 0,
|
||
mapUnmountCount: 0,
|
||
resetToViewBaselineCalls: [] as Array<{
|
||
view: 'overview' | 'floor'
|
||
floorId?: string
|
||
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
||
}>,
|
||
searchMountCount: 0,
|
||
searchUnmountCount: 0,
|
||
searchRestoreCount: 0,
|
||
searchCollapseAfterDetailCount: 0,
|
||
searchResetCount: 0,
|
||
onShowHandler: null as null | (() => Promise<void> | void),
|
||
halls: [] as Array<{ id: string; poiId?: string; location?: { poiId?: string }; name: string }>,
|
||
explainResults: [] as Array<{
|
||
type: 'hall' | 'exhibit'
|
||
hallId?: string
|
||
exhibitId?: string
|
||
}>
|
||
}))
|
||
|
||
const hostEnvironmentMocks = vi.hoisted(() => ({
|
||
embeddedInWechatMiniProgram: false
|
||
}))
|
||
|
||
vi.mock('@dcloudio/uni-app', () => ({
|
||
onLoad: vi.fn(),
|
||
onShow: (handler: () => Promise<void> | void) => {
|
||
testState.onShowHandler = handler
|
||
}
|
||
}))
|
||
|
||
vi.mock('@/utils/hostEnvironment', () => ({
|
||
isEmbeddedInWechatMiniProgram: () => hostEnvironmentMocks.embeddedInWechatMiniProgram
|
||
}))
|
||
|
||
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 () => testState.halls,
|
||
searchExplain: async () => testState.explainResults
|
||
}
|
||
}))
|
||
|
||
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', 'indoor-view-change'],
|
||
setup() {
|
||
onMounted(() => {
|
||
testState.mapMountCount += 1
|
||
})
|
||
onUnmounted(() => {
|
||
testState.mapUnmountCount += 1
|
||
})
|
||
return {}
|
||
},
|
||
methods: {
|
||
resetToViewBaseline(options: {
|
||
view: 'overview' | 'floor'
|
||
floorId?: string
|
||
reason: 'poi-detail-close' | 'manual-reset' | 'floor-reset'
|
||
}) {
|
||
testState.resetToViewBaselineCalls.push(options)
|
||
return true
|
||
}
|
||
},
|
||
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() {
|
||
testState.searchResetCount += 1
|
||
},
|
||
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
|
||
}
|
||
|
||
const confirmLatestNavigation = () => {
|
||
const navigation = vi.mocked(uni.navigateTo).mock.calls.at(-1)?.[0]
|
||
navigation?.success?.({ errMsg: 'navigateTo:ok' } as never)
|
||
}
|
||
|
||
beforeEach(() => {
|
||
hostEnvironmentMocks.embeddedInWechatMiniProgram = false
|
||
testState.mapMountCount = 0
|
||
testState.mapUnmountCount = 0
|
||
testState.resetToViewBaselineCalls = []
|
||
testState.searchMountCount = 0
|
||
testState.searchUnmountCount = 0
|
||
testState.searchRestoreCount = 0
|
||
testState.searchCollapseAfterDetailCount = 0
|
||
testState.searchResetCount = 0
|
||
testState.onShowHandler = null
|
||
testState.halls = []
|
||
testState.explainResults = []
|
||
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.restoreAllMocks()
|
||
vi.unstubAllGlobals()
|
||
})
|
||
|
||
describe('首页搜索与地图闭环', () => {
|
||
it('微信内嵌展开搜索只写入一条覆盖层历史记录', async () => {
|
||
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||
window.location.hash = '#/pages/index/index?tab=guide&embedded=wechat-mini-program'
|
||
window.history.replaceState({}, '', window.location.href)
|
||
const wrapper = await mountIndex()
|
||
const search = wrapper.getComponent(PoiSearchPanelStub)
|
||
const pushState = vi.spyOn(window.history, 'pushState')
|
||
|
||
search.vm.$emit('expanded-change', true)
|
||
await flushPromises()
|
||
expect(pushState).toHaveBeenCalledTimes(1)
|
||
expect(pushState).toHaveBeenLastCalledWith(
|
||
expect.objectContaining({ museumGuideOverlay: 'poi-search' }),
|
||
'',
|
||
window.location.href
|
||
)
|
||
|
||
search.vm.$emit('expanded-change', true)
|
||
await flushPromises()
|
||
expect(pushState).toHaveBeenCalledTimes(1)
|
||
wrapper.unmount()
|
||
})
|
||
|
||
it('微信内嵌搜索返回消费 popstate 并回到首页收缩态', async () => {
|
||
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||
window.location.hash = '#/pages/index/index?tab=guide&embedded=wechat-mini-program'
|
||
window.history.replaceState({}, '', window.location.href)
|
||
const wrapper = await mountIndex()
|
||
const search = wrapper.getComponent(PoiSearchPanelStub)
|
||
const currentUrl = window.location.href
|
||
|
||
search.vm.$emit('expanded-change', true)
|
||
await flushPromises()
|
||
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }))
|
||
await flushPromises()
|
||
|
||
expect(testState.searchResetCount).toBeGreaterThan(0)
|
||
expect(window.location.href).toBe(currentUrl)
|
||
expect(wrapper.getComponent(GuideMapShellStub).props('visiblePoiIds')).toBeNull()
|
||
wrapper.unmount()
|
||
})
|
||
|
||
it('微信内嵌搜索多次展开和收缩不会累积覆盖层历史', async () => {
|
||
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||
window.location.hash = '#/pages/index/index?tab=guide&embedded=wechat-mini-program'
|
||
window.history.replaceState({}, '', window.location.href)
|
||
const wrapper = await mountIndex()
|
||
const search = wrapper.getComponent(PoiSearchPanelStub)
|
||
let currentHistoryState: Record<string, unknown> = {}
|
||
vi.spyOn(window.history, 'state', 'get').mockImplementation(() => currentHistoryState)
|
||
const pushState = vi.spyOn(window.history, 'pushState').mockImplementation((state) => {
|
||
currentHistoryState = state as Record<string, unknown>
|
||
})
|
||
const historyBack = vi.spyOn(window.history, 'back').mockImplementation(() => {
|
||
currentHistoryState = {}
|
||
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }))
|
||
})
|
||
|
||
search.vm.$emit('expanded-change', true)
|
||
search.vm.$emit('expanded-change', false)
|
||
await flushPromises()
|
||
search.vm.$emit('expanded-change', true)
|
||
search.vm.$emit('expanded-change', false)
|
||
await flushPromises()
|
||
|
||
expect(pushState).toHaveBeenCalledTimes(2)
|
||
expect(historyBack).toHaveBeenCalledTimes(2)
|
||
expect(currentHistoryState).not.toMatchObject({ museumGuideOverlay: 'poi-search' })
|
||
wrapper.unmount()
|
||
})
|
||
|
||
it('普通 H5 展开搜索不写入覆盖层历史,也不消费浏览器返回', async () => {
|
||
const wrapper = await mountIndex()
|
||
const search = wrapper.getComponent(PoiSearchPanelStub)
|
||
const pushState = vi.spyOn(window.history, 'pushState')
|
||
|
||
search.vm.$emit('expanded-change', true)
|
||
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }))
|
||
await flushPromises()
|
||
|
||
expect(pushState).not.toHaveBeenCalled()
|
||
expect(testState.searchResetCount).toBe(0)
|
||
wrapper.unmount()
|
||
})
|
||
|
||
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()
|
||
const map = wrapper.getComponent(GuideMapShellStub)
|
||
const guideAction = wrapper.findAll('.guide-quick-action')[0]
|
||
|
||
expect(wrapper.text()).not.toContain('来馆')
|
||
expect(wrapper.findAll('.guide-quick-action').map((item) => item.text())).toEqual([
|
||
'导览',
|
||
'讲解'
|
||
])
|
||
|
||
await guideAction.trigger('tap')
|
||
await flushPromises()
|
||
expect(map.props('indoorView')).toBe('overview')
|
||
map.vm.$emit('floor-change', 'L1')
|
||
await flushPromises()
|
||
expect(map.props('indoorView')).toBe('floor')
|
||
expect(wrapper.findAll('.guide-quick-action')[0]?.text()).toBe('导览')
|
||
|
||
await guideAction.trigger('tap')
|
||
await flushPromises()
|
||
expect(map.props('indoorView')).toBe('overview')
|
||
expect(wrapper.findAll('.guide-quick-action')[0]?.text()).toBe('导览')
|
||
})
|
||
|
||
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('楼层数据按高到低返回时,首次进入仍将 1F 作为自动缩放的默认目标', async () => {
|
||
const originalFloors = [...floors]
|
||
floors.splice(0, floors.length,
|
||
{ id: 'F5', label: 'F5', order: 5 },
|
||
{ id: 'F2', label: 'F2', order: 2 },
|
||
{ id: 'F1', label: 'F1', order: 1 }
|
||
)
|
||
|
||
try {
|
||
const wrapper = await mountIndex()
|
||
const map = wrapper.getComponent(GuideMapShellStub)
|
||
|
||
expect(map.props('activeFloor')).toBe('F1')
|
||
expect(wrapper.getComponent(PoiSearchPanelStub).props('currentFloorId')).toBe('F1')
|
||
} finally {
|
||
floors.splice(0, floors.length, ...originalFloors)
|
||
}
|
||
})
|
||
|
||
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)
|
||
|
||
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')).toBeNull()
|
||
expect(map.props('targetFocusRequest')).toBeNull()
|
||
|
||
const resetCount = testState.searchResetCount
|
||
await testState.onShowHandler?.()
|
||
await flushPromises()
|
||
expect(testState.searchResetCount).toBe(resetCount)
|
||
})
|
||
|
||
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()
|
||
confirmLatestNavigation()
|
||
await testState.onShowHandler?.()
|
||
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')
|
||
})
|
||
|
||
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('模型点选展厅后查看详情,返回时恢复该楼层视觉基线且不重挂载地图', async () => {
|
||
testState.halls = [{ id: 'hall-l2', poiId: poi.id, name: '二层展厅' }]
|
||
const wrapper = await mountIndex()
|
||
const map = wrapper.getComponent(GuideMapShellStub)
|
||
|
||
map.vm.$emit('poi-click', {
|
||
...poi,
|
||
kind: 'hall',
|
||
primaryCategory: 'exhibition_hall',
|
||
primaryCategoryZh: '展厅'
|
||
})
|
||
await flushPromises()
|
||
|
||
const viewHallAction = wrapper.find('.poi-action.primary')
|
||
expect(viewHallAction.exists()).toBe(true)
|
||
await viewHallAction.trigger('tap')
|
||
await flushPromises()
|
||
|
||
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/explain/guide-stop-list?hallId=hall-l2')
|
||
await testState.onShowHandler?.()
|
||
await flushPromises()
|
||
expect(testState.resetToViewBaselineCalls).toHaveLength(0)
|
||
expect(wrapper.find('.guide-poi-card').exists()).toBe(true)
|
||
|
||
confirmLatestNavigation()
|
||
await testState.onShowHandler?.()
|
||
await flushPromises()
|
||
|
||
expect(testState.resetToViewBaselineCalls).toEqual([{
|
||
view: 'floor',
|
||
floorId: 'L2',
|
||
reason: 'poi-detail-close'
|
||
}])
|
||
expect(map.props('activeFloor')).toBe('L2')
|
||
expect(map.props('visiblePoiIds')).toBeNull()
|
||
expect(map.props('targetFocusRequest')).toBeNull()
|
||
expect(wrapper.find('.guide-poi-card').exists()).toBe(false)
|
||
expect(testState.mapMountCount).toBe(1)
|
||
expect(testState.mapUnmountCount).toBe(0)
|
||
|
||
await testState.onShowHandler?.()
|
||
await flushPromises()
|
||
expect(testState.resetToViewBaselineCalls).toHaveLength(1)
|
||
})
|
||
|
||
it('模型点选展厅只显示一个讲解入口,并在返回时复位原楼层', async () => {
|
||
testState.halls = [{ id: 'hall-l2', poiId: poi.id, name: '二层展厅' }]
|
||
const wrapper = await mountIndex()
|
||
const map = wrapper.getComponent(GuideMapShellStub)
|
||
|
||
map.vm.$emit('poi-click', {
|
||
...poi,
|
||
kind: 'hall',
|
||
primaryCategory: 'exhibition_hall',
|
||
primaryCategoryZh: '展厅'
|
||
})
|
||
await flushPromises()
|
||
|
||
const actions = wrapper.findAll('.poi-action')
|
||
expect(actions).toHaveLength(1)
|
||
expect(wrapper.text()).not.toContain('相关讲解')
|
||
await wrapper.find('.poi-action.primary').trigger('tap')
|
||
await flushPromises()
|
||
|
||
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/explain/guide-stop-list?hallId=hall-l2')
|
||
confirmLatestNavigation()
|
||
await testState.onShowHandler?.()
|
||
await flushPromises()
|
||
|
||
expect(testState.resetToViewBaselineCalls).toEqual([{
|
||
view: 'floor',
|
||
floorId: 'L2',
|
||
reason: 'poi-detail-close'
|
||
}])
|
||
expect(wrapper.find('.guide-poi-card').exists()).toBe(false)
|
||
})
|
||
|
||
it('展厅类型空间没有讲解展厅关联时不显示展厅与讲解按钮', async () => {
|
||
testState.halls = [{ id: 'hall-biology', poiId: 'another-poi', name: '生物厅' }]
|
||
const wrapper = await mountIndex()
|
||
const map = wrapper.getComponent(GuideMapShellStub)
|
||
|
||
map.vm.$emit('poi-click', {
|
||
...poi,
|
||
id: 'space-l2-temporary',
|
||
name: '生物厅临展空间',
|
||
kind: 'space',
|
||
hallId: 'space-l2-temporary',
|
||
primaryCategory: 'exhibition_hall',
|
||
primaryCategoryZh: '展厅'
|
||
})
|
||
await flushPromises()
|
||
|
||
expect(wrapper.find('.guide-poi-card').exists()).toBe(true)
|
||
expect(wrapper.find('.poi-card-actions').exists()).toBe(false)
|
||
expect(wrapper.text()).not.toContain('查看展厅')
|
||
expect(wrapper.text()).not.toContain('相关讲解')
|
||
})
|
||
|
||
it('展厅编号名称规范化后仍能关联真实讲解展厅', async () => {
|
||
testState.halls = [{ id: 'hall-biology', name: '生物厅' }]
|
||
const wrapper = await mountIndex()
|
||
const map = wrapper.getComponent(GuideMapShellStub)
|
||
|
||
map.vm.$emit('poi-click', {
|
||
...poi,
|
||
id: 'hall-space-biology',
|
||
name: '展厅_6生物厅',
|
||
kind: 'space',
|
||
hallId: 'space-biology',
|
||
primaryCategory: 'exhibition_hall',
|
||
primaryCategoryZh: '展厅'
|
||
})
|
||
await flushPromises()
|
||
|
||
const actions = wrapper.findAll('.poi-action')
|
||
expect(actions).toHaveLength(1)
|
||
await actions[0]!.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 () => {
|
||
testState.halls = [{ id: 'hall-l2', poiId: poi.id, name: '二层展厅' }]
|
||
const wrapper = await mountIndex()
|
||
const map = wrapper.getComponent(GuideMapShellStub)
|
||
|
||
map.vm.$emit('poi-click', {
|
||
...poi,
|
||
kind: 'hall',
|
||
primaryCategory: 'exhibition_hall',
|
||
primaryCategoryZh: '展厅'
|
||
})
|
||
await flushPromises()
|
||
await wrapper.find('.poi-action.primary').trigger('tap')
|
||
await flushPromises()
|
||
|
||
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/explain/guide-stop-list?hallId=hall-l2')
|
||
confirmLatestNavigation()
|
||
await testState.onShowHandler?.()
|
||
await flushPromises()
|
||
expect(testState.resetToViewBaselineCalls[0]).toMatchObject({ view: 'floor', floorId: 'L2' })
|
||
})
|
||
|
||
it('模型点选详情打开失败时取消返回上下文,不触发楼层复位', async () => {
|
||
testState.halls = [{ id: 'hall-l2', poiId: poi.id, name: '二层展厅' }]
|
||
const wrapper = await mountIndex()
|
||
const map = wrapper.getComponent(GuideMapShellStub)
|
||
|
||
map.vm.$emit('poi-click', {
|
||
...poi,
|
||
kind: 'hall',
|
||
primaryCategory: 'exhibition_hall',
|
||
primaryCategoryZh: '展厅'
|
||
})
|
||
await flushPromises()
|
||
await wrapper.find('.poi-action.primary').trigger('tap')
|
||
await flushPromises()
|
||
|
||
const navigation = vi.mocked(uni.navigateTo).mock.calls[0]?.[0]
|
||
navigation?.fail?.({ errMsg: 'navigateTo:fail simulated' })
|
||
await testState.onShowHandler?.()
|
||
await flushPromises()
|
||
|
||
expect(testState.resetToViewBaselineCalls).toHaveLength(0)
|
||
expect(wrapper.find('.guide-poi-card').exists()).toBe(true)
|
||
expect(map.props('activeFloor')).toBe('L2')
|
||
})
|
||
|
||
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)
|
||
expect(testState.resetToViewBaselineCalls).toHaveLength(0)
|
||
})
|
||
|
||
it('直接点选普通设施后展开并关闭卡片,恢复该楼层视觉基线且不重挂载地图', async () => {
|
||
const wrapper = await mountIndex()
|
||
const map = wrapper.getComponent(GuideMapShellStub)
|
||
|
||
map.vm.$emit('poi-click', {
|
||
...poi,
|
||
primaryCategory: 'restroom',
|
||
primaryCategoryZh: '卫生间'
|
||
})
|
||
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()
|
||
await wrapper.find('.poi-card-close').trigger('tap')
|
||
await flushPromises()
|
||
|
||
expect(testState.resetToViewBaselineCalls).toEqual([{
|
||
view: 'floor',
|
||
floorId: poi.floorId,
|
||
reason: 'floor-reset'
|
||
}])
|
||
expect(wrapper.find('.guide-poi-card').exists()).toBe(false)
|
||
expect(map.props('visiblePoiIds')).toBeNull()
|
||
expect(map.props('targetFocusRequest')).toBeNull()
|
||
expect(testState.mapMountCount).toBe(1)
|
||
expect(testState.mapUnmountCount).toBe(0)
|
||
})
|
||
|
||
it('直接点选展厅后关闭卡片,复用楼层基线复位而非详情返回语义', async () => {
|
||
const wrapper = await mountIndex()
|
||
const map = wrapper.getComponent(GuideMapShellStub)
|
||
|
||
map.vm.$emit('poi-click', {
|
||
...poi,
|
||
kind: 'hall',
|
||
primaryCategory: 'exhibition_hall',
|
||
primaryCategoryZh: '展厅'
|
||
})
|
||
await wrapper.vm.$nextTick()
|
||
|
||
expect(wrapper.find('.poi-card-close').exists()).toBe(true)
|
||
await wrapper.find('.poi-card-close').trigger('tap')
|
||
await flushPromises()
|
||
|
||
expect(testState.resetToViewBaselineCalls).toEqual([{
|
||
view: 'floor',
|
||
floorId: poi.floorId,
|
||
reason: 'floor-reset'
|
||
}])
|
||
expect(wrapper.find('.guide-poi-card').exists()).toBe(false)
|
||
expect(testState.mapMountCount).toBe(1)
|
||
expect(testState.mapUnmountCount).toBe(0)
|
||
})
|
||
|
||
it('renderer 清选和返回建筑外观不会触发楼层复位', async () => {
|
||
const wrapper = await mountIndex()
|
||
const map = wrapper.getComponent(GuideMapShellStub)
|
||
|
||
map.vm.$emit('poi-click', { ...poi, primaryCategory: 'restroom' })
|
||
map.vm.$emit('selection-clear')
|
||
map.vm.$emit('indoor-view-change', 'overview')
|
||
await wrapper.vm.$nextTick()
|
||
|
||
expect(testState.resetToViewBaselineCalls).toHaveLength(0)
|
||
})
|
||
|
||
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()
|
||
})
|
||
})
|