dify/web/app/components/base/audio-btn/__tests__/index.spec.tsx

203 lines
6.4 KiB
TypeScript
Raw Normal View History

import { act, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import i18next from 'i18next'
import { useParams, usePathname } from 'next/navigation'
import AudioBtn from '../index'
const mockPlayAudio = vi.fn()
const mockPauseAudio = vi.fn()
const mockGetAudioPlayer = vi.fn()
vi.mock('next/navigation', () => ({
useParams: vi.fn(),
usePathname: vi.fn(),
}))
vi.mock('@/app/components/base/audio-btn/audio.player.manager', () => ({
AudioPlayerManager: {
getInstance: vi.fn(() => ({
getAudioPlayer: mockGetAudioPlayer,
})),
},
}))
describe('AudioBtn', () => {
const getButton = () => screen.getByRole('button')
const mockUseParams = (value: Partial<Record<string, string>>) => {
vi.mocked(useParams).mockReturnValue(value as ReturnType<typeof useParams>)
}
const mockUsePathname = (value: string) => {
vi.mocked(usePathname).mockReturnValue(value)
}
const hoverAndCheckTooltip = async (expectedText: string) => {
await userEvent.hover(getButton())
expect(await screen.findByText(expectedText)).toBeInTheDocument()
}
const getLatestAudioCallback = () => {
const lastCall = mockGetAudioPlayer.mock.calls[mockGetAudioPlayer.mock.calls.length - 1]
const callback = lastCall?.[5]
if (typeof callback !== 'function')
throw new Error('Audio callback not found in latest getAudioPlayer call')
return callback as (event: string) => void
}
beforeAll(async () => {
await i18next.init({})
})
beforeEach(() => {
vi.clearAllMocks()
mockGetAudioPlayer.mockReturnValue({
playAudio: mockPlayAudio,
pauseAudio: mockPauseAudio,
})
mockUseParams({})
mockUsePathname('/')
})
// Core rendering and base UI integration.
describe('Rendering', () => {
it('should render button with play tooltip by default', async () => {
render(<AudioBtn value="hello" />)
expect(getButton()).toBeInTheDocument()
expect(getButton()).not.toBeDisabled()
await hoverAndCheckTooltip('play')
})
it('should apply className in initial state', () => {
const { container } = render(<AudioBtn value="hello" className="custom-wrapper" />)
const wrapper = container.firstElementChild
expect(wrapper).toHaveClass('custom-wrapper')
})
})
// URL path resolution for app/public audio endpoints.
describe('URL routing', () => {
it('should call public text-to-audio endpoint when token exists', async () => {
mockUseParams({ token: 'public-token' })
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
const call = mockGetAudioPlayer.mock.calls[0]
expect(call[0]).toBe('/text-to-audio')
expect(call[1]).toBe(true)
})
it('should call app endpoint when appId exists', async () => {
mockUseParams({ appId: '123' })
mockUsePathname('/apps/123/chat')
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
const call = mockGetAudioPlayer.mock.calls[0]
expect(call[0]).toBe('/apps/123/text-to-audio')
expect(call[1]).toBe(false)
})
it('should call installed app endpoint for explore installed routes', async () => {
mockUseParams({ appId: '456' })
mockUsePathname('/explore/installed/app/456')
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
const call = mockGetAudioPlayer.mock.calls[0]
expect(call[0]).toBe('/installed-apps/456/text-to-audio')
expect(call[1]).toBe(false)
})
})
// User-visible playback state transitions.
describe('Playback interactions', () => {
it('should start loading and call playAudio when button is clicked', async () => {
render(<AudioBtn value="test" className="custom-wrapper" />)
await userEvent.click(getButton())
await waitFor(() => {
expect(mockPlayAudio).toHaveBeenCalledTimes(1)
expect(getButton()).toBeDisabled()
})
expect(screen.getByRole('status')).toBeInTheDocument()
await hoverAndCheckTooltip('loading')
})
it('should pause audio when clicked while playing', async () => {
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
await act(() => {
getLatestAudioCallback()('play')
})
await hoverAndCheckTooltip('playing')
expect(getButton()).not.toBeDisabled()
await userEvent.click(getButton())
await waitFor(() => expect(mockPauseAudio).toHaveBeenCalledTimes(1))
})
})
// Audio event callback handling from the player manager.
describe('Audio callback events', () => {
it('should set loading tooltip when loaded event is received', async () => {
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
await act(() => {
getLatestAudioCallback()('loaded')
})
await hoverAndCheckTooltip('loading')
expect(getButton()).toBeDisabled()
})
it.each(['ended', 'paused', 'error'])('should return to play tooltip when %s event is received', async (event) => {
render(<AudioBtn value="test" />)
await userEvent.click(getButton())
await act(() => {
getLatestAudioCallback()(event)
})
await hoverAndCheckTooltip('play')
expect(getButton()).not.toBeDisabled()
})
})
// Prop forwarding and minimal-input behavior.
describe('Props and edge cases', () => {
it('should pass id, value, and voice to getAudioPlayer', async () => {
render(<AudioBtn id="msg-1" value="hello" voice="en-US" />)
await userEvent.click(getButton())
await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
const call = mockGetAudioPlayer.mock.calls[0]
expect(call[2]).toBe('msg-1')
expect(call[3]).toBe('hello')
expect(call[4]).toBe('en-US')
})
it('should keep empty route when neither token nor appId is present', async () => {
render(<AudioBtn />)
await userEvent.click(getButton())
await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled())
const call = mockGetAudioPlayer.mock.calls[0]
expect(call[0]).toBe('')
expect(call[1]).toBe(false)
expect(call[3]).toBeUndefined()
})
})
})