import { describe, expect, it, vi } from 'vitest' import type { GuideStopDetail } 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 createDetail = (overrides: Partial = {}): GuideStopDetail => ({ id: 'stop-1', version: 'standard', name: 'Guide stop', galleryUrls: [], imageStatus: 'MISSING', linkedExhibits: [{ id: 'exhibit-1', name: 'Linked exhibit', coverImageUrl: '/linked-exhibit.jpg' }], isSharedStop: false, textVariants: [ { version: 'standard', languageCode: 'zh-CN', text: '普通话正文' }, { version: 'standard', languageCode: 'en-US', text: 'English text' } ], audioTracks: [ { channelCode: 'standard.zh-CN.female', version: 'standard', languageCode: 'zh-CN', gender: 'female', playUrl: '/zh-female.mp3' }, { channelCode: 'standard.yue-HK.female', version: 'standard', languageCode: 'yue-HK', gender: 'female', playUrl: '/yue-female.mp3' } ], hasText: true, hasAudio: true, textVariantCount: 2, audioTrackCount: 2, ...overrides }) const createUseCase = (detail: GuideStopDetail) => { const explain = { getExhibitById: vi.fn().mockResolvedValue(null), listExplainExhibits: vi.fn().mockResolvedValue([]) } as unknown as ExplainRepository const audio = { getStopDetail: vi.fn().mockResolvedValue(detail) } as unknown as AudioPlayInfoRepository return { useCase: new ExplainUseCase(explain, audio), explain, audio } } describe('ExplainUseCase unified stop detail', () => { it('uses stopId once in remote mode without requesting a catalog fallback', async () => { const originalMode = dataSourceConfig.explainContentMode Object.assign(dataSourceConfig, { explainContentMode: 'remote' }) const { useCase, explain, audio } = createUseCase(createDetail()) const result = await useCase.enterExplainDetail({ exhibitId: 'stop-1', stopId: 'stop-1', hallName: '宇宙厅' }) expect(audio.getStopDetail).toHaveBeenCalledWith('stop-1', { version: 'standard' }) expect(explain.getExhibitById).not.toHaveBeenCalled() expect(result.hallName).toBe('宇宙厅') Object.assign(dataSourceConfig, { explainContentMode: originalMode }) }) it('does not use linked exhibit images when detail reports MISSING', async () => { const { useCase } = createUseCase(createDetail()) const result = await useCase.enterExplainDetail({ exhibitId: 'stop-1', stopId: 'stop-1' }) expect(result.image).toBeUndefined() expect(result.galleryUrls).toEqual([]) expect(result.linkedExhibits?.[0]?.coverImageUrl).toBe('/linked-exhibit.jpg') }) it('uses local standard text and audio variants, with Chinese text fallback for Cantonese', async () => { const { useCase, audio } = createUseCase(createDetail()) const result = await useCase.enterExplainDetail({ exhibitId: 'stop-1', stopId: 'stop-1', lang: 'yue-HK' }) const text = await useCase.loadExplainDetailText(result) const selection = await useCase.selectAudioForExplainDetail(result, { voiceGender: 'male' }) expect(result.guideText).toBe('普通话正文') expect(text.textInfo.text).toBe('普通话正文') expect(selection.media?.url).toBe('/yue-female.mp3') expect(audio.getStopDetail).toHaveBeenCalledTimes(1) }) it('uses the recommended track for the current detail version by default', async () => { const { useCase } = createUseCase(createDetail({ recommendedTrackCode: 'standard.zh-CN.female', audioTracks: [ { channelCode: 'standard.zh-CN.female', version: 'standard', languageCode: 'zh-CN', gender: 'female', playUrl: '/zh-female.mp3' }, { channelCode: 'standard.zh-CN.male', version: 'standard', languageCode: 'zh-CN', gender: 'male', playUrl: '/zh-male.mp3' } ] })) const result = await useCase.enterExplainDetail({ exhibitId: 'stop-1', stopId: 'stop-1' }) const selection = await useCase.selectAudioForExplainDetail(result) expect(selection.media?.url).toBe('/zh-female.mp3') }) })