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:
parent
15a658a5dc
commit
96d7188a4c
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue