@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
@@ -0,0 +1,588 @@
1
+ import { assert, expect } from '@open-wc/testing';
2
+ import * as sinon from 'sinon';
3
+ import { useFakeTimers } from 'sinon';
4
+ import { CustomEventType } from '../src/interfaces';
5
+ import { RunList } from '../src/list/RunList';
6
+ import { assertScreenshot, getClip, getComponent, mockGET, mockAPI } from './utils.test';
7
+ let clock;
8
+ const TAG = 'temba-run-list';
9
+ const getRunList = async (attrs = {}, width = 250, height = 0) => {
10
+ const runList = (await getComponent(TAG, attrs, '', width, height));
11
+ if (!runList.endpoint) {
12
+ return runList;
13
+ }
14
+ return new Promise((resolve) => {
15
+ runList.addEventListener(CustomEventType.FetchComplete, async () => {
16
+ resolve(runList);
17
+ }, { once: true });
18
+ });
19
+ };
20
+ describe('temba-run-list', () => {
21
+ beforeEach(function () {
22
+ clock = useFakeTimers();
23
+ // set up general mocking
24
+ mockAPI();
25
+ // mock the runs API endpoint
26
+ mockGET(/\/api\/v2\/runs\.json/, '/test-assets/list/runs.json');
27
+ });
28
+ afterEach(function () {
29
+ clock.restore();
30
+ });
31
+ it('can be created', async () => {
32
+ const runList = await getRunList();
33
+ assert.instanceOf(runList, RunList);
34
+ expect(runList.responses).to.equal(true);
35
+ expect(runList.allowDelete).to.equal(false);
36
+ expect(runList.valueKey).to.equal('uuid');
37
+ expect(runList.hideShadow).to.equal(true);
38
+ expect(runList.reverseRefresh).to.equal(false);
39
+ });
40
+ it('initializes with correct default properties', async () => {
41
+ const runList = await getRunList();
42
+ expect(runList.responses).to.equal(true);
43
+ expect(runList.allowDelete).to.equal(false);
44
+ expect(runList.resultPreview).to.be.undefined;
45
+ expect(runList.selectedRun).to.be.undefined;
46
+ expect(runList.results).to.be.undefined;
47
+ expect(runList.flow).to.be.undefined;
48
+ });
49
+ it('sets endpoint when flow property changes', async () => {
50
+ const runList = await getRunList();
51
+ runList.flow = 'test-flow-uuid';
52
+ await runList.updateComplete;
53
+ expect(runList.endpoint).to.equal('/api/v2/runs.json?flow=test-flow-uuid&responded=1');
54
+ });
55
+ it('sets endpoint without responded parameter when responses is false', async () => {
56
+ const runList = await getRunList();
57
+ runList.responses = false;
58
+ runList.flow = 'test-flow-uuid';
59
+ await runList.updateComplete;
60
+ expect(runList.endpoint).to.equal('/api/v2/runs.json?flow=test-flow-uuid');
61
+ });
62
+ it('loads runs with flow endpoint', async () => {
63
+ const runList = await getRunList({
64
+ flow: 'test-flow-uuid'
65
+ }, 250, 400); // use bigger height to avoid overlap
66
+ expect(runList.items.length).to.equal(5);
67
+ await assertScreenshot('run-list/basic', getClip(runList));
68
+ });
69
+ it('handles results property change', async () => {
70
+ const runList = await getRunList();
71
+ // mock temba-select element
72
+ const mockSelect = document.createElement('div');
73
+ mockSelect.setOptions = sinon.spy();
74
+ sinon.stub(runList.shadowRoot, 'querySelector').returns(mockSelect);
75
+ const results = [
76
+ { key: 'name', name: 'Name', categories: ['Text'] },
77
+ { key: 'age', name: 'Age', categories: ['Number'] }
78
+ ];
79
+ runList.results = results;
80
+ await runList.updateComplete;
81
+ expect(mockSelect.setOptions.calledWith(results)).to.be.true;
82
+ // Since resultKeys is private, we test the observable behavior indirectly
83
+ // by verifying the results were processed correctly via setOptions call.
84
+ });
85
+ it('calls createRenderOption when resultPreview changes', async () => {
86
+ const runList = await getRunList();
87
+ const createRenderOptionSpy = sinon.spy(runList, 'createRenderOption');
88
+ runList.resultPreview = { key: 'name', name: 'Name' };
89
+ await runList.updateComplete;
90
+ expect(createRenderOptionSpy.called).to.be.true;
91
+ });
92
+ it('getIcon returns correct icon for completed run', async () => {
93
+ const runList = await getRunList();
94
+ const run = { exit_type: 'completed' };
95
+ const icon = runList.getIcon(run);
96
+ expect(icon.strings[0]).to.contain('temba-icon');
97
+ expect(icon.strings[0]).to.contain('name="check"');
98
+ });
99
+ it('getIcon returns correct icon for interrupted run', async () => {
100
+ const runList = await getRunList();
101
+ const run = { exit_type: 'interrupted' };
102
+ const icon = runList.getIcon(run);
103
+ expect(icon.strings[0]).to.contain('temba-icon');
104
+ expect(icon.strings[0]).to.contain('name="x-octagon"');
105
+ });
106
+ it('getIcon returns correct icon for expired run', async () => {
107
+ const runList = await getRunList();
108
+ const run = { exit_type: 'expired' };
109
+ const icon = runList.getIcon(run);
110
+ expect(icon.strings[0]).to.contain('temba-icon');
111
+ expect(icon.strings[0]).to.contain('name="clock"');
112
+ });
113
+ it('getIcon returns activity icon for active responded run', async () => {
114
+ const runList = await getRunList();
115
+ const run = { exit_type: null, responded: true };
116
+ const icon = runList.getIcon(run);
117
+ expect(icon.strings[0]).to.contain('temba-icon');
118
+ expect(icon.strings[0]).to.contain('name="activity"');
119
+ });
120
+ it('getIcon returns hourglass icon for active non-responded run', async () => {
121
+ const runList = await getRunList();
122
+ const run = { exit_type: null, responded: false };
123
+ const icon = runList.getIcon(run);
124
+ expect(icon.strings[0]).to.contain('temba-icon');
125
+ expect(icon.strings[0]).to.contain('name="hourglass"');
126
+ });
127
+ it('renderResultPreview returns category for multi-category result', async () => {
128
+ const runList = await getRunList();
129
+ runList.resultPreview = {
130
+ key: 'gender',
131
+ categories: ['Male', 'Female', 'Other']
132
+ };
133
+ const run = {
134
+ values: {
135
+ gender: {
136
+ category: 'Male',
137
+ value: 'Male'
138
+ }
139
+ }
140
+ };
141
+ const result = runList.renderResultPreview(run);
142
+ expect(result).to.equal('Male');
143
+ });
144
+ it('renderResultPreview returns value for single-category result', async () => {
145
+ const runList = await getRunList();
146
+ runList.resultPreview = { key: 'name', categories: ['Text'] };
147
+ const run = {
148
+ values: {
149
+ name: {
150
+ category: 'Text',
151
+ value: 'John Doe'
152
+ }
153
+ }
154
+ };
155
+ const result = runList.renderResultPreview(run);
156
+ expect(result).to.equal('John Doe');
157
+ });
158
+ it('renderResultPreview returns null when no result preview', async () => {
159
+ const runList = await getRunList();
160
+ const run = { values: {} };
161
+ const result = runList.renderResultPreview(run);
162
+ expect(result).to.be.null;
163
+ });
164
+ it('renderResultPreview returns null when no matching value', async () => {
165
+ const runList = await getRunList();
166
+ runList.resultPreview = { key: 'missing', categories: ['Text'] };
167
+ const run = { values: {} };
168
+ const result = runList.renderResultPreview(run);
169
+ expect(result).to.be.null;
170
+ });
171
+ it('handles results property change when results is null', async () => {
172
+ const runList = await getRunList();
173
+ // set initial results
174
+ runList.results = [{ key: 'name', name: 'Name' }];
175
+ await runList.updateComplete;
176
+ // clear results
177
+ runList.results = null;
178
+ await runList.updateComplete;
179
+ // should not throw an error
180
+ expect(runList.results).to.be.null;
181
+ });
182
+ it('handles responses/flow change when flow is not set', async () => {
183
+ const runList = await getRunList();
184
+ // change responses without setting flow
185
+ runList.responses = false;
186
+ await runList.updateComplete;
187
+ // endpoint should not be set
188
+ expect(runList.endpoint).to.be.undefined;
189
+ });
190
+ it('renderResultPreview returns null when category is missing in multi-category result', async () => {
191
+ const runList = await getRunList();
192
+ runList.resultPreview = {
193
+ key: 'gender',
194
+ categories: ['Male', 'Female', 'Other']
195
+ };
196
+ const run = {
197
+ values: {
198
+ gender: {
199
+ value: 'Male'
200
+ // missing category property
201
+ }
202
+ }
203
+ };
204
+ const result = runList.renderResultPreview(run);
205
+ expect(result).to.be.null;
206
+ });
207
+ it('removeRun removes item and updates cursor', async () => {
208
+ const runList = await getRunList({
209
+ flow: 'test-flow-uuid'
210
+ });
211
+ const initialCount = runList.items.length;
212
+ expect(initialCount).to.equal(5);
213
+ runList.cursorIndex = 2;
214
+ runList.removeRun(2);
215
+ expect(runList.items.length).to.equal(4);
216
+ expect(runList.items.find((item) => item.id === 2)).to.be.undefined;
217
+ expect(runList.cursorIndex).to.equal(2);
218
+ });
219
+ it('removeRun adjusts cursor when removing last item', async () => {
220
+ const runList = await getRunList();
221
+ // set up items manually
222
+ runList.items = [
223
+ { id: 1, uuid: 'uuid-1' },
224
+ { id: 2, uuid: 'uuid-2' }
225
+ ];
226
+ runList.cursorIndex = 1;
227
+ runList.removeRun(2);
228
+ expect(runList.items.length).to.equal(1);
229
+ expect(runList.cursorIndex).to.equal(1);
230
+ });
231
+ it('getRefreshEndpoint returns endpoint with after parameter when items exist', async () => {
232
+ const runList = await getRunList({
233
+ flow: 'test-flow-uuid'
234
+ });
235
+ const endpoint = runList.getRefreshEndpoint();
236
+ expect(endpoint).to.contain('&after=');
237
+ expect(endpoint).to.contain('2023-12-01T10:30:00.000Z');
238
+ });
239
+ it('getRefreshEndpoint returns base endpoint when no items', async () => {
240
+ const runList = await getRunList();
241
+ runList.endpoint = '/api/v2/runs.json?flow=test';
242
+ const endpoint = runList.getRefreshEndpoint();
243
+ expect(endpoint).to.equal('/api/v2/runs.json?flow=test');
244
+ });
245
+ it('toggleResponded updates responses property', async () => {
246
+ const runList = await getRunList();
247
+ // mock checkbox element
248
+ const mockCheckbox = document.createElement('input');
249
+ mockCheckbox.checked = false;
250
+ sinon.stub(runList.shadowRoot, 'querySelector').returns(mockCheckbox);
251
+ runList.toggleResponded();
252
+ expect(runList.responses).to.equal(false);
253
+ });
254
+ it('handleColumnChanged sets resultPreview from event', async () => {
255
+ const runList = await getRunList();
256
+ const event = {
257
+ target: {
258
+ values: [{ key: 'name', name: 'Name' }]
259
+ }
260
+ };
261
+ runList.handleColumnChanged(event);
262
+ expect(runList.resultPreview).to.deep.equal({ key: 'name', name: 'Name' });
263
+ });
264
+ it('handleColumnChanged clears resultPreview when no values', async () => {
265
+ const runList = await getRunList();
266
+ runList.resultPreview = { key: 'name', name: 'Name' };
267
+ const event = {
268
+ target: {
269
+ values: []
270
+ }
271
+ };
272
+ runList.handleColumnChanged(event);
273
+ expect(runList.resultPreview).to.be.null;
274
+ });
275
+ it('handleSelected sets selectedRun', async () => {
276
+ const runList = await getRunList();
277
+ const selectedRun = { id: 1, uuid: 'test-uuid' };
278
+ runList.handleSelected(selectedRun);
279
+ expect(runList.selectedRun).to.equal(selectedRun);
280
+ });
281
+ it('getListStyle returns empty string', async () => {
282
+ const runList = await getRunList();
283
+ const style = runList.getListStyle();
284
+ expect(style).to.equal('');
285
+ });
286
+ it('renderHeader shows checkbox', async () => {
287
+ const runList = await getRunList();
288
+ const header = runList.renderHeader();
289
+ const headerString = header.strings.join('');
290
+ expect(headerString).to.contain('temba-checkbox');
291
+ expect(headerString).to.contain('Responses Only');
292
+ });
293
+ it('renderHeader shows select when results exist', async () => {
294
+ const runList = await getRunList();
295
+ runList.results = [{ key: 'name', name: 'Name' }];
296
+ await runList.updateComplete;
297
+ const header = runList.renderHeader();
298
+ // check if the template includes the results check and nested template
299
+ expect(header.values).to.have.length.greaterThan(0);
300
+ // check that results is truthy which will render the select
301
+ expect(runList.results).to.not.be.null;
302
+ });
303
+ it('renderHeader without results hides select', async () => {
304
+ const runList = await getRunList();
305
+ const header = runList.renderHeader();
306
+ expect(header.strings.join('')).to.not.contain('temba-select');
307
+ expect(header.strings.join('')).to.contain('temba-checkbox');
308
+ });
309
+ it('renderFooter returns null when no selectedRun', async () => {
310
+ const runList = await getRunList();
311
+ const footer = runList.renderFooter();
312
+ expect(footer).to.be.null;
313
+ });
314
+ it('renderFooter returns null when no resultKeys', async () => {
315
+ const runList = await getRunList();
316
+ runList.selectedRun = { id: 1, values: {} };
317
+ // Don't set results, so resultKeys will be empty object {} which is truthy.
318
+ // The method only returns null if selectedRun is null/undefined, not for empty resultKeys.
319
+ const footer = runList.renderFooter();
320
+ expect(footer).to.not.be.null; // Empty object {} is truthy, so footer should render
321
+ });
322
+ it('renderFooter handles selectedRun without values', async () => {
323
+ const runList = await getRunList();
324
+ // mock temba-select element for the results
325
+ const mockSelect = document.createElement('div');
326
+ mockSelect.setOptions = sinon.spy();
327
+ sinon.stub(runList.shadowRoot, 'querySelector').returns(mockSelect);
328
+ // set results to populate resultKeys
329
+ runList.results = [];
330
+ await runList.updateComplete;
331
+ runList.selectedRun = {
332
+ id: 1,
333
+ contact: {
334
+ uuid: 'contact-uuid',
335
+ name: 'John Doe',
336
+ urn: 'tel:+1234567890'
337
+ },
338
+ created_on: '2023-12-01T10:00:00.000Z'
339
+ };
340
+ // should work now with the safety check
341
+ const footer = runList.renderFooter();
342
+ expect(footer).to.not.be.null;
343
+ });
344
+ it('renderFooter displays contact information', async () => {
345
+ const runList = await getRunList();
346
+ // mock temba-select element for the results
347
+ const mockSelect = document.createElement('div');
348
+ mockSelect.setOptions = sinon.spy();
349
+ sinon.stub(runList.shadowRoot, 'querySelector').returns(mockSelect);
350
+ // set results to populate resultKeys
351
+ runList.results = [{ key: 'name', name: 'Name', categories: ['Text'] }];
352
+ await runList.updateComplete;
353
+ runList.selectedRun = {
354
+ id: 1,
355
+ contact: {
356
+ uuid: 'contact-uuid',
357
+ name: 'John Doe',
358
+ urn: 'tel:+1234567890'
359
+ },
360
+ exit_type: 'completed',
361
+ exited_on: '2023-12-01T10:30:00.000Z',
362
+ created_on: '2023-12-01T10:00:00.000Z',
363
+ values: {
364
+ name: { name: 'Name', key: 'name', value: 'John Doe', category: 'Text' }
365
+ }
366
+ };
367
+ const footer = runList.renderFooter();
368
+ expect(footer).to.not.be.null;
369
+ expect(footer.strings[0]).to.contain('temba-contact-name');
370
+ });
371
+ it('renderFooter shows delete icon when allowDelete is true', async () => {
372
+ const runList = await getRunList();
373
+ // mock temba-select element for the results
374
+ const mockSelect = document.createElement('div');
375
+ mockSelect.setOptions = sinon.spy();
376
+ sinon.stub(runList.shadowRoot, 'querySelector').returns(mockSelect);
377
+ // set results to populate resultKeys
378
+ runList.results = [];
379
+ await runList.updateComplete;
380
+ runList.allowDelete = true;
381
+ runList.selectedRun = {
382
+ id: 1,
383
+ contact: {
384
+ uuid: 'contact-uuid',
385
+ name: 'John Doe',
386
+ urn: 'tel:+1234567890'
387
+ },
388
+ created_on: '2023-12-01T10:00:00.000Z',
389
+ values: {}
390
+ };
391
+ const footer = runList.renderFooter();
392
+ expect(footer).to.not.be.null;
393
+ // verify the conditions that would show the delete icon
394
+ expect(runList.allowDelete).to.be.true;
395
+ expect(runList.selectedRun.id).to.equal(1);
396
+ });
397
+ it('renderFooter shows active run status', async () => {
398
+ const runList = await getRunList();
399
+ // mock temba-select element for the results
400
+ const mockSelect = document.createElement('div');
401
+ mockSelect.setOptions = sinon.spy();
402
+ sinon.stub(runList.shadowRoot, 'querySelector').returns(mockSelect);
403
+ // set results to populate resultKeys
404
+ runList.results = [];
405
+ await runList.updateComplete;
406
+ runList.selectedRun = {
407
+ id: 1,
408
+ contact: {
409
+ uuid: 'contact-uuid',
410
+ name: 'John Doe',
411
+ urn: 'tel:+1234567890'
412
+ },
413
+ exit_type: null,
414
+ created_on: '2023-12-01T10:00:00.000Z',
415
+ values: {}
416
+ };
417
+ const footer = runList.renderFooter();
418
+ expect(footer.strings.join('')).to.contain('Started');
419
+ });
420
+ it('createRenderOption creates renderOption function', async () => {
421
+ const runList = await getRunList();
422
+ expect(runList.renderOption).to.be.a('function');
423
+ const run = {
424
+ contact: {
425
+ name: 'John Doe',
426
+ urn: 'tel:+1234567890',
427
+ anon_display: '1234567890'
428
+ },
429
+ modified_on: '2023-12-01T10:30:00.000Z',
430
+ exited_on: '2023-12-01T10:30:00.000Z',
431
+ responded: true
432
+ };
433
+ const result = runList.renderOption(run, false);
434
+ expect(result.strings.join('')).to.contain('temba-contact-name');
435
+ expect(result.strings.join('')).to.contain('temba-date');
436
+ });
437
+ it('renderOption handles run without contact name', async () => {
438
+ const runList = await getRunList();
439
+ const run = {
440
+ contact: {
441
+ name: null,
442
+ urn: 'tel:+1234567890',
443
+ anon_display: '1234567890'
444
+ },
445
+ modified_on: '2023-12-01T10:30:00.000Z',
446
+ exited_on: null,
447
+ responded: false
448
+ };
449
+ const result = runList.renderOption(run, false);
450
+ expect(result.strings.join('')).to.contain('temba-contact-name');
451
+ });
452
+ it('renderOption handles run without contact', async () => {
453
+ const runList = await getRunList();
454
+ const run = {
455
+ modified_on: '2023-12-01T10:30:00.000Z',
456
+ exited_on: null,
457
+ responded: false
458
+ };
459
+ const result = runList.renderOption(run, false);
460
+ expect(result.strings.join('')).to.contain('temba-contact-name');
461
+ });
462
+ it('handles results without categories in renderResultPreview', async () => {
463
+ const runList = await getRunList();
464
+ runList.resultPreview = { key: 'name', categories: ['Text'] };
465
+ const run = {
466
+ values: {
467
+ name: {
468
+ value: 'Test Value'
469
+ // missing category property
470
+ }
471
+ }
472
+ };
473
+ const result = runList.renderResultPreview(run);
474
+ expect(result).to.equal('Test Value');
475
+ });
476
+ it('firstUpdated calls super', async () => {
477
+ const runList = await getRunList();
478
+ const superSpy = sinon.spy(Object.getPrototypeOf(RunList.prototype), 'firstUpdated');
479
+ runList.firstUpdated(new Map());
480
+ expect(superSpy.called).to.be.true;
481
+ superSpy.restore();
482
+ });
483
+ it('renderFooter shows result values', async () => {
484
+ const runList = await getRunList();
485
+ // mock temba-select element for the results
486
+ const mockSelect = document.createElement('div');
487
+ mockSelect.setOptions = sinon.spy();
488
+ sinon.stub(runList.shadowRoot, 'querySelector').returns(mockSelect);
489
+ // set results to populate resultKeys
490
+ runList.results = [{ key: 'name', name: 'Name', categories: ['Text'] }];
491
+ await runList.updateComplete;
492
+ runList.selectedRun = {
493
+ id: 1,
494
+ contact: {
495
+ uuid: 'contact-uuid',
496
+ name: 'John Doe',
497
+ urn: 'tel:+1234567890'
498
+ },
499
+ created_on: '2023-12-01T10:00:00.000Z',
500
+ values: {
501
+ name: { name: 'Name', key: 'name', value: 'John Doe', category: 'Text' }
502
+ }
503
+ };
504
+ const footer = runList.renderFooter();
505
+ expect(footer).to.not.be.null;
506
+ // check that the conditions for showing the table are met
507
+ const resultKeys = Object.keys(runList.selectedRun.values || {});
508
+ expect(resultKeys.length).to.be.greaterThan(0);
509
+ });
510
+ it('renderFooter shows multi-category display', async () => {
511
+ const runList = await getRunList();
512
+ // mock temba-select element for the results
513
+ const mockSelect = document.createElement('div');
514
+ mockSelect.setOptions = sinon.spy();
515
+ sinon.stub(runList.shadowRoot, 'querySelector').returns(mockSelect);
516
+ // set results to populate resultKeys
517
+ runList.results = [
518
+ { key: 'gender', name: 'Gender', categories: ['Male', 'Female', 'Other'] }
519
+ ];
520
+ await runList.updateComplete;
521
+ runList.selectedRun = {
522
+ id: 1,
523
+ contact: {
524
+ uuid: 'contact-uuid',
525
+ name: 'John Doe',
526
+ urn: 'tel:+1234567890'
527
+ },
528
+ created_on: '2023-12-01T10:00:00.000Z',
529
+ values: {
530
+ gender: {
531
+ name: 'Gender',
532
+ key: 'gender',
533
+ value: 'Male',
534
+ category: 'Male'
535
+ }
536
+ }
537
+ };
538
+ const footer = runList.renderFooter();
539
+ expect(footer).to.not.be.null;
540
+ // check that we have data setup correctly by verifying the selected run has values
541
+ expect(Object.keys(runList.selectedRun.values).length).to.be.greaterThan(0);
542
+ });
543
+ it('renderFooter handles missing contact uuid', async () => {
544
+ const runList = await getRunList();
545
+ // mock temba-select element for the results
546
+ const mockSelect = document.createElement('div');
547
+ mockSelect.setOptions = sinon.spy();
548
+ sinon.stub(runList.shadowRoot, 'querySelector').returns(mockSelect);
549
+ // set results to populate resultKeys
550
+ runList.results = [];
551
+ await runList.updateComplete;
552
+ runList.selectedRun = {
553
+ id: 1,
554
+ contact: null, // Missing contact to trigger the fallback
555
+ created_on: '2023-12-01T10:00:00.000Z',
556
+ values: {}
557
+ };
558
+ const footer = runList.renderFooter();
559
+ expect(footer).to.not.be.null;
560
+ expect(footer.strings[0]).to.contain('temba-contact-name');
561
+ });
562
+ it('renderFooter shows single-category result display', async () => {
563
+ const runList = await getRunList();
564
+ // mock temba-select element for the results
565
+ const mockSelect = document.createElement('div');
566
+ mockSelect.setOptions = sinon.spy();
567
+ sinon.stub(runList.shadowRoot, 'querySelector').returns(mockSelect);
568
+ // set results to populate resultKeys
569
+ runList.results = [{ key: 'name', name: 'Name', categories: ['Text'] }];
570
+ await runList.updateComplete;
571
+ runList.selectedRun = {
572
+ id: 1,
573
+ contact: {
574
+ uuid: 'contact-uuid',
575
+ name: 'John Doe',
576
+ urn: 'tel:+1234567890'
577
+ },
578
+ created_on: '2023-12-01T10:00:00.000Z',
579
+ values: {
580
+ name: { name: 'Name', key: 'name', value: 'John Doe', category: 'Text' }
581
+ }
582
+ };
583
+ const footer = runList.renderFooter();
584
+ const footerString = footer.strings.join('');
585
+ expect(footerString).to.contain('--'); // Single category shows '--' for category
586
+ });
587
+ });
588
+ //# sourceMappingURL=temba-run-list.test.js.map