@@ -159,7 +159,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { onHide, onLoad, onUnload } from '@dcloudio/uni-app'
|
||||
import { onBackPress, onHide, onLoad, onUnload } from '@dcloudio/uni-app'
|
||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import GuideFeedbackState from '@/components/navigation/GuideFeedbackState.vue'
|
||||
import GuideLoadingState from '@/components/navigation/GuideLoadingState.vue'
|
||||
@@ -188,6 +188,7 @@ import {
|
||||
} from '@/utils/placeholders'
|
||||
import { normalizeSameOriginPublicUrl } from '@/utils/publicUrl'
|
||||
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
|
||||
import { returnToWechatMiniProgram } from '@/services/WechatMiniProgramBridgeService'
|
||||
import { normalizeGuideAudioLanguage } from '@/data/adapters/guideStopInfoAdapter'
|
||||
import { explainGuideStopListUrl } from '@/utils/explainNavigation'
|
||||
import {
|
||||
@@ -355,6 +356,7 @@ const isCurrentDetailAudioTarget = computed(() => {
|
||||
)
|
||||
})
|
||||
let detailAudioClosedOnExit = false
|
||||
let returningToMiniProgram = false
|
||||
|
||||
const closeDetailAudioOnExit = () => {
|
||||
if (detailAudioClosedOnExit || !isCurrentDetailAudioTarget.value) return
|
||||
@@ -945,15 +947,42 @@ const fallbackToExplainObjectList = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const returnToExplainObjectList = () => {
|
||||
const cameFromExplainObjectList = () => {
|
||||
const pages = getCurrentPages()
|
||||
const previousPage = pages[pages.length - 2] as { route?: string } | undefined
|
||||
return previousPage?.route === 'pages/explain/guide-stop-list'
|
||||
}
|
||||
|
||||
const returnToExplainObjectList = async () => {
|
||||
closeDetailAudioOnExit()
|
||||
if (cameFromExplainObjectList()) {
|
||||
uni.navigateBack({
|
||||
delta: 1,
|
||||
fail: fallbackToExplainObjectList
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (isEmbeddedInWechatMiniProgram()) {
|
||||
if (returningToMiniProgram) return
|
||||
returningToMiniProgram = true
|
||||
const result = await returnToWechatMiniProgram()
|
||||
if (result.status === 'returned') return
|
||||
|
||||
returningToMiniProgram = false
|
||||
uni.showToast({ title: '返回小程序失败,请重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
fallbackToExplainObjectList()
|
||||
}
|
||||
|
||||
const handleBack = returnToExplainObjectList
|
||||
const handleBack = () => { void returnToExplainObjectList() }
|
||||
|
||||
onBackPress(() => {
|
||||
void returnToExplainObjectList()
|
||||
return true
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -20,13 +20,15 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { onBackPress, onLoad } from '@dcloudio/uni-app'
|
||||
import GuidePageFrame from '@/components/navigation/GuidePageFrame.vue'
|
||||
import ExplainGuideStopCatalog, { type ExplainGuideStopCatalogItem } from '@/components/explain/ExplainGuideStopCatalog.vue'
|
||||
import { explainUseCase } from '@/usecases/explainUseCase'
|
||||
import type { ExplainGuideStop } from '@/domain/museum'
|
||||
import { normalizeExplainDetailTargetFromGuideStop } from '@/domain/explainDetailTarget'
|
||||
import { navigateToGuideTopTab, type GuideTopTab } from '@/utils/guideTopTabs'
|
||||
import { isEmbeddedInWechatMiniProgram } from '@/utils/hostEnvironment'
|
||||
import { returnToWechatMiniProgram } from '@/services/WechatMiniProgramBridgeService'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
const selectedExplainHallId = ref('')
|
||||
@@ -40,6 +42,7 @@ const hasMore = ref(false)
|
||||
const nextPageNo = ref(1)
|
||||
let requestVersion = 0
|
||||
let disposed = false
|
||||
let returningToMiniProgram = false
|
||||
|
||||
const toGuideStopItems = (stops: ExplainGuideStop[]): ExplainGuideStopCatalogItem[] => stops.map((stop) => ({
|
||||
id: stop.id,
|
||||
@@ -153,7 +156,7 @@ type ExplainPageStackEntry = {
|
||||
}
|
||||
}
|
||||
|
||||
const returnToHallList = () => {
|
||||
const returnToHallList = async () => {
|
||||
const pages = getCurrentPages()
|
||||
const previousPage = pages[pages.length - 2] as ExplainPageStackEntry | undefined
|
||||
const previousRoute = previousPage?.route
|
||||
@@ -170,11 +173,27 @@ const returnToHallList = () => {
|
||||
return
|
||||
}
|
||||
|
||||
if (isEmbeddedInWechatMiniProgram()) {
|
||||
if (returningToMiniProgram) return
|
||||
returningToMiniProgram = true
|
||||
const result = await returnToWechatMiniProgram()
|
||||
if (result.status === 'returned') return
|
||||
|
||||
returningToMiniProgram = false
|
||||
uni.showToast({ title: '返回小程序失败,请重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
fallbackToHallList()
|
||||
}
|
||||
|
||||
const handleBack = returnToHallList
|
||||
const handleBack = () => { void returnToHallList() }
|
||||
const handleTopTabChange = (tab: GuideTopTab) => navigateToGuideTopTab(tab)
|
||||
|
||||
onBackPress(() => {
|
||||
void returnToHallList()
|
||||
return true
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -39,9 +39,9 @@ import {
|
||||
type GuideTopTab
|
||||
} from '@/utils/guideTopTabs'
|
||||
import {
|
||||
isEmbeddedInWechatMiniProgram,
|
||||
navigateBackToWechatMiniProgram
|
||||
isEmbeddedInWechatMiniProgram
|
||||
} from '@/utils/hostEnvironment'
|
||||
import { returnToWechatMiniProgram } from '@/services/WechatMiniProgramBridgeService'
|
||||
import { syncHostNavigationTitle } from '@/utils/hostNavigationTitle'
|
||||
import { explainGuideStopListUrl } from '@/utils/explainNavigation'
|
||||
|
||||
@@ -162,9 +162,10 @@ const returnToProjectExplainHome = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const returnFromStandaloneMiniProgramEntry = () => {
|
||||
const returnFromStandaloneMiniProgramEntry = async () => {
|
||||
isReturningFromExplainList = true
|
||||
if (navigateBackToWechatMiniProgram()) return
|
||||
const result = await returnToWechatMiniProgram()
|
||||
if (result.status === 'returned') return
|
||||
|
||||
isReturningFromExplainList = false
|
||||
uni.showToast({
|
||||
@@ -184,7 +185,7 @@ const returnToGuideHome = () => {
|
||||
}
|
||||
|
||||
if (shouldUseHostNavigation.value) {
|
||||
returnFromStandaloneMiniProgramEntry()
|
||||
void returnFromStandaloneMiniProgramEntry()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
interface MiniProgramBridge {
|
||||
getEnv?: (callback: (result: { miniprogram?: boolean }) => void) => void
|
||||
navigateTo?: (options: { url: string; success?: () => void; fail?: (error: { errMsg?: string }) => void }) => void
|
||||
navigateBack?: (options: { delta: number; success?: () => void; fail?: (error: { errMsg?: string }) => void }) => void
|
||||
postMessage?: (options: { data: { type: string } }) => void
|
||||
}
|
||||
|
||||
export type WechatMiniProgramBridgeResult =
|
||||
@@ -15,13 +17,20 @@ export type WechatMiniProgramBridgeResult =
|
||||
| { status: 'not-embedded' }
|
||||
| { status: 'failed'; reason: 'invalid-target' | 'bridge-unavailable' | 'not-in-mini-program' | 'navigation-failed' | 'page-not-found' | 'timeout' }
|
||||
|
||||
export type WechatMiniProgramReturnResult =
|
||||
| { status: 'returned' }
|
||||
| { status: 'not-embedded' }
|
||||
| { status: 'failed'; reason: 'bridge-unavailable' | 'not-in-mini-program' | 'navigation-failed' | 'timeout' }
|
||||
|
||||
const SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.3.2.js'
|
||||
export const MINI_PROGRAM_RETURN_MESSAGE = 'museum-guide:return'
|
||||
const BRIDGE_WAIT_TIMEOUT = 2500
|
||||
const BRIDGE_POLL_INTERVAL = 50
|
||||
const GET_ENV_TIMEOUT = 1500
|
||||
const NAVIGATION_TIMEOUT = 3000
|
||||
|
||||
let pendingNavigation: Promise<WechatMiniProgramBridgeResult> | null = null
|
||||
let pendingReturn: Promise<WechatMiniProgramReturnResult> | null = null
|
||||
let sdkLoadPromise: Promise<void> | null = null
|
||||
|
||||
const getBridge = (): MiniProgramBridge | null => {
|
||||
@@ -108,6 +117,45 @@ const navigateToHostLocationPage = (bridge: MiniProgramBridge, target: WechatOpe
|
||||
}
|
||||
})
|
||||
|
||||
const requestHostReturnFallback = (bridge: MiniProgramBridge) => {
|
||||
if (!bridge.postMessage) return false
|
||||
|
||||
try {
|
||||
bridge.postMessage({ data: { type: MINI_PROGRAM_RETURN_MESSAGE } })
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('请求宿主小程序安全返回失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const navigateBackToHost = (bridge: MiniProgramBridge) => new Promise<WechatMiniProgramReturnResult>((resolve) => {
|
||||
if (!bridge.navigateBack) return resolve({ status: 'failed', reason: 'bridge-unavailable' })
|
||||
|
||||
let settled = false
|
||||
const finish = (result: WechatMiniProgramReturnResult) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
clearTimeout(timeout)
|
||||
resolve(result)
|
||||
}
|
||||
const timeout = setTimeout(() => finish({ status: 'failed', reason: 'timeout' }), NAVIGATION_TIMEOUT)
|
||||
|
||||
try {
|
||||
bridge.navigateBack({
|
||||
delta: 1,
|
||||
success: () => finish({ status: 'returned' }),
|
||||
fail: (error) => {
|
||||
console.error('返回宿主小程序失败:', error)
|
||||
finish({ status: 'failed', reason: 'navigation-failed' })
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('调用宿主小程序返回桥接失败:', error)
|
||||
finish({ status: 'failed', reason: 'navigation-failed' })
|
||||
}
|
||||
})
|
||||
|
||||
const openOnce = async (target: WechatOpenLocationTarget): Promise<WechatMiniProgramBridgeResult> => {
|
||||
const validation = validateWechatOpenLocationTarget(target)
|
||||
if (!validation.ok) return { status: 'failed', reason: 'invalid-target' }
|
||||
@@ -123,3 +171,24 @@ export const openWechatMiniProgramLocation = (target: WechatOpenLocationTarget)
|
||||
pendingNavigation = openOnce(target).finally(() => { pendingNavigation = null })
|
||||
return pendingNavigation
|
||||
}
|
||||
|
||||
const returnOnce = async (): Promise<WechatMiniProgramReturnResult> => {
|
||||
if (!isEmbeddedInWechatMiniProgram()) return { status: 'not-embedded' }
|
||||
|
||||
const bridge = await waitForBridge()
|
||||
if (!bridge) return { status: 'failed', reason: 'bridge-unavailable' }
|
||||
if (!await isMiniProgramWebView(bridge)) return { status: 'failed', reason: 'not-in-mini-program' }
|
||||
|
||||
const result = await navigateBackToHost(bridge)
|
||||
if (result.status === 'returned') return result
|
||||
|
||||
// 宿主页可能由分享或冷启动直接打开,页面栈没有上一页时改由宿主执行安全回退。
|
||||
if (requestHostReturnFallback(bridge)) return { status: 'returned' }
|
||||
return result
|
||||
}
|
||||
|
||||
export const returnToWechatMiniProgram = () => {
|
||||
if (pendingReturn) return pendingReturn
|
||||
pendingReturn = returnOnce().finally(() => { pendingReturn = null })
|
||||
return pendingReturn
|
||||
}
|
||||
|
||||
@@ -43,28 +43,6 @@ export const isEmbeddedInWechatMiniProgram = () => {
|
||||
return /miniProgram/i.test(window.navigator?.userAgent || '')
|
||||
}
|
||||
|
||||
type WechatMiniProgramNavigationBridge = {
|
||||
navigateBack?: (options: { delta: number }) => void
|
||||
}
|
||||
|
||||
export const navigateBackToWechatMiniProgram = () => {
|
||||
if (!isEmbeddedInWechatMiniProgram() || typeof window === 'undefined') return false
|
||||
|
||||
const bridge = (window as Window & {
|
||||
wx?: { miniProgram?: WechatMiniProgramNavigationBridge }
|
||||
}).wx?.miniProgram
|
||||
|
||||
if (typeof bridge?.navigateBack !== 'function') return false
|
||||
|
||||
try {
|
||||
bridge.navigateBack({ delta: 1 })
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('返回小程序失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const isWechatMiniProgramRuntime = () => (
|
||||
isNativeWechatMiniProgram() || isEmbeddedInWechatMiniProgram()
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ 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(),
|
||||
@@ -17,7 +18,9 @@ const mocks = vi.hoisted(() => ({
|
||||
close: vi.fn(),
|
||||
seekToPercent: vi.fn(),
|
||||
setPlaybackRate: vi.fn(),
|
||||
toggleMute: vi.fn()
|
||||
toggleMute: vi.fn(),
|
||||
embeddedInWechatMiniProgram: false,
|
||||
returnToWechatMiniProgram: vi.fn(() => Promise.resolve({ status: 'failed' as const, reason: 'bridge-unavailable' as const }))
|
||||
}))
|
||||
|
||||
const audioState = vi.hoisted(() => ({
|
||||
@@ -36,6 +39,9 @@ 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
|
||||
},
|
||||
@@ -74,7 +80,11 @@ vi.mock('@/composables/useGlobalAudioPlayer', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/hostEnvironment', () => ({
|
||||
isEmbeddedInWechatMiniProgram: () => false
|
||||
isEmbeddedInWechatMiniProgram: () => mocks.embeddedInWechatMiniProgram
|
||||
}))
|
||||
|
||||
vi.mock('@/services/WechatMiniProgramBridgeService', () => ({
|
||||
returnToWechatMiniProgram: () => mocks.returnToWechatMiniProgram()
|
||||
}))
|
||||
|
||||
import ExhibitDetail from '@/pages/exhibit/detail.vue'
|
||||
@@ -106,6 +116,7 @@ const exhibit = {
|
||||
describe('讲解详情音频优先布局', () => {
|
||||
beforeEach(() => {
|
||||
mocks.onLoadHandler = null
|
||||
mocks.onBackPressHandler = null
|
||||
mocks.onHideHandler = null
|
||||
mocks.onUnloadHandler = null
|
||||
audioState.currentAudio.value = null
|
||||
@@ -131,6 +142,9 @@ 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(),
|
||||
@@ -375,6 +389,28 @@ 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: '恐龙厅'
|
||||
})
|
||||
wrapper.getComponent(GuidePageFrameStub).vm.$emit('back')
|
||||
await flushPromises()
|
||||
|
||||
expect(mocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
|
||||
expect(uni.navigateBack).not.toHaveBeenCalled()
|
||||
expect(uni.redirectTo).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('离开详情页时立即关闭当前讲解播放器', async () => {
|
||||
audioState.currentAudio.value = { id: 'audio-1', audioUrl: exhibit.audioUrl }
|
||||
audioState.currentSource.value = { targetType: 'STOP', targetId: 'stop-1', lang: 'en-US' }
|
||||
@@ -407,9 +443,6 @@ describe('讲解详情音频优先布局', () => {
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
vi.mocked(uni.navigateBack).mockImplementationOnce((options: any) => {
|
||||
options.fail?.()
|
||||
})
|
||||
wrapper.getComponent(GuidePageFrameStub).vm.$emit('back')
|
||||
await flushPromises()
|
||||
|
||||
@@ -418,6 +451,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).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
|
||||
expect(uni.navigateBack).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,15 +7,35 @@ 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)
|
||||
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 }))
|
||||
}))
|
||||
|
||||
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({
|
||||
@@ -53,6 +73,10 @@ 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(),
|
||||
@@ -155,6 +179,19 @@ 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('页面栈返回失败时回退至独立展厅列表', async () => {
|
||||
vi.stubGlobal('getCurrentPages', vi.fn(() => [
|
||||
{ route: 'pages/explain/list', options: {} },
|
||||
|
||||
@@ -6,8 +6,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import ExplainListPage from '@/pages/explain/list.vue'
|
||||
|
||||
const hostEnvironmentMocks = vi.hoisted(() => ({
|
||||
embeddedInWechatMiniProgram: false,
|
||||
navigateBackToWechatMiniProgram: vi.fn(() => false)
|
||||
embeddedInWechatMiniProgram: false
|
||||
}))
|
||||
|
||||
const miniProgramReturnMocks = vi.hoisted(() => ({
|
||||
returnToWechatMiniProgram: vi.fn(() => Promise.resolve({ status: 'failed' as const, reason: 'bridge-unavailable' as const }))
|
||||
}))
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
@@ -25,8 +28,11 @@ vi.mock('@dcloudio/uni-app', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/hostEnvironment', () => ({
|
||||
isEmbeddedInWechatMiniProgram: () => hostEnvironmentMocks.embeddedInWechatMiniProgram,
|
||||
navigateBackToWechatMiniProgram: () => hostEnvironmentMocks.navigateBackToWechatMiniProgram()
|
||||
isEmbeddedInWechatMiniProgram: () => hostEnvironmentMocks.embeddedInWechatMiniProgram
|
||||
}))
|
||||
|
||||
vi.mock('@/services/WechatMiniProgramBridgeService', () => ({
|
||||
returnToWechatMiniProgram: () => miniProgramReturnMocks.returnToWechatMiniProgram()
|
||||
}))
|
||||
|
||||
vi.mock('@/usecases/explainUseCase', () => ({
|
||||
@@ -62,8 +68,8 @@ const mountExplainList = () => mount(ExplainListPage, {
|
||||
|
||||
beforeEach(() => {
|
||||
hostEnvironmentMocks.embeddedInWechatMiniProgram = false
|
||||
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReset()
|
||||
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReturnValue(false)
|
||||
miniProgramReturnMocks.returnToWechatMiniProgram.mockReset()
|
||||
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'failed', reason: 'bridge-unavailable' })
|
||||
testState.onLoadHandler = null
|
||||
testState.onBackPressHandler = null
|
||||
window.location.hash = '#/pages/explain/list'
|
||||
@@ -100,7 +106,7 @@ describe('讲解展厅列表返回', () => {
|
||||
|
||||
it('小程序中的首页讲解 Tab 进入时仍优先回到项目首页', async () => {
|
||||
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||||
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReturnValue(true)
|
||||
miniProgramReturnMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'returned' })
|
||||
vi.stubGlobal('getCurrentPages', vi.fn(() => [
|
||||
{ route: 'pages/index/index', options: { tab: 'explain' } },
|
||||
{ route: 'pages/explain/list', options: {} }
|
||||
@@ -110,7 +116,7 @@ describe('讲解展厅列表返回', () => {
|
||||
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
|
||||
|
||||
expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
|
||||
expect(hostEnvironmentMocks.navigateBackToWechatMiniProgram).not.toHaveBeenCalled()
|
||||
expect(miniProgramReturnMocks.returnToWechatMiniProgram).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
@@ -128,23 +134,24 @@ describe('讲解展厅列表返回', () => {
|
||||
|
||||
it('小程序直达独立入口时返回小程序,而不是重启项目首页', async () => {
|
||||
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||||
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReturnValue(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(hostEnvironmentMocks.navigateBackToWechatMiniProgram).toHaveBeenCalledTimes(1)
|
||||
expect(miniProgramReturnMocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
|
||||
expect(uni.reLaunch).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('微信嵌入态从非讲解首页进入时返回小程序', async () => {
|
||||
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||||
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReturnValue(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(() => [
|
||||
@@ -154,9 +161,10 @@ describe('讲解展厅列表返回', () => {
|
||||
const wrapper = mountExplainList()
|
||||
|
||||
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(uni.navigateBack).not.toHaveBeenCalled()
|
||||
expect(hostEnvironmentMocks.navigateBackToWechatMiniProgram).toHaveBeenCalledTimes(1)
|
||||
expect(miniProgramReturnMocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
|
||||
expect(uni.reLaunch).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
@@ -216,7 +224,7 @@ describe('讲解展厅列表返回', () => {
|
||||
|
||||
it('小程序直达入口的浏览器返回会委托给小程序宿主', async () => {
|
||||
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
|
||||
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReturnValue(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()
|
||||
@@ -230,7 +238,7 @@ describe('讲解展厅列表返回', () => {
|
||||
|
||||
expect(uni.navigateBack).not.toHaveBeenCalled()
|
||||
expect(pushState).toHaveBeenCalledTimes(1)
|
||||
expect(hostEnvironmentMocks.navigateBackToWechatMiniProgram).toHaveBeenCalledTimes(1)
|
||||
expect(miniProgramReturnMocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
|
||||
expect(uni.reLaunch).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
@@ -4,6 +4,11 @@ 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()
|
||||
@@ -33,6 +38,7 @@ describe('微信小程序 web-view 桥接', () => {
|
||||
|
||||
it('等待延迟注入的 bridge 后导航到编码后的宿主页', async () => {
|
||||
setEmbeddedRuntime()
|
||||
addWechatSdkMarker()
|
||||
const { openWechatMiniProgramLocation } = await loadService()
|
||||
const result = openWechatMiniProgramLocation(target)
|
||||
await vi.advanceTimersByTimeAsync(100)
|
||||
@@ -84,4 +90,45 @@ 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,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
isEmbeddedInWechatMiniProgram,
|
||||
navigateBackToWechatMiniProgram
|
||||
isEmbeddedInWechatMiniProgram
|
||||
} from '@/utils/hostEnvironment'
|
||||
|
||||
const createWindow = (overrides: Record<string, unknown> = {}) => ({
|
||||
@@ -50,24 +49,3 @@ describe('微信小程序宿主环境识别', () => {
|
||||
expect(isEmbeddedInWechatMiniProgram()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('小程序宿主返回', () => {
|
||||
it('嵌入态调用小程序 navigateBack', () => {
|
||||
const navigateBack = vi.fn()
|
||||
vi.stubGlobal('window', createWindow({
|
||||
__wxjs_environment: 'miniprogram',
|
||||
wx: { miniProgram: { navigateBack } }
|
||||
}))
|
||||
|
||||
expect(navigateBackToWechatMiniProgram()).toBe(true)
|
||||
expect(navigateBack).toHaveBeenCalledWith({ delta: 1 })
|
||||
})
|
||||
|
||||
it('非嵌入态不会调用小程序返回桥接', () => {
|
||||
const navigateBack = vi.fn()
|
||||
vi.stubGlobal('window', createWindow({ wx: { miniProgram: { navigateBack } } }))
|
||||
|
||||
expect(navigateBackToWechatMiniProgram()).toBe(false)
|
||||
expect(navigateBack).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user