@servicetitan/titan-chat-ui 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/components/chat/__tests-cy__/chat-messages.test.js +14 -13
- package/dist/components/chat/__tests-cy__/chat-messages.test.js.map +1 -1
- package/dist/components/chat/__tests-cy__/chat.test.js +21 -20
- package/dist/components/chat/__tests-cy__/chat.test.js.map +1 -1
- package/dist/components/chat/chat-error.js +1 -1
- package/dist/components/chat/chat-error.js.map +1 -1
- package/dist/components/chat/chat-message-template-user.js +1 -1
- package/dist/components/chat/chat-message-template-user.js.map +1 -1
- package/dist/components/chat/chat.js +1 -1
- package/dist/components/chat/chat.js.map +1 -1
- package/dist/components/messages/__tests-cy__/message-agent.test.js +13 -22
- package/dist/components/messages/__tests-cy__/message-agent.test.js.map +1 -1
- package/dist/components/messages/__tests-cy__/message-system.test.js +3 -2
- package/dist/components/messages/__tests-cy__/message-system.test.js.map +1 -1
- package/dist/components/messages/__tests-cy__/message-timeout.test.js +6 -11
- package/dist/components/messages/__tests-cy__/message-timeout.test.js.map +1 -1
- package/dist/components/messages/__tests-cy__/message-typing.test.js +10 -9
- package/dist/components/messages/__tests-cy__/message-typing.test.js.map +1 -1
- package/dist/components/messages/__tests-cy__/message-user.test.js +5 -9
- package/dist/components/messages/__tests-cy__/message-user.test.js.map +1 -1
- package/dist/components/messages/message-agent.d.ts +3 -2
- package/dist/components/messages/message-agent.d.ts.map +1 -1
- package/dist/components/messages/message-agent.js +4 -5
- package/dist/components/messages/message-agent.js.map +1 -1
- package/dist/components/messages/message-avatar.d.ts.map +1 -1
- package/dist/components/messages/message-avatar.js +5 -1
- package/dist/components/messages/message-avatar.js.map +1 -1
- package/dist/components/messages/message-system.d.ts +2 -2
- package/dist/components/messages/message-system.d.ts.map +1 -1
- package/dist/components/messages/message-system.js +2 -2
- package/dist/components/messages/message-system.js.map +1 -1
- package/dist/components/messages/message-timeout.d.ts +1 -2
- package/dist/components/messages/message-timeout.d.ts.map +1 -1
- package/dist/components/messages/message-timeout.js +2 -4
- package/dist/components/messages/message-timeout.js.map +1 -1
- package/dist/components/messages/message-typing.d.ts +1 -2
- package/dist/components/messages/message-typing.d.ts.map +1 -1
- package/dist/components/messages/message-typing.js +1 -5
- package/dist/components/messages/message-typing.js.map +1 -1
- package/dist/components/messages/message-user.d.ts +1 -2
- package/dist/components/messages/message-user.d.ts.map +1 -1
- package/dist/components/messages/message-user.js +3 -6
- package/dist/components/messages/message-user.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/stores/__tests__/chat-input.store.test.js +5 -1
- package/dist/stores/__tests__/chat-input.store.test.js.map +1 -1
- package/dist/stores/__tests__/chat-ui.store.test.d.ts +2 -0
- package/dist/stores/__tests__/chat-ui.store.test.d.ts.map +1 -0
- package/dist/stores/__tests__/chat-ui.store.test.js +424 -0
- package/dist/stores/__tests__/chat-ui.store.test.js.map +1 -0
- package/dist/stores/chat-ui.store.d.ts.map +1 -1
- package/dist/stores/chat-ui.store.js +9 -10
- package/dist/stores/chat-ui.store.js.map +1 -1
- package/dist/utils/__tests__/text-utils.test.d.ts +2 -0
- package/dist/utils/__tests__/text-utils.test.d.ts.map +1 -0
- package/dist/utils/__tests__/text-utils.test.js +59 -0
- package/dist/utils/__tests__/text-utils.test.js.map +1 -0
- package/dist/utils/text-utils.d.ts +0 -5
- package/dist/utils/text-utils.d.ts.map +1 -1
- package/dist/utils/text-utils.js +2 -51
- package/dist/utils/text-utils.js.map +1 -1
- package/package.json +9 -3
- package/src/components/chat/__tests-cy__/chat-messages.test.tsx +17 -17
- package/src/components/chat/__tests-cy__/chat.test.tsx +21 -20
- package/src/components/chat/chat-error.tsx +1 -1
- package/src/components/chat/chat-message-template-user.tsx +2 -2
- package/src/components/chat/chat.tsx +1 -1
- package/src/components/messages/__tests-cy__/message-agent.test.tsx +13 -31
- package/src/components/messages/__tests-cy__/message-system.test.tsx +3 -2
- package/src/components/messages/__tests-cy__/message-timeout.test.tsx +6 -13
- package/src/components/messages/__tests-cy__/message-typing.test.tsx +10 -9
- package/src/components/messages/__tests-cy__/message-user.test.tsx +5 -14
- package/src/components/messages/message-agent.tsx +18 -11
- package/src/components/messages/message-avatar.tsx +7 -3
- package/src/components/messages/message-system.tsx +5 -4
- package/src/components/messages/message-timeout.tsx +9 -8
- package/src/components/messages/message-typing.tsx +12 -16
- package/src/components/messages/message-user.tsx +7 -10
- package/src/index.ts +1 -0
- package/src/stores/__tests__/chat-input.store.test.ts +5 -1
- package/src/stores/__tests__/chat-ui.store.test.ts +531 -0
- package/src/stores/chat-ui.store.ts +9 -10
- package/src/utils/__tests__/text-utils.test.ts +70 -0
- package/src/utils/text-utils.ts +2 -59
- package/tsconfig.json +5 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/models/component.d.ts +0 -4
- package/dist/models/component.d.ts.map +0 -1
- package/dist/models/component.js +0 -2
- package/dist/models/component.js.map +0 -1
- package/dist/utils/test-utils.d.ts +0 -5
- package/dist/utils/test-utils.d.ts.map +0 -1
- package/dist/utils/test-utils.js +0 -17
- package/dist/utils/test-utils.js.map +0 -1
- package/src/models/component.ts +0 -3
- package/src/utils/test-utils.ts +0 -22
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { expect } from '@jest/globals';
|
|
2
|
+
import { initTestContainer } from '@local/utils';
|
|
3
|
+
import { FileDescriptor } from '@servicetitan/form';
|
|
4
|
+
import { Container } from '@servicetitan/react-ioc';
|
|
5
|
+
import {
|
|
6
|
+
ChatCustomizations,
|
|
7
|
+
ChatEndReason,
|
|
8
|
+
ChatError,
|
|
9
|
+
ChatMessageModelBase,
|
|
10
|
+
ChatMessageModelText,
|
|
11
|
+
ChatMessageModelWelcome,
|
|
12
|
+
ChatMessageState,
|
|
13
|
+
ChatParticipantIcon,
|
|
14
|
+
ChatRunState,
|
|
15
|
+
} from '../../models';
|
|
16
|
+
import { ChatUiEvent, ChatUiStore, symbolAgent, symbolUser } from '../chat-ui.store';
|
|
17
|
+
|
|
18
|
+
const mockAudio = {
|
|
19
|
+
Audio: {
|
|
20
|
+
play: jest.fn().mockResolvedValue(undefined),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
const mockEventEmitter = {
|
|
24
|
+
on: jest.fn(),
|
|
25
|
+
off: jest.fn(),
|
|
26
|
+
emit: jest.fn(),
|
|
27
|
+
listeners: jest.fn(),
|
|
28
|
+
listenerCount: jest.fn(),
|
|
29
|
+
};
|
|
30
|
+
jest.mock('events', () => ({
|
|
31
|
+
EventEmitter: class {
|
|
32
|
+
on = mockEventEmitter.on;
|
|
33
|
+
off = mockEventEmitter.off;
|
|
34
|
+
emit = mockEventEmitter.emit;
|
|
35
|
+
listeners = mockEventEmitter.listeners;
|
|
36
|
+
listenerCount = mockEventEmitter.listenerCount;
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const initContainer = initTestContainer(ChatUiStore, () => {});
|
|
41
|
+
|
|
42
|
+
describe('[ChatUiStore]', () => {
|
|
43
|
+
let container: Container;
|
|
44
|
+
let store: ChatUiStore<ChatCustomizations>;
|
|
45
|
+
let audioBackup: any;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
container = initContainer();
|
|
49
|
+
store = container.get(ChatUiStore);
|
|
50
|
+
jest.useFakeTimers();
|
|
51
|
+
jest.setSystemTime(Date.parse('2021-01-01T00:00:00.000Z'));
|
|
52
|
+
audioBackup = global.Audio;
|
|
53
|
+
global.Audio = jest.fn().mockImplementation(() => ({
|
|
54
|
+
play: mockAudio.Audio.play,
|
|
55
|
+
}));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
if (audioBackup) {
|
|
60
|
+
global.Audio = audioBackup;
|
|
61
|
+
}
|
|
62
|
+
jest.useRealTimers();
|
|
63
|
+
jest.clearAllMocks();
|
|
64
|
+
mockEventEmitter.on.mockClear();
|
|
65
|
+
mockEventEmitter.off.mockClear();
|
|
66
|
+
mockEventEmitter.emit.mockClear();
|
|
67
|
+
mockEventEmitter.listeners.mockClear();
|
|
68
|
+
mockEventEmitter.listenerCount.mockClear();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('should initialize with default values', () => {
|
|
72
|
+
expect(store.isAgentTyping).toBe(false);
|
|
73
|
+
expect(store.status).toBe(ChatRunState.Offline);
|
|
74
|
+
expect(store.isFilePickerEnabled).toBe(false);
|
|
75
|
+
expect(store.file).toBeUndefined();
|
|
76
|
+
expect(store.currentFileMessage).toBeUndefined();
|
|
77
|
+
expect(store.error).toBeUndefined();
|
|
78
|
+
expect(store.endReason).toBeUndefined();
|
|
79
|
+
expect(store.messages).toEqual([]);
|
|
80
|
+
expect(store.scrollCounter).toBe(1);
|
|
81
|
+
expect(store.timer).toBeUndefined();
|
|
82
|
+
expect(store.participants[symbolAgent].isAgent).toBe(true);
|
|
83
|
+
expect(store.participants[symbolAgent].name).toBe('Agent');
|
|
84
|
+
expect(store.participants[symbolAgent].icon).toBe(ChatParticipantIcon.Initials);
|
|
85
|
+
expect(store.participants[symbolUser].isAgent).toBe(false);
|
|
86
|
+
expect(store.participants[symbolUser].name).toBe('User');
|
|
87
|
+
expect(store.participants[symbolUser].icon).toBe(ChatParticipantIcon.Initials);
|
|
88
|
+
// Getters
|
|
89
|
+
expect(store.agent).toBe(store.participants[symbolAgent]);
|
|
90
|
+
expect(store.user).toBe(store.participants[symbolUser]);
|
|
91
|
+
expect(store.isStarted).toBe(false);
|
|
92
|
+
expect(store.isError).toBe(false);
|
|
93
|
+
expect(store.isEnded).toBe(false);
|
|
94
|
+
expect(store.isStarting).toBe(false);
|
|
95
|
+
expect(store.customizations).toEqual({});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('should subscribe and unsubscribe to events', () => {
|
|
99
|
+
const mockCallback = jest.fn();
|
|
100
|
+
store.on('testEvent', mockCallback);
|
|
101
|
+
expect(mockEventEmitter.on).toHaveBeenCalledWith('testEvent', mockCallback);
|
|
102
|
+
|
|
103
|
+
store.off('testEvent', mockCallback);
|
|
104
|
+
expect(mockEventEmitter.off).toHaveBeenCalledWith('testEvent', mockCallback);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('should set customizations context', async () => {
|
|
108
|
+
const customizations = { abc: '123' } as ChatCustomizations;
|
|
109
|
+
store.setCustomizationContext(customizations);
|
|
110
|
+
expect(store.customizations).toEqual(customizations);
|
|
111
|
+
await checkScrollTriggered();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('should getLastFailedMessages', () => {
|
|
115
|
+
store.addMessage(false, 'message1');
|
|
116
|
+
const m2 = store.addMessage(false, 'message2');
|
|
117
|
+
const m3 = store.addMessage(false, 'message3');
|
|
118
|
+
expect(store.getLastFailedMessages()).toEqual([]);
|
|
119
|
+
|
|
120
|
+
store.setMessageState(m3, ChatMessageState.Failed);
|
|
121
|
+
let failedMessages = store.getLastFailedMessages();
|
|
122
|
+
expect(failedMessages).toEqual([m3]);
|
|
123
|
+
|
|
124
|
+
store.setMessageState(m2, ChatMessageState.Failed);
|
|
125
|
+
failedMessages = store.getLastFailedMessages();
|
|
126
|
+
expect(failedMessages).toEqual([m3, m2]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('with file messages', () => {
|
|
130
|
+
test('should set file', () => {
|
|
131
|
+
const file = mockFile();
|
|
132
|
+
store.setFile(file);
|
|
133
|
+
expect(store.file).toBe(file);
|
|
134
|
+
|
|
135
|
+
store.setFile(undefined);
|
|
136
|
+
expect(store.file).toBeUndefined();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('should set file picker enabled state', async () => {
|
|
140
|
+
store.setFile(mockFile());
|
|
141
|
+
store.setFilePickerEnabled(true);
|
|
142
|
+
expect(store.isFilePickerEnabled).toBe(true);
|
|
143
|
+
expect(store.file).toBeUndefined();
|
|
144
|
+
await checkScrollTriggered();
|
|
145
|
+
|
|
146
|
+
store.setFile(mockFile());
|
|
147
|
+
store.setFilePickerEnabled(false);
|
|
148
|
+
expect(store.isFilePickerEnabled).toBe(false);
|
|
149
|
+
expect(store.file).toBeUndefined();
|
|
150
|
+
await checkScrollTriggered();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('with chat errors', () => {
|
|
155
|
+
test('should set error', async () => {
|
|
156
|
+
store.setError('Test error', {
|
|
157
|
+
title: 'Test error title',
|
|
158
|
+
recoverStrategy: { recoverButtonTitle: 'Test error button' },
|
|
159
|
+
data: { some: 'data' },
|
|
160
|
+
});
|
|
161
|
+
expect(store.error?.toJSON()).toEqual({
|
|
162
|
+
data: { some: 'data' },
|
|
163
|
+
message: 'Test error',
|
|
164
|
+
name: 'ChatError',
|
|
165
|
+
recoverStrategy: {
|
|
166
|
+
recoverButtonTitle: 'Test error button',
|
|
167
|
+
},
|
|
168
|
+
title: 'Test error title',
|
|
169
|
+
});
|
|
170
|
+
expect(store.status).toBe(ChatRunState.Error);
|
|
171
|
+
expect(store.isError).toBe(true);
|
|
172
|
+
await checkScrollTriggered();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('should reset error', async () => {
|
|
176
|
+
store.setError('Test error');
|
|
177
|
+
store.resetError(ChatRunState.Started);
|
|
178
|
+
expect(store.error).toBeUndefined();
|
|
179
|
+
expect(store.status).toBe(ChatRunState.Started);
|
|
180
|
+
await checkScrollTriggered();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('with messages', () => {
|
|
185
|
+
function expectText(message: ChatMessageModelBase, expected: string) {
|
|
186
|
+
expect(message.type).toBe('message');
|
|
187
|
+
expect((message as ChatMessageModelText).message).toBe(expected);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
test('should add message without sound', async () => {
|
|
191
|
+
store.addMessageWelcome('welcome');
|
|
192
|
+
await jest.runOnlyPendingTimersAsync();
|
|
193
|
+
expect(mockAudio.Audio.play).toHaveBeenCalledTimes(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('should add message', async () => {
|
|
197
|
+
store.setIncomingSound('non-existing-sound.mp3');
|
|
198
|
+
store.setIncomingSound('non-existing-sound2.mp3');
|
|
199
|
+
|
|
200
|
+
// Welcome message
|
|
201
|
+
store.addMessageWelcome('welcome');
|
|
202
|
+
await jest.runOnlyPendingTimersAsync();
|
|
203
|
+
expect(mockAudio.Audio.play).toHaveBeenCalledTimes(1);
|
|
204
|
+
|
|
205
|
+
// Incoming message
|
|
206
|
+
store.addMessage(true, 'message1');
|
|
207
|
+
await jest.runOnlyPendingTimersAsync();
|
|
208
|
+
expect(mockAudio.Audio.play).toHaveBeenCalledTimes(2);
|
|
209
|
+
|
|
210
|
+
// Outgoing message
|
|
211
|
+
store.addMessage(false, 'message2');
|
|
212
|
+
await jest.runOnlyPendingTimersAsync();
|
|
213
|
+
expect(mockAudio.Audio.play).toHaveBeenCalledTimes(2); // No sound for outgoing messages
|
|
214
|
+
|
|
215
|
+
// File message
|
|
216
|
+
store.addMessageFile({
|
|
217
|
+
file: new File([], 'fileName'),
|
|
218
|
+
displayName: 'displayName',
|
|
219
|
+
});
|
|
220
|
+
await jest.runOnlyPendingTimersAsync();
|
|
221
|
+
expect(mockAudio.Audio.play).toHaveBeenCalledTimes(2); // No sound for outgoing messages
|
|
222
|
+
|
|
223
|
+
expect(store.messages.length).toBe(4);
|
|
224
|
+
expect(store.messages[0].participant.isAgent).toBe(true);
|
|
225
|
+
expect((store.messages[0] as ChatMessageModelWelcome).message).toBe('welcome');
|
|
226
|
+
expect(store.messages[1].participant.isAgent).toBe(true);
|
|
227
|
+
expectText(store.messages[1], 'message1');
|
|
228
|
+
expect(store.messages[2].participant.isAgent).toBe(false);
|
|
229
|
+
expectText(store.messages[2], 'message2');
|
|
230
|
+
expect(store.messages[3].participant.isAgent).toBe(false);
|
|
231
|
+
expect(store.messages[3].type).toBe('file');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('should addMessageTimeout', async () => {
|
|
235
|
+
// Adding timeout message should not happen when no messages are present
|
|
236
|
+
store.addMessageTimeout();
|
|
237
|
+
expect(store.messages.length).toBe(0);
|
|
238
|
+
|
|
239
|
+
// Adding timeout message should not happen when no non-welcome messages are present
|
|
240
|
+
store.addMessageWelcome('welcome');
|
|
241
|
+
store.addMessageTimeout();
|
|
242
|
+
expect(store.messages.length).toBe(1);
|
|
243
|
+
expect(store.messages[0].type).toBe('welcome');
|
|
244
|
+
|
|
245
|
+
// Adding timeout message should happen when at least one message is present
|
|
246
|
+
store.addMessage(true, 'message1');
|
|
247
|
+
store.addMessageTimeout();
|
|
248
|
+
expect(store.messages.length).toBe(3);
|
|
249
|
+
expect(store.messages.at(-1)?.type).toBe('timeout');
|
|
250
|
+
|
|
251
|
+
// Adding new message should dismiss the timeout message
|
|
252
|
+
store.addMessage(true, 'message2');
|
|
253
|
+
expect(store.messages.some(message => message.type === 'timeout')).toBe(false);
|
|
254
|
+
await checkScrollTriggered();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('should configure', () => {
|
|
259
|
+
store.configure({
|
|
260
|
+
agentIcon: ChatParticipantIcon.Bot,
|
|
261
|
+
agentName: 'Test Agent',
|
|
262
|
+
});
|
|
263
|
+
expect(store.participants[symbolAgent].icon).toBe(ChatParticipantIcon.Bot);
|
|
264
|
+
expect(store.participants[symbolAgent].name).toBe('Test Agent');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('with chat events', () => {
|
|
268
|
+
test('should run', async () => {
|
|
269
|
+
// Without subscription: should throw an error
|
|
270
|
+
mockEventEmitter.listeners.mockImplementation(() => []);
|
|
271
|
+
await expect(store.run()).rejects.toThrow(
|
|
272
|
+
"No listeners for ChatUiEvent.eventRun ('eventRun')"
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const spyConfigure = jest.spyOn(store, 'configure');
|
|
276
|
+
mockEmit();
|
|
277
|
+
|
|
278
|
+
await store.run(
|
|
279
|
+
{
|
|
280
|
+
agentIcon: ChatParticipantIcon.Bot,
|
|
281
|
+
agentName: 'Test Agent',
|
|
282
|
+
},
|
|
283
|
+
'chat data'
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
expect(spyConfigure).toHaveBeenCalled();
|
|
287
|
+
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
288
|
+
ChatUiEvent.eventRun,
|
|
289
|
+
expect.any(Function),
|
|
290
|
+
expect.any(Function),
|
|
291
|
+
'chat data'
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('should recover', async () => {
|
|
296
|
+
mockEmit();
|
|
297
|
+
store.setError('Test error');
|
|
298
|
+
await store.recover();
|
|
299
|
+
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
300
|
+
ChatUiEvent.eventRecover,
|
|
301
|
+
expect.any(Function),
|
|
302
|
+
expect.any(Function),
|
|
303
|
+
new ChatError('Test error')
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('should destroy', async () => {
|
|
308
|
+
const spyReset = jest.spyOn(store, 'reset');
|
|
309
|
+
mockEmit();
|
|
310
|
+
await store.destroy();
|
|
311
|
+
expect(spyReset).toHaveBeenCalledWith();
|
|
312
|
+
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
313
|
+
ChatUiEvent.eventDestroy,
|
|
314
|
+
expect.any(Function),
|
|
315
|
+
expect.any(Function)
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('should restart', async () => {
|
|
320
|
+
const spyReset = jest.spyOn(store, 'reset');
|
|
321
|
+
mockEmit();
|
|
322
|
+
await store.restart();
|
|
323
|
+
expect(spyReset).toHaveBeenCalledWith();
|
|
324
|
+
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
325
|
+
ChatUiEvent.eventRestart,
|
|
326
|
+
expect.any(Function),
|
|
327
|
+
expect.any(Function)
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('should cancelChat', async () => {
|
|
332
|
+
mockEmit();
|
|
333
|
+
await store.cancelChat();
|
|
334
|
+
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
335
|
+
ChatUiEvent.eventCancel,
|
|
336
|
+
expect.any(Function),
|
|
337
|
+
expect.any(Function)
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('should endChat', async () => {
|
|
342
|
+
mockEmit();
|
|
343
|
+
await store.endChat(ChatEndReason.ByAgent);
|
|
344
|
+
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
345
|
+
ChatUiEvent.eventEndChat,
|
|
346
|
+
expect.any(Function),
|
|
347
|
+
expect.any(Function),
|
|
348
|
+
ChatEndReason.ByAgent
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('should chasitorTyping', async () => {
|
|
353
|
+
mockEmit();
|
|
354
|
+
await store.chasitorTyping(false);
|
|
355
|
+
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
356
|
+
ChatUiEvent.eventChasitorTyping,
|
|
357
|
+
expect.any(Function),
|
|
358
|
+
expect.any(Function),
|
|
359
|
+
false
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
await store.chasitorTyping(true);
|
|
363
|
+
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
364
|
+
ChatUiEvent.eventChasitorTyping,
|
|
365
|
+
expect.any(Function),
|
|
366
|
+
expect.any(Function),
|
|
367
|
+
true
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('should sendMessageText', async () => {
|
|
372
|
+
mockEmit();
|
|
373
|
+
await store.sendMessageText('Hello, world!');
|
|
374
|
+
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
375
|
+
ChatUiEvent.eventMessageSend,
|
|
376
|
+
expect.any(Function),
|
|
377
|
+
expect.any(Function),
|
|
378
|
+
expect.objectContaining({
|
|
379
|
+
message: 'Hello, world!',
|
|
380
|
+
type: 'message',
|
|
381
|
+
state: ChatMessageState.Delivering,
|
|
382
|
+
})
|
|
383
|
+
);
|
|
384
|
+
expect(store.messages.length).toBe(1);
|
|
385
|
+
expect(store.messages[0].type).toBe('message');
|
|
386
|
+
expect((store.messages[0] as ChatMessageModelText).message).toBe('Hello, world!');
|
|
387
|
+
await checkScrollTriggered();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('should sendMessageText with file', async () => {
|
|
391
|
+
mockEmit();
|
|
392
|
+
const file = mockFile();
|
|
393
|
+
store.setFilePickerEnabled(true);
|
|
394
|
+
store.setFile(file);
|
|
395
|
+
await store.sendMessageText('Hello, world!');
|
|
396
|
+
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
397
|
+
ChatUiEvent.eventMessageSendFile,
|
|
398
|
+
expect.any(Function),
|
|
399
|
+
expect.any(Function),
|
|
400
|
+
store.currentFileMessage,
|
|
401
|
+
store.file,
|
|
402
|
+
expect.objectContaining({
|
|
403
|
+
message: 'Hello, world!',
|
|
404
|
+
type: 'message',
|
|
405
|
+
state: ChatMessageState.Delivering,
|
|
406
|
+
})
|
|
407
|
+
);
|
|
408
|
+
expect(store.messages.length).toBe(2);
|
|
409
|
+
expect(store.messages[0].type).toBe('file');
|
|
410
|
+
expect(store.messages[1].type).toBe('message');
|
|
411
|
+
expect((store.messages[1] as ChatMessageModelText).message).toBe('Hello, world!');
|
|
412
|
+
expect(store.currentFileMessage).toBeDefined();
|
|
413
|
+
await checkScrollTriggered();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test('should sendMessageText empty', async () => {
|
|
417
|
+
mockEmit();
|
|
418
|
+
await store.sendMessageText(' ');
|
|
419
|
+
expect(mockEventEmitter.emit).not.toHaveBeenCalled();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('should sendMessageRetry', async () => {
|
|
423
|
+
mockEmit();
|
|
424
|
+
|
|
425
|
+
store.setError('Test error');
|
|
426
|
+
const message = store.addMessage(true, 'message1');
|
|
427
|
+
store.setMessageState(message, ChatMessageState.Failed);
|
|
428
|
+
|
|
429
|
+
const p = store.sendMessageRetry(message);
|
|
430
|
+
|
|
431
|
+
expect(message.state).toBe(ChatMessageState.Delivering);
|
|
432
|
+
await p;
|
|
433
|
+
expect(store.isError).toBe(false);
|
|
434
|
+
expect(store.status).toBe(ChatRunState.Started);
|
|
435
|
+
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
436
|
+
ChatUiEvent.eventMessageSendRetry,
|
|
437
|
+
expect.any(Function),
|
|
438
|
+
expect.any(Function),
|
|
439
|
+
message
|
|
440
|
+
);
|
|
441
|
+
expect(message.state).toBe(ChatMessageState.Delivering);
|
|
442
|
+
await checkScrollTriggered();
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test('should sendMessageFileRetry', async () => {
|
|
446
|
+
mockEmit();
|
|
447
|
+
|
|
448
|
+
store.setError('Test error');
|
|
449
|
+
const fileMessage = store.addMessageFile({
|
|
450
|
+
file: new File([], 'fileName'),
|
|
451
|
+
displayName: 'displayName',
|
|
452
|
+
});
|
|
453
|
+
store.setMessageState(fileMessage, ChatMessageState.Failed);
|
|
454
|
+
|
|
455
|
+
const p = store.sendMessageFileRetry(fileMessage);
|
|
456
|
+
|
|
457
|
+
expect(fileMessage.state).toBe(ChatMessageState.Delivering);
|
|
458
|
+
await p;
|
|
459
|
+
expect(store.isError).toBe(false);
|
|
460
|
+
expect(store.status).toBe(ChatRunState.Started);
|
|
461
|
+
expect(mockEventEmitter.emit).toHaveBeenCalledWith(
|
|
462
|
+
ChatUiEvent.eventMessageSendFileRetry,
|
|
463
|
+
expect.any(Function),
|
|
464
|
+
expect.any(Function),
|
|
465
|
+
fileMessage
|
|
466
|
+
);
|
|
467
|
+
expect(fileMessage.state).toBe(ChatMessageState.Delivering);
|
|
468
|
+
await checkScrollTriggered();
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test('should setTimer', () => {
|
|
473
|
+
store.setTimer({ secondsLeft: 10, secondsTotal: 20 });
|
|
474
|
+
expect(store.timer).toEqual({
|
|
475
|
+
secondsLeft: 10,
|
|
476
|
+
secondsTotal: 20,
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test('should setAgentTyping', async () => {
|
|
481
|
+
store.setAgentTyping(true);
|
|
482
|
+
expect(store.isAgentTyping).toBe(true);
|
|
483
|
+
await checkScrollTriggered();
|
|
484
|
+
|
|
485
|
+
store.setAgentTyping(false);
|
|
486
|
+
expect(store.isAgentTyping).toBe(false);
|
|
487
|
+
await checkScrollTriggered();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test('should setEndReason', async () => {
|
|
491
|
+
store.setEndReason(ChatEndReason.ByTimeout);
|
|
492
|
+
expect(store.endReason).toBe(ChatEndReason.ByTimeout);
|
|
493
|
+
await checkScrollTriggered();
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test('should setEndedChatState', async () => {
|
|
497
|
+
store.isFilePickerEnabled = true;
|
|
498
|
+
store.setFile(mockFile());
|
|
499
|
+
store.setEndedChatState(ChatEndReason.ByTimeout);
|
|
500
|
+
expect(store.endReason).toBe(ChatEndReason.ByTimeout);
|
|
501
|
+
expect(store.status).toBe(ChatRunState.Ended);
|
|
502
|
+
expect(store.isAgentTyping).toBe(false);
|
|
503
|
+
expect(store.currentFileMessage).toBeUndefined();
|
|
504
|
+
expect(store.file).toBeUndefined();
|
|
505
|
+
expect(store.isFilePickerEnabled).toBe(false);
|
|
506
|
+
await checkScrollTriggered();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
function mockFile() {
|
|
510
|
+
const file: FileDescriptor = {
|
|
511
|
+
file: new File(['content'], 'file.txt', { type: 'text/plain' }),
|
|
512
|
+
displayName: 'file.txt',
|
|
513
|
+
};
|
|
514
|
+
return file;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function mockEmit() {
|
|
518
|
+
mockEventEmitter.listeners.mockImplementation(() => ['1']);
|
|
519
|
+
mockEventEmitter.listenerCount.mockReturnValue(1);
|
|
520
|
+
mockEventEmitter.emit.mockImplementation(async (eventName: string, resolve: () => void) => {
|
|
521
|
+
await Promise.resolve();
|
|
522
|
+
resolve();
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function checkScrollTriggered() {
|
|
527
|
+
const counter = store.scrollCounter;
|
|
528
|
+
await jest.runOnlyPendingTimersAsync();
|
|
529
|
+
expect(counter < store.scrollCounter).toBe(true);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
@@ -237,8 +237,7 @@ export class ChatUiStore<T extends ChatCustomizations> implements IChatUiStore<T
|
|
|
237
237
|
|
|
238
238
|
@action
|
|
239
239
|
setError<T = any>(message: string, options?: ChatErrorOptions<T>) {
|
|
240
|
-
this.
|
|
241
|
-
this.setChatError(this.error);
|
|
240
|
+
this.setChatError(new ChatError(message, options));
|
|
242
241
|
}
|
|
243
242
|
|
|
244
243
|
@action
|
|
@@ -332,11 +331,12 @@ export class ChatUiStore<T extends ChatCustomizations> implements IChatUiStore<T
|
|
|
332
331
|
|
|
333
332
|
@action
|
|
334
333
|
async sendMessageText(message: string) {
|
|
335
|
-
await this.restartTimers();
|
|
336
334
|
if (this.isFilePickerEnabled && this.file) {
|
|
337
335
|
// Send text message with file
|
|
338
|
-
|
|
339
|
-
|
|
336
|
+
runInAction(() => {
|
|
337
|
+
this.isFilePickerEnabled = false;
|
|
338
|
+
this.currentFileMessage = this.addMessageFile(this.file!);
|
|
339
|
+
});
|
|
340
340
|
let messageModel: ChatMessageModelText | undefined;
|
|
341
341
|
if (message.trim() !== '') {
|
|
342
342
|
messageModel = this.addMessage(false, message);
|
|
@@ -355,10 +355,10 @@ export class ChatUiStore<T extends ChatCustomizations> implements IChatUiStore<T
|
|
|
355
355
|
|
|
356
356
|
@action
|
|
357
357
|
async sendMessageRetry(messageModel: ChatMessageModelBase) {
|
|
358
|
-
await this.restartTimers();
|
|
359
358
|
this.resetError(ChatRunState.Started);
|
|
360
359
|
this.setMessageState(messageModel, ChatMessageState.Delivering);
|
|
361
360
|
await this.emitAsync(ChatUiEvent.eventMessageSendRetry, messageModel);
|
|
361
|
+
await this.restartTimers();
|
|
362
362
|
}
|
|
363
363
|
|
|
364
364
|
@action
|
|
@@ -460,7 +460,7 @@ export class ChatUiStore<T extends ChatCustomizations> implements IChatUiStore<T
|
|
|
460
460
|
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
461
461
|
const message = this.messages[i];
|
|
462
462
|
if (message.type === 'message' && !message.participant.isAgent) {
|
|
463
|
-
if (message.state
|
|
463
|
+
if (message.state !== ChatMessageState.Failed) {
|
|
464
464
|
break;
|
|
465
465
|
}
|
|
466
466
|
result.push(message as ChatMessageModelText);
|
|
@@ -494,12 +494,11 @@ export class ChatUiStore<T extends ChatCustomizations> implements IChatUiStore<T
|
|
|
494
494
|
} else {
|
|
495
495
|
this.restartTimers().then(() => {});
|
|
496
496
|
}
|
|
497
|
-
|
|
498
|
-
this.setMessages([...this.messages, newMessage]);
|
|
497
|
+
this.setMessages([...this.messages, this.createMessage<T>(message)]);
|
|
499
498
|
if (message.participant.isAgent) {
|
|
500
499
|
this.beepIncoming().catch(() => {});
|
|
501
500
|
}
|
|
502
|
-
return
|
|
501
|
+
return this.messages.at(-1) as T;
|
|
503
502
|
}
|
|
504
503
|
|
|
505
504
|
protected removeMessageByType(type: string) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { expect } from '@jest/globals';
|
|
2
|
+
import {
|
|
3
|
+
extractFilenameAndExt,
|
|
4
|
+
formatChatMessageDate,
|
|
5
|
+
getFirstName,
|
|
6
|
+
getNameInitials,
|
|
7
|
+
getNameInitialsFirst,
|
|
8
|
+
} from '../text-utils';
|
|
9
|
+
|
|
10
|
+
describe('text-utils', () => {
|
|
11
|
+
test('should get initials from name', () => {
|
|
12
|
+
expect(getNameInitials('')).toEqual('');
|
|
13
|
+
expect(getNameInitials('A')).toEqual('A');
|
|
14
|
+
expect(getNameInitials('AB')).toEqual('A');
|
|
15
|
+
expect(getNameInitials('abcd')).toEqual('A');
|
|
16
|
+
expect(getNameInitials(' abcd ')).toEqual('A');
|
|
17
|
+
expect(getNameInitials(' a b c ')).toEqual('AB');
|
|
18
|
+
expect(getNameInitials('abcd ef')).toEqual('AE');
|
|
19
|
+
expect(getNameInitials('a b c d')).toEqual('AB');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('should extract filename and ext', () => {
|
|
23
|
+
expect(extractFilenameAndExt('')).toEqual(['', '']);
|
|
24
|
+
expect(extractFilenameAndExt('a')).toEqual(['a', '']);
|
|
25
|
+
expect(extractFilenameAndExt('a.')).toEqual(['a.', '']);
|
|
26
|
+
expect(extractFilenameAndExt('a.b')).toEqual(['a.', 'b']);
|
|
27
|
+
expect(extractFilenameAndExt('a.b.c')).toEqual(['a.b.', 'c']);
|
|
28
|
+
expect(extractFilenameAndExt('aaaa.bbbb.cccc')).toEqual(['aaaa.bbbb.', 'cccc']);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should getFirstName', () => {
|
|
32
|
+
expect(getFirstName('')).toEqual('');
|
|
33
|
+
expect(getFirstName('A')).toEqual('A');
|
|
34
|
+
expect(getFirstName('AB')).toEqual('AB');
|
|
35
|
+
expect(getFirstName('abcd')).toEqual('abcd');
|
|
36
|
+
expect(getFirstName(' abcd ')).toEqual('abcd');
|
|
37
|
+
expect(getFirstName(' a b c ')).toEqual('a');
|
|
38
|
+
expect(getFirstName('abcd ef')).toEqual('abcd');
|
|
39
|
+
expect(getFirstName('a b c d')).toEqual('a');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should getNameInitialsFirst', () => {
|
|
43
|
+
expect(getNameInitials('')).toEqual('');
|
|
44
|
+
expect(getNameInitials('A')).toEqual('A');
|
|
45
|
+
expect(getNameInitials('AB')).toEqual('A');
|
|
46
|
+
expect(getNameInitials('abcd')).toEqual('A');
|
|
47
|
+
expect(getNameInitials(' abcd ')).toEqual('A');
|
|
48
|
+
expect(getNameInitials(' a b c ')).toEqual('AB');
|
|
49
|
+
expect(getNameInitials('abcd ef')).toEqual('AE');
|
|
50
|
+
expect(getNameInitials('a b c d')).toEqual('AB');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('should formatChatMessageDate', () => {
|
|
54
|
+
const date = new Date('2023-10-01T12:00:00Z');
|
|
55
|
+
expect(formatChatMessageDate(date)).toEqual('12:00 PM');
|
|
56
|
+
expect(formatChatMessageDate(new Date('2023-10-01T00:00:00Z'))).toEqual('12:00 AM');
|
|
57
|
+
expect(formatChatMessageDate(new Date('2023-10-01T23:59:59Z'))).toEqual('11:59 PM');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('should getNameInitialsFirst', () => {
|
|
61
|
+
expect(getNameInitialsFirst('')).toEqual('');
|
|
62
|
+
expect(getNameInitialsFirst('A')).toEqual('A');
|
|
63
|
+
expect(getNameInitialsFirst('AB')).toEqual('A');
|
|
64
|
+
expect(getNameInitialsFirst('abcd')).toEqual('A');
|
|
65
|
+
expect(getNameInitialsFirst(' abcd ')).toEqual('A');
|
|
66
|
+
expect(getNameInitialsFirst(' a b c ')).toEqual('A');
|
|
67
|
+
expect(getNameInitialsFirst('abcd ef')).toEqual('A');
|
|
68
|
+
expect(getNameInitialsFirst('a b c d')).toEqual('A');
|
|
69
|
+
});
|
|
70
|
+
});
|