// @vitest-environment happy-dom import { defineComponent } from 'vue' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { flushPromises, mount } from '@vue/test-utils' const mocks = vi.hoisted(() => ({ onLoadHandler: null as null | ((options?: Record) => Promise), enterExplainDetail: vi.fn(), loadExplainDetailText: vi.fn(), selectAudioForExplainDetail: vi.fn(), play: vi.fn(), pause: vi.fn(), resume: vi.fn(), close: vi.fn() })) const audioState = vi.hoisted(() => ({ currentAudio: { value: null as any }, currentSource: { value: null as any }, playing: { value: false }, currentTime: { value: 0 }, duration: { value: 0 } })) vi.mock('@dcloudio/uni-app', () => ({ onLoad: (handler: (options?: Record) => Promise) => { mocks.onLoadHandler = handler }, onUnload: () => undefined })) vi.mock('@/usecases/explainUseCase', () => ({ explainUseCase: { enterExplainDetail: mocks.enterExplainDetail, loadExplainDetailText: mocks.loadExplainDetailText, selectAudioForExplainDetail: mocks.selectAudioForExplainDetail } })) vi.mock('@/usecases/guideUseCase', () => ({ guideUseCase: { resolveContentLocationPreviewTarget: vi.fn() } })) vi.mock('@/composables/useGlobalAudioPlayer', () => ({ useGlobalAudioPlayer: () => ({ ...audioState, isCurrentSource: () => false, play: mocks.play, pause: mocks.pause, resume: mocks.resume, switchLanguage: vi.fn(), close: mocks.close }) })) vi.mock('@/utils/hostEnvironment', () => ({ isEmbeddedInWechatMiniProgram: () => false })) import ExhibitDetail from '@/pages/exhibit/detail.vue' const GuidePageFrameStub = defineComponent({ name: 'GuidePageFrameStub', emits: ['back'], template: '
' }) const exhibit = { id: 'exhibit-1', name: '[Classical Astrolabe] 标准解说', hallName: 'Universe Hall', floorLabel: 'B2', image: '/assets/astrolabe.jpg', description: 'A detailed English narration for this exhibit.', guideText: 'A detailed English narration for this exhibit.', audioUrl: '/assets/astrolabe.mp3', audioDuration: 81, audioStatus: 'READY', audioLanguage: 'en-US', audioHasText: false, playTargetType: 'STOP', playTargetId: 'stop-1' } describe('讲解详情音频优先布局', () => { beforeEach(() => { mocks.onLoadHandler = null mocks.enterExplainDetail.mockResolvedValue(exhibit) mocks.loadExplainDetailText.mockResolvedValue({ available: false }) mocks.selectAudioForExplainDetail.mockResolvedValue({ playable: true, exhibit, media: { id: 'audio-1', url: exhibit.audioUrl, duration: exhibit.audioDuration, language: 'en-US' }, playInfo: { title: exhibit.name } }) mocks.play.mockResolvedValue(undefined) vi.stubGlobal('uni', { showToast: vi.fn(), navigateBack: vi.fn() }) }) afterEach(() => { vi.clearAllMocks() vi.unstubAllGlobals() }) it('将展品身份、流内播放器、语言栏和英文正文按顺序渲染', async () => { const wrapper = mount(ExhibitDetail, { global: { stubs: { GuidePageFrame: GuidePageFrameStub } } }) await mocks.onLoadHandler?.({ id: exhibit.id, lang: 'en-US', targetType: 'STOP', targetId: 'stop-1' }) await flushPromises() expect(wrapper.get('.detail-title').text()).toBe('Classical Astrolabe') expect(wrapper.get('.detail-audio-title').text()).toBe('Classical Astrolabe') expect(wrapper.get('.detail-meta').text()).toContain('Universe Hall · B2') expect(wrapper.get('.detail-chip').text()).toBe('标准解说') expect(wrapper.find('.detail-audio-control').exists()).toBe(true) expect(wrapper.find('.detail-audio-dock').exists()).toBe(false) expect(wrapper.find('.language-switch').text()).toContain('English') expect(wrapper.get('.content-section').classes()).toContain('is-english') expect(wrapper.get('.section-title').text()).toBe('Transcript') expect(wrapper.get('.section-text').text()).toContain('detailed English narration') }) it('通过同一全局音频播放器开始播放,不创建页面播放器', async () => { const wrapper = mount(ExhibitDetail, { global: { stubs: { GuidePageFrame: GuidePageFrameStub } } }) await mocks.onLoadHandler?.({ id: exhibit.id, lang: 'en-US', targetType: 'STOP', targetId: 'stop-1' }) await flushPromises() await wrapper.get('.detail-audio-play').trigger('tap') expect(mocks.play).toHaveBeenCalledWith(expect.objectContaining({ audioUrl: exhibit.audioUrl, language: 'en-US' }), expect.objectContaining({ displayMode: 'mini' })) }) })