import React from 'react' import { useChannelStateContext, useMessageContext } from 'stream-chat-react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { renderWithProviders, screen, within } from '../../test/utils' import { SentMessageDeliveryStatus } from './SentMessageDeliveryStatus' // stream-chat-react's `MessageStatus` owns own-message / not-error gating and // only exposes a terminal state once a message is stored — Stream's tested // code. Its threadList guard skips `sending`, so this component owns // thread-list suppression plus the "Delivered"/"Sending" labels and the // "latest message in the thread" gate from live channel state. Mock // `MessageStatus` to render the slots it was handed, and the context hooks // the label reads. vi.mock('stream-chat-react', () => ({ useMessageContext: vi.fn(), useChannelStateContext: vi.fn(), MessageStatus: ({ MessageSentStatus, MessageDeliveredStatus, MessageReadStatus, MessageSendingStatus, }: Record) => (
), })) const mockedUseMessageContext = vi.mocked(useMessageContext) const mockedUseChannelStateContext = vi.mocked(useChannelStateContext) // A run where the viewer ('me') sent the first and last messages, with an // incoming message in between — the label must sit on the last message ('own-2') // and clear from the earlier one. const CHANNEL_MESSAGES = [ { id: 'own-1', user: { id: 'me' } }, { id: 'their-1', user: { id: 'them' } }, { id: 'own-2', user: { id: 'me' } }, ] const TERMINAL_SLOTS = ['sent-slot', 'delivered-slot', 'read-slot'] as const describe('SentMessageDeliveryStatus', () => { beforeEach(() => { mockedUseChannelStateContext.mockReturnValue({ messages: CHANNEL_MESSAGES, } as never) }) it('renders "Delivered" in every terminal slot for the last message in the thread', () => { // delivered/read fire ahead of sent in receipt channels, so all three // must show the label — never Stream's default delivered icon / read // avatars. mockedUseMessageContext.mockReturnValue({ message: { id: 'own-2' }, isMyMessage: () => true, } as never) renderWithProviders() for (const slot of TERMINAL_SLOTS) { expect( within(screen.getByTestId(slot)).getByText('Delivered') ).toBeInTheDocument() } }) it('renders nothing for an earlier own message once a later message exists', () => { mockedUseMessageContext.mockReturnValue({ message: { id: 'own-1' }, isMyMessage: () => true, } as never) renderWithProviders() for (const slot of TERMINAL_SLOTS) { expect(screen.getByTestId(slot)).toBeEmptyDOMElement() } }) it('renders nothing for an own message once a reply arrives after it', () => { // 'own-2' was the viewer's last sent message, but 'their-1' is now the // latest message in the thread — the label must clear from the bubble. mockedUseChannelStateContext.mockReturnValue({ messages: [ { id: 'own-1', user: { id: 'me' } }, { id: 'own-2', user: { id: 'me' } }, { id: 'their-1', user: { id: 'them' } }, ], } as never) mockedUseMessageContext.mockReturnValue({ message: { id: 'own-2' }, isMyMessage: () => true, } as never) renderWithProviders() for (const slot of TERMINAL_SLOTS) { expect(screen.getByTestId(slot)).toBeEmptyDOMElement() } }) it('ignores a trailing system row when the last real message is still the viewer’s own', () => { mockedUseChannelStateContext.mockReturnValue({ messages: [ { id: 'own-1', user: { id: 'me' } }, { id: 'their-1', user: { id: 'them' } }, { id: 'own-2', user: { id: 'me' } }, { id: 'system-1', type: 'system' }, ], } as never) mockedUseMessageContext.mockReturnValue({ message: { id: 'own-2' }, isMyMessage: () => true, } as never) renderWithProviders() for (const slot of TERMINAL_SLOTS) { expect( within(screen.getByTestId(slot)).getByText('Delivered') ).toBeInTheDocument() } }) it('renders "Sending" in the sending slot for the latest message', () => { mockedUseMessageContext.mockReturnValue({ message: { id: 'own-2', status: 'sending' }, isMyMessage: () => true, threadList: false, } as never) renderWithProviders() expect(screen.getByTestId('sending-slot')).toHaveTextContent(/sending/i) }) it('does not render "Sending" for an earlier message', () => { mockedUseMessageContext.mockReturnValue({ message: { id: 'own-1', status: 'sending' }, isMyMessage: () => true, threadList: false, } as never) renderWithProviders() expect(screen.getByTestId('sending-slot')).toBeEmptyDOMElement() }) it('does not render "Sending" inside a thread list', () => { // Stream's MessageStatus still invokes MessageSendingStatus when // threadList is true; we suppress the stamp ourselves. mockedUseMessageContext.mockReturnValue({ message: { id: 'own-2', status: 'sending' }, isMyMessage: () => true, threadList: true, } as never) renderWithProviders() expect(screen.queryByTestId('sending-slot')).not.toBeInTheDocument() expect( screen.queryByTestId('sent-message-sending-status') ).not.toBeInTheDocument() }) it('renders an outbound label for every own outbound message, including non-latest messages', () => { mockedUseMessageContext.mockReturnValue({ message: { id: 'own-1', metadata: { outbound_id: 'outbound-1', }, }, isMyMessage: () => true, threadList: false, } as never) renderWithProviders( ) expect( screen.getByTestId('sent-message-outbound-status') ).toHaveTextContent('July 9th Class attendees') expect( screen.queryByTestId('sent-message-delivery-status') ).not.toBeInTheDocument() expect(screen.queryByText('•')).not.toBeInTheDocument() }) it('uses a generic label and invokes the outbound callback when the name is missing', () => { const onOutboundClick = vi.fn() mockedUseMessageContext.mockReturnValue({ message: { id: 'own-2', metadata: { outbound_id: 'outbound-1' } }, isMyMessage: () => true, threadList: false, } as never) renderWithProviders( ) expect( screen.getByTestId('sent-message-outbound-status') ).toHaveTextContent('Broadcast') expect( screen.getByRole('button', { name: 'View broadcast Broadcast' }) ).toBeInTheDocument() const button = screen.getByRole('button', { name: 'View broadcast Broadcast', }) expect( button.querySelector('.sent-message-outbound-badge') ).toBeInTheDocument() expect( button.querySelector('.sent-message-outbound-name') ).toHaveTextContent('Broadcast') expect(button).not.toHaveTextContent('•') expect(screen.getAllByText('•')).toHaveLength(1) button.click() expect(onOutboundClick).toHaveBeenCalledWith({ id: 'automation-1' }) }) it('keeps the latest separator outside the button and underlines only the name', () => { const onOutboundClick = vi.fn() mockedUseMessageContext.mockReturnValue({ message: { id: 'own-2', metadata: { outbound_id: 'outbound-1', }, }, isMyMessage: () => true, threadList: false, } as never) renderWithProviders( ) const button = screen.getByRole('button', { name: 'View broadcast July 9th Class attendees', }) const name = within(button).getByTestId('sent-message-outbound-status') expect(screen.getAllByText('•')).toHaveLength(1) expect(button).not.toHaveTextContent('•') expect(name).toHaveClass('sent-message-outbound-name') expect( button.querySelector('.sent-message-outbound-badge') ).toHaveAttribute('aria-hidden', 'true') expect(screen.getAllByTestId('sent-message-delivery-status')).toHaveLength( 3 ) }) it('passes the resolved outbound name to the click callback', () => { const onOutboundClick = vi.fn() mockedUseMessageContext.mockReturnValue({ message: { id: 'own-2', metadata: { outbound_id: 'outbound-1', }, }, isMyMessage: () => true, threadList: false, } as never) renderWithProviders( ) screen .getByRole('button', { name: 'View broadcast July 9th Class absentee', }) .click() expect(onOutboundClick).toHaveBeenCalledWith({ id: 'automation-1', name: 'July 9th Class absentee', }) }) it('reports a campaign named Broadcast on click', () => { const onOutboundClick = vi.fn() mockedUseMessageContext.mockReturnValue({ message: { id: 'own-3', metadata: { outbound_id: 'outbound-1', }, }, isMyMessage: () => true, threadList: false, } as never) renderWithProviders( ) screen .getByRole('button', { name: 'View broadcast Broadcast', }) .click() expect(onOutboundClick).toHaveBeenCalledWith({ id: 'automation-1', name: 'Broadcast', }) }) it('does not render an outbound label for a visitor viewing an outbound message', () => { mockedUseMessageContext.mockReturnValue({ message: { id: 'automation-1', metadata: { outbound_id: 'outbound-1', }, }, isMyMessage: () => false, threadList: false, } as never) renderWithProviders( ) expect( screen.queryByTestId('sent-message-outbound-status') ).not.toBeInTheDocument() }) it('renders "Failed to send" for every own failed message', () => { mockedUseMessageContext.mockReturnValue({ message: { id: 'own-1', status: 'failed', type: 'regular' }, isMyMessage: () => true, threadList: false, } as never) renderWithProviders() expect(screen.getByTestId('sent-message-failed-status')).toHaveTextContent( /failed to send/i ) expect(screen.queryByTestId('sending-slot')).not.toBeInTheDocument() }) it.each([ ['a paid message with text', 'thanks!'], ['a textless paid message', undefined], ])('does not render the delivery stamp on %s', (_label, text) => { mockedUseMessageContext.mockReturnValue({ message: { id: 'own-2', text, metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' }, }, isMyMessage: () => true, threadList: false, } as never) renderWithProviders() for (const slot of TERMINAL_SLOTS) { expect(screen.queryByTestId(slot)).not.toBeInTheDocument() } expect(screen.queryByTestId('sending-slot')).not.toBeInTheDocument() }) it('keeps the delivery stamp on a tip, which renders a pill instead of a footer tag', () => { mockedUseMessageContext.mockReturnValue({ message: { id: 'own-2', text: 'thanks!', metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.00' }, }, isMyMessage: () => true, threadList: false, } as never) renderWithProviders() for (const slot of TERMINAL_SLOTS) { expect(screen.getByTestId(slot)).toHaveTextContent(/delivered/i) } }) it('does not render the sending stamp on a paid message', () => { mockedUseMessageContext.mockReturnValue({ message: { id: 'own-2', status: 'sending', text: 'thanks!', metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' }, }, isMyMessage: () => true, threadList: false, } as never) renderWithProviders() expect(screen.queryByTestId('sending-slot')).not.toBeInTheDocument() expect( screen.queryByTestId('sent-message-sending-status') ).not.toBeInTheDocument() }) it('renders "Delivered" instead of "Sending" on a pending textless tip', () => { mockedUseMessageContext.mockReturnValue({ message: { id: 'own-2', status: 'sending', metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.00' }, }, isMyMessage: () => true, threadList: false, } as never) renderWithProviders() expect(screen.getByTestId('sending-slot')).toHaveTextContent(/delivered/i) expect( screen.queryByTestId('sent-message-sending-status') ).not.toBeInTheDocument() }) it('keeps "Failed to send" on a tip that fails', () => { mockedUseMessageContext.mockReturnValue({ message: { id: 'own-2', status: 'failed', type: 'regular', metadata: { custom_type: 'MESSAGE_TIP', amount_text: '$5.00' }, }, isMyMessage: () => true, threadList: false, } as never) renderWithProviders() expect(screen.getByTestId('sent-message-failed-status')).toHaveTextContent( /failed to send/i ) }) it('keeps the failed and outbound stamps on a paid message', () => { mockedUseMessageContext.mockReturnValue({ message: { id: 'own-2', status: 'failed', type: 'regular', text: 'thanks!', metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00' }, }, isMyMessage: () => true, threadList: false, } as never) const { rerender } = renderWithProviders() expect(screen.getByTestId('sent-message-failed-status')).toHaveTextContent( /failed to send/i ) mockedUseMessageContext.mockReturnValue({ message: { id: 'own-2', text: 'thanks!', metadata: { custom_type: 'MESSAGE_PAID', amount_text: '$10.00', outbound_id: 'outbound-1', }, }, isMyMessage: () => true, threadList: false, } as never) rerender( ) expect( screen.getByTestId('sent-message-outbound-status') ).toHaveTextContent('July 9th Class') expect(screen.queryByText('•')).not.toBeInTheDocument() }) it('does not render failure status for a received message or thread message', () => { mockedUseMessageContext.mockReturnValue({ message: { id: 'own-1', status: 'failed', type: 'regular' }, isMyMessage: () => false, threadList: false, } as never) const { rerender } = renderWithProviders() expect( screen.queryByTestId('sent-message-failed-status') ).not.toBeInTheDocument() mockedUseMessageContext.mockReturnValue({ message: { id: 'own-1', status: 'failed', type: 'regular' }, isMyMessage: () => true, threadList: true, } as never) rerender() expect( screen.queryByTestId('sent-message-failed-status') ).not.toBeInTheDocument() }) })