From 5d3b6d42e4652b836d9d473e8e60828563e5e15e Mon Sep 17 00:00:00 2001 From: Nuwan Date: Tue, 27 Jan 2026 08:17:12 +0530 Subject: [PATCH] test(07-02): add failing tests for sendMessage async thunk with optimistic updates Add unit tests for sendMessage optimistic UI flow: - Test pending sets loading status - Test pending adds optimistic message immediately - Test pending initializes channel if needed - Test fulfilled replaces optimistic message with real one - Test rejected removes optimistic message - Test other messages preserved during replace/remove Tests fail as expected - async thunk not yet implemented. Co-Authored-By: Claude Sonnet 4.5 --- .../__tests__/sessionChatSlice.test.js | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js b/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js index d8464cf69..e3e8d8662 100644 --- a/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js +++ b/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js @@ -649,6 +649,216 @@ describe('fetchChatHistory async thunk', () => { }); }); +describe('sendMessage async thunk', () => { + // Import will be added in implementation phase + // This is the RED phase - tests should fail + let sendMessage; + + test('sets loading state on pending', () => { + const action = { + type: 'sessionChat/sendMessage/pending', + meta: { + arg: { + channel: 'session-abc', + message: 'Hello world', + optimisticId: 'temp-1', + userId: 'user-1', + userName: 'John Doe' + } + } + }; + const state = { + sendStatus: 'idle', + sendError: null, + messagesByChannel: { 'session-abc': [] }, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const newState = sessionChatReducer(state, action); + expect(newState.sendStatus).toBe('loading'); + expect(newState.sendError).toBeNull(); + }); + + test('adds message optimistically on pending', () => { + const action = { + type: 'sessionChat/sendMessage/pending', + meta: { + arg: { + channel: 'session-abc', + message: 'Hello world', + optimisticId: 'temp-1', + userId: 'user-1', + userName: 'John Doe' + } + } + }; + const state = { + messagesByChannel: { 'session-abc': [] }, + sendStatus: 'idle', + sendError: null, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const newState = sessionChatReducer(state, action); + expect(newState.messagesByChannel['session-abc']).toHaveLength(1); + expect(newState.messagesByChannel['session-abc'][0].id).toBe('temp-1'); + expect(newState.messagesByChannel['session-abc'][0].message).toBe('Hello world'); + expect(newState.messagesByChannel['session-abc'][0].senderId).toBe('user-1'); + expect(newState.messagesByChannel['session-abc'][0].senderName).toBe('John Doe'); + expect(newState.messagesByChannel['session-abc'][0].isOptimistic).toBe(true); + }); + + test('initializes channel if not exists on pending', () => { + const action = { + type: 'sessionChat/sendMessage/pending', + meta: { + arg: { + channel: 'new-channel', + message: 'First message', + optimisticId: 'temp-1', + userId: 'user-1', + userName: 'John' + } + } + }; + const state = { + messagesByChannel: {}, + sendStatus: 'idle', + sendError: null, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const newState = sessionChatReducer(state, action); + expect(newState.messagesByChannel['new-channel']).toBeDefined(); + expect(newState.messagesByChannel['new-channel']).toHaveLength(1); + }); + + test('replaces optimistic message with real one on fulfilled', () => { + const state = { + messagesByChannel: { + 'session-abc': [ + { id: 'temp-1', message: 'Hello world', senderId: 'user-1', isOptimistic: true } + ] + }, + sendStatus: 'loading', + sendError: null, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const action = { + type: 'sessionChat/sendMessage/fulfilled', + meta: { + arg: { optimisticId: 'temp-1', channel: 'session-abc' } + }, + payload: { + message: { + id: 'msg-real', + message: 'Hello world', + sender_id: 'user-1', + sender_name: 'John Doe', + created_at: '2026-01-26T12:00:00Z', + channel: 'session' + } + } + }; + const newState = sessionChatReducer(state, action); + expect(newState.messagesByChannel['session-abc']).toHaveLength(1); + expect(newState.messagesByChannel['session-abc'][0].id).toBe('msg-real'); + expect(newState.messagesByChannel['session-abc'][0].isOptimistic).toBeUndefined(); + expect(newState.sendStatus).toBe('succeeded'); + }); + + test('removes optimistic message on rejected', () => { + const state = { + messagesByChannel: { + 'session-abc': [ + { id: 'temp-1', message: 'Hello world', senderId: 'user-1', isOptimistic: true } + ] + }, + sendStatus: 'loading', + sendError: null, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const action = { + type: 'sessionChat/sendMessage/rejected', + meta: { + arg: { optimisticId: 'temp-1', channel: 'session-abc' } + }, + error: { message: 'Network error' } + }; + const newState = sessionChatReducer(state, action); + expect(newState.messagesByChannel['session-abc']).toHaveLength(0); + expect(newState.sendStatus).toBe('failed'); + expect(newState.sendError).toBe('Network error'); + }); + + test('keeps other messages when replacing optimistic message', () => { + const state = { + messagesByChannel: { + 'session-abc': [ + { id: 'msg-1', message: 'Existing', createdAt: '2026-01-26T11:00:00Z' }, + { id: 'temp-1', message: 'Optimistic', senderId: 'user-1', isOptimistic: true } + ] + }, + sendStatus: 'loading', + sendError: null, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const action = { + type: 'sessionChat/sendMessage/fulfilled', + meta: { + arg: { optimisticId: 'temp-1', channel: 'session-abc' } + }, + payload: { + message: { + id: 'msg-real', + message: 'Optimistic', + sender_id: 'user-1', + created_at: '2026-01-26T12:00:00Z' + } + } + }; + 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-real'); + }); + + test('keeps other messages when removing optimistic message', () => { + const state = { + messagesByChannel: { + 'session-abc': [ + { id: 'msg-1', message: 'Existing', createdAt: '2026-01-26T11:00:00Z' }, + { id: 'temp-1', message: 'Failed', senderId: 'user-1', isOptimistic: true } + ] + }, + sendStatus: 'loading', + sendError: null, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const action = { + type: 'sessionChat/sendMessage/rejected', + meta: { + arg: { optimisticId: 'temp-1', channel: 'session-abc' } + }, + error: { message: 'Failed to send' } + }; + const newState = sessionChatReducer(state, action); + expect(newState.messagesByChannel['session-abc']).toHaveLength(1); + expect(newState.messagesByChannel['session-abc'][0].id).toBe('msg-1'); + }); +}); + describe('sessionChat integration', () => { test('complete message flow: receive → set active → open window → mark read', () => { const initialState = {