@gregoriusrippenstein/erlang-red-unittest 0.12.4 → 0.15.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 (211) hide show
  1. package/README.md +5 -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,10 @@ 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
+ ## Forum Discussions
42
+
43
+ - [Flow testsuite for testing core node functionality - May 2025](https://discourse.nodered.org/t/flow-testsuite-for-testing-core-node-functionality/97106)
44
+
41
45
  ## Tee Time
42
46
 
43
47
  <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 */
@@ -1,14 +1,6 @@
1
1
  <script type="text/javascript">
2
2
  (function(){
3
-
4
-
5
- function frontendSupportFunction() {
6
- }
7
-
8
- var functTwo = (arg) => {
9
-
10
- };
11
-
3
+
12
4
  RED.nodes.registerType('ut-assert-success',{
13
5
  color: '#addb7b',
14
6
  icon: "font-awesome/fa-smile-o",
@@ -17,6 +9,9 @@
17
9
  name: {
18
10
  value:"",
19
11
  },
12
+ msglimit: {
13
+ value:"=="
14
+ },
20
15
  count: {
21
16
  value: 1,
22
17
  required: true,
@@ -36,8 +31,8 @@
36
31
 
37
32
  if ( this.count == "") { this.count = 1 }
38
33
 
39
- if ( this.count > 1 ) {
40
- label = `${label}: ${this.count}`
34
+ if ( this.count > 0 ) {
35
+ label = `${label}: ${this.msglimit || '=='} ${this.count}`
41
36
  }
42
37
  return label;
43
38
  },
@@ -55,6 +50,21 @@
55
50
 
56
51
  oneditprepare: function() {
57
52
  if (!this.count || this.count == "") { this.count = 1 }
53
+
54
+ $(".limit-msgcnt-button-group").removeClass("selected")
55
+ $('#node-input-msglimit').val(this.msglimit || "==")
56
+
57
+ $(".limit-msgcnt-button-group").map((i, d) => {
58
+ if ($(d).data("value") == $('#node-input-msglimit').val()) {
59
+ $(d).addClass("selected")
60
+ }
61
+ })
62
+
63
+ $(".limit-msgcnt-button-group").on("click", function () {
64
+ $(".limit-msgcnt-button-group").removeClass("selected");
65
+ $(this).addClass("selected");
66
+ $('#node-input-msglimit').val($(this).data("value"))
67
+ })
58
68
  },
59
69
 
60
70
  oneditcancel: function() {
@@ -78,9 +88,20 @@
78
88
  <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
79
89
  <input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
80
90
  </div>
81
-
91
+
82
92
  <div class="form-row">
83
93
  <label for="node-input-count" data-i18n="ut-assert-success.label.count"></label>
94
+
95
+
96
+ <span class="button-group">
97
+ <button type="button" data-value="<=" class="red-ui-button toggle limit-msgcnt-button-group selected"><span data-i18n="ut-assert-success.label.limit.upperlimit"></span></button>
98
+ <button type="button" data-value="=="class="red-ui-button toggle limit-msgcnt-button-group"><span data-i18n="ut-assert-success.label.limit.exactly"></span></button>
99
+ <button type="button" data-value=">=" class="red-ui-button toggle limit-msgcnt-button-group"><span data-i18n="ut-assert-success.label.limit.lowerlimit"></span></button>
100
+ </span>
101
+
102
+ <input type="text" id="node-input-msglimit" class='hide'></input>
84
103
  <input type="number" id="node-input-count" data-i18n="[placeholder]ut-assert-success.placeholder.count" style="width: 100px;"></input>
104
+
105
+ <span data-i18n="ut-assert-success.label.messages"></span>
85
106
  </div>
86
107
  </script>