test(07-01): add failing tests for core reducers with message deduplication
RED phase of TDD for Task 2: - Write tests for addMessageFromWebSocket with comprehensive scenarios: * Add message to new/existing channel * Message deduplication by msg_id (critical for WebSocket + REST) * Sort messages by createdAt ascending * Unread count increment logic (window closed OR different channel) * Global vs session channel handling - Write tests for setActiveChannel (channel + channelType) - Write tests for openChatWindow (resets unread, updates lastReadAt) - Write tests for closeChatWindow (preserves unread counts) Tests fail as expected - action creators don't exist yet. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e98504ebad
commit
60a559e58f
|
|
@ -1,4 +1,9 @@
|
|||
import sessionChatReducer from '../sessionChatSlice';
|
||||
import sessionChatReducer, {
|
||||
addMessageFromWebSocket,
|
||||
setActiveChannel,
|
||||
openChatWindow,
|
||||
closeChatWindow
|
||||
} from '../sessionChatSlice';
|
||||
|
||||
describe('sessionChatSlice initial state', () => {
|
||||
test('has empty messagesByChannel object', () => {
|
||||
|
|
@ -79,3 +84,295 @@ describe('sessionChatSlice initial state', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addMessageFromWebSocket', () => {
|
||||
test('adds message to new channel', () => {
|
||||
const state = {
|
||||
messagesByChannel: {},
|
||||
unreadCounts: {},
|
||||
isWindowOpen: false,
|
||||
activeChannel: null
|
||||
};
|
||||
const message = {
|
||||
id: 'msg-1',
|
||||
senderId: 'user-1',
|
||||
senderName: 'Alice',
|
||||
message: 'Hello',
|
||||
channel: 'session',
|
||||
sessionId: 'session-abc',
|
||||
createdAt: '2026-01-26T12:00:00Z'
|
||||
};
|
||||
const action = addMessageFromWebSocket(message);
|
||||
const newState = sessionChatReducer(state, action);
|
||||
|
||||
expect(newState.messagesByChannel['session-abc']).toBeDefined();
|
||||
expect(newState.messagesByChannel['session-abc']).toHaveLength(1);
|
||||
expect(newState.messagesByChannel['session-abc'][0]).toEqual(message);
|
||||
});
|
||||
|
||||
test('adds message to existing channel', () => {
|
||||
const existingMessage = {
|
||||
id: 'msg-1',
|
||||
message: 'Hello',
|
||||
createdAt: '2026-01-26T12:00:00Z'
|
||||
};
|
||||
const state = {
|
||||
messagesByChannel: {
|
||||
'session-abc': [existingMessage]
|
||||
},
|
||||
unreadCounts: {},
|
||||
isWindowOpen: false,
|
||||
activeChannel: null
|
||||
};
|
||||
const newMessage = {
|
||||
id: 'msg-2',
|
||||
senderId: 'user-2',
|
||||
senderName: 'Bob',
|
||||
message: 'Hi there',
|
||||
channel: 'session',
|
||||
sessionId: 'session-abc',
|
||||
createdAt: '2026-01-26T12:01:00Z'
|
||||
};
|
||||
const action = addMessageFromWebSocket(newMessage);
|
||||
const newState = sessionChatReducer(state, action);
|
||||
|
||||
expect(newState.messagesByChannel['session-abc']).toHaveLength(2);
|
||||
expect(newState.messagesByChannel['session-abc'][1]).toEqual(newMessage);
|
||||
});
|
||||
|
||||
test('deduplicates messages by msg_id', () => {
|
||||
const message = {
|
||||
id: 'msg-1',
|
||||
senderId: 'user-1',
|
||||
message: 'Hello',
|
||||
channel: 'session',
|
||||
sessionId: 'session-abc',
|
||||
createdAt: '2026-01-26T12:00:00Z'
|
||||
};
|
||||
const state = {
|
||||
messagesByChannel: {
|
||||
'session-abc': [message]
|
||||
},
|
||||
unreadCounts: {},
|
||||
isWindowOpen: false,
|
||||
activeChannel: null
|
||||
};
|
||||
// Try to add same message again
|
||||
const action = addMessageFromWebSocket(message);
|
||||
const newState = sessionChatReducer(state, action);
|
||||
|
||||
expect(newState.messagesByChannel['session-abc']).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('sorts messages by createdAt ascending', () => {
|
||||
const state = {
|
||||
messagesByChannel: {
|
||||
'session-abc': [
|
||||
{ id: 'msg-2', message: 'Second', createdAt: '2026-01-26T12:01:00Z' }
|
||||
]
|
||||
},
|
||||
unreadCounts: {},
|
||||
isWindowOpen: false,
|
||||
activeChannel: null
|
||||
};
|
||||
const earlierMessage = {
|
||||
id: 'msg-1',
|
||||
senderId: 'user-1',
|
||||
message: 'First',
|
||||
channel: 'session',
|
||||
sessionId: 'session-abc',
|
||||
createdAt: '2026-01-26T12:00:00Z'
|
||||
};
|
||||
const action = addMessageFromWebSocket(earlierMessage);
|
||||
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('increments unread count if window closed', () => {
|
||||
const state = {
|
||||
messagesByChannel: {},
|
||||
unreadCounts: {},
|
||||
isWindowOpen: false,
|
||||
activeChannel: 'session-abc'
|
||||
};
|
||||
const message = {
|
||||
id: 'msg-1',
|
||||
message: 'Hello',
|
||||
channel: 'session',
|
||||
sessionId: 'session-abc',
|
||||
createdAt: '2026-01-26T12:00:00Z'
|
||||
};
|
||||
const action = addMessageFromWebSocket(message);
|
||||
const newState = sessionChatReducer(state, action);
|
||||
|
||||
expect(newState.unreadCounts['session-abc']).toBe(1);
|
||||
});
|
||||
|
||||
test('increments unread count if viewing different channel', () => {
|
||||
const state = {
|
||||
messagesByChannel: {},
|
||||
unreadCounts: {},
|
||||
isWindowOpen: true,
|
||||
activeChannel: 'session-xyz'
|
||||
};
|
||||
const message = {
|
||||
id: 'msg-1',
|
||||
message: 'Hello',
|
||||
channel: 'session',
|
||||
sessionId: 'session-abc',
|
||||
createdAt: '2026-01-26T12:00:00Z'
|
||||
};
|
||||
const action = addMessageFromWebSocket(message);
|
||||
const newState = sessionChatReducer(state, action);
|
||||
|
||||
expect(newState.unreadCounts['session-abc']).toBe(1);
|
||||
});
|
||||
|
||||
test('does not increment unread count if window open and viewing same channel', () => {
|
||||
const state = {
|
||||
messagesByChannel: {},
|
||||
unreadCounts: {},
|
||||
isWindowOpen: true,
|
||||
activeChannel: 'session-abc'
|
||||
};
|
||||
const message = {
|
||||
id: 'msg-1',
|
||||
message: 'Hello',
|
||||
channel: 'session',
|
||||
sessionId: 'session-abc',
|
||||
createdAt: '2026-01-26T12:00:00Z'
|
||||
};
|
||||
const action = addMessageFromWebSocket(message);
|
||||
const newState = sessionChatReducer(state, action);
|
||||
|
||||
expect(newState.unreadCounts['session-abc']).toBeUndefined();
|
||||
});
|
||||
|
||||
test('handles global channel correctly', () => {
|
||||
const state = {
|
||||
messagesByChannel: {},
|
||||
unreadCounts: {},
|
||||
isWindowOpen: false,
|
||||
activeChannel: null
|
||||
};
|
||||
const message = {
|
||||
id: 'msg-1',
|
||||
message: 'Hello world',
|
||||
channel: 'global',
|
||||
createdAt: '2026-01-26T12:00:00Z'
|
||||
};
|
||||
const action = addMessageFromWebSocket(message);
|
||||
const newState = sessionChatReducer(state, action);
|
||||
|
||||
expect(newState.messagesByChannel['global']).toBeDefined();
|
||||
expect(newState.messagesByChannel['global']).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setActiveChannel', () => {
|
||||
test('sets active channel and type', () => {
|
||||
const state = {
|
||||
activeChannel: null,
|
||||
channelType: null
|
||||
};
|
||||
const action = setActiveChannel({
|
||||
channel: 'session-abc',
|
||||
channelType: 'session'
|
||||
});
|
||||
const newState = sessionChatReducer(state, action);
|
||||
|
||||
expect(newState.activeChannel).toBe('session-abc');
|
||||
expect(newState.channelType).toBe('session');
|
||||
});
|
||||
|
||||
test('updates existing active channel', () => {
|
||||
const state = {
|
||||
activeChannel: 'session-abc',
|
||||
channelType: 'session'
|
||||
};
|
||||
const action = setActiveChannel({
|
||||
channel: 'global',
|
||||
channelType: 'global'
|
||||
});
|
||||
const newState = sessionChatReducer(state, action);
|
||||
|
||||
expect(newState.activeChannel).toBe('global');
|
||||
expect(newState.channelType).toBe('global');
|
||||
});
|
||||
});
|
||||
|
||||
describe('openChatWindow', () => {
|
||||
test('opens window and resets unread count for active channel', () => {
|
||||
const state = {
|
||||
isWindowOpen: false,
|
||||
activeChannel: 'session-abc',
|
||||
unreadCounts: { 'session-abc': 5 },
|
||||
lastReadAt: {}
|
||||
};
|
||||
const action = openChatWindow();
|
||||
const newState = sessionChatReducer(state, action);
|
||||
|
||||
expect(newState.isWindowOpen).toBe(true);
|
||||
expect(newState.unreadCounts['session-abc']).toBe(0);
|
||||
expect(newState.lastReadAt['session-abc']).toBeTruthy();
|
||||
// Verify it's an ISO timestamp
|
||||
expect(newState.lastReadAt['session-abc']).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||
});
|
||||
|
||||
test('opens window without active channel', () => {
|
||||
const state = {
|
||||
isWindowOpen: false,
|
||||
activeChannel: null,
|
||||
unreadCounts: {},
|
||||
lastReadAt: {}
|
||||
};
|
||||
const action = openChatWindow();
|
||||
const newState = sessionChatReducer(state, action);
|
||||
|
||||
expect(newState.isWindowOpen).toBe(true);
|
||||
expect(Object.keys(newState.unreadCounts)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('preserves other channel unread counts', () => {
|
||||
const state = {
|
||||
isWindowOpen: false,
|
||||
activeChannel: 'session-abc',
|
||||
unreadCounts: { 'session-abc': 5, 'global': 3 },
|
||||
lastReadAt: {}
|
||||
};
|
||||
const action = openChatWindow();
|
||||
const newState = sessionChatReducer(state, action);
|
||||
|
||||
expect(newState.unreadCounts['session-abc']).toBe(0);
|
||||
expect(newState.unreadCounts['global']).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('closeChatWindow', () => {
|
||||
test('closes window without changing unread counts', () => {
|
||||
const state = {
|
||||
isWindowOpen: true,
|
||||
unreadCounts: { 'session-abc': 0, 'global': 2 }
|
||||
};
|
||||
const action = closeChatWindow();
|
||||
const newState = sessionChatReducer(state, action);
|
||||
|
||||
expect(newState.isWindowOpen).toBe(false);
|
||||
expect(newState.unreadCounts['session-abc']).toBe(0);
|
||||
expect(newState.unreadCounts['global']).toBe(2);
|
||||
});
|
||||
|
||||
test('closes already closed window', () => {
|
||||
const state = {
|
||||
isWindowOpen: false,
|
||||
unreadCounts: {}
|
||||
};
|
||||
const action = closeChatWindow();
|
||||
const newState = sessionChatReducer(state, action);
|
||||
|
||||
expect(newState.isWindowOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue