Files
frontend-miniapp/tests/unit/ExplainListNavigation.spec.ts
lyf d94d689b40
Some checks failed
CI / verify (push) Has been cancelled
修复讲解列表返回分流
2026-07-25 02:28:38 +08:00

316 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @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 miniProgramBridgeMocks = vi.hoisted(() => ({
returnToWechatMiniProgram: vi.fn()
}))
const testState = vi.hoisted(() => ({
onLoadHandler: null as null | (() => void),
onBackPressHandler: null as null | (() => boolean)
}))
vi.mock('@dcloudio/uni-app', () => ({
onLoad: (handler: () => void) => {
testState.onLoadHandler = handler
},
onBackPress: (handler: () => boolean) => {
testState.onBackPressHandler = handler
}
}))
vi.mock('@/utils/hostEnvironment', () => ({
isEmbeddedInWechatMiniProgram: () => hostEnvironmentMocks.embeddedInWechatMiniProgram
}))
vi.mock('@/services/WechatMiniProgramBridgeService', () => ({
returnToWechatMiniProgram: miniProgramBridgeMocks.returnToWechatMiniProgram
}))
vi.mock('@/usecases/explainUseCase', () => ({
explainUseCase: {
loadExplainHalls: async () => [],
loadExplainHallSummaries: async () => ({})
}
}))
const GuidePageFrameStub = defineComponent({
name: 'GuidePageFrame',
emits: ['back'],
template: '<main><button data-testid="frame-back" @click="$emit(\'back\')">顶部返回</button><slot /></main>'
})
const ExplainHallSelectStub = defineComponent({
name: 'ExplainHallSelect',
props: {
forceInternalHeader: Boolean
},
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
miniProgramBridgeMocks.returnToWechatMiniProgram.mockResolvedValue({ status: 'not-embedded' })
testState.onLoadHandler = null
testState.onBackPressHandler = null
window.location.hash = '#/pages/explain/list'
window.history.replaceState({}, '', window.location.href)
vi.stubGlobal('uni', {
navigateBack: vi.fn(),
reLaunch: vi.fn(),
showToast: vi.fn()
})
vi.stubGlobal('getCurrentPages', vi.fn(() => []))
})
afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
describe('讲解展厅列表返回', () => {
it('首页讲解 Tab 进入时,顶部返回使用 navigateBack 保留讲解 Tab', async () => {
vi.stubGlobal('getCurrentPages', vi.fn(() => [
{ route: 'pages/index/index', options: { tab: 'explain' } },
{ route: 'pages/explain/list', options: {} }
]))
const wrapper = mountExplainList()
testState.onLoadHandler?.()
await flushPromises()
await wrapper.get('[data-testid="frame-back"]').trigger('click')
expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
expect(uni.reLaunch).not.toHaveBeenCalled()
expect(miniProgramBridgeMocks.returnToWechatMiniProgram).not.toHaveBeenCalled()
wrapper.unmount()
})
it('页面栈只有展厅列表时使用 reLaunch 回到导览首页', async () => {
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('微信嵌入态直达页面由宿主提供标题与返回,并返回小程序', async () => {
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
miniProgramBridgeMocks.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(uni.reLaunch).not.toHaveBeenCalled()
expect(miniProgramBridgeMocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
wrapper.unmount()
})
it('微信嵌入态从本项目首页进入时仍返回本项目首页', async () => {
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
window.location.hash = '#/pages/explain/list?embedded=wechat-mini-program'
window.history.replaceState({}, '', window.location.href)
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(uni.reLaunch).not.toHaveBeenCalled()
expect(miniProgramBridgeMocks.returnToWechatMiniProgram).not.toHaveBeenCalled()
wrapper.unmount()
})
it('上一页不是首页时使用 reLaunch避免回到不相关页面', async () => {
vi.stubGlobal('getCurrentPages', vi.fn(() => [
{ route: 'pages/search/index', options: {} },
{ 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('首页返回失败时重启到原讲解 Tab', async () => {
vi.stubGlobal('getCurrentPages', vi.fn(() => [
{ route: 'pages/index/index', options: { tab: 'explain' } },
{ route: 'pages/explain/list', options: {} }
]))
vi.stubGlobal('uni', {
navigateBack: vi.fn((options: { fail?: () => void }) => options.fail?.()),
reLaunch: vi.fn(),
showToast: vi.fn()
})
const wrapper = mountExplainList()
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({
url: '/pages/index/index?tab=explain'
}))
wrapper.unmount()
})
it('导览首页返回失败后重启到原导览 Tab', 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(),
showToast: vi.fn()
})
const wrapper = mountExplainList()
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({
url: '/pages/index/index?tab=guide'
}))
wrapper.unmount()
})
it('重启导览首页失败时显示明确提示', async () => {
vi.stubGlobal('uni', {
navigateBack: vi.fn(),
reLaunch: vi.fn((options: { fail?: () => void }) => options.fail?.()),
showToast: vi.fn()
})
const wrapper = mountExplainList()
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
expect(uni.showToast).toHaveBeenCalledWith({
title: '返回导览首页失败,请重试',
icon: 'none'
})
wrapper.unmount()
})
it('微信嵌入态根栈不创建 H5 history 标记,平台返回交给宿主', async () => {
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
miniProgramBridgeMocks.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()
const pushState = vi.spyOn(window.history, 'pushState')
testState.onLoadHandler?.()
testState.onLoadHandler?.()
await flushPromises()
expect(pushState).not.toHaveBeenCalled()
expect(uni.navigateBack).not.toHaveBeenCalled()
expect(uni.reLaunch).not.toHaveBeenCalled()
expect(testState.onBackPressHandler?.()).toBe(true)
await flushPromises()
expect(miniProgramBridgeMocks.returnToWechatMiniProgram).toHaveBeenCalledTimes(1)
wrapper.unmount()
})
it('根栈页内返回先消费历史标记,再回到导览首页', async () => {
let currentHistoryState: Record<string, unknown> = {}
vi.spyOn(window.history, 'state', 'get').mockImplementation(() => currentHistoryState)
vi.spyOn(window.history, 'pushState').mockImplementation((state) => {
currentHistoryState = (state || {}) as Record<string, unknown>
})
const wrapper = mountExplainList()
const historyBack = vi.spyOn(window.history, 'back').mockImplementation(() => {
currentHistoryState = {}
window.dispatchEvent(new PopStateEvent('popstate', { state: currentHistoryState }))
})
testState.onLoadHandler?.()
expect(window.history.state).toMatchObject({ museumGuidePage: 'explain-list' })
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
await flushPromises()
expect(historyBack).toHaveBeenCalledTimes(1)
expect(uni.reLaunch).toHaveBeenCalledTimes(1)
expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({
url: '/pages/index/index?tab=guide'
}))
wrapper.unmount()
})
it('普通 H5 浏览器返回也会回到导览首页,且不调用 history.back', async () => {
const historyBack = vi.spyOn(window.history, 'back')
const wrapper = mountExplainList()
testState.onLoadHandler?.()
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }))
await flushPromises()
expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({
url: '/pages/index/index?tab=guide'
}))
expect(historyBack).not.toHaveBeenCalled()
wrapper.unmount()
})
it('正常内部页面栈不创建历史标记,并保留框架原有浏览器返回', async () => {
vi.stubGlobal('getCurrentPages', vi.fn(() => [
{ route: 'pages/index/index', options: { tab: 'guide' } },
{ route: 'pages/explain/list', options: {} }
]))
const wrapper = mountExplainList()
const pushState = vi.spyOn(window.history, 'pushState')
testState.onLoadHandler?.()
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }))
await flushPromises()
expect(pushState).not.toHaveBeenCalled()
expect(uni.reLaunch).not.toHaveBeenCalled()
wrapper.unmount()
})
it('平台返回事件复用同一返回策略并阻止默认退出', () => {
const wrapper = mountExplainList()
expect(testState.onBackPressHandler?.()).toBe(true)
expect(uni.reLaunch).toHaveBeenCalledWith(expect.objectContaining({
url: '/pages/index/index?tab=guide'
}))
wrapper.unmount()
})
})