test(07-01): add failing tests for unread tracking and integration flows

RED phase of TDD for Task 3:
- Write tests for markAsRead (resets count, updates lastReadAt)
- Write tests for incrementUnreadCount (handles missing channel)
- Write tests for setWindowPosition (UI persistence)
- Write comprehensive integration tests:
  * Complete message flow: receive → set active → open → close
  * Multi-channel scenario with independent unread counts
  * Message deduplication preventing double unread increment
- Integration tests validate all 7 reducers working together

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:07:35 +05:30
parent 15a658a5dc
commit 96d7188a4c
1 changed files with 266 additions and 1 deletions

View File

@ -2,7 +2,10 @@ import sessionChatReducer, {
addMessageFromWebSocket,
setActiveChannel,
openChatWindow,
closeChatWindow
closeChatWindow,
markAsRead,
incrementUnreadCount,
setWindowPosition
} from '../sessionChatSlice';
describe('sessionChatSlice initial state', () => {
@ -376,3 +379,265 @@ describe('closeChatWindow', () => {
expect(newState.isWindowOpen).toBe(false);
});
});
describe('markAsRead', () => {
test('resets unread count for specified channel', () => {
const state = {
unreadCounts: { 'session-abc': 5, 'global': 2 },
lastReadAt: {}
};
const action = markAsRead({ channel: 'session-abc' });
const newState = sessionChatReducer(state, action);
expect(newState.unreadCounts['session-abc']).toBe(0);
expect(newState.unreadCounts['global']).toBe(2); // Unchanged
expect(newState.lastReadAt['session-abc']).toBeTruthy();
expect(newState.lastReadAt['session-abc']).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
});
test('handles channel with no existing unread count', () => {
const state = {
unreadCounts: {},
lastReadAt: {}
};
const action = markAsRead({ channel: 'session-abc' });
const newState = sessionChatReducer(state, action);
expect(newState.unreadCounts['session-abc']).toBe(0);
expect(newState.lastReadAt['session-abc']).toBeTruthy();
});
test('updates lastReadAt timestamp', () => {
const oldTimestamp = '2026-01-26T10:00:00.000Z';
const state = {
unreadCounts: { 'session-abc': 3 },
lastReadAt: { 'session-abc': oldTimestamp }
};
const action = markAsRead({ channel: 'session-abc' });
const newState = sessionChatReducer(state, action);
expect(newState.lastReadAt['session-abc']).not.toBe(oldTimestamp);
expect(new Date(newState.lastReadAt['session-abc']).getTime()).toBeGreaterThan(
new Date(oldTimestamp).getTime()
);
});
});
describe('incrementUnreadCount', () => {
test('increments unread count for channel', () => {
const state = {
unreadCounts: { 'session-abc': 3 }
};
const action = incrementUnreadCount({ channel: 'session-abc' });
const newState = sessionChatReducer(state, action);
expect(newState.unreadCounts['session-abc']).toBe(4);
});
test('initializes unread count if not exists', () => {
const state = {
unreadCounts: {}
};
const action = incrementUnreadCount({ channel: 'session-abc' });
const newState = sessionChatReducer(state, action);
expect(newState.unreadCounts['session-abc']).toBe(1);
});
test('handles multiple channels independently', () => {
const state = {
unreadCounts: { 'session-abc': 2, 'global': 5 }
};
const action = incrementUnreadCount({ channel: 'session-abc' });
const newState = sessionChatReducer(state, action);
expect(newState.unreadCounts['session-abc']).toBe(3);
expect(newState.unreadCounts['global']).toBe(5); // Unchanged
});
});
describe('setWindowPosition', () => {
test('updates window position', () => {
const state = {
windowPosition: null
};
const action = setWindowPosition({ x: 300, y: 400 });
const newState = sessionChatReducer(state, action);
expect(newState.windowPosition).toEqual({ x: 300, y: 400 });
});
test('overwrites existing position', () => {
const state = {
windowPosition: { x: 100, y: 200 }
};
const action = setWindowPosition({ x: 500, y: 600 });
const newState = sessionChatReducer(state, action);
expect(newState.windowPosition).toEqual({ x: 500, y: 600 });
});
test('can set position to null', () => {
const state = {
windowPosition: { x: 300, y: 400 }
};
const action = setWindowPosition(null);
const newState = sessionChatReducer(state, action);
expect(newState.windowPosition).toBeNull();
});
});
describe('sessionChat integration', () => {
test('complete message flow: receive → set active → open window → mark read', () => {
const initialState = {
messagesByChannel: {},
activeChannel: null,
channelType: null,
unreadCounts: {},
lastReadAt: {},
isWindowOpen: false,
windowPosition: null
};
// Step 1: Receive message (window closed, no active channel)
let state = sessionChatReducer(
initialState,
addMessageFromWebSocket({
id: 'msg-1',
senderId: 'user-1',
message: 'Hello',
channel: 'session',
sessionId: 'session-abc',
createdAt: '2026-01-26T12:00:00Z'
})
);
expect(state.messagesByChannel['session-abc']).toHaveLength(1);
expect(state.unreadCounts['session-abc']).toBe(1);
// Step 2: Set active channel
state = sessionChatReducer(
state,
setActiveChannel({ channel: 'session-abc', channelType: 'session' })
);
expect(state.activeChannel).toBe('session-abc');
expect(state.channelType).toBe('session');
// Step 3: Open window (should mark as read)
state = sessionChatReducer(state, openChatWindow());
expect(state.isWindowOpen).toBe(true);
expect(state.unreadCounts['session-abc']).toBe(0);
expect(state.lastReadAt['session-abc']).toBeTruthy();
// Step 4: Receive another message while window open and viewing channel
state = sessionChatReducer(
state,
addMessageFromWebSocket({
id: 'msg-2',
senderId: 'user-2',
message: 'Hi back',
channel: 'session',
sessionId: 'session-abc',
createdAt: '2026-01-26T12:01:00Z'
})
);
expect(state.messagesByChannel['session-abc']).toHaveLength(2);
expect(state.unreadCounts['session-abc']).toBe(0); // Should NOT increment
// Step 5: Close window
state = sessionChatReducer(state, closeChatWindow());
expect(state.isWindowOpen).toBe(false);
// Step 6: Receive message while window closed
state = sessionChatReducer(
state,
addMessageFromWebSocket({
id: 'msg-3',
senderId: 'user-1',
message: 'Are you there?',
channel: 'session',
sessionId: 'session-abc',
createdAt: '2026-01-26T12:02:00Z'
})
);
expect(state.messagesByChannel['session-abc']).toHaveLength(3);
expect(state.unreadCounts['session-abc']).toBe(1); // Should increment
});
test('multi-channel flow: messages in different channels', () => {
const initialState = {
messagesByChannel: {},
activeChannel: null,
channelType: null,
unreadCounts: {},
lastReadAt: {},
isWindowOpen: true,
windowPosition: null
};
// Receive message in session-abc (window open, but no active channel)
let state = sessionChatReducer(
initialState,
addMessageFromWebSocket({
id: 'msg-1',
message: 'Session message',
channel: 'session',
sessionId: 'session-abc',
createdAt: '2026-01-26T12:00:00Z'
})
);
expect(state.unreadCounts['session-abc']).toBe(1); // Increments (no active channel)
// Set active to session-abc
state = sessionChatReducer(
state,
setActiveChannel({ channel: 'session-abc', channelType: 'session' })
);
// Receive message in global (window open, but viewing session-abc)
state = sessionChatReducer(
state,
addMessageFromWebSocket({
id: 'msg-2',
message: 'Global message',
channel: 'global',
createdAt: '2026-01-26T12:01:00Z'
})
);
expect(state.unreadCounts['global']).toBe(1); // Increments (different channel)
// Mark global as read
state = sessionChatReducer(state, markAsRead({ channel: 'global' }));
expect(state.unreadCounts['global']).toBe(0);
expect(state.unreadCounts['session-abc']).toBe(1); // Unchanged
});
test('message deduplication prevents double unread increment', () => {
const initialState = {
messagesByChannel: {},
activeChannel: null,
channelType: null,
unreadCounts: {},
lastReadAt: {},
isWindowOpen: false,
windowPosition: null
};
const message = {
id: 'msg-1',
message: 'Hello',
channel: 'session',
sessionId: 'session-abc',
createdAt: '2026-01-26T12:00:00Z'
};
// Add message first time
let state = sessionChatReducer(initialState, addMessageFromWebSocket(message));
expect(state.unreadCounts['session-abc']).toBe(1);
// Try to add same message again (WebSocket + REST scenario)
state = sessionChatReducer(state, addMessageFromWebSocket(message));
expect(state.unreadCounts['session-abc']).toBe(1); // Should NOT increment again
expect(state.messagesByChannel['session-abc']).toHaveLength(1);
});
});