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 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-01-27 08:17:12 +05:30
parent 4f5339d7eb
commit 5d3b6d42e4
1 changed files with 210 additions and 0 deletions

View File

@ -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', () => { describe('sessionChat integration', () => {
test('complete message flow: receive → set active → open window → mark read', () => { test('complete message flow: receive → set active → open window → mark read', () => {
const initialState = { const initialState = {