@nyaruka/temba-components 0.129.3 → 0.129.5

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 (304) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.github/workflows/build.yml +135 -3
  3. package/CHANGELOG.md +19 -0
  4. package/demo/data/flows/sample-flow.json +110 -87
  5. package/demo/field-config-demo.html +135 -0
  6. package/dist/temba-components.js +1257 -675
  7. package/dist/temba-components.js.map +1 -1
  8. package/docs/ActionEditor-Migration.md +118 -0
  9. package/out-tsc/src/events.js.map +1 -1
  10. package/out-tsc/src/flow/{EditorNode.js → CanvasNode.js} +345 -42
  11. package/out-tsc/src/flow/CanvasNode.js.map +1 -0
  12. package/out-tsc/src/flow/Editor.js +107 -3
  13. package/out-tsc/src/flow/Editor.js.map +1 -1
  14. package/out-tsc/src/flow/NodeEditor.js +1211 -0
  15. package/out-tsc/src/flow/NodeEditor.js.map +1 -0
  16. package/out-tsc/src/flow/Plumber.js +0 -6
  17. package/out-tsc/src/flow/Plumber.js.map +1 -1
  18. package/out-tsc/src/flow/actions/add_contact_groups.js +40 -0
  19. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -0
  20. package/out-tsc/src/flow/actions/add_contact_urn.js +16 -0
  21. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -0
  22. package/out-tsc/src/flow/actions/add_input_labels.js +11 -0
  23. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -0
  24. package/out-tsc/src/flow/actions/call_classifier.js +11 -0
  25. package/out-tsc/src/flow/actions/call_classifier.js.map +1 -0
  26. package/out-tsc/src/flow/actions/call_llm.js +11 -0
  27. package/out-tsc/src/flow/actions/call_llm.js.map +1 -0
  28. package/out-tsc/src/flow/actions/call_resthook.js +11 -0
  29. package/out-tsc/src/flow/actions/call_resthook.js.map +1 -0
  30. package/out-tsc/src/flow/actions/call_webhook.js +122 -0
  31. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -0
  32. package/out-tsc/src/flow/actions/enter_flow.js +14 -0
  33. package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
  34. package/out-tsc/src/flow/actions/open_ticket.js +11 -0
  35. package/out-tsc/src/flow/actions/open_ticket.js.map +1 -0
  36. package/out-tsc/src/flow/actions/play_audio.js +11 -0
  37. package/out-tsc/src/flow/actions/play_audio.js.map +1 -0
  38. package/out-tsc/src/flow/actions/remove_contact_groups.js +62 -0
  39. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -0
  40. package/out-tsc/src/flow/actions/request_optin.js +11 -0
  41. package/out-tsc/src/flow/actions/request_optin.js.map +1 -0
  42. package/out-tsc/src/flow/actions/say_msg.js +11 -0
  43. package/out-tsc/src/flow/actions/say_msg.js.map +1 -0
  44. package/out-tsc/src/flow/actions/send_broadcast.js +33 -0
  45. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -0
  46. package/out-tsc/src/flow/actions/send_email.js +56 -0
  47. package/out-tsc/src/flow/actions/send_email.js.map +1 -0
  48. package/out-tsc/src/flow/actions/send_msg.js +55 -0
  49. package/out-tsc/src/flow/actions/send_msg.js.map +1 -0
  50. package/out-tsc/src/flow/actions/set_contact_channel.js +12 -0
  51. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -0
  52. package/out-tsc/src/flow/actions/set_contact_field.js +12 -0
  53. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -0
  54. package/out-tsc/src/flow/actions/set_contact_language.js +10 -0
  55. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -0
  56. package/out-tsc/src/flow/actions/set_contact_name.js +10 -0
  57. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -0
  58. package/out-tsc/src/flow/actions/set_contact_status.js +10 -0
  59. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -0
  60. package/out-tsc/src/flow/actions/set_run_result.js +10 -0
  61. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -0
  62. package/out-tsc/src/flow/actions/split_by_expression_example.js +77 -0
  63. package/out-tsc/src/flow/actions/split_by_expression_example.js.map +1 -0
  64. package/out-tsc/src/flow/actions/start_session.js +11 -0
  65. package/out-tsc/src/flow/actions/start_session.js.map +1 -0
  66. package/out-tsc/src/flow/actions/transfer_airtime.js +11 -0
  67. package/out-tsc/src/flow/actions/transfer_airtime.js.map +1 -0
  68. package/out-tsc/src/flow/config.js +88 -193
  69. package/out-tsc/src/flow/config.js.map +1 -1
  70. package/out-tsc/src/flow/nodes/execute_actions.js +4 -0
  71. package/out-tsc/src/flow/nodes/execute_actions.js.map +1 -0
  72. package/out-tsc/src/flow/nodes/split_by_airtime.js +9 -0
  73. package/out-tsc/src/flow/nodes/split_by_airtime.js.map +1 -0
  74. package/out-tsc/src/flow/nodes/split_by_contact_field.js +7 -0
  75. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -0
  76. package/out-tsc/src/flow/nodes/split_by_expression.js +7 -0
  77. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -0
  78. package/out-tsc/src/flow/nodes/split_by_groups.js +7 -0
  79. package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -0
  80. package/out-tsc/src/flow/nodes/split_by_random.js +10 -0
  81. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -0
  82. package/out-tsc/src/flow/nodes/split_by_run_result.js +7 -0
  83. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -0
  84. package/out-tsc/src/flow/nodes/split_by_scheme.js +7 -0
  85. package/out-tsc/src/flow/nodes/split_by_scheme.js.map +1 -0
  86. package/out-tsc/src/flow/nodes/split_by_subflow.js +9 -0
  87. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -0
  88. package/out-tsc/src/flow/nodes/split_by_webhook.js +18 -0
  89. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -0
  90. package/out-tsc/src/flow/nodes/wait_for_audio.js +7 -0
  91. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
  92. package/out-tsc/src/flow/nodes/wait_for_digits.js +7 -0
  93. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -0
  94. package/out-tsc/src/flow/nodes/wait_for_image.js +7 -0
  95. package/out-tsc/src/flow/nodes/wait_for_image.js.map +1 -0
  96. package/out-tsc/src/flow/nodes/wait_for_location.js +7 -0
  97. package/out-tsc/src/flow/nodes/wait_for_location.js.map +1 -0
  98. package/out-tsc/src/flow/nodes/wait_for_menu.js +7 -0
  99. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -0
  100. package/out-tsc/src/flow/nodes/wait_for_response.js +7 -0
  101. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -0
  102. package/out-tsc/src/flow/nodes/wait_for_video.js +7 -0
  103. package/out-tsc/src/flow/nodes/wait_for_video.js.map +1 -0
  104. package/out-tsc/src/flow/types.js +79 -0
  105. package/out-tsc/src/flow/types.js.map +1 -0
  106. package/out-tsc/src/flow/utils.js +65 -0
  107. package/out-tsc/src/flow/utils.js.map +1 -0
  108. package/out-tsc/src/form/ArrayEditor.js +199 -0
  109. package/out-tsc/src/form/ArrayEditor.js.map +1 -0
  110. package/out-tsc/src/form/BaseListEditor.js +128 -0
  111. package/out-tsc/src/form/BaseListEditor.js.map +1 -0
  112. package/out-tsc/src/form/Checkbox.js +17 -2
  113. package/out-tsc/src/form/Checkbox.js.map +1 -1
  114. package/out-tsc/src/form/Completion.js +6 -0
  115. package/out-tsc/src/form/Completion.js.map +1 -1
  116. package/out-tsc/src/form/FormField.js +110 -11
  117. package/out-tsc/src/form/FormField.js.map +1 -1
  118. package/out-tsc/src/form/KeyValueEditor.js +223 -0
  119. package/out-tsc/src/form/KeyValueEditor.js.map +1 -0
  120. package/out-tsc/src/form/select/Select.js +92 -32
  121. package/out-tsc/src/form/select/Select.js.map +1 -1
  122. package/out-tsc/src/interfaces.js +6 -0
  123. package/out-tsc/src/interfaces.js.map +1 -1
  124. package/out-tsc/src/live/ContactChat.js +2 -76
  125. package/out-tsc/src/live/ContactChat.js.map +1 -1
  126. package/out-tsc/temba-modules.js +9 -2
  127. package/out-tsc/temba-modules.js.map +1 -1
  128. package/out-tsc/test/ActionHelper.js +116 -0
  129. package/out-tsc/test/ActionHelper.js.map +1 -0
  130. package/out-tsc/test/actions/add_contact_groups.test.js +66 -0
  131. package/out-tsc/test/actions/add_contact_groups.test.js.map +1 -0
  132. package/out-tsc/test/actions/remove_contact_groups.test.js +226 -0
  133. package/out-tsc/test/actions/remove_contact_groups.test.js.map +1 -0
  134. package/out-tsc/test/actions/send_email.test.js +160 -0
  135. package/out-tsc/test/actions/send_email.test.js.map +1 -0
  136. package/out-tsc/test/actions/send_msg.test.js +95 -0
  137. package/out-tsc/test/actions/send_msg.test.js.map +1 -0
  138. package/out-tsc/test/temba-action-editing-integration.test.js +183 -0
  139. package/out-tsc/test/temba-action-editing-integration.test.js.map +1 -0
  140. package/out-tsc/test/temba-checkbox.test.js +1 -1
  141. package/out-tsc/test/temba-checkbox.test.js.map +1 -1
  142. package/out-tsc/test/temba-field-config.test.js +133 -0
  143. package/out-tsc/test/temba-field-config.test.js.map +1 -0
  144. package/out-tsc/test/temba-flow-editor-node.test.js +14 -14
  145. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  146. package/out-tsc/test/temba-node-editor.test.js +283 -0
  147. package/out-tsc/test/temba-node-editor.test.js.map +1 -0
  148. package/out-tsc/test/temba-select.test.js +158 -0
  149. package/out-tsc/test/temba-select.test.js.map +1 -1
  150. package/package.json +1 -1
  151. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  152. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  153. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  154. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  155. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  156. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  157. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  158. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  159. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  160. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  161. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  162. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  163. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  164. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  165. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  166. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  167. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  168. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  169. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  170. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  171. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  172. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  173. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  174. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  175. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  176. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  177. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  178. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  179. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  180. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  181. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  182. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  183. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  184. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  185. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  186. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  187. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  188. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  189. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  190. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  191. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  192. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  193. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  194. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  195. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  196. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  197. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  198. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  199. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  200. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  201. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  202. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  203. package/screenshots/truth/editor/router.png +0 -0
  204. package/screenshots/truth/editor/send_msg.png +0 -0
  205. package/screenshots/truth/editor/set_contact_language.png +0 -0
  206. package/screenshots/truth/editor/set_contact_name.png +0 -0
  207. package/screenshots/truth/editor/set_run_result.png +0 -0
  208. package/screenshots/truth/editor/wait.png +0 -0
  209. package/screenshots/truth/formfield/markdown-errors.png +0 -0
  210. package/screenshots/truth/formfield/plain-text-errors.png +0 -0
  211. package/screenshots/truth/formfield/widget-only-markdown-errors.png +0 -0
  212. package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
  213. package/src/events.ts +1 -40
  214. package/src/flow/{EditorNode.ts → CanvasNode.ts} +424 -48
  215. package/src/flow/Editor.ts +140 -4
  216. package/src/flow/NodeEditor.ts +1454 -0
  217. package/src/flow/Plumber.ts +0 -9
  218. package/src/flow/actions/add_contact_groups.ts +42 -0
  219. package/src/flow/actions/add_contact_urn.ts +17 -0
  220. package/src/flow/actions/add_input_labels.ts +12 -0
  221. package/src/flow/actions/call_classifier.ts +12 -0
  222. package/src/flow/actions/call_llm.ts +12 -0
  223. package/src/flow/actions/call_resthook.ts +12 -0
  224. package/src/flow/actions/call_webhook.ts +133 -0
  225. package/src/flow/actions/enter_flow.ts +15 -0
  226. package/src/flow/actions/open_ticket.ts +12 -0
  227. package/src/flow/actions/play_audio.ts +12 -0
  228. package/src/flow/actions/remove_contact_groups.ts +66 -0
  229. package/src/flow/actions/request_optin.ts +12 -0
  230. package/src/flow/actions/say_msg.ts +12 -0
  231. package/src/flow/actions/send_broadcast.ts +35 -0
  232. package/src/flow/actions/send_email.ts +60 -0
  233. package/src/flow/actions/send_msg.ts +58 -0
  234. package/src/flow/actions/set_contact_channel.ts +13 -0
  235. package/src/flow/actions/set_contact_field.ts +13 -0
  236. package/src/flow/actions/set_contact_language.ts +11 -0
  237. package/src/flow/actions/set_contact_name.ts +11 -0
  238. package/src/flow/actions/set_contact_status.ts +11 -0
  239. package/src/flow/actions/set_run_result.ts +11 -0
  240. package/src/flow/actions/split_by_expression_example.ts +88 -0
  241. package/src/flow/actions/start_session.ts +12 -0
  242. package/src/flow/actions/transfer_airtime.ts +12 -0
  243. package/src/flow/config.ts +93 -232
  244. package/src/flow/nodes/execute_actions.ts +5 -0
  245. package/src/flow/nodes/split_by_airtime.ts +9 -0
  246. package/src/flow/nodes/split_by_contact_field.ts +7 -0
  247. package/src/flow/nodes/split_by_expression.ts +7 -0
  248. package/src/flow/nodes/split_by_groups.ts +7 -0
  249. package/src/flow/nodes/split_by_random.ts +10 -0
  250. package/src/flow/nodes/split_by_run_result.ts +7 -0
  251. package/src/flow/nodes/split_by_scheme.ts +7 -0
  252. package/src/flow/nodes/split_by_subflow.ts +9 -0
  253. package/src/flow/nodes/split_by_webhook.ts +19 -0
  254. package/src/flow/nodes/wait_for_audio.ts +7 -0
  255. package/src/flow/nodes/wait_for_digits.ts +7 -0
  256. package/src/flow/nodes/wait_for_image.ts +7 -0
  257. package/src/flow/nodes/wait_for_location.ts +7 -0
  258. package/src/flow/nodes/wait_for_menu.ts +7 -0
  259. package/src/flow/nodes/wait_for_response.ts +7 -0
  260. package/src/flow/nodes/wait_for_video.ts +7 -0
  261. package/src/flow/types.ts +352 -0
  262. package/src/flow/utils.ts +76 -0
  263. package/src/form/ArrayEditor.ts +240 -0
  264. package/src/form/BaseListEditor.ts +177 -0
  265. package/src/form/Checkbox.ts +22 -3
  266. package/src/form/Completion.ts +6 -0
  267. package/src/form/FormField.ts +115 -11
  268. package/src/form/KeyValueEditor.ts +251 -0
  269. package/src/form/select/Select.ts +105 -32
  270. package/src/interfaces.ts +7 -2
  271. package/src/live/ContactChat.ts +3 -97
  272. package/src/store/flow-definition.d.ts +6 -1
  273. package/static/api/contacts.json +30 -0
  274. package/static/api/groups.json +4 -426
  275. package/static/api/locations.json +24 -0
  276. package/static/api/media.json +5 -0
  277. package/static/api/optins.json +16 -0
  278. package/static/api/orgs.json +13 -0
  279. package/static/api/topics.json +21 -0
  280. package/static/api/users.json +26 -0
  281. package/static/css/temba-components.css +3 -6
  282. package/temba-modules.ts +9 -2
  283. package/test/ActionHelper.ts +142 -0
  284. package/test/actions/add_contact_groups.test.ts +89 -0
  285. package/test/actions/remove_contact_groups.test.ts +265 -0
  286. package/test/actions/send_email.test.ts +214 -0
  287. package/test/actions/send_msg.test.ts +130 -0
  288. package/test/temba-action-editing-integration.test.ts +240 -0
  289. package/test/temba-checkbox.test.ts +1 -1
  290. package/test/temba-field-config.test.ts +152 -0
  291. package/test/temba-flow-editor-node.test.ts +18 -18
  292. package/test/temba-node-editor.test.ts +353 -0
  293. package/test/temba-select.test.ts +234 -0
  294. package/test-assets/contacts/history.json +11 -33
  295. package/web-dev-server.config.mjs +34 -0
  296. package/.github/workflows/coverage.yml +0 -80
  297. package/demo/sticky-note-demo.html +0 -155
  298. package/out-tsc/src/flow/EditorNode.js.map +0 -1
  299. package/out-tsc/src/flow/render.js +0 -358
  300. package/out-tsc/src/flow/render.js.map +0 -1
  301. package/out-tsc/test/temba-flow-render.test.js +0 -794
  302. package/out-tsc/test/temba-flow-render.test.js.map +0 -1
  303. package/src/flow/render.ts +0 -443
  304. package/test/temba-flow-render.test.ts +0 -1003
