@@ -1,47 +1,81 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DefaultAudioPlayInfoRepository } from '@/repositories/AudioPlayInfoRepository'
|
||||
|
||||
const stopInfoResponse = {
|
||||
const detailResponse = {
|
||||
code: 0,
|
||||
data: {
|
||||
available: true,
|
||||
targetType: 'STOP',
|
||||
targetId: 'stop-1',
|
||||
resolvedStopId: 'stop-1',
|
||||
lang: 'zh-CN',
|
||||
title: '测试讲解点',
|
||||
description: '测试简介',
|
||||
imageStatus: 'READY',
|
||||
imageSource: 'STOP',
|
||||
galleryUrls: '[]',
|
||||
playTargetType: 'STOP',
|
||||
playTargetId: 'stop-1',
|
||||
hasAudio: false,
|
||||
id: '865546647764037632',
|
||||
version: 'standard',
|
||||
name: '测试讲解点',
|
||||
imageStatus: 'MISSING',
|
||||
textVariants: [{ version: 'standard', languageCode: 'zh-CN', text: '测试正文' }],
|
||||
audioTracks: [{
|
||||
version: 'standard', languageCode: 'zh-CN', gender: 'female',
|
||||
playUrl: '/museum-assets/audio/zh-female.mp3'
|
||||
}],
|
||||
hasText: true,
|
||||
supportedLanguages: ['zh-CN'],
|
||||
audioStatus: 'MISSING',
|
||||
audioOptions: []
|
||||
hasAudio: true
|
||||
}
|
||||
}
|
||||
|
||||
describe('DefaultAudioPlayInfoRepository stop-info loading mode', () => {
|
||||
describe('DefaultAudioPlayInfoRepository unified stop detail', () => {
|
||||
afterEach(() => vi.unstubAllGlobals())
|
||||
|
||||
it('defaults H5 detail requests to lightweight mode and keeps its cache separate from legacy mode', async () => {
|
||||
it('requests the versioned endpoint and caches each detail version independently', async () => {
|
||||
const requestUrls: string[] = []
|
||||
vi.stubGlobal('uni', {
|
||||
request: ({ url, success }: { url: string, success: (response: unknown) => void }) => {
|
||||
requestUrls.push(url)
|
||||
success({ statusCode: 200, data: stopInfoResponse })
|
||||
success({
|
||||
statusCode: 200,
|
||||
data: {
|
||||
...detailResponse,
|
||||
data: {
|
||||
...detailResponse.data,
|
||||
version: String(url).includes('version=extended') ? 'extended' : 'standard'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
const repository = new DefaultAudioPlayInfoRepository()
|
||||
|
||||
await repository.getStopInfo({ targetType: 'STOP', targetId: 'stop-1', lang: 'zh-CN' })
|
||||
await repository.getStopInfo({ targetType: 'STOP', targetId: 'stop-1', lang: 'zh-CN', lightweight: false })
|
||||
const first = await repository.getStopDetail('865546647764037632')
|
||||
const second = await repository.getStopDetail('865546647764037632')
|
||||
const extended = await repository.getStopDetail('865546647764037632', { version: 'extended' })
|
||||
|
||||
expect(requestUrls).toHaveLength(2)
|
||||
expect(new URL(requestUrls[0], 'http://localhost').searchParams.get('lightweight')).toBe('true')
|
||||
expect(new URL(requestUrls[1], 'http://localhost').searchParams.get('lightweight')).toBe('false')
|
||||
expect(first.id).toBe('865546647764037632')
|
||||
expect(second).toBe(first)
|
||||
expect(extended).not.toBe(first)
|
||||
expect(extended.version).toBe('extended')
|
||||
expect(requestUrls).toEqual([
|
||||
'/app-api/gis/guide/stops/865546647764037632?version=standard',
|
||||
'/app-api/gis/guide/stops/865546647764037632?version=extended'
|
||||
])
|
||||
expect(first.audioTracks[0]?.channelCode).toBe('standard.zh-CN.female')
|
||||
})
|
||||
|
||||
it('bypasses a completed entry only when refresh is explicitly requested', async () => {
|
||||
const request = vi.fn(({ success }: { success: (response: unknown) => void }) => {
|
||||
success({ statusCode: 200, data: detailResponse })
|
||||
})
|
||||
vi.stubGlobal('uni', { request })
|
||||
const repository = new DefaultAudioPlayInfoRepository()
|
||||
|
||||
await repository.getStopDetail('stop-1')
|
||||
await repository.getStopDetail('stop-1', { refresh: true })
|
||||
|
||||
expect(request).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('surfaces a business missing-detail response as a detail error', async () => {
|
||||
vi.stubGlobal('uni', {
|
||||
request: ({ success }: { success: (response: unknown) => void }) => {
|
||||
success({ statusCode: 200, data: { code: 1020005000, msg: '导览讲解点不存在' } })
|
||||
}
|
||||
})
|
||||
const repository = new DefaultAudioPlayInfoRepository()
|
||||
|
||||
await expect(repository.getStopDetail('not-found')).rejects.toThrow('导览讲解点不存在')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,10 @@ const installRequest = (handler: (url: string) => unknown) => {
|
||||
}
|
||||
|
||||
describe('BackendExplainContentProvider hall stop paging', () => {
|
||||
afterEach(() => vi.unstubAllGlobals())
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('uses the paged hall endpoint and maps independent pages without outlines', async () => {
|
||||
const requests: string[] = []
|
||||
@@ -23,8 +26,8 @@ describe('BackendExplainContentProvider hall stop paging', () => {
|
||||
data: {
|
||||
total: 30,
|
||||
list: pageNo === '1'
|
||||
? [{ stopId: 'stop-1', name: '对象一', imageStatus: 'READY', coverImageUrl: '/one.jpg', playTargetType: 'STOP', playTargetId: 'target-1' }]
|
||||
: [{ stopId: 'stop-2', name: '对象二', imageStatus: 'MISSING', playTargetType: 'STOP', playTargetId: 'target-2' }]
|
||||
? [{ stopId: 'stop-1', name: '对象一', imageStatus: 'READY', coverImageUrl: '/one.jpg', outlineId: 'outline-1', linkedExhibits: [] }]
|
||||
: [{ stopId: 'stop-2', name: '对象二', imageStatus: 'MISSING', outlineId: 'outline-1', linkedExhibits: [] }]
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -35,7 +38,7 @@ describe('BackendExplainContentProvider hall stop paging', () => {
|
||||
])
|
||||
|
||||
expect(first).toMatchObject({ total: 30, pageNo: 1, pageSize: 20, hasMore: true })
|
||||
expect(first.items[0]).toMatchObject({ id: 'stop-1', hallName: '恐龙厅', playTargetId: 'target-1' })
|
||||
expect(first.items[0]).toMatchObject({ id: 'stop-1', stopId: 'stop-1', hallName: '恐龙厅', outlineId: 'outline-1' })
|
||||
expect(second.items.map((item) => item.id)).toEqual(['stop-2'])
|
||||
expect(requests.filter((url) => url.includes('/stops/page'))).toHaveLength(2)
|
||||
expect(requests.some((url) => url.includes('/outlines'))).toBe(false)
|
||||
@@ -79,4 +82,25 @@ describe('BackendExplainContentProvider hall stop paging', () => {
|
||||
expect(requests.filter((url) => url.includes('/catalog/halls?'))).toHaveLength(1)
|
||||
expect(requests.filter((url) => url.includes('/stops/page'))).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('warns when a hall has guide stops but every compatibility outline reports zero', async () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||||
installRequest((url) => {
|
||||
if (url.includes('/catalog/halls?')) {
|
||||
return { code: 0, data: [{ id: 'hall-1', name: '恐龙厅', stopCount: 2 }] }
|
||||
}
|
||||
if (url.includes('/catalog/halls/hall-1/outlines?')) {
|
||||
return { code: 0, data: [{ id: 'outline-1', name: '单元一', hallId: 'hall-1', stopCount: 0 }] }
|
||||
}
|
||||
throw new Error(`Unexpected request: ${url}`)
|
||||
})
|
||||
|
||||
const units = await new BackendExplainContentProvider().listTemporaryBusinessUnitsByHall('hall-1')
|
||||
|
||||
expect(units).toHaveLength(1)
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
'一级单元与讲解点关联异常,一级单元仅作为兼容入口:',
|
||||
expect.objectContaining({ hallId: 'hall-1', hallStopCount: 2 })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -295,7 +295,7 @@ describe('讲解详情音频优先布局', () => {
|
||||
expect(wrapper.find('.detail-audio-status').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('播放失败显示错误并通过既有刷新链路重试', async () => {
|
||||
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 = '音频暂时无法播放'
|
||||
@@ -316,7 +316,10 @@ describe('讲解详情音频优先布局', () => {
|
||||
|
||||
await wrapper.get('.detail-audio-play').trigger('tap')
|
||||
await flushPromises()
|
||||
expect(mocks.selectAudioForExplainDetail).toHaveBeenCalledWith(expect.any(Object), { refreshPlayInfo: true })
|
||||
expect(mocks.selectAudioForExplainDetail).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: exhibit.id,
|
||||
guideVersion: 'standard'
|
||||
}))
|
||||
expect(mocks.play).toHaveBeenCalledWith(expect.objectContaining({ id: 'audio-refresh' }), expect.objectContaining({
|
||||
retryOnError: expect.any(Function),
|
||||
displayMode: 'mini'
|
||||
@@ -349,13 +352,13 @@ describe('讲解详情音频优先布局', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.get('.detail-audio-speed').text()).toBe('1.0')
|
||||
expect(wrapper.get('.detail-audio-voice').attributes('aria-label')).toBe('切换为女声讲解')
|
||||
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(wrapper.get('.detail-audio-voice').attributes('aria-label')).toBe('切换为女声讲解')
|
||||
expect(mocks.toggleMute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -407,9 +410,6 @@ describe('讲解详情音频优先布局', () => {
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
vi.mocked(uni.navigateBack).mockImplementationOnce((options: any) => {
|
||||
options.fail?.()
|
||||
})
|
||||
wrapper.getComponent(GuidePageFrameStub).vm.$emit('back')
|
||||
await flushPromises()
|
||||
|
||||
@@ -418,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).toHaveBeenCalledWith(expect.objectContaining({ delta: 1 }))
|
||||
expect(uni.navigateBack).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('ExplainGuideStopCatalog', () => {
|
||||
expect(wrapper.findAll('.stop-status')).toHaveLength(3)
|
||||
expect(wrapper.text()).toContain('讲解')
|
||||
expect(wrapper.text()).toContain('图文')
|
||||
expect(wrapper.text()).toContain('暂无内容')
|
||||
expect(wrapper.text()).not.toContain('暂无内容')
|
||||
expect(wrapper.find('.hall-arrow').exists()).toBe(false)
|
||||
expect(wrapper.find('.stop-name').classes()).toContain('stop-name')
|
||||
})
|
||||
|
||||
@@ -110,8 +110,7 @@ describe('讲解对象列表返回', () => {
|
||||
expect(url.split('?')[0]).toBe('/pages/exhibit/detail')
|
||||
expect(params.get('hallId')).toBe('hall-1')
|
||||
expect(params.get('hallName')).toBe('恐龙厅')
|
||||
expect(params.get('targetType')).toBe('STOP')
|
||||
expect(params.get('targetId')).toBe('target-1')
|
||||
expect(params.get('stopId')).toBe('stop-1')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,102 +1,89 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { GuideStopInfo } from '@/data/adapters/guideStopInfoAdapter'
|
||||
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 createStopInfo = (overrides: Partial<GuideStopInfo> = {}): GuideStopInfo => ({
|
||||
available: true,
|
||||
targetType: 'STOP',
|
||||
targetId: 'stop-1',
|
||||
lang: 'zh-CN',
|
||||
title: 'Guide stop',
|
||||
const createDetail = (overrides: Partial<GuideStopDetail> = {}): GuideStopDetail => ({
|
||||
id: 'stop-1',
|
||||
version: 'standard',
|
||||
name: 'Guide stop',
|
||||
galleryUrls: [],
|
||||
imageStatus: 'MISSING',
|
||||
linkedExhibits: [
|
||||
{
|
||||
id: 'exhibit-1',
|
||||
name: 'Linked exhibit',
|
||||
coverImageUrl: '/linked-exhibit.jpg'
|
||||
}
|
||||
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' }
|
||||
],
|
||||
playTargetType: 'STOP',
|
||||
playTargetId: 'stop-1',
|
||||
hasAudio: false,
|
||||
hasText: false,
|
||||
supportedLanguages: [],
|
||||
audioStatus: 'MISSING',
|
||||
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 = (stopInfo: GuideStopInfo) => {
|
||||
const createUseCase = (detail: GuideStopDetail) => {
|
||||
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)
|
||||
const audio = { getStopDetail: vi.fn().mockResolvedValue(detail) } as unknown as AudioPlayInfoRepository
|
||||
return { useCase: new ExplainUseCase(explain, audio), explain, audio }
|
||||
}
|
||||
|
||||
describe('ExplainUseCase stop image policy', () => {
|
||||
it('uses stop-info first in remote mode without requesting a catalog fallback', async () => {
|
||||
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 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
|
||||
const { useCase, explain, audio } = createUseCase(createDetail())
|
||||
|
||||
await new ExplainUseCase(explain, audio).enterExplainDetail({
|
||||
exhibitId: 'stop-1', targetType: 'STOP', targetId: 'stop-1', hallName: '宇宙厅'
|
||||
})
|
||||
const result = await useCase.enterExplainDetail({ exhibitId: 'stop-1', stopId: 'stop-1', hallName: '宇宙厅' })
|
||||
|
||||
expect(audio.getStopInfo).toHaveBeenCalledTimes(1)
|
||||
expect(audio.getStopDetail).toHaveBeenCalledWith('stop-1', { version: 'standard' })
|
||||
expect(explain.getExhibitById).not.toHaveBeenCalled()
|
||||
expect(explain.listExplainExhibits).not.toHaveBeenCalled()
|
||||
expect(result.hallName).toBe('宇宙厅')
|
||||
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'
|
||||
})
|
||||
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(detail.image).toBeUndefined()
|
||||
expect(detail.galleryUrls).toEqual([])
|
||||
expect(detail.linkedExhibits?.[0]?.coverImageUrl).toBe('/linked-exhibit.jpg')
|
||||
expect(result.image).toBeUndefined()
|
||||
expect(result.galleryUrls).toEqual([])
|
||||
expect(result.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'
|
||||
})
|
||||
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(detail.image).toBe('/guide-stop.jpg')
|
||||
expect(detail.galleryUrls).toEqual(['/guide-stop-gallery.jpg'])
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const repositoryMocks = vi.hoisted(() => ({
|
||||
getPlayInfo: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/repositories/AudioPlayInfoRepository', () => ({
|
||||
audioPlayInfoRepository: {
|
||||
getPlayInfo: repositoryMocks.getPlayInfo
|
||||
},
|
||||
audioReasonToText: () => '当前语言暂无语音讲解'
|
||||
}))
|
||||
|
||||
import { useGlobalAudioPlayer } from '@/composables/useGlobalAudioPlayer'
|
||||
|
||||
describe('全局讲解播放器语言切换', () => {
|
||||
@@ -20,10 +8,7 @@ describe('全局讲解播放器语言切换', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
player.close()
|
||||
repositoryMocks.getPlayInfo.mockReset()
|
||||
vi.stubGlobal('uni', {
|
||||
showToast: vi.fn()
|
||||
})
|
||||
vi.stubGlobal('uni', { showToast: vi.fn() })
|
||||
vi.spyOn(window.HTMLMediaElement.prototype, 'play').mockResolvedValue(undefined)
|
||||
vi.spyOn(window.HTMLMediaElement.prototype, 'pause').mockImplementation(() => undefined)
|
||||
vi.spyOn(window.HTMLMediaElement.prototype, 'load').mockImplementation(() => undefined)
|
||||
@@ -35,18 +20,9 @@ describe('全局讲解播放器语言切换', () => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('切换到粤语时立即中断旧音频,并使用 yue-HK 播放地址', async () => {
|
||||
repositoryMocks.getPlayInfo.mockResolvedValue({
|
||||
playable: true,
|
||||
targetType: 'STOP',
|
||||
targetId: '1823450596800289',
|
||||
lang: 'yue-HK',
|
||||
audioId: 'cantonese-audio',
|
||||
playUrl: '/museum-assets/audio/cantonese.mp3',
|
||||
hasText: true,
|
||||
fallback: false
|
||||
})
|
||||
|
||||
it('切换到粤语时只从当前统一详情的音轨数组选择,不产生额外 HTTP 请求', async () => {
|
||||
const request = vi.fn()
|
||||
vi.stubGlobal('uni', { request, showToast: vi.fn() })
|
||||
await player.play({
|
||||
id: 'mandarin-audio',
|
||||
name: '测试讲解',
|
||||
@@ -54,28 +30,25 @@ describe('全局讲解播放器语言切换', () => {
|
||||
language: 'zh-CN'
|
||||
}, {
|
||||
source: {
|
||||
targetType: 'STOP',
|
||||
targetId: '1823450596800289',
|
||||
lang: 'zh-CN'
|
||||
stopId: '1823450596800289',
|
||||
lang: 'zh-CN',
|
||||
voiceGender: 'male',
|
||||
audioOptions: [
|
||||
{ channelCode: 'standard.zh-CN.male', version: 'standard', languageCode: 'zh-CN', gender: 'male', playUrl: '/museum-assets/audio/mandarin.mp3' },
|
||||
{ channelCode: 'standard.yue-HK.female', version: 'standard', languageCode: 'yue-HK', gender: 'female', playUrl: '/museum-assets/audio/cantonese.mp3' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const pauseSpy = vi.mocked(window.HTMLMediaElement.prototype.pause)
|
||||
pauseSpy.mockClear()
|
||||
const switching = player.switchLanguage('yue-HK')
|
||||
|
||||
expect(pauseSpy).toHaveBeenCalledTimes(1)
|
||||
expect(player.playing.value).toBe(false)
|
||||
expect(player.currentTime.value).toBe(0)
|
||||
expect(repositoryMocks.getPlayInfo).toHaveBeenCalledWith(expect.objectContaining({
|
||||
targetType: 'STOP',
|
||||
targetId: '1823450596800289',
|
||||
lang: 'yue-HK',
|
||||
refresh: true
|
||||
}))
|
||||
|
||||
await expect(switching).resolves.toBe(true)
|
||||
expect(player.currentSource.value?.lang).toBe('yue-HK')
|
||||
expect(request).not.toHaveBeenCalled()
|
||||
expect(player.currentSource.value).toEqual(expect.objectContaining({
|
||||
stopId: '1823450596800289',
|
||||
lang: 'yue-HK',
|
||||
voiceGender: 'female'
|
||||
}))
|
||||
expect(player.currentAudio.value).toEqual(expect.objectContaining({
|
||||
language: 'yue-HK',
|
||||
audioUrl: '/museum-assets/audio/cantonese.mp3'
|
||||
|
||||
@@ -1,137 +1,67 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toGuideStopInfo } from '@/data/adapters/guideStopInfoAdapter'
|
||||
import { toGuideStopDetail } from '@/data/adapters/guideStopInfoAdapter'
|
||||
|
||||
describe('guide stop-info adapter', () => {
|
||||
it('normalizes aggregated language variants and exposes only displayable languages', () => {
|
||||
const result = toGuideStopInfo({
|
||||
available: true,
|
||||
targetType: 'STOP',
|
||||
targetId: '9001',
|
||||
lang: 'zh-CN',
|
||||
const languages = ['zh-CN', 'en-US', 'yue-HK'] as const
|
||||
|
||||
describe('unified guide stop detail adapter', () => {
|
||||
it('preserves snowflake IDs, synthesizes stable track codes, and ignores images marked missing', () => {
|
||||
const textVariants = languages.map((languageCode) => ({
|
||||
version: 'standard',
|
||||
languageCode,
|
||||
text: `standard-${languageCode}`
|
||||
}))
|
||||
const audioTracks = languages.flatMap((languageCode) => (
|
||||
languageCode === 'yue-HK'
|
||||
? [{ version: 'standard', languageCode, gender: 'female', playUrl: `/standard-${languageCode}-female.mp3` }]
|
||||
: ['female', 'male'].map((gender) => ({ version: 'standard', languageCode, gender, playUrl: `/standard-${languageCode}-${gender}.mp3` }))
|
||||
))
|
||||
const detail = toGuideStopDetail({
|
||||
id: '865546647764037632',
|
||||
version: 'standard',
|
||||
outlineId: '7467940240901013505',
|
||||
linkedExhibits: [{ id: '865546647764037633', name: '关联展品' }],
|
||||
name: '青铜神树',
|
||||
coverImageUrl: '/should-not-be-used.webp',
|
||||
galleryUrls: '["/should-not-be-used-gallery.webp"]',
|
||||
imageStatus: 'MISSING',
|
||||
recommendedTrackCode: 'standard.zh-CN.female',
|
||||
textVariants,
|
||||
audioTracks,
|
||||
textVariantCount: 3,
|
||||
audioTrackCount: 5
|
||||
})
|
||||
|
||||
expect(detail.id).toBe('865546647764037632')
|
||||
expect(detail.version).toBe('standard')
|
||||
expect(detail.outlineId).toBe('7467940240901013505')
|
||||
expect(detail.linkedExhibits[0]?.id).toBe('865546647764037633')
|
||||
expect(detail.coverImageUrl).toBeUndefined()
|
||||
expect(detail.galleryUrls).toEqual([])
|
||||
expect(detail.textVariants).toHaveLength(3)
|
||||
expect(detail.audioTracks).toHaveLength(5)
|
||||
expect(detail.recommendedTrackCode).toBe('standard.zh-CN.female')
|
||||
expect(detail.audioTracks.find((track) => track.channelCode === 'standard.zh-CN.female')?.playUrl)
|
||||
.toBe('/standard-zh-CN-female.mp3')
|
||||
})
|
||||
|
||||
it('drops unusable tracks while retaining a Chinese text variant for Cantonese fallback', () => {
|
||||
const detail = toGuideStopDetail({
|
||||
id: 'stop-1',
|
||||
version: 'extended',
|
||||
name: '测试',
|
||||
imageStatus: 'READY',
|
||||
coverImageUrl: '/museum-assets/cover.webp',
|
||||
languageVariants: [
|
||||
{
|
||||
lang: 'zh',
|
||||
enabled: true,
|
||||
playable: true,
|
||||
audioStatus: 'READY',
|
||||
playUrl: '/museum-assets/audio/zh.mp3',
|
||||
duration: '50',
|
||||
audioId: 12,
|
||||
hasText: true,
|
||||
textAvailable: true,
|
||||
text: '普通话讲解词',
|
||||
textLength: '6',
|
||||
textHash: 'zh-hash'
|
||||
},
|
||||
{
|
||||
lang: 'yue-HK',
|
||||
enabled: true,
|
||||
playable: false,
|
||||
audioStatus: 'MISSING',
|
||||
textAvailable: true,
|
||||
text: '粤语使用普通话文案',
|
||||
reason: 'NO_PUBLISHED_AUDIO'
|
||||
},
|
||||
{
|
||||
lang: 'en-US',
|
||||
enabled: false,
|
||||
playable: true,
|
||||
playUrl: '/museum-assets/audio/en.mp3'
|
||||
}
|
||||
textVariants: [{ version: 'extended', languageCode: 'zh-CN', text: '普通话正文' }],
|
||||
audioTracks: [
|
||||
{ version: 'extended', languageCode: 'yue-HK', gender: 'female', playUrl: '/cantonese.mp3' },
|
||||
{ version: 'extended', languageCode: 'zh-CN', gender: 'female' }
|
||||
]
|
||||
}, {
|
||||
targetType: 'STOP',
|
||||
targetId: '9001',
|
||||
lang: 'zh-CN'
|
||||
})
|
||||
|
||||
expect(result.supportedLanguages).toEqual(['zh-CN', 'yue-HK'])
|
||||
expect(result.languageVariants['zh-CN']).toMatchObject({
|
||||
playable: true,
|
||||
playUrl: '/museum-assets/audio/zh.mp3',
|
||||
duration: 50,
|
||||
audioId: '12',
|
||||
text: '普通话讲解词',
|
||||
textLength: 6
|
||||
})
|
||||
expect(result.languageVariants['yue-HK']).toMatchObject({
|
||||
playable: false,
|
||||
textAvailable: true,
|
||||
reason: 'NO_PUBLISHED_AUDIO'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps supportedLanguages as the compatibility fallback when variants are absent', () => {
|
||||
const result = toGuideStopInfo({
|
||||
supportedLanguages: ['zh-CN', 'en-US']
|
||||
}, {
|
||||
targetType: 'STOP',
|
||||
targetId: '9001',
|
||||
lang: 'zh-CN'
|
||||
})
|
||||
|
||||
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
|
||||
})
|
||||
expect(detail.version).toBe('extended')
|
||||
expect(detail.textVariants[0]?.text).toBe('普通话正文')
|
||||
expect(detail.audioTracks).toEqual([expect.objectContaining({
|
||||
channelCode: 'extended.yue-HK.female',
|
||||
gender: 'female'
|
||||
})])
|
||||
})
|
||||
})
|
||||
|
||||
37
tests/unit/PoiDetailUseCase.spec.ts
Normal file
37
tests/unit/PoiDetailUseCase.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { explainUseCase } from '@/usecases/explainUseCase'
|
||||
import { PoiDetailUseCase } from '@/usecases/poiDetailUseCase'
|
||||
|
||||
vi.mock('@/usecases/explainUseCase', () => ({
|
||||
explainUseCase: {
|
||||
getExhibitById: vi.fn(),
|
||||
listHalls: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
describe('PoiDetailUseCase', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(explainUseCase.getExhibitById).mockReset()
|
||||
})
|
||||
|
||||
it('resolves a POI exhibit reference to a guide stop before opening the unified detail route', async () => {
|
||||
vi.mocked(explainUseCase.getExhibitById).mockResolvedValue({
|
||||
id: 'stop-865546647764037632',
|
||||
resolvedStopId: '865546647764037632'
|
||||
} as never)
|
||||
|
||||
const target = await new PoiDetailUseCase().resolve({
|
||||
id: 'poi-1',
|
||||
name: '青铜神树',
|
||||
floorId: 'floor-1',
|
||||
exhibitId: 'exhibit-1'
|
||||
})
|
||||
|
||||
const params = new URLSearchParams(target.url.split('?')[1])
|
||||
expect(explainUseCase.getExhibitById).toHaveBeenCalledWith('exhibit-1')
|
||||
expect(target.detailId).toBe('865546647764037632')
|
||||
expect(target.stopId).toBe('865546647764037632')
|
||||
expect(params.get('id')).toBe('exhibit-1')
|
||||
expect(params.get('stopId')).toBe('865546647764037632')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user