@nyaruka/temba-components 0.122.0 → 0.123.0

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 (224) hide show
  1. package/.github/copilot-instructions.md +163 -0
  2. package/.github/workflows/build.yml +3 -3
  3. package/.github/workflows/cla.yml +6 -6
  4. package/.github/workflows/copilot-setup-steps.yml +86 -0
  5. package/CHANGELOG.md +33 -0
  6. package/demo/index.html +42 -0
  7. package/dist/locales/es.js +1 -0
  8. package/dist/locales/es.js.map +1 -1
  9. package/dist/locales/fr.js +1 -0
  10. package/dist/locales/fr.js.map +1 -1
  11. package/dist/locales/pt.js +1 -0
  12. package/dist/locales/pt.js.map +1 -1
  13. package/dist/temba-components.js +59 -72
  14. package/dist/temba-components.js.map +1 -1
  15. package/out-tsc/src/chart/TembaChart.js +81 -14
  16. package/out-tsc/src/chart/TembaChart.js.map +1 -1
  17. package/out-tsc/src/list/RunList.js +13 -8
  18. package/out-tsc/src/list/RunList.js.map +1 -1
  19. package/out-tsc/src/locales/es.js +1 -0
  20. package/out-tsc/src/locales/es.js.map +1 -1
  21. package/out-tsc/src/locales/fr.js +1 -0
  22. package/out-tsc/src/locales/fr.js.map +1 -1
  23. package/out-tsc/src/locales/pt.js +1 -0
  24. package/out-tsc/src/locales/pt.js.map +1 -1
  25. package/out-tsc/src/options/Options.js +36 -13
  26. package/out-tsc/src/options/Options.js.map +1 -1
  27. package/out-tsc/src/select/Select.js +28 -5
  28. package/out-tsc/src/select/Select.js.map +1 -1
  29. package/out-tsc/src/store/AppState.js +3 -3
  30. package/out-tsc/src/store/AppState.js.map +1 -1
  31. package/out-tsc/src/utils/index.js +6 -1
  32. package/out-tsc/src/utils/index.js.map +1 -1
  33. package/out-tsc/src/vectoricon/VectorIcon.js +2 -1
  34. package/out-tsc/src/vectoricon/VectorIcon.js.map +1 -1
  35. package/out-tsc/temba-modules.js +0 -2
  36. package/out-tsc/temba-modules.js.map +1 -1
  37. package/out-tsc/test/temba-appstate-language.test.js +176 -0
  38. package/out-tsc/test/temba-appstate-language.test.js.map +1 -0
  39. package/out-tsc/test/temba-chart.test.js +125 -0
  40. package/out-tsc/test/temba-chart.test.js.map +1 -1
  41. package/out-tsc/test/temba-dropdown.test.js +317 -0
  42. package/out-tsc/test/temba-dropdown.test.js.map +1 -0
  43. package/out-tsc/test/temba-run-list.test.js +588 -0
  44. package/out-tsc/test/temba-run-list.test.js.map +1 -0
  45. package/out-tsc/test/temba-select.test.js +16 -0
  46. package/out-tsc/test/temba-select.test.js.map +1 -1
  47. package/out-tsc/test/temba-toast.test.js +299 -0
  48. package/out-tsc/test/temba-toast.test.js.map +1 -0
  49. package/out-tsc/test/temba-utils-index.test.js +1178 -0
  50. package/out-tsc/test/temba-utils-index.test.js.map +1 -0
  51. package/out-tsc/test/temba-webchat.test.js +816 -0
  52. package/out-tsc/test/temba-webchat.test.js.map +1 -0
  53. package/out-tsc/test/utils.test.js +3 -1
  54. package/out-tsc/test/utils.test.js.map +1 -1
  55. package/package.json +6 -8
  56. package/screenshots/truth/alert/error.png +0 -0
  57. package/screenshots/truth/alert/info.png +0 -0
  58. package/screenshots/truth/alert/warning.png +0 -0
  59. package/screenshots/truth/checkbox/checkbox-label-background-hover.png +0 -0
  60. package/screenshots/truth/checkbox/checked.png +0 -0
  61. package/screenshots/truth/checkbox/default.png +0 -0
  62. package/screenshots/truth/colorpicker/default.png +0 -0
  63. package/screenshots/truth/colorpicker/focused.png +0 -0
  64. package/screenshots/truth/colorpicker/initialized.png +0 -0
  65. package/screenshots/truth/colorpicker/selected.png +0 -0
  66. package/screenshots/truth/compose/attachments-tab.png +0 -0
  67. package/screenshots/truth/compose/attachments-with-files-focused.png +0 -0
  68. package/screenshots/truth/compose/attachments-with-files.png +0 -0
  69. package/screenshots/truth/compose/intial-text.png +0 -0
  70. package/screenshots/truth/compose/no-counter.png +0 -0
  71. package/screenshots/truth/compose/wraps-text-and-spaces.png +0 -0
  72. package/screenshots/truth/compose/wraps-text-and-url.png +0 -0
  73. package/screenshots/truth/compose/wraps-text-no-spaces.png +0 -0
  74. package/screenshots/truth/contacts/badges.png +0 -0
  75. package/screenshots/truth/contacts/chat-failure.png +0 -0
  76. package/screenshots/truth/contacts/chat-for-active-contact.png +0 -0
  77. package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
  78. package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
  79. package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
  80. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  81. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  82. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  83. package/screenshots/truth/content-menu/button-no-items.png +0 -0
  84. package/screenshots/truth/content-menu/items-and-buttons.png +0 -0
  85. package/screenshots/truth/counter/summary.png +0 -0
  86. package/screenshots/truth/counter/text.png +0 -0
  87. package/screenshots/truth/counter/unicode-variables.png +0 -0
  88. package/screenshots/truth/counter/unicode.png +0 -0
  89. package/screenshots/truth/counter/variable.png +0 -0
  90. package/screenshots/truth/date/date-inline.png +0 -0
  91. package/screenshots/truth/date/date.png +0 -0
  92. package/screenshots/truth/date/datetime.png +0 -0
  93. package/screenshots/truth/date/duration.png +0 -0
  94. package/screenshots/truth/date/timedate.png +0 -0
  95. package/screenshots/truth/datepicker/date-truncated-time.png +0 -0
  96. package/screenshots/truth/datepicker/date.png +0 -0
  97. package/screenshots/truth/datepicker/initial-timezone.png +0 -0
  98. package/screenshots/truth/datepicker/updated-keyboard-date.png +0 -0
  99. package/screenshots/truth/dialog/focused.png +0 -0
  100. package/screenshots/truth/dropdown/after-blur.png +0 -0
  101. package/screenshots/truth/dropdown/bottom-edge-collision.png +0 -0
  102. package/screenshots/truth/dropdown/custom-arrow-size.png +0 -0
  103. package/screenshots/truth/dropdown/default.png +0 -0
  104. package/screenshots/truth/dropdown/narrow-toggle.png +0 -0
  105. package/screenshots/truth/dropdown/no-mask.png +0 -0
  106. package/screenshots/truth/dropdown/opened.png +0 -0
  107. package/screenshots/truth/dropdown/positioned.png +0 -0
  108. package/screenshots/truth/dropdown/right-edge-collision.png +0 -0
  109. package/screenshots/truth/dropdown/with-mask.png +0 -0
  110. package/screenshots/truth/label/custom.png +0 -0
  111. package/screenshots/truth/label/danger.png +0 -0
  112. package/screenshots/truth/label/dark.png +0 -0
  113. package/screenshots/truth/label/default-icon.png +0 -0
  114. package/screenshots/truth/label/no-icon.png +0 -0
  115. package/screenshots/truth/label/primary.png +0 -0
  116. package/screenshots/truth/label/secondary.png +0 -0
  117. package/screenshots/truth/label/shadow.png +0 -0
  118. package/screenshots/truth/label/tertiary.png +0 -0
  119. package/screenshots/truth/lightbox/img-zoomed.png +0 -0
  120. package/screenshots/truth/list/fields-dragging.png +0 -0
  121. package/screenshots/truth/list/fields-filtered.png +0 -0
  122. package/screenshots/truth/list/fields-hovered.png +0 -0
  123. package/screenshots/truth/list/fields.png +0 -0
  124. package/screenshots/truth/list/items-selected.png +0 -0
  125. package/screenshots/truth/list/items-updated.png +0 -0
  126. package/screenshots/truth/list/items.png +0 -0
  127. package/screenshots/truth/list/sortable-dragging.png +0 -0
  128. package/screenshots/truth/list/sortable-dropped.png +0 -0
  129. package/screenshots/truth/list/sortable.png +0 -0
  130. package/screenshots/truth/menu/menu-focused-with items.png +0 -0
  131. package/screenshots/truth/menu/menu-refresh-1.png +0 -0
  132. package/screenshots/truth/menu/menu-refresh-2.png +0 -0
  133. package/screenshots/truth/menu/menu-root.png +0 -0
  134. package/screenshots/truth/menu/menu-submenu.png +0 -0
  135. package/screenshots/truth/menu/menu-tasks-nextup.png +0 -0
  136. package/screenshots/truth/menu/menu-tasks.png +0 -0
  137. package/screenshots/truth/modax/form.png +0 -0
  138. package/screenshots/truth/modax/simple.png +0 -0
  139. package/screenshots/truth/omnibox/selected.png +0 -0
  140. package/screenshots/truth/options/block.png +0 -0
  141. package/screenshots/truth/run-list/basic.png +0 -0
  142. package/screenshots/truth/select/disabled-multi-selection.png +0 -0
  143. package/screenshots/truth/select/disabled-selection.png +0 -0
  144. package/screenshots/truth/select/disabled.png +0 -0
  145. package/screenshots/truth/select/embedded.png +0 -0
  146. package/screenshots/truth/select/empty-options.png +0 -0
  147. package/screenshots/truth/select/expression-selected.png +0 -0
  148. package/screenshots/truth/select/expressions.png +0 -0
  149. package/screenshots/truth/select/functions.png +0 -0
  150. package/screenshots/truth/select/local-options.png +0 -0
  151. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  152. package/screenshots/truth/select/multiple-initial-values.png +0 -0
  153. package/screenshots/truth/select/remote-options.png +0 -0
  154. package/screenshots/truth/select/search-enabled.png +0 -0
  155. package/screenshots/truth/select/search-multi-no-matches.png +0 -0
  156. package/screenshots/truth/select/search-selected-focus.png +0 -0
  157. package/screenshots/truth/select/search-selected.png +0 -0
  158. package/screenshots/truth/select/search-with-selected.png +0 -0
  159. package/screenshots/truth/select/searching.png +0 -0
  160. package/screenshots/truth/select/selected-multi-maxitems-reached.png +0 -0
  161. package/screenshots/truth/select/selected-multi.png +0 -0
  162. package/screenshots/truth/select/selected-single.png +0 -0
  163. package/screenshots/truth/select/selection-clearable.png +0 -0
  164. package/screenshots/truth/select/static-initial-value.png +0 -0
  165. package/screenshots/truth/select/static-initial-via-selected.png +0 -0
  166. package/screenshots/truth/select/truncated-selection.png +0 -0
  167. package/screenshots/truth/select/with-placeholder.png +0 -0
  168. package/screenshots/truth/select/without-placeholder.png +0 -0
  169. package/screenshots/truth/slider/custom-min-custom-max-valid-value.png +0 -0
  170. package/screenshots/truth/slider/custom-min-default-max-no-value.png +0 -0
  171. package/screenshots/truth/slider/default-min-custom-max-no-value.png +0 -0
  172. package/screenshots/truth/slider/default-min-default-max-invalid-value.png +0 -0
  173. package/screenshots/truth/slider/default-min-default-max-valid-value.png +0 -0
  174. package/screenshots/truth/slider/update-slider-on-value-change.png +0 -0
  175. package/screenshots/truth/templates/default.png +0 -0
  176. package/screenshots/truth/templates/unapproved.png +0 -0
  177. package/screenshots/truth/textinput/input-disabled.png +0 -0
  178. package/screenshots/truth/textinput/input-focused.png +0 -0
  179. package/screenshots/truth/textinput/input-form.png +0 -0
  180. package/screenshots/truth/textinput/input-inserted.png +0 -0
  181. package/screenshots/truth/textinput/input-placeholder.png +0 -0
  182. package/screenshots/truth/textinput/input-updated.png +0 -0
  183. package/screenshots/truth/textinput/input.png +0 -0
  184. package/screenshots/truth/textinput/textarea-focused.png +0 -0
  185. package/screenshots/truth/textinput/textarea.png +0 -0
  186. package/screenshots/truth/tip/bottom.png +0 -0
  187. package/screenshots/truth/tip/left.png +0 -0
  188. package/screenshots/truth/tip/right.png +0 -0
  189. package/screenshots/truth/tip/top.png +0 -0
  190. package/screenshots/truth/webchat/closed-widget.png +0 -0
  191. package/screenshots/truth/webchat/connected-state.png +0 -0
  192. package/screenshots/truth/webchat/connecting-state.png +0 -0
  193. package/screenshots/truth/webchat/disconnected-state.png +0 -0
  194. package/screenshots/truth/webchat/opened-widget.png +0 -0
  195. package/src/chart/TembaChart.ts +86 -15
  196. package/src/list/RunList.ts +11 -8
  197. package/src/locales/es.ts +1 -0
  198. package/src/locales/fr.ts +1 -0
  199. package/src/locales/pt.ts +1 -0
  200. package/src/options/Options.ts +38 -13
  201. package/src/select/Select.ts +32 -5
  202. package/src/store/AppState.ts +3 -3
  203. package/src/utils/index.ts +17 -5
  204. package/src/vectoricon/VectorIcon.ts +2 -1
  205. package/temba-modules.ts +0 -2
  206. package/test/temba-appstate-language.test.ts +218 -0
  207. package/test/temba-chart.test.ts +161 -1
  208. package/test/temba-dropdown.test.ts +444 -0
  209. package/test/temba-run-list.test.ts +774 -0
  210. package/test/temba-select.test.ts +27 -0
  211. package/test/temba-toast.test.ts +386 -0
  212. package/test/temba-utils-index.test.ts +1547 -0
  213. package/test/temba-webchat.test.ts +1095 -0
  214. package/test/utils.test.ts +4 -2
  215. package/test-assets/list/flow-results.json +17 -0
  216. package/test-assets/list/runs.json +126 -0
  217. package/test-assets/style.css +23 -0
  218. package/web-test-runner.config.mjs +33 -7
  219. package/xliff/es.xlf +3 -0
  220. package/xliff/fr.xlf +3 -0
  221. package/xliff/pt.xlf +3 -0
  222. package/out-tsc/src/outboxmonitor/OutboxMonitor.js +0 -136
  223. package/out-tsc/src/outboxmonitor/OutboxMonitor.js.map +0 -1
  224. package/src/outboxmonitor/OutboxMonitor.ts +0 -148
@@ -0,0 +1,1095 @@
1
+ import { useFakeTimers, stub, SinonStub } from 'sinon';
2
+ import { WebChat } from '../src/webchat/WebChat';
3
+ import {
4
+ assertScreenshot,
5
+ getClip,
6
+ getComponent,
7
+ mockNow,
8
+ mouseClickElement
9
+ } from '../test/utils.test';
10
+ import { expect, assert } from '@open-wc/testing';
11
+
12
+ let clock: any;
13
+ mockNow('2021-03-31T00:31:00.000-00:00');
14
+
15
+ const TAG = 'temba-webchat';
16
+
17
+ const getWebChat = async (attrs: any = {}) => {
18
+ const webChat = (await getComponent(TAG, attrs, '', 400, 600)) as WebChat;
19
+
20
+ return webChat;
21
+ };
22
+
23
+ // Mock WebSocket
24
+ class MockWebSocket {
25
+ public onopen: ((event: Event) => void) | null = null;
26
+ public onclose: ((event: CloseEvent) => void) | null = null;
27
+ public onmessage: ((event: MessageEvent) => void) | null = null;
28
+ public onerror: ((event: Event) => void) | null = null;
29
+ public readyState: number = 0;
30
+ public url: string;
31
+ public sentMessages: string[] = [];
32
+ public autoOpen: boolean = true;
33
+
34
+ constructor(url: string) {
35
+ this.url = url;
36
+ // Only auto-open if enabled
37
+ if (this.autoOpen) {
38
+ setTimeout(() => {
39
+ this.readyState = 1; // OPEN
40
+ if (this.onopen) {
41
+ this.onopen(new Event('open'));
42
+ }
43
+ }, 0);
44
+ }
45
+ }
46
+
47
+ send(data: string) {
48
+ this.sentMessages.push(data);
49
+ }
50
+
51
+ close() {
52
+ this.readyState = 3; // CLOSED
53
+ if (this.onclose) {
54
+ this.onclose(new CloseEvent('close'));
55
+ }
56
+ }
57
+
58
+ // Test helper to manually open connection
59
+ manualOpen() {
60
+ this.readyState = 1; // OPEN
61
+ if (this.onopen) {
62
+ this.onopen(new Event('open'));
63
+ }
64
+ }
65
+
66
+ // Test helper to simulate incoming messages
67
+ simulateMessage(data: any) {
68
+ if (this.onmessage) {
69
+ this.onmessage(
70
+ new MessageEvent('message', { data: JSON.stringify(data) })
71
+ );
72
+ }
73
+ }
74
+
75
+ // Test helper to simulate errors
76
+ simulateError() {
77
+ if (this.onerror) {
78
+ this.onerror(new Event('error'));
79
+ }
80
+ }
81
+ }
82
+
83
+ describe('temba-webchat', () => {
84
+ let originalWebSocket: any;
85
+ let mockWebSocket: MockWebSocket;
86
+ let webSocketStub: SinonStub;
87
+ let cookieStub: SinonStub;
88
+
89
+ beforeEach(() => {
90
+ // Mock WebSocket
91
+ originalWebSocket = window.WebSocket;
92
+ webSocketStub = stub(window, 'WebSocket').callsFake((url: string) => {
93
+ mockWebSocket = new MockWebSocket(url);
94
+ mockWebSocket.autoOpen = false; // Disable auto-open by default
95
+ return mockWebSocket as any;
96
+ });
97
+
98
+ // Mock document.cookie
99
+ cookieStub = stub(document, 'cookie').value('');
100
+
101
+ clock = useFakeTimers();
102
+ });
103
+
104
+ afterEach(() => {
105
+ clock.restore();
106
+ webSocketStub.restore();
107
+ cookieStub.restore();
108
+ window.WebSocket = originalWebSocket;
109
+ });
110
+
111
+ describe('Component Initialization', () => {
112
+ it('creates component with default properties', async () => {
113
+ const webChat = await getWebChat();
114
+
115
+ assert.instanceOf(webChat, WebChat);
116
+ expect(webChat.open).to.equal(false);
117
+ expect(webChat.status).to.equal('disconnected');
118
+ expect(webChat.hasPendingText).to.equal(false);
119
+ expect(webChat.messageGroups).to.deep.equal([]);
120
+ expect(webChat.blockHistoryFetching).to.equal(false);
121
+ });
122
+
123
+ it('accepts channel and urn properties', async () => {
124
+ const webChat = await getWebChat({
125
+ channel: 'test-channel',
126
+ urn: 'test-urn-123'
127
+ });
128
+
129
+ expect(webChat.channel).to.equal('test-channel');
130
+ expect(webChat.urn).to.equal('test-urn-123');
131
+ });
132
+
133
+ it('accepts host and activeUserAvatar properties', async () => {
134
+ const webChat = await getWebChat({
135
+ host: 'example.com',
136
+ activeUserAvatar: 'https://example.com/avatar.jpg'
137
+ });
138
+
139
+ expect(webChat.host).to.equal('example.com');
140
+ expect(webChat.activeUserAvatar).to.equal(
141
+ 'https://example.com/avatar.jpg'
142
+ );
143
+ });
144
+
145
+ it('initializes chat component on first update', async () => {
146
+ const webChat = await getWebChat({
147
+ channel: 'test-channel'
148
+ });
149
+
150
+ // Check that the chat component was initialized
151
+ const chatElement = webChat.shadowRoot.querySelector('temba-chat');
152
+ expect(chatElement).to.exist;
153
+ });
154
+ });
155
+
156
+ describe('UI Rendering', () => {
157
+ it('renders closed chat widget by default', async () => {
158
+ const webChat = await getWebChat({
159
+ channel: 'test-channel'
160
+ });
161
+
162
+ await assertScreenshot('webchat/closed-widget', getClip(webChat));
163
+ });
164
+
165
+ it('renders opened chat widget', async () => {
166
+ const webChat = await getWebChat({
167
+ channel: 'test-channel',
168
+ open: true
169
+ });
170
+
171
+ await assertScreenshot('webchat/opened-widget', getClip(webChat));
172
+ });
173
+
174
+ it('renders connecting state', async () => {
175
+ const webChat = await getWebChat({
176
+ channel: 'test-channel'
177
+ });
178
+
179
+ expect(webChat.open).to.equal(false);
180
+ expect(webChat.status).to.equal('disconnected');
181
+
182
+ // Click to open the widget, which should trigger connecting state
183
+ const toggleElement = webChat.shadowRoot.querySelector('.toggle');
184
+ expect(toggleElement).to.exist;
185
+
186
+ await mouseClickElement(toggleElement);
187
+ await webChat.updateComplete;
188
+
189
+ // Now it should be open and connecting
190
+ expect(webChat.open).to.equal(true);
191
+ expect(webChat.status).to.equal('connecting');
192
+
193
+ await assertScreenshot('webchat/connecting-state', getClip(webChat));
194
+ });
195
+
196
+ it('renders disconnected state with reconnect option', async () => {
197
+ const webChat = await getWebChat({
198
+ channel: 'test-channel',
199
+ open: true,
200
+ status: 'disconnected'
201
+ });
202
+
203
+ await assertScreenshot('webchat/disconnected-state', getClip(webChat));
204
+ });
205
+
206
+ it('renders connected state with input field', async () => {
207
+ const webChat = await getWebChat({
208
+ channel: 'test-channel',
209
+ open: true,
210
+ status: 'connected'
211
+ });
212
+
213
+ await assertScreenshot('webchat/connected-state', getClip(webChat));
214
+ });
215
+ });
216
+
217
+ describe('Chat Toggle Functionality', () => {
218
+ it('toggles chat open/closed', async () => {
219
+ const webChat = await getWebChat({
220
+ channel: 'test-channel'
221
+ });
222
+
223
+ expect(webChat.open).to.equal(false);
224
+
225
+ // Click the toggle element
226
+ const toggleElement = webChat.shadowRoot.querySelector('.toggle');
227
+ expect(toggleElement).to.exist;
228
+
229
+ await mouseClickElement(toggleElement);
230
+ await webChat.updateComplete;
231
+
232
+ expect(webChat.open).to.equal(true);
233
+
234
+ // Click toggle again
235
+ await mouseClickElement(toggleElement);
236
+ await webChat.updateComplete;
237
+
238
+ expect(webChat.open).to.equal(false);
239
+ });
240
+
241
+ it('toggles chat via close button', async () => {
242
+ const webChat = await getWebChat({
243
+ channel: 'test-channel',
244
+ open: true
245
+ });
246
+
247
+ expect(webChat.open).to.equal(true);
248
+
249
+ // Click close button in header
250
+ const closeButton = webChat.shadowRoot.querySelector('.close-button');
251
+ expect(closeButton).to.exist;
252
+
253
+ await mouseClickElement(closeButton);
254
+ await webChat.updateComplete;
255
+
256
+ expect(webChat.open).to.equal(false);
257
+ });
258
+
259
+ it('opens chat programmatically', async () => {
260
+ const webChat = await getWebChat({
261
+ channel: 'test-channel'
262
+ });
263
+
264
+ expect(webChat.open).to.equal(false);
265
+
266
+ webChat.openChat();
267
+ await webChat.updateComplete;
268
+
269
+ expect(webChat.open).to.equal(true);
270
+ });
271
+ });
272
+
273
+ describe('Socket Connection Management', () => {
274
+ it('opens socket when chat is opened', async () => {
275
+ const webChat = await getWebChat({
276
+ channel: 'test-channel'
277
+ });
278
+
279
+ expect(webChat.status).to.equal('disconnected');
280
+
281
+ // Open chat - this should trigger socket connection
282
+ webChat.open = true;
283
+ await webChat.updateComplete;
284
+
285
+ expect(webChat.status).to.equal('connecting');
286
+ expect(webSocketStub.called).to.be.true;
287
+ expect(mockWebSocket.url).to.include('test-channel');
288
+
289
+ // Now simulate the socket opening
290
+ mockWebSocket.manualOpen();
291
+ await webChat.updateComplete;
292
+
293
+ expect(webChat.status).to.equal('connected');
294
+ });
295
+
296
+ it('does not open socket if already connecting or connected', async () => {
297
+ const webChat = await getWebChat({
298
+ channel: 'test-channel'
299
+ });
300
+
301
+ // First connection
302
+ webChat.open = true;
303
+ await webChat.updateComplete;
304
+ await clock.tick(0);
305
+
306
+ const firstCallCount = webSocketStub.callCount;
307
+
308
+ // Try to connect again while connecting
309
+ webChat.open = false;
310
+ webChat.open = true;
311
+ await webChat.updateComplete;
312
+ await clock.tick(0);
313
+
314
+ // Should not create another socket
315
+ expect(webSocketStub.callCount).to.equal(firstCallCount);
316
+ });
317
+
318
+ it('constructs correct WebSocket URL with channel', async () => {
319
+ const webChat = await getWebChat({
320
+ channel: 'my-channel'
321
+ });
322
+
323
+ webChat.open = true;
324
+ await webChat.updateComplete;
325
+ await clock.tick(0);
326
+
327
+ expect(mockWebSocket.url).to.equal(
328
+ 'wss://localhost.textit.com/wc/connect/my-channel/'
329
+ );
330
+ });
331
+
332
+ it('includes urn in WebSocket URL when present', async () => {
333
+ const webChat = await getWebChat({
334
+ channel: 'my-channel',
335
+ urn: 'chat-123'
336
+ });
337
+
338
+ webChat.open = true;
339
+ await webChat.updateComplete;
340
+ await clock.tick(0);
341
+
342
+ expect(mockWebSocket.url).to.equal(
343
+ 'wss://localhost.textit.com/wc/connect/my-channel/?chat_id=chat-123'
344
+ );
345
+ });
346
+
347
+ it('sends start_chat command on socket open', async () => {
348
+ const webChat = await getWebChat({
349
+ channel: 'test-channel'
350
+ });
351
+
352
+ webChat.open = true;
353
+ await webChat.updateComplete;
354
+ await clock.tick(0);
355
+
356
+ expect(mockWebSocket.sentMessages).to.have.length(1);
357
+ const sentMessage = JSON.parse(mockWebSocket.sentMessages[0]);
358
+ expect(sentMessage.type).to.equal('start_chat');
359
+ });
360
+
361
+ it('includes chat_id in start_chat command when urn is present', async () => {
362
+ // Set the cookie directly in document.cookie
363
+ cookieStub.value('temba-chat-urn=existing-chat-123');
364
+
365
+ const webChat = await getWebChat({
366
+ channel: 'test-channel',
367
+ urn: 'existing-chat-123'
368
+ });
369
+
370
+ webChat.open = true;
371
+ await webChat.updateComplete;
372
+
373
+ mockWebSocket.manualOpen();
374
+ await webChat.updateComplete;
375
+
376
+ const sentMessage = JSON.parse(mockWebSocket.sentMessages[0]);
377
+ expect(sentMessage.type).to.equal('start_chat');
378
+ // The chat_id should be 'existing-chat-123' either from the urn property or cookie
379
+ expect(sentMessage.chat_id).to.equal('existing-chat-123');
380
+ });
381
+
382
+ it('handles socket close event', async () => {
383
+ const webChat = await getWebChat({
384
+ channel: 'test-channel'
385
+ });
386
+
387
+ webChat.open = true;
388
+ await webChat.updateComplete;
389
+
390
+ expect(webChat.status).to.equal('connecting');
391
+
392
+ mockWebSocket.close();
393
+ await webChat.updateComplete;
394
+
395
+ expect(webChat.status).to.equal('disconnected');
396
+ });
397
+
398
+ it('handles socket error event', async () => {
399
+ const webChat = await getWebChat({
400
+ channel: 'test-channel'
401
+ });
402
+
403
+ webChat.open = true;
404
+ await webChat.updateComplete;
405
+
406
+ expect(webChat.status).to.equal('connecting');
407
+
408
+ mockWebSocket.simulateError();
409
+ await webChat.updateComplete;
410
+
411
+ expect(webChat.status).to.equal('disconnected');
412
+ });
413
+
414
+ it('reconnects when reconnect button is clicked', async () => {
415
+ const webChat = await getWebChat({
416
+ channel: 'test-channel',
417
+ open: true,
418
+ status: 'disconnected'
419
+ });
420
+
421
+ const reconnectButton = webChat.shadowRoot.querySelector('.reconnect');
422
+ expect(reconnectButton).to.exist;
423
+
424
+ await mouseClickElement(reconnectButton);
425
+ await webChat.updateComplete;
426
+
427
+ expect(webSocketStub.called).to.be.true;
428
+ expect(webChat.status).to.equal('connecting');
429
+ });
430
+ });
431
+
432
+ describe('Message Handling', () => {
433
+ it('handles chat_started message', async () => {
434
+ const webChat = await getWebChat({
435
+ channel: 'test-channel'
436
+ });
437
+
438
+ webChat.open = true;
439
+ await webChat.updateComplete;
440
+ mockWebSocket.manualOpen();
441
+ await webChat.updateComplete;
442
+
443
+ expect(webChat.status).to.equal('connected');
444
+
445
+ // Simulate chat_started message
446
+ mockWebSocket.simulateMessage({
447
+ type: 'chat_started',
448
+ chat_id: 'new-chat-456'
449
+ });
450
+ await webChat.updateComplete;
451
+
452
+ expect(webChat.urn).to.equal('new-chat-456');
453
+ expect(webChat.messageGroups).to.deep.equal([]);
454
+ });
455
+
456
+ it('handles chat_resumed message and fetches history', async () => {
457
+ const webChat = await getWebChat({
458
+ channel: 'test-channel'
459
+ });
460
+
461
+ webChat.open = true;
462
+ await webChat.updateComplete;
463
+ mockWebSocket.manualOpen();
464
+ await webChat.updateComplete;
465
+
466
+ // Simulate chat_resumed message
467
+ mockWebSocket.simulateMessage({
468
+ type: 'chat_resumed',
469
+ chat_id: 'resumed-chat-789'
470
+ });
471
+ await webChat.updateComplete;
472
+
473
+ expect(webChat.urn).to.equal('resumed-chat-789');
474
+
475
+ // Should have sent get_history command
476
+ expect(mockWebSocket.sentMessages.length).to.be.greaterThan(1);
477
+ const lastMessage = JSON.parse(
478
+ mockWebSocket.sentMessages[mockWebSocket.sentMessages.length - 1]
479
+ );
480
+ expect(lastMessage.type).to.equal('get_history');
481
+ });
482
+
483
+ it('handles chat_out message and sends ack', async () => {
484
+ const webChat = await getWebChat({
485
+ channel: 'test-channel'
486
+ });
487
+
488
+ webChat.open = true;
489
+ await webChat.updateComplete;
490
+ mockWebSocket.manualOpen();
491
+ await webChat.updateComplete;
492
+
493
+ const initialMessageCount = mockWebSocket.sentMessages.length;
494
+
495
+ // Simulate incoming message
496
+ mockWebSocket.simulateMessage({
497
+ type: 'chat_out',
498
+ msg_out: {
499
+ id: 'msg-123',
500
+ text: 'Hello from server',
501
+ time: '2021-03-31T00:31:00.000Z',
502
+ user: {
503
+ id: 'user-1',
504
+ name: 'Test User',
505
+ email: 'test@example.com'
506
+ }
507
+ }
508
+ });
509
+ await webChat.updateComplete;
510
+
511
+ // Should have sent ack
512
+ expect(mockWebSocket.sentMessages.length).to.equal(
513
+ initialMessageCount + 1
514
+ );
515
+ const ackMessage = JSON.parse(
516
+ mockWebSocket.sentMessages[mockWebSocket.sentMessages.length - 1]
517
+ );
518
+ expect(ackMessage.type).to.equal('ack_chat');
519
+ expect(ackMessage.msg_id).to.equal('msg-123');
520
+ });
521
+
522
+ it('handles history response', async () => {
523
+ const webChat = await getWebChat({
524
+ channel: 'test-channel'
525
+ });
526
+
527
+ webChat.open = true;
528
+ await webChat.updateComplete;
529
+ mockWebSocket.manualOpen();
530
+ await webChat.updateComplete;
531
+
532
+ // Simulate history response
533
+ mockWebSocket.simulateMessage({
534
+ type: 'history',
535
+ history: [
536
+ {
537
+ msg_out: {
538
+ id: 'msg-1',
539
+ text: 'First message',
540
+ time: '2021-03-31T00:30:00.000Z',
541
+ user: { id: 'user-1', name: 'User', email: 'user@example.com' }
542
+ }
543
+ },
544
+ {
545
+ msg_in: {
546
+ id: 'msg-2',
547
+ text: 'Second message',
548
+ time: '2021-03-31T00:30:30.000Z'
549
+ }
550
+ }
551
+ ]
552
+ });
553
+ await webChat.updateComplete;
554
+
555
+ // Should have updated beforeTime and unblocked history fetching
556
+ expect(webChat.blockHistoryFetching).to.equal(false);
557
+ });
558
+
559
+ it('clears messages when chat_id changes', async () => {
560
+ const webChat = await getWebChat({
561
+ channel: 'test-channel',
562
+ urn: 'old-chat-123'
563
+ });
564
+
565
+ webChat.messageGroups = [['existing', 'messages']];
566
+
567
+ webChat.open = true;
568
+ await webChat.updateComplete;
569
+ mockWebSocket.manualOpen();
570
+ await webChat.updateComplete;
571
+
572
+ // Simulate chat_started with different chat_id
573
+ mockWebSocket.simulateMessage({
574
+ type: 'chat_started',
575
+ chat_id: 'new-chat-456'
576
+ });
577
+ await webChat.updateComplete;
578
+
579
+ expect(webChat.urn).to.equal('new-chat-456');
580
+ expect(webChat.messageGroups).to.deep.equal([]);
581
+ });
582
+
583
+ it('keeps messages when chat_id is the same', async () => {
584
+ const webChat = await getWebChat({
585
+ channel: 'test-channel'
586
+ });
587
+
588
+ webChat.open = true;
589
+ await webChat.updateComplete;
590
+ mockWebSocket.manualOpen();
591
+ await webChat.updateComplete;
592
+
593
+ // First, simulate a chat_started to set the URN
594
+ mockWebSocket.simulateMessage({
595
+ type: 'chat_started',
596
+ chat_id: 'same-chat-123'
597
+ });
598
+ await webChat.updateComplete;
599
+
600
+ // Now set some messages
601
+ webChat.messageGroups = [['existing', 'messages']];
602
+ await webChat.updateComplete;
603
+
604
+ // Simulate another chat_started with the same chat_id
605
+ mockWebSocket.simulateMessage({
606
+ type: 'chat_started',
607
+ chat_id: 'same-chat-123'
608
+ });
609
+ await webChat.updateComplete;
610
+
611
+ expect(webChat.urn).to.equal('same-chat-123');
612
+ expect(webChat.messageGroups).to.deep.equal([['existing', 'messages']]);
613
+ });
614
+ });
615
+
616
+ describe('User Input and Message Sending', () => {
617
+ it('updates hasPendingText on keyup', async () => {
618
+ const webChat = await getWebChat({
619
+ channel: 'test-channel',
620
+ open: true,
621
+ status: 'connected'
622
+ });
623
+
624
+ const inputField = webChat.shadowRoot.querySelector(
625
+ '.input'
626
+ ) as HTMLInputElement;
627
+ expect(inputField).to.exist;
628
+
629
+ expect(webChat.hasPendingText).to.equal(false);
630
+
631
+ // Simulate typing
632
+ inputField.value = 'Hello';
633
+ inputField.dispatchEvent(new KeyboardEvent('keydown', { key: 'h' }));
634
+ await webChat.updateComplete;
635
+
636
+ expect(webChat.hasPendingText).to.equal(true);
637
+
638
+ // Clear input
639
+ inputField.value = '';
640
+ inputField.dispatchEvent(
641
+ new KeyboardEvent('keydown', { key: 'Backspace' })
642
+ );
643
+ await webChat.updateComplete;
644
+
645
+ expect(webChat.hasPendingText).to.equal(false);
646
+ });
647
+
648
+ it('sends message on Enter key', async () => {
649
+ const webChat = await getWebChat({
650
+ channel: 'test-channel'
651
+ });
652
+
653
+ webChat.open = true;
654
+ await webChat.updateComplete;
655
+ mockWebSocket.manualOpen();
656
+ await webChat.updateComplete;
657
+
658
+ // Ensure the socket is properly set
659
+ expect(webChat.status).to.equal('connected');
660
+
661
+ const inputField = webChat.shadowRoot.querySelector(
662
+ '.input'
663
+ ) as HTMLInputElement;
664
+ inputField.value = 'Test message';
665
+ webChat.hasPendingText = true;
666
+
667
+ const initialMessageCount = mockWebSocket.sentMessages.length;
668
+
669
+ // Call handleKeyUp directly with Enter key event
670
+ webChat.handleKeyUp({ key: 'Enter', target: inputField });
671
+ await webChat.updateComplete;
672
+
673
+ // Should have sent message
674
+ expect(mockWebSocket.sentMessages.length).to.equal(
675
+ initialMessageCount + 1
676
+ );
677
+ const sentMessage = JSON.parse(
678
+ mockWebSocket.sentMessages[mockWebSocket.sentMessages.length - 1]
679
+ );
680
+ expect(sentMessage.type).to.equal('send_msg');
681
+ expect(sentMessage.text).to.equal('Test message');
682
+
683
+ // input should be cleared and hasPendingText should be false
684
+ expect(inputField.value).to.equal('');
685
+ expect(webChat.hasPendingText).to.equal(false);
686
+ });
687
+
688
+ it('sends message via send button click', async () => {
689
+ const webChat = await getWebChat({
690
+ channel: 'test-channel'
691
+ });
692
+
693
+ webChat.open = true;
694
+ await webChat.updateComplete;
695
+ mockWebSocket.manualOpen();
696
+ await webChat.updateComplete;
697
+
698
+ const inputField = webChat.shadowRoot.querySelector(
699
+ '.input'
700
+ ) as HTMLInputElement;
701
+
702
+ inputField.value = 'Button click message';
703
+ webChat.hasPendingText = true;
704
+ await webChat.updateComplete;
705
+
706
+ const initialMessageCount = mockWebSocket.sentMessages.length;
707
+
708
+ // call sendPendingMessage directly since it's a private method called by click handler
709
+ (webChat as any).sendPendingMessage();
710
+ await webChat.updateComplete;
711
+
712
+ // should have sent message
713
+ expect(mockWebSocket.sentMessages.length).to.equal(
714
+ initialMessageCount + 1
715
+ );
716
+ const sentMessage = JSON.parse(
717
+ mockWebSocket.sentMessages[mockWebSocket.sentMessages.length - 1]
718
+ );
719
+ expect(sentMessage.type).to.equal('send_msg');
720
+ expect(sentMessage.text).to.equal('Button click message');
721
+ });
722
+
723
+ it('focuses input when input panel is clicked', async () => {
724
+ const webChat = await getWebChat({
725
+ channel: 'test-channel',
726
+ open: true,
727
+ status: 'connected'
728
+ });
729
+
730
+ const inputPanel = webChat.shadowRoot.querySelector('.input-panel');
731
+ expect(inputPanel).to.exist;
732
+
733
+ // Mock focus method
734
+ const inputField = webChat.shadowRoot.querySelector(
735
+ '.input'
736
+ ) as HTMLInputElement;
737
+ let focusCalled = false;
738
+ inputField.focus = () => {
739
+ focusCalled = true;
740
+ };
741
+
742
+ await mouseClickElement(inputPanel);
743
+
744
+ expect(focusCalled).to.be.true;
745
+ });
746
+
747
+ it('does not send message when disconnected', async () => {
748
+ const webChat = await getWebChat({
749
+ channel: 'test-channel',
750
+ open: true
751
+ });
752
+
753
+ // Keep the status as disconnected - don't connect the socket
754
+ expect(webChat.status).to.equal('disconnected');
755
+
756
+ // Find input field - it might not exist in disconnected state
757
+ const inputField = webChat.shadowRoot.querySelector(
758
+ '.input'
759
+ ) as HTMLInputElement;
760
+ if (inputField) {
761
+ inputField.value = 'Should not send';
762
+ webChat.hasPendingText = true;
763
+
764
+ // Try to send message by calling handleKeyUp
765
+ webChat.handleKeyUp({ key: 'Enter', target: inputField });
766
+ await webChat.updateComplete;
767
+ }
768
+
769
+ // Should not have created any WebSocket connections
770
+ expect(webSocketStub.called).to.be.false;
771
+ });
772
+ });
773
+
774
+ describe('History Fetching', () => {
775
+ it('fetches previous messages when requested', async () => {
776
+ const webChat = await getWebChat({
777
+ channel: 'test-channel'
778
+ });
779
+
780
+ webChat.open = true;
781
+ await webChat.updateComplete;
782
+ mockWebSocket.manualOpen();
783
+ await webChat.updateComplete;
784
+
785
+ const initialMessageCount = mockWebSocket.sentMessages.length;
786
+
787
+ // Manually call fetchPreviousMessages
788
+ webChat.fetchPreviousMessages();
789
+ await webChat.updateComplete;
790
+
791
+ expect(webChat.blockHistoryFetching).to.equal(true);
792
+ expect(mockWebSocket.sentMessages.length).to.equal(
793
+ initialMessageCount + 1
794
+ );
795
+
796
+ const historyRequest = JSON.parse(
797
+ mockWebSocket.sentMessages[mockWebSocket.sentMessages.length - 1]
798
+ );
799
+ expect(historyRequest.type).to.equal('get_history');
800
+ expect(historyRequest.before).to.exist;
801
+ });
802
+
803
+ it('does not fetch when already fetching', async () => {
804
+ const webChat = await getWebChat({
805
+ channel: 'test-channel'
806
+ });
807
+
808
+ webChat.blockHistoryFetching = true;
809
+
810
+ webChat.open = true;
811
+ await webChat.updateComplete;
812
+ mockWebSocket.manualOpen();
813
+ await webChat.updateComplete;
814
+
815
+ const initialMessageCount = mockWebSocket.sentMessages.length;
816
+
817
+ // Try to fetch - should be blocked
818
+ webChat.fetchPreviousMessages();
819
+ await webChat.updateComplete;
820
+
821
+ expect(mockWebSocket.sentMessages.length).to.equal(initialMessageCount);
822
+ });
823
+
824
+ it('completes fetch and unblocks history fetching', async () => {
825
+ const webChat = await getWebChat({
826
+ channel: 'test-channel'
827
+ });
828
+
829
+ webChat.blockHistoryFetching = true;
830
+
831
+ webChat.fetchComplete();
832
+
833
+ expect(webChat.blockHistoryFetching).to.equal(false);
834
+ });
835
+ });
836
+
837
+ describe('Edge Cases and Additional Coverage', () => {
838
+ it('handles keyup events that are not Enter', async () => {
839
+ const webChat = await getWebChat({
840
+ channel: 'test-channel'
841
+ });
842
+
843
+ webChat.open = true;
844
+ await webChat.updateComplete;
845
+ mockWebSocket.manualOpen();
846
+ await webChat.updateComplete;
847
+
848
+ const inputField = webChat.shadowRoot.querySelector(
849
+ '.input'
850
+ ) as HTMLInputElement;
851
+ inputField.value = 'Some text';
852
+
853
+ // Test with non-Enter key
854
+ webChat.handleKeyUp({ key: 'a', target: inputField });
855
+ await webChat.updateComplete;
856
+
857
+ expect(webChat.hasPendingText).to.equal(true);
858
+
859
+ // Test with empty value
860
+ inputField.value = '';
861
+ webChat.handleKeyUp({ key: 'Backspace', target: inputField });
862
+ await webChat.updateComplete;
863
+
864
+ expect(webChat.hasPendingText).to.equal(false);
865
+ });
866
+
867
+ it('does not send message on Enter if no pending text', async () => {
868
+ const webChat = await getWebChat({
869
+ channel: 'test-channel'
870
+ });
871
+
872
+ webChat.open = true;
873
+ await webChat.updateComplete;
874
+ mockWebSocket.manualOpen();
875
+ await webChat.updateComplete;
876
+
877
+ const inputField = webChat.shadowRoot.querySelector(
878
+ '.input'
879
+ ) as HTMLInputElement;
880
+ inputField.value = '';
881
+ webChat.hasPendingText = false;
882
+
883
+ const initialMessageCount = mockWebSocket.sentMessages.length;
884
+
885
+ // Try to send with Enter but no pending text
886
+ webChat.handleKeyUp({ key: 'Enter', target: inputField });
887
+ await webChat.updateComplete;
888
+
889
+ // Should not have sent any new messages
890
+ expect(mockWebSocket.sentMessages.length).to.equal(initialMessageCount);
891
+ });
892
+
893
+ it('updates status when open changes from false to true', async () => {
894
+ const webChat = await getWebChat({
895
+ channel: 'test-channel'
896
+ });
897
+
898
+ expect(webChat.status).to.equal('disconnected');
899
+
900
+ // Open the chat for the first time
901
+ webChat.open = true;
902
+ await webChat.updateComplete;
903
+
904
+ expect(webChat.status).to.equal('connecting');
905
+ expect(webSocketStub.called).to.be.true;
906
+ });
907
+
908
+ it('focuses input when status changes to connected', async () => {
909
+ const webChat = await getWebChat({
910
+ channel: 'test-channel'
911
+ });
912
+
913
+ // Open chat to trigger connection
914
+ webChat.open = true;
915
+ await webChat.updateComplete;
916
+
917
+ expect(webChat.status).to.equal('connecting');
918
+
919
+ // Mock focus method before the input is rendered
920
+ let focusCalled = false;
921
+ const originalFocus = HTMLInputElement.prototype.focus;
922
+ HTMLInputElement.prototype.focus = function () {
923
+ focusCalled = true;
924
+ };
925
+
926
+ // Manually open socket to trigger status change to connected
927
+ mockWebSocket.manualOpen();
928
+ await webChat.updateComplete;
929
+
930
+ // Restore original focus
931
+ HTMLInputElement.prototype.focus = originalFocus;
932
+
933
+ // Focus should have been called when status changed to connected
934
+ expect(focusCalled).to.be.true;
935
+ });
936
+
937
+ it('handles focus input when input element does not exist', async () => {
938
+ const webChat = await getWebChat({
939
+ channel: 'test-channel',
940
+ open: true,
941
+ status: 'disconnected'
942
+ });
943
+
944
+ // Try to focus input when it might not exist
945
+ (webChat as any).focusInput();
946
+
947
+ // Should not throw an error
948
+ expect(true).to.be.true;
949
+ });
950
+
951
+ it('handles empty history response', async () => {
952
+ const webChat = await getWebChat({
953
+ channel: 'test-channel'
954
+ });
955
+
956
+ webChat.open = true;
957
+ await webChat.updateComplete;
958
+ mockWebSocket.manualOpen();
959
+ await webChat.updateComplete;
960
+
961
+ // Simulate empty history response
962
+ mockWebSocket.simulateMessage({
963
+ type: 'history',
964
+ history: []
965
+ });
966
+ await webChat.updateComplete;
967
+
968
+ expect(webChat.blockHistoryFetching).to.equal(false);
969
+ });
970
+
971
+ it('handles message with msg_in in history response', async () => {
972
+ const webChat = await getWebChat({
973
+ channel: 'test-channel'
974
+ });
975
+
976
+ webChat.open = true;
977
+ await webChat.updateComplete;
978
+ mockWebSocket.manualOpen();
979
+ await webChat.updateComplete;
980
+
981
+ // Simulate history response with msg_in first
982
+ mockWebSocket.simulateMessage({
983
+ type: 'history',
984
+ history: [
985
+ {
986
+ msg_in: {
987
+ id: 'msg-1',
988
+ text: 'Incoming message',
989
+ time: '2021-03-31T00:30:00.000Z'
990
+ }
991
+ }
992
+ ]
993
+ });
994
+ await webChat.updateComplete;
995
+
996
+ expect(webChat.blockHistoryFetching).to.equal(false);
997
+ });
998
+
999
+ it('prevents event propagation on input panel click', async () => {
1000
+ const webChat = await getWebChat({
1001
+ channel: 'test-channel',
1002
+ open: true,
1003
+ status: 'connected'
1004
+ });
1005
+
1006
+ const inputPanel = webChat.shadowRoot.querySelector('.input-panel');
1007
+ expect(inputPanel).to.exist;
1008
+
1009
+ let preventDefaultCalled = false;
1010
+ let stopPropagationCalled = false;
1011
+
1012
+ const mockEvent = {
1013
+ preventDefault: () => {
1014
+ preventDefaultCalled = true;
1015
+ },
1016
+ stopPropagation: () => {
1017
+ stopPropagationCalled = true;
1018
+ }
1019
+ };
1020
+
1021
+ (webChat as any).handleClickInputPanel(mockEvent);
1022
+
1023
+ expect(preventDefaultCalled).to.be.true;
1024
+ expect(stopPropagationCalled).to.be.true;
1025
+ });
1026
+
1027
+ it('handles unknown message types gracefully', async () => {
1028
+ const webChat = await getWebChat({
1029
+ channel: 'test-channel'
1030
+ });
1031
+
1032
+ webChat.open = true;
1033
+ await webChat.updateComplete;
1034
+ mockWebSocket.manualOpen();
1035
+ await webChat.updateComplete;
1036
+
1037
+ // Simulate unknown message type
1038
+ mockWebSocket.simulateMessage({
1039
+ type: 'unknown_message_type',
1040
+ data: 'some data'
1041
+ });
1042
+ await webChat.updateComplete;
1043
+
1044
+ // Should not throw an error
1045
+ expect(true).to.be.true;
1046
+ });
1047
+ });
1048
+
1049
+ describe('Integration with Chat Component', () => {
1050
+ it('listens to scroll threshold event for history fetching', async () => {
1051
+ const webChat = await getWebChat({
1052
+ channel: 'test-channel'
1053
+ });
1054
+
1055
+ webChat.open = true;
1056
+ await webChat.updateComplete;
1057
+ mockWebSocket.manualOpen();
1058
+ await webChat.updateComplete;
1059
+
1060
+ const chatElement = webChat.shadowRoot.querySelector('temba-chat');
1061
+ expect(chatElement).to.exist;
1062
+
1063
+ const initialMessageCount = mockWebSocket.sentMessages.length;
1064
+
1065
+ // Simulate scroll threshold event
1066
+ chatElement.dispatchEvent(new CustomEvent('temba-scroll-threshold'));
1067
+ await webChat.updateComplete;
1068
+
1069
+ // Should have triggered history fetch
1070
+ expect(mockWebSocket.sentMessages.length).to.equal(
1071
+ initialMessageCount + 1
1072
+ );
1073
+ const historyRequest = JSON.parse(
1074
+ mockWebSocket.sentMessages[mockWebSocket.sentMessages.length - 1]
1075
+ );
1076
+ expect(historyRequest.type).to.equal('get_history');
1077
+ });
1078
+
1079
+ it('listens to fetch complete event', async () => {
1080
+ const webChat = await getWebChat({
1081
+ channel: 'test-channel'
1082
+ });
1083
+
1084
+ webChat.blockHistoryFetching = true;
1085
+
1086
+ const chatElement = webChat.shadowRoot.querySelector('temba-chat');
1087
+ expect(chatElement).to.exist;
1088
+
1089
+ // Simulate fetch complete event
1090
+ chatElement.dispatchEvent(new CustomEvent('temba-fetch-complete'));
1091
+
1092
+ expect(webChat.blockHistoryFetching).to.equal(false);
1093
+ });
1094
+ });
1095
+ });