@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/components/chat/__tests-cy__/chat-messages.test.js +14 -13
  3. package/dist/components/chat/__tests-cy__/chat-messages.test.js.map +1 -1
  4. package/dist/components/chat/__tests-cy__/chat.test.js +21 -20
  5. package/dist/components/chat/__tests-cy__/chat.test.js.map +1 -1
  6. package/dist/components/chat/chat-error.js +1 -1
  7. package/dist/components/chat/chat-error.js.map +1 -1
  8. package/dist/components/chat/chat-message-template-user.js +1 -1
  9. package/dist/components/chat/chat-message-template-user.js.map +1 -1
  10. package/dist/components/chat/chat.js +1 -1
  11. package/dist/components/chat/chat.js.map +1 -1
  12. package/dist/components/messages/__tests-cy__/message-agent.test.js +13 -22
  13. package/dist/components/messages/__tests-cy__/message-agent.test.js.map +1 -1
  14. package/dist/components/messages/__tests-cy__/message-system.test.js +3 -2
  15. package/dist/components/messages/__tests-cy__/message-system.test.js.map +1 -1
  16. package/dist/components/messages/__tests-cy__/message-timeout.test.js +6 -11
  17. package/dist/components/messages/__tests-cy__/message-timeout.test.js.map +1 -1
  18. package/dist/components/messages/__tests-cy__/message-typing.test.js +10 -9
  19. package/dist/components/messages/__tests-cy__/message-typing.test.js.map +1 -1
  20. package/dist/components/messages/__tests-cy__/message-user.test.js +5 -9
  21. package/dist/components/messages/__tests-cy__/message-user.test.js.map +1 -1
  22. package/dist/components/messages/message-agent.d.ts +3 -2
  23. package/dist/components/messages/message-agent.d.ts.map +1 -1
  24. package/dist/components/messages/message-agent.js +4 -5
  25. package/dist/components/messages/message-agent.js.map +1 -1
  26. package/dist/components/messages/message-avatar.d.ts.map +1 -1
  27. package/dist/components/messages/message-avatar.js +5 -1
  28. package/dist/components/messages/message-avatar.js.map +1 -1
  29. package/dist/components/messages/message-system.d.ts +2 -2
  30. package/dist/components/messages/message-system.d.ts.map +1 -1
  31. package/dist/components/messages/message-system.js +2 -2
  32. package/dist/components/messages/message-system.js.map +1 -1
  33. package/dist/components/messages/message-timeout.d.ts +1 -2
  34. package/dist/components/messages/message-timeout.d.ts.map +1 -1
  35. package/dist/components/messages/message-timeout.js +2 -4
  36. package/dist/components/messages/message-timeout.js.map +1 -1
  37. package/dist/components/messages/message-typing.d.ts +1 -2
  38. package/dist/components/messages/message-typing.d.ts.map +1 -1
  39. package/dist/components/messages/message-typing.js +1 -5
  40. package/dist/components/messages/message-typing.js.map +1 -1
  41. package/dist/components/messages/message-user.d.ts +1 -2
  42. package/dist/components/messages/message-user.d.ts.map +1 -1
  43. package/dist/components/messages/message-user.js +3 -6
  44. package/dist/components/messages/message-user.js.map +1 -1
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +1 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/stores/__tests__/chat-input.store.test.js +5 -1
  50. package/dist/stores/__tests__/chat-input.store.test.js.map +1 -1
  51. package/dist/stores/__tests__/chat-ui.store.test.d.ts +2 -0
  52. package/dist/stores/__tests__/chat-ui.store.test.d.ts.map +1 -0
  53. package/dist/stores/__tests__/chat-ui.store.test.js +424 -0
  54. package/dist/stores/__tests__/chat-ui.store.test.js.map +1 -0
  55. package/dist/stores/chat-ui.store.d.ts.map +1 -1
  56. package/dist/stores/chat-ui.store.js +9 -10
  57. package/dist/stores/chat-ui.store.js.map +1 -1
  58. package/dist/utils/__tests__/text-utils.test.d.ts +2 -0
  59. package/dist/utils/__tests__/text-utils.test.d.ts.map +1 -0
  60. package/dist/utils/__tests__/text-utils.test.js +59 -0
  61. package/dist/utils/__tests__/text-utils.test.js.map +1 -0
  62. package/dist/utils/text-utils.d.ts +0 -5
  63. package/dist/utils/text-utils.d.ts.map +1 -1
  64. package/dist/utils/text-utils.js +2 -51
  65. package/dist/utils/text-utils.js.map +1 -1
  66. package/package.json +9 -3
  67. package/src/components/chat/__tests-cy__/chat-messages.test.tsx +17 -17
  68. package/src/components/chat/__tests-cy__/chat.test.tsx +21 -20
  69. package/src/components/chat/chat-error.tsx +1 -1
  70. package/src/components/chat/chat-message-template-user.tsx +2 -2
  71. package/src/components/chat/chat.tsx +1 -1
  72. package/src/components/messages/__tests-cy__/message-agent.test.tsx +13 -31
  73. package/src/components/messages/__tests-cy__/message-system.test.tsx +3 -2
  74. package/src/components/messages/__tests-cy__/message-timeout.test.tsx +6 -13
  75. package/src/components/messages/__tests-cy__/message-typing.test.tsx +10 -9
  76. package/src/components/messages/__tests-cy__/message-user.test.tsx +5 -14
  77. package/src/components/messages/message-agent.tsx +18 -11
  78. package/src/components/messages/message-avatar.tsx +7 -3
  79. package/src/components/messages/message-system.tsx +5 -4
  80. package/src/components/messages/message-timeout.tsx +9 -8
  81. package/src/components/messages/message-typing.tsx +12 -16
  82. package/src/components/messages/message-user.tsx +7 -10
  83. package/src/index.ts +1 -0
  84. package/src/stores/__tests__/chat-input.store.test.ts +5 -1
  85. package/src/stores/__tests__/chat-ui.store.test.ts +531 -0
  86. package/src/stores/chat-ui.store.ts +9 -10
  87. package/src/utils/__tests__/text-utils.test.ts +70 -0
  88. package/src/utils/text-utils.ts +2 -59
  89. package/tsconfig.json +5 -0
  90. package/tsconfig.tsbuildinfo +1 -1
  91. package/dist/models/component.d.ts +0 -4
  92. package/dist/models/component.d.ts.map +0 -1
  93. package/dist/models/component.js +0 -2
  94. package/dist/models/component.js.map +0 -1
  95. package/dist/utils/test-utils.d.ts +0 -5
  96. package/dist/utils/test-utils.d.ts.map +0 -1
  97. package/dist/utils/test-utils.js +0 -17
  98. package/dist/utils/test-utils.js.map +0 -1
  99. package/src/models/component.ts +0 -3
  100. 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.error = new ChatError(message, options);
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
- this.isFilePickerEnabled = false;
339
- this.currentFileMessage = this.addMessageFile(this.file);
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 === ChatMessageState.Delivered) {
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
- const newMessage = this.createMessage<T>(message);
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 newMessage;
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
+ });