Files
frontend-miniapp/tests/unit/ExplainGuideStopListNavigation.spec.ts
lyf 06ad609e1f
Some checks failed
CI / verify (push) Has been cancelled
修复小程序讲解内部返回控件
2026-07-28 14:42:02 +08:00

236 lines
9.5 KiB
TypeScript

// @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 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 }))
}))
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({
items: [], total: 0, pageNo: 1, pageSize: 20, hasMore: false
})
}
}))
const GuidePageFrameStub = defineComponent({
name: 'GuidePageFrame',
emits: ['back'],
template: '<main><button data-testid="frame-back" @click="$emit(\'back\')">返回</button><slot /></main>'
})
const ExplainGuideStopCatalogStub = defineComponent({
name: 'ExplainGuideStopCatalog',
props: ['guideStops', 'selectedHallName', 'showEmbeddedBack', 'loading', 'loadingMore', 'hasMore', 'error', 'loadMoreError'],
emits: ['back', 'retry', 'retryMore', 'guideStopClick'],
setup(_, { emit }) {
return {
emitDetail: () => emit('guideStopClick', { id: 'stop-1', name: '对象', playTargetType: 'STOP', playTargetId: 'target-1' })
}
},
template: '<button data-testid="list-back" @click="$emit(\'back\')">返回</button><button data-testid="retry-more" @click="$emit(\'retryMore\')">重试更多</button><button data-testid="detail-click" @click="emitDetail">详情</button>'
})
const mountGuideStopList = () => mount(ExplainGuideStopListPage, {
global: {
stubs: {
GuidePageFrame: GuidePageFrameStub,
ExplainGuideStopCatalog: ExplainGuideStopCatalogStub
}
}
})
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(),
navigateTo: vi.fn(),
setNavigationBarTitle: vi.fn()
})
vi.stubGlobal('getCurrentPages', vi.fn(() => []))
vi.mocked(explainUseCase.listGuideStopsPageByHall).mockReset()
vi.mocked(explainUseCase.listGuideStopsPageByHall).mockResolvedValue({ items: [], total: 0, pageNo: 1, pageSize: 20, hasMore: false })
})
afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
describe('讲解对象列表返回', () => {
it('ignores a delayed first-page response after unmount', async () => {
let resolvePage: ((page: { items: Array<{ id: string, name: string }>, total: number, pageNo: number, pageSize: number, hasMore: boolean }) => void) | undefined
vi.mocked(explainUseCase.listGuideStopsPageByHall).mockImplementationOnce(() => new Promise((resolve) => { resolvePage = resolve }))
const wrapper = mountGuideStopList()
testState.onLoadHandler?.({ hallId: 'hall-1' })
wrapper.unmount()
resolvePage?.({ items: [{ id: 'late', name: '延迟' }], total: 1, pageNo: 1, pageSize: 20, hasMore: false })
await flushPromises()
expect(explainUseCase.listGuideStopsPageByHall).toHaveBeenCalledTimes(1)
})
it('permits retry-more recovery after a failed page', async () => {
vi.mocked(explainUseCase.listGuideStopsPageByHall)
.mockResolvedValueOnce({ items: [{ id: 'one', name: '一号' }], total: 2, pageNo: 1, pageSize: 20, hasMore: true })
.mockRejectedValueOnce(new Error('temporary'))
.mockResolvedValueOnce({ items: [{ id: 'two', name: '二号' }], total: 2, pageNo: 2, pageSize: 20, hasMore: false })
const wrapper = mountGuideStopList()
testState.onLoadHandler?.({ hallId: 'hall-1' })
await flushPromises()
await wrapper.get('[data-testid="retry-more"]').trigger('click')
await flushPromises()
expect(explainUseCase.listGuideStopsPageByHall).toHaveBeenCalledTimes(2)
await wrapper.get('[data-testid="retry-more"]').trigger('click')
await flushPromises()
expect(explainUseCase.listGuideStopsPageByHall).toHaveBeenCalledTimes(3)
wrapper.unmount()
})
it('keeps the selected hall context when an object payload omits hall fields', async () => {
const wrapper = mountGuideStopList()
testState.onLoadHandler?.({ hallId: 'hall-1', hallName: '恐龙厅' })
await flushPromises()
await wrapper.get('[data-testid="detail-click"]').trigger('click')
const url = vi.mocked(uni.navigateTo).mock.calls[0]?.[0]?.url || ''
const params = new URLSearchParams(url.split('?')[1])
expect(url.split('?')[0]).toBe('/pages/exhibit/detail')
expect(params.get('hallId')).toBe('hall-1')
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()
})
it('上一页是独立展厅列表时回到该列表', async () => {
vi.stubGlobal('getCurrentPages', vi.fn(() => [
{ route: 'pages/explain/list', options: {} },
{ route: 'pages/explain/guide-stop-list', options: {} }
]))
const wrapper = mountGuideStopList()
testState.onLoadHandler?.({ hallId: 'hall-1', hallName: '恐龙厅' })
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 () => {
vi.stubGlobal('getCurrentPages', vi.fn(() => [
{ route: 'pages/index/index', options: { tab: 'explain' } },
{ route: 'pages/explain/guide-stop-list', options: {} }
]))
const wrapper = mountGuideStopList()
await wrapper.get('[data-testid="list-back"]').trigger('click')
expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
expect(uni.reLaunch).not.toHaveBeenCalled()
wrapper.unmount()
})
it('深链或不相关上一页直接回退至独立展厅列表', async () => {
const wrapper = mountGuideStopList()
await wrapper.get('[data-testid="list-back"]').trigger('click')
expect(uni.navigateBack).not.toHaveBeenCalled()
expect(uni.reLaunch).toHaveBeenCalledWith({ url: '/pages/explain/list' })
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: {} },
{ route: 'pages/explain/guide-stop-list', options: {} }
]))
vi.stubGlobal('uni', {
navigateBack: vi.fn((options: { fail?: () => void }) => options.fail?.()),
reLaunch: vi.fn(),
navigateTo: vi.fn(),
setNavigationBarTitle: vi.fn()
})
const wrapper = mountGuideStopList()
await wrapper.get('[data-testid="list-back"]').trigger('click')
expect(uni.reLaunch).toHaveBeenCalledWith({ url: '/pages/explain/list' })
wrapper.unmount()
})
})