@gregoriusrippenstein/erlang-red-unittest 0.12.4 → 0.15.1

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 (211) hide show
  1. package/README.md +90 -1
  2. package/nodes/locales/en-US/ut-assert-success.html +8 -2
  3. package/nodes/locales/en-US/ut-assert-success.json +7 -1
  4. package/nodes/ut-assert-debug.js +385 -2
  5. package/nodes/ut-assert-failure.js +29 -18
  6. package/nodes/ut-assert-status.js +11 -2
  7. package/nodes/ut-assert-success.html +33 -12
  8. package/nodes/ut-assert-success.js +59 -29
  9. package/nodes/ut-assert-values.js +123 -16
  10. package/package.json +1 -1
  11. package/plugins/sidebar.html +131 -28
  12. package/testflows/01b074cfa6ae8818/flows.json +1 -0
  13. package/testflows/02b18fc0ed9de178/flows.json +1 -0
  14. package/testflows/0616f795a4aee63e/flows.json +1 -0
  15. package/testflows/069552b2eb6bce48/flows.json +265 -0
  16. package/testflows/08183069b5f8ef9c/flows.json +156 -0
  17. package/testflows/084fb0fc954b7156/flows.json +132 -0
  18. package/testflows/090eb2d5d71fd45f/flows.json +158 -0
  19. package/testflows/09aab59a2455cf15/flows.json +1 -0
  20. package/testflows/0b299a3175fc0df1/flows.json +1 -0
  21. package/testflows/0c35d269ca7178c6/flows.json +443 -0
  22. package/testflows/0cb8971e5f88ed1e/flows.json +1 -0
  23. package/testflows/0cc95e44532c14ec/flows.json +94 -0
  24. package/testflows/0e54563ba63b1501/flows.json +1 -0
  25. package/testflows/12572f9ac11e1786/flows.json +1 -0
  26. package/testflows/1566e453a88578a9/flows.json +1 -0
  27. package/testflows/15c2fad089960984/flows.json +1 -0
  28. package/testflows/1658196c31549916/flows.json +1 -0
  29. package/testflows/16b0bc0cdb0c5807/flows.json +1 -0
  30. package/testflows/16b254125478329a/flows.json +114 -0
  31. package/testflows/182731f54d855071/flows.json +52 -0
  32. package/testflows/1a196bce3e57f5c1/flows.json +1 -0
  33. package/testflows/1ace9e051618db79/flows.json +125 -0
  34. package/testflows/1bac0a78e0a36ec0/flows.json +1 -0
  35. package/testflows/1c1d2cb981cf9f01/flows.json +120 -0
  36. package/testflows/1d3326a724ecbea0/flows.json +1 -0
  37. package/testflows/1db9c9a1b92e0263/flows.json +172 -0
  38. package/testflows/1eb35963b752eca9/flows.json +1 -0
  39. package/testflows/2037454a5ee0d6c3/flows.json +1 -0
  40. package/testflows/20eeaf41372f7b17/flows.json +62 -0
  41. package/testflows/213803d61ecfde3a/flows.json +1 -0
  42. package/testflows/218350f2e240a15d/flows.json +1 -0
  43. package/testflows/24f35eaa8656755f/flows.json +1 -0
  44. package/testflows/27804627fb8f56bd/flows.json +178 -0
  45. package/testflows/2a12af161b544aa1/flows.json +1 -0
  46. package/testflows/2a2c508260b96236/flows.json +141 -0
  47. package/testflows/2a95a7b40a798878/flows.json +1 -0
  48. package/testflows/2c1c291ec8466bfb/flows.json +1 -0
  49. package/testflows/2c59ef974b2523ba/flows.json +1 -0
  50. package/testflows/30465efe017cc9b0/flows.json +1 -0
  51. package/testflows/30c79fc42a5c35eb/flows.json +1 -0
  52. package/testflows/338c5b411f4edfe8/flows.json +1 -0
  53. package/testflows/34d211e37618313c/flows.json +1 -0
  54. package/testflows/36846ad6698e1bc0/flows.json +163 -0
  55. package/testflows/37883612d913fd66/flows.json +1 -0
  56. package/testflows/38616d22e1569aa5/flows.json +1 -0
  57. package/testflows/3be6bc074c7647bd/flows.json +319 -0
  58. package/testflows/3e9e526b992d4c48/flows.json +1 -0
  59. package/testflows/3ea113b4ec3b6e5e/flows.json +1 -0
  60. package/testflows/3ed472eab9503b4f/flows.json +1 -0
  61. package/testflows/3edda6bd788f88c2/flows.json +1 -0
  62. package/testflows/435d3b48dd0f462c/flows.json +1 -0
  63. package/testflows/44f12f6e4a455084/flows.json +272 -0
  64. package/testflows/45209c67f865ce73/flows.json +1 -0
  65. package/testflows/459322e0f8e0b785/flows.json +1 -0
  66. package/testflows/4657dbc0aa4a97bb/flows.json +268 -0
  67. package/testflows/47514a335b4cd67e/flows.json +362 -0
  68. package/testflows/486c1412721bb241/flows.json +525 -0
  69. package/testflows/48d9fdcde317974e/flows.json +1 -0
  70. package/testflows/4de34883c5d93dc3/flows.json +1 -0
  71. package/testflows/50d5679818477b66/flows.json +1 -0
  72. package/testflows/51870a76d1aaf67a/flows.json +1 -0
  73. package/testflows/51bfa9456b1745c0/flows.json +639 -0
  74. package/testflows/538be5947c639b32/flows.json +75 -0
  75. package/testflows/538d2f4704cd2eca/flows.json +1 -0
  76. package/testflows/5433b57035d597f9/flows.json +1 -0
  77. package/testflows/545871d31c3f5f3f/flows.json +376 -0
  78. package/testflows/55f105cfd894b06c/flows.json +121 -0
  79. package/testflows/55f7fbe90e5befa8/flows.json +1 -0
  80. package/testflows/562c518969666f24/flows.json +1 -0
  81. package/testflows/57d44c342a1f00e2/flows.json +1 -0
  82. package/testflows/597ee2a683d9908f/flows.json +129 -0
  83. package/testflows/59aa8a866d8d7a70/flows.json +1 -0
  84. package/testflows/59da63b5adb460db/flows.json +188 -0
  85. package/testflows/5a06352874fa379d/flows.json +1 -0
  86. package/testflows/5cd133958df17529/flows.json +1 -0
  87. package/testflows/5cf6aec7d688fce4/flows.json +1 -0
  88. package/testflows/5d4908af3d4f95e5/flows.json +1 -0
  89. package/testflows/5e07acf2da13b504/flows.json +1 -0
  90. package/testflows/5f6929bb3374b782/flows.json +1 -0
  91. package/testflows/61142eb3e4e54eab/flows.json +536 -0
  92. package/testflows/61847d719e50f83f/flows.json +1 -0
  93. package/testflows/63ca9baf860d1d8f/flows.json +1 -0
  94. package/testflows/641ddaab2819c61d/flows.json +1 -0
  95. package/testflows/64445798b59d2630/flows.json +257 -0
  96. package/testflows/684b9066e98b9722/flows.json +1 -0
  97. package/testflows/698b83b8aeb5d5b6/flows.json +1 -0
  98. package/testflows/6a9ffd45c418497b/flows.json +1 -0
  99. package/testflows/6d46e3fe1f2245b8/flows.json +86 -0
  100. package/testflows/6dd3a5754f83dc2e/flows.json +232 -0
  101. package/testflows/6ea4c6b373eeaa8d/flows.json +1 -0
  102. package/testflows/6f0dcbf18234c5e2/flows.json +1 -0
  103. package/testflows/6ff45e2a0ce77393/flows.json +149 -0
  104. package/testflows/700c94899fdd3334/flows.json +107 -0
  105. package/testflows/71f65246c742cfc9/flows.json +1 -0
  106. package/testflows/7581c2be261f9c17/flows.json +1874 -0
  107. package/testflows/77854a5ea962257b/flows.json +1 -0
  108. package/testflows/7b77600dcf78933f/flows.json +1 -0
  109. package/testflows/7dae2b1a7881c607/flows.json +1 -0
  110. package/testflows/7e1d04570c6bdff9/flows.json +1 -0
  111. package/testflows/7ecfd18f58fc6510/flows.json +1 -0
  112. package/testflows/7fea9696f6186962/flows.json +141 -0
  113. package/testflows/80655e5d4035fa71/flows.json +215 -0
  114. package/testflows/8180ae5891c673de/flows.json +1 -0
  115. package/testflows/8421f37b6c40e1ec/flows.json +1 -0
  116. package/testflows/84a5a362cafe703f/flows.json +1 -0
  117. package/testflows/866410b56fa42447/flows.json +1076 -0
  118. package/testflows/894f6e38bfd1aca6/flows.json +1 -0
  119. package/testflows/89aeaeef64509bed/flows.json +101 -0
  120. package/testflows/8a627c9bfe3b4aff/flows.json +552 -0
  121. package/testflows/8bc7412d6a4869c6/flows.json +1 -0
  122. package/testflows/92054e0dd22f14c6/flows.json +1 -0
  123. package/testflows/93c6e969dcf84e70/flows.json +1 -0
  124. package/testflows/9463eba0ab843567/flows.json +1 -0
  125. package/testflows/96f6f83dcab507e5/flows.json +105 -0
  126. package/testflows/9746601768c3b3b6/flows.json +93 -0
  127. package/testflows/987cd33b9cda8529/flows.json +1 -0
  128. package/testflows/988f27b9abdde6e9/flows.json +1 -0
  129. package/testflows/9942fa1aeb1b2428/flows.json +2346 -0
  130. package/testflows/9a1bdfdcd7b2be8e/flows.json +1 -0
  131. package/testflows/9b1023c95a2613cc/flows.json +141 -0
  132. package/testflows/9c51a179141515b2/flows.json +1 -0
  133. package/testflows/9d11bfc3a6d88535/flows.json +301 -0
  134. package/testflows/9e7ccc97f80d9afb/flows.json +93 -0
  135. package/testflows/9f0d6c3ad7798a77/flows.json +142 -0
  136. package/testflows/9f7351d7a7304584/flows.json +83 -0
  137. package/testflows/9ff2e8ab21d922c5/flows.json +1 -0
  138. package/testflows/a123cd431f1967cd/flows.json +1757 -0
  139. package/testflows/a4c9cabf4b335488/flows.json +86 -0
  140. package/testflows/a6dc7002d0a8640e/flows.json +1 -0
  141. package/testflows/a862ae412a70ded1/flows.json +1 -0
  142. package/testflows/a916165378c446e3/flows.json +240 -0
  143. package/testflows/ad2bf1089481dcfb/flows.json +1 -0
  144. package/testflows/ae38b2dbd23d1681/flows.json +3089 -0
  145. package/testflows/ae8a9b0bbbaff447/flows.json +271 -0
  146. package/testflows/b07b511dc8bb8339/flows.json +92 -0
  147. package/testflows/b1430ea37ba7cc19/flows.json +1 -0
  148. package/testflows/b1463d7bbf545725/flows.json +1 -0
  149. package/testflows/b1501de32c769cf2/flows.json +94 -0
  150. package/testflows/b2a67e301fabab0e/flows.json +1 -0
  151. package/testflows/b2fcc5806b2715c0/flows.json +1 -0
  152. package/testflows/b3f365d0b7d1f97d/flows.json +1 -0
  153. package/testflows/b50f775ae3b74d38/flows.json +1 -0
  154. package/testflows/b5fbcdbffb568569/flows.json +1 -0
  155. package/testflows/b6d4e4592b27a344/flows.json +1 -0
  156. package/testflows/b723353a316fa50e/flows.json +1 -0
  157. package/testflows/b7ebaf91f4d66ab3/flows.json +1 -0
  158. package/testflows/b85795a24882502b/flows.json +1 -0
  159. package/testflows/b864b3e3510cd120/flows.json +205 -0
  160. package/testflows/b98d0b05a760ad79/flows.json +1 -0
  161. package/testflows/b9cd19107a5145c4/flows.json +1 -0
  162. package/testflows/bbb1fc2d47c3cd5f/flows.json +1 -0
  163. package/testflows/bdbe94d065f3b3a6/flows.json +143 -0
  164. package/testflows/bfced61e72c69715/flows.json +616 -0
  165. package/testflows/c0b1cf6656eac77f/flows.json +518 -0
  166. package/testflows/c1370220fd37968e/flows.json +1 -0
  167. package/testflows/c163c627bd2a37c7/flows.json +1 -0
  168. package/testflows/c274a07715a87ca4/flows.json +1 -0
  169. package/testflows/c28048d859db3773/flows.json +1 -0
  170. package/testflows/c381b5135cc947cd/flows.json +272 -0
  171. package/testflows/c4690c0a085d6ef5/flows.json +145 -0
  172. package/testflows/c5f513bc380bc30a/flows.json +696 -0
  173. package/testflows/c6ee6e89a51c98fc/flows.json +1 -0
  174. package/testflows/c843dacf753a19d8/flows.json +1 -0
  175. package/testflows/caccb020fce3f485/flows.json +1 -0
  176. package/testflows/cd44a688eb2ab3cf/flows.json +1 -0
  177. package/testflows/ce2f98273da05245/flows.json +1 -0
  178. package/testflows/ced70c769db825eb/flows.json +1 -0
  179. package/testflows/cef9dd7dc179b8b3/flows.json +1 -0
  180. package/testflows/cfa8595b2e2b8269/flows.json +255 -0
  181. package/testflows/cffa610c817beb43/flows.json +1 -0
  182. package/testflows/d17062b01c4a9435/flows.json +1 -0
  183. package/testflows/d17fb2553fe8013a/flows.json +157 -0
  184. package/testflows/d206746f9f2594a6/flows.json +1 -0
  185. package/testflows/d4ce58359ff6ff96/flows.json +1 -0
  186. package/testflows/d5424657b497d5af/flows.json +1 -0
  187. package/testflows/d595c53abba88f72/flows.json +1 -0
  188. package/testflows/d61182e87a604efa/flows.json +340 -0
  189. package/testflows/def45a46c8f4da72/flows.json +1 -0
  190. package/testflows/def56742e02f6a75/flows.json +1 -0
  191. package/testflows/e1ced3b16782f7c8/flows.json +1 -0
  192. package/testflows/e2a801d2b3e2143f/flows.json +1 -0
  193. package/testflows/e7184b7ca4f07907/flows.json +1 -0
  194. package/testflows/ea246f68766c8630/flows.json +1 -0
  195. package/testflows/eb447048178f6e16/flows.json +201 -0
  196. package/testflows/ec921769b3c24fb6/flows.json +1 -0
  197. package/testflows/ed621e03921c13de/flows.json +1 -0
  198. package/testflows/eed68dbcecd1431f/flows.json +204 -0
  199. package/testflows/ef61f644d5436dbe/flows.json +1 -0
  200. package/testflows/f19fdae0c02b4f03/flows.json +1008 -0
  201. package/testflows/f1e71f2ccb34fd6e/flows.json +1 -0
  202. package/testflows/f2f61ec9fc46a468/flows.json +575 -0
  203. package/testflows/f346d45c81f595e5/flows.json +501 -0
  204. package/testflows/f38603a59963386c/flows.json +1 -0
  205. package/testflows/f4c4a57e77439a1b/flows.json +1 -0
  206. package/testflows/f5bdafa844f6d280/flows.json +1 -0
  207. package/testflows/f5f82ca50317fda7/flows.json +121 -0
  208. package/testflows/f8727464c799eb22/flows.json +1 -0
  209. package/testflows/f946aafaebb1398d/flows.json +160 -0
  210. package/testflows/f94db507552f4934/flows.json +183 -0
  211. package/testflows/fb50bac16667fc54/flows.json +364 -0
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ## Unit testing nodes for Erlang-RED
1
+ ## Unit testing nodes for Erlang-Red and Node-RED
2
2
 
3
3
  Node-RED Nodes for testing flow correctness in the [Erlang-Red](https://github.com/gorenje/erlang-red) project.
4
4
 
@@ -38,6 +38,95 @@ This package also defines the following actions that can be mapped to keyboard s
38
38
  - Send Halt To Test Server: (Erlang-Red only) restarts the server
39
39
  - Run All Tests: (Erlang-Red only) run all defined unit tests.
40
40
 
41
+ ## Specification of core functionality for Node-RED.
42
+
43
+ A collection of nodes also contains a set of flows that represent unit tests that test core functionality of core nodes.
44
+
45
+ The intention is to have a set of visual flows that test specific functionality of the Node-RED core nodes. In this package form, tests can be installed in future versions of Node-RED to ensure that functionality is maintained.
46
+
47
+ **Why do this?**
48
+
49
+ Because it provides a method for anyone to create flows to test functionality. As opposed to unit tests and integration tests defined in the project as NodeJS code, this approach, a visual approach allows anyone who has an interest to create mini unit-tests to ensure that the functionality that they expect is maintained in Node-RED.
50
+
51
+ What is provided is a collection of 200 tests that can be executed via a sidebar plugin. Tests are executed in the background on the server. Existing flows may be affected, so it is best to do this **without** any existing flows defined.
52
+
53
+ It is intended to be done once, maybe for an initial release of Node-RED. There is no real reason to do this continually. It does provide a way of doing unit tests as well. So the idea is also to provide a mechanism—a visual mechanism—for defining unit tests for other projects as well. Future releases of this package will allow independent collections of unit tests for your own projects.
54
+
55
+ The aim would be to have a project directory, and within that project directory, there are a set of flows that test specifics of that project. Those tests would then be shown in the plugin on the sidebar, and those tests would then be executed for changes to the project, to the flows, the existing flows. So that's the goal in the long term: to provide a visual unit testing framework for projects.
56
+
57
+ So far this is only a demonstration of how to do this for the Node-RED project itself.
58
+
59
+ ## How do the provided unit tests work?
60
+
61
+ Each unit test defines one or more inject nodes that are triggered when the test starts. Once the test is started, the assert nodes are triggered and test specific conditions. If any assert node fails, the entire test fails.
62
+
63
+ Assert nodes can either test that a message arrives or that a message doesn't arrive. The assert false asserts that if a message arrives, it will fail. The assert success node ensures that one or more messages arrive, a limit of messages arrive, or a maximum number of messages arrive. If the assert success node is configured with zero message count, then 1 or more messages are expected.
64
+
65
+ The assert values node is used to test specific values on the message object and ensure that those are correct. Assert values node fails if values aren't correct or if a message is not received. The assert values node expects at least a single message, multiple messages are tested against the same conditions.
66
+
67
+ Failures of all assert nodes are shown in the Node-RED console log.
68
+
69
+ ## Configuration via flow environment values
70
+
71
+ Tests can also be given specific properties via the flow-tab environment variables. Features such as pending tests can be indicated, so that if a test haven’t been completed yet or will fail, tests can be set to pending.
72
+
73
+ A test can be given a timeout period, to specify when tests time out. The timeout defaults to 5 seconds, so after a test starts, it has 5 seconds to complete. If it requires longer, then a timeout value of higher than 5 can be given. The timeout values are in seconds, and are provided in the environment values of the flow tab.
74
+
75
+ ## Sidebar Plugin Functionality
76
+
77
+ The sidebar functionality provides testing features for testing all tests, a subset of tests and a single test. Also tests can be loaded into the editor using a double click on the test name.
78
+
79
+ Modifications of tests are not stored nor can tests be modified inside the package. Defining your own tests will require creating your own flow files.
80
+
81
+ ## What is correct functionality?
82
+
83
+ What do I define as core node functionality? What I mean by that is that things like status and debug messages are also core functionality. So if a node generates a status message, then I want to test that it generates that status message. And if it generates a debug message, then I want to test that it generates that debug message.
84
+
85
+ That's why the assert status and assert debug nodes in this package - that is what they are testing for. I consider this part of the core functionality, of a core node because folks could rely on that.
86
+
87
+ Especially the debug count: when you see how many messages are going through, that could be a very important dependency that might be used for ensuring that flows work correctly, depending on the setup. So, that's why for me, also checking that nodes generate the expected debug message and the expected status messages or status indications is also part of core functionality of core nodes.
88
+
89
+ ## True Negatives and False Positives
90
+
91
+ Obviously, the testing is not 100 percent; nothing’s perfect in this world. I have tried my best to get it as accurate as possible, but sometimes because of timing issues with the tests, I do make assumptions about certain things happening in a certain order or taking a certain amount of time. Sometimes these conditions don't hold, and so the test do fail.
92
+
93
+ I have endeavoured to indicate that on those tests that are potentially not so consistent. However, I haven't obviously covered everything, as it is just a first attempt. There are cases where there are sometimes tests failing, and I have endeavoured to mark those, comment on those, and ensure that you know, to the best of my abilities, I've tried to make these tests as stable and correct as possible.
94
+
95
+ That includes making certain assumptions about functionality that might not actually be correct, as there is no written-down standardised functionality for Node-RED. Sometimes I might even be testing functionality that isn't actually meant to be correct.
96
+
97
+ However, that is the intention of this package: it is also to define those things and make that clearer—what is functionality and what isn't. That is, I think, part of this project: to define that a little bit more clearly.
98
+
99
+ ## Incomplete implementation
100
+
101
+ The assert values node isn't completely implemented; certain values aren't supported. The assert debug and the assert status nodes don't work either; they aren't supported yet.
102
+
103
+ What this package doesn't provide, unfortunately, are the two assert nodes related to status and debug. So these nodes are designed to capture any debug messages sent by a node or any status generated by the node.
104
+
105
+ So this is partly, obviously, for the status. One can use the status node itself, but it's difficult to test the status node if you're using the status node. So that's why there's an assert status node and the assert debug cannot be replaced by anything else because there's no way in Node-RED to capture the debug output generated by a node.
106
+
107
+ The unfortunate thing is then it's hard to test the output generated by nodes. That meaning debug messages or status messages to ensure that there's actually something generated. This is also part of the functionality of Node-RED. So I see that if a node generates a status message, then that should be tested for. If a node generates a debug message, then that should be tested for. So that's what these debug nodes, assert debug and assert status are meant to be doing.
108
+
109
+ But they are unfortunately not implementable for Node-RED because there's no way of adding hooks to these messages. Or at least none that I found.
110
+
111
+ ## Discussion: why is this important enough for me?
112
+
113
+ Why do I see this standardisation as important? Obviously, I've invested quite a bit of time into making these tests and creating this package. Why do I think that's so important? There are a couple of reasons.
114
+
115
+ One is that I see this as a way of ensuring that there's a benchmark for Node-RED functionality. So you say, okay, the complete node does just this, completes, and the change node can do this, this, and this, and it can only do that. That's then well-defined as a test or a series of tests to say, "Okay, this is what it has to be doing”. With that, it is clear: okay, this is within the bounds of the change node, and this is not within the bounds of the change nodes. Then a discussion can be started to say whether the bounds of the change node should be extended, or whether a new node should be defined for some specific functionality. So that's one thing I think is important: to define clearly what core nodes do and, equally important, what they will not do - the boundary of their functionality.
116
+
117
+ In a [long discussion](https://discourse.nodered.org/t/brittle-flows-or-is-it-the-visual-paradigm/100911/30) with Nick, it became clear that there is no such standardisation or definition for the functionality of nodes. It's kind of like whatever feels good and feels right, a "will do" type of decision-making, which is fine, and I'm not criticising that. My second point is to say, okay, well, for industry applications and for industries which are based on standards, it could be important to say, okay, well, if we're going to use Node-RED, then we want to know that version X of Node-RED will be doing the same thing as version Y down the road. And this kind of node package, this unit testing of the functionality, can be a guarantee of that consistency into the future.
118
+
119
+ Thirdly, it could also show when there's a divergence of functionality. And this makes upgrading versions of Node-RED for an industry application safer. That's another thing that I see could be useful for this package.
120
+
121
+ The final point that I see is also the origins of this entire project: compatibility testing. So, having created Erlang-RED, which is the Erlang-backed Node-RED (the Node-RED frontend editor with an Erlang backend), I was kind of interested in how do I define the functionality of the nodes. How do I know that my nodes are working correctly according to what Node-RED is doing? Because I want to do want to have 100% compatibility with Node-RED flows. Why do I want that? It's because I want to be able to take in a visual abstraction, a Node-RED flow, and have it run as Erlang code, or as NodeJS code, or Python code, or whatever code. So that the visualisation, the visual flow representation, becomes an abstraction between different programming languages.
122
+
123
+ Therefore, to be able to do that, to be able to port flows one-to-one to Erlang-RED, I need to be able to ensure that the Erlang-RED nodes are doing what the Node-RED nodes are doing. And so that's why I started creating these unit tests, which run both in Erlang-RED and in Node-RED. So this Node package I actually use for Erlang-RED as well, with a slightly modified set of tests than the ones defined here.
124
+
125
+ ## Forum Discussions
126
+
127
+ - [Standardisation of core functionality - May 2026](https://discourse.nodered.org/t/brittle-flows-or-is-it-the-visual-paradigm/100911)
128
+ - [Flow testsuite for testing core node functionality - May 2025](https://discourse.nodered.org/t/flow-testsuite-for-testing-core-node-functionality/97106)
129
+
41
130
  ## Tee Time
42
131
 
43
132
  <a href="https://www.buymeacoffee.com/gorenje" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-red.png" alt="Buy Me A Tee" style="height: 42px !important;width: 152px !important;" ></a>
@@ -1,6 +1,12 @@
1
1
  <script type="text/html" data-help-name="ut-assert-success">
2
- <p>This node failes if, during flow execution, it is not reached.</p>
3
- A success assertion meaning that this node **must** be reached when a flow is executed as part of a test.
2
+ <p>This node failes if, during flow execution, it is not reached.</p>
3
+ A success assertion meaning that this node <b>must</b> be reached when a flow is executed as part of a test.
4
4
 
5
+ <p>
5
6
  If this node isn't reached, then the test will fail.
7
+ </p>
8
+
9
+ <p>
10
+ A message count of zero is assumed to be <b>at least one message</b>, it's the same as minimum of one message.
11
+ </p>
6
12
  </script>
@@ -7,7 +7,13 @@
7
7
  "succeed": "assert succeed",
8
8
  "count": "Msg Count",
9
9
  "unsupported": "Unsupported",
10
- "failed": "assert failed"
10
+ "failed": "assert failed",
11
+ "messages": "Msgs",
12
+ "limit": {
13
+ "upperlimit" : "Max",
14
+ "exactly": "Exactly",
15
+ "lowerlimit": "Min"
16
+ }
11
17
  },
12
18
 
13
19
  "status": {
@@ -5,8 +5,17 @@ module.exports = function(RED) {
5
5
  var node = this;
6
6
  var cfg = config;
7
7
 
8
- node.on('close', function() {
9
- node.status({});
8
+ node.on('close', function (removed, done) {
9
+ if (removed) {
10
+ node.status({});
11
+ } else {
12
+ // use node.log(..) here because node.error(..) sends a message to the debug
13
+ // panel but that errors out because the frontend can't find the workspace:
14
+ // Uncaught TypeError: can't access property "label", RED.nodes.workspace(...) is undefined
15
+ // that has follow-on effects.
16
+ node.log(`UNSUPPORTED [${node.z}] debug is not supported`)
17
+ }
18
+ done()
10
19
  });
11
20
 
12
21
  /* msg handler, in this case pass the message on unchanged */
@@ -36,4 +45,378 @@ module.exports = function(RED) {
36
45
 
37
46
  RED.nodes.registerType("ut-assert-debug", Coreut_assert_debugFunctionality);
38
47
 
48
+ // Backend endpoints
49
+ RED.httpAdmin.get("/UnitTesting/:flowid/runtest",
50
+ (req, res) => {
51
+ const path = require('path');
52
+ const fs = require('fs')
53
+ const jsonClone = require("rfdc")();
54
+
55
+ // API defined here --> https://github.com/node-red/node-red/blob/master/packages/node_modules/%40node-red/runtime/lib/index.js
56
+ const runtime = require("@node-red/runtime");
57
+
58
+ let testDir = path.resolve(path.dirname(__filename), "..", "testflows")
59
+
60
+ // if a test generates an uncaught exception, then captcha that and generate
61
+ // a failure - tests are considered failed if they generate uncaught exceptions/errors.
62
+ let createExtraCatchNode = () => {
63
+ let changeId = runtime.util.generateId()
64
+ let assertId = runtime.util.generateId()
65
+ let debugId = runtime.util.generateId()
66
+
67
+ return [
68
+ {
69
+ "id": runtime.util.generateId(),
70
+ "type": "catch",
71
+ "name": "",
72
+ "scope": null,
73
+ "uncaught": true,
74
+ "wires": [
75
+ [
76
+ changeId
77
+ ]
78
+ ]
79
+ },
80
+ {
81
+ "id": changeId,
82
+ "type": "change",
83
+ "name": "",
84
+ "rules": [
85
+ {
86
+ "t": "set",
87
+ "p": "_unittest_triggered",
88
+ "pt": "msg",
89
+ "to": "true",
90
+ "tot": "bool"
91
+ }
92
+ ],
93
+ "action": "",
94
+ "property": "",
95
+ "from": "",
96
+ "to": "",
97
+ "reg": false,
98
+ "wires": [
99
+ [
100
+ assertId,
101
+ debugId
102
+ ]
103
+ ]
104
+ },
105
+ {
106
+ "id": debugId,
107
+ "type": "debug",
108
+ "name": "",
109
+ "active": true,
110
+ "tosidebar": false,
111
+ "console": true,
112
+ "tostatus": false,
113
+ "complete": "payload",
114
+ "targetType": "msg",
115
+ "statusVal": "",
116
+ "statusType": "auto",
117
+ "wires": []
118
+ },
119
+ {
120
+ "id": assertId,
121
+ "type": "ut-assert-failure",
122
+ "name": "",
123
+ "wires": []
124
+ }
125
+ ]
126
+ }
127
+
128
+ let isTestPending = (tabDetails) => {
129
+ return tabDetails.env.filter(d => d.name == "NRED_PENDING")[0]?.value == "true"
130
+ }
131
+
132
+ // chunkify an array into chunks of `size` size
133
+ let chunkify = (array, size) => {
134
+ const chunkedArray = [];
135
+ for (let i = 0; i < array.length; i += size) {
136
+ chunkedArray.push(array.slice(i, i + size));
137
+ }
138
+ return chunkedArray;
139
+ }
140
+
141
+ // respond to request with a list of total tests to be done. The frontend then
142
+ // shows a progress indication of success / pending / failure
143
+ let respondWithCount = (res, count) => {
144
+ res.writeHead(200, { 'Content-Type': 'application/json' });
145
+ res.end(JSON.stringify({ status: "ok", todo: count }));
146
+ }
147
+
148
+
149
+ if (req.params.flowid == "all") {
150
+ //
151
+ // Run all tests. This is a little more complicated as this runs all tests in batches
152
+ // of ten tests at a time but these have to be added in serial and also removed in
153
+ // serial, so there has to be promise handling ... and that makes this stuff a little
154
+ // more complicated.
155
+ //
156
+
157
+ let allTests = fs.globSync(`${testDir}/**/*.json`)
158
+
159
+ // if a limit (i.e. a list of testFlow ids) is provided, only peform those tests.
160
+ if (req.query.limit) {
161
+ let testIds = req.query.limit.split(",")
162
+
163
+ allTests = allTests.filter( filename => {
164
+ let origFlowId = path.basename(path.parse(filename).dir)
165
+ return testIds.indexOf(origFlowId) > -1
166
+ })
167
+ }
168
+
169
+ let chunkedFilenames = chunkify(allTests, 10)
170
+
171
+ respondWithCount(res, allTests.length)
172
+
173
+ let runTestsForFileNames = (fileIdx) => {
174
+ if (fileIdx >= chunkedFilenames.length) { return }
175
+
176
+ // test all is a multi step process, first we have to add all flows to the main show
177
+ // then we have to trigger them and finally we have to remove those that weren't
178
+ // there in the first place.
179
+ let flowIdsToBeRemoved = []
180
+ let injNodesToBeTriggered = {}
181
+
182
+ let timeoutValues = []
183
+
184
+ let flowsToAdd = chunkedFilenames[fileIdx].map((filename) => {
185
+
186
+ let origFlowId = path.basename(path.parse(filename).dir)
187
+
188
+ let flowDetails = JSON.parse(fs.readFileSync(filename))
189
+ let tabDetails = flowDetails.filter(d => d.type == "tab")[0]
190
+ let injNodesIds = flowDetails.filter(d => d.type == "inject").map(d => d.id)
191
+
192
+ RED.log.debug(`unittest: adding test case [${origFlowId}] - '${tabDetails.label}'`)
193
+
194
+ if (isTestPending(tabDetails) && req.query["testpend"] != "true") {
195
+ RED.comms.publish("unittesting:testresults", {
196
+ flowid: origFlowId,
197
+ status: "pending"
198
+ })
199
+ return null
200
+ }
201
+
202
+ // compute any timeout that is defined for the flow test
203
+ tabDetails.env.filter(d => d.name == "NRED_TIMEOUT").forEach( d => {
204
+ timeoutValues.push(parseInt(d.value) * 1000)
205
+ })
206
+
207
+ // add an catch all and trigger a assert false if any exceptions are
208
+ // raised by the test - if there isn't already a catch all node.
209
+ if (flowDetails.filter(d => d.type == "catch").filter(d => d.scope == null).length == 0) {
210
+ flowDetails = flowDetails.concat(createExtraCatchNode())
211
+ }
212
+
213
+ if (runtime._.flows.getFlow(origFlowId)) {
214
+ injNodesToBeTriggered[origFlowId] = injNodesIds.map(d => d)
215
+ return null
216
+ } else {
217
+ return {
218
+ injNodesIds: injNodesIds, origFlowId: origFlowId, nodes: flowDetails
219
+ }
220
+ }
221
+ }).filter(d => !!d)
222
+
223
+ // addFlow is a promise but I want to add flows in series not parallel, so the
224
+ // promise returns a promise that adds the next flow after the initial flow
225
+ // has been added. It's promises all the way down - until the turtles start.
226
+ let addFlow = (idx) => {
227
+ if (idx >= flowsToAdd.length) { return }
228
+ var { injNodesIds, origFlowId, nodes } = flowsToAdd[idx]
229
+
230
+ var newConfig = jsonClone(runtime._.flows.getFlows().flows);
231
+ newConfig = newConfig.concat(nodes);
232
+
233
+ return runtime._.flows.setFlows(newConfig, null, 'flows', false, null, "root")
234
+ .then(() => {
235
+ let newFlowId = origFlowId
236
+ injNodesToBeTriggered[newFlowId] = injNodesIds.map(d => d)
237
+ flowIdsToBeRemoved.push(newFlowId)
238
+ })
239
+ .catch(e => { RED.log.error(e); RED.log.error(`Exception happened with ${origFlowId}`) })
240
+ .finally(() => addFlow(idx + 1))
241
+ }
242
+
243
+ Promise.all([addFlow(0)]).then(() => {
244
+ RED.log.debug("unittest: done adding all flows")
245
+
246
+ runtime._.flows.startFlows("full", null, false, true).then(d => {
247
+ Object.keys(injNodesToBeTriggered).forEach(flowId => {
248
+ injNodesToBeTriggered[flowId].forEach(ndeId => {
249
+ RED.log.debug(`unittest: triggering inject node: ${ndeId}`)
250
+ RED.nodes.getNode(ndeId)?.receive({"_unittest_triggered": true})
251
+ })
252
+ })
253
+
254
+ let removeFlow = (idx) => {
255
+ if (idx >= flowIdsToBeRemoved.length) { return }
256
+ let flowid = flowIdsToBeRemoved[idx]
257
+
258
+ return runtime._.flows.removeFlow(flowid, "root")
259
+ .then((_ignore) => {
260
+ RED.comms.publish("unittesting:testresults", {
261
+ flowid: flowid,
262
+ status: "stopped"
263
+ })
264
+ })
265
+ .catch(e => {
266
+ RED.log.error(e);
267
+ RED.log.error(`Error happened removing flow ${flowid}`)
268
+ RED.comms.publish("unittesting:testresults", {
269
+ flowid: flowid,
270
+ status: "stopped"
271
+ })
272
+ })
273
+ .finally(() => removeFlow(idx + 1))
274
+ }
275
+
276
+ setTimeout(() => {
277
+ Promise.all([removeFlow(0)]).then(() => {
278
+ RED.log.debug("unittest: removed all flows")
279
+ runTestsForFileNames(fileIdx+1)
280
+ })
281
+ }, Math.max(...(timeoutValues.concat([5000]))))
282
+ })
283
+ })
284
+ }
285
+ runTestsForFileNames(0)
286
+ } else {
287
+
288
+ //
289
+ // run a single unit test but ensure that the test case does exist on disk
290
+ //
291
+
292
+ respondWithCount(res, 1)
293
+
294
+ fs.globSync(`${testDir}/**/*.json`).filter(d => d.includes(req.params.flowid)).forEach(filename => {
295
+ let flowDetails = JSON.parse(fs.readFileSync(filename))
296
+ let origFlowId = req.params.flowid
297
+
298
+ let tabDetails = flowDetails.filter(d => d.type == "tab")[0]
299
+ let injNodesIds = flowDetails.filter(d => d.type == "inject").map(d => d.id)
300
+
301
+ let timeoutValues = []
302
+
303
+ if (isTestPending(tabDetails) && req.query["testpend"] != "true") {
304
+ RED.comms.publish("unittesting:testresults", {
305
+ flowid: origFlowId,
306
+ status: "pending"
307
+ })
308
+ return null
309
+ }
310
+
311
+ // compute any timeout that is defined for the flow test
312
+ tabDetails.env.filter(d => d.name == "NRED_TIMEOUT").forEach(d => {
313
+ timeoutValues.push(parseInt(d.value) * 1000)
314
+ })
315
+
316
+ // add an catch all and trigger a assert false if any exceptions are
317
+ // raised by the test - if there isn't already a catch all node.
318
+ if (flowDetails.filter(d => d.type == "catch").filter(d => d.scope == null).length == 0) {
319
+ flowDetails = flowDetails.concat(createExtraCatchNode())
320
+ }
321
+
322
+ // if flow already exists, then just trigger the inject nodes
323
+ if (runtime._.flows.getFlow(origFlowId)) {
324
+ setTimeout(() => {
325
+ injNodesIds.forEach(ndeId => {
326
+ RED.nodes.getNode(ndeId)?.receive({"_unittest_triggered": true})
327
+ })
328
+ }, 500)
329
+
330
+ setTimeout(() => {
331
+ // tell the frontend that we've done with the test. If no
332
+ // status has been posted for the unit test, then it succeeds.
333
+ RED.comms.publish("unittesting:testresults", {
334
+ flowid: origFlowId,
335
+ status: "stopped"
336
+ })
337
+ }, Math.max(...(timeoutValues.concat([5000]))))
338
+ } else {
339
+ var newConfig = jsonClone(runtime._.flows.getFlows().flows);
340
+ newConfig = newConfig.concat(flowDetails);
341
+
342
+ runtime._.flows.setFlows(newConfig, null, 'flows', false, null, "root").then(() => {
343
+ runtime._.flows.startFlows("full", null, false, true).then(d => {
344
+ setTimeout(() => {
345
+ injNodesIds.forEach(ndeId => {
346
+ RED.nodes.getNode(ndeId)?.receive({"_unittest_triggered": true})
347
+ })
348
+ }, 500)
349
+
350
+ setTimeout(() => {
351
+ runtime._.flows.removeFlow(origFlowId, "root").then(result => {
352
+ // tell the frontend that we've done with the test. If no
353
+ // status has been posted for the unit test, then it succeeds.
354
+ RED.comms.publish("unittesting:testresults", {
355
+ flowid: origFlowId,
356
+ status: "stopped"
357
+ })
358
+ })
359
+ }, Math.max(...(timeoutValues.concat([5000]))))
360
+ })
361
+ })
362
+ }
363
+ })
364
+ }
365
+ });
366
+
367
+ RED.httpAdmin.get("/UnitTesting/halt",
368
+ (req, res) => {
369
+ var runtime = require("@node-red/runtime");
370
+ runtime._.flows.stopFlows()
371
+ res.sendStatus(200);
372
+ });
373
+
374
+ RED.httpAdmin.get("/UnitTesting/:flowid/retrieve",
375
+ (req, res) => {
376
+ const path = require('path');
377
+ const fs = require('fs')
378
+
379
+ let fileName = path.resolve(path.dirname(__filename), "..", "testflows", req.params.flowid, "flows.json")
380
+ const data = fs.readFileSync(fileName);
381
+
382
+ try {
383
+ res.writeHead(200, { 'Content-Type': 'application/json' });
384
+ res.end(JSON.stringify({ flowdata: JSON.parse(data) }));
385
+ } catch (err) {
386
+ res.sendStatus(500);
387
+ }
388
+ });
389
+
390
+ RED.httpAdmin.get("/UnitTesting/tests.json",
391
+ (req, res) => {
392
+ let testData = {
393
+ "status": "ok",
394
+ "last_updated_at": "2025-04-09T14:54:01.665Z",
395
+ "data": {
396
+ }
397
+ }
398
+
399
+ const path = require('path');
400
+ const fs = require('fs')
401
+
402
+ let testDir = path.resolve(path.dirname(__filename), "..", "testflows")
403
+
404
+ fs.globSync(`${testDir}/**/*.json`).forEach(filename => {
405
+ let details = path.parse(filename)
406
+ const data = fs.readFileSync(filename);
407
+
408
+ testData.data[path.basename(details.dir)] = {
409
+ "id": path.basename(details.dir),
410
+ "name": JSON.parse(data).filter(d => d.type == "tab")[0].label
411
+ }
412
+ })
413
+
414
+ try {
415
+ res.writeHead(200, { 'Content-Type': 'application/json' });
416
+ res.end(JSON.stringify(testData));
417
+ } catch (err) {
418
+ res.sendStatus(500);
419
+ }
420
+ });
421
+
39
422
  }
@@ -1,35 +1,46 @@
1
- module.exports = function(RED) {
1
+ module.exports = function (RED) {
2
2
  function CoreutassertfailureFunctionality(config) {
3
- RED.nodes.createNode(this,config);
3
+ RED.nodes.createNode(this, config);
4
4
 
5
5
  var node = this;
6
6
  var cfg = config;
7
7
 
8
- node.on('close', function() {
8
+ node.on('close', function () {
9
9
  node.status({});
10
10
  });
11
11
 
12
12
  /* msg handler, in this case pass the message on unchanged */
13
- node.on("input", function(msg, send, done) {
13
+ node.on("input", function (msg, send, done) {
14
14
 
15
- node.status({ fill: "red", shape: "dot", text: RED._("ut-assert-failure.label.failed") });
15
+ RED.comms.publish("unittesting:testresults", {
16
+ flowid: node.z,
17
+ status: "failed"
18
+ })
16
19
 
17
- // Send a message and how to handle errors.
20
+ // use node.log(..) here because node.error(..) sends a message to the debug
21
+ // panel but that errors out because the frontend can't find the workspace:
22
+ // Uncaught TypeError: can't access property "label", RED.nodes.workspace(...) is undefined
23
+ // that has follow-on effects.
24
+ // see https://nodered.org/docs/creating-nodes/node-js#logging-events for more details
25
+ node.log(`FAILED [${node.z}] assert failure was sent a message`, msg)
26
+ node.status({ fill: "red", shape: "dot", text: RED._("ut-assert-failure.label.failed") });
27
+
28
+ // Send a message and how to handle errors.
29
+ try {
18
30
  try {
19
- try {
20
- send(msg);
21
- done();
22
- } catch ( err ) {
23
- // use node.error if the node might send subsequent messages
24
- node.error("error occurred", { ...msg, error: err })
25
- done();
26
- }
31
+ send(msg);
32
+ done();
27
33
  } catch (err) {
28
- // use done if the node won't send anymore messages for the
29
- // message it received.
30
- msg.error = err
31
- done(err.message, msg)
34
+ // use node.error if the node might send subsequent messages
35
+ node.error("error occurred", { ...msg, error: err })
36
+ done();
32
37
  }
38
+ } catch (err) {
39
+ // use done if the node won't send anymore messages for the
40
+ // message it received.
41
+ msg.error = err
42
+ done(err.message, msg)
43
+ }
33
44
  });
34
45
  }
35
46
 
@@ -5,8 +5,17 @@ module.exports = function(RED) {
5
5
  var node = this;
6
6
  var cfg = config;
7
7
 
8
- node.on('close', function() {
9
- node.status({});
8
+ node.on('close', function(removed, done) {
9
+ if ( removed) {
10
+ node.status({});
11
+ } else {
12
+ // use node.log(..) here because node.error(..) sends a message to the debug
13
+ // panel but that errors out because the frontend can't find the workspace:
14
+ // Uncaught TypeError: can't access property "label", RED.nodes.workspace(...) is undefined
15
+ // that has follow-on effects.
16
+ node.log(`UNSUPPORTED [${node.z}] status is not supported`)
17
+ }
18
+ done()
10
19
  });
11
20
 
12
21
  /* msg handler, in this case pass the message on unchanged */