修复馆内模型初始视图与点位返回复位
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-15 18:20:18 +08:00
parent 1072b25c46
commit 6566d38cb0
11 changed files with 568 additions and 44 deletions

View File

@@ -457,6 +457,14 @@ test('floor view baseline is deterministic across repeated resets', async ({ pag
await api.resetToViewBaseline({ view: 'floor', floorId, reason: 'floor-reset' })
}, floor.floorId)
const first = await getReport(page)
const baseline = await page.evaluate((floorId) => {
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
.__GUIDE_3D_VISUAL_STABILITY__
if (!api) throw new Error('ThreeMap visual stability diagnostics unavailable')
return api.getFloorBaseline(floorId)
}, floor.floorId)
expect(baseline, `missing floor baseline ${floor.floorId}`).not.toBeNull()
expect(getMaximumCameraDelta(baseline!, first.camera)).toBeLessThan(1e-6)
await page.evaluate(async (floorId) => {
const api = (window as Window & { __GUIDE_3D_VISUAL_STABILITY__?: VisualStabilityApi })
.__GUIDE_3D_VISUAL_STABILITY__
@@ -467,5 +475,6 @@ test('floor view baseline is deterministic across repeated resets', async ({ pag
expect(second.activeView).toBe('floor')
expect(second.floorId).toBe(floor.floorId)
expect(getMaximumCameraDelta(first.camera, second.camera)).toBeLessThan(1e-6)
expect(getMaximumCameraDelta(baseline!, second.camera)).toBeLessThan(1e-6)
}
})

View File

@@ -8,6 +8,14 @@ import GuideMapShell from '@/components/navigation/GuideMapShell.vue'
const ThreeMapStub = defineComponent({
name: 'ThreeMap',
props: {
initialFloorId: {
type: String,
default: ''
},
initialView: {
type: String,
default: ''
},
visiblePoiIds: {
type: Array,
default: null
@@ -71,4 +79,29 @@ describe('GuideMapShell 点位过滤契约', () => {
expect(wrapper.emitted('floorChange')).toEqual([['L2']])
expect(wrapper.emitted('targetFocus')).toEqual([[focusResult]])
})
it('动态楼层业务视图不会覆盖首次外观模型初始化视图', () => {
const wrapper = mount(GuideMapShell, {
props: {
mapType: 'indoor',
indoorModelSource: modelSource,
floors: [
{ id: 'L1', label: '1F' },
{ id: 'L2', label: '2F' }
],
activeFloor: 'L2',
indoorView: 'floor',
indoorInitialView: 'overview'
},
global: {
stubs: {
ThreeMap: ThreeMapStub
}
}
})
const renderer = wrapper.getComponent(ThreeMapStub)
expect(renderer.props('initialFloorId')).toBe('L2')
expect(renderer.props('initialView')).toBe('overview')
})
})

View File

@@ -6,12 +6,25 @@ 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)
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(() => ({
@@ -63,8 +76,8 @@ vi.mock('@/usecases/explainUseCase', () => ({
explainUseCase: {
loadExplainHalls: async () => [],
loadExplainHallSummaries: async () => ({}),
listHalls: async () => [],
searchExplain: async () => []
listHalls: async () => testState.halls,
searchExplain: async () => testState.explainResults
}
}))
@@ -82,6 +95,25 @@ const GuideMapShellStub = defineComponent({
targetFocusRequest: { type: Object, default: null }
},
emits: ['floor-change', 'poi-click', 'selection-clear'],
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>'
})
@@ -162,14 +194,24 @@ const mountIndex = async () => {
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(),
@@ -351,6 +393,7 @@ describe('首页搜索与地图闭环', () => {
expect(testState.searchMountCount).toBe(1)
expect(testState.searchUnmountCount).toBe(0)
confirmLatestNavigation()
await testState.onShowHandler?.()
await flushPromises()
@@ -378,6 +421,7 @@ describe('首页搜索与地图闭环', () => {
search.vm.$emit('result-tap', poi, fullSearchContext)
await flushPromises()
confirmLatestNavigation()
await testState.onShowHandler?.()
await flushPromises()
@@ -417,6 +461,129 @@ describe('首页搜索与地图闭环', () => {
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/hall/detail?id=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.explainResults = [{ type: 'exhibit', exhibitId: 'exhibit-l2' }]
const wrapper = await mountIndex()
const map = wrapper.getComponent(GuideMapShellStub)
map.vm.$emit('poi-click', {
...poi,
kind: 'hall',
primaryCategory: 'exhibition_hall',
primaryCategoryZh: '展厅'
})
await flushPromises()
const relatedActions = wrapper.findAll('.poi-action')
expect(relatedActions).toHaveLength(2)
await relatedActions[1]!.trigger('tap')
await flushPromises()
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/exhibit/detail?id=exhibit-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-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.findAll('.poi-action')[1]!.trigger('tap')
await flushPromises()
expect(vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url).toContain('/pages/hall/detail?id=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)