Compare commits
2 Commits
267b415673
...
7caa671fe9
| Author | SHA1 | Date | |
|---|---|---|---|
| 7caa671fe9 | |||
| 1f72926f58 |
@@ -20,11 +20,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { onLoad, onUnload } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||||
import ExplainHallSelect, {
|
import ExplainHallSelect, {
|
||||||
type ExplainBusinessUnitSelectItem,
|
|
||||||
type ExplainHallSelectItem
|
type ExplainHallSelectItem
|
||||||
} from '@/components/explain/ExplainHallSelect.vue'
|
} from '@/components/explain/ExplainHallSelect.vue'
|
||||||
import {
|
import {
|
||||||
@@ -69,53 +68,49 @@ const buildExplainHallItems = (
|
|||||||
|
|
||||||
const shouldUseHostNavigation = computed(() => isEmbeddedInWechatMiniProgram())
|
const shouldUseHostNavigation = computed(() => isEmbeddedInWechatMiniProgram())
|
||||||
|
|
||||||
const explainBusinessUnitItems = computed<ExplainBusinessUnitSelectItem[]>(() => (
|
type ExplainListHistoryState = {
|
||||||
[]))
|
museumGuidePage?: 'explain-list'
|
||||||
|
[key: string]: unknown
|
||||||
|
|
||||||
type ExplainHistoryState = {
|
|
||||||
museumExplainList?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let syncingExplainHistory = false
|
let explainListHistoryPushed = false
|
||||||
let applyingExplainHistory = false
|
let returningFromExplainListHistory = false
|
||||||
|
|
||||||
const currentExplainHistoryState = (): ExplainHistoryState => ({
|
const isExplainListHistoryState = (state: unknown): state is ExplainListHistoryState => (
|
||||||
museumExplainList: true
|
Boolean(
|
||||||
})
|
state
|
||||||
|
&& typeof state === 'object'
|
||||||
|
&& (state as ExplainListHistoryState).museumGuidePage === 'explain-list'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const writeExplainHistoryState = (replace = false) => {
|
const pushExplainListHistory = () => {
|
||||||
if (!shouldUseHostNavigation.value || typeof window === 'undefined') return
|
if (!shouldUseHostNavigation.value || typeof window === 'undefined') return
|
||||||
|
if (explainListHistoryPushed) return
|
||||||
|
|
||||||
syncingExplainHistory = true
|
if (isExplainListHistoryState(window.history.state)) {
|
||||||
const state = currentExplainHistoryState()
|
explainListHistoryPushed = true
|
||||||
const url = window.location.href
|
return
|
||||||
|
|
||||||
if (replace) {
|
|
||||||
window.history.replaceState(state, '', url)
|
|
||||||
} else {
|
|
||||||
window.history.pushState(state, '', url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.history.pushState({
|
||||||
syncingExplainHistory = false
|
...(window.history.state || {}),
|
||||||
}, 0)
|
museumGuidePage: 'explain-list'
|
||||||
|
} satisfies ExplainListHistoryState, '', window.location.href)
|
||||||
|
explainListHistoryPushed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyExplainHistoryState = async (state: ExplainHistoryState | null) => {
|
const handleExplainListHistoryPopState = (event: PopStateEvent) => {
|
||||||
if (!state?.museumExplainList || applyingExplainHistory) return
|
if (!shouldUseHostNavigation.value || returningFromExplainListHistory) return
|
||||||
|
|
||||||
applyingExplainHistory = true
|
if (isExplainListHistoryState(event.state)) {
|
||||||
applyingExplainHistory = false
|
explainListHistoryPushed = true
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExplainHistoryPopState = (event: PopStateEvent) => {
|
explainListHistoryPushed = false
|
||||||
if (syncingExplainHistory) return
|
returningFromExplainListHistory = true
|
||||||
void applyExplainHistoryState((event.state || null) as ExplainHistoryState | null)
|
navigateToGuideTopTab('explain')
|
||||||
}
|
|
||||||
|
|
||||||
const replaceExplainStageHistory = () => {
|
|
||||||
writeExplainHistoryState(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadExplainHalls = async () => {
|
const loadExplainHalls = async () => {
|
||||||
@@ -144,21 +139,26 @@ const loadExplainHalls = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onLoad(() => {
|
onLoad(() => {
|
||||||
replaceExplainStageHistory()
|
pushExplainListHistory()
|
||||||
void loadExplainHalls()
|
void loadExplainHalls()
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.addEventListener('popstate', handleExplainHistoryPopState)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnload(() => {
|
onMounted(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window === 'undefined') return
|
||||||
window.removeEventListener('popstate', handleExplainHistoryPopState)
|
window.addEventListener('popstate', handleExplainListHistoryPopState)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
window.removeEventListener('popstate', handleExplainListHistoryPopState)
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
|
if (shouldUseHostNavigation.value) {
|
||||||
|
navigateToGuideTopTab('explain')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
uni.navigateBack({
|
uni.navigateBack({
|
||||||
delta: 1,
|
delta: 1,
|
||||||
fail: () => {
|
fail: () => {
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
<view class="guide-quick-icon-floor top"></view>
|
<view class="guide-quick-icon-floor top"></view>
|
||||||
<view class="guide-quick-icon-floor bottom"></view>
|
<view class="guide-quick-icon-floor bottom"></view>
|
||||||
</view>
|
</view>
|
||||||
<text class="guide-quick-action-text">{{ indoorQuickActionLabel }}</text>
|
<text class="guide-quick-action-text">导览</text>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
class="guide-quick-action"
|
class="guide-quick-action"
|
||||||
@@ -307,6 +307,7 @@ import {
|
|||||||
isGuideTopTab,
|
isGuideTopTab,
|
||||||
type GuideTopTab
|
type GuideTopTab
|
||||||
} from '@/utils/guideTopTabs'
|
} from '@/utils/guideTopTabs'
|
||||||
|
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
|
||||||
import {
|
import {
|
||||||
guideUseCase
|
guideUseCase
|
||||||
} from '@/usecases/guideUseCase'
|
} from '@/usecases/guideUseCase'
|
||||||
@@ -418,7 +419,7 @@ const guideMapShellRef = ref<{
|
|||||||
} | null>(null)
|
} | null>(null)
|
||||||
const homeSearchPanelRef = ref<{
|
const homeSearchPanelRef = ref<{
|
||||||
collapseHomePanel?: () => void
|
collapseHomePanel?: () => void
|
||||||
resetSearchState?: () => void
|
resetSearchState?: () => Promise<void> | void
|
||||||
returnToCollapsedAfterDetail?: () => Promise<void>
|
returnToCollapsedAfterDetail?: () => Promise<void>
|
||||||
restoreResultListScroll?: () => Promise<void>
|
restoreResultListScroll?: () => Promise<void>
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
@@ -430,6 +431,14 @@ let searchTargetFocusRequestId = 0
|
|||||||
let pendingCollapseSearchAfterDetail = false
|
let pendingCollapseSearchAfterDetail = false
|
||||||
let pendingSearchDetailFocusRequest: TargetPoiFocusRequestViewModel | null = null
|
let pendingSearchDetailFocusRequest: TargetPoiFocusRequestViewModel | null = null
|
||||||
|
|
||||||
|
type PoiSearchOverlayHistoryState = {
|
||||||
|
museumGuideOverlay?: 'poi-search'
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
let poiSearchHistoryPushed = false
|
||||||
|
let removingPoiSearchHistory = false
|
||||||
|
|
||||||
let launchOverlayFallbackTimer: ReturnType<typeof setTimeout> | null = null
|
let launchOverlayFallbackTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
let launchOverlayFadeTimer: ReturnType<typeof setTimeout> | null = null
|
let launchOverlayFadeTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
let launchOverlayHideDelayTimer: ReturnType<typeof setTimeout> | null = null
|
let launchOverlayHideDelayTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
@@ -662,9 +671,6 @@ const guideQuickActiveAction = computed<'indoor' | 'arrival' | 'explain'>(() =>
|
|||||||
if (showArrivalPanel.value) return 'arrival'
|
if (showArrivalPanel.value) return 'arrival'
|
||||||
return is3DMode.value ? 'indoor' : 'arrival'
|
return is3DMode.value ? 'indoor' : 'arrival'
|
||||||
})
|
})
|
||||||
const indoorQuickActionLabel = computed(() => (
|
|
||||||
is3DMode.value && indoorView.value !== 'overview' ? '外观' : '馆内'
|
|
||||||
))
|
|
||||||
|
|
||||||
const toRoutePointOption = (target: GuideRouteTarget): RoutePointOption => ({
|
const toRoutePointOption = (target: GuideRouteTarget): RoutePointOption => ({
|
||||||
poiId: target.poiId,
|
poiId: target.poiId,
|
||||||
@@ -725,6 +731,87 @@ const syncTopTabToUrl = (tabId: GuideTopTab) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPoiSearchOverlayHistoryState = (state: unknown): state is PoiSearchOverlayHistoryState => (
|
||||||
|
Boolean(
|
||||||
|
state
|
||||||
|
&& typeof state === 'object'
|
||||||
|
&& (state as PoiSearchOverlayHistoryState).museumGuideOverlay === 'poi-search'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const canManagePoiSearchOverlayHistory = () => (
|
||||||
|
typeof window !== 'undefined'
|
||||||
|
&& isIndexHashRoute()
|
||||||
|
&& isEmbeddedInWechatMiniProgram()
|
||||||
|
)
|
||||||
|
|
||||||
|
const removePoiSearchOverlayFromCurrentHistoryState = () => {
|
||||||
|
if (!canManagePoiSearchOverlayHistory()) return
|
||||||
|
if (!isPoiSearchOverlayHistoryState(window.history.state)) return
|
||||||
|
|
||||||
|
const nextState = { ...window.history.state }
|
||||||
|
delete nextState.museumGuideOverlay
|
||||||
|
window.history.replaceState(nextState, '', window.location.href)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pushPoiSearchOverlayHistory = () => {
|
||||||
|
if (!canManagePoiSearchOverlayHistory() || !homeSearchExpanded.value) return
|
||||||
|
if (poiSearchHistoryPushed) return
|
||||||
|
|
||||||
|
if (isPoiSearchOverlayHistoryState(window.history.state)) {
|
||||||
|
poiSearchHistoryPushed = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.history.pushState({
|
||||||
|
...(window.history.state || {}),
|
||||||
|
museumGuideOverlay: 'poi-search'
|
||||||
|
} satisfies PoiSearchOverlayHistoryState, '', window.location.href)
|
||||||
|
poiSearchHistoryPushed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const removePoiSearchOverlayHistory = () => {
|
||||||
|
if (!canManagePoiSearchOverlayHistory() || !poiSearchHistoryPushed) return
|
||||||
|
|
||||||
|
if (!isPoiSearchOverlayHistoryState(window.history.state)) {
|
||||||
|
poiSearchHistoryPushed = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
poiSearchHistoryPushed = false
|
||||||
|
removingPoiSearchHistory = true
|
||||||
|
window.history.back()
|
||||||
|
}
|
||||||
|
|
||||||
|
const collapsePoiSearchAfterHistoryBack = async () => {
|
||||||
|
homeSearchPanelRef.value?.collapseHomePanel?.()
|
||||||
|
await homeSearchPanelRef.value?.resetSearchState?.()
|
||||||
|
clearHomeSearchMapState()
|
||||||
|
selectedGuidePoi.value = null
|
||||||
|
isPoiCardCollapsed.value = false
|
||||||
|
homeSearchExpanded.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePoiSearchHistoryPopState = (event: PopStateEvent) => {
|
||||||
|
if (!canManagePoiSearchOverlayHistory()) return
|
||||||
|
|
||||||
|
if (removingPoiSearchHistory) {
|
||||||
|
removingPoiSearchHistory = false
|
||||||
|
poiSearchHistoryPushed = false
|
||||||
|
if (homeSearchExpanded.value) pushPoiSearchOverlayHistory()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!homeSearchExpanded.value) return
|
||||||
|
if (isPoiSearchOverlayHistoryState(event.state)) {
|
||||||
|
poiSearchHistoryPushed = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
poiSearchHistoryPushed = false
|
||||||
|
void collapsePoiSearchAfterHistoryBack()
|
||||||
|
}
|
||||||
|
|
||||||
const loadTopTabData = (tabId: GuideTopTab) => {
|
const loadTopTabData = (tabId: GuideTopTab) => {
|
||||||
if (tabId === 'explain') {
|
if (tabId === 'explain') {
|
||||||
void loadExplainHalls()
|
void loadExplainHalls()
|
||||||
@@ -786,7 +873,9 @@ onMounted(() => {
|
|||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
|
|
||||||
syncTopTabFromHash()
|
syncTopTabFromHash()
|
||||||
|
removePoiSearchOverlayFromCurrentHistoryState()
|
||||||
window.addEventListener('hashchange', syncTopTabFromHash)
|
window.addEventListener('hashchange', syncTopTabFromHash)
|
||||||
|
window.addEventListener('popstate', handlePoiSearchHistoryPopState)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -800,6 +889,7 @@ onUnmounted(() => {
|
|||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
|
|
||||||
window.removeEventListener('hashchange', syncTopTabFromHash)
|
window.removeEventListener('hashchange', syncTopTabFromHash)
|
||||||
|
window.removeEventListener('popstate', handlePoiSearchHistoryPopState)
|
||||||
})
|
})
|
||||||
|
|
||||||
onLoad((options: any = {}) => {
|
onLoad((options: any = {}) => {
|
||||||
@@ -1412,6 +1502,12 @@ const handleExplainQuickTap = () => {
|
|||||||
|
|
||||||
const handleHomeSearchExpandedChange = (expanded: boolean) => {
|
const handleHomeSearchExpandedChange = (expanded: boolean) => {
|
||||||
homeSearchExpanded.value = expanded
|
homeSearchExpanded.value = expanded
|
||||||
|
if (expanded) {
|
||||||
|
pushPoiSearchOverlayHistory()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
removePoiSearchOverlayHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearHomeSearchMapState = () => {
|
const clearHomeSearchMapState = () => {
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
export type GuideTopTab = 'guide' | 'explain'
|
export type GuideTopTab = 'guide' | 'explain'
|
||||||
export const GUIDE_LOCATION_PREVIEW_STORAGE_KEY = 'museum-guide:last-location-preview-url'
|
export const GUIDE_LOCATION_PREVIEW_STORAGE_KEY = 'museum-guide:last-location-preview-url'
|
||||||
|
const HOST_NAVIGATION_QUERY_KEYS = [
|
||||||
|
'embedded',
|
||||||
|
'embed',
|
||||||
|
'host',
|
||||||
|
'weapp',
|
||||||
|
'mini-program'
|
||||||
|
]
|
||||||
|
|
||||||
export const GUIDE_TOP_TABS: Array<{ id: GuideTopTab; label: string }> = [
|
export const GUIDE_TOP_TABS: Array<{ id: GuideTopTab; label: string }> = [
|
||||||
{ id: 'guide', label: '馆内' },
|
{ id: 'guide', label: '馆内' },
|
||||||
@@ -10,7 +17,32 @@ export const isGuideTopTab = (value: unknown): value is GuideTopTab => {
|
|||||||
return value === 'guide' || value === 'explain'
|
return value === 'guide' || value === 'explain'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const guideTopTabUrl = (tab: GuideTopTab) => `/pages/index/index?tab=${tab}`
|
const getHostNavigationQueryParams = () => {
|
||||||
|
if (typeof window === 'undefined') return new URLSearchParams()
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(window.location.search)
|
||||||
|
const hash = window.location.hash || ''
|
||||||
|
const hashQueryStart = hash.indexOf('?')
|
||||||
|
if (hashQueryStart >= 0) {
|
||||||
|
const hashParams = new URLSearchParams(hash.slice(hashQueryStart + 1))
|
||||||
|
hashParams.forEach((value, key) => {
|
||||||
|
if (!searchParams.has(key)) searchParams.set(key, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostParams = new URLSearchParams()
|
||||||
|
HOST_NAVIGATION_QUERY_KEYS.forEach((key) => {
|
||||||
|
const value = searchParams.get(key)
|
||||||
|
if (value !== null) hostParams.set(key, value)
|
||||||
|
})
|
||||||
|
return hostParams
|
||||||
|
}
|
||||||
|
|
||||||
|
export const guideTopTabUrl = (tab: GuideTopTab) => {
|
||||||
|
const params = new URLSearchParams({ tab })
|
||||||
|
getHostNavigationQueryParams().forEach((value, key) => params.set(key, value))
|
||||||
|
return `/pages/index/index?${params.toString()}`
|
||||||
|
}
|
||||||
|
|
||||||
export const saveLastGuideLocationPreviewUrl = (url: string) => {
|
export const saveLastGuideLocationPreviewUrl = (url: string) => {
|
||||||
if (typeof window === 'undefined' || !url) return
|
if (typeof window === 'undefined' || !url) return
|
||||||
|
|||||||
129
tests/unit/ExplainListNavigation.spec.ts
Normal file
129
tests/unit/ExplainListNavigation.spec.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import ExplainListPage from '@/pages/explain/list.vue'
|
||||||
|
|
||||||
|
const hostEnvironmentMocks = vi.hoisted(() => ({
|
||||||
|
embeddedInWechatMiniProgram: false
|
||||||
|
}))
|
||||||
|
|
||||||
|
const testState = vi.hoisted(() => ({
|
||||||
|
onLoadHandler: null as null | (() => void)
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@dcloudio/uni-app', () => ({
|
||||||
|
onLoad: (handler: () => void) => {
|
||||||
|
testState.onLoadHandler = handler
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/utils/hostEnvironment', () => ({
|
||||||
|
isEmbeddedInWechatMiniProgram: () => hostEnvironmentMocks.embeddedInWechatMiniProgram
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/usecases/explainUseCase', () => ({
|
||||||
|
explainUseCase: {
|
||||||
|
loadExplainHalls: async () => [],
|
||||||
|
loadExplainHallSummaries: async () => ({})
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const GuidePageFrameStub = defineComponent({
|
||||||
|
name: 'GuidePageFrame',
|
||||||
|
template: '<main><slot /></main>'
|
||||||
|
})
|
||||||
|
|
||||||
|
const ExplainHallSelectStub = defineComponent({
|
||||||
|
name: 'ExplainHallSelect',
|
||||||
|
emits: ['back'],
|
||||||
|
template: '<button data-testid="explain-list-back" @click="$emit(\'back\')">返回</button>'
|
||||||
|
})
|
||||||
|
|
||||||
|
const mountExplainList = () => mount(ExplainListPage, {
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
GuidePageFrame: GuidePageFrameStub,
|
||||||
|
ExplainHallSelect: ExplainHallSelectStub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
hostEnvironmentMocks.embeddedInWechatMiniProgram = false
|
||||||
|
testState.onLoadHandler = null
|
||||||
|
window.location.hash = '#/pages/explain/list'
|
||||||
|
window.history.replaceState({}, '', window.location.href)
|
||||||
|
vi.stubGlobal('uni', {
|
||||||
|
navigateBack: vi.fn(),
|
||||||
|
redirectTo: vi.fn(),
|
||||||
|
reLaunch: vi.fn()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('讲解展厅列表返回', () => {
|
||||||
|
it('微信内嵌时不调用 navigateBack,并保留宿主标识返回首页讲解 Tab', async () => {
|
||||||
|
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||||||
|
window.location.hash = '#/pages/explain/list?embedded=wechat-mini-program&weapp=1&mini-program=1'
|
||||||
|
window.history.replaceState({}, '', window.location.href)
|
||||||
|
const wrapper = mountExplainList()
|
||||||
|
const pushState = vi.spyOn(window.history, 'pushState')
|
||||||
|
testState.onLoadHandler?.()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
|
||||||
|
|
||||||
|
expect(pushState).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ museumGuidePage: 'explain-list' }),
|
||||||
|
'',
|
||||||
|
window.location.href
|
||||||
|
)
|
||||||
|
expect(uni.navigateBack).not.toHaveBeenCalled()
|
||||||
|
expect(uni.reLaunch).toHaveBeenCalledTimes(1)
|
||||||
|
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('explain')
|
||||||
|
expect(query.get('embedded')).toBe('wechat-mini-program')
|
||||||
|
expect(query.get('weapp')).toBe('1')
|
||||||
|
expect(query.get('mini-program')).toBe('1')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('微信内嵌浏览器返回时回到首页讲解 Tab', async () => {
|
||||||
|
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||||||
|
window.location.hash = '#/pages/explain/list?embedded=wechat-mini-program&weapp=1'
|
||||||
|
window.history.replaceState({}, '', window.location.href)
|
||||||
|
const wrapper = mountExplainList()
|
||||||
|
testState.onLoadHandler?.()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }))
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(uni.navigateBack).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('explain')
|
||||||
|
expect(query.get('embedded')).toBe('wechat-mini-program')
|
||||||
|
expect(query.get('weapp')).toBe('1')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('普通 H5 保持原有 navigateBack 行为', async () => {
|
||||||
|
const wrapper = mountExplainList()
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
|
||||||
|
|
||||||
|
expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
|
||||||
|
expect(uni.reLaunch).not.toHaveBeenCalled()
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -10,9 +10,14 @@ const testState = vi.hoisted(() => ({
|
|||||||
searchUnmountCount: 0,
|
searchUnmountCount: 0,
|
||||||
searchRestoreCount: 0,
|
searchRestoreCount: 0,
|
||||||
searchCollapseAfterDetailCount: 0,
|
searchCollapseAfterDetailCount: 0,
|
||||||
|
searchResetCount: 0,
|
||||||
onShowHandler: null as null | (() => Promise<void> | void)
|
onShowHandler: null as null | (() => Promise<void> | void)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const hostEnvironmentMocks = vi.hoisted(() => ({
|
||||||
|
embeddedInWechatMiniProgram: false
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@dcloudio/uni-app', () => ({
|
vi.mock('@dcloudio/uni-app', () => ({
|
||||||
onLoad: vi.fn(),
|
onLoad: vi.fn(),
|
||||||
onShow: (handler: () => Promise<void> | void) => {
|
onShow: (handler: () => Promise<void> | void) => {
|
||||||
@@ -20,6 +25,10 @@ vi.mock('@dcloudio/uni-app', () => ({
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/utils/hostEnvironment', () => ({
|
||||||
|
isEmbeddedInWechatMiniProgram: () => hostEnvironmentMocks.embeddedInWechatMiniProgram
|
||||||
|
}))
|
||||||
|
|
||||||
const floors = [
|
const floors = [
|
||||||
{ id: 'L1', label: '1F', order: 1 },
|
{ id: 'L1', label: '1F', order: 1 },
|
||||||
{ id: 'L2', label: '2F', order: 2 }
|
{ id: 'L2', label: '2F', order: 2 }
|
||||||
@@ -100,7 +109,9 @@ const PoiSearchPanelStub = defineComponent({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
collapseHomePanel() {},
|
collapseHomePanel() {},
|
||||||
resetSearchState() {},
|
resetSearchState() {
|
||||||
|
testState.searchResetCount += 1
|
||||||
|
},
|
||||||
returnToCollapsedAfterDetail() {
|
returnToCollapsedAfterDetail() {
|
||||||
testState.searchCollapseAfterDetailCount += 1
|
testState.searchCollapseAfterDetailCount += 1
|
||||||
},
|
},
|
||||||
@@ -152,10 +163,12 @@ const mountIndex = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
hostEnvironmentMocks.embeddedInWechatMiniProgram = false
|
||||||
testState.searchMountCount = 0
|
testState.searchMountCount = 0
|
||||||
testState.searchUnmountCount = 0
|
testState.searchUnmountCount = 0
|
||||||
testState.searchRestoreCount = 0
|
testState.searchRestoreCount = 0
|
||||||
testState.searchCollapseAfterDetailCount = 0
|
testState.searchCollapseAfterDetailCount = 0
|
||||||
|
testState.searchResetCount = 0
|
||||||
testState.onShowHandler = null
|
testState.onShowHandler = null
|
||||||
window.history.replaceState({}, '', '/#/pages/index/index?tab=guide')
|
window.history.replaceState({}, '', '/#/pages/index/index?tab=guide')
|
||||||
vi.stubGlobal('uni', {
|
vi.stubGlobal('uni', {
|
||||||
@@ -168,10 +181,96 @@ beforeEach(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
vi.unstubAllGlobals()
|
vi.unstubAllGlobals()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('首页搜索与地图闭环', () => {
|
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 () => {
|
it('同步首页标签时保留内嵌宿主参数', async () => {
|
||||||
window.location.hash = '#/pages/index/index?tab=guide&embedded=wechat-mini-program'
|
window.location.hash = '#/pages/index/index?tab=guide&embedded=wechat-mini-program'
|
||||||
|
|
||||||
@@ -180,14 +279,26 @@ describe('首页搜索与地图闭环', () => {
|
|||||||
expect(window.location.hash).toBe('#/pages/index/index?tab=guide&embedded=wechat-mini-program')
|
expect(window.location.hash).toBe('#/pages/index/index?tab=guide&embedded=wechat-mini-program')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('首页暂不展示来馆入口及其到馆面板', async () => {
|
it('首页暂不展示来馆入口,导览快捷按钮文案固定且保留原切换行为', async () => {
|
||||||
const wrapper = await mountIndex()
|
const wrapper = await mountIndex()
|
||||||
|
const map = wrapper.getComponent(GuideMapShellStub)
|
||||||
|
const guideAction = wrapper.findAll('.guide-quick-action')[0]
|
||||||
|
|
||||||
expect(wrapper.text()).not.toContain('来馆')
|
expect(wrapper.text()).not.toContain('来馆')
|
||||||
expect(wrapper.findAll('.guide-quick-action').map((item) => item.text())).toEqual([
|
expect(wrapper.findAll('.guide-quick-action').map((item) => item.text())).toEqual([
|
||||||
'馆内',
|
'导览',
|
||||||
'讲解'
|
'讲解'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
await guideAction.trigger('tap')
|
||||||
|
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 () => {
|
it('向搜索面板传入当前楼层,并将分类结果 ID 精确传给地图', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user