@nyaruka/temba-components 0.138.6 → 0.140.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 (196) hide show
  1. package/.github/workflows/cla.yml +1 -1
  2. package/.github/workflows/copilot-setup-steps.yml +6 -1
  3. package/CHANGELOG.md +26 -0
  4. package/demo/data/flows/sample-flow.json +24 -0
  5. package/dist/locales/es.js +5 -5
  6. package/dist/locales/es.js.map +1 -1
  7. package/dist/locales/fr.js +5 -5
  8. package/dist/locales/fr.js.map +1 -1
  9. package/dist/locales/locale-codes.js +2 -11
  10. package/dist/locales/locale-codes.js.map +1 -1
  11. package/dist/locales/pt.js +5 -5
  12. package/dist/locales/pt.js.map +1 -1
  13. package/dist/temba-components.js +1112 -882
  14. package/dist/temba-components.js.map +1 -1
  15. package/out-tsc/src/display/Chat.js +10 -7
  16. package/out-tsc/src/display/Chat.js.map +1 -1
  17. package/out-tsc/src/display/Dropdown.js +3 -1
  18. package/out-tsc/src/display/Dropdown.js.map +1 -1
  19. package/out-tsc/src/display/FloatingTab.js +25 -32
  20. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  21. package/out-tsc/src/display/Thumbnail.js +163 -5
  22. package/out-tsc/src/display/Thumbnail.js.map +1 -1
  23. package/out-tsc/src/flow/CanvasMenu.js +5 -3
  24. package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
  25. package/out-tsc/src/flow/CanvasNode.js +70 -29
  26. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  27. package/out-tsc/src/flow/Editor.js +290 -239
  28. package/out-tsc/src/flow/Editor.js.map +1 -1
  29. package/out-tsc/src/flow/NodeEditor.js +118 -10
  30. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  31. package/out-tsc/src/flow/Plumber.js +757 -403
  32. package/out-tsc/src/flow/Plumber.js.map +1 -1
  33. package/out-tsc/src/flow/StickyNote.js +13 -4
  34. package/out-tsc/src/flow/StickyNote.js.map +1 -1
  35. package/out-tsc/src/flow/actions/audio-player.js +112 -0
  36. package/out-tsc/src/flow/actions/audio-player.js.map +1 -0
  37. package/out-tsc/src/flow/actions/enter_flow.js +43 -0
  38. package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
  39. package/out-tsc/src/flow/actions/play_audio.js +57 -4
  40. package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
  41. package/out-tsc/src/flow/actions/say_msg.js +86 -3
  42. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  43. package/out-tsc/src/flow/config.js +11 -3
  44. package/out-tsc/src/flow/config.js.map +1 -1
  45. package/out-tsc/src/flow/nodes/shared-rules.js +1 -1
  46. package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -1
  47. package/out-tsc/src/flow/nodes/terminal.js +7 -0
  48. package/out-tsc/src/flow/nodes/terminal.js.map +1 -0
  49. package/out-tsc/src/flow/nodes/wait_for_audio.js +77 -0
  50. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
  51. package/out-tsc/src/flow/nodes/wait_for_dial.js +151 -0
  52. package/out-tsc/src/flow/nodes/wait_for_dial.js.map +1 -0
  53. package/out-tsc/src/flow/nodes/wait_for_digits.js +61 -1
  54. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
  55. package/out-tsc/src/flow/nodes/wait_for_menu.js +173 -2
  56. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
  57. package/out-tsc/src/flow/operators.js +21 -5
  58. package/out-tsc/src/flow/operators.js.map +1 -1
  59. package/out-tsc/src/flow/types.js.map +1 -1
  60. package/out-tsc/src/flow/utils.js +213 -65
  61. package/out-tsc/src/flow/utils.js.map +1 -1
  62. package/out-tsc/src/form/ArrayEditor.js +4 -2
  63. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  64. package/out-tsc/src/form/FieldRenderer.js +49 -0
  65. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  66. package/out-tsc/src/interfaces.js +2 -0
  67. package/out-tsc/src/interfaces.js.map +1 -1
  68. package/out-tsc/src/layout/Dialog.js +52 -7
  69. package/out-tsc/src/layout/Dialog.js.map +1 -1
  70. package/out-tsc/src/list/TicketList.js +4 -1
  71. package/out-tsc/src/list/TicketList.js.map +1 -1
  72. package/out-tsc/src/live/TembaChart.js.map +1 -1
  73. package/out-tsc/src/locales/es.js +5 -5
  74. package/out-tsc/src/locales/es.js.map +1 -1
  75. package/out-tsc/src/locales/fr.js +5 -5
  76. package/out-tsc/src/locales/fr.js.map +1 -1
  77. package/out-tsc/src/locales/locale-codes.js +2 -11
  78. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  79. package/out-tsc/src/locales/pt.js +5 -5
  80. package/out-tsc/src/locales/pt.js.map +1 -1
  81. package/out-tsc/src/simulator/Simulator.js +10 -3
  82. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  83. package/out-tsc/src/store/AppState.js +89 -3
  84. package/out-tsc/src/store/AppState.js.map +1 -1
  85. package/out-tsc/test/actions/play_audio.test.js +118 -0
  86. package/out-tsc/test/actions/play_audio.test.js.map +1 -0
  87. package/out-tsc/test/actions/say_msg.test.js +158 -0
  88. package/out-tsc/test/actions/say_msg.test.js.map +1 -0
  89. package/out-tsc/test/nodes/wait_for_audio.test.js +156 -0
  90. package/out-tsc/test/nodes/wait_for_audio.test.js.map +1 -0
  91. package/out-tsc/test/nodes/wait_for_dial.test.js +336 -0
  92. package/out-tsc/test/nodes/wait_for_dial.test.js.map +1 -0
  93. package/out-tsc/test/nodes/wait_for_digits.test.js +198 -84
  94. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  95. package/out-tsc/test/nodes/wait_for_menu.test.js +340 -0
  96. package/out-tsc/test/nodes/wait_for_menu.test.js.map +1 -0
  97. package/out-tsc/test/temba-floating-tab.test.js +4 -6
  98. package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
  99. package/out-tsc/test/temba-flow-collision.test.js +473 -220
  100. package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
  101. package/out-tsc/test/temba-flow-editor.test.js +0 -2
  102. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  103. package/out-tsc/test/temba-flow-plumber-connections.test.js +83 -84
  104. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  105. package/out-tsc/test/temba-flow-plumber.test.js +102 -93
  106. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  107. package/out-tsc/test/temba-node-type-selector.test.js +6 -6
  108. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
  109. package/package.json +1 -1
  110. package/screenshots/truth/actions/play_audio/editor/expression-url.png +0 -0
  111. package/screenshots/truth/actions/play_audio/editor/static-url.png +0 -0
  112. package/screenshots/truth/actions/play_audio/render/expression-url.png +0 -0
  113. package/screenshots/truth/actions/play_audio/render/static-url.png +0 -0
  114. package/screenshots/truth/actions/say_msg/editor/multiline-text.png +0 -0
  115. package/screenshots/truth/actions/say_msg/editor/simple-text.png +0 -0
  116. package/screenshots/truth/actions/say_msg/editor/text-with-audio-url.png +0 -0
  117. package/screenshots/truth/actions/say_msg/render/multiline-text.png +0 -0
  118. package/screenshots/truth/actions/say_msg/render/simple-text.png +0 -0
  119. package/screenshots/truth/actions/say_msg/render/text-with-audio-url.png +0 -0
  120. package/screenshots/truth/editor/router.png +0 -0
  121. package/screenshots/truth/editor/wait.png +0 -0
  122. package/screenshots/truth/nodes/wait_for_audio/editor/basic-audio-wait.png +0 -0
  123. package/screenshots/truth/nodes/wait_for_audio/render/basic-audio-wait.png +0 -0
  124. package/screenshots/truth/nodes/wait_for_dial/editor/basic-dial.png +0 -0
  125. package/screenshots/truth/nodes/wait_for_dial/editor/dial-with-limits.png +0 -0
  126. package/screenshots/truth/nodes/wait_for_dial/render/basic-dial.png +0 -0
  127. package/screenshots/truth/nodes/wait_for_dial/render/dial-with-limits.png +0 -0
  128. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  129. package/screenshots/truth/nodes/wait_for_digits/editor/digits-with-rules.png +0 -0
  130. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  131. package/screenshots/truth/nodes/wait_for_digits/render/digits-with-rules.png +0 -0
  132. package/screenshots/truth/nodes/wait_for_menu/editor/menu-with-digits.png +0 -0
  133. package/screenshots/truth/nodes/wait_for_menu/render/menu-with-digits.png +0 -0
  134. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  135. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  136. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  137. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  138. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  139. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  140. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  141. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  142. package/src/display/Chat.ts +13 -7
  143. package/src/display/Dropdown.ts +3 -1
  144. package/src/display/FloatingTab.ts +24 -33
  145. package/src/display/Thumbnail.ts +162 -2
  146. package/src/flow/CanvasMenu.ts +8 -3
  147. package/src/flow/CanvasNode.ts +75 -30
  148. package/src/flow/Editor.ts +336 -288
  149. package/src/flow/NodeEditor.ts +137 -9
  150. package/src/flow/Plumber.ts +1011 -457
  151. package/src/flow/StickyNote.ts +14 -4
  152. package/src/flow/actions/audio-player.ts +127 -0
  153. package/src/flow/actions/enter_flow.ts +44 -0
  154. package/src/flow/actions/play_audio.ts +64 -5
  155. package/src/flow/actions/say_msg.ts +94 -4
  156. package/src/flow/config.ts +11 -3
  157. package/src/flow/nodes/shared-rules.ts +1 -1
  158. package/src/flow/nodes/terminal.ts +9 -0
  159. package/src/flow/nodes/wait_for_audio.ts +88 -0
  160. package/src/flow/nodes/wait_for_dial.ts +176 -0
  161. package/src/flow/nodes/wait_for_digits.ts +86 -2
  162. package/src/flow/nodes/wait_for_menu.ts +209 -3
  163. package/src/flow/operators.ts +23 -5
  164. package/src/flow/types.ts +23 -1
  165. package/src/flow/utils.ts +238 -81
  166. package/src/form/ArrayEditor.ts +4 -2
  167. package/src/form/FieldRenderer.ts +64 -1
  168. package/src/interfaces.ts +3 -1
  169. package/src/layout/Dialog.ts +53 -7
  170. package/src/list/TicketList.ts +4 -1
  171. package/src/live/TembaChart.ts +1 -1
  172. package/src/locales/es.ts +13 -18
  173. package/src/locales/fr.ts +13 -18
  174. package/src/locales/locale-codes.ts +2 -11
  175. package/src/locales/pt.ts +13 -18
  176. package/src/simulator/Simulator.ts +13 -3
  177. package/src/store/AppState.ts +105 -1
  178. package/src/store/flow-definition.d.ts +2 -0
  179. package/test/actions/play_audio.test.ts +155 -0
  180. package/test/actions/say_msg.test.ts +196 -0
  181. package/test/nodes/wait_for_audio.test.ts +182 -0
  182. package/test/nodes/wait_for_dial.test.ts +382 -0
  183. package/test/nodes/wait_for_digits.test.ts +233 -109
  184. package/test/nodes/wait_for_menu.test.ts +383 -0
  185. package/test/temba-floating-tab.test.ts +4 -6
  186. package/test/temba-flow-collision.test.ts +495 -293
  187. package/test/temba-flow-editor.test.ts +0 -2
  188. package/test/temba-flow-plumber-connections.test.ts +97 -97
  189. package/test/temba-flow-plumber.test.ts +116 -103
  190. package/test/temba-node-type-selector.test.ts +6 -6
  191. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  192. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  193. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  194. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  195. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  196. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
@@ -2,6 +2,7 @@ import { expect } from '@open-wc/testing';
2
2
  import { wait_for_digits } from '../../src/flow/nodes/wait_for_digits';
3
3
  import { Node } from '../../src/store/flow-definition';
4
4
  import { NodeTest } from '../NodeHelper';
5
+ import { createOperatorOption } from '../../src/flow/operators';
5
6
 
6
7
  /**
7
8
  * Test suite for the wait_for_digits node configuration.
@@ -20,15 +21,29 @@ describe('wait_for_digits node config', () => {
20
21
  expect(wait_for_digits.type).to.equal('wait_for_digits');
21
22
  });
22
23
 
23
- it('has correct group', () => {
24
- expect(wait_for_digits.group).to.exist;
24
+ it('is voice-only', () => {
25
+ expect(wait_for_digits.flowTypes).to.deep.equal(['voice']);
25
26
  });
26
27
 
27
- it('is a simple node config without form or layout', () => {
28
- expect(wait_for_digits.form).to.be.undefined;
29
- expect(wait_for_digits.layout).to.be.undefined;
30
- expect(wait_for_digits.toFormData).to.be.undefined;
31
- expect(wait_for_digits.fromFormData).to.be.undefined;
28
+ it('has form with rules and result_name', () => {
29
+ expect(wait_for_digits.form).to.exist;
30
+ expect(wait_for_digits.form!.rules).to.exist;
31
+ expect(wait_for_digits.form!.result_name).to.exist;
32
+ });
33
+
34
+ it('has layout configuration', () => {
35
+ expect(wait_for_digits.layout).to.deep.equal([
36
+ {
37
+ type: 'text',
38
+ text: 'Rules match against all digits pressed by the caller followed by the # sign.'
39
+ },
40
+ 'rules',
41
+ 'result_name'
42
+ ]);
43
+ });
44
+
45
+ it('has large dialog size', () => {
46
+ expect(wait_for_digits.dialogSize).to.equal('large');
32
47
  });
33
48
  });
34
49
 
@@ -40,157 +55,266 @@ describe('wait_for_digits node config', () => {
40
55
  actions: [],
41
56
  router: {
42
57
  type: 'switch',
58
+ operand: '@input.text',
43
59
  wait: {
44
60
  type: 'msg',
45
61
  hint: {
46
- type: 'digits',
47
- count: 4
62
+ type: 'digits'
48
63
  }
49
64
  },
50
65
  result_name: 'digits',
66
+ default_category_uuid: 'all-cat',
67
+ cases: [],
51
68
  categories: [
52
69
  {
53
- uuid: 'digits-cat-1',
54
- name: 'Has Number',
55
- exit_uuid: 'digits-exit-1'
56
- },
57
- {
58
- uuid: 'digits-cat-2',
59
- name: 'Other',
60
- exit_uuid: 'digits-exit-2'
70
+ uuid: 'all-cat',
71
+ name: 'All Responses',
72
+ exit_uuid: 'all-exit'
61
73
  }
62
74
  ]
63
75
  },
64
- exits: [
65
- { uuid: 'digits-exit-1', destination_uuid: null },
66
- { uuid: 'digits-exit-2', destination_uuid: null }
67
- ]
76
+ exits: [{ uuid: 'all-exit', destination_uuid: null }]
68
77
  } as Node,
69
78
  { type: 'wait_for_digits' },
70
79
  'basic-digits-wait'
71
80
  );
72
81
  });
73
82
 
74
- it('renders single digit with timeout', async () => {
83
+ it('renders digits with rules', async () => {
75
84
  await helper.testNode(
76
85
  {
77
86
  uuid: 'test-digits-node-2',
78
87
  actions: [],
79
88
  router: {
80
89
  type: 'switch',
90
+ operand: '@input.text',
81
91
  wait: {
82
92
  type: 'msg',
83
93
  hint: {
84
- type: 'digits',
85
- count: 1
86
- },
87
- timeout: {
88
- category_uuid: 'timeout-cat',
89
- seconds: 30
94
+ type: 'digits'
90
95
  }
91
96
  },
92
- result_name: 'pin_digit',
93
- categories: [
97
+ result_name: 'pin',
98
+ default_category_uuid: 'other-cat',
99
+ cases: [
94
100
  {
95
- uuid: 'digits-cat-1',
96
- name: 'Has Number',
97
- exit_uuid: 'digits-exit-1'
98
- },
101
+ uuid: 'case-1',
102
+ type: 'has_number_eq',
103
+ arguments: ['1234'],
104
+ category_uuid: 'valid-cat'
105
+ }
106
+ ],
107
+ categories: [
99
108
  {
100
- uuid: 'timeout-cat',
101
- name: 'No Response',
102
- exit_uuid: 'timeout-exit'
109
+ uuid: 'valid-cat',
110
+ name: 'Valid PIN',
111
+ exit_uuid: 'valid-exit'
103
112
  },
104
113
  {
105
- uuid: 'digits-cat-2',
114
+ uuid: 'other-cat',
106
115
  name: 'Other',
107
- exit_uuid: 'digits-exit-2'
116
+ exit_uuid: 'other-exit'
108
117
  }
109
118
  ]
110
119
  },
111
120
  exits: [
112
- { uuid: 'digits-exit-1', destination_uuid: null },
113
- { uuid: 'timeout-exit', destination_uuid: null },
114
- { uuid: 'digits-exit-2', destination_uuid: null }
121
+ { uuid: 'valid-exit', destination_uuid: null },
122
+ { uuid: 'other-exit', destination_uuid: null }
115
123
  ]
116
124
  } as Node,
117
125
  { type: 'wait_for_digits' },
118
- 'single-digit-with-timeout'
126
+ 'digits-with-rules'
119
127
  );
120
128
  });
129
+ });
121
130
 
122
- it('renders phone number collection', async () => {
123
- await helper.testNode(
124
- {
125
- uuid: 'test-digits-node-3',
126
- actions: [],
127
- router: {
128
- type: 'switch',
129
- wait: {
130
- type: 'msg',
131
- hint: {
132
- type: 'digits',
133
- count: 10
134
- }
131
+ describe('data transformation', () => {
132
+ it('converts node to form data', () => {
133
+ const node: Node = {
134
+ uuid: 'test-node',
135
+ actions: [],
136
+ router: {
137
+ type: 'switch',
138
+ result_name: 'digits',
139
+ operand: '@input.text',
140
+ categories: [
141
+ {
142
+ uuid: 'valid-cat',
143
+ name: 'Valid',
144
+ exit_uuid: 'valid-exit'
135
145
  },
136
- result_name: 'phone_number',
137
- categories: [
138
- {
139
- uuid: 'phone-cat-1',
140
- name: 'Valid Phone',
141
- exit_uuid: 'phone-exit-1'
142
- },
143
- {
144
- uuid: 'phone-cat-2',
145
- name: 'Invalid',
146
- exit_uuid: 'phone-exit-2'
147
- },
148
- { uuid: 'phone-cat-3', name: 'Other', exit_uuid: 'phone-exit-3' }
149
- ]
150
- },
151
- exits: [
152
- { uuid: 'phone-exit-1', destination_uuid: null },
153
- { uuid: 'phone-exit-2', destination_uuid: null },
154
- { uuid: 'phone-exit-3', destination_uuid: null }
146
+ {
147
+ uuid: 'other-cat',
148
+ name: 'Other',
149
+ exit_uuid: 'other-exit'
150
+ }
151
+ ],
152
+ cases: [
153
+ {
154
+ uuid: 'case-1',
155
+ type: 'has_number_eq',
156
+ arguments: ['1234'],
157
+ category_uuid: 'valid-cat'
158
+ }
155
159
  ]
156
- } as Node,
157
- { type: 'wait_for_digits' },
158
- 'phone-number-collection'
159
- );
160
+ },
161
+ exits: [
162
+ { uuid: 'valid-exit', destination_uuid: null },
163
+ { uuid: 'other-exit', destination_uuid: null }
164
+ ]
165
+ };
166
+
167
+ const formData = wait_for_digits.toFormData!(node);
168
+
169
+ expect(formData.uuid).to.equal('test-node');
170
+ expect(formData.result_name).to.equal('digits');
171
+ expect(formData.rules).to.have.length(1);
172
+ expect(formData.rules[0]).to.deep.equal({
173
+ operator: createOperatorOption('has_number_eq'),
174
+ value1: '1234',
175
+ value2: '',
176
+ category: 'Valid'
177
+ });
160
178
  });
161
179
 
162
- it('renders verification code', async () => {
163
- await helper.testNode(
164
- {
165
- uuid: 'test-digits-node-4',
166
- actions: [],
167
- router: {
168
- type: 'switch',
169
- wait: {
170
- type: 'msg',
171
- hint: {
172
- type: 'digits',
173
- count: 6
174
- }
180
+ it('converts node with no rules to form data', () => {
181
+ const node: Node = {
182
+ uuid: 'test-node',
183
+ actions: [],
184
+ router: {
185
+ type: 'switch',
186
+ result_name: 'digits',
187
+ categories: []
188
+ },
189
+ exits: []
190
+ };
191
+
192
+ const formData = wait_for_digits.toFormData!(node);
193
+ expect(formData.rules).to.deep.equal([]);
194
+ expect(formData.result_name).to.equal('digits');
195
+ });
196
+
197
+ it('converts form data to node with rules', () => {
198
+ const formData = {
199
+ uuid: 'test-node',
200
+ result_name: 'digits',
201
+ rules: [
202
+ {
203
+ operator: { value: 'has_number_eq', name: 'is equal to' },
204
+ value1: '1234',
205
+ value2: '',
206
+ category: 'Valid PIN'
207
+ }
208
+ ]
209
+ };
210
+
211
+ const originalNode: Node = {
212
+ uuid: 'test-node',
213
+ actions: [],
214
+ exits: [],
215
+ router: { type: 'switch', categories: [] }
216
+ };
217
+
218
+ const result = wait_for_digits.fromFormData!(formData, originalNode);
219
+
220
+ expect(result.uuid).to.equal('test-node');
221
+ expect(result.router?.result_name).to.equal('digits');
222
+ expect(result.router?.operand).to.equal('@input.text');
223
+ expect(result.router?.categories).to.have.length(2); // Valid PIN, Other
224
+ expect(result.router?.cases).to.have.length(1);
225
+ expect(result.exits).to.have.length(2);
226
+
227
+ // Check wait config has digits hint (without count)
228
+ expect(result.router?.wait?.type).to.equal('msg');
229
+ expect(result.router?.wait?.hint?.type).to.equal('digits');
230
+ expect(result.router?.wait?.hint?.count).to.be.undefined;
231
+ });
232
+
233
+ it('converts form data with no rules', () => {
234
+ const formData = {
235
+ uuid: 'test-node',
236
+ result_name: 'digits',
237
+ rules: []
238
+ };
239
+
240
+ const originalNode: Node = {
241
+ uuid: 'test-node',
242
+ actions: [],
243
+ exits: [],
244
+ router: { type: 'switch', categories: [] }
245
+ };
246
+
247
+ const result = wait_for_digits.fromFormData!(formData, originalNode);
248
+
249
+ // Should have just "All Responses" default category (no user rules)
250
+ expect(result.router?.categories).to.have.length(1);
251
+ expect(result.router!.categories[0].name).to.equal('All Responses');
252
+ expect(result.router?.cases).to.have.length(0);
253
+ expect(result.router?.wait?.hint?.type).to.equal('digits');
254
+ });
255
+
256
+ it('preserves category UUIDs', () => {
257
+ const formData = {
258
+ uuid: 'test-node',
259
+ result_name: 'digits',
260
+ rules: [
261
+ {
262
+ operator: { value: 'has_number_eq', name: 'is equal to' },
263
+ value1: '1234',
264
+ value2: '',
265
+ category: 'Valid'
266
+ }
267
+ ]
268
+ };
269
+
270
+ const originalNode: Node = {
271
+ uuid: 'test-node',
272
+ actions: [],
273
+ router: {
274
+ type: 'switch',
275
+ categories: [
276
+ {
277
+ uuid: 'orig-valid',
278
+ name: 'Valid',
279
+ exit_uuid: 'orig-valid-exit'
175
280
  },
176
- result_name: 'verification_code',
177
- categories: [
178
- {
179
- uuid: 'code-cat-1',
180
- name: 'Valid Code',
181
- exit_uuid: 'code-exit-1'
182
- },
183
- { uuid: 'code-cat-2', name: 'Other', exit_uuid: 'code-exit-2' }
184
- ]
185
- },
186
- exits: [
187
- { uuid: 'code-exit-1', destination_uuid: null },
188
- { uuid: 'code-exit-2', destination_uuid: null }
281
+ {
282
+ uuid: 'orig-other',
283
+ name: 'Other',
284
+ exit_uuid: 'orig-other-exit'
285
+ }
286
+ ],
287
+ cases: [
288
+ {
289
+ uuid: 'orig-case',
290
+ type: 'has_number_eq',
291
+ arguments: ['1234'],
292
+ category_uuid: 'orig-valid'
293
+ }
189
294
  ]
190
- } as Node,
191
- { type: 'wait_for_digits' },
192
- 'verification-code'
193
- );
295
+ },
296
+ exits: [
297
+ { uuid: 'orig-valid-exit', destination_uuid: 'dest-1' },
298
+ { uuid: 'orig-other-exit', destination_uuid: null }
299
+ ]
300
+ };
301
+
302
+ const result = wait_for_digits.fromFormData!(formData, originalNode);
303
+
304
+ const valid = result.router!.categories.find((c) => c.name === 'Valid');
305
+ expect(valid?.uuid).to.equal('orig-valid');
306
+ expect(valid?.exit_uuid).to.equal('orig-valid-exit');
307
+
308
+ const other = result.router!.categories.find((c) => c.name === 'Other');
309
+ expect(other?.uuid).to.equal('orig-other');
310
+ });
311
+ });
312
+
313
+ describe('validation', () => {
314
+ it('validates with no errors', () => {
315
+ const formData = { rules: [] };
316
+ const result = wait_for_digits.validate!(formData);
317
+ expect(result.valid).to.be.true;
194
318
  });
195
319
  });
196
320
  });