Files
frontend-miniapp/tests/unit/ExplainUseCase.spec.ts
lyf 7001ec355a
Some checks failed
CI / verify (push) Has been cancelled
适配讲解详情多语言声线播放
2026-07-25 01:04:42 +08:00

248 lines
8.4 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import type { GuideStopInfo } from '@/data/adapters/guideStopInfoAdapter'
import type { AudioPlayInfoRepository } from '@/repositories/AudioPlayInfoRepository'
import type { ExplainRepository } from '@/repositories/ExplainRepository'
import { ExplainUseCase } from '@/usecases/explainUseCase'
import { dataSourceConfig } from '@/config/dataSource'
const createStopInfo = (overrides: Partial<GuideStopInfo> = {}): GuideStopInfo => ({
available: true,
targetType: 'STOP',
targetId: 'stop-1',
lang: 'zh-CN',
title: 'Guide stop',
galleryUrls: [],
imageStatus: 'MISSING',
linkedExhibits: [
{
id: 'exhibit-1',
name: 'Linked exhibit',
coverImageUrl: '/linked-exhibit.jpg'
}
],
playTargetType: 'STOP',
playTargetId: 'stop-1',
hasAudio: false,
hasText: false,
supportedLanguages: [],
audioOptions: [],
languageVariants: {},
audioStatus: 'MISSING',
...overrides
})
const createUseCase = (stopInfo: GuideStopInfo) => {
const explain = {
getExhibitById: vi.fn().mockResolvedValue(null),
listExplainExhibits: vi.fn().mockResolvedValue([])
} as unknown as ExplainRepository
const audioPlayInfo = {
getStopInfo: vi.fn().mockResolvedValue(stopInfo)
} as unknown as AudioPlayInfoRepository
return new ExplainUseCase(explain, audioPlayInfo)
}
describe('ExplainUseCase stop image policy', () => {
it('uses stop-info first in remote mode without requesting a catalog fallback', async () => {
const originalMode = dataSourceConfig.explainContentMode
Object.assign(dataSourceConfig, { explainContentMode: 'remote' })
const explain = {
getExhibitById: vi.fn(),
listExplainExhibits: vi.fn(),
listExplainExhibitsByHall: vi.fn()
} as unknown as ExplainRepository
const audio = { getStopInfo: vi.fn().mockResolvedValue(createStopInfo()) } as unknown as AudioPlayInfoRepository
await new ExplainUseCase(explain, audio).enterExplainDetail({
exhibitId: 'stop-1', targetType: 'STOP', targetId: 'stop-1', hallName: '宇宙厅'
})
expect(audio.getStopInfo).toHaveBeenCalledTimes(1)
expect(explain.getExhibitById).not.toHaveBeenCalled()
expect(explain.listExplainExhibits).not.toHaveBeenCalled()
Object.assign(dataSourceConfig, { explainContentMode: originalMode })
})
it('delegates hall exhibits and business-unit stops without loading full exhibits', async () => {
const explain = {
listExplainExhibitsByHall: vi.fn().mockResolvedValue([]),
listGuideStopsByBusinessUnit: vi.fn().mockResolvedValue([])
} as unknown as ExplainRepository
const useCase = new ExplainUseCase(explain, {} as AudioPlayInfoRepository)
await useCase.listExhibitsByHallId('hall-1')
await useCase.listGuideStopsByBusinessUnit('hall-1', 'outline-1')
expect(explain.listExplainExhibitsByHall).toHaveBeenCalledWith('hall-1')
expect(explain.listGuideStopsByBusinessUnit).toHaveBeenCalledWith('hall-1', 'outline-1')
})
it('does not use linked exhibit images when stop-info reports MISSING', async () => {
const detail = await createUseCase(createStopInfo()).enterExplainDetail({
exhibitId: 'stop-1',
targetType: 'STOP',
targetId: 'stop-1'
})
expect(detail.image).toBeUndefined()
expect(detail.galleryUrls).toEqual([])
expect(detail.linkedExhibits?.[0]?.coverImageUrl).toBe('/linked-exhibit.jpg')
})
it('uses the stop image when stop-info reports READY', async () => {
const detail = await createUseCase(createStopInfo({
imageStatus: 'READY',
coverImageUrl: '/guide-stop.jpg',
galleryUrls: ['/guide-stop-gallery.jpg']
})).enterExplainDetail({
exhibitId: 'stop-1',
targetType: 'STOP',
targetId: 'stop-1'
})
expect(detail.image).toBe('/guide-stop.jpg')
expect(detail.galleryUrls).toEqual(['/guide-stop-gallery.jpg'])
})
it('uses stop-info language variants locally without requesting play-info or text-info', async () => {
const audio = {
getStopInfo: vi.fn().mockResolvedValue(createStopInfo({
hasAudio: true,
hasText: true,
supportedLanguages: ['zh-CN', 'yue-HK'],
languageVariants: {
'zh-CN': {
lang: 'zh-CN',
enabled: true,
playable: true,
playUrl: '/audio/zh.mp3',
duration: 50,
hasText: true,
textAvailable: true,
text: '普通话讲解词',
fallback: false,
audioStatus: 'READY'
},
'yue-HK': {
lang: 'yue-HK',
enabled: true,
playable: true,
playUrl: '/audio/yue.mp3',
duration: 52,
hasText: true,
textAvailable: true,
text: '粤语通道使用普通话文案',
fallback: false,
audioStatus: 'READY'
}
} as GuideStopInfo['languageVariants']
})),
getPlayInfo: vi.fn(),
getTextInfo: vi.fn()
} as unknown as AudioPlayInfoRepository
const explain = {
getExhibitById: vi.fn(),
listExplainExhibits: vi.fn()
} as unknown as ExplainRepository
const useCase = new ExplainUseCase(explain, audio)
const detail = await useCase.enterExplainDetail({
exhibitId: 'stop-1', targetType: 'STOP', targetId: 'stop-1', lang: 'zh-CN'
})
const cantonese = useCase.selectExplainDetailLanguage(detail, 'yue-HK')
expect(detail.guideText).toBe('普通话讲解词')
expect(detail.audioUrl).toBe('/audio/zh.mp3')
expect(cantonese.guideText).toBe('粤语通道使用普通话文案')
expect(cantonese.audioUrl).toBe('/audio/yue.mp3')
expect(cantonese.audioDuration).toBe(52)
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: '普通话讲解词'
})
})
})