diff --git a/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js b/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js index 45116551e..b980445cf 100644 --- a/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js +++ b/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js @@ -1,4 +1,9 @@ -import sessionChatReducer from '../sessionChatSlice'; +import sessionChatReducer, { + addMessageFromWebSocket, + setActiveChannel, + openChatWindow, + closeChatWindow +} from '../sessionChatSlice'; describe('sessionChatSlice initial state', () => { test('has empty messagesByChannel object', () => { @@ -79,3 +84,295 @@ describe('sessionChatSlice initial state', () => { }); }); }); + +describe('addMessageFromWebSocket', () => { + test('adds message to new channel', () => { + const state = { + messagesByChannel: {}, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const message = { + id: 'msg-1', + senderId: 'user-1', + senderName: 'Alice', + message: 'Hello', + channel: 'session', + sessionId: 'session-abc', + createdAt: '2026-01-26T12:00:00Z' + }; + const action = addMessageFromWebSocket(message); + const newState = sessionChatReducer(state, action); + + expect(newState.messagesByChannel['session-abc']).toBeDefined(); + expect(newState.messagesByChannel['session-abc']).toHaveLength(1); + expect(newState.messagesByChannel['session-abc'][0]).toEqual(message); + }); + + test('adds message to existing channel', () => { + const existingMessage = { + id: 'msg-1', + message: 'Hello', + createdAt: '2026-01-26T12:00:00Z' + }; + const state = { + messagesByChannel: { + 'session-abc': [existingMessage] + }, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const newMessage = { + id: 'msg-2', + senderId: 'user-2', + senderName: 'Bob', + message: 'Hi there', + channel: 'session', + sessionId: 'session-abc', + createdAt: '2026-01-26T12:01:00Z' + }; + const action = addMessageFromWebSocket(newMessage); + const newState = sessionChatReducer(state, action); + + expect(newState.messagesByChannel['session-abc']).toHaveLength(2); + expect(newState.messagesByChannel['session-abc'][1]).toEqual(newMessage); + }); + + test('deduplicates messages by msg_id', () => { + const message = { + id: 'msg-1', + senderId: 'user-1', + message: 'Hello', + channel: 'session', + sessionId: 'session-abc', + createdAt: '2026-01-26T12:00:00Z' + }; + const state = { + messagesByChannel: { + 'session-abc': [message] + }, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + // Try to add same message again + const action = addMessageFromWebSocket(message); + const newState = sessionChatReducer(state, action); + + expect(newState.messagesByChannel['session-abc']).toHaveLength(1); + }); + + test('sorts messages by createdAt ascending', () => { + const state = { + messagesByChannel: { + 'session-abc': [ + { id: 'msg-2', message: 'Second', createdAt: '2026-01-26T12:01:00Z' } + ] + }, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const earlierMessage = { + id: 'msg-1', + senderId: 'user-1', + message: 'First', + channel: 'session', + sessionId: 'session-abc', + createdAt: '2026-01-26T12:00:00Z' + }; + const action = addMessageFromWebSocket(earlierMessage); + const newState = sessionChatReducer(state, action); + + expect(newState.messagesByChannel['session-abc']).toHaveLength(2); + expect(newState.messagesByChannel['session-abc'][0].id).toBe('msg-1'); + expect(newState.messagesByChannel['session-abc'][1].id).toBe('msg-2'); + }); + + test('increments unread count if window closed', () => { + const state = { + messagesByChannel: {}, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: 'session-abc' + }; + const message = { + id: 'msg-1', + message: 'Hello', + channel: 'session', + sessionId: 'session-abc', + createdAt: '2026-01-26T12:00:00Z' + }; + const action = addMessageFromWebSocket(message); + const newState = sessionChatReducer(state, action); + + expect(newState.unreadCounts['session-abc']).toBe(1); + }); + + test('increments unread count if viewing different channel', () => { + const state = { + messagesByChannel: {}, + unreadCounts: {}, + isWindowOpen: true, + activeChannel: 'session-xyz' + }; + const message = { + id: 'msg-1', + message: 'Hello', + channel: 'session', + sessionId: 'session-abc', + createdAt: '2026-01-26T12:00:00Z' + }; + const action = addMessageFromWebSocket(message); + const newState = sessionChatReducer(state, action); + + expect(newState.unreadCounts['session-abc']).toBe(1); + }); + + test('does not increment unread count if window open and viewing same channel', () => { + const state = { + messagesByChannel: {}, + unreadCounts: {}, + isWindowOpen: true, + activeChannel: 'session-abc' + }; + const message = { + id: 'msg-1', + message: 'Hello', + channel: 'session', + sessionId: 'session-abc', + createdAt: '2026-01-26T12:00:00Z' + }; + const action = addMessageFromWebSocket(message); + const newState = sessionChatReducer(state, action); + + expect(newState.unreadCounts['session-abc']).toBeUndefined(); + }); + + test('handles global channel correctly', () => { + const state = { + messagesByChannel: {}, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const message = { + id: 'msg-1', + message: 'Hello world', + channel: 'global', + createdAt: '2026-01-26T12:00:00Z' + }; + const action = addMessageFromWebSocket(message); + const newState = sessionChatReducer(state, action); + + expect(newState.messagesByChannel['global']).toBeDefined(); + expect(newState.messagesByChannel['global']).toHaveLength(1); + }); +}); + +describe('setActiveChannel', () => { + test('sets active channel and type', () => { + const state = { + activeChannel: null, + channelType: null + }; + const action = setActiveChannel({ + channel: 'session-abc', + channelType: 'session' + }); + const newState = sessionChatReducer(state, action); + + expect(newState.activeChannel).toBe('session-abc'); + expect(newState.channelType).toBe('session'); + }); + + test('updates existing active channel', () => { + const state = { + activeChannel: 'session-abc', + channelType: 'session' + }; + const action = setActiveChannel({ + channel: 'global', + channelType: 'global' + }); + const newState = sessionChatReducer(state, action); + + expect(newState.activeChannel).toBe('global'); + expect(newState.channelType).toBe('global'); + }); +}); + +describe('openChatWindow', () => { + test('opens window and resets unread count for active channel', () => { + const state = { + isWindowOpen: false, + activeChannel: 'session-abc', + unreadCounts: { 'session-abc': 5 }, + lastReadAt: {} + }; + const action = openChatWindow(); + const newState = sessionChatReducer(state, action); + + expect(newState.isWindowOpen).toBe(true); + expect(newState.unreadCounts['session-abc']).toBe(0); + expect(newState.lastReadAt['session-abc']).toBeTruthy(); + // Verify it's an ISO timestamp + expect(newState.lastReadAt['session-abc']).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); + }); + + test('opens window without active channel', () => { + const state = { + isWindowOpen: false, + activeChannel: null, + unreadCounts: {}, + lastReadAt: {} + }; + const action = openChatWindow(); + const newState = sessionChatReducer(state, action); + + expect(newState.isWindowOpen).toBe(true); + expect(Object.keys(newState.unreadCounts)).toHaveLength(0); + }); + + test('preserves other channel unread counts', () => { + const state = { + isWindowOpen: false, + activeChannel: 'session-abc', + unreadCounts: { 'session-abc': 5, 'global': 3 }, + lastReadAt: {} + }; + const action = openChatWindow(); + const newState = sessionChatReducer(state, action); + + expect(newState.unreadCounts['session-abc']).toBe(0); + expect(newState.unreadCounts['global']).toBe(3); + }); +}); + +describe('closeChatWindow', () => { + test('closes window without changing unread counts', () => { + const state = { + isWindowOpen: true, + unreadCounts: { 'session-abc': 0, 'global': 2 } + }; + const action = closeChatWindow(); + const newState = sessionChatReducer(state, action); + + expect(newState.isWindowOpen).toBe(false); + expect(newState.unreadCounts['session-abc']).toBe(0); + expect(newState.unreadCounts['global']).toBe(2); + }); + + test('closes already closed window', () => { + const state = { + isWindowOpen: false, + unreadCounts: {} + }; + const action = closeChatWindow(); + const newState = sessionChatReducer(state, action); + + expect(newState.isWindowOpen).toBe(false); + }); +});