Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 125 additions & 1 deletion packages/odie-client/src/hooks/test/use-get-combined-chat.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ let mockIsChatLoaded = true;
let mockConnectionStatus: string | undefined;
let mockCurrentSupportInteraction: Record< string, unknown > | undefined;
let mockConversation: { id: string; messages: Message[] } | null;
let mockOdieChat: Record< string, unknown > | undefined;
const mockGetZendeskConversation = jest.fn();
const mockStartNewInteraction = jest.fn();

Expand Down Expand Up @@ -43,7 +44,7 @@ jest.mock( '../use-logged-out-session', () => ( {
jest.mock( '../../data', () => ( {
useGetZendeskConversation: () => mockGetZendeskConversation,
useManageSupportInteraction: () => ( { startNewInteraction: mockStartNewInteraction } ),
useOdieChat: () => ( { data: undefined, isFetching: false } ),
useOdieChat: () => ( { data: mockOdieChat, isFetching: false } ),
} ) );

jest.mock( '../../data/use-current-support-interaction', () => ( {
Expand Down Expand Up @@ -100,9 +101,132 @@ beforeEach( () => {
mockConnectionStatus = undefined;
mockCurrentSupportInteraction = undefined;
mockConversation = null;
mockOdieChat = undefined;
mockGetZendeskConversation.mockImplementation( () => Promise.resolve( mockConversation ) );
} );

describe( 'useGetCombinedChat — merging the Odie and Zendesk halves', () => {
const userMessage = ( id: number, content: string ): Message =>
( {
content,
role: 'user',
type: 'message',
message_id: id,
metadata: {},
} ) as unknown as Message;

const countMessages = ( messages: Message[], content: string ) =>
messages.filter( ( message ) => message.content === content ).length;

it( 'keeps a pre-escalation user message single after the conversation is re-fetched', async () => {
mockCurrentSupportInteraction = {
uuid: 'int-1',
conversationId: 'conv-1',
odieId: 42,
status: 'open',
};
mockOdieChat = {
odieId: 42,
wpcomUserId: 99,
conversationId: null,
provider: 'odie',
status: 'loaded',
messages: [ userMessage( 10, 'How do I backup my site?' ) ],
};
mockConversation = { id: 'conv-1', messages: [ agentMessage( 1, 'Happiness Engineer here' ) ] };

const { result, rerender } = renderCombinedChat();

await waitFor( () => {
expect( mockGetZendeskConversation ).toHaveBeenCalledWith( 'conv-1' );
expect(
countMessages( result.current.mainChatState.messages, 'How do I backup my site?' )
).toBe( 1 );
} );

// Any reconnect re-runs the merge against the previous chat state. The second agent
// message marks when that re-merge has landed.
mockConversation = {
id: 'conv-1',
messages: [ agentMessage( 1, 'Happiness Engineer here' ), agentMessage( 2, 'still here' ) ],
};
act( () => {
mockConnectionStatus = 'connected';
} );
rerender();

await waitFor( () => {
expect( result.current.mainChatState.messages.some( ( m ) => m.message_id === 2 ) ).toBe(
true
);
} );

expect(
countMessages( result.current.mainChatState.messages, 'How do I backup my site?' )
).toBe( 1 );
} );

it( 'keeps a queued Zendesk message that the re-fetched conversation does not have yet', async () => {
mockCurrentSupportInteraction = {
uuid: 'int-1',
conversationId: 'conv-1',
odieId: 42,
status: 'open',
};
mockOdieChat = {
odieId: 42,
wpcomUserId: 99,
conversationId: null,
provider: 'odie',
status: 'loaded',
messages: [],
};
mockConversation = { id: 'conv-1', messages: [ agentMessage( 1, 'Happiness Engineer here' ) ] };

const { result, rerender } = renderCombinedChat();

await waitFor( () => {
expect( result.current.mainChatState.messages.some( ( m ) => m.message_id === 1 ) ).toBe(
true
);
} );

// The user sends a message that has not reached the server yet: it lives only in the
// chat state, carrying the temporary id the composer attaches for Zendesk sends.
act( () => {
result.current.setMainChatState( ( chat ) => ( {
...chat,
messages: [
...chat.messages,
{
content: 'my site is down',
role: 'user',
type: 'message',
metadata: { temporary_id: 'temp-1' },
} as unknown as Message,
],
} ) );
} );

mockConversation = {
id: 'conv-1',
messages: [ agentMessage( 1, 'Happiness Engineer here' ), agentMessage( 2, 'still here' ) ],
};
act( () => {
mockConnectionStatus = 'connected';
} );
rerender();

await waitFor( () => {
expect( result.current.mainChatState.messages.some( ( m ) => m.message_id === 2 ) ).toBe(
true
);
} );

expect( countMessages( result.current.mainChatState.messages, 'my site is down' ) ).toBe( 1 );
} );
} );

describe( 'useGetCombinedChat — message recovery on Smooch re-init', () => {
it( 're-fetches the conversation and recovers gap messages when isChatLoaded flips false → true', async () => {
mockCurrentSupportInteraction = {
Expand Down
11 changes: 10 additions & 1 deletion packages/odie-client/src/hooks/use-get-combined-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ function isEqual( message1: Message, message2: Message ) {
return message1Id && message1Id === message2Id;
}

/**
* A user message sent through Zendesk: only those carry a `temporary_id`, so Odie messages don't match.
* @param message - The message to check.
* @returns Whether the message is a Zendesk message sent by the user.
*/
function isQueuedZendeskMessage( message: Message ) {
return message.role === 'user' && !! message.metadata?.temporary_id;
}

/**
* Deduplicate Zendesk messages by their temporary id. During connection recovery, some duplication can occur.
* @param messages - The messages to deduplicate.
Expand Down Expand Up @@ -209,7 +218,7 @@ export const useGetCombinedChat = (
...( deduplicateZDMessages( [
// During connection recovery, the user queued messages can be deleted. This ensure they remain. And `deduplicateZDMessages` takes of duplication.
...( isSameConversation
? prevChat.messages.filter( ( message ) => message.role === 'user' )
? prevChat.messages.filter( isQueuedZendeskMessage )
: [] ),
...conversation.messages,
] ) as Message[] ),
Expand Down
Loading