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:
Nuwan 2026-01-27 08:06:13 +05:30
parent e98504ebad
commit 60a559e58f
1 changed files with 298 additions and 1 deletions

View File

@ -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);
});
});