@@ -0,0 +1,142 @@
1
+ import { fixture, expect } from '@open-wc/testing';
2
+ import { html } from 'lit';
3
+ import { Action, Node } from '../src/store/flow-definition';
4
+ import { assertScreenshot, getClip } from './utils.test';
5
+ import { Editor } from '../src/flow/Editor';
6
+ import '../temba-modules';
7
+
8
+ /**
9
+ * Generic action test framework
10
+ * Tests the complete action lifecycle: render → edit → save → validate
11
+ */
12
+ export class ActionTest<T extends Action> {
13
+ constructor(private actionConfig: any, private actionName: string) {}
14
+
15
+ /**
16
+ * Renders an action in the flow editor and returns the flow node
17
+ */
18
+ private async renderAction(action: T): Promise<HTMLElement> {
19
+ const node: Node = {
20
+ uuid: 'test-node',
21
+ actions: [action],
22
+ exits: []
23
+ };
24
+
25
+ const mockDefinition = {
26
+ nodes: [node],
27
+ _ui: {
28
+ nodes: {
29
+ [node.uuid]: {
30
+ type: 'execute_actions',
31
+ position: { left: 50, top: 50 }
32
+ }
33
+ }
34
+ }
35
+ };
36
+
37
+ const editor = (await fixture(html`
38
+ <temba-flow-editor>
39
+ <div id="canvas"></div>
40
+ </temba-flow-editor>
41
+ `)) as Editor;
42
+
43
+ (editor as any).definition = mockDefinition;
44
+ (editor as any).canvasSize = { width: 400, height: 300 };
45
+ await editor.updateComplete;
46
+
47
+ const flowNode = editor.querySelector('temba-flow-node') as HTMLElement;
48
+ expect(flowNode).to.exist;
49
+
50
+ return flowNode;
51
+ }
52
+
53
+ /**
54
+ * Opens the node editor for an action and returns the editor element
55
+ */
56
+ private async openNodeEditor(action: T): Promise<HTMLElement> {
57
+ const nodeEditor = (await fixture(html`
58
+ <temba-node-editor .action=${action} .isOpen=${true}></temba-node-editor>
59
+ `)) as HTMLElement;
60
+
61
+ await (nodeEditor as any).updateComplete;
62
+ expect(nodeEditor).to.exist;
63
+
64
+ return nodeEditor;
65
+ }
66
+
67
+ /**
68
+ * Takes a screenshot of the dialog container within a node editor
69
+ */
70
+ private async assertDialogScreenshot(
71
+ el: HTMLElement,
72
+ screenshotName: string
73
+ ) {
74
+ const dialog = el.shadowRoot
75
+ .querySelector('temba-dialog')
76
+ .shadowRoot.querySelector('.dialog-container') as HTMLElement;
77
+ await assertScreenshot(screenshotName, getClip(dialog));
78
+ }
79
+
80
+ /**
81
+ * Complete test for an action configuration
82
+ * 1. Renders the action in a flow node (with screenshot)
83
+ * 2. Opens the node editor (with screenshot)
84
+ * 3. Simulates save and validates round-trip conversion
85
+ */
86
+ async testAction(action: T, testName: string) {
87
+ it(`${testName}`, async () => {
88
+ // Step 1: Render action in flow node
89
+ const flowNode = await this.renderAction(action);
90
+ expect(flowNode.querySelector('.body')).to.exist;
91
+ await assertScreenshot(
92
+ `actions/${this.actionName}/render/${testName}`,
93
+ getClip(flowNode)
94
+ );
95
+
96
+ // Step 2: Open node editor
97
+ const nodeEditor = await this.openNodeEditor(action);
98
+ await this.assertDialogScreenshot(
99
+ nodeEditor,
100
+ `actions/${this.actionName}/editor/${testName}`
101
+ );
102
+
103
+ // Step 3: Test round-trip conversion (simulates save workflow)
104
+ if (this.actionConfig.toFormData && this.actionConfig.fromFormData) {
105
+ const formData = this.actionConfig.toFormData(action);
106
+ const convertedAction = this.actionConfig.fromFormData(formData) as T;
107
+
108
+ // Validate the round trip worked
109
+ expect(convertedAction.uuid).to.equal(action.uuid);
110
+ expect(convertedAction.type).to.equal(action.type);
111
+
112
+ // Validate the converted action
113
+ if (this.actionConfig.validate) {
114
+ const validation = this.actionConfig.validate(convertedAction);
115
+ expect(validation.valid).to.be.true;
116
+ }
117
+ }
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Run basic property tests
123
+ */
124
+ testBasicProperties() {
125
+ it('has correct basic properties', () => {
126
+ expect(this.actionConfig.name).to.be.a('string');
127
+ expect(this.actionConfig.color).to.exist;
128
+
129
+ // toFormData and fromFormData are optional - only needed for complex data transformations
130
+ if (this.actionConfig.toFormData) {
131
+ expect(this.actionConfig.toFormData).to.be.a('function');
132
+ }
133
+ if (this.actionConfig.fromFormData) {
134
+ expect(this.actionConfig.fromFormData).to.be.a('function');
135
+ }
136
+
137
+ if (this.actionConfig.validate) {
138
+ expect(this.actionConfig.validate).to.be.a('function');
139
+ }
140
+ });
141
+ }
142
+ }
@@ -0,0 +1,89 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { add_contact_groups } from '../../src/flow/actions/add_contact_groups';
3
+ import { AddToGroup } from '../../src/store/flow-definition';
4
+ import { ActionTest } from '../ActionHelper';
5
+
6
+ /**
7
+ * Test suite for the add_contact_groups action configuration.
8
+ */
9
+ describe('add_contact_groups action config', () => {
10
+ const helper = new ActionTest(add_contact_groups, 'add_contact_groups');
11
+
12
+ describe('basic properties', () => {
13
+ helper.testBasicProperties();
14
+
15
+ it('has correct name', () => {
16
+ expect(add_contact_groups.name).to.equal('Add to Group');
17
+ });
18
+ });
19
+
20
+ describe('action scenarios', () => {
21
+ helper.testAction(
22
+ {
23
+ uuid: 'test-action-1',
24
+ type: 'add_contact_groups',
25
+ groups: [{ uuid: 'group-1', name: 'VIP Customers' }]
26
+ } as AddToGroup,
27
+ 'single-group'
28
+ );
29
+
30
+ helper.testAction(
31
+ {
32
+ uuid: 'test-action-2',
33
+ type: 'add_contact_groups',
34
+ groups: [
35
+ { uuid: 'group-1', name: 'VIP Customers' },
36
+ { uuid: 'group-2', name: 'Newsletter Subscribers' },
37
+ { uuid: 'group-3', name: 'Beta Testers' }
38
+ ]
39
+ } as AddToGroup,
40
+ 'multiple-groups'
41
+ );
42
+
43
+ helper.testAction(
44
+ {
45
+ uuid: 'test-action-3',
46
+ type: 'add_contact_groups',
47
+ groups: [
48
+ {
49
+ uuid: 'group-1',
50
+ name: 'Very Long Group Name That Might Wrap to Multiple Lines'
51
+ },
52
+ {
53
+ uuid: 'group-2',
54
+ name: 'Another Extremely Long Group Name for Testing Layout'
55
+ }
56
+ ]
57
+ } as AddToGroup,
58
+ 'long-group-names'
59
+ );
60
+
61
+ helper.testAction(
62
+ {
63
+ uuid: 'test-action-4',
64
+ type: 'add_contact_groups',
65
+ groups: [
66
+ { uuid: 'group-1', name: 'Sales Team' },
67
+ { uuid: 'group-2', name: 'Marketing Team' },
68
+ { uuid: 'group-3', name: 'Support Team' },
69
+ { uuid: 'group-4', name: 'Development Team' },
70
+ { uuid: 'group-5', name: 'QA Team' }
71
+ ]
72
+ } as AddToGroup,
73
+ 'many-groups'
74
+ );
75
+
76
+ helper.testAction(
77
+ {
78
+ uuid: 'test-action-5',
79
+ type: 'add_contact_groups',
80
+ groups: [
81
+ { uuid: 'group-1', name: 'Customers - Premium Plan (Monthly)' },
82
+ { uuid: 'group-2', name: 'Customers - Enterprise Plan (Annual)' },
83
+ { uuid: 'group-3', name: 'Trial Users - 30 Day Free Trial' }
84
+ ]
85
+ } as AddToGroup,
86
+ 'descriptive-group-names'
87
+ );
88
+ });
89
+ });
@@ -0,0 +1,265 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { remove_contact_groups } from '../../src/flow/actions/remove_contact_groups';
3
+ import { RemoveFromGroup } from '../../src/store/flow-definition';
4
+ import { ActionTest } from '../ActionHelper';
5
+
6
+ /**
7
+ * Test suite for the remove_contact_groups action configuration.
8
+ */
9
+ describe('remove_contact_groups action config', () => {
10
+ const helper = new ActionTest(remove_contact_groups, 'remove_contact_groups');
11
+
12
+ describe('basic properties', () => {
13
+ helper.testBasicProperties();
14
+
15
+ it('has correct name', () => {
16
+ expect(remove_contact_groups.name).to.equal('Remove from Group');
17
+ });
18
+ });
19
+
20
+ describe('action scenarios', () => {
21
+ helper.testAction(
22
+ {
23
+ uuid: 'test-action-1',
24
+ type: 'remove_contact_groups',
25
+ groups: [
26
+ {
27
+ uuid: 'group-1',
28
+ name: 'VIP Customers',
29
+ status: 'ready',
30
+ system: false,
31
+ query: '',
32
+ count: 150
33
+ }
34
+ ],
35
+ all_groups: false
36
+ } as RemoveFromGroup,
37
+ 'single-group'
38
+ );
39
+
40
+ helper.testAction(
41
+ {
42
+ uuid: 'test-action-2',
43
+ type: 'remove_contact_groups',
44
+ groups: [
45
+ {
46
+ uuid: 'group-1',
47
+ name: 'Newsletter Subscribers',
48
+ status: 'ready',
49
+ system: false,
50
+ query: '',
51
+ count: 1250
52
+ },
53
+ {
54
+ uuid: 'group-2',
55
+ name: 'Beta Testers',
56
+ status: 'ready',
57
+ system: false,
58
+ query: '',
59
+ count: 45
60
+ },
61
+ {
62
+ uuid: 'group-3',
63
+ name: 'Inactive Users',
64
+ status: 'ready',
65
+ system: false,
66
+ query: '',
67
+ count: 300
68
+ }
69
+ ],
70
+ all_groups: false
71
+ } as RemoveFromGroup,
72
+ 'multiple-groups'
73
+ );
74
+
75
+ helper.testAction(
76
+ {
77
+ uuid: 'test-action-3',
78
+ type: 'remove_contact_groups',
79
+ groups: [],
80
+ all_groups: true
81
+ } as RemoveFromGroup,
82
+ 'remove-from-all-groups'
83
+ );
84
+
85
+ helper.testAction(
86
+ {
87
+ uuid: 'test-action-4',
88
+ type: 'remove_contact_groups',
89
+ groups: [
90
+ {
91
+ uuid: 'group-1',
92
+ name: 'Customers - Premium Subscription (Annual Billing)',
93
+ status: 'ready',
94
+ system: false,
95
+ query: '',
96
+ count: 89
97
+ },
98
+ {
99
+ uuid: 'group-2',
100
+ name: 'Users - Trial Period Expired (30+ Days)',
101
+ status: 'ready',
102
+ system: false,
103
+ query: '',
104
+ count: 234
105
+ }
106
+ ],
107
+ all_groups: false
108
+ } as RemoveFromGroup,
109
+ 'long-descriptive-group-names'
110
+ );
111
+
112
+ helper.testAction(
113
+ {
114
+ uuid: 'test-action-5',
115
+ type: 'remove_contact_groups',
116
+ groups: [
117
+ {
118
+ uuid: 'group-1',
119
+ name: 'Sales Team',
120
+ status: 'ready',
121
+ system: false,
122
+ query: '',
123
+ count: 12
124
+ },
125
+ {
126
+ uuid: 'group-2',
127
+ name: 'Marketing Team',
128
+ status: 'ready',
129
+ system: false,
130
+ query: '',
131
+ count: 8
132
+ },
133
+ {
134
+ uuid: 'group-3',
135
+ name: 'Support Team',
136
+ status: 'ready',
137
+ system: false,
138
+ query: '',
139
+ count: 15
140
+ },
141
+ {
142
+ uuid: 'group-4',
143
+ name: 'Development Team',
144
+ status: 'ready',
145
+ system: false,
146
+ query: '',
147
+ count: 25
148
+ },
149
+ {
150
+ uuid: 'group-5',
151
+ name: 'QA Team',
152
+ status: 'ready',
153
+ system: false,
154
+ query: '',
155
+ count: 6
156
+ },
157
+ {
158
+ uuid: 'group-6',
159
+ name: 'Management Team',
160
+ status: 'ready',
161
+ system: false,
162
+ query: '',
163
+ count: 4
164
+ }
165
+ ],
166
+ all_groups: false
167
+ } as RemoveFromGroup,
168
+ 'many-groups'
169
+ );
170
+
171
+ helper.testAction(
172
+ {
173
+ uuid: 'test-action-6',
174
+ type: 'remove_contact_groups',
175
+ groups: [
176
+ {
177
+ uuid: 'group-1',
178
+ name: 'Unsubscribed Users',
179
+ status: 'ready',
180
+ system: false,
181
+ query: '',
182
+ count: 567
183
+ },
184
+ {
185
+ uuid: 'group-2',
186
+ name: 'Bounced Email Addresses',
187
+ status: 'ready',
188
+ system: false,
189
+ query: '',
190
+ count: 89
191
+ }
192
+ ],
193
+ all_groups: false
194
+ } as RemoveFromGroup,
195
+ 'cleanup-groups'
196
+ );
197
+ });
198
+
199
+ describe('validation edge cases', () => {
200
+ it('fails validation when no groups selected and all_groups is false', () => {
201
+ const action: RemoveFromGroup = {
202
+ uuid: 'test-action',
203
+ type: 'remove_contact_groups',
204
+ groups: [],
205
+ all_groups: false
206
+ };
207
+
208
+ const result = remove_contact_groups.validate(action);
209
+ expect(result.valid).to.be.false;
210
+ expect(result.errors.groups).to.equal(
211
+ 'At least one group must be selected or check "Remove from All Groups"'
212
+ );
213
+ });
214
+
215
+ it('fails validation when groups is undefined and all_groups is false', () => {
216
+ const action: RemoveFromGroup = {
217
+ uuid: 'test-action',
218
+ type: 'remove_contact_groups',
219
+ groups: undefined as any,
220
+ all_groups: false
221
+ };
222
+
223
+ const result = remove_contact_groups.validate(action);
224
+ expect(result.valid).to.be.false;
225
+ expect(result.errors.groups).to.equal(
226
+ 'At least one group must be selected or check "Remove from All Groups"'
227
+ );
228
+ });
229
+
230
+ it('passes validation when all_groups is true even with empty groups', () => {
231
+ const action: RemoveFromGroup = {
232
+ uuid: 'test-action',
233
+ type: 'remove_contact_groups',
234
+ groups: [],
235
+ all_groups: true
236
+ };
237
+
238
+ const result = remove_contact_groups.validate(action);
239
+ expect(result.valid).to.be.true;
240
+ expect(Object.keys(result.errors)).to.have.length(0);
241
+ });
242
+
243
+ it('passes validation when groups are provided and all_groups is false', () => {
244
+ const action: RemoveFromGroup = {
245
+ uuid: 'test-action',
246
+ type: 'remove_contact_groups',
247
+ groups: [
248
+ {
249
+ uuid: 'group-1',
250
+ name: 'Test Group',
251
+ status: 'ready',
252
+ system: false,
253
+ query: '',
254
+ count: 10
255
+ }
256
+ ],
257
+ all_groups: false
258
+ };
259
+
260
+ const result = remove_contact_groups.validate(action);
261
+ expect(result.valid).to.be.true;
262
+ expect(Object.keys(result.errors)).to.have.length(0);
263
+ });
264
+ });
265
+ });
@@ -0,0 +1,214 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { send_email } from '../../src/flow/actions/send_email';
3
+ import { SendEmail } from '../../src/store/flow-definition';
4
+ import { ActionTest } from '../ActionHelper';
5
+
6
+ /**
7
+ * Test suite for the send_email action configuration.
8
+ */
9
+ describe('send_email action config', () => {
10
+ const helper = new ActionTest(send_email, 'send_email');
11
+
12
+ describe('basic properties', () => {
13
+ helper.testBasicProperties();
14
+
15
+ it('has correct name', () => {
16
+ expect(send_email.name).to.equal('Send Email');
17
+ });
18
+ });
19
+
20
+ describe('action scenarios', () => {
21
+ helper.testAction(
22
+ {
23
+ uuid: 'test-action-1',
24
+ type: 'send_email',
25
+ addresses: ['user@example.com'],
26
+ subject: 'Welcome!',
27
+ body: 'Thank you for signing up.'
28
+ } as SendEmail,
29
+ 'simple-email'
30
+ );
31
+
32
+ helper.testAction(
33
+ {
34
+ uuid: 'test-action-2',
35
+ type: 'send_email',
36
+ addresses: [
37
+ 'user1@example.com',
38
+ 'user2@example.com',
39
+ 'user3@example.com'
40
+ ],
41
+ subject: 'Multiple Recipients',
42
+ body: 'This email is sent to multiple recipients.'
43
+ } as SendEmail,
44
+ 'multiple-recipients'
45
+ );
46
+
47
+ helper.testAction(
48
+ {
49
+ uuid: 'test-action-3',
50
+ type: 'send_email',
51
+ addresses: ['admin@company.com'],
52
+ subject: 'Multi-line Email Content',
53
+ body: 'Dear User,\n\nThis is a multi-line email body.\n\nIt contains multiple paragraphs and line breaks.\n\nBest regards,\nThe Team'
54
+ } as SendEmail,
55
+ 'multiline-body'
56
+ );
57
+
58
+ helper.testAction(
59
+ {
60
+ uuid: 'test-action-4',
61
+ type: 'send_email',
62
+ addresses: ['support@company.com'],
63
+ subject: 'Email with expressions',
64
+ body: 'Hello @contact.name,\n\nYour order @run.results.order_id has been processed.\n\nThank you!'
65
+ } as SendEmail,
66
+ 'with-expressions'
67
+ );
68
+
69
+ helper.testAction(
70
+ {
71
+ uuid: 'test-action-5',
72
+ type: 'send_email',
73
+ addresses: ['notification@company.com'],
74
+ subject:
75
+ 'Very Long Subject Line That Could Potentially Exceed Normal Length Limits To Test Subject Field Handling',
76
+ body: 'Short body content.'
77
+ } as SendEmail,
78
+ 'long-subject'
79
+ );
80
+
81
+ helper.testAction(
82
+ {
83
+ uuid: 'test-action-6',
84
+ type: 'send_email',
85
+ addresses: [
86
+ 'customer.service@department.company.com',
87
+ 'technical.support@department.company.com',
88
+ 'billing.inquiries@department.company.com'
89
+ ],
90
+ subject: 'Customer Support Escalation',
91
+ body: 'Customer Name: @contact.name\nTicket ID: @run.results.ticket_id\n\nIssue Description:\n@run.results.issue_description\n\nPriority: High\nCategory: @run.results.category\n\nPlease respond within 2 hours.'
92
+ } as SendEmail,
93
+ 'complex-business-email'
94
+ );
95
+
96
+ helper.testAction(
97
+ {
98
+ uuid: 'test-action-7',
99
+ type: 'send_email',
100
+ addresses: ['alerts@company.com'],
101
+ subject: '',
102
+ body: 'Email with empty subject'
103
+ } as SendEmail,
104
+ 'empty-subject'
105
+ );
106
+
107
+ helper.testAction(
108
+ {
109
+ uuid: 'test-action-8',
110
+ type: 'send_email',
111
+ addresses: ['test@company.com'],
112
+ subject: 'Empty body test',
113
+ body: ''
114
+ } as SendEmail,
115
+ 'empty-body'
116
+ );
117
+ });
118
+
119
+ describe('validation edge cases', () => {
120
+ it('fails validation for empty addresses array', () => {
121
+ const action: SendEmail = {
122
+ uuid: 'test-action',
123
+ type: 'send_email',
124
+ addresses: [],
125
+ subject: 'Test Subject',
126
+ body: 'Test Body'
127
+ };
128
+
129
+ const result = send_email.validate(action);
130
+ expect(result.valid).to.be.false;
131
+ expect(result.errors.addresses).to.equal(
132
+ 'At least one recipient email address is required'
133
+ );
134
+ });
135
+
136
+ it('fails validation for null addresses', () => {
137
+ const action: SendEmail = {
138
+ uuid: 'test-action',
139
+ type: 'send_email',
140
+ addresses: null as any,
141
+ subject: 'Test Subject',
142
+ body: 'Test Body'
143
+ };
144
+
145
+ const result = send_email.validate(action);
146
+ expect(result.valid).to.be.false;
147
+ expect(result.errors.addresses).to.equal(
148
+ 'At least one recipient email address is required'
149
+ );
150
+ });
151
+
152
+ it('fails validation for undefined addresses', () => {
153
+ const action: SendEmail = {
154
+ uuid: 'test-action',
155
+ type: 'send_email',
156
+ addresses: undefined as any,
157
+ subject: 'Test Subject',
158
+ body: 'Test Body'
159
+ };
160
+
161
+ const result = send_email.validate(action);
162
+ expect(result.valid).to.be.false;
163
+ expect(result.errors.addresses).to.equal(
164
+ 'At least one recipient email address is required'
165
+ );
166
+ });
167
+
168
+ it('passes validation with valid email addresses', () => {
169
+ const action: SendEmail = {
170
+ uuid: 'test-action',
171
+ type: 'send_email',
172
+ addresses: ['valid@example.com'],
173
+ subject: 'Test Subject',
174
+ body: 'Test Body'
175
+ };
176
+
177
+ const result = send_email.validate(action);
178
+ expect(result.valid).to.be.true;
179
+ expect(Object.keys(result.errors)).to.have.length(0);
180
+ });
181
+
182
+ it('passes validation with multiple email addresses', () => {
183
+ const action: SendEmail = {
184
+ uuid: 'test-action',
185
+ type: 'send_email',
186
+ addresses: [
187
+ 'user1@example.com',
188
+ 'user2@example.com',
189
+ 'user3@example.com'
190
+ ],
191
+ subject: 'Test Subject',
192
+ body: 'Test Body'
193
+ };
194
+
195
+ const result = send_email.validate(action);
196
+ expect(result.valid).to.be.true;
197
+ expect(Object.keys(result.errors)).to.have.length(0);
198
+ });
199
+
200
+ it('passes validation with empty subject and body', () => {
201
+ const action: SendEmail = {
202
+ uuid: 'test-action',
203
+ type: 'send_email',
204
+ addresses: ['test@example.com'],
205
+ subject: '',
206
+ body: ''
207
+ };
208
+
209
+ const result = send_email.validate(action);
210
+ expect(result.valid).to.be.true;
211
+ expect(Object.keys(result.errors)).to.have.length(0);
212
+ });
213
+ });
214
+ });