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', () => {
|
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 = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue