test(07-02): add failing tests for fetchChatHistory async thunk
Add unit tests for fetchChatHistory extra reducers: - Test pending state sets loading status - Test fulfilled state adds messages and updates status - Test message deduplication on fulfilled - Test pagination: prepending older messages - Test rejected state sets error - Test null cursor handling 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
95483da6fe
commit
b0b4ae1a53
|
|
@ -488,6 +488,167 @@ describe('setWindowPosition', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('fetchChatHistory async thunk', () => {
|
||||
// Import will be added in implementation phase
|
||||
// This is the RED phase - tests should fail
|
||||
let fetchChatHistory;
|
||||
|
||||
test('sets loading state on pending', () => {
|
||||
// Mock the pending action type
|
||||
const action = {
|
||||
type: 'sessionChat/fetchChatHistory/pending',
|
||||
meta: { arg: { channel: 'session-abc' } }
|
||||
};
|
||||
const state = {
|
||||
fetchStatus: {},
|
||||
fetchError: {},
|
||||
messagesByChannel: {},
|
||||
unreadCounts: {},
|
||||
isWindowOpen: false,
|
||||
activeChannel: null
|
||||
};
|
||||
const newState = sessionChatReducer(state, action);
|
||||
expect(newState.fetchStatus['session-abc']).toBe('loading');
|
||||
expect(newState.fetchError['session-abc']).toBeNull();
|
||||
});
|
||||
|
||||
test('adds messages on fulfilled', () => {
|
||||
const action = {
|
||||
type: 'sessionChat/fetchChatHistory/fulfilled',
|
||||
meta: { arg: { channel: 'session-abc' } },
|
||||
payload: {
|
||||
channel: 'session-abc',
|
||||
messages: [
|
||||
{ id: 'msg-1', message: 'Hello', sender_id: 'user-1', created_at: '2026-01-26T12:00:00Z' }
|
||||
],
|
||||
next: 20
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
messagesByChannel: {},
|
||||
fetchStatus: { 'session-abc': 'loading' },
|
||||
fetchError: {},
|
||||
nextCursors: {},
|
||||
unreadCounts: {},
|
||||
isWindowOpen: false,
|
||||
activeChannel: null
|
||||
};
|
||||
const newState = sessionChatReducer(state, action);
|
||||
expect(newState.messagesByChannel['session-abc']).toHaveLength(1);
|
||||
expect(newState.fetchStatus['session-abc']).toBe('succeeded');
|
||||
expect(newState.nextCursors['session-abc']).toBe(20);
|
||||
});
|
||||
|
||||
test('deduplicates messages on fulfilled', () => {
|
||||
// Test that fetched messages don't duplicate existing ones
|
||||
const state = {
|
||||
messagesByChannel: {
|
||||
'session-abc': [{ id: 'msg-1', message: 'Hello', createdAt: '2026-01-26T12:00:00Z' }]
|
||||
},
|
||||
fetchStatus: { 'session-abc': 'loading' },
|
||||
fetchError: {},
|
||||
nextCursors: {},
|
||||
unreadCounts: {},
|
||||
isWindowOpen: false,
|
||||
activeChannel: null
|
||||
};
|
||||
const action = {
|
||||
type: 'sessionChat/fetchChatHistory/fulfilled',
|
||||
meta: { arg: { channel: 'session-abc' } },
|
||||
payload: {
|
||||
channel: 'session-abc',
|
||||
messages: [
|
||||
{ id: 'msg-1', message: 'Hello', createdAt: '2026-01-26T12:00:00Z' }, // Duplicate
|
||||
{ id: 'msg-2', message: 'World', createdAt: '2026-01-26T12:01:00Z' } // New
|
||||
],
|
||||
next: null
|
||||
}
|
||||
};
|
||||
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('prepends older messages for pagination', () => {
|
||||
// When fetching older messages, they should be prepended (oldest first)
|
||||
const state = {
|
||||
messagesByChannel: {
|
||||
'session-abc': [
|
||||
{ id: 'msg-3', message: 'Newest', createdAt: '2026-01-26T12:02:00Z' }
|
||||
]
|
||||
},
|
||||
fetchStatus: { 'session-abc': 'loading' },
|
||||
fetchError: {},
|
||||
nextCursors: {},
|
||||
unreadCounts: {},
|
||||
isWindowOpen: false,
|
||||
activeChannel: null
|
||||
};
|
||||
const action = {
|
||||
type: 'sessionChat/fetchChatHistory/fulfilled',
|
||||
meta: { arg: { channel: 'session-abc', before: 3 } },
|
||||
payload: {
|
||||
channel: 'session-abc',
|
||||
messages: [
|
||||
{ id: 'msg-1', message: 'Oldest', createdAt: '2026-01-26T12:00:00Z' },
|
||||
{ id: 'msg-2', message: 'Middle', createdAt: '2026-01-26T12:01:00Z' }
|
||||
],
|
||||
next: null
|
||||
}
|
||||
};
|
||||
const newState = sessionChatReducer(state, action);
|
||||
expect(newState.messagesByChannel['session-abc']).toHaveLength(3);
|
||||
expect(newState.messagesByChannel['session-abc'][0].id).toBe('msg-1');
|
||||
expect(newState.messagesByChannel['session-abc'][1].id).toBe('msg-2');
|
||||
expect(newState.messagesByChannel['session-abc'][2].id).toBe('msg-3');
|
||||
});
|
||||
|
||||
test('sets error state on rejected', () => {
|
||||
const action = {
|
||||
type: 'sessionChat/fetchChatHistory/rejected',
|
||||
meta: { arg: { channel: 'session-abc' } },
|
||||
error: { message: 'Not Found' }
|
||||
};
|
||||
const state = {
|
||||
fetchStatus: { 'session-abc': 'loading' },
|
||||
fetchError: {},
|
||||
messagesByChannel: {},
|
||||
unreadCounts: {},
|
||||
isWindowOpen: false,
|
||||
activeChannel: null
|
||||
};
|
||||
const newState = sessionChatReducer(state, action);
|
||||
expect(newState.fetchStatus['session-abc']).toBe('failed');
|
||||
expect(newState.fetchError['session-abc']).toBe('Not Found');
|
||||
});
|
||||
|
||||
test('handles null next cursor', () => {
|
||||
const action = {
|
||||
type: 'sessionChat/fetchChatHistory/fulfilled',
|
||||
meta: { arg: { channel: 'session-abc' } },
|
||||
payload: {
|
||||
channel: 'session-abc',
|
||||
messages: [
|
||||
{ id: 'msg-1', message: 'Hello', createdAt: '2026-01-26T12:00:00Z' }
|
||||
],
|
||||
next: null
|
||||
}
|
||||
};
|
||||
const state = {
|
||||
messagesByChannel: {},
|
||||
fetchStatus: { 'session-abc': 'loading' },
|
||||
fetchError: {},
|
||||
nextCursors: {},
|
||||
unreadCounts: {},
|
||||
isWindowOpen: false,
|
||||
activeChannel: null
|
||||
};
|
||||
const newState = sessionChatReducer(state, action);
|
||||
expect(newState.nextCursors['session-abc']).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sessionChat integration', () => {
|
||||
test('complete message flow: receive → set active → open window → mark read', () => {
|
||||
const initialState = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue