Files
frontend-miniapp/tests/unit/ExplainListNavigation.spec.ts
lyf fc93b8d7d3
Some checks failed
CI / verify (push) Has been cancelled
修正讲解列表入口返回规则
2026-07-28 00:20:05 +08:00

306 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,
navigateBackToWechatMiniProgram: vi.fn(() => false)
}))
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,
navigateBackToWechatMiniProgram: () => hostEnvironmentMocks.navigateBackToWechatMiniProgram()
}))
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
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReset()
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReturnValue(false)
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 回到项目首页', 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()
wrapper.unmount()
})
it('小程序中的首页讲解 Tab 进入时仍优先回到项目首页', async () => {
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReturnValue(true)
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(hostEnvironmentMocks.navigateBackToWechatMiniProgram).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
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReturnValue(true)
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')
expect(uni.navigateBack).not.toHaveBeenCalled()
expect(hostEnvironmentMocks.navigateBackToWechatMiniProgram).toHaveBeenCalledTimes(1)
expect(uni.reLaunch).not.toHaveBeenCalled()
wrapper.unmount()
})
it('微信嵌入态从非讲解首页进入时返回小程序', async () => {
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReturnValue(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: 'guide' } },
{ route: 'pages/explain/list', options: {} }
]))
const wrapper = mountExplainList()
await wrapper.get('[data-testid="explain-list-back"]').trigger('click')
expect(uni.navigateBack).not.toHaveBeenCalled()
expect(hostEnvironmentMocks.navigateBackToWechatMiniProgram).toHaveBeenCalledTimes(1)
expect(uni.reLaunch).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 返回失败后重启到项目首页讲解 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('重启导览首页失败时显示明确提示', 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('小程序直达入口的浏览器返回会委托给小程序宿主', async () => {
hostEnvironmentMocks.embeddedInWechatMiniProgram = true
hostEnvironmentMocks.navigateBackToWechatMiniProgram.mockReturnValue(true)
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()
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }))
await flushPromises()
expect(uni.navigateBack).not.toHaveBeenCalled()
expect(pushState).toHaveBeenCalledTimes(1)
expect(hostEnvironmentMocks.navigateBackToWechatMiniProgram).toHaveBeenCalledTimes(1)
expect(uni.reLaunch).not.toHaveBeenCalled()
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()
})
})