This commit is contained in:
@@ -66,7 +66,7 @@ test('hall goes directly to paged guide objects without outline requests', async
|
||||
expect(requests.filter((url) => url.includes('/stops/page')).map((url) => new URL(url).searchParams.get('pageNo'))).toEqual(['1', '2'])
|
||||
})
|
||||
|
||||
test('embedded direct hall list delegates browser return to the host mini program', async ({ page }) => {
|
||||
test('embedded hall list exposes an H5 return control that keeps host markers', async ({ page }) => {
|
||||
await page.route('**/app-api/gis/guide/catalog/**', async (route) => {
|
||||
const url = route.request().url()
|
||||
if (url.includes('/catalog/halls?')) {
|
||||
@@ -76,38 +76,31 @@ test('embedded direct hall list delegates browser return to the host mini progra
|
||||
await route.fulfill({ json: { code: 0, data: {} } })
|
||||
})
|
||||
|
||||
await page.addInitScript(() => {
|
||||
document.documentElement.dataset.wechatNavigateBackCount = '0'
|
||||
;(window as Window & { wx?: unknown }).wx = {
|
||||
miniProgram: {
|
||||
navigateBack: () => {
|
||||
const count = Number(document.documentElement.dataset.wechatNavigateBackCount || '0')
|
||||
document.documentElement.dataset.wechatNavigateBackCount = String(count + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await page.goto('/#/pages/explain/list?embedded=wechat-mini-program&weapp=1')
|
||||
await expect(page.locator('.header-back')).toHaveCount(0)
|
||||
const back = page.locator('.header-back')
|
||||
await expect(back).toHaveCount(1)
|
||||
await expect(back).toBeVisible()
|
||||
await expect(page.locator('.hall-overview-card')).toHaveCount(1)
|
||||
for (const width of [375, 390, 430]) {
|
||||
await page.setViewportSize({ width, height: 844 })
|
||||
const layout = await page.locator('.explain-hall-select').evaluate((container) => {
|
||||
const header = container.querySelector<HTMLElement>('.explain-page-header')
|
||||
const card = container.querySelector<HTMLElement>('.hall-overview-card')
|
||||
if (!card) throw new Error('missing hall card')
|
||||
if (!header || !card) throw new Error('missing header or hall card')
|
||||
const headerRect = header.getBoundingClientRect()
|
||||
const cardRect = card.getBoundingClientRect()
|
||||
return {
|
||||
clientWidth: container.clientWidth,
|
||||
scrollWidth: container.scrollWidth,
|
||||
headerBottom: headerRect.bottom,
|
||||
cardTop: cardRect.top
|
||||
}
|
||||
})
|
||||
expect(layout.scrollWidth).toBeLessThanOrEqual(layout.clientWidth)
|
||||
expect(layout.cardTop).toBeGreaterThanOrEqual(0)
|
||||
expect(layout.cardTop).toBeGreaterThanOrEqual(layout.headerBottom)
|
||||
}
|
||||
await page.evaluate(() => window.history.back())
|
||||
await expect.poll(() => page.locator('html').getAttribute('data-wechat-navigate-back-count')).toBe('1')
|
||||
await back.click()
|
||||
await expect(page).toHaveURL(/#\/\?tab=guide&embedded=wechat-mini-program&weapp=1$/)
|
||||
})
|
||||
|
||||
test('ordinary H5 hall list return reaches the guide home', async ({ page }) => {
|
||||
|
||||
@@ -1,84 +1,92 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ArrivalPanel from '@/components/navigation/ArrivalPanel.vue'
|
||||
import { ARRIVAL_TARGETS } from '@/data/arrivalSearchTargets'
|
||||
import { openExternalNavigation } from '@/services/MapNavigationService'
|
||||
import { openWechatMiniProgramLocation } from '@/services/WechatMiniProgramBridgeService'
|
||||
|
||||
vi.mock('@/services/MapNavigationService', () => ({ openExternalNavigation: vi.fn() }))
|
||||
vi.mock('@/services/WechatMiniProgramBridgeService', () => ({ openWechatMiniProgramLocation: vi.fn() }))
|
||||
const selectedTarget = ARRIVAL_TARGETS.find((target) => target.type === 'parking')!
|
||||
|
||||
const selectedTarget = ARRIVAL_TARGETS[1]
|
||||
const defaultUserAgent = window.navigator.userAgent
|
||||
const mountArrivalPanel = () => mount(ArrivalPanel, {
|
||||
const mountArrivalPanel = (overrides: Record<string, unknown> = {}) => mount(ArrivalPanel, {
|
||||
props: {
|
||||
visible: true,
|
||||
activeType: selectedTarget.type,
|
||||
targets: [selectedTarget],
|
||||
selectedTargetId: selectedTarget.id,
|
||||
selectedTarget
|
||||
mode: 'browse',
|
||||
activeType: null,
|
||||
targets: [],
|
||||
selectedTargetId: '',
|
||||
selectedTarget: null,
|
||||
loading: false,
|
||||
error: '',
|
||||
...overrides
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(openExternalNavigation).mockResolvedValue(true)
|
||||
vi.mocked(openWechatMiniProgramLocation).mockResolvedValue({ status: 'not-embedded' })
|
||||
vi.stubGlobal('uni', { showToast: vi.fn(), showActionSheet: vi.fn() })
|
||||
window.history.replaceState({}, '', '/#/pages/index/index?tab=guide')
|
||||
delete (window as Window & { __wxjs_environment?: string }).__wxjs_environment
|
||||
Object.defineProperty(window.navigator, 'userAgent', { configurable: true, value: 'Mozilla/5.0 (Linux; Android 14)' })
|
||||
})
|
||||
describe('来馆目的地选择', () => {
|
||||
it('初始只显示分类入口,默认不选中也不请求或展示附近点位', async () => {
|
||||
const wrapper = mountArrivalPanel()
|
||||
const tabs = wrapper.findAll('.arrival-tab')
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(window.navigator, 'userAgent', { configurable: true, value: defaultUserAgent })
|
||||
delete (window as Window & { __wxjs_environment?: string }).__wxjs_environment
|
||||
vi.clearAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
expect(tabs.every((tab) => !tab.classes('active'))).toBe(true)
|
||||
expect(wrapper.find('.arrival-target-row').exists()).toBe(false)
|
||||
await tabs[0].trigger('tap')
|
||||
|
||||
describe('来馆面板 H5 导航分流', () => {
|
||||
it('嵌入小程序 web-view 时只向宿主页面导航,且完整传递当前选中目标', async () => {
|
||||
Object.assign(window, { __wxjs_environment: 'miniprogram' })
|
||||
Object.defineProperty(window.navigator, 'userAgent', { configurable: true, value: 'Mozilla/5.0 MicroMessenger/8.0.50' })
|
||||
vi.mocked(openWechatMiniProgramLocation).mockResolvedValue({ status: 'opened' })
|
||||
expect(wrapper.emitted('typeChange')).toEqual([['bus']])
|
||||
expect(wrapper.find('.arrival-primary').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('选择列表点位后向父级交付完整的地图目标', async () => {
|
||||
const wrapper = mountArrivalPanel({
|
||||
activeType: selectedTarget.type,
|
||||
targets: [selectedTarget]
|
||||
})
|
||||
|
||||
await wrapper.get('.arrival-target-row').trigger('tap')
|
||||
|
||||
expect(wrapper.emitted('selectTarget')).toEqual([[selectedTarget]])
|
||||
})
|
||||
|
||||
it('详情态不展示冗余标题,关闭后回到分类查询', async () => {
|
||||
const wrapper = mountArrivalPanel({
|
||||
mode: 'detail',
|
||||
selectedTargetId: selectedTarget.id,
|
||||
selectedTarget
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain(selectedTarget.title)
|
||||
expect(wrapper.text()).not.toContain('目的地详情')
|
||||
await wrapper.get('.arrival-primary').trigger('tap')
|
||||
await wrapper.get('.arrival-close').trigger('tap')
|
||||
|
||||
expect(wrapper.emitted('navigate')).toEqual([[selectedTarget]])
|
||||
expect(wrapper.emitted('back')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('分类查询态不显示关闭按钮', () => {
|
||||
const wrapper = mountArrivalPanel()
|
||||
|
||||
expect(wrapper.get('.arrival-primary-text').text()).toBe('打开地图导航')
|
||||
await wrapper.get('.arrival-primary').trigger('tap')
|
||||
|
||||
expect(openWechatMiniProgramLocation).toHaveBeenCalledTimes(1)
|
||||
expect(openWechatMiniProgramLocation).toHaveBeenCalledWith({
|
||||
latitude: selectedTarget.latitude,
|
||||
longitude: selectedTarget.longitude,
|
||||
name: selectedTarget.title,
|
||||
address: selectedTarget.subtitle
|
||||
})
|
||||
expect(openExternalNavigation).not.toHaveBeenCalled()
|
||||
expect((uni as unknown as { showActionSheet: ReturnType<typeof vi.fn> }).showActionSheet).not.toHaveBeenCalled()
|
||||
expect(wrapper.find('.arrival-close').exists()).toBe(false)
|
||||
expect(wrapper.find('.arrival-collapse').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('普通 H5 仅回退至第三方地图,并使用当前选中目标', async () => {
|
||||
const wrapper = mountArrivalPanel()
|
||||
expect(wrapper.get('.arrival-primary-text').text()).toBe('第三方导航')
|
||||
await wrapper.get('.arrival-primary').trigger('tap')
|
||||
|
||||
expect(openExternalNavigation).toHaveBeenCalledWith({
|
||||
latitude: selectedTarget.latitude,
|
||||
longitude: selectedTarget.longitude,
|
||||
name: selectedTarget.title,
|
||||
address: selectedTarget.subtitle
|
||||
it('展开分类结果后可收起到初始分类栏', async () => {
|
||||
const wrapper = mountArrivalPanel({
|
||||
activeType: selectedTarget.type,
|
||||
targets: [selectedTarget]
|
||||
})
|
||||
|
||||
await wrapper.get('.arrival-collapse').trigger('tap')
|
||||
|
||||
expect(wrapper.emitted('collapse')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('诊断信息默认隐藏,仅在 debug-build=1 时显示构建边界', () => {
|
||||
const normalWrapper = mountArrivalPanel()
|
||||
expect(normalWrapper.find('[data-testid="arrival-build-diagnostics"]').exists()).toBe(false)
|
||||
normalWrapper.unmount()
|
||||
it('服务异常时提供原位重试,不展示失真的静态结果', async () => {
|
||||
const wrapper = mountArrivalPanel({
|
||||
activeType: selectedTarget.type,
|
||||
error: '周边点位暂时无法加载'
|
||||
})
|
||||
|
||||
window.location.hash = '#/pages/index/index?tab=guide&debug-build=1'
|
||||
const debugWrapper = mountArrivalPanel()
|
||||
expect(debugWrapper.get('[data-testid="arrival-build-diagnostics"]').text()).toContain('构建平台:h5')
|
||||
expect(wrapper.text()).toContain('周边点位暂时无法加载')
|
||||
await wrapper.get('.arrival-retry').trigger('tap')
|
||||
expect(wrapper.emitted('retry')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,7 +6,6 @@ import { flushPromises, mount } from '@vue/test-utils'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
onLoadHandler: null as null | ((options?: Record<string, unknown>) => Promise<void>),
|
||||
onBackPressHandler: null as null | (() => boolean),
|
||||
onHideHandler: null as null | (() => void),
|
||||
onUnloadHandler: null as null | (() => void),
|
||||
enterExplainDetail: vi.fn(),
|
||||
@@ -18,9 +17,7 @@ const mocks = vi.hoisted(() => ({
|
||||
close: vi.fn(),
|
||||
seekToPercent: vi.fn(),
|
||||
setPlaybackRate: vi.fn(),
|
||||
toggleMute: vi.fn(),
|
||||
embeddedInWechatMiniProgram: false,
|
||||
returnToWechatMiniProgram: vi.fn(() => Promise.resolve({ status: 'failed' as const, reason: 'bridge-unavailable' as const }))
|
||||
toggleMute: vi.fn()
|
||||
}))
|
||||
|
||||
const audioState = vi.hoisted(() => ({
|
||||
@@ -39,9 +36,6 @@ vi.mock('@dcloudio/uni-app', () => ({
|
||||
onLoad: (handler: (options?: Record<string, unknown>) => Promise<void>) => {
|
||||
mocks.onLoadHandler = handler
|
||||
},
|
||||
onBackPress: (handler: () => boolean) => {
|
||||
mocks.onBackPressHandler = handler
|
||||
},
|
||||
onHide: (handler: () => void) => {
|
||||
mocks.onHideHandler = handler
|
||||
},
|
||||
@@ -80,11 +74,7 @@ vi.mock('@/composables/useGlobalAudioPlayer', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/hostEnvironment', () => ({
|
||||
isEmbeddedInWechatMiniProgram: () => mocks.embeddedInWechatMiniProgram
|
||||
}))
|
||||
|
||||
vi.mock('@/services/WechatMiniProgramBridgeService', () => ({
|
||||
returnToWechatMiniProgram: () => mocks.returnToWechatMiniProgram()
|
||||
isEmbeddedInWechatMiniProgram: () => false
|
||||
}))
|
||||
|
||||
import ExhibitDetail from '@/pages/exhibit/detail.vue'
|
||||
@@ -116,7 +106,6 @@ const exhibit = {
|
||||
describe('讲解详情音频优先布局', () => {
|
||||
beforeEach(() => {
|
||||
mocks.onLoadHandler = null
|
||||
mocks.onBackPressHandler = null
|
||||
mocks.onHideHandler = null
|
||||
mocks.onUnloadHandler = null
|
||||
audioState.currentAudio.value = null
|
||||
@@ -142,9 +131,6 @@ describe('讲解详情音频优先布局', () => {
|
||||
playInfo: { title: exhibit.name }
|
||||
})
|
||||
mocks.play.mockResolvedValue(undefined)
|
||||
mocks.embeddedInWechatMiniProgram = false
|
||||
mocks.returnToWechatMiniProgram.mockReset()
|
||||
mocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'failed', reason: 'bridge-unavailable' })
|
||||
vi.stubGlobal('uni', {
|
||||
showToast: vi.fn(),
|
||||
navigateBack: vi.fn(),
|
||||
@@ -389,58 +375,6 @@ describe('讲解详情音频优先布局', () => {
|
||||
expect(uni.redirectTo).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('小程序直达讲解详情时返回宿主,不进入 H5 讲解对象列表', async () => {
|
||||
mocks.embeddedInWechatMiniProgram = true
|
||||
mocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' })
|
||||
const wrapper = mount(ExhibitDetail, {
|
||||
global: { stubs: { GuidePageFrame: GuidePageFrameStub } }
|
||||
})
|
||||
|
||||
await mocks.onLoadHandler?.({
|
||||
id: exhibit.id,
|
||||
targetType: 'STOP',
|
||||
targetId: 'stop-1',
|
||||
hallId: 'hall-1',
|
||||
hallName: '恐龙厅'
|
||||
})
|
||||
expect(wrapper.find('.hero-back').exists()).toBe(false)
|
||||
wrapper.getComponent(GuidePageFrameStub).vm.$emit('back')
|
||||
await flushPromises()
|
||||
|
||||
expect(mocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
|
||||
expect(uni.navigateBack).not.toHaveBeenCalled()
|
||||
expect(uni.redirectTo).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('从讲解对象列表进入时,即使 H5 页面栈缺失也回到对象列表', async () => {
|
||||
mocks.embeddedInWechatMiniProgram = true
|
||||
mocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' })
|
||||
const wrapper = mount(ExhibitDetail, {
|
||||
global: { stubs: { GuidePageFrame: GuidePageFrameStub } }
|
||||
})
|
||||
|
||||
await mocks.onLoadHandler?.({
|
||||
id: exhibit.id,
|
||||
targetType: 'STOP',
|
||||
targetId: 'stop-1',
|
||||
hallId: 'hall-1',
|
||||
hallName: '恐龙厅',
|
||||
returnToGuideStopList: '1',
|
||||
returnToHallList: '1'
|
||||
})
|
||||
expect(wrapper.find('.hero-back').exists()).toBe(true)
|
||||
await wrapper.get('.hero-back').trigger('tap')
|
||||
await flushPromises()
|
||||
|
||||
const url = vi.mocked(uni.redirectTo).mock.calls[0]?.[0]?.url || ''
|
||||
const params = new URLSearchParams(url.split('?')[1])
|
||||
expect(url.split('?')[0]).toBe('/pages/explain/guide-stop-list')
|
||||
expect(params.get('hallId')).toBe('hall-1')
|
||||
expect(params.get('returnToHallList')).toBe('1')
|
||||
expect(mocks.returnToWechatMiniProgram).not.toHaveBeenCalled()
|
||||
expect(uni.navigateBack).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('离开详情页时立即关闭当前讲解播放器', async () => {
|
||||
audioState.currentAudio.value = { id: 'audio-1', audioUrl: exhibit.audioUrl }
|
||||
audioState.currentSource.value = { targetType: 'STOP', targetId: 'stop-1', lang: 'en-US' }
|
||||
@@ -473,6 +407,9 @@ describe('讲解详情音频优先布局', () => {
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
vi.mocked(uni.navigateBack).mockImplementationOnce((options: any) => {
|
||||
options.fail?.()
|
||||
})
|
||||
wrapper.getComponent(GuidePageFrameStub).vm.$emit('back')
|
||||
await flushPromises()
|
||||
|
||||
@@ -481,6 +418,6 @@ describe('讲解详情音频优先布局', () => {
|
||||
expect(url.split('?')[0]).toBe('/pages/explain/guide-stop-list')
|
||||
expect(params.get('hallId')).toBe('hall-1')
|
||||
expect(params.get('hallName')).toBe('恐龙厅')
|
||||
expect(uni.navigateBack).not.toHaveBeenCalled()
|
||||
expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,35 +7,15 @@ import ExplainGuideStopListPage from '@/pages/explain/guide-stop-list.vue'
|
||||
import { explainUseCase } from '@/usecases/explainUseCase'
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
onLoadHandler: null as null | ((options?: Record<string, string>) => void),
|
||||
onBackPressHandler: null as null | (() => boolean)
|
||||
}))
|
||||
|
||||
const hostEnvironmentMocks = vi.hoisted(() => ({
|
||||
embeddedInWechatMiniProgram: false
|
||||
}))
|
||||
|
||||
const miniProgramReturnMocks = vi.hoisted(() => ({
|
||||
returnToWechatMiniProgram: vi.fn(() => Promise.resolve({ status: 'failed' as const, reason: 'bridge-unavailable' as const }))
|
||||
onLoadHandler: null as null | ((options?: Record<string, string>) => void)
|
||||
}))
|
||||
|
||||
vi.mock('@dcloudio/uni-app', () => ({
|
||||
onLoad: (handler: (options?: Record<string, string>) => void) => {
|
||||
testState.onLoadHandler = handler
|
||||
},
|
||||
onBackPress: (handler: () => boolean) => {
|
||||
testState.onBackPressHandler = handler
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/hostEnvironment', () => ({
|
||||
isEmbeddedInWechatMiniProgram: () => hostEnvironmentMocks.embeddedInWechatMiniProgram
|
||||
}))
|
||||
|
||||
vi.mock('@/services/WechatMiniProgramBridgeService', () => ({
|
||||
returnToWechatMiniProgram: () => miniProgramReturnMocks.returnToWechatMiniProgram()
|
||||
}))
|
||||
|
||||
vi.mock('@/usecases/explainUseCase', () => ({
|
||||
explainUseCase: {
|
||||
listGuideStopsPageByHall: vi.fn().mockResolvedValue({
|
||||
@@ -52,7 +32,7 @@ const GuidePageFrameStub = defineComponent({
|
||||
|
||||
const ExplainGuideStopCatalogStub = defineComponent({
|
||||
name: 'ExplainGuideStopCatalog',
|
||||
props: ['guideStops', 'selectedHallName', 'showEmbeddedBack', 'loading', 'loadingMore', 'hasMore', 'error', 'loadMoreError'],
|
||||
props: ['guideStops', 'selectedHallName', 'loading', 'loadingMore', 'hasMore', 'error', 'loadMoreError'],
|
||||
emits: ['back', 'retry', 'retryMore', 'guideStopClick'],
|
||||
setup(_, { emit }) {
|
||||
return {
|
||||
@@ -73,10 +53,6 @@ const mountGuideStopList = () => mount(ExplainGuideStopListPage, {
|
||||
|
||||
beforeEach(() => {
|
||||
testState.onLoadHandler = null
|
||||
testState.onBackPressHandler = null
|
||||
hostEnvironmentMocks.embeddedInWechatMiniProgram = false
|
||||
miniProgramReturnMocks.returnToWechatMiniProgram.mockReset()
|
||||
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'failed', reason: 'bridge-unavailable' })
|
||||
vi.stubGlobal('uni', {
|
||||
navigateBack: vi.fn(),
|
||||
reLaunch: vi.fn(),
|
||||
@@ -136,7 +112,6 @@ describe('讲解对象列表返回', () => {
|
||||
expect(params.get('hallName')).toBe('恐龙厅')
|
||||
expect(params.get('targetType')).toBe('STOP')
|
||||
expect(params.get('targetId')).toBe('target-1')
|
||||
expect(params.get('returnToGuideStopList')).toBe('1')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
@@ -180,40 +155,6 @@ describe('讲解对象列表返回', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('小程序直达展厅时返回宿主,不重启 H5 展厅列表', async () => {
|
||||
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||||
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' })
|
||||
const wrapper = mountGuideStopList()
|
||||
|
||||
await wrapper.get('[data-testid="list-back"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(miniProgramReturnMocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
|
||||
expect(uni.reLaunch).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('从展厅列表进入时,即使 H5 页面栈缺失也保留内部返回路径', async () => {
|
||||
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||||
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' })
|
||||
const wrapper = mountGuideStopList()
|
||||
testState.onLoadHandler?.({ hallId: 'hall-1', hallName: '恐龙厅', returnToHallList: '1' })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.getComponent(ExplainGuideStopCatalogStub).props('showEmbeddedBack')).toBe(true)
|
||||
|
||||
await wrapper.get('[data-testid="detail-click"]').trigger('click')
|
||||
const detailUrl = vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url || ''
|
||||
expect(new URLSearchParams(detailUrl.split('?')[1]).get('returnToHallList')).toBe('1')
|
||||
|
||||
await wrapper.get('[data-testid="list-back"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(uni.reLaunch).toHaveBeenCalledWith({ url: '/pages/explain/list' })
|
||||
expect(miniProgramReturnMocks.returnToWechatMiniProgram).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('页面栈返回失败时回退至独立展厅列表', async () => {
|
||||
vi.stubGlobal('getCurrentPages', vi.fn(() => [
|
||||
{ route: 'pages/explain/list', options: {} },
|
||||
|
||||
@@ -9,10 +9,6 @@ const hostEnvironmentMocks = vi.hoisted(() => ({
|
||||
embeddedInWechatMiniProgram: false
|
||||
}))
|
||||
|
||||
const miniProgramReturnMocks = vi.hoisted(() => ({
|
||||
returnToWechatMiniProgram: vi.fn(() => Promise.resolve({ status: 'failed' as const, reason: 'bridge-unavailable' as const }))
|
||||
}))
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
onLoadHandler: null as null | (() => void),
|
||||
onBackPressHandler: null as null | (() => boolean)
|
||||
@@ -31,10 +27,6 @@ vi.mock('@/utils/hostEnvironment', () => ({
|
||||
isEmbeddedInWechatMiniProgram: () => hostEnvironmentMocks.embeddedInWechatMiniProgram
|
||||
}))
|
||||
|
||||
vi.mock('@/services/WechatMiniProgramBridgeService', () => ({
|
||||
returnToWechatMiniProgram: () => miniProgramReturnMocks.returnToWechatMiniProgram()
|
||||
}))
|
||||
|
||||
vi.mock('@/usecases/explainUseCase', () => ({
|
||||
explainUseCase: {
|
||||
loadExplainHalls: async () => [],
|
||||
@@ -68,8 +60,6 @@ const mountExplainList = () => mount(ExplainListPage, {
|
||||
|
||||
beforeEach(() => {
|
||||
hostEnvironmentMocks.embeddedInWechatMiniProgram = false
|
||||
miniProgramReturnMocks.returnToWechatMiniProgram.mockReset()
|
||||
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'failed', reason: 'bridge-unavailable' })
|
||||
testState.onLoadHandler = null
|
||||
testState.onBackPressHandler = null
|
||||
window.location.hash = '#/pages/explain/list'
|
||||
@@ -88,9 +78,9 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('讲解展厅列表返回', () => {
|
||||
it('首页讲解 Tab 进入时,顶部返回使用 navigateBack 回到项目首页', async () => {
|
||||
it('首页通过内部页面栈进入时,顶部返回使用 navigateBack 回到导览首页', async () => {
|
||||
vi.stubGlobal('getCurrentPages', vi.fn(() => [
|
||||
{ route: 'pages/index/index', options: { tab: 'explain' } },
|
||||
{ route: 'pages/index/index', options: { tab: 'guide' } },
|
||||
{ route: 'pages/explain/list', options: {} }
|
||||
]))
|
||||
const wrapper = mountExplainList()
|
||||
@@ -104,22 +94,6 @@ describe('讲解展厅列表返回', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('小程序中的首页讲解 Tab 进入时仍优先回到项目首页', async () => {
|
||||
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||||
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' })
|
||||
vi.stubGlobal('getCurrentPages', vi.fn(() => [
|
||||
{ route: 'pages/index/index', options: { tab: 'explain' } },
|
||||
{ route: 'pages/explain/list', options: {} }
|
||||
]))
|
||||
const wrapper = mountExplainList()
|
||||
|
||||
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
|
||||
|
||||
expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
|
||||
expect(miniProgramReturnMocks.returnToWechatMiniProgram).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('页面栈只有展厅列表时使用 reLaunch 回到导览首页', async () => {
|
||||
const wrapper = mountExplainList()
|
||||
|
||||
@@ -132,26 +106,24 @@ describe('讲解展厅列表返回', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('小程序直达独立入口时返回小程序,而不是重启项目首页', async () => {
|
||||
it('微信嵌入态不强制渲染 H5 页头,并保留宿主识别参数', async () => {
|
||||
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||||
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' })
|
||||
window.location.hash = '#/pages/explain/list?embedded=wechat-mini-program&weapp=1'
|
||||
window.history.replaceState({}, '', window.location.href)
|
||||
const wrapper = mountExplainList()
|
||||
|
||||
expect(wrapper.getComponent(ExplainHallSelectStub).props('forceInternalHeader')).toBe(false)
|
||||
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(uni.navigateBack).not.toHaveBeenCalled()
|
||||
expect(miniProgramReturnMocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
|
||||
expect(uni.reLaunch).not.toHaveBeenCalled()
|
||||
expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({
|
||||
url: '/pages/index/index?tab=guide&embedded=wechat-mini-program&weapp=1'
|
||||
}))
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('微信嵌入态从非讲解首页进入时返回小程序', async () => {
|
||||
it('微信嵌入态即使前置页是导览首页也不调用宿主返回', async () => {
|
||||
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||||
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' })
|
||||
window.location.hash = '#/pages/explain/list?embedded=wechat-mini-program'
|
||||
window.history.replaceState({}, '', window.location.href)
|
||||
vi.stubGlobal('getCurrentPages', vi.fn(() => [
|
||||
@@ -161,11 +133,11 @@ describe('讲解展厅列表返回', () => {
|
||||
const wrapper = mountExplainList()
|
||||
|
||||
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(uni.navigateBack).not.toHaveBeenCalled()
|
||||
expect(miniProgramReturnMocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
|
||||
expect(uni.reLaunch).not.toHaveBeenCalled()
|
||||
expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({
|
||||
url: '/pages/index/index?tab=guide&embedded=wechat-mini-program'
|
||||
}))
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
@@ -185,11 +157,27 @@ describe('讲解展厅列表返回', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('首页讲解 Tab 返回失败后重启到项目首页讲解 Tab', async () => {
|
||||
it('上一页是首页但不是导览 Tab 时重启到导览首页', async () => {
|
||||
vi.stubGlobal('getCurrentPages', vi.fn(() => [
|
||||
{ route: 'pages/index/index', options: { tab: 'explain' } },
|
||||
{ route: 'pages/explain/list', options: {} }
|
||||
]))
|
||||
const wrapper = mountExplainList()
|
||||
|
||||
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
|
||||
|
||||
expect(uni.navigateBack).not.toHaveBeenCalled()
|
||||
expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({
|
||||
url: '/pages/index/index?tab=guide'
|
||||
}))
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('navigateBack 失败后重启到导览首页', async () => {
|
||||
vi.stubGlobal('getCurrentPages', vi.fn(() => [
|
||||
{ route: 'pages/index/index', options: { tab: 'guide' } },
|
||||
{ route: 'pages/explain/list', options: {} }
|
||||
]))
|
||||
vi.stubGlobal('uni', {
|
||||
navigateBack: vi.fn((options: { fail?: () => void }) => options.fail?.()),
|
||||
reLaunch: vi.fn(),
|
||||
@@ -200,7 +188,7 @@ describe('讲解展厅列表返回', () => {
|
||||
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
|
||||
|
||||
expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({
|
||||
url: '/pages/index/index?tab=explain'
|
||||
url: '/pages/index/index?tab=guide'
|
||||
}))
|
||||
wrapper.unmount()
|
||||
})
|
||||
@@ -222,9 +210,8 @@ describe('讲解展厅列表返回', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('小程序直达入口的浏览器返回会委托给小程序宿主', async () => {
|
||||
it('根栈只创建一条历史标记,浏览器返回会回到导览首页', async () => {
|
||||
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||||
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' })
|
||||
window.location.hash = '#/pages/explain/list?embedded=wechat-mini-program&weapp=1'
|
||||
window.history.replaceState({}, '', window.location.href)
|
||||
const wrapper = mountExplainList()
|
||||
@@ -238,8 +225,12 @@ describe('讲解展厅列表返回', () => {
|
||||
|
||||
expect(uni.navigateBack).not.toHaveBeenCalled()
|
||||
expect(pushState).toHaveBeenCalledTimes(1)
|
||||
expect(miniProgramReturnMocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
|
||||
expect(uni.reLaunch).not.toHaveBeenCalled()
|
||||
const url = vi.mocked(uni.reLaunch).mock.calls[0]?.[0]?.url || ''
|
||||
const query = new URLSearchParams(url.split('?')[1])
|
||||
expect(url.split('?')[0]).toBe('/pages/index/index')
|
||||
expect(query.get('tab')).toBe('guide')
|
||||
expect(query.get('embedded')).toBe('wechat-mini-program')
|
||||
expect(query.get('weapp')).toBe('1')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
|
||||
@@ -240,6 +240,37 @@ describe('GuideFloorBasemap projection', () => {
|
||||
expect(await loadGuideFloorBasemapManifest()).toBeNull()
|
||||
})
|
||||
|
||||
it('uses the sole valid floor basemap when the published GLB version is newer', () => {
|
||||
const basemap = createBasemap()
|
||||
const manifest = {
|
||||
schemaVersion: GUIDE_FLOOR_BASEMAP_SCHEMA_VERSION,
|
||||
render: {
|
||||
projection: GUIDE_FLOOR_BASEMAP_PROJECTION,
|
||||
projectionVersion: GUIDE_FLOOR_BASEMAP_PROJECTION_VERSION
|
||||
},
|
||||
floors: [basemap],
|
||||
routeAssets: []
|
||||
}
|
||||
|
||||
expect(findGuideFloorBasemap(manifest, 'L1', 'v2')).toEqual(basemap)
|
||||
})
|
||||
|
||||
it('does not guess a basemap when a floor has multiple nonmatching versions', () => {
|
||||
const older = createBasemap()
|
||||
const newer = { ...createBasemap(), modelVersion: 'v2' }
|
||||
const manifest = {
|
||||
schemaVersion: GUIDE_FLOOR_BASEMAP_SCHEMA_VERSION,
|
||||
render: {
|
||||
projection: GUIDE_FLOOR_BASEMAP_PROJECTION,
|
||||
projectionVersion: GUIDE_FLOOR_BASEMAP_PROJECTION_VERSION
|
||||
},
|
||||
floors: [older, newer],
|
||||
routeAssets: []
|
||||
}
|
||||
|
||||
expect(findGuideFloorBasemap(manifest, 'L1', 'v3')).toBeNull()
|
||||
})
|
||||
|
||||
it('requires an orthographic floor entry under the schema 4 render identity', () => {
|
||||
const basemap = createBasemap()
|
||||
const manifest = {
|
||||
|
||||
@@ -29,7 +29,13 @@ const ThreeMapStub = defineComponent({
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['floor-change', 'target-focus', 'auto-switch', 'render-mode-fallback'],
|
||||
emits: [
|
||||
'floor-change',
|
||||
'target-focus',
|
||||
'auto-switch',
|
||||
'render-mode-fallback',
|
||||
'semantic-exterior-exit'
|
||||
],
|
||||
setup(_, { expose }) {
|
||||
expose({
|
||||
switchFloor: vi.fn(),
|
||||
@@ -48,6 +54,12 @@ const ThreeMapStub = defineComponent({
|
||||
template: '<div data-testid="three-map"></div>'
|
||||
})
|
||||
|
||||
const TencentJsMapStub = defineComponent({
|
||||
name: 'TencentJsMap',
|
||||
emits: ['semantic-enter-building'],
|
||||
template: '<div data-testid="tencent-js-map"></div>'
|
||||
})
|
||||
|
||||
const modelSource = {
|
||||
loadPackage: async () => ({ overviewModelUrl: '', floors: [] }),
|
||||
loadFloorPois: async () => []
|
||||
@@ -166,6 +178,54 @@ describe('GuideMapShell 点位过滤契约', () => {
|
||||
expect(wrapper.emitted('targetFocus')).toEqual([[focusResult]])
|
||||
})
|
||||
|
||||
it('仅在语义缩放启用时转发室内外层级切换事件', async () => {
|
||||
const indoorWrapper = mount(GuideMapShell, {
|
||||
props: {
|
||||
mapType: 'indoor',
|
||||
indoorModelSource: modelSource,
|
||||
floors: [{ id: 'L1', label: '1F' }],
|
||||
activeFloor: 'L1',
|
||||
semanticZoomEnabled: true
|
||||
},
|
||||
global: { stubs: { ThreeMap: ThreeMapStub } }
|
||||
})
|
||||
indoorWrapper.getComponent(ThreeMapStub).vm.$emit('semantic-exterior-exit')
|
||||
await indoorWrapper.vm.$nextTick()
|
||||
expect(indoorWrapper.emitted('semanticExitOutdoor')).toEqual([[]])
|
||||
|
||||
const outdoorWrapper = mount(GuideMapShell, {
|
||||
props: {
|
||||
mapType: 'outdoor',
|
||||
indoorModelSource: modelSource,
|
||||
floors: [{ id: 'L1', label: '1F' }],
|
||||
activeFloor: 'L1',
|
||||
semanticZoomEnabled: true
|
||||
},
|
||||
global: { stubs: { TencentJsMap: TencentJsMapStub, TencentMap: true } }
|
||||
})
|
||||
const viewport = { latitude: 22.692763, longitude: 114.363572, zoom: 18.5 }
|
||||
outdoorWrapper.getComponent(TencentJsMapStub).vm.$emit('semantic-enter-building', viewport)
|
||||
await outdoorWrapper.vm.$nextTick()
|
||||
expect(outdoorWrapper.emitted('semanticEnterBuilding')).toEqual([[viewport]])
|
||||
})
|
||||
|
||||
it('语义缩放关闭时不转发地图层级切换事件', async () => {
|
||||
const wrapper = mount(GuideMapShell, {
|
||||
props: {
|
||||
mapType: 'indoor',
|
||||
indoorModelSource: modelSource,
|
||||
floors: [{ id: 'L1', label: '1F' }],
|
||||
activeFloor: 'L1',
|
||||
semanticZoomEnabled: false
|
||||
},
|
||||
global: { stubs: { ThreeMap: ThreeMapStub } }
|
||||
})
|
||||
|
||||
wrapper.getComponent(ThreeMapStub).vm.$emit('semantic-exterior-exit')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.emitted('semanticExitOutdoor')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('忽略旧渲染版本的 WebP 回退事件', async () => {
|
||||
const wrapper = mount(GuideMapShell, {
|
||||
props: {
|
||||
|
||||
52
tests/unit/GuideSemanticZoom.spec.ts
Normal file
52
tests/unit/GuideSemanticZoom.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
OUTDOOR_BUILDING_EXIT_ZOOM,
|
||||
OUTDOOR_BUILDING_ENTRY_ZOOM,
|
||||
canEnterBuildingSemanticMap,
|
||||
distanceBetweenCoordinatesMeters,
|
||||
hasReachedOutdoorExitBoundary
|
||||
} from '@/domain/guideSemanticZoom'
|
||||
|
||||
const museum = { latitude: 22.692763, longitude: 114.363572 }
|
||||
|
||||
describe('guide semantic zoom', () => {
|
||||
it('uses a stable outdoor extent below the building-entry threshold', () => {
|
||||
expect(OUTDOOR_BUILDING_EXIT_ZOOM).toBeLessThan(OUTDOOR_BUILDING_ENTRY_ZOOM)
|
||||
})
|
||||
|
||||
it('starts the outdoor handoff before the 3D camera reaches its hard zoom clamp', () => {
|
||||
expect(hasReachedOutdoorExitBoundary(92, 100)).toBe(true)
|
||||
expect(hasReachedOutdoorExitBoundary(91.9, 100)).toBe(false)
|
||||
expect(hasReachedOutdoorExitBoundary(0, 0)).toBe(false)
|
||||
})
|
||||
|
||||
it('enters the building only when the museum is centered at the entry zoom', () => {
|
||||
expect(canEnterBuildingSemanticMap({
|
||||
...museum,
|
||||
zoom: OUTDOOR_BUILDING_ENTRY_ZOOM
|
||||
}, museum)).toBe(true)
|
||||
})
|
||||
|
||||
it('does not enter the building when the outdoor map has not been zoomed in enough', () => {
|
||||
expect(canEnterBuildingSemanticMap({
|
||||
...museum,
|
||||
zoom: OUTDOOR_BUILDING_ENTRY_ZOOM - 0.1
|
||||
}, museum)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not switch for a similarly zoomed but unrelated outdoor location', () => {
|
||||
expect(canEnterBuildingSemanticMap({
|
||||
latitude: 22.698,
|
||||
longitude: 114.37,
|
||||
zoom: OUTDOOR_BUILDING_ENTRY_ZOOM + 1
|
||||
}, museum)).toBe(false)
|
||||
})
|
||||
|
||||
it('calculates a usable geographic distance for the focus guard', () => {
|
||||
expect(distanceBetweenCoordinatesMeters(museum, museum)).toBe(0)
|
||||
expect(distanceBetweenCoordinatesMeters(museum, {
|
||||
latitude: 22.693763,
|
||||
longitude: 114.363572
|
||||
})).toBeGreaterThan(100)
|
||||
})
|
||||
})
|
||||
@@ -53,6 +53,64 @@ const hostEnvironmentMocks = vi.hoisted(() => ({
|
||||
embeddedInWechatMiniProgram: false
|
||||
}))
|
||||
|
||||
const outdoorMapMocks = vi.hoisted(() => ({
|
||||
getOutdoorMapProfile: vi.fn(async () => ({
|
||||
enabled: true,
|
||||
h5Enabled: true,
|
||||
available: true,
|
||||
state: 'AVAILABLE',
|
||||
message: '腾讯地图服务探测正常',
|
||||
quotaStatus: 'NOT_QUERYABLE',
|
||||
availableModes: ['DRIVING', 'WALKING', 'TRANSIT'],
|
||||
jsMapEnabled: true,
|
||||
jsMapKey: 'test-js-key'
|
||||
})),
|
||||
refreshOutdoorMapProfile: vi.fn(async () => ({
|
||||
enabled: true,
|
||||
h5Enabled: true,
|
||||
available: true,
|
||||
state: 'AVAILABLE',
|
||||
message: '腾讯地图服务探测正常',
|
||||
quotaStatus: 'NOT_QUERYABLE',
|
||||
availableModes: ['DRIVING', 'WALKING', 'TRANSIT'],
|
||||
jsMapEnabled: true,
|
||||
jsMapKey: 'test-js-key'
|
||||
})),
|
||||
subscribeOutdoorMapAvailability: vi.fn(() => () => {}),
|
||||
searchOutdoorPlaces: vi.fn(async () => ({
|
||||
items: [{
|
||||
id: 'bus-stop-museum',
|
||||
title: '深圳自然博物馆[公交站]',
|
||||
address: 'M574路',
|
||||
category: '公交站',
|
||||
latitude: 22.69258,
|
||||
longitude: 114.36372,
|
||||
distance: 80
|
||||
}]
|
||||
})),
|
||||
suggestOutdoorPlaces: vi.fn(async () => ({
|
||||
items: [{
|
||||
id: 'manual-start-suggestion',
|
||||
title: '深圳北站',
|
||||
address: '深圳市龙华区',
|
||||
category: '火车站',
|
||||
latitude: 22.609,
|
||||
longitude: 114.03
|
||||
}]
|
||||
})),
|
||||
reverseGeocodeOutdoorLocation: vi.fn(async () => ({
|
||||
address: '深圳市坪山区龙坪路 1 号',
|
||||
latitude: 22.688,
|
||||
longitude: 114.359
|
||||
})),
|
||||
planOutdoorRoute: vi.fn()
|
||||
}))
|
||||
|
||||
const browserLocationMocks = vi.hoisted(() => ({
|
||||
getBrowserGcjLocation: vi.fn(),
|
||||
getBrowserLocationPermissionState: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@dcloudio/uni-app', () => ({
|
||||
onLoad: vi.fn(),
|
||||
onShow: (handler: () => Promise<void> | void) => {
|
||||
@@ -64,6 +122,21 @@ vi.mock('@/utils/hostEnvironment', () => ({
|
||||
isEmbeddedInWechatMiniProgram: () => hostEnvironmentMocks.embeddedInWechatMiniProgram
|
||||
}))
|
||||
|
||||
vi.mock('@/services/outdoor/OutdoorMapAppService', () => outdoorMapMocks)
|
||||
|
||||
vi.mock('@/services/outdoor/BrowserLocationService', () => ({
|
||||
BrowserLocationError: class BrowserLocationError extends Error {
|
||||
reason: string
|
||||
|
||||
constructor(reason: string) {
|
||||
super(reason)
|
||||
this.reason = reason
|
||||
}
|
||||
},
|
||||
getBrowserGcjLocation: browserLocationMocks.getBrowserGcjLocation,
|
||||
getBrowserLocationPermissionState: browserLocationMocks.getBrowserLocationPermissionState
|
||||
}))
|
||||
|
||||
const floors = [
|
||||
{ id: 'L1', label: '1F', order: 1 },
|
||||
{ id: 'L2', label: '2F', order: 2 }
|
||||
@@ -120,13 +193,16 @@ const GuideMapShellStub = defineComponent({
|
||||
indoorView: { type: String, default: '' },
|
||||
visiblePoiIds: { type: Array, default: null },
|
||||
targetFocusRequest: { type: Object, default: null },
|
||||
mapType: { type: String, default: 'indoor' },
|
||||
outdoorMarkers: { type: Array, default: () => [] },
|
||||
activeOutdoorMarkerId: { type: String, default: '' },
|
||||
showFloor: { type: Boolean, default: false },
|
||||
floorBottom: { type: String, default: '' },
|
||||
disableAutoExit: { type: Boolean, default: false },
|
||||
routeStartSelectionActive: { type: Boolean, default: false },
|
||||
routeSelectablePoiIds: { type: Array, default: () => [] }
|
||||
},
|
||||
emits: ['floor-change', 'poi-click', 'selection-clear', 'indoor-view-change', 'auto-switch', 'route-start-candidate', 'route-start-candidate-rejected', 'route-roaming-progress'],
|
||||
emits: ['floor-change', 'poi-click', 'selection-clear', 'indoor-view-change', 'auto-switch', 'semantic-exit-outdoor', 'route-start-candidate', 'route-start-candidate-rejected', 'route-roaming-progress', 'map-tap'],
|
||||
setup() {
|
||||
onMounted(() => {
|
||||
testState.mapMountCount += 1
|
||||
@@ -199,12 +275,49 @@ const RoutePlannerPanelStub = defineComponent({
|
||||
template: '<section data-testid="route-planner"></section>'
|
||||
})
|
||||
|
||||
const ArrivalPanelStub = defineComponent({
|
||||
name: 'ArrivalPanel',
|
||||
props: {
|
||||
mode: { type: String, default: 'browse' },
|
||||
activeType: { type: String, default: null },
|
||||
targets: { type: Array, default: () => [] },
|
||||
selectedTarget: { type: Object, default: null }
|
||||
},
|
||||
emits: ['type-change', 'select-target', 'navigate', 'back', 'retry', 'close'],
|
||||
template: '<section data-testid="arrival-panel"></section>'
|
||||
})
|
||||
|
||||
const OutdoorArrivalRoutePanelStub = defineComponent({
|
||||
name: 'OutdoorArrivalRoutePanel',
|
||||
props: {
|
||||
phase: { type: String, default: 'locating' },
|
||||
locationHelpVisible: { type: Boolean, default: false },
|
||||
startPoint: { type: Object, default: null },
|
||||
destination: { type: Object, default: null },
|
||||
loading: { type: Boolean, default: false },
|
||||
routeError: { type: String, default: '' },
|
||||
manualSearchResults: { type: Array, default: () => [] }
|
||||
},
|
||||
emits: [
|
||||
'retry-location',
|
||||
'manual-start-request',
|
||||
'manual-map-request',
|
||||
'manual-start-confirm',
|
||||
'manual-start-search',
|
||||
'confirm-start',
|
||||
'retry-route',
|
||||
'back'
|
||||
],
|
||||
template: '<section data-testid="outdoor-route-panel"></section>'
|
||||
})
|
||||
|
||||
const stubs = {
|
||||
GuidePageFrame: GuidePageFrameStub,
|
||||
GuideMapShell: GuideMapShellStub,
|
||||
PoiSearchPanel: PoiSearchPanelStub,
|
||||
RoutePlannerPanel: RoutePlannerPanelStub,
|
||||
ArrivalPanel: true,
|
||||
ArrivalPanel: ArrivalPanelStub,
|
||||
OutdoorArrivalRoutePanel: OutdoorArrivalRoutePanelStub,
|
||||
ExplainHallSelect: true
|
||||
}
|
||||
|
||||
@@ -262,6 +375,12 @@ beforeEach(() => {
|
||||
testState.hallResolutionPromises = []
|
||||
testState.explainResults = []
|
||||
testState.routeTargets = []
|
||||
outdoorMapMocks.searchOutdoorPlaces.mockClear()
|
||||
outdoorMapMocks.suggestOutdoorPlaces.mockClear()
|
||||
outdoorMapMocks.reverseGeocodeOutdoorLocation.mockClear()
|
||||
outdoorMapMocks.planOutdoorRoute.mockClear()
|
||||
browserLocationMocks.getBrowserGcjLocation.mockReset()
|
||||
browserLocationMocks.getBrowserLocationPermissionState.mockReset()
|
||||
window.history.replaceState({}, '', '/#/pages/index/index?tab=guide')
|
||||
vi.stubGlobal('uni', {
|
||||
navigateTo: vi.fn(),
|
||||
@@ -371,13 +490,14 @@ describe('首页搜索与地图闭环', () => {
|
||||
expect(window.location.hash).toBe('#/pages/index/index?tab=guide&embedded=wechat-mini-program')
|
||||
})
|
||||
|
||||
it('首页暂不展示来馆入口,导览快捷按钮文案固定且保留原切换行为', async () => {
|
||||
it('首页来馆入口默认聚焦博物馆,仅在选择类别后读取并联动附近点位', async () => {
|
||||
const wrapper = await mountIndex()
|
||||
const map = wrapper.getComponent(GuideMapShellStub)
|
||||
const guideAction = wrapper.findAll('.guide-quick-action')[0]
|
||||
const arrivalAction = wrapper.findAll('.guide-quick-action')[0]
|
||||
const guideAction = wrapper.findAll('.guide-quick-action')[1]
|
||||
|
||||
expect(wrapper.text()).not.toContain('来馆')
|
||||
expect(wrapper.findAll('.guide-quick-action').map((item) => item.text())).toEqual([
|
||||
'来馆',
|
||||
'导览',
|
||||
'讲解'
|
||||
])
|
||||
@@ -388,12 +508,292 @@ describe('首页搜索与地图闭环', () => {
|
||||
map.vm.$emit('floor-change', 'L1')
|
||||
await flushPromises()
|
||||
expect(map.props('indoorView')).toBe('floor')
|
||||
expect(wrapper.findAll('.guide-quick-action')[0]?.text()).toBe('导览')
|
||||
expect(wrapper.findAll('.guide-quick-action')[1]?.text()).toBe('导览')
|
||||
|
||||
await guideAction.trigger('tap')
|
||||
await flushPromises()
|
||||
expect(map.props('indoorView')).toBe('overview')
|
||||
expect(wrapper.findAll('.guide-quick-action')[0]?.text()).toBe('导览')
|
||||
expect(wrapper.findAll('.guide-quick-action')[1]?.text()).toBe('导览')
|
||||
|
||||
await arrivalAction.trigger('tap')
|
||||
await flushPromises()
|
||||
expect(map.props('mapType')).toBe('outdoor')
|
||||
const arrivalPanel = wrapper.getComponent(ArrivalPanelStub)
|
||||
expect(arrivalPanel.props('mode')).toBe('browse')
|
||||
expect(arrivalPanel.props('activeType')).toBeNull()
|
||||
expect(arrivalPanel.props('targets')).toEqual([])
|
||||
expect(map.props('outdoorMarkers')).toEqual([expect.objectContaining({
|
||||
id: 'museum-destination',
|
||||
title: '深圳自然博物馆'
|
||||
})])
|
||||
expect(map.props('activeOutdoorMarkerId')).toBe('museum-destination')
|
||||
|
||||
arrivalPanel.vm.$emit('type-change', 'bus')
|
||||
await flushPromises()
|
||||
expect(arrivalPanel.props('activeType')).toBe('bus')
|
||||
expect(arrivalPanel.props('targets')).toEqual([expect.objectContaining({
|
||||
id: 'bus-stop-museum',
|
||||
title: '深圳自然博物馆[公交站]'
|
||||
})])
|
||||
expect(map.props('outdoorMarkers')).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'museum-destination', markerKind: 'museum' }),
|
||||
expect.objectContaining({ id: 'bus-stop-museum', markerKind: 'bus' })
|
||||
]))
|
||||
|
||||
const busTarget = arrivalPanel.props('targets')[0] as { id: string; type: string }
|
||||
arrivalPanel.vm.$emit('select-target', busTarget)
|
||||
await flushPromises()
|
||||
expect(arrivalPanel.props('mode')).toBe('detail')
|
||||
expect(map.props('activeOutdoorMarkerId')).toBe('bus-stop-museum')
|
||||
expect(wrapper.find('.guide-quick-actions').classes()).toContain('is-outdoor')
|
||||
expect(wrapper.findAll('.guide-quick-action').map((item) => item.text())).toEqual([
|
||||
'来馆',
|
||||
'导览',
|
||||
'讲解'
|
||||
])
|
||||
expect(wrapper.findAll('.guide-quick-action')[0]?.classes()).toContain('active')
|
||||
})
|
||||
|
||||
it('腾讯地图状态不可用时隐藏来馆入口并保持馆内导览', async () => {
|
||||
outdoorMapMocks.refreshOutdoorMapProfile.mockResolvedValueOnce({
|
||||
enabled: true,
|
||||
h5Enabled: false,
|
||||
available: false,
|
||||
state: 'PROVIDER_UNAVAILABLE',
|
||||
message: '腾讯地图服务暂时不可用',
|
||||
quotaStatus: 'NOT_QUERYABLE',
|
||||
availableModes: [],
|
||||
jsMapEnabled: false,
|
||||
jsMapKey: null
|
||||
})
|
||||
|
||||
const wrapper = await mountIndex()
|
||||
const map = wrapper.getComponent(GuideMapShellStub)
|
||||
|
||||
expect(wrapper.findAll('.guide-quick-action').map((item) => item.text())).toEqual(['导览', '讲解'])
|
||||
map.vm.$emit('semantic-exit-outdoor')
|
||||
await flushPromises()
|
||||
expect(map.props('mapType')).toBe('indoor')
|
||||
})
|
||||
|
||||
it('地铁和停车场分类同样在地图上显示可点击的高亮标记', async () => {
|
||||
const wrapper = await mountIndex()
|
||||
const map = wrapper.getComponent(GuideMapShellStub)
|
||||
const arrivalAction = wrapper.findAll('.guide-quick-action')[0]
|
||||
|
||||
await arrivalAction.trigger('tap')
|
||||
await flushPromises()
|
||||
const arrivalPanel = wrapper.getComponent(ArrivalPanelStub)
|
||||
|
||||
arrivalPanel.vm.$emit('type-change', 'metro')
|
||||
await flushPromises()
|
||||
expect(map.props('outdoorMarkers')).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'museum-destination', markerKind: 'museum' }),
|
||||
expect.objectContaining({ id: 'bus-stop-museum', markerKind: 'metro' })
|
||||
]))
|
||||
|
||||
arrivalPanel.vm.$emit('type-change', 'parking')
|
||||
await flushPromises()
|
||||
expect(map.props('outdoorMarkers')).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'museum-destination', markerKind: 'museum' }),
|
||||
expect.objectContaining({ id: 'bus-stop-museum', markerKind: 'parking' })
|
||||
]))
|
||||
})
|
||||
|
||||
it('来馆分类在当前页面会复用短时缓存,不重复查询腾讯地点服务', async () => {
|
||||
const wrapper = await mountIndex()
|
||||
await wrapper.findAll('.guide-quick-action')[0].trigger('tap')
|
||||
await flushPromises()
|
||||
const arrivalPanel = wrapper.getComponent(ArrivalPanelStub)
|
||||
|
||||
arrivalPanel.vm.$emit('type-change', 'bus')
|
||||
await flushPromises()
|
||||
arrivalPanel.vm.$emit('type-change', 'metro')
|
||||
await flushPromises()
|
||||
arrivalPanel.vm.$emit('type-change', 'bus')
|
||||
await flushPromises()
|
||||
|
||||
expect(outdoorMapMocks.searchOutdoorPlaces).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('缩小建筑外观切换到来馆时展示初始分类查询栏', async () => {
|
||||
const wrapper = await mountIndex()
|
||||
const map = wrapper.getComponent(GuideMapShellStub)
|
||||
|
||||
map.vm.$emit('semantic-exit-outdoor')
|
||||
await flushPromises()
|
||||
|
||||
expect(map.props('mapType')).toBe('outdoor')
|
||||
const arrivalPanel = wrapper.getComponent(ArrivalPanelStub)
|
||||
expect(arrivalPanel.props('mode')).toBe('browse')
|
||||
expect(arrivalPanel.props('activeType')).toBeNull()
|
||||
expect(arrivalPanel.props('targets')).toEqual([])
|
||||
expect(map.props('outdoorMarkers')).toEqual([expect.objectContaining({
|
||||
id: 'museum-destination',
|
||||
title: '深圳自然博物馆'
|
||||
})])
|
||||
})
|
||||
|
||||
it('来馆目的地确认后,实时定位成功即自动调用腾讯路线规划', async () => {
|
||||
browserLocationMocks.getBrowserGcjLocation.mockResolvedValue({
|
||||
latitude: 22.688,
|
||||
longitude: 114.359,
|
||||
accuracyMeters: 18,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
outdoorMapMocks.planOutdoorRoute.mockResolvedValue({
|
||||
mode: 'TRANSIT',
|
||||
distance: 1850,
|
||||
duration: 840,
|
||||
polyline: []
|
||||
})
|
||||
|
||||
const wrapper = await mountIndex()
|
||||
await wrapper.findAll('.guide-quick-action')[0].trigger('tap')
|
||||
await flushPromises()
|
||||
const arrivalPanel = wrapper.getComponent(ArrivalPanelStub)
|
||||
const destination = {
|
||||
id: 'bus-stop-museum',
|
||||
type: 'bus',
|
||||
title: '深圳自然博物馆[公交站]',
|
||||
subtitle: 'M574路',
|
||||
keyword: '深圳自然博物馆[公交站]',
|
||||
region: '深圳市',
|
||||
latitude: 22.69258,
|
||||
longitude: 114.36372
|
||||
}
|
||||
|
||||
arrivalPanel.vm.$emit('navigate', destination)
|
||||
await flushPromises()
|
||||
|
||||
const routePanel = wrapper.getComponent(OutdoorArrivalRoutePanelStub)
|
||||
expect(routePanel.props('phase')).toBe('route')
|
||||
expect(routePanel.props('startPoint')).toMatchObject({ name: '我的位置' })
|
||||
expect(outdoorMapMocks.planOutdoorRoute).toHaveBeenCalledWith(expect.objectContaining({
|
||||
from: expect.objectContaining({ name: '我的位置' }),
|
||||
to: expect.objectContaining({ id: 'bus-stop-museum' }),
|
||||
mode: 'TRANSIT'
|
||||
}))
|
||||
})
|
||||
|
||||
it('定位不可用时保留重新检测、输入地点和地图选点入口', async () => {
|
||||
browserLocationMocks.getBrowserGcjLocation.mockRejectedValue(new Error('未获得位置权限'))
|
||||
const wrapper = await mountIndex()
|
||||
await wrapper.findAll('.guide-quick-action')[0].trigger('tap')
|
||||
await flushPromises()
|
||||
const arrivalPanel = wrapper.getComponent(ArrivalPanelStub)
|
||||
const destination = {
|
||||
id: 'bus-stop-museum',
|
||||
type: 'bus',
|
||||
title: '深圳自然博物馆[公交站]',
|
||||
subtitle: 'M574路',
|
||||
keyword: '深圳自然博物馆[公交站]',
|
||||
region: '深圳市',
|
||||
latitude: 22.69258,
|
||||
longitude: 114.36372
|
||||
}
|
||||
|
||||
arrivalPanel.vm.$emit('navigate', destination)
|
||||
await flushPromises()
|
||||
|
||||
const routePanel = wrapper.getComponent(OutdoorArrivalRoutePanelStub)
|
||||
expect(routePanel.props('phase')).toBe('permission-needed')
|
||||
|
||||
browserLocationMocks.getBrowserLocationPermissionState.mockResolvedValue('denied')
|
||||
routePanel.vm.$emit('retry-location')
|
||||
await flushPromises()
|
||||
expect(routePanel.props('locationHelpVisible')).toBe(true)
|
||||
|
||||
routePanel.vm.$emit('manual-start-request')
|
||||
await flushPromises()
|
||||
expect(routePanel.props('phase')).toBe('manual-search')
|
||||
|
||||
routePanel.vm.$emit('manual-map-request')
|
||||
await flushPromises()
|
||||
expect(routePanel.props('phase')).toBe('manual-map')
|
||||
|
||||
routePanel.vm.$emit('manual-start-confirm', {
|
||||
title: '深圳北站',
|
||||
latitude: 22.609,
|
||||
longitude: 114.03
|
||||
})
|
||||
await flushPromises()
|
||||
expect(routePanel.props('phase')).toBe('confirm-start')
|
||||
expect(routePanel.props('startPoint')).toMatchObject({ name: '深圳北站' })
|
||||
})
|
||||
|
||||
it('手动起点使用输入提示,地图选点会补充可读地址并标记当前位置', async () => {
|
||||
browserLocationMocks.getBrowserGcjLocation.mockRejectedValue(new Error('未获得位置权限'))
|
||||
const wrapper = await mountIndex()
|
||||
const map = wrapper.getComponent(GuideMapShellStub)
|
||||
await wrapper.findAll('.guide-quick-action')[0].trigger('tap')
|
||||
await flushPromises()
|
||||
const arrivalPanel = wrapper.getComponent(ArrivalPanelStub)
|
||||
const destination = {
|
||||
id: 'bus-stop-museum',
|
||||
type: 'bus',
|
||||
title: '深圳自然博物馆[公交站]',
|
||||
subtitle: 'M574路',
|
||||
keyword: '深圳自然博物馆[公交站]',
|
||||
region: '深圳市',
|
||||
latitude: 22.69258,
|
||||
longitude: 114.36372
|
||||
}
|
||||
|
||||
arrivalPanel.vm.$emit('navigate', destination)
|
||||
await flushPromises()
|
||||
const routePanel = wrapper.getComponent(OutdoorArrivalRoutePanelStub)
|
||||
routePanel.vm.$emit('manual-start-request')
|
||||
await flushPromises()
|
||||
routePanel.vm.$emit('manual-start-search', '深圳北')
|
||||
await flushPromises()
|
||||
expect(outdoorMapMocks.suggestOutdoorPlaces).toHaveBeenCalledWith(expect.objectContaining({ keyword: '深圳北' }))
|
||||
|
||||
routePanel.vm.$emit('manual-map-request')
|
||||
await flushPromises()
|
||||
map.vm.$emit('map-tap', { latitude: 22.688, longitude: 114.359 })
|
||||
await flushPromises()
|
||||
|
||||
expect(outdoorMapMocks.reverseGeocodeOutdoorLocation).toHaveBeenCalledWith({ latitude: 22.688, longitude: 114.359 })
|
||||
expect(routePanel.props('phase')).toBe('confirm-start')
|
||||
expect(routePanel.props('startPoint')).toMatchObject({ name: '深圳市坪山区龙坪路 1 号' })
|
||||
expect(map.props('outdoorMarkers')).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ id: 'outdoor-route-start', markerKind: 'user' })
|
||||
]))
|
||||
})
|
||||
|
||||
it('切换手动起点方式后忽略迟到的输入提示结果', async () => {
|
||||
browserLocationMocks.getBrowserGcjLocation.mockRejectedValue(new Error('未获得位置权限'))
|
||||
let resolveSuggestion: ((value: { items: Array<Record<string, unknown>> }) => void) | null = null
|
||||
outdoorMapMocks.suggestOutdoorPlaces.mockImplementationOnce(() => new Promise((resolve) => {
|
||||
resolveSuggestion = resolve
|
||||
}))
|
||||
const wrapper = await mountIndex()
|
||||
await wrapper.findAll('.guide-quick-action')[0].trigger('tap')
|
||||
await flushPromises()
|
||||
const arrivalPanel = wrapper.getComponent(ArrivalPanelStub)
|
||||
arrivalPanel.vm.$emit('navigate', {
|
||||
id: 'bus-stop-museum',
|
||||
type: 'bus',
|
||||
title: '深圳自然博物馆[公交站]',
|
||||
subtitle: 'M574路',
|
||||
keyword: '深圳自然博物馆[公交站]',
|
||||
region: '深圳市',
|
||||
latitude: 22.69258,
|
||||
longitude: 114.36372
|
||||
})
|
||||
await flushPromises()
|
||||
const routePanel = wrapper.getComponent(OutdoorArrivalRoutePanelStub)
|
||||
routePanel.vm.$emit('manual-start-request')
|
||||
await flushPromises()
|
||||
routePanel.vm.$emit('manual-start-search', '深圳北')
|
||||
await flushPromises()
|
||||
routePanel.vm.$emit('manual-map-request')
|
||||
resolveSuggestion?.({ items: [{ id: 'late-result', title: '迟到结果' }] })
|
||||
await flushPromises()
|
||||
|
||||
expect(routePanel.props('phase')).toBe('manual-map')
|
||||
expect(routePanel.props('manualSearchResults')).toEqual([])
|
||||
})
|
||||
|
||||
it('向搜索面板传入当前楼层,并将分类结果 ID 精确传给地图', async () => {
|
||||
|
||||
118
tests/unit/OutdoorArrivalRoutePanel.spec.ts
Normal file
118
tests/unit/OutdoorArrivalRoutePanel.spec.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import OutdoorArrivalRoutePanel from '@/components/navigation/OutdoorArrivalRoutePanel.vue'
|
||||
|
||||
const destination = {
|
||||
title: '深圳自然博物馆[公交站]',
|
||||
address: 'M574路',
|
||||
latitude: 22.69258,
|
||||
longitude: 114.36372
|
||||
}
|
||||
|
||||
const start = {
|
||||
name: '我的位置',
|
||||
latitude: 22.688,
|
||||
longitude: 114.359
|
||||
}
|
||||
|
||||
const mountPanel = (overrides: Record<string, unknown> = {}) => mount(OutdoorArrivalRoutePanel, {
|
||||
props: {
|
||||
visible: true,
|
||||
phase: 'permission-needed',
|
||||
destination,
|
||||
startPoint: null,
|
||||
loading: false,
|
||||
routeError: '',
|
||||
routeDistance: 0,
|
||||
routeDuration: 0,
|
||||
manualSearchResults: [],
|
||||
manualSearchLoading: false,
|
||||
...overrides
|
||||
}
|
||||
})
|
||||
|
||||
describe('来馆路线起点确认', () => {
|
||||
it('定位不可用时提示重新检测或手动选择,而不显示旧馆外导览界面', async () => {
|
||||
const wrapper = mountPanel({
|
||||
locationMessage: '请在微信小程序的设置中开启位置权限后重试'
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('暂未获取实时位置')
|
||||
expect(wrapper.text()).toContain('重新检测')
|
||||
expect(wrapper.text()).toContain('手动选择起点')
|
||||
expect(wrapper.text()).not.toContain('馆外导览')
|
||||
|
||||
await wrapper.get('.outdoor-route-action.secondary').trigger('tap')
|
||||
await wrapper.get('.outdoor-route-action.primary').trigger('tap')
|
||||
|
||||
expect(wrapper.emitted('retryLocation')).toHaveLength(1)
|
||||
expect(wrapper.emitted('manualStartRequest')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('浏览器已拒绝位置权限时,重新检测区域会给出明确的设置指引', () => {
|
||||
const wrapper = mountPanel({
|
||||
locationHelpVisible: true,
|
||||
locationMessage: '当前位置权限已关闭'
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('请开启位置权限')
|
||||
expect(wrapper.text()).toContain('微信小程序的设置')
|
||||
expect(wrapper.text()).toContain('重新检测')
|
||||
})
|
||||
|
||||
it('手动选择的起点保留确认操作,避免误用地图点位', async () => {
|
||||
const wrapper = mountPanel({
|
||||
phase: 'confirm-start',
|
||||
startPoint: { ...start, name: '深圳北站' }
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('深圳北站')
|
||||
expect(wrapper.text()).toContain('从这里出发')
|
||||
|
||||
await wrapper.get('.outdoor-route-action.primary').trigger('tap')
|
||||
await wrapper.get('.outdoor-route-text-action').trigger('tap')
|
||||
|
||||
expect(wrapper.emitted('confirmStart')).toHaveLength(1)
|
||||
expect(wrapper.emitted('manualStartRequest')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('手动起点支持输入地点与地图选点两条路径', async () => {
|
||||
const wrapper = mountPanel({ phase: 'manual-search' })
|
||||
|
||||
await wrapper.get('.outdoor-route-switch + .outdoor-route-switch').trigger('tap')
|
||||
expect(wrapper.emitted('manualMapRequest')).toHaveLength(1)
|
||||
|
||||
await wrapper.setProps({
|
||||
manualSearchResults: [{
|
||||
id: 'ping-shan-station',
|
||||
title: '深圳坪山站',
|
||||
address: '深圳市坪山区',
|
||||
category: '火车站',
|
||||
latitude: 22.699,
|
||||
longitude: 114.347
|
||||
}]
|
||||
})
|
||||
await wrapper.get('.outdoor-route-search-result').trigger('tap')
|
||||
|
||||
expect(wrapper.emitted('manualStartConfirm')).toEqual([[expect.objectContaining({
|
||||
id: 'ping-shan-station',
|
||||
title: '深圳坪山站'
|
||||
})]])
|
||||
})
|
||||
|
||||
it('路线已生成时仅展示路线结果,不重复显示出行方式选择', () => {
|
||||
const wrapper = mountPanel({
|
||||
phase: 'route',
|
||||
startPoint: start,
|
||||
routeDistance: 1850,
|
||||
routeDuration: 14
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain('1.9 公里')
|
||||
expect(wrapper.text()).toContain('约 14 分钟')
|
||||
expect(wrapper.text()).toContain('路线已显示在地图上')
|
||||
expect(wrapper.find('.outdoor-route-mode-list').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -13,6 +13,7 @@ describe('shared POI icon registry', () => {
|
||||
name: '售票机'
|
||||
})).toBe('ticket-office')
|
||||
expect(getPoiIconHref('ticket-office')).toBe(getPoiCategoryIconHref('ticket-office'))
|
||||
expect(getPoiIconHref('ticket-office')).toBe('#poi-ticket-office')
|
||||
})
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -421,7 +421,7 @@ describe('WeakNetworkGuideFallback', () => {
|
||||
const marker = wrapper.get('[data-poi-id="ticket"]')
|
||||
expect(marker.attributes('data-poi-icon')).toBe('ticket-office')
|
||||
expect(marker.get('.weak-network-marker-label-icon use').attributes('href'))
|
||||
.toBe('/static/icons/poi/shortcut-icons.svg#poi-ticket-office')
|
||||
.toBe('#poi-ticket-office')
|
||||
expect(marker.get('.weak-network-marker-label').classes()).toContain('weak-network-marker-label')
|
||||
})
|
||||
|
||||
@@ -557,6 +557,39 @@ describe('WeakNetworkGuideFallback', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('reserves two exterior zoom-out steps before reporting the Tencent Map handoff boundary', async () => {
|
||||
const wrapper = mount(WeakNetworkGuideFallback, {
|
||||
props: {
|
||||
floorId: '2065808919904776194',
|
||||
sceneView: 'overview',
|
||||
floorModelVersion: 'model-v1',
|
||||
basemapManifest: exteriorBasemapManifest,
|
||||
presentation: 'two-dimensional',
|
||||
pois: []
|
||||
}
|
||||
})
|
||||
const renderer = wrapper.vm as unknown as {
|
||||
zoomCamera: (direction: 'in' | 'out') => void
|
||||
}
|
||||
|
||||
renderer.zoomCamera('out')
|
||||
renderer.zoomCamera('out')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const intents = wrapper.emitted('zoomIntent') || []
|
||||
expect(intents).toHaveLength(2)
|
||||
expect(intents[0][0]).toMatchObject({
|
||||
direction: 'out',
|
||||
outdoorExitReached: false,
|
||||
viewport: { visibleWorldSpan: 125 }
|
||||
})
|
||||
expect(intents[1][0]).toMatchObject({
|
||||
direction: 'out',
|
||||
outdoorExitReached: true,
|
||||
viewport: { visibleWorldSpan: 135 }
|
||||
})
|
||||
})
|
||||
|
||||
it('routes native H5 wheel input through the semantic viewport zoom', async () => {
|
||||
const wrapper = mount(WeakNetworkGuideFallback, {
|
||||
props: {
|
||||
@@ -651,7 +684,7 @@ describe('WeakNetworkGuideFallback', () => {
|
||||
expect(wrapper.findAll('.weak-network-route-line')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('falls back to the neutral grid when the floor model version has no basemap', () => {
|
||||
it('keeps the sole valid floor basemap when the model version has advanced', () => {
|
||||
const wrapper = mount(WeakNetworkGuideFallback, {
|
||||
props: {
|
||||
floorId: 'L1',
|
||||
@@ -661,8 +694,8 @@ describe('WeakNetworkGuideFallback', () => {
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('[data-testid="weak-network-basemap"]').exists()).toBe(false)
|
||||
expect(wrapper.get('[data-testid="weak-network-neutral-grid"]').exists()).toBe(true)
|
||||
expect(wrapper.get('[data-testid="weak-network-basemap"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="weak-network-neutral-grid"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps map-based route-start selection available in the two-dimensional guide', async () => {
|
||||
|
||||
@@ -4,11 +4,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const target = { latitude: 22.604321, longitude: 114.058765, name: '停车场 A&B', address: '龙华区 #1' }
|
||||
const setEmbeddedRuntime = () => Object.assign(window, { __wxjs_environment: 'miniprogram' })
|
||||
const addWechatSdkMarker = () => {
|
||||
const script = document.createElement('script')
|
||||
script.dataset.wechatJsSdk = 'true'
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
const loadService = async () => {
|
||||
vi.resetModules()
|
||||
@@ -38,7 +33,6 @@ describe('微信小程序 web-view 桥接', () => {
|
||||
|
||||
it('等待延迟注入的 bridge 后导航到编码后的宿主页', async () => {
|
||||
setEmbeddedRuntime()
|
||||
addWechatSdkMarker()
|
||||
const { openWechatMiniProgramLocation } = await loadService()
|
||||
const result = openWechatMiniProgramLocation(target)
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
@@ -90,45 +84,4 @@ describe('微信小程序 web-view 桥接', () => {
|
||||
await expect(first).resolves.toEqual({ status: 'opened' })
|
||||
expect(navigateTo).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('等待延迟 bridge 后返回宿主小程序', async () => {
|
||||
setEmbeddedRuntime()
|
||||
addWechatSdkMarker()
|
||||
const { returnToWechatMiniProgram } = await loadService()
|
||||
const result = returnToWechatMiniProgram()
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
const navigateBack = vi.fn(({ success }) => success())
|
||||
Object.assign(window, {
|
||||
wx: {
|
||||
miniProgram: {
|
||||
getEnv: (done: (value: { miniprogram: boolean }) => void) => done({ miniprogram: true }),
|
||||
navigateBack
|
||||
}
|
||||
}
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
|
||||
await expect(result).resolves.toEqual({ status: 'returned' })
|
||||
expect(navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
|
||||
})
|
||||
|
||||
it('原生返回失败时委托宿主页执行安全回退', async () => {
|
||||
setEmbeddedRuntime()
|
||||
const { MINI_PROGRAM_RETURN_MESSAGE, returnToWechatMiniProgram } = await loadService()
|
||||
const postMessage = vi.fn()
|
||||
Object.assign(window, {
|
||||
wx: {
|
||||
miniProgram: {
|
||||
getEnv: (done: (value: { miniprogram: boolean }) => void) => done({ miniprogram: true }),
|
||||
navigateBack: ({ fail }: { fail?: (error: { errMsg?: string }) => void }) => fail?.({ errMsg: 'navigateBack:fail page stack is empty' }),
|
||||
postMessage
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await expect(returnToWechatMiniProgram()).resolves.toEqual({ status: 'returned' })
|
||||
expect(postMessage).toHaveBeenCalledWith({
|
||||
data: { type: MINI_PROGRAM_RETURN_MESSAGE }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
isEmbeddedInWechatMiniProgram
|
||||
} from '@/utils/hostEnvironment'
|
||||
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
|
||||
|
||||
const createWindow = (overrides: Record<string, unknown> = {}) => ({
|
||||
location: { search: '', hash: '' },
|
||||
|
||||
Reference in New Issue
Block a user