This commit is contained in:
@@ -9,8 +9,6 @@ const mocks = vi.hoisted(() => ({
|
||||
onHideHandler: null as null | (() => void),
|
||||
onUnloadHandler: null as null | (() => void),
|
||||
enterExplainDetail: vi.fn(),
|
||||
selectExplainDetailLanguage: vi.fn(),
|
||||
selectExplainDetailAudioOption: vi.fn(),
|
||||
loadExplainDetailText: vi.fn(),
|
||||
selectAudioForExplainDetail: vi.fn(),
|
||||
play: vi.fn(),
|
||||
@@ -18,8 +16,8 @@ const mocks = vi.hoisted(() => ({
|
||||
resume: vi.fn(),
|
||||
close: vi.fn(),
|
||||
seekToPercent: vi.fn(),
|
||||
cyclePlaybackRate: vi.fn(),
|
||||
toggleMuted: vi.fn()
|
||||
setPlaybackRate: vi.fn(),
|
||||
toggleMute: vi.fn()
|
||||
}))
|
||||
|
||||
const audioState = vi.hoisted(() => ({
|
||||
@@ -49,8 +47,6 @@ vi.mock('@dcloudio/uni-app', () => ({
|
||||
vi.mock('@/usecases/explainUseCase', () => ({
|
||||
explainUseCase: {
|
||||
enterExplainDetail: mocks.enterExplainDetail,
|
||||
selectExplainDetailLanguage: mocks.selectExplainDetailLanguage,
|
||||
selectExplainDetailAudioOption: mocks.selectExplainDetailAudioOption,
|
||||
loadExplainDetailText: mocks.loadExplainDetailText,
|
||||
selectAudioForExplainDetail: mocks.selectAudioForExplainDetail
|
||||
}
|
||||
@@ -65,19 +61,15 @@ vi.mock('@/usecases/guideUseCase', () => ({
|
||||
vi.mock('@/composables/useGlobalAudioPlayer', () => ({
|
||||
useGlobalAudioPlayer: () => ({
|
||||
...audioState,
|
||||
isCurrentSource: (source: { targetType?: string, targetId?: string, lang?: string }) => (
|
||||
audioState.currentSource.value?.targetType === source.targetType
|
||||
&& audioState.currentSource.value?.targetId === source.targetId
|
||||
&& audioState.currentSource.value?.lang === source.lang
|
||||
),
|
||||
isCurrentSource: () => false,
|
||||
play: mocks.play,
|
||||
pause: mocks.pause,
|
||||
resume: mocks.resume,
|
||||
switchLanguage: vi.fn(),
|
||||
close: mocks.close,
|
||||
seekToPercent: mocks.seekToPercent,
|
||||
cyclePlaybackRate: mocks.cyclePlaybackRate,
|
||||
toggleMuted: mocks.toggleMuted
|
||||
setPlaybackRate: mocks.setPlaybackRate,
|
||||
toggleMute: mocks.toggleMute
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -126,11 +118,6 @@ describe('讲解详情音频优先布局', () => {
|
||||
audioState.playbackRate.value = 1
|
||||
audioState.muted.value = false
|
||||
mocks.enterExplainDetail.mockResolvedValue(exhibit)
|
||||
mocks.selectExplainDetailLanguage.mockImplementation((source, lang) => ({
|
||||
...source,
|
||||
audioLanguage: lang
|
||||
}))
|
||||
mocks.selectExplainDetailAudioOption.mockImplementation((source, channelCode) => source)
|
||||
mocks.loadExplainDetailText.mockResolvedValue({ available: false })
|
||||
mocks.selectAudioForExplainDetail.mockResolvedValue({
|
||||
playable: true,
|
||||
@@ -146,7 +133,6 @@ describe('讲解详情音频优先布局', () => {
|
||||
mocks.play.mockResolvedValue(undefined)
|
||||
vi.stubGlobal('uni', {
|
||||
showToast: vi.fn(),
|
||||
showActionSheet: vi.fn(),
|
||||
navigateBack: vi.fn(),
|
||||
redirectTo: vi.fn(),
|
||||
reLaunch: vi.fn()
|
||||
@@ -168,34 +154,38 @@ describe('讲解详情音频优先布局', () => {
|
||||
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-dock').exists()).toBe(true)
|
||||
expect(wrapper.get('.detail-audio-dock').classes()).toContain('is-playable')
|
||||
expect(wrapper.get('.detail-audio-subtitle').text()).toBe('0:00 / 1:21')
|
||||
expect(wrapper.get('.detail-audio-rate').text()).toBe('1.0')
|
||||
expect(wrapper.find('.detail-audio-mute').exists()).toBe(true)
|
||||
expect(wrapper.find('.language-switch').text()).toContain('English')
|
||||
expect(wrapper.find('.detail-language-bar').exists()).toBe(true)
|
||||
expect(wrapper.find('.content .language-switch').exists()).toBe(false)
|
||||
expect(wrapper.get('.content-section').classes()).toContain('is-english')
|
||||
expect(wrapper.find('.section-title').exists()).toBe(false)
|
||||
expect(wrapper.get('.section-text').text()).toContain('detailed English narration')
|
||||
expect(wrapper.get('.detail-audio-time').text()).toBe('0:00 / 1:21')
|
||||
expect(wrapper.get('.detail-audio-speed').attributes('aria-label')).toBe('播放速度 1.0倍')
|
||||
})
|
||||
|
||||
it('正常音频底栏可切换播放速率和静音', async () => {
|
||||
it('详情加载失败时使用统一的系统反馈状态和紧凑操作按钮', async () => {
|
||||
mocks.enterExplainDetail.mockRejectedValue(new Error('请检查网络后重试。'))
|
||||
const wrapper = mount(ExhibitDetail, {
|
||||
global: { stubs: { GuidePageFrame: GuidePageFrameStub } }
|
||||
})
|
||||
|
||||
await mocks.onLoadHandler?.({ id: exhibit.id, lang: 'en-US', targetType: 'STOP', targetId: 'stop-1' })
|
||||
await mocks.onLoadHandler?.({ id: exhibit.id, targetType: 'STOP', targetId: 'stop-1' })
|
||||
await flushPromises()
|
||||
await wrapper.get('.detail-audio-rate').trigger('tap')
|
||||
await wrapper.get('.detail-audio-mute').trigger('tap')
|
||||
|
||||
expect(mocks.cyclePlaybackRate).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.toggleMuted).toHaveBeenCalledTimes(1)
|
||||
const state = wrapper.get('[data-testid="exhibit-detail-state"]')
|
||||
expect(state.classes()).toContain('guide-feedback-state')
|
||||
expect(state.text()).toContain('讲解对象加载失败')
|
||||
expect(state.text()).toContain('请检查网络后重试。')
|
||||
expect(state.get('.feedback-action.primary').text()).toBe('重新加载')
|
||||
expect(state.get('.feedback-action.secondary').text()).toBe('返回讲解列表')
|
||||
|
||||
await state.get('.feedback-action.primary').trigger('tap')
|
||||
expect(mocks.enterExplainDetail).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('仅展示 stop-info 声明支持的语言按钮', async () => {
|
||||
@@ -230,125 +220,7 @@ describe('讲解详情音频优先布局', () => {
|
||||
expect(wrapper.find('.detail-language-bar').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('仅在当前语言同时存在男女声时展示声线按钮并重播所选音频', async () => {
|
||||
const voiceExhibit = {
|
||||
...exhibit,
|
||||
audioOptions: [
|
||||
{
|
||||
channelCode: 'standard.en-US.female',
|
||||
displayName: '英文女声',
|
||||
languageCode: 'en-US',
|
||||
gender: 'female',
|
||||
audioUrl: '/assets/astrolabe-en-female.mp3',
|
||||
duration: 81,
|
||||
isDefault: true
|
||||
},
|
||||
{
|
||||
channelCode: 'standard.en-US.male',
|
||||
displayName: '英文男声',
|
||||
languageCode: 'en-US',
|
||||
gender: 'male',
|
||||
audioUrl: '/assets/astrolabe-en-male.mp3',
|
||||
duration: 82,
|
||||
isDefault: false
|
||||
},
|
||||
{
|
||||
channelCode: 'standard.yue-HK.female',
|
||||
displayName: '粤语女声',
|
||||
languageCode: 'yue-HK',
|
||||
gender: 'female',
|
||||
audioUrl: '/assets/astrolabe-yue-female.mp3',
|
||||
duration: 83,
|
||||
isDefault: true
|
||||
}
|
||||
],
|
||||
audioChannelCode: 'standard.en-US.female',
|
||||
audioVoiceGender: 'female',
|
||||
audioVoiceDisplayName: '英文女声',
|
||||
audioUrl: '/assets/astrolabe-en-female.mp3'
|
||||
}
|
||||
const maleVoiceExhibit = {
|
||||
...voiceExhibit,
|
||||
audioUrl: '/assets/astrolabe-en-male.mp3',
|
||||
audioDuration: 82,
|
||||
audioChannelCode: 'standard.en-US.male',
|
||||
audioVoiceGender: 'male',
|
||||
audioVoiceDisplayName: '英文男声'
|
||||
}
|
||||
mocks.enterExplainDetail.mockResolvedValue(voiceExhibit)
|
||||
mocks.selectExplainDetailAudioOption.mockReturnValue(maleVoiceExhibit)
|
||||
mocks.selectAudioForExplainDetail.mockImplementation((source) => Promise.resolve({
|
||||
playable: true,
|
||||
exhibit: source,
|
||||
media: {
|
||||
id: source.audioChannelCode,
|
||||
url: source.audioUrl,
|
||||
duration: source.audioDuration,
|
||||
language: source.audioLanguage
|
||||
},
|
||||
playInfo: { title: source.name }
|
||||
}))
|
||||
audioState.currentAudio.value = { id: 'audio-1', audioUrl: voiceExhibit.audioUrl }
|
||||
audioState.currentSource.value = { targetType: 'STOP', targetId: 'stop-1', lang: 'en-US' }
|
||||
audioState.playing.value = true
|
||||
mocks.close.mockImplementation(() => {
|
||||
audioState.currentAudio.value = null
|
||||
audioState.currentSource.value = null
|
||||
audioState.playing.value = false
|
||||
})
|
||||
vi.mocked(uni.showActionSheet).mockImplementation(({ success }) => success?.({ tapIndex: 1 } as any))
|
||||
|
||||
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-voice').text()).toBe('女声')
|
||||
|
||||
await wrapper.get('.detail-audio-voice').trigger('tap')
|
||||
await flushPromises()
|
||||
|
||||
expect(uni.showActionSheet).toHaveBeenCalledWith(expect.objectContaining({
|
||||
itemList: ['英文女声', '英文男声']
|
||||
}))
|
||||
expect(mocks.selectExplainDetailAudioOption).toHaveBeenCalledWith(voiceExhibit, 'standard.en-US.male')
|
||||
expect(wrapper.get('.detail-audio-voice').text()).toBe('男声')
|
||||
expect(mocks.close).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.play).toHaveBeenCalledWith(expect.objectContaining({
|
||||
audioUrl: '/assets/astrolabe-en-male.mp3'
|
||||
}), expect.any(Object))
|
||||
})
|
||||
|
||||
it('粤语只有女声时保留禁用的声线按钮', async () => {
|
||||
mocks.enterExplainDetail.mockResolvedValue({
|
||||
...exhibit,
|
||||
audioLanguage: 'yue-HK',
|
||||
audioVoiceGender: 'female',
|
||||
audioOptions: [
|
||||
{
|
||||
channelCode: 'standard.yue-HK.female',
|
||||
displayName: '粤语女声',
|
||||
languageCode: 'yue-HK',
|
||||
gender: 'female',
|
||||
audioUrl: '/assets/astrolabe-yue-female.mp3',
|
||||
duration: 83,
|
||||
isDefault: true
|
||||
}
|
||||
]
|
||||
})
|
||||
const wrapper = mount(ExhibitDetail, {
|
||||
global: { stubs: { GuidePageFrame: GuidePageFrameStub } }
|
||||
})
|
||||
|
||||
await mocks.onLoadHandler?.({ id: exhibit.id, lang: 'yue-HK', targetType: 'STOP', targetId: 'stop-1' })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.get('.detail-audio-voice').text()).toBe('女声')
|
||||
expect(wrapper.get('.detail-audio-voice').attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('深链语言不受支持时本地回退至中文并规范化地址', async () => {
|
||||
it('深链语言不受支持时回退至中文并规范化地址', async () => {
|
||||
mocks.enterExplainDetail.mockImplementation(async (request: { lang: string }) => ({
|
||||
...exhibit,
|
||||
audioLanguage: request.lang,
|
||||
@@ -362,9 +234,8 @@ describe('讲解详情音频优先布局', () => {
|
||||
await mocks.onLoadHandler?.({ id: exhibit.id, lang: 'en-US', targetType: 'STOP', targetId: 'stop-1' })
|
||||
await flushPromises()
|
||||
|
||||
expect(mocks.enterExplainDetail).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.enterExplainDetail).toHaveBeenCalledWith(expect.objectContaining({ lang: 'en-US' }))
|
||||
expect(mocks.selectExplainDetailLanguage).toHaveBeenCalledWith(expect.any(Object), 'zh-CN')
|
||||
expect(mocks.enterExplainDetail).toHaveBeenNthCalledWith(1, expect.objectContaining({ lang: 'en-US' }))
|
||||
expect(mocks.enterExplainDetail).toHaveBeenNthCalledWith(2, expect.objectContaining({ lang: 'zh-CN' }))
|
||||
expect(wrapper.get('.language-option').classes()).toContain('active')
|
||||
expect(wrapper.get('.language-option-text').text()).toBe('中文')
|
||||
expect(replaceState).toHaveBeenCalledWith(null, '', expect.stringContaining('lang=zh-CN'))
|
||||
@@ -397,39 +268,11 @@ describe('讲解详情音频优先布局', () => {
|
||||
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)
|
||||
expect(wrapper.find('.detail-audio-status').exists()).toBe(false)
|
||||
expect(wrapper.find('.detail-audio-progress').exists()).toBe(true)
|
||||
expect(wrapper.get('.detail-audio-play').attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('切换语言时使用初次详情响应中的变体,不重复请求详情或正文', async () => {
|
||||
const localizedExhibit = {
|
||||
...exhibit,
|
||||
guideText: '粤语通道使用普通话文案。',
|
||||
audioUrl: '/assets/astrolabe-yue.mp3',
|
||||
audioDuration: 83,
|
||||
audioLanguage: 'yue-HK',
|
||||
audioStatus: 'READY',
|
||||
audioHasText: true,
|
||||
supportedLanguages: ['zh-CN', 'yue-HK', 'en-US']
|
||||
}
|
||||
mocks.selectExplainDetailLanguage.mockReturnValue(localizedExhibit)
|
||||
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.findAll('.language-option').find((node) => node.text() === '粤语')?.trigger('tap')
|
||||
await flushPromises()
|
||||
|
||||
expect(mocks.enterExplainDetail).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.loadExplainDetailText).not.toHaveBeenCalled()
|
||||
expect(mocks.selectExplainDetailLanguage).toHaveBeenCalledWith(expect.any(Object), 'yue-HK')
|
||||
expect(wrapper.get('.section-text').text()).toContain('粤语通道使用普通话文案')
|
||||
expect(wrapper.get('.detail-audio-subtitle').text()).toBe('0:00 / 1:23')
|
||||
})
|
||||
|
||||
it('无音频时禁用播放按钮并显示原因', async () => {
|
||||
mocks.enterExplainDetail.mockResolvedValue({
|
||||
...exhibit,
|
||||
@@ -449,7 +292,7 @@ describe('讲解详情音频优先布局', () => {
|
||||
|
||||
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('当前语言暂无语音讲解。')
|
||||
expect(wrapper.find('.detail-audio-status').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('播放失败显示错误并通过既有刷新链路重试', async () => {
|
||||
@@ -469,9 +312,9 @@ describe('讲解详情音频优先布局', () => {
|
||||
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('重试')
|
||||
expect(wrapper.find('.detail-audio-retry').exists()).toBe(false)
|
||||
|
||||
await wrapper.get('.detail-audio-retry').trigger('tap')
|
||||
await wrapper.get('.detail-audio-play').trigger('tap')
|
||||
await flushPromises()
|
||||
expect(mocks.selectAudioForExplainDetail).toHaveBeenCalledWith(expect.any(Object), { refreshPlayInfo: true })
|
||||
expect(mocks.play).toHaveBeenCalledWith(expect.objectContaining({ id: 'audio-refresh' }), expect.objectContaining({
|
||||
@@ -480,6 +323,42 @@ describe('讲解详情音频优先布局', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
it('播放器单行提供倍速循环和男女声切换', async () => {
|
||||
mocks.enterExplainDetail.mockResolvedValue({
|
||||
...exhibit,
|
||||
audioOptions: [
|
||||
{
|
||||
channelCode: 'standard.en-US.male',
|
||||
languageCode: 'en-US',
|
||||
gender: 'male',
|
||||
playUrl: '/assets/astrolabe-en-male.mp3'
|
||||
},
|
||||
{
|
||||
channelCode: 'standard.en-US.female',
|
||||
languageCode: 'en-US',
|
||||
gender: 'female',
|
||||
playUrl: '/assets/astrolabe-en-female.mp3'
|
||||
}
|
||||
]
|
||||
})
|
||||
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-speed').text()).toBe('1.0')
|
||||
expect(wrapper.get('.detail-audio-voice').attributes('aria-label')).toBe('切换为女声讲解')
|
||||
|
||||
await wrapper.get('.detail-audio-speed').trigger('tap')
|
||||
await wrapper.get('.detail-audio-voice').trigger('tap')
|
||||
|
||||
expect(mocks.setPlaybackRate).toHaveBeenCalledWith(1.25)
|
||||
expect(wrapper.get('.detail-audio-voice').attributes('aria-label')).toBe('切换为男声讲解')
|
||||
expect(mocks.toggleMute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('上一页是讲解对象列表时使用页面栈返回', async () => {
|
||||
vi.stubGlobal('getCurrentPages', vi.fn(() => [
|
||||
{ route: 'pages/explain/guide-stop-list', options: {} },
|
||||
@@ -528,6 +407,9 @@ describe('讲解详情音频优先布局', () => {
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
vi.mocked(uni.navigateBack).mockImplementationOnce((options: any) => {
|
||||
options.fail?.()
|
||||
})
|
||||
wrapper.getComponent(GuidePageFrameStub).vm.$emit('back')
|
||||
await flushPromises()
|
||||
|
||||
@@ -536,6 +418,6 @@ describe('讲解详情音频优先布局', () => {
|
||||
expect(url.split('?')[0]).toBe('/pages/explain/guide-stop-list')
|
||||
expect(params.get('hallId')).toBe('hall-1')
|
||||
expect(params.get('hallName')).toBe('恐龙厅')
|
||||
expect(uni.navigateBack).not.toHaveBeenCalled()
|
||||
expect(uni.navigateBack).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user