import React from 'react' import type { Event } from 'stream-chat' import { beforeEach, describe, expect, it, vi } from 'vitest' import { renderWithProviders, screen } from '../../test/utils' import { DmAgentEnabledContext } from './DmAgentContext' const visitor = { id: 'visitor-1', name: 'Visitor' } const agent = { id: 'creator-1', name: 'Creator', image: 'agent.png' } let typingContext: { typing: Record } = { typing: {} } let aiStateContext: { aiState: string } = { aiState: 'AI_STATE_IDLE' } let channelStateContext: { channel: { state: { members: Record } } channelConfig: { typing_events: boolean } thread: undefined } = { channel: { state: { members: { [visitor.id]: { user: visitor }, [agent.id]: { user: agent }, }, }, }, channelConfig: { typing_events: true }, thread: undefined, } vi.mock('stream-chat-react', () => ({ AIStates: { Error: 'AI_STATE_ERROR', ExternalSources: 'AI_STATE_EXTERNAL_SOURCES', Generating: 'AI_STATE_GENERATING', Idle: 'AI_STATE_IDLE', Stop: 'AI_STATE_STOP', Thinking: 'AI_STATE_THINKING', }, useAIState: () => aiStateContext, useChannelStateContext: () => channelStateContext, useChatContext: () => ({ client: { user: visitor } }), useTypingContext: () => typingContext, })) vi.mock('../Avatar', () => ({ Avatar: ({ name, id }: { name: string; id: string }) => (
{name}
), })) const importIndicator = async () => (await import('.')).default const renderIndicator = async (dmAgentEnabled = true) => { const CustomTypingIndicator = await importIndicator() return renderWithProviders( ) } describe('CustomTypingIndicator', () => { beforeEach(() => { typingContext = { typing: {} } aiStateContext = { aiState: 'AI_STATE_IDLE' } channelStateContext = { channel: { state: { members: { [visitor.id]: { user: visitor }, [agent.id]: { user: agent }, }, }, }, channelConfig: { typing_events: true }, thread: undefined, } }) it('renders nothing when idle and no typers', async () => { await renderIndicator() expect(screen.queryByTestId('typing-indicator')).toBeNull() expect(screen.queryByTestId('typing-indicator-ai')).toBeNull() }) it('renders the human typing bubble when someone else is typing', async () => { typingContext = { typing: { [agent.id]: { type: 'typing.start', user: agent, parent_id: undefined, } as Event, }, } await renderIndicator() expect(screen.getByTestId('typing-indicator')).toBeInTheDocument() expect(screen.getByTestId('avatar')).toHaveAttribute('data-id', agent.id) }) it('hides the human typing bubble when typing_events is disabled', async () => { channelStateContext.channelConfig.typing_events = false typingContext = { typing: { [agent.id]: { type: 'typing.start', user: agent, parent_id: undefined, } as Event, }, } await renderIndicator() expect(screen.queryByTestId('typing-indicator')).toBeNull() }) it('renders the AI bubble when the agent is thinking', async () => { aiStateContext = { aiState: 'AI_STATE_THINKING' } await renderIndicator() expect(screen.getByTestId('typing-indicator-ai')).toBeInTheDocument() expect(screen.getByTestId('avatar')).toHaveAttribute('data-id', agent.id) }) it('renders the AI bubble when the agent is generating', async () => { aiStateContext = { aiState: 'AI_STATE_GENERATING' } await renderIndicator() expect(screen.getByTestId('typing-indicator-ai')).toBeInTheDocument() }) it('renders the AI bubble when the agent is checking external sources', async () => { aiStateContext = { aiState: 'AI_STATE_EXTERNAL_SOURCES' } await renderIndicator() expect(screen.getByTestId('typing-indicator-ai')).toBeInTheDocument() }) it('does not render the AI bubble inside a thread list', async () => { aiStateContext = { aiState: 'AI_STATE_GENERATING' } const CustomTypingIndicator = await importIndicator() renderWithProviders( ) expect(screen.queryByTestId('typing-indicator-ai')).toBeNull() }) it('renders the AI bubble even when typing_events is disabled', async () => { channelStateContext.channelConfig.typing_events = false aiStateContext = { aiState: 'AI_STATE_THINKING' } await renderIndicator() expect(screen.getByTestId('typing-indicator-ai')).toBeInTheDocument() }) it('falls back to a default avatar id when no other channel member exists', async () => { channelStateContext.channel.state.members = { [visitor.id]: { user: visitor }, } aiStateContext = { aiState: 'AI_STATE_GENERATING' } await renderIndicator() expect(screen.getByTestId('typing-indicator-ai')).toBeInTheDocument() expect(screen.getByTestId('avatar')).toHaveAttribute('data-id', 'ai-agent') }) it('does not render the AI bubble when dmAgentEnabled is false', async () => { aiStateContext = { aiState: 'AI_STATE_GENERATING' } await renderIndicator(false) expect(screen.queryByTestId('typing-indicator-ai')).toBeNull() }) it('still renders the human bubble when dmAgentEnabled is false', async () => { typingContext = { typing: { [agent.id]: { type: 'typing.start', user: agent, parent_id: undefined, } as Event, }, } await renderIndicator(false) expect(screen.getByTestId('typing-indicator')).toBeInTheDocument() }) })