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:
parent
4f5339d7eb
commit
5d3b6d42e4
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue