@nyaruka/temba-components 0.121.7 → 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 +41 -0
  6. package/demo/index.html +61 -12
  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 +555 -465
  14. package/dist/temba-components.js.map +1 -1
  15. package/out-tsc/src/chart/TembaChart.js +377 -0
  16. package/out-tsc/src/chart/TembaChart.js.map +1 -0
  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 +37 -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 +2 -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 +171 -0
  40. package/out-tsc/test/temba-chart.test.js.map +1 -0
  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 +8 -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 +399 -0
  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 +39 -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 +2 -2
  206. package/test/temba-appstate-language.test.ts +218 -0
  207. package/test/temba-chart.test.ts +215 -0
  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
@@ -234,6 +234,33 @@ describe('temba-select', () => {
234
234
  await assertScreenshot('select/embedded', getClipWithOptions(select));
235
235
  });
236
236
 
237
+ it('shows no options message when opening with empty options', async () => {
238
+ const select = await createSelect(
239
+ clock,
240
+ getSelectHTML([], { placeholder: 'Select an option' })
241
+ );
242
+
243
+ // attempt to open the select with no options
244
+ await open(clock, select);
245
+
246
+ // should show options dropdown even though there are no options
247
+ const options = getOptions(select);
248
+ assert.instanceOf(options, Options);
249
+
250
+ // the options dropdown should be visible
251
+ assert.isTrue(
252
+ options.shadowRoot
253
+ .querySelector('.options-container')
254
+ .classList.contains('show')
255
+ );
256
+
257
+ // should contain a "No options" message
258
+ const noOptionsText = options.shadowRoot.textContent;
259
+ assert.include(noOptionsText.toLowerCase(), 'no options');
260
+
261
+ await assertScreenshot('select/empty-options', getClipWithOptions(select));
262
+ });
263
+
237
264
  describe('single selection', () => {
238
265
  it('can select a single option', async () => {
239
266
  const select = await createSelect(clock, getSelectHTML());
@@ -0,0 +1,386 @@
1
+ import { fixture, assert, expect } from '@open-wc/testing';
2
+ import { Toast } from '../src/toast/Toast';
3
+
4
+ // Register the component if it's not already registered
5
+ if (!customElements.get('temba-toast')) {
6
+ customElements.define('temba-toast', Toast);
7
+ }
8
+
9
+ export const createToast = async (attrs: any = {}) => {
10
+ const attrString = Object.keys(attrs)
11
+ .map((key) => `${key}="${attrs[key]}"`)
12
+ .join(' ');
13
+
14
+ return (await fixture(`<temba-toast ${attrString}></temba-toast>`)) as Toast;
15
+ };
16
+
17
+ describe('temba-toast', () => {
18
+ it('can be created', async () => {
19
+ const toast = await createToast();
20
+ assert.instanceOf(toast, Toast);
21
+ expect(toast.messages).to.deep.equal([]);
22
+ expect(toast.staleDuration).to.equal(5000);
23
+ expect(toast.animationDuration).to.equal(200);
24
+ expect(toast.errorSticky).to.be.false;
25
+ expect(toast.warningSticky).to.be.false;
26
+ expect(toast.infoSticky).to.be.false;
27
+ });
28
+
29
+ it('can set properties via attributes', async () => {
30
+ const toast = await createToast({
31
+ duration: '3000',
32
+ animation: '300',
33
+ 'error-sticky': 'true',
34
+ 'warning-sticky': 'true',
35
+ 'info-sticky': 'true'
36
+ });
37
+
38
+ expect(toast.staleDuration).to.equal(3000);
39
+ expect(toast.animationDuration).to.equal(300);
40
+ expect(toast.errorSticky).to.be.true;
41
+ expect(toast.warningSticky).to.be.true;
42
+ expect(toast.infoSticky).to.be.true;
43
+ });
44
+
45
+ it('adds info message', async () => {
46
+ const toast = await createToast();
47
+ toast.info('This is an info message');
48
+
49
+ expect(toast.messages).to.have.length(1);
50
+ expect(toast.messages[0].text).to.equal('This is an info message');
51
+ expect(toast.messages[0].level).to.equal('info');
52
+ expect(toast.messages[0].id).to.equal(1);
53
+ expect(toast.messages[0].time).to.be.instanceOf(Date);
54
+ expect(toast.messages[0].visible).to.be.undefined;
55
+ });
56
+
57
+ it('adds warning message', async () => {
58
+ const toast = await createToast();
59
+ toast.warning('This is a warning message');
60
+
61
+ expect(toast.messages).to.have.length(1);
62
+ expect(toast.messages[0].text).to.equal('This is a warning message');
63
+ expect(toast.messages[0].level).to.equal('warning');
64
+ expect(toast.messages[0].id).to.equal(1);
65
+ });
66
+
67
+ it('adds error message', async () => {
68
+ const toast = await createToast();
69
+ toast.error('This is an error message');
70
+
71
+ expect(toast.messages).to.have.length(1);
72
+ expect(toast.messages[0].text).to.equal('This is an error message');
73
+ expect(toast.messages[0].level).to.equal('error');
74
+ expect(toast.messages[0].id).to.equal(1);
75
+ });
76
+
77
+ it('adds multiple messages with incrementing IDs', async () => {
78
+ const toast = await createToast();
79
+ toast.info('First message');
80
+ toast.warning('Second message');
81
+ toast.error('Third message');
82
+
83
+ expect(toast.messages).to.have.length(3);
84
+ expect(toast.messages[0].id).to.equal(1);
85
+ expect(toast.messages[1].id).to.equal(2);
86
+ expect(toast.messages[2].id).to.equal(3);
87
+ });
88
+
89
+ it('adds multiple messages using addMessages', async () => {
90
+ const toast = await createToast();
91
+ const messages = [
92
+ {
93
+ text: 'First message',
94
+ level: 'info' as const,
95
+ id: 1,
96
+ time: new Date()
97
+ },
98
+ {
99
+ text: 'Second message',
100
+ level: 'warning' as const,
101
+ id: 2,
102
+ time: new Date()
103
+ },
104
+ {
105
+ text: 'Third message',
106
+ level: 'error' as const,
107
+ id: 3,
108
+ time: new Date()
109
+ }
110
+ ];
111
+
112
+ toast.addMessages(messages);
113
+
114
+ expect(toast.messages).to.have.length(3);
115
+ expect(toast.messages[0].text).to.equal('First message');
116
+ expect(toast.messages[0].level).to.equal('info');
117
+ expect(toast.messages[1].text).to.equal('Second message');
118
+ expect(toast.messages[1].level).to.equal('warning');
119
+ expect(toast.messages[2].text).to.equal('Third message');
120
+ expect(toast.messages[2].level).to.equal('error');
121
+ });
122
+
123
+ it('makes messages visible after delay', async () => {
124
+ const toast = await createToast();
125
+ toast.info('Test message');
126
+
127
+ // Initially not visible
128
+ expect(toast.messages[0].visible).to.be.undefined;
129
+
130
+ // Wait for the timeout
131
+ await new Promise((resolve) => setTimeout(resolve, 150));
132
+
133
+ expect(toast.messages[0].visible).to.be.true;
134
+ });
135
+
136
+ it('removes message manually', async () => {
137
+ const toast = await createToast();
138
+ toast.info('Test message');
139
+
140
+ const message = toast.messages[0];
141
+ expect(toast.messages).to.have.length(1);
142
+
143
+ toast.removeMessage(message);
144
+
145
+ // Message should have removeTime set
146
+ expect(message.removeTime).to.be.instanceOf(Date);
147
+
148
+ // Wait for animation to complete
149
+ await new Promise((resolve) => setTimeout(resolve, 250));
150
+
151
+ expect(toast.messages).to.have.length(0);
152
+ });
153
+
154
+ it('handles message click to remove', async () => {
155
+ const toast = await createToast();
156
+ toast.info('Test message');
157
+
158
+ // Wait for render
159
+ await toast.updateComplete;
160
+
161
+ const closeIcon = toast.shadowRoot?.querySelector(
162
+ 'temba-icon[name="close"]'
163
+ ) as HTMLElement;
164
+ expect(closeIcon).to.exist;
165
+
166
+ // Simulate click
167
+ closeIcon.click();
168
+
169
+ // Wait for animation
170
+ await new Promise((resolve) => setTimeout(resolve, 250));
171
+
172
+ expect(toast.messages).to.have.length(0);
173
+ });
174
+
175
+ it('handles invalid message ID in click handler', async () => {
176
+ const toast = await createToast();
177
+ toast.info('Test message');
178
+
179
+ await toast.updateComplete;
180
+
181
+ // Create a mock event with invalid message_id
182
+ const mockEvent = {
183
+ target: {
184
+ getAttribute: () => 'invalid'
185
+ }
186
+ } as any;
187
+
188
+ // Should not throw error
189
+ expect(() => {
190
+ (toast as any).handleMessageClicked(mockEvent);
191
+ }).to.not.throw();
192
+
193
+ // Message should still exist
194
+ expect(toast.messages).to.have.length(1);
195
+ });
196
+
197
+ it('handles missing message in click handler', async () => {
198
+ const toast = await createToast();
199
+ toast.info('Test message');
200
+
201
+ await toast.updateComplete;
202
+
203
+ // Create a mock event with non-existent message_id
204
+ const mockEvent = {
205
+ target: {
206
+ getAttribute: () => '999'
207
+ }
208
+ } as any;
209
+
210
+ // Should not throw error
211
+ expect(() => {
212
+ (toast as any).handleMessageClicked(mockEvent);
213
+ }).to.not.throw();
214
+
215
+ // Message should still exist
216
+ expect(toast.messages).to.have.length(1);
217
+ });
218
+
219
+ it('checks for stale messages', async () => {
220
+ const toast = await createToast({ duration: '100' }); // 100ms duration
221
+ toast.info('Test message');
222
+
223
+ // Wait for message to become stale
224
+ await new Promise((resolve) => setTimeout(resolve, 150));
225
+
226
+ // Manually trigger stale check
227
+ toast.checkForStaleMessages();
228
+
229
+ // Wait for removal animation
230
+ await new Promise((resolve) => setTimeout(resolve, 250));
231
+
232
+ expect(toast.messages).to.have.length(0);
233
+ });
234
+
235
+ it('respects sticky info messages', async () => {
236
+ const toast = await createToast({
237
+ duration: '100',
238
+ 'info-sticky': 'true'
239
+ });
240
+ toast.info('Sticky info message');
241
+
242
+ // Wait for message to become "stale"
243
+ await new Promise((resolve) => setTimeout(resolve, 150));
244
+
245
+ toast.checkForStaleMessages();
246
+
247
+ // Message should still exist because it's sticky
248
+ expect(toast.messages).to.have.length(1);
249
+ });
250
+
251
+ it('respects sticky warning messages', async () => {
252
+ const toast = await createToast({
253
+ duration: '100',
254
+ 'warning-sticky': 'true'
255
+ });
256
+ toast.warning('Sticky warning message');
257
+
258
+ // Wait for message to become "stale"
259
+ await new Promise((resolve) => setTimeout(resolve, 150));
260
+
261
+ toast.checkForStaleMessages();
262
+
263
+ // Message should still exist because it's sticky
264
+ expect(toast.messages).to.have.length(1);
265
+ });
266
+
267
+ it('respects sticky error messages', async () => {
268
+ const toast = await createToast({
269
+ duration: '100',
270
+ 'error-sticky': 'true'
271
+ });
272
+ toast.error('Sticky error message');
273
+
274
+ // Wait for message to become "stale"
275
+ await new Promise((resolve) => setTimeout(resolve, 150));
276
+
277
+ toast.checkForStaleMessages();
278
+
279
+ // Message should still exist because it's sticky
280
+ expect(toast.messages).to.have.length(1);
281
+ });
282
+
283
+ it('clears interval when no messages remain', async () => {
284
+ const toast = await createToast({ duration: '100' });
285
+ toast.info('Test message');
286
+
287
+ // Verify interval is set
288
+ expect((toast as any).checker).to.be.greaterThan(0);
289
+
290
+ // Wait for stale and removal
291
+ await new Promise((resolve) => setTimeout(resolve, 150));
292
+ toast.checkForStaleMessages();
293
+
294
+ // Wait for the removeMessage animation to complete
295
+ await new Promise((resolve) => setTimeout(resolve, 250));
296
+
297
+ // Now trigger checkForStaleMessages again to clear the interval
298
+ toast.checkForStaleMessages();
299
+
300
+ // Interval should be cleared
301
+ expect((toast as any).checker).to.equal(0);
302
+ });
303
+
304
+ it('clears existing interval when adding new message', async () => {
305
+ const toast = await createToast();
306
+ toast.info('First message');
307
+
308
+ const firstChecker = (toast as any).checker;
309
+ expect(firstChecker).to.be.greaterThan(0);
310
+
311
+ toast.info('Second message');
312
+
313
+ // Should have new interval
314
+ expect((toast as any).checker).to.be.greaterThan(0);
315
+ expect((toast as any).checker).to.not.equal(firstChecker);
316
+ });
317
+
318
+ it('renders messages with correct CSS classes', async () => {
319
+ const toast = await createToast();
320
+ toast.info('Info message');
321
+ toast.warning('Warning message');
322
+ toast.error('Error message');
323
+
324
+ await toast.updateComplete;
325
+
326
+ const messages = toast.shadowRoot?.querySelectorAll('.message');
327
+ expect(messages).to.have.length(3);
328
+
329
+ expect(messages?.[0]).to.have.class('info');
330
+ expect(messages?.[1]).to.have.class('warning');
331
+ expect(messages?.[2]).to.have.class('error');
332
+ });
333
+
334
+ it('renders messages with visible class after delay', async () => {
335
+ const toast = await createToast();
336
+ toast.info('Test message');
337
+
338
+ await toast.updateComplete;
339
+
340
+ const message = toast.shadowRoot?.querySelector('.message');
341
+ expect(message).to.not.have.class('visible');
342
+
343
+ // Wait for visibility timeout
344
+ await new Promise((resolve) => setTimeout(resolve, 150));
345
+ await toast.updateComplete;
346
+
347
+ expect(message).to.have.class('visible');
348
+ });
349
+
350
+ it('renders messages with removing class when removed', async () => {
351
+ const toast = await createToast();
352
+ toast.info('Test message');
353
+
354
+ await toast.updateComplete;
355
+
356
+ const messageData = toast.messages[0];
357
+ toast.removeMessage(messageData);
358
+
359
+ await toast.updateComplete;
360
+
361
+ const message = toast.shadowRoot?.querySelector('.message');
362
+ expect(message).to.have.class('removing');
363
+ });
364
+
365
+ it('renders close icons with correct message_id', async () => {
366
+ const toast = await createToast();
367
+ toast.info('Test message');
368
+
369
+ await toast.updateComplete;
370
+
371
+ const closeIcon = toast.shadowRoot?.querySelector(
372
+ 'temba-icon[name="close"]'
373
+ );
374
+ expect(closeIcon?.getAttribute('message_id')).to.equal('1');
375
+ });
376
+
377
+ it('renders correct animation duration styles', async () => {
378
+ const toast = await createToast({ animation: '500' });
379
+ toast.info('Test message');
380
+
381
+ await toast.updateComplete;
382
+
383
+ const message = toast.shadowRoot?.querySelector('.message') as HTMLElement;
384
+ expect(message?.style.transitionDuration).to.equal('500ms');
385
+ });
386
+ });