@@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({
|
||||
onUnloadHandler: null as null | (() => void),
|
||||
enterExplainDetail: vi.fn(),
|
||||
selectExplainDetailLanguage: vi.fn(),
|
||||
selectExplainDetailAudioOption: vi.fn(),
|
||||
loadExplainDetailText: vi.fn(),
|
||||
selectAudioForExplainDetail: vi.fn(),
|
||||
play: vi.fn(),
|
||||
@@ -49,6 +50,7 @@ vi.mock('@/usecases/explainUseCase', () => ({
|
||||
explainUseCase: {
|
||||
enterExplainDetail: mocks.enterExplainDetail,
|
||||
selectExplainDetailLanguage: mocks.selectExplainDetailLanguage,
|
||||
selectExplainDetailAudioOption: mocks.selectExplainDetailAudioOption,
|
||||
loadExplainDetailText: mocks.loadExplainDetailText,
|
||||
selectAudioForExplainDetail: mocks.selectAudioForExplainDetail
|
||||
}
|
||||
@@ -63,7 +65,11 @@ vi.mock('@/usecases/guideUseCase', () => ({
|
||||
vi.mock('@/composables/useGlobalAudioPlayer', () => ({
|
||||
useGlobalAudioPlayer: () => ({
|
||||
...audioState,
|
||||
isCurrentSource: () => false,
|
||||
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
|
||||
),
|
||||
play: mocks.play,
|
||||
pause: mocks.pause,
|
||||
resume: mocks.resume,
|
||||
@@ -124,6 +130,7 @@ describe('讲解详情音频优先布局', () => {
|
||||
...source,
|
||||
audioLanguage: lang
|
||||
}))
|
||||
mocks.selectExplainDetailAudioOption.mockImplementation((source, channelCode) => source)
|
||||
mocks.loadExplainDetailText.mockResolvedValue({ available: false })
|
||||
mocks.selectAudioForExplainDetail.mockResolvedValue({
|
||||
playable: true,
|
||||
@@ -139,6 +146,7 @@ describe('讲解详情音频优先布局', () => {
|
||||
mocks.play.mockResolvedValue(undefined)
|
||||
vi.stubGlobal('uni', {
|
||||
showToast: vi.fn(),
|
||||
showActionSheet: vi.fn(),
|
||||
navigateBack: vi.fn(),
|
||||
redirectTo: vi.fn(),
|
||||
reLaunch: vi.fn()
|
||||
@@ -222,6 +230,124 @@ 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 () => {
|
||||
mocks.enterExplainDetail.mockImplementation(async (request: { lang: string }) => ({
|
||||
...exhibit,
|
||||
|
||||
@@ -25,6 +25,7 @@ const createStopInfo = (overrides: Partial<GuideStopInfo> = {}): GuideStopInfo =
|
||||
hasAudio: false,
|
||||
hasText: false,
|
||||
supportedLanguages: [],
|
||||
audioOptions: [],
|
||||
languageVariants: {},
|
||||
audioStatus: 'MISSING',
|
||||
...overrides
|
||||
@@ -156,4 +157,91 @@ describe('ExplainUseCase stop image policy', () => {
|
||||
expect((audio as any).getPlayInfo).not.toHaveBeenCalled()
|
||||
expect((audio as any).getTextInfo).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('prefers default audio options and switches the selected voice locally', async () => {
|
||||
const useCase = createUseCase(createStopInfo({
|
||||
hasAudio: true,
|
||||
hasText: true,
|
||||
supportedLanguages: ['zh-CN', 'en-US'],
|
||||
languageVariants: {
|
||||
'zh-CN': {
|
||||
lang: 'zh-CN',
|
||||
enabled: true,
|
||||
playable: true,
|
||||
playUrl: '/audio/legacy-zh.mp3',
|
||||
textAvailable: true,
|
||||
text: '普通话讲解词',
|
||||
hasText: true,
|
||||
fallback: false,
|
||||
audioStatus: 'READY'
|
||||
},
|
||||
'en-US': {
|
||||
lang: 'en-US',
|
||||
enabled: true,
|
||||
playable: true,
|
||||
playUrl: '/audio/legacy-en.mp3',
|
||||
textAvailable: true,
|
||||
text: 'English narration',
|
||||
hasText: true,
|
||||
fallback: false,
|
||||
audioStatus: 'READY'
|
||||
}
|
||||
} as GuideStopInfo['languageVariants'],
|
||||
audioOptions: [
|
||||
{
|
||||
channelCode: 'standard.zh-CN.female',
|
||||
displayName: '普通话女声',
|
||||
languageCode: 'zh-CN',
|
||||
gender: 'female',
|
||||
playUrl: '/audio/zh-female.mp3',
|
||||
duration: 50,
|
||||
isDefault: true
|
||||
},
|
||||
{
|
||||
channelCode: 'standard.zh-CN.male',
|
||||
displayName: '普通话男声',
|
||||
languageCode: 'zh-CN',
|
||||
gender: 'male',
|
||||
playUrl: '/audio/zh-male.mp3',
|
||||
duration: 51,
|
||||
isDefault: false
|
||||
},
|
||||
{
|
||||
channelCode: 'standard.en-US.female',
|
||||
displayName: '英文女声',
|
||||
languageCode: 'en-US',
|
||||
gender: 'female',
|
||||
playUrl: '/audio/en-female.mp3',
|
||||
duration: 49,
|
||||
isDefault: true
|
||||
}
|
||||
]
|
||||
}))
|
||||
|
||||
const chinese = await useCase.enterExplainDetail({
|
||||
exhibitId: 'stop-1', targetType: 'STOP', targetId: 'stop-1', lang: 'zh-CN'
|
||||
})
|
||||
const english = useCase.selectExplainDetailLanguage(chinese, 'en-US')
|
||||
const chineseMale = useCase.selectExplainDetailAudioOption(chinese, 'standard.zh-CN.male')
|
||||
|
||||
expect(chinese).toMatchObject({
|
||||
audioUrl: '/audio/zh-female.mp3',
|
||||
audioDuration: 50,
|
||||
audioChannelCode: 'standard.zh-CN.female',
|
||||
audioVoiceGender: 'female',
|
||||
guideText: '普通话讲解词'
|
||||
})
|
||||
expect(english).toMatchObject({
|
||||
audioUrl: '/audio/en-female.mp3',
|
||||
audioChannelCode: 'standard.en-US.female',
|
||||
guideText: 'English narration'
|
||||
})
|
||||
expect(chineseMale).toMatchObject({
|
||||
audioUrl: '/audio/zh-male.mp3',
|
||||
audioDuration: 51,
|
||||
audioChannelCode: 'standard.zh-CN.male',
|
||||
audioVoiceGender: 'male',
|
||||
guideText: '普通话讲解词'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -75,4 +75,63 @@ describe('guide stop-info adapter', () => {
|
||||
expect(result.supportedLanguages).toEqual(['zh-CN', 'en-US'])
|
||||
expect(result.languageVariants).toEqual({})
|
||||
})
|
||||
|
||||
it('uses playable audio options as the primary language and voice contract', () => {
|
||||
const result = toGuideStopInfo({
|
||||
supportedLanguages: ['zh-CN', 'yue-HK'],
|
||||
audioOptions: [
|
||||
{
|
||||
channelCode: 'standard.zh-CN.male',
|
||||
displayName: '普通话男声',
|
||||
languageCode: 'zh-CN',
|
||||
gender: 'male',
|
||||
playUrl: '/museum-assets/audio/zh-male.mp3',
|
||||
duration: '51',
|
||||
isDefault: false,
|
||||
sortOrder: 11
|
||||
},
|
||||
{
|
||||
channelCode: 'standard.en-US.female',
|
||||
displayName: '英文女声',
|
||||
languageCode: 'en-US',
|
||||
gender: 'female',
|
||||
playUrl: '/museum-assets/audio/en-female.mp3',
|
||||
duration: 49,
|
||||
isDefault: true,
|
||||
sortOrder: 20
|
||||
},
|
||||
{
|
||||
channelCode: 'standard.zh-CN.female',
|
||||
displayName: '普通话女声',
|
||||
languageCode: 'zh-CN',
|
||||
gender: 'female',
|
||||
playUrl: '/museum-assets/audio/zh-female.mp3',
|
||||
isDefault: true,
|
||||
sortOrder: 10
|
||||
},
|
||||
{
|
||||
channelCode: 'invalid',
|
||||
languageCode: 'zh-CN',
|
||||
gender: 'female'
|
||||
}
|
||||
]
|
||||
}, {
|
||||
targetType: 'STOP',
|
||||
targetId: '9001',
|
||||
lang: 'zh-CN'
|
||||
})
|
||||
|
||||
expect(result.supportedLanguages).toEqual(['zh-CN', 'en-US'])
|
||||
expect(result.audioOptions.map((option) => option.channelCode)).toEqual([
|
||||
'standard.zh-CN.female',
|
||||
'standard.zh-CN.male',
|
||||
'standard.en-US.female'
|
||||
])
|
||||
expect(result.audioOptions[0]).toMatchObject({
|
||||
displayName: '普通话女声',
|
||||
gender: 'female',
|
||||
playUrl: '/museum-assets/audio/zh-female.mp3',
|
||||
isDefault: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user