import type { IEditedMessage, IMessage } from '@rocket.chat/core-typings'; import { mockAppRoot } from '@rocket.chat/mock-providers'; import { clientCallbacks } from '@rocket.chat/ui-client'; import { renderHook, act } from '@testing-library/react'; import { useHasNewMessages } from './useHasNewMessages'; jest.mock('../../contexts/ChatContext', () => ({ useChat: () => ({ composer: { focus: jest.fn() }, }), })); jest.mock('../../../../lib/RoomHistoryManager', () => ({ RoomHistoryManager: { clear: jest.fn(), getMoreIfIsEmpty: jest.fn(), hasMoreNext: jest.fn(), }, })); describe('useHasNewMessages', () => { const isAtBottom = { current: true }; const setShouldJumpToBottom = jest.fn(); const rid = 'room-id'; const uid = 'current-user-id'; beforeEach(() => { jest.clearAllMocks(); clientCallbacks.remove('streamNewMessage', rid); }); afterEach(() => { clientCallbacks.remove('streamNewMessage', rid); }); it('should NOT show new messages button when user sends their own message', () => { const { result } = renderHook(() => useHasNewMessages(rid, uid, setShouldJumpToBottom, isAtBottom), { wrapper: mockAppRoot().build() }); const ownMsg: IMessage = { _id: 'msg-1', rid, u: { _id: uid, username: 'current-user', name: 'Current User' }, msg: 'Hello', ts: new Date(), _updatedAt: new Date(), }; const callbacks = clientCallbacks.getCallbacks('streamNewMessage'); act(() => { callbacks.forEach((callback) => callback(ownMsg)); }); expect(result.current.hasNewMessages).toBe(false); }); it('should NOT show new messages button when user sends own message during race condition (not at bottom)', () => { const { result } = renderHook(() => useHasNewMessages(rid, uid, setShouldJumpToBottom, isAtBottom), { wrapper: mockAppRoot().build() }); const ownMsg: IMessage = { _id: 'msg-race', rid, u: { _id: uid, username: 'current-user', name: 'Current User' }, msg: 'Message sent during scroll', ts: new Date(), _updatedAt: new Date(), }; const callbacks = clientCallbacks.getCallbacks('streamNewMessage'); act(() => { callbacks.forEach((callback) => callback(ownMsg)); }); expect(result.current.hasNewMessages).toBe(false); }); it('should show new messages button when another user sends a message and user is NOT at bottom', () => { const { result } = renderHook(() => useHasNewMessages(rid, uid, setShouldJumpToBottom, { current: false }), { wrapper: mockAppRoot().build(), }); const otherUserMsg: IMessage = { _id: 'msg-2', rid, u: { _id: 'other-user-id', username: 'other-user', name: 'Other User' }, msg: 'Hello from another user', ts: new Date(), _updatedAt: new Date(), }; const callbacks = clientCallbacks.getCallbacks('streamNewMessage'); act(() => { callbacks.forEach((callback) => callback(otherUserMsg)); }); expect(result.current.hasNewMessages).toBe(true); }); it('should NOT show new messages button when another user sends a message but user IS at bottom', () => { const { result } = renderHook(() => useHasNewMessages(rid, uid, setShouldJumpToBottom, isAtBottom), { wrapper: mockAppRoot().build() }); const otherUserMsg: IMessage = { _id: 'msg-3', rid, u: { _id: 'other-user-id', username: 'other-user', name: 'Other User' }, msg: 'Hello from another user', ts: new Date(), _updatedAt: new Date(), }; const callbacks = clientCallbacks.getCallbacks('streamNewMessage'); act(() => { callbacks.forEach((callback) => callback(otherUserMsg)); }); expect(result.current.hasNewMessages).toBe(false); }); it('should ignore edited messages in streamNewMessage', () => { const { result } = renderHook(() => useHasNewMessages(rid, uid, setShouldJumpToBottom, isAtBottom), { wrapper: mockAppRoot().build() }); const editedMsg: IEditedMessage = { _id: 'msg-4', rid, u: { _id: 'other-user-id', username: 'other-user', name: 'Other User' }, msg: 'Edited message', ts: new Date(), _updatedAt: new Date(), editedAt: new Date(), editedBy: { _id: 'other-user-id', username: 'other-user' }, }; const callbacks = clientCallbacks.getCallbacks('streamNewMessage'); act(() => { callbacks.forEach((callback) => callback(editedMsg)); }); expect(result.current.hasNewMessages).toBe(false); }); it('should ignore thread messages in streamNewMessage', () => { const { result } = renderHook(() => useHasNewMessages(rid, uid, setShouldJumpToBottom, isAtBottom), { wrapper: mockAppRoot().build() }); const threadMsg: IMessage = { _id: 'msg-5', rid, tmid: 'parent-thread-id', u: { _id: 'other-user-id', username: 'other-user', name: 'Other User' }, msg: 'Thread reply', ts: new Date(), _updatedAt: new Date(), }; const callbacks = clientCallbacks.getCallbacks('streamNewMessage'); act(() => { callbacks.forEach((callback) => callback(threadMsg)); }); expect(result.current.hasNewMessages).toBe(false); }); it('should NOT jump to bottom when streamNewMessage fires for current user editing their own message', () => { renderHook(() => useHasNewMessages(rid, uid, setShouldJumpToBottom, isAtBottom), { wrapper: mockAppRoot().build() }); const editedOwnMsg: IEditedMessage = { _id: 'msg-edit', rid, u: { _id: uid, username: 'current-user', name: 'Current User' }, msg: 'Edited message', ts: new Date(), _updatedAt: new Date(), editedAt: new Date(), editedBy: { _id: uid, username: 'current-user' }, }; const streamNewMessageCallbacks = clientCallbacks.getCallbacks('streamNewMessage'); act(() => { streamNewMessageCallbacks.forEach((callback) => callback(editedOwnMsg)); }); expect(setShouldJumpToBottom).not.toHaveBeenCalled(); }); it('should clear hasNewMessages when streamNewMessage fires for current users message', () => { const { result } = renderHook(() => useHasNewMessages(rid, uid, setShouldJumpToBottom, { current: false }), { wrapper: mockAppRoot().build(), }); const otherUserMsg: IMessage = { _id: 'msg-6', rid, u: { _id: 'other-user-id', username: 'other-user', name: 'Other User' }, msg: 'Hello', ts: new Date(), _updatedAt: new Date(), }; const streamCallbacks = clientCallbacks.getCallbacks('streamNewMessage'); act(() => { streamCallbacks.forEach((callback) => callback(otherUserMsg)); }); expect(result.current.hasNewMessages).toBe(true); const ownMsg: IMessage = { _id: 'msg-7', rid, u: { _id: uid, username: 'current-user', name: 'Current User' }, msg: 'My reply', ts: new Date(), _updatedAt: new Date(), }; const streamNewMessageCallbacks = clientCallbacks.getCallbacks('streamNewMessage'); act(() => { streamNewMessageCallbacks.forEach((callback) => callback(ownMsg)); }); expect(result.current.hasNewMessages).toBe(false); expect(setShouldJumpToBottom).toHaveBeenCalledWith(true); }); });