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>) => { vi.mocked(useParams).mockReturnValue(value as ReturnType) } 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() expect(getButton()).toBeInTheDocument() expect(getButton()).not.toBeDisabled() await hoverAndCheckTooltip('play') }) it('should apply className in initial state', () => { const { container } = render() 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() 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() 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() 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() 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() 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() 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() 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() 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() 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() }) }) })