调整讲解详情页音频播放器状态
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
lyf
2026-07-15 17:26:11 +08:00
parent 57d1e315b5
commit f461f2e325
2 changed files with 385 additions and 222 deletions

View File

@@ -12,13 +12,16 @@ const mocks = vi.hoisted(() => ({
play: vi.fn(),
pause: vi.fn(),
resume: vi.fn(),
close: vi.fn()
close: vi.fn(),
seekToPercent: vi.fn()
}))
const audioState = vi.hoisted(() => ({
currentAudio: { value: null as any },
currentSource: { value: null as any },
playing: { value: false },
loading: { value: false },
error: { value: '' },
currentTime: { value: 0 },
duration: { value: 0 }
}))
@@ -52,7 +55,8 @@ vi.mock('@/composables/useGlobalAudioPlayer', () => ({
pause: mocks.pause,
resume: mocks.resume,
switchLanguage: vi.fn(),
close: mocks.close
close: mocks.close,
seekToPercent: mocks.seekToPercent
})
}))
@@ -88,6 +92,13 @@ const exhibit = {
describe('讲解详情音频优先布局', () => {
beforeEach(() => {
mocks.onLoadHandler = null
audioState.currentAudio.value = null
audioState.currentSource.value = null
audioState.playing.value = false
audioState.loading.value = false
audioState.error.value = ''
audioState.currentTime.value = 0
audioState.duration.value = 0
mocks.enterExplainDetail.mockResolvedValue(exhibit)
mocks.loadExplainDetailText.mockResolvedValue({ available: false })
mocks.selectAudioForExplainDetail.mockResolvedValue({
@@ -102,7 +113,12 @@ describe('讲解详情音频优先布局', () => {
playInfo: { title: exhibit.name }
})
mocks.play.mockResolvedValue(undefined)
vi.stubGlobal('uni', { showToast: vi.fn(), navigateBack: vi.fn() })
vi.stubGlobal('uni', {
showToast: vi.fn(),
navigateBack: vi.fn(),
redirectTo: vi.fn(),
reLaunch: vi.fn()
})
})
afterEach(() => {
@@ -110,7 +126,7 @@ describe('讲解详情音频优先布局', () => {
vi.unstubAllGlobals()
})
it('将展品身份、流内播放器、语言栏和英文正文按顺序渲染', async () => {
it('正常状态渲染固定底部播放器、语言栏和英文正文', async () => {
const wrapper = mount(ExhibitDetail, {
global: { stubs: { GuidePageFrame: GuidePageFrameStub } }
})
@@ -122,8 +138,8 @@ describe('讲解详情音频优先布局', () => {
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('.detail-audio-dock').exists()).toBe(true)
expect(wrapper.get('.detail-audio-dock').classes()).toContain('is-playable')
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')
@@ -145,7 +161,23 @@ describe('讲解详情音频优先布局', () => {
}), expect.objectContaining({ displayMode: 'mini' }))
})
it('音频不可用时只在音频控制区显示一次提示', async () => {
it('加载中使用同一高度底栏和不确定进度', async () => {
audioState.currentAudio.value = { id: 'audio-1' }
audioState.currentSource.value = { targetType: 'STOP', targetId: 'stop-1', lang: 'en-US' }
audioState.loading.value = true
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-audio-dock').classes()).toContain('is-loading')
expect(wrapper.find('.audio-loading-spinner').exists()).toBe(true)
expect(wrapper.find('.detail-audio-progress.is-indeterminate').exists()).toBe(true)
})
it('无音频时禁用播放按钮并显示原因', async () => {
mocks.enterExplainDetail.mockResolvedValue({
...exhibit,
audioUrl: undefined,
@@ -162,8 +194,47 @@ describe('讲解详情音频优先布局', () => {
await mocks.onLoadHandler?.({ id: exhibit.id, lang: 'yue-HK', targetType: 'STOP', targetId: 'stop-1' })
await flushPromises()
expect(wrapper.find('.detail-audio-unavailable').text()).toBe('当前语言暂无语音讲解。')
expect(wrapper.find('.audio-status-hint').exists()).toBe(false)
expect(wrapper.findAll('.section-hint')).toHaveLength(0)
expect(wrapper.get('.detail-audio-dock').classes()).toContain('is-unavailable')
expect(wrapper.get('.detail-audio-play').attributes('disabled')).toBeDefined()
expect(wrapper.get('.detail-audio-subtitle').text()).toContain('当前语言暂无语音讲解。')
})
it('播放失败显示错误并通过既有刷新链路重试', async () => {
audioState.currentAudio.value = { id: 'audio-1', audioUrl: exhibit.audioUrl }
audioState.currentSource.value = { targetType: 'STOP', targetId: 'stop-1', lang: 'en-US' }
audioState.error.value = '音频暂时无法播放'
mocks.selectAudioForExplainDetail.mockResolvedValue({
playable: true,
exhibit,
media: { id: 'audio-refresh', url: exhibit.audioUrl, duration: exhibit.audioDuration, language: 'en-US' },
playInfo: { title: exhibit.name }
})
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-audio-dock').classes()).toContain('is-failed')
expect(wrapper.get('.detail-audio-retry').text()).toBe('重试')
await wrapper.get('.detail-audio-retry').trigger('tap')
await flushPromises()
expect(mocks.selectAudioForExplainDetail).toHaveBeenCalledWith(expect.any(Object), { refreshPlayInfo: true })
expect(mocks.play).toHaveBeenCalledWith(expect.objectContaining({ id: 'audio-refresh' }), expect.objectContaining({
retryOnError: expect.any(Function),
displayMode: 'mini'
}))
})
it('顶部返回统一使用页面栈,以便导览首页 onShow 消费复位上下文', async () => {
const wrapper = mount(ExhibitDetail, {
global: { stubs: { GuidePageFrame: GuidePageFrameStub } }
})
wrapper.getComponent(GuidePageFrameStub).vm.$emit('back')
await flushPromises()
expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
})
})