├── .gitignore ├── test ├── _spec.js └── settings_spec.js ├── examples ├── nodes │ ├── lower-case.js │ ├── lower-case.html │ ├── 90-comment.js │ └── 80-function.js ├── comment_spec.js ├── lower-case_spec.js └── function_spec.js ├── package.json ├── CHANGELOG.md ├── LICENSE ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode/launch.json 3 | -------------------------------------------------------------------------------- /test/_spec.js: -------------------------------------------------------------------------------- 1 | var should = require("should"); 2 | var helper = require('../index.js'); 3 | 4 | describe('_spec.js', function() { 5 | console.log('todo'); 6 | 7 | it('should have credentials', function(done) { 8 | helper.should.have.property('credentials'); 9 | done(); 10 | }); 11 | }); -------------------------------------------------------------------------------- /examples/nodes/lower-case.js: -------------------------------------------------------------------------------- 1 | module.exports = function(RED) { 2 | function LowerCaseNode(config) { 3 | RED.nodes.createNode(this,config); 4 | var node = this; 5 | node.on('input', function(msg) { 6 | msg.payload = msg.payload.toLowerCase(); 7 | node.send(msg); 8 | }); 9 | } 10 | RED.nodes.registerType("lower-case",LowerCaseNode); 11 | } -------------------------------------------------------------------------------- /examples/nodes/lower-case.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 24 | -------------------------------------------------------------------------------- /examples/nodes/90-comment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright JS Foundation and other contributors, http://js.foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | module.exports = function(RED) { 18 | "use strict"; 19 | function CommentNode(n) { 20 | RED.nodes.createNode(this,n); 21 | } 22 | RED.nodes.registerType("comment",CommentNode); 23 | } 24 | -------------------------------------------------------------------------------- /test/settings_spec.js: -------------------------------------------------------------------------------- 1 | var should = require("should"); 2 | var NodeTestHelper = require('../index.js').NodeTestHelper; 3 | 4 | var helper; 5 | beforeEach(function() { 6 | // .init() is implicitly called on instantiation so not required 7 | helper = new NodeTestHelper(); 8 | }); 9 | 10 | describe('add custom settings on init', function () { 11 | it('should merge custom settings with RED.settings defaults', function () { 12 | helper._settings.should.not.have.property('functionGlobalContext'); 13 | helper.init(null, {functionGlobalContext: {}}); 14 | helper._settings.should.have.property('functionGlobalContext'); 15 | }); 16 | }); 17 | 18 | describe('helper.settings() usage', function() { 19 | it('should return a settings Object', function() { 20 | var settings = helper.settings(); 21 | should.exist(settings); 22 | settings.should.have.property('get'); 23 | }); 24 | it('should not maintain settings state across multiple invocations', function() { 25 | helper.settings({ foo: true }).should.have.property('foo'); 26 | helper.settings({ bar: true }).should.not.have.property('foo'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-node-test-helper", 3 | "version": "0.3.5", 4 | "description": "A test framework for Node-RED nodes", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha \"test/**/*_spec.js\"", 8 | "examples": "mocha \"examples/**/*_spec.js\"" 9 | }, 10 | "license": "Apache-2.0", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/node-red/node-red-node-test-helper.git" 14 | }, 15 | "dependencies": { 16 | "body-parser": "^1.20.3", 17 | "express": "^4.21.0", 18 | "semver": "^7.5.4", 19 | "should": "^13.2.3", 20 | "should-sinon": "^0.0.6", 21 | "sinon": "^11.1.2", 22 | "stoppable": "^1.1.0", 23 | "supertest": "^7.1.4" 24 | }, 25 | "devDependencies": { 26 | "mocha": "^11.7.1" 27 | }, 28 | "contributors": [ 29 | { 30 | "name": "Nick O'Leary" 31 | }, 32 | { 33 | "name": "Dave Conway-Jones" 34 | }, 35 | { 36 | "name": "Mike Blackstock" 37 | }, 38 | { 39 | "name": "Dean Cording" 40 | } 41 | ], 42 | "keywords": [ 43 | "test", 44 | "iot", 45 | "node-red" 46 | ], 47 | "engines": { 48 | "node": ">=14" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/comment_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright JS Foundation and other contributors, http://js.foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var should = require("should"); 18 | var helper = require("../index.js"); 19 | helper.init(require.resolve('node-red')); 20 | 21 | var commentNode = require("./nodes/90-comment.js"); 22 | 23 | describe('comment Node', function() { 24 | 25 | afterEach(function() { 26 | helper.unload(); 27 | }); 28 | 29 | it('should be loaded', function(done) { 30 | var flow = [{id:"n1", type:"comment", name: "comment" }]; 31 | helper.load(commentNode, flow, function() { 32 | var n1 = helper.getNode("n1"); 33 | n1.should.have.property('name', 'comment'); 34 | done(); 35 | }); 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #### 0.3.5 2 | 3 | - Update mocha 4 | - Bump supertest (#83) @hardillb 5 | - feat: make load, setFlows async/await (enable vitest) (#82) @AllanOricil 6 | - feat: enable async start/stop server (#81) @AllanOricil 7 | - Bump express.js and body-parser (#79) @hardillb 8 | 9 | #### 0.3.4 10 | 11 | - Update dependencies 12 | 13 | #### 0.3.3 14 | 15 | - Add plugin stub to runtime (#73) @joepavitt 16 | - Use compatible versions rather than specific version of dependencies (#70) @Pezmc 17 | 18 | #### 0.3.2 19 | 20 | - Fix async module loading (#65) @knolleary 21 | - Update README.md (#61) @andreasmarkussen 22 | 23 | #### 0.3.1 24 | 25 | - Add support for async node modules (#63) @knolleary 26 | 27 | #### 0.3.0 28 | 29 | - Require node.js >=14 30 | - Add `setFlows` so that node being tested can modify flows (#54) @Steve-Mcl 31 | 32 | #### 0.2.7 33 | 34 | - Wait for startFlows to resolve before returning from loadFlow call - required with Node-RED 1.3+ 35 | - README.md: Update example unit test to report assertion failures 36 | - examples: lower-case_spec.js: Allow proper assertion failure reporting (#45) 37 | 38 | #### 0.2.6 39 | 40 | - Optionally preload catch/status/complete nodes in test cases Fixes #48 41 | 42 | #### 0.2.5 43 | 44 | - Add proper middleware on httpAdmin express app 45 | 46 | #### 0.2.4 47 | 48 | - Update dependencies 49 | - #43 Helper.load return a Promise 50 | 51 | #### 0.2.3 52 | 53 | - Allow runtime settings to be provided in `helper.init(runtimepath, userSettings)` 54 | - Add `helper.settings(userSettings)` 55 | -------------------------------------------------------------------------------- /examples/lower-case_spec.js: -------------------------------------------------------------------------------- 1 | var helper = require("../index.js"); 2 | var lowerNode = require("./nodes/lower-case.js"); 3 | 4 | helper.init(require.resolve('node-red')); 5 | 6 | describe('lower-case Node', function () { 7 | 8 | afterEach(function () { 9 | helper.unload(); 10 | }); 11 | 12 | it('should be loaded', function (done) { 13 | var flow = [{ id: "n1", type: "lower-case", name: "lower-case" }]; 14 | helper.load(lowerNode, flow, function () { 15 | var n1 = helper.getNode("n1"); 16 | try { 17 | n1.should.have.property('name', 'lower-case'); 18 | done(); 19 | } catch(err) { 20 | done(err); 21 | } 22 | }); 23 | }); 24 | 25 | it('should be loaded in exported flow', function (done) { 26 | var flow = [{"id":"3912a37a.c3818c","type":"lower-case","z":"e316ac4b.c85a2","name":"lower-case","x":240,"y":320,"wires":[[]]}]; 27 | helper.load(lowerNode, flow, function () { 28 | var n1 = helper.getNode("3912a37a.c3818c"); 29 | try { 30 | n1.should.have.property('name', 'lower-case'); 31 | done(); 32 | } catch(err) { 33 | done(err); 34 | } 35 | }); 36 | }); 37 | 38 | it('should make payload lower case', function (done) { 39 | var flow = [ 40 | { id: "n1", type: "lower-case", name: "test name",wires:[["n2"]] }, 41 | { id: "n2", type: "helper" } 42 | ]; 43 | helper.load(lowerNode, flow, function () { 44 | var n2 = helper.getNode("n2"); 45 | var n1 = helper.getNode("n1"); 46 | n2.on("input", function (msg) { 47 | try { 48 | msg.should.have.property('payload', 'uppercase'); 49 | done(); 50 | } catch(err) { 51 | done(err); 52 | } 53 | }); 54 | n1.receive({ payload: "UpperCase" }); 55 | }); 56 | }); 57 | it('should modify the flow then lower case of payload', async function () { 58 | const flow = [ 59 | { id: "n2", type: "helper" } 60 | ] 61 | await helper.load(lowerNode, flow) 62 | 63 | const newFlow = [...flow] 64 | newFlow.push( { id: "n1", type: "lower-case", name: "lower-case", wires:[['n2']] },) 65 | await helper.setFlows(newFlow) 66 | const n1 = helper.getNode('n1') 67 | n1.should.have.a.property('name', 'lower-case') 68 | await new Promise((resolve, reject) => { 69 | const n2 = helper.getNode('n2') 70 | n2.on('input', function (msg) { 71 | try { 72 | msg.should.have.property('payload', 'hello'); 73 | resolve() 74 | } catch (err) { 75 | reject(err); 76 | } 77 | }); 78 | n1.receive({ payload: 'HELLO' }); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /examples/nodes/80-function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright JS Foundation and other contributors, http://js.foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | module.exports = function(RED) { 18 | "use strict"; 19 | var util = require("util"); 20 | var vm = require("vm"); 21 | 22 | function sendResults(node,_msgid,msgs) { 23 | if (msgs == null) { 24 | return; 25 | } else if (!util.isArray(msgs)) { 26 | msgs = [msgs]; 27 | } 28 | var msgCount = 0; 29 | for (var m=0; m0) { 52 | node.send(msgs); 53 | } 54 | } 55 | 56 | function FunctionNode(n) { 57 | RED.nodes.createNode(this,n); 58 | var node = this; 59 | this.name = n.name; 60 | this.func = n.func; 61 | var functionText = "var results = null;"+ 62 | "results = (function(msg){ "+ 63 | "var __msgid__ = msg._msgid;"+ 64 | "var node = {"+ 65 | "log:__node__.log,"+ 66 | "error:__node__.error,"+ 67 | "warn:__node__.warn,"+ 68 | "on:__node__.on,"+ 69 | "status:__node__.status,"+ 70 | "send:function(msgs){ __node__.send(__msgid__,msgs);}"+ 71 | "};\n"+ 72 | this.func+"\n"+ 73 | "})(msg);"; 74 | this.topic = n.topic; 75 | this.outstandingTimers = []; 76 | this.outstandingIntervals = []; 77 | var sandbox = { 78 | console:console, 79 | util:util, 80 | Buffer:Buffer, 81 | RED: { 82 | util: RED.util 83 | }, 84 | __node__: { 85 | log: function() { 86 | node.log.apply(node, arguments); 87 | }, 88 | error: function() { 89 | node.error.apply(node, arguments); 90 | }, 91 | warn: function() { 92 | node.warn.apply(node, arguments); 93 | }, 94 | send: function(id, msgs) { 95 | sendResults(node, id, msgs); 96 | }, 97 | on: function() { 98 | if (arguments[0] === "input") { 99 | throw new Error(RED._("function.error.inputListener")); 100 | } 101 | node.on.apply(node, arguments); 102 | }, 103 | status: function() { 104 | node.status.apply(node, arguments); 105 | } 106 | }, 107 | context: { 108 | set: function() { 109 | node.context().set.apply(node,arguments); 110 | }, 111 | get: function() { 112 | return node.context().get.apply(node,arguments); 113 | }, 114 | keys: function() { 115 | return node.context().keys.apply(node,arguments); 116 | }, 117 | get global() { 118 | return node.context().global; 119 | }, 120 | get flow() { 121 | return node.context().flow; 122 | } 123 | }, 124 | flow: { 125 | set: function() { 126 | node.context().flow.set.apply(node,arguments); 127 | }, 128 | get: function() { 129 | return node.context().flow.get.apply(node,arguments); 130 | }, 131 | keys: function() { 132 | return node.context().flow.keys.apply(node,arguments); 133 | } 134 | }, 135 | global: { 136 | set: function() { 137 | node.context().global.set.apply(node,arguments); 138 | }, 139 | get: function() { 140 | return node.context().global.get.apply(node,arguments); 141 | }, 142 | keys: function() { 143 | return node.context().global.keys.apply(node,arguments); 144 | } 145 | }, 146 | setTimeout: function () { 147 | var func = arguments[0]; 148 | var timerId; 149 | arguments[0] = function() { 150 | sandbox.clearTimeout(timerId); 151 | try { 152 | func.apply(this,arguments); 153 | } catch(err) { 154 | node.error(err,{}); 155 | } 156 | }; 157 | timerId = setTimeout.apply(this,arguments); 158 | node.outstandingTimers.push(timerId); 159 | return timerId; 160 | }, 161 | clearTimeout: function(id) { 162 | clearTimeout(id); 163 | var index = node.outstandingTimers.indexOf(id); 164 | if (index > -1) { 165 | node.outstandingTimers.splice(index,1); 166 | } 167 | }, 168 | setInterval: function() { 169 | var func = arguments[0]; 170 | var timerId; 171 | arguments[0] = function() { 172 | try { 173 | func.apply(this,arguments); 174 | } catch(err) { 175 | node.error(err,{}); 176 | } 177 | }; 178 | timerId = setInterval.apply(this,arguments); 179 | node.outstandingIntervals.push(timerId); 180 | return timerId; 181 | }, 182 | clearInterval: function(id) { 183 | clearInterval(id); 184 | var index = node.outstandingIntervals.indexOf(id); 185 | if (index > -1) { 186 | node.outstandingIntervals.splice(index,1); 187 | } 188 | } 189 | }; 190 | if (util.hasOwnProperty('promisify')) { 191 | sandbox.setTimeout[util.promisify.custom] = function(after, value) { 192 | return new Promise(function(resolve, reject) { 193 | sandbox.setTimeout(function(){ resolve(value) }, after); 194 | }); 195 | } 196 | } 197 | var context = vm.createContext(sandbox); 198 | try { 199 | this.script = vm.createScript(functionText); 200 | this.on("input", function(msg) { 201 | try { 202 | var start = process.hrtime(); 203 | context.msg = msg; 204 | this.script.runInContext(context); 205 | sendResults(this,msg._msgid,context.results); 206 | 207 | var duration = process.hrtime(start); 208 | var converted = Math.floor((duration[0] * 1e9 + duration[1])/10000)/100; 209 | this.metric("duration", msg, converted); 210 | if (process.env.NODE_RED_FUNCTION_TIME) { 211 | this.status({fill:"yellow",shape:"dot",text:""+converted}); 212 | } 213 | } catch(err) { 214 | 215 | var line = 0; 216 | var errorMessage; 217 | var stack = err.stack.split(/\r?\n/); 218 | if (stack.length > 0) { 219 | while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) { 220 | line++; 221 | } 222 | 223 | if (line < stack.length) { 224 | errorMessage = stack[line]; 225 | var m = /:(\d+):(\d+)$/.exec(stack[line+1]); 226 | if (m) { 227 | var lineno = Number(m[1])-1; 228 | var cha = m[2]; 229 | errorMessage += " (line "+lineno+", col "+cha+")"; 230 | } 231 | } 232 | } 233 | if (!errorMessage) { 234 | errorMessage = err.toString(); 235 | } 236 | this.error(errorMessage, msg); 237 | } 238 | }); 239 | this.on("close", function() { 240 | while (node.outstandingTimers.length > 0) { 241 | clearTimeout(node.outstandingTimers.pop()) 242 | } 243 | while (node.outstandingIntervals.length > 0) { 244 | clearInterval(node.outstandingIntervals.pop()) 245 | } 246 | this.status({}); 247 | }) 248 | } catch(err) { 249 | // eg SyntaxError - which v8 doesn't include line number information 250 | // so we can't do better than this 251 | this.error(err); 252 | } 253 | } 254 | RED.nodes.registerType("function",FunctionNode); 255 | RED.library.register("functions"); 256 | } 257 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright JS Foundation and other contributors, http://js.foundation 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node Test Helper 2 | 3 | This test-helper module makes the node unit test framework from the Node-RED core available for node contributors. 4 | 5 | Using the test-helper, your tests can start the Node-RED runtime, load a test flow, and receive messages to ensure your node code is correct. 6 | 7 | ## Adding to your node project dependencies 8 | 9 | Node-RED is required by the helper as a peer dependency, meaning it must be installed along with the helper itself. To create unit tests for your node project, add this test helper and Node-RED as follows: 10 | 11 | npm install node-red-node-test-helper node-red --save-dev 12 | 13 | This will add the helper module to your `package.json` file: 14 | 15 | ```json 16 | ... 17 | "devDependencies": { 18 | "node-red":"^0.18.4", 19 | "node-red-node-test-helper": "^0.1.8" 20 | } 21 | ... 22 | ``` 23 | 24 | ## Using a local Node-RED install for tests 25 | 26 | If you already have Node-RED installed for development, you can create a symbolic link to your local installation. For example, if Node-RED is cloned in your `~/projects` folder use: 27 | 28 | npm install ~/projects/node-red --no-save 29 | 30 | ## Adding test script to `package.json` 31 | 32 | To run your tests you can add a test script to your `package.json` file in the `scripts` section. To run all of the files with the `_spec.js` prefix in the test directory for example: 33 | 34 | ```json 35 | ... 36 | "scripts": { 37 | "test": "mocha \"test/**/*_spec.js\"" 38 | }, 39 | ... 40 | ``` 41 | 42 | This will allow you to use `npm test` on the command line. 43 | 44 | ## Creating unit tests 45 | 46 | We recommend putting unit test scripts in the `test/` folder of your project and using the `*_spec.js` (for specification) suffix naming convention. 47 | 48 | ## Example unit test 49 | 50 | Here is an example test for testing the lower-case node in the [Node-RED documentation](https://nodered.org/docs/creating-nodes/first-node). Here we name our test script `test/lower-case_spec.js`. 51 | 52 | ### `test/lower-case_spec.js`: 53 | 54 | ```javascript 55 | var should = require("should"); 56 | var helper = require("node-red-node-test-helper"); 57 | var lowerNode = require("../lower-case.js"); 58 | 59 | helper.init(require.resolve('node-red')); 60 | 61 | describe('lower-case Node', function () { 62 | 63 | beforeEach(function (done) { 64 | helper.startServer(done); 65 | }); 66 | 67 | afterEach(function (done) { 68 | helper.unload(); 69 | helper.stopServer(done); 70 | }); 71 | 72 | it('should be loaded', function (done) { 73 | var flow = [{ id: "n1", type: "lower-case", name: "lower-case" }]; 74 | helper.load(lowerNode, flow, function () { 75 | var n1 = helper.getNode("n1"); 76 | try { 77 | n1.should.have.property('name', 'lower-case'); 78 | done(); 79 | } catch(err) { 80 | done(err); 81 | } 82 | }); 83 | }); 84 | 85 | it('should make payload lower case', function (done) { 86 | var flow = [ 87 | { id: "n1", type: "lower-case", name: "lower-case",wires:[["n2"]] }, 88 | { id: "n2", type: "helper" } 89 | ]; 90 | helper.load(lowerNode, flow, function () { 91 | var n2 = helper.getNode("n2"); 92 | var n1 = helper.getNode("n1"); 93 | n2.on("input", function (msg) { 94 | try { 95 | msg.should.have.property('payload', 'uppercase'); 96 | done(); 97 | } catch(err) { 98 | done(err); 99 | } 100 | }); 101 | n1.receive({ payload: "UpperCase" }); 102 | }); 103 | }); 104 | }); 105 | ``` 106 | 107 | In this example, we require `should` for assertions, this helper module, as well as the `lower-case` node we want to test, located in the parent directory. 108 | 109 | We then have a set of mocha unit tests. These tests check that the node loads correctly, and ensures it makes the payload string lower case as expected. 110 | 111 | Note how the assertion failures are caught explicitly and passed to the `done()` call. Node-RED swallows exceptions that are raised in the flow, so we make sure the test framework is aware of them. Not doing so would simply lead to a test timeout because `done()` is never called in case of an assertion failure. 112 | 113 | ## Initializing Helper 114 | 115 | To get started, we need to tell the helper where to find the node-red runtime. This is done by calling `helper.init(require.resolve('node-red'))` as shown. 116 | 117 | The helper takes an optional `userSettings` parameter which is merged with the runtime defaults. 118 | 119 | ```javascript 120 | helper.init(require.resolve('node-red'), { 121 | functionGlobalContext: { os:require('os') } 122 | }); 123 | ``` 124 | 125 | ## Getting nodes in the runtime 126 | 127 | The asynchronous `helper.load()` method calls the supplied callback function once the Node-RED server and runtime is ready. We can then call the `helper.getNode(id)` method to get a reference to nodes in the runtime. For more information on these methods see the API section below. 128 | 129 | ## Receiving messages from nodes 130 | 131 | The second test uses a `helper` node in the runtime connected to the output of our `lower-case` node under test. The `helper` node is a mock node with no functionality. By adding "input" event handlers as in the example, we can check the messages received by the `helper`. 132 | 133 | To send a message into the `lower-case` node `n1` under test we call `n1.receive({ payload: "UpperCase" })` on that node. We can then check that the payload is indeed lower case in the `helper` node input event handler. 134 | 135 | ## Working with Spies 136 | 137 | A Spy ([docs](http://sinonjs.org/releases/v5.0.6/spies/)) helps you collect information about how many times a function was called, with what, what it returned, etc. 138 | 139 | This helper library automatically creates spies for the following functions on `Node.prototype` (these are the same functions as mentioned in the ["Creating Nodes" guide](https://nodered.org/docs/creating-nodes/node-js)): 140 | 141 | - `trace()` 142 | - `debug()` 143 | - `warn()` 144 | - `log()` 145 | - `status()` 146 | - `send()` 147 | 148 | > **Warning:** Don't try to spy on these functions yourself with `sinon.spy()`; since they are already spies, Sinon will throw an exception! 149 | 150 | ### Synchronous Example: Initialization 151 | 152 | The `FooNode` `Node` will call `warn()` when it's initialized/constructed if `somethingGood` isn't present in the config, like so: 153 | 154 | ```js 155 | // /path/to/foo-node.js 156 | module.exports = function FooNode (config) { 157 | RED.nodes.createNode(this, config); 158 | 159 | if (!config.somethingGood) { 160 | this.warn('badness'); 161 | } 162 | } 163 | ``` 164 | 165 | You can then assert: 166 | 167 | ```js 168 | // /path/to/test/foo-node_spec.js 169 | const FooNode = require('/path/to/foo-node'); 170 | 171 | it('should warn if the `somethingGood` prop is falsy', function (done) { 172 | const flow = { 173 | name: 'n1', 174 | somethingGood: false, 175 | /* ..etc.. */ 176 | }; 177 | helper.load(FooNode, flow, function () { 178 | n1.warn.should.be.calledWithExactly('badness'); 179 | done(); 180 | }); 181 | }); 182 | ``` 183 | 184 | ### Synchronous Example: Input 185 | 186 | When it receives input, `FooNode` will immediately call `error()` if `msg.omg` is `true`: 187 | 188 | ```js 189 | // somewhere in FooNode constructor 190 | this.on('input', msg => { 191 | if (msg.omg) { 192 | this.error('lolwtf'); 193 | } 194 | // ..etc.. 195 | }); 196 | ``` 197 | 198 | Here's an example of how to make that assertion: 199 | 200 | ```js 201 | describe('if `omg` in input message', function () { 202 | it('should call `error` with "lolwtf" ', function (done) { 203 | const flow = { 204 | name: 'n1', 205 | /* ..etc.. */ 206 | }; 207 | helper.load(FooNode, flow, function () { 208 | const n1 = helper.getNode('n1') 209 | n1.receive({omg: true}); 210 | n1.on('input', () => { 211 | n1.warn.should.be.calledWithExactly('lolwtf'); 212 | done(); 213 | }); 214 | }); 215 | }); 216 | }); 217 | ``` 218 | 219 | ### Asynchronous Example 220 | 221 | Later in `FooNode`'s `input` listener, `warn()` may *asynchronously* be called, like so: 222 | 223 | ```js 224 | // somewhere in FooNode constructor function 225 | this.on('input', msg => { 226 | if (msg.omg) { 227 | this.error('lolwtf'); 228 | } 229 | // ..etc.. 230 | 231 | Promise.resolve() 232 | .then(() => { 233 | if (msg.somethingBadAndWeird) { 234 | this.warn('bad weirdness'); 235 | } 236 | }); 237 | }); 238 | ``` 239 | 240 | The strategy in the previous example used for testing behavior of `msg.omg` will *not* work! `n1.warn.should.be.calledWithExactly('bad weirdness')` will throw an `AssertionError`, because `warn()` hasn't been called yet; `EventEmitter`s are synchronous, and the test's `input` listener is called directly after the `input` listener in `FooNode`'s function finished--but *before* the `Promise` is resolved! 241 | 242 | Since we don't know *when* exactly `warn()` will get called (short of the slow, race-condition-prone solution of using a `setTimeout` and waiting *n* milliseconds, *then* checking), we need a different way to inspect the call. Miraculously, this helper module provides a solution. 243 | 244 | The helper will cause the `FooNode` to asynchronously emit an event when `warn` is called (as well as the other methods in the above list). The event name will be of the format `call:`; in this case, `methodName` is `warn`, so the event name is `call:warn`. The event Will pass a single argument: a Spy Call object ([docs](http://sinonjs.org/releases/v5.0.6/spy-call/)) corresponding to the latest method call. You can then make an assertion against this Spy Call argument, like so: 245 | 246 | ```js 247 | describe('if `somethingBadAndWeird` in input msg', function () { 248 | it('should call "warn" with "bad weirdness" ', function (done) { 249 | const flow = { 250 | name: 'n1', 251 | /* ..etc.. */ 252 | }; 253 | helper.load(FooNode, flow, function () { 254 | const n1 = helper.getNode('n1') 255 | n1.receive({somethingBadAndWeird: true}); 256 | // because the emit happens asynchronously, this listener 257 | // will be registered before `call:warn` is emitted. 258 | n1.on('call:warn', call => { 259 | call.should.be.calledWithExactly('bad weirdness'); 260 | done(); 261 | }); 262 | }); 263 | }); 264 | }); 265 | ``` 266 | 267 | As you can see, looks very similar to the synchronous solution; the only differences are the event name and assertion target. 268 | 269 | > **Note**: The "asynchronous" strategy will also work *if and only if* a synchronous call to the spy is *still the most recent* when we attempt to make the assertion. This can lead to subtle bugs when refactoring, so exercise care when choosing which strategy to use. 270 | 271 | ## Running your tests 272 | 273 | To run your tests: 274 | 275 | npm test 276 | 277 | Producing the following output (for this example): 278 | 279 | > red-contrib-lower-case@0.1.0 test /dev/work/node-red-contrib-lower-case 280 | > mocha "test/**/*_spec.js" 281 | 282 | lower-case Node 283 | ✓ should be loaded 284 | ✓ should make payload lower case 285 | 286 | 2 passing (50ms) 287 | 288 | ## Creating test flows with the editor 289 | 290 | To create a test flow with the Node-RED editor, export the test flow to the clipboard, and then paste the flow into your unit test code. One helpful technique to include `helper` nodes in this way is to use a `debug` node as a placeholder for a `helper` node, and then search and replace `"type":"debug"` with `"type":"helper"` where needed. 291 | 292 | ## Using `catch` and `status` nodes in test flows 293 | 294 | To use `catch` and `status` or other nodes that depend on special handling in the runtime in your test flows, you will often need to add a `tab` to identify the flow, and associated `z` properties to your nodes to associate the nodes with the flow. For example: 295 | 296 | ```javascript 297 | var flow = [{id:"f1", type:"tab", label:"Test flow"}, 298 | { id: "n1", z:"f1", type: "lower-case", name: "test name",wires:[["n2"]] }, 299 | { id: "n2", z:"f1", type: "helper" } 300 | ``` 301 | 302 | ## Additional examples 303 | 304 | For additional test examples taken from the Node-RED core, see the `.js` files supplied in the `test/examples` folder and the associated test code at `test/nodes` in [the Node-RED repository](https://github.com/node-red/node-red/tree/master/test/nodes). 305 | 306 | ## API 307 | 308 | > *Work in progress.* 309 | 310 | ### load(testNode, testFlows, testCredentials, cb) 311 | 312 | Loads a flow then starts the flow. This function has the following arguments: 313 | 314 | * testNode: (object|array of objects) Module object of a node to be tested returned by require function. This node will be registered, and can be used in testFlows. 315 | * testFlow: (array of objects) Flow data to test a node. If you want to use flow data exported from Node-RED editor, export the flow to the clipboard and paste the content into your test scripts. 316 | * testCredentials: (object) Optional node credentials. 317 | * cb: (function) Function to call back when testFlows has been started (not required when called wih `await`) 318 | 319 | ### setFlows(testFlow, type, testCredentials, cb) 320 | 321 | Updates the currently loaded flow. This function has the following arguments: 322 | 323 | * testFlow: (array of objects) Flow data to test a node. If you want to use flow data exported from Node-RED editor, export the flow to the clipboard and paste the content into your test scripts. 324 | * type: (string) Flow data to test a node. If you want to use flow data exported from Node-RED editor, export the flow to the clipboard and paste the content into your test scripts. 325 | * testCredentials: (object) Optional node credentials. 326 | * cb: (function) Function to call back when testFlows has been loaded (not required when called wih `await`) 327 | 328 | #### Example 329 | 330 | ```js 331 | it('should modify the flow then lower case of payload', async function () { 332 | const flow = [ 333 | { id: "n2", type: "helper" } 334 | ] 335 | await helper.load(lowerNode, flow) 336 | const newFlow = [...flow] 337 | newFlow.push( { id: "n1", type: "lower-case", name: "lower-case", wires:[['n2']] },) 338 | await helper.setFlows(newFlow, "nodes") //update flows 339 | const n1 = helper.getNode('n1') 340 | n1.should.have.a.property('name', 'lower-case') 341 | await new Promise((resolve, reject) => { 342 | const n2 = helper.getNode('n2') 343 | n2.on('input', function (msg) { 344 | try { 345 | msg.should.have.property('payload', 'hello'); 346 | resolve() 347 | } catch (err) { 348 | reject(err); 349 | } 350 | }); 351 | n1.receive({ payload: 'HELLO' }); 352 | }); 353 | }); 354 | ``` 355 | 356 | ### unload() 357 | 358 | Return promise to stop all flows, clean up test runtime. 359 | 360 | ### getNode(id) 361 | 362 | Returns a node instance by id in the testFlow. Any node that is defined in testFlows can be retrieved, including any helper node added to the flow. 363 | 364 | ### clearFlows() 365 | 366 | Stop all flows. 367 | 368 | ### request() 369 | 370 | Create http ([supertest](https://www.npmjs.com/package/supertest)) request to the editor/admin url. 371 | 372 | Example: 373 | 374 | ```javascript 375 | helper.request().post('/inject/invalid').expect(404).end(done); 376 | ``` 377 | 378 | ### settings(userSettings) 379 | 380 | Merges any userSettings with the defaults returned by `RED.settings`. Each invocation of this method will overwrite the previous userSettings to prevent unexpected problems in your tests. 381 | 382 | This will enable you to replicate your production environment within your tests, for example where you're using the `functionGlobalContext` to enable extra node modules within your functions. 383 | 384 | ```javascript 385 | // functions can now access os via global.get('os') 386 | helper.settings({ functionGlobalContext: { os:require('os') } }); 387 | 388 | // reset back to defaults 389 | helper.settings({ }); 390 | ``` 391 | 392 | ### startServer(done) 393 | 394 | Starts a Node-RED server for testing nodes that depend on http or web sockets endpoints like the debug node. 395 | To start a Node-RED server before all test cases: 396 | 397 | ```javascript 398 | before(function(done) { 399 | helper.startServer(done); 400 | }); 401 | ``` 402 | 403 | ### stopServer(done) 404 | 405 | Stop server. Generally called after unload() complete. For example, to unload a flow then stop a server after each test: 406 | 407 | ```javascript 408 | afterEach(function(done) { 409 | helper.unload().then(function() { 410 | helper.stopServer(done); 411 | }); 412 | }); 413 | ``` 414 | 415 | ### url() 416 | 417 | Return the URL of the helper server including the ephemeral port used when starting the server. 418 | 419 | ### log() 420 | 421 | Return a spy on the logs to look for events from the node under test. For example: 422 | 423 | ```javascript 424 | var logEvents = helper.log().args.filter(function(event) { 425 | return event[0].type == "batch"; 426 | }); 427 | ``` 428 | 429 | ## Running helper examples 430 | 431 | npm run examples 432 | 433 | This runs tests on an included lower-case node (as above) as well as snaphots of some of the core nodes' Javascript files to ensure the helper is working as expected. 434 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright JS Foundation and other contributors, http://js.foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 'use strict'; 17 | 18 | const path = require("path"); 19 | const process = require("process") 20 | const sinon = require("sinon"); 21 | const should = require('should'); 22 | const fs = require('fs'); 23 | require('should-sinon'); 24 | const request = require('supertest'); 25 | var bodyParser = require("body-parser"); 26 | const express = require("express"); 27 | const http = require('http'); 28 | const stoppable = require('stoppable'); 29 | const semver = require('semver'); 30 | const EventEmitter = require('events').EventEmitter; 31 | 32 | const PROXY_METHODS = ['log', 'status', 'warn', 'error', 'debug', 'trace', 'send']; 33 | 34 | 35 | // Find the nearest package.json 36 | function findPackageJson(dir) { 37 | dir = path.resolve(dir || process.cwd()) 38 | const { root } = path.parse(dir) 39 | if (dir === root) { 40 | return null 41 | } 42 | const packagePath = path.join(dir, 'package.json') 43 | if (fs.existsSync(packagePath)) { 44 | return { 45 | path: packagePath, 46 | packageJson: JSON.parse(fs.readFileSync(packagePath, 'utf-8')) 47 | } 48 | } else { 49 | return findPackageJson(path.resolve(path.join(dir, '..'))) 50 | } 51 | } 52 | 53 | /** 54 | * Finds the NR runtime path by inspecting environment 55 | */ 56 | function findRuntimePath() { 57 | const upPkg = findPackageJson() 58 | // case 1: we're in NR itself 59 | if (upPkg.packageJson.name === 'node-red') { 60 | if (checkSemver(upPkg.packageJson.version,"<0.20.0")) { 61 | return path.join(path.dirname(upPkg.path), upPkg.packageJson.main); 62 | } else { 63 | return path.join(path.dirname(upPkg.path),"packages","node_modules","node-red"); 64 | } 65 | } 66 | // case 2: NR is resolvable from here 67 | try { 68 | return require.resolve('node-red'); 69 | } catch (ignored) {} 70 | // case 3: NR is installed alongside node-red-node-test-helper 71 | if ((upPkg.packageJson.dependencies && upPkg.packageJson.dependencies['node-red']) || 72 | (upPkg.packageJson.devDependencies && upPkg.packageJson.devDependencies['node-red'])) { 73 | const dirpath = path.join(path.dirname(upPkg.path), 'node_modules', 'node-red'); 74 | try { 75 | const pkg = require(path.join(dirpath, 'package.json')); 76 | return path.join(dirpath, pkg.main); 77 | } catch (ignored) {} 78 | } 79 | // case 4: NR & NRNTH are git repos sat alongside each other 80 | try { 81 | const nrpkg = require("../node-red/package.json"); 82 | return "../node-red/packages/node_modules/node-red" 83 | } catch(ignored) {} 84 | } 85 | 86 | 87 | // As we have prerelease tags in development version, they need stripping off 88 | // before semver will do a sensible comparison with a range. 89 | function checkSemver(localVersion,testRange) { 90 | var parts = localVersion.split("-"); 91 | return semver.satisfies(parts[0],testRange); 92 | } 93 | 94 | class NodeTestHelper extends EventEmitter { 95 | constructor() { 96 | super(); 97 | 98 | this._sandbox = sinon.createSandbox(); 99 | 100 | this._address = '127.0.0.1'; 101 | this._listenPort = 0; // ephemeral 102 | 103 | this.init(); 104 | } 105 | 106 | _initRuntime(requirePath) { 107 | try { 108 | const RED = this._RED = require(requirePath); 109 | // public runtime API 110 | this._log = RED.log; 111 | // access internal Node-RED runtime methods 112 | let prefix = path.dirname(requirePath); 113 | if (checkSemver(RED.version(),"<0.20.0")) { 114 | this._settings = RED.settings; 115 | this._events = RED.events; 116 | this._redNodes = RED.nodes; 117 | this._context = require(path.join(prefix, 'runtime', 'nodes', 'context')); 118 | this._comms = require(path.join(prefix, 'api', 'editor', 'comms')); 119 | this.credentials = require(path.join(prefix, 'runtime', 'nodes', 'credentials')); 120 | // proxy the methods on Node.prototype to both be Sinon spies and asynchronously emit 121 | // information about the latest call 122 | this._NodePrototype = require(path.join(prefix, 'runtime', 'nodes', 'Node')).prototype; 123 | } else { 124 | if (!fs.existsSync(path.join(prefix, '@node-red/runtime/lib/nodes'))) { 125 | // Not in the NR source tree, need to go hunting for the modules.... 126 | if (fs.existsSync(path.join(prefix,'..','node_modules','@node-red/runtime/lib/nodes'))) { 127 | // path/to/node_modules/node-red/lib 128 | // path/to/node_modules/node-red/node_modules/@node-red 129 | prefix = path.resolve(path.join(prefix,"..","node_modules")); 130 | } else if (fs.existsSync(path.join(prefix,'..','..','@node-red/runtime/lib/nodes'))) { 131 | // path/to/node_modules/node-red/lib 132 | // path/to/node_modules/@node-red 133 | prefix = path.resolve(path.join(prefix,"..","..")); 134 | } else { 135 | throw new Error("Cannot find the NR source tree. Path: '"+prefix+"'. Please raise an issue against node-red/node-red-node-test-helper with full details."); 136 | } 137 | } 138 | 139 | this._redNodes = require(path.join(prefix, '@node-red/runtime/lib/nodes')); 140 | this._context = require(path.join(prefix, '@node-red/runtime/lib/nodes/context')); 141 | this._comms = require(path.join(prefix, '@node-red/editor-api/lib/editor/comms')); 142 | this._registryUtil = require(path.join(prefix, '@node-red/registry/lib/util')); 143 | this.credentials = require(path.join(prefix, '@node-red/runtime/lib/nodes/credentials')); 144 | // proxy the methods on Node.prototype to both be Sinon spies and asynchronously emit 145 | // information about the latest call 146 | this._NodePrototype = require(path.join(prefix, '@node-red/runtime/lib/nodes/Node')).prototype; 147 | this._settings = RED.settings; 148 | this._events = RED.runtime.events; 149 | 150 | this._nodeModules = { 151 | 'catch': require(path.join(prefix, '@node-red/nodes/core/common/25-catch.js')), 152 | 'status': require(path.join(prefix, '@node-red/nodes/core/common/25-status.js')), 153 | 'complete': require(path.join(prefix, '@node-red/nodes/core/common/24-complete.js')) 154 | } 155 | 156 | 157 | } 158 | } catch (ignored) { 159 | console.log(ignored); 160 | // ignore, assume init will be called again by a test script supplying the runtime path 161 | } 162 | } 163 | 164 | init(runtimePath, userSettings) { 165 | runtimePath = runtimePath || findRuntimePath(); 166 | if (runtimePath) { 167 | this._initRuntime(runtimePath); 168 | if (userSettings) { 169 | this.settings(userSettings); 170 | } 171 | } 172 | } 173 | 174 | /** 175 | * Merges any userSettings with the defaults returned by `RED.settings`. Each 176 | * invocation of this method will overwrite the previous userSettings to prevent 177 | * unexpected problems in your tests. 178 | * 179 | * This will enable you to replicate your production environment within your tests, 180 | * for example where you're using the `functionGlobalContext` to enable extra node 181 | * modules within your functions. 182 | * @example 183 | * helper.settings({ functionGlobalContext: { os:require('os') } }); 184 | * @param {Object} userSettings - an object containing the runtime settings 185 | * @return {Object} custom userSettings merged with default RED.settings 186 | */ 187 | settings(userSettings) { 188 | if (userSettings) { 189 | // to prevent unexpected problems, always merge with the default RED.settings 190 | this._settings = Object.assign({}, this._RED.settings, userSettings); 191 | } 192 | return this._settings; 193 | } 194 | 195 | async load(testNode, testFlow, testCredentials, cb) { 196 | const log = this._log; 197 | const logSpy = this._logSpy = this._sandbox.spy(log, 'log'); 198 | logSpy.FATAL = log.FATAL; 199 | logSpy.ERROR = log.ERROR; 200 | logSpy.WARN = log.WARN; 201 | logSpy.INFO = log.INFO; 202 | logSpy.DEBUG = log.DEBUG; 203 | logSpy.TRACE = log.TRACE; 204 | logSpy.METRIC = log.METRIC; 205 | 206 | const self = this; 207 | PROXY_METHODS.forEach(methodName => { 208 | const spy = this._sandbox.spy(self._NodePrototype, methodName); 209 | self._NodePrototype[methodName] = new Proxy(spy, { 210 | apply: (target, thisArg, args) => { 211 | const retval = Reflect.apply(target, thisArg, args); 212 | process.nextTick(function(call) { return () => { 213 | self._NodePrototype.emit.call(thisArg, `call:${methodName}`, call); 214 | }}(spy.lastCall)); 215 | return retval; 216 | } 217 | }); 218 | }); 219 | 220 | 221 | if (typeof testCredentials === 'function') { 222 | cb = testCredentials; 223 | testCredentials = {}; 224 | } 225 | const conf = {flows:testFlow,credentials:testCredentials|| {}} 226 | const storage = { 227 | conf: conf, 228 | getFlows: function () { 229 | return Promise.resolve(conf); 230 | }, 231 | saveFlows: function(conf) { 232 | storage.conf = conf; 233 | return Promise.resolve(); 234 | } 235 | }; 236 | 237 | // mock out the runtime plugins api 238 | const plugins = { 239 | registerPlugin () { 240 | return; 241 | }, 242 | getPlugin () { 243 | return; 244 | }, 245 | getPluginsByType () { 246 | return []; 247 | } 248 | } 249 | 250 | // this._settings.logging = {console:{level:'off'}}; 251 | this._settings.available = function() { return false; } 252 | 253 | const redNodes = this._redNodes; 254 | this._httpAdmin = express(); 255 | this._httpAdmin.use(bodyParser.json({limit:'5mb'})); 256 | this._httpAdmin.use(bodyParser.urlencoded({limit:'5mb',extended:true})); 257 | 258 | const mockRuntime = { 259 | nodes: redNodes, 260 | events: this._events, 261 | util: this._RED.util, 262 | settings: this._settings, 263 | storage: storage, 264 | plugins: plugins, 265 | log: this._log, 266 | nodeApp: express(), 267 | adminApp: this._httpAdmin, 268 | library: {register: function() {}}, 269 | get server() { return self._server } 270 | } 271 | redNodes.init(mockRuntime); 272 | redNodes.registerType("helper", function (n) { 273 | redNodes.createNode(this, n); 274 | }); 275 | 276 | var red; 277 | if (this._registryUtil) { 278 | this._registryUtil.init(mockRuntime); 279 | red = this._registryUtil.createNodeApi({}); 280 | red._ = v=>v; 281 | red.settings = this._settings; 282 | } else { 283 | red = { 284 | _: v => v 285 | }; 286 | Object.keys(this._RED).filter(prop => !/^(init|start|stop)$/.test(prop)) 287 | .forEach(prop => { 288 | const propDescriptor = Object.getOwnPropertyDescriptor(this._RED, prop); 289 | Object.defineProperty(red, prop, propDescriptor); 290 | }); 291 | } 292 | const initPromises = [] 293 | 294 | let preloadedCoreModules = new Set(); 295 | testFlow.forEach(n => { 296 | if (this._nodeModules.hasOwnProperty(n.type)) { 297 | // Go find the 'real' core node module and load it... 298 | const result = this._nodeModules[n.type](red); 299 | if (result?.then) { 300 | initPromises.push(result) 301 | } 302 | preloadedCoreModules.add(this._nodeModules[n.type]); 303 | } 304 | }) 305 | 306 | if (!Array.isArray(testNode)) { 307 | testNode = [testNode]; 308 | } 309 | testNode.forEach(fn => { 310 | if (!preloadedCoreModules.has(fn)) { 311 | const result = fn(red); 312 | if (result?.then) { 313 | initPromises.push(result) 314 | } 315 | } 316 | }); 317 | try { 318 | await Promise.all(initPromises); 319 | await redNodes.loadFlows(); 320 | await redNodes.startFlows(); 321 | should.deepEqual(testFlow, redNodes.getFlows().flows); 322 | if (cb) cb(); 323 | } catch (error) { 324 | if (cb) cb(error); 325 | else throw error; 326 | } 327 | } 328 | 329 | unload() { 330 | // TODO: any other state to remove between tests? 331 | this._redNodes.clearRegistry(); 332 | this._logSpy && this._logSpy.restore(); 333 | this._sandbox.restore(); 334 | 335 | // internal API 336 | this._context.clean({allNodes:[]}); 337 | return this._redNodes.stopFlows() 338 | } 339 | 340 | /** 341 | * Returns a Node by id. 342 | * @param {string} id - Node ID 343 | * @returns {Node} 344 | */ 345 | getNode(id) { 346 | return this._redNodes.getNode(id); 347 | } 348 | 349 | clearFlows() { 350 | return this._redNodes.stopFlows(); 351 | } 352 | 353 | /** 354 | * Update flows 355 | * @param {object|object[]} testFlow Flow data to test a node 356 | * @param {"full"|"flows"|"nodes"} type The type of deploy mode "full", "flows" or "nodes" (defaults to "full") 357 | * @param {object} [testCredentials] Optional node credentials 358 | * @param {function} [cb] Optional callback (not required when called with await) 359 | * @returns {Promise} 360 | */ 361 | async setFlows(testFlow, type, testCredentials, cb) { 362 | const helper = this; 363 | if (typeof testCredentials === 'string' ) { 364 | cb = testCredentials; 365 | testCredentials = {}; 366 | } 367 | if(!type || typeof type != "string") { 368 | type = "full" 369 | } 370 | async function waitStarted() { 371 | return new Promise((resolve, reject) => { 372 | let timeover = setTimeout(() => { 373 | if (timeover) { 374 | timeover = null 375 | reject(Error("timeout waiting event")) 376 | } 377 | }, 300); 378 | function hander() { 379 | clearTimeout(timeover) 380 | helper._events.off('flows:started', hander) 381 | if (timeover) { 382 | timeover = null 383 | resolve() 384 | } 385 | } 386 | helper._events.on('flows:started', hander); // call resolve when its done 387 | }); 388 | } 389 | try { 390 | await this._redNodes.setFlows(testFlow, testCredentials || {}, type); 391 | await waitStarted(); 392 | 393 | if (cb) cb(); 394 | } catch (error) { 395 | if (cb) cb(error); 396 | else throw error; 397 | } 398 | } 399 | 400 | request() { 401 | return request(this._httpAdmin); 402 | } 403 | 404 | async startServer(cb) { 405 | try { 406 | await new Promise((resolve, reject) => { 407 | this._app = express(); 408 | const server = stoppable( 409 | http.createServer((req, res) => this._app(req, res)), 410 | 0 411 | ); 412 | 413 | this._RED.init(server, { 414 | logging: { console: { level: 'off' } }, 415 | }); 416 | 417 | server.listen(this._listenPort, this._address); 418 | 419 | server.on('listening', () => { 420 | this._port = server.address().port; 421 | this._comms.start(); 422 | this._server = server; 423 | resolve(); 424 | }); 425 | 426 | server.on('error', reject); 427 | }); 428 | 429 | if (cb) cb(); 430 | } catch (error) { 431 | if (cb) cb(error); 432 | else throw error; 433 | } 434 | } 435 | 436 | async stopServer(cb) { 437 | try { 438 | if (this._server) { 439 | await new Promise((resolve, reject) => { 440 | this._comms.stop(); 441 | 442 | this._server.stop((error) => { 443 | if (error) reject(error); 444 | else resolve(); 445 | }); 446 | }); 447 | } 448 | 449 | if (cb) cb(); 450 | } catch (error) { 451 | if (cb) cb(error); 452 | else throw error; 453 | } 454 | } 455 | 456 | url() { 457 | return `http://${this._address}:${this._port}`; 458 | } 459 | 460 | log() { 461 | return this._logSpy; 462 | } 463 | } 464 | 465 | module.exports = new NodeTestHelper(); 466 | module.exports.NodeTestHelper = NodeTestHelper; 467 | -------------------------------------------------------------------------------- /examples/function_spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright JS Foundation and other contributors, http://js.foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var should = require("should"); 18 | var helper = require("../index.js"); 19 | helper.init(require.resolve('node-red')); 20 | 21 | var functionNode = require("./nodes/80-function.js"); 22 | 23 | describe('function node', function() { 24 | 25 | before(function(done) { 26 | helper.startServer(done); 27 | }); 28 | 29 | after(function(done) { 30 | helper.stopServer(done); 31 | }); 32 | 33 | afterEach(function() { 34 | helper.unload(); 35 | }); 36 | 37 | it('should be loaded', function(done) { 38 | var flow = [{id:"n1", type:"function", name: "function" }]; 39 | helper.load(functionNode, flow, function() { 40 | var n1 = helper.getNode("n1"); 41 | n1.should.have.property('name', 'function'); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('should send returned message', function(done) { 47 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"return msg;"}, 48 | {id:"n2", type:"helper"}]; 49 | helper.load(functionNode, flow, function() { 50 | var n1 = helper.getNode("n1"); 51 | var n2 = helper.getNode("n2"); 52 | n2.on("input", function(msg) { 53 | msg.should.have.property('topic', 'bar'); 54 | msg.should.have.property('payload', 'foo'); 55 | done(); 56 | }); 57 | n1.receive({payload:"foo",topic: "bar"}); 58 | }); 59 | }); 60 | 61 | it('should send returned message using send()', function(done) { 62 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"node.send(msg);"}, 63 | {id:"n2", type:"helper"}]; 64 | helper.load(functionNode, flow, function() { 65 | var n1 = helper.getNode("n1"); 66 | var n2 = helper.getNode("n2"); 67 | n2.on("input", function(msg) { 68 | msg.should.have.property('topic', 'bar'); 69 | msg.should.have.property('payload', 'foo'); 70 | done(); 71 | }); 72 | n1.receive({payload:"foo",topic: "bar"}); 73 | }); 74 | }); 75 | 76 | it('should pass through _topic', function(done) { 77 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"return msg;"}, 78 | {id:"n2", type:"helper"}]; 79 | helper.load(functionNode, flow, function() { 80 | var n1 = helper.getNode("n1"); 81 | var n2 = helper.getNode("n2"); 82 | n2.on("input", function(msg) { 83 | msg.should.have.property('topic', 'bar'); 84 | msg.should.have.property('payload', 'foo'); 85 | msg.should.have.property('_topic', 'baz'); 86 | done(); 87 | }); 88 | n1.receive({payload:"foo",topic: "bar", _topic: "baz"}); 89 | }); 90 | }); 91 | 92 | it('should send to multiple outputs', function(done) { 93 | var flow = [{id:"n1",type:"function",wires:[["n2"],["n3"]], 94 | func:"return [{payload: '1'},{payload: '2'}];"}, 95 | {id:"n2", type:"helper"}, {id:"n3", type:"helper"} ]; 96 | helper.load(functionNode, flow, function() { 97 | var n1 = helper.getNode("n1"); 98 | var n2 = helper.getNode("n2"); 99 | var n3 = helper.getNode("n3"); 100 | var count = 0; 101 | n2.on("input", function(msg) { 102 | should(msg).have.property('payload', '1'); 103 | count++; 104 | if (count == 2) { 105 | done(); 106 | } 107 | }); 108 | n3.on("input", function(msg) { 109 | should(msg).have.property('payload', '2'); 110 | count++; 111 | if (count == 2) { 112 | done(); 113 | } 114 | }); 115 | n1.receive({payload:"foo",topic: "bar"}); 116 | }); 117 | }); 118 | 119 | it('should send to multiple messages', function(done) { 120 | var flow = [{id:"n1",type:"function",wires:[["n2"]], 121 | func:"return [[{payload: 1},{payload: 2}]];"}, 122 | {id:"n2", type:"helper"} ]; 123 | helper.load(functionNode, flow, function() { 124 | var n1 = helper.getNode("n1"); 125 | var n2 = helper.getNode("n2"); 126 | var count = 0; 127 | n2.on("input", function(msg) { 128 | count++; 129 | try { 130 | should(msg).have.property('payload', count); 131 | should(msg).have.property('_msgid', 1234); 132 | if (count == 2) { 133 | done(); 134 | } 135 | } catch(err) { 136 | done(err); 137 | } 138 | }); 139 | n1.receive({payload:"foo", topic: "bar",_msgid:1234}); 140 | }); 141 | }); 142 | 143 | it('should allow input to be discarded by returning null', function(done) { 144 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"return null"}, 145 | {id:"n2", type:"helper"}]; 146 | helper.load(functionNode, flow, function() { 147 | var n1 = helper.getNode("n1"); 148 | var n2 = helper.getNode("n2"); 149 | setTimeout(function() { 150 | done(); 151 | }, 20); 152 | n2.on("input", function(msg) { 153 | should.fail(null,null,"unexpected message"); 154 | }); 155 | n1.receive({payload:"foo",topic: "bar"}); 156 | }); 157 | }); 158 | 159 | it('should handle null amongst valid messages', function(done) { 160 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"return [[msg,null,msg],null]"}, 161 | {id:"n2", type:"helper"}, 162 | {id:"n3", type:"helper"}]; 163 | helper.load(functionNode, flow, function() { 164 | var n1 = helper.getNode("n1"); 165 | var n2 = helper.getNode("n2"); 166 | var n3 = helper.getNode("n3"); 167 | var n2MsgCount = 0; 168 | var n3MsgCount = 0; 169 | n2.on("input", function(msg) { 170 | n2MsgCount++; 171 | }); 172 | n3.on("input", function(msg) { 173 | n3MsgCount++; 174 | }); 175 | n1.receive({payload:"foo",topic: "bar"}); 176 | setTimeout(function() { 177 | n2MsgCount.should.equal(2); 178 | n3MsgCount.should.equal(0); 179 | done(); 180 | },20); 181 | }); 182 | }); 183 | 184 | it('should get keys in global context', function(done) { 185 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"msg.payload=global.keys();return msg;"}, 186 | {id:"n2", type:"helper"}]; 187 | helper.load(functionNode, flow, function() { 188 | var n1 = helper.getNode("n1"); 189 | var n2 = helper.getNode("n2"); 190 | n1.context().global.set("count","0"); 191 | n2.on("input", function(msg) { 192 | msg.should.have.property('topic', 'bar'); 193 | msg.should.have.property('payload', ['count']); 194 | done(); 195 | }); 196 | n1.receive({payload:"foo",topic: "bar"}); 197 | }); 198 | }); 199 | 200 | it('should access functionGlobalContext set via herlp settings()', function(done) { 201 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"msg.payload=global.get('foo');return msg;"}, 202 | {id:"n2", type:"helper"}]; 203 | helper.settings({ 204 | functionGlobalContext: { 205 | foo: (function() { 206 | return 'bar'; 207 | })(), 208 | }, 209 | }); 210 | helper.load(functionNode, flow, function() { 211 | var n1 = helper.getNode("n1"); 212 | var n2 = helper.getNode("n2"); 213 | n2.on("input", function(msg) { 214 | msg.should.have.property('payload', 'bar'); 215 | done(); 216 | }); 217 | n1.receive({payload:"replaceme"}); 218 | }); 219 | helper.settings({}); 220 | }); 221 | 222 | function testNonObjectMessage(functionText,done) { 223 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:functionText}, 224 | {id:"n2", type:"helper"}]; 225 | helper.load(functionNode, flow, function() { 226 | var n1 = helper.getNode("n1"); 227 | var n2 = helper.getNode("n2"); 228 | var n2MsgCount = 0; 229 | n2.on("input", function(msg) { 230 | n2MsgCount++; 231 | }); 232 | n1.receive({}); 233 | setTimeout(function() { 234 | try { 235 | n2MsgCount.should.equal(0); 236 | var logEvents = helper.log().args.filter(function(evt) { 237 | return evt[0].type == "function"; 238 | }); 239 | logEvents.should.have.length(1); 240 | var msg = logEvents[0][0]; 241 | msg.should.have.property('level', helper.log().ERROR); 242 | msg.should.have.property('id', 'n1'); 243 | msg.should.have.property('type', 'function'); 244 | msg.should.have.property('msg', 'function.error.non-message-returned'); 245 | done(); 246 | } catch(err) { 247 | done(err); 248 | } 249 | },20); 250 | }); 251 | } 252 | it('should drop and log non-object message types - string', function(done) { 253 | testNonObjectMessage('return "foo"', done) 254 | }); 255 | it('should drop and log non-object message types - buffer', function(done) { 256 | testNonObjectMessage('return new Buffer("hello")', done) 257 | }); 258 | it('should drop and log non-object message types - array', function(done) { 259 | testNonObjectMessage('return [[[1,2,3]]]', done) 260 | }); 261 | it('should drop and log non-object message types - boolean', function(done) { 262 | testNonObjectMessage('return true', done) 263 | }); 264 | it('should drop and log non-object message types - number', function(done) { 265 | testNonObjectMessage('return 123', done) 266 | }); 267 | 268 | it('should handle and log script error', function(done) { 269 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"retunr"}]; 270 | helper.load(functionNode, flow, function() { 271 | var n1 = helper.getNode("n1"); 272 | n1.receive({payload:"foo",topic: "bar"}); 273 | try { 274 | helper.log().called.should.be.true(); 275 | var logEvents = helper.log().args.filter(function(evt) { 276 | return evt[0].type == "function"; 277 | }); 278 | logEvents.should.have.length(1); 279 | var msg = logEvents[0][0]; 280 | msg.should.have.property('level', helper.log().ERROR); 281 | msg.should.have.property('id', 'n1'); 282 | msg.should.have.property('type', 'function'); 283 | msg.should.have.property('msg', 'ReferenceError: retunr is not defined (line 1, col 1)'); 284 | done(); 285 | } catch(err) { 286 | done(err); 287 | } 288 | }); 289 | }); 290 | 291 | it('should handle node.on()', function(done) { 292 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"node.on('close',function(){node.log('closed')});"}]; 293 | helper.load(functionNode, flow, function() { 294 | var n1 = helper.getNode("n1"); 295 | n1.receive({payload:"foo",topic: "bar"}); 296 | helper.getNode("n1").close(); 297 | try { 298 | helper.log().called.should.be.true(); 299 | var logEvents = helper.log().args.filter(function(evt) { 300 | return evt[0].type == "function"; 301 | }); 302 | logEvents.should.have.length(1); 303 | var msg = logEvents[0][0]; 304 | msg.should.have.property('level', helper.log().INFO); 305 | msg.should.have.property('id', 'n1'); 306 | msg.should.have.property('type', 'function'); 307 | msg.should.have.property('msg', 'closed'); 308 | done(); 309 | } catch(err) { 310 | done(err); 311 | } 312 | }); 313 | }); 314 | 315 | it('should set node context', function(done) { 316 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"context.set('count','0');return msg;"}, 317 | {id:"n2", type:"helper"}]; 318 | helper.load(functionNode, flow, function() { 319 | var n1 = helper.getNode("n1"); 320 | var n2 = helper.getNode("n2"); 321 | n2.on("input", function(msg) { 322 | msg.should.have.property('topic', 'bar'); 323 | msg.should.have.property('payload', 'foo'); 324 | n1.context().get("count").should.equal("0"); 325 | done(); 326 | }); 327 | n1.receive({payload:"foo",topic: "bar"}); 328 | }); 329 | }); 330 | 331 | it('should get node context', function(done) { 332 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"msg.payload=context.get('count');return msg;"}, 333 | {id:"n2", type:"helper"}]; 334 | helper.load(functionNode, flow, function() { 335 | var n1 = helper.getNode("n1"); 336 | var n2 = helper.getNode("n2"); 337 | n1.context().set("count","0"); 338 | n2.on("input", function(msg) { 339 | msg.should.have.property('topic', 'bar'); 340 | msg.should.have.property('payload', '0'); 341 | done(); 342 | }); 343 | n1.receive({payload:"foo",topic: "bar"}); 344 | }); 345 | }); 346 | 347 | it('should get keys in node context', function(done) { 348 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"msg.payload=context.keys();return msg;"}, 349 | {id:"n2", type:"helper"}]; 350 | helper.load(functionNode, flow, function() { 351 | var n1 = helper.getNode("n1"); 352 | var n2 = helper.getNode("n2"); 353 | n1.context().set("count","0"); 354 | n2.on("input", function(msg) { 355 | msg.should.have.property('topic', 'bar'); 356 | msg.should.have.property('payload', ['count']); 357 | done(); 358 | }); 359 | n1.receive({payload:"foo",topic: "bar"}); 360 | }); 361 | }); 362 | 363 | it('should set flow context', function(done) { 364 | var flow = [{id:"n1",type:"function",z:"flowA",wires:[["n2"]],func:"flow.set('count','0');return msg;"}, 365 | {id:"n2", type:"helper",z:"flowA"}]; 366 | helper.load(functionNode, flow, function() { 367 | var n1 = helper.getNode("n1"); 368 | var n2 = helper.getNode("n2"); 369 | n2.on("input", function(msg) { 370 | msg.should.have.property('topic', 'bar'); 371 | msg.should.have.property('payload', 'foo'); 372 | n2.context().flow.get("count").should.equal("0"); 373 | done(); 374 | }); 375 | n1.receive({payload:"foo",topic: "bar"}); 376 | }); 377 | }); 378 | 379 | it('should get flow context', function(done) { 380 | var flow = [{id:"n1",type:"function",z:"flowA",wires:[["n2"]],func:"msg.payload=flow.get('count');return msg;"}, 381 | {id:"n2", type:"helper",z:"flowA"}]; 382 | helper.load(functionNode, flow, function() { 383 | var n1 = helper.getNode("n1"); 384 | var n2 = helper.getNode("n2"); 385 | n1.context().flow.set("count","0"); 386 | n2.on("input", function(msg) { 387 | msg.should.have.property('topic', 'bar'); 388 | msg.should.have.property('payload', '0'); 389 | done(); 390 | }); 391 | n1.receive({payload:"foo",topic: "bar"}); 392 | }); 393 | }); 394 | 395 | it('should get flow context', function(done) { 396 | var flow = [{id:"n1",type:"function",z:"flowA",wires:[["n2"]],func:"msg.payload=context.flow.get('count');return msg;"}, 397 | {id:"n2", type:"helper",z:"flowA"}]; 398 | helper.load(functionNode, flow, function() { 399 | var n1 = helper.getNode("n1"); 400 | var n2 = helper.getNode("n2"); 401 | n1.context().flow.set("count","0"); 402 | n2.on("input", function(msg) { 403 | msg.should.have.property('topic', 'bar'); 404 | msg.should.have.property('payload', '0'); 405 | done(); 406 | }); 407 | n1.receive({payload:"foo",topic: "bar"}); 408 | }); 409 | }); 410 | 411 | it('should get keys in flow context', function(done) { 412 | var flow = [{id:"n1",type:"function",z:"flowA",wires:[["n2"]],func:"msg.payload=flow.keys();return msg;"}, 413 | {id:"n2", type:"helper",z:"flowA"}]; 414 | helper.load(functionNode, flow, function() { 415 | var n1 = helper.getNode("n1"); 416 | var n2 = helper.getNode("n2"); 417 | n1.context().flow.set("count","0"); 418 | n2.on("input", function(msg) { 419 | msg.should.have.property('topic', 'bar'); 420 | msg.should.have.property('payload', ['count']); 421 | done(); 422 | }); 423 | n1.receive({payload:"foo",topic: "bar"}); 424 | }); 425 | }); 426 | 427 | it('should set global context', function(done) { 428 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"global.set('count','0');return msg;"}, 429 | {id:"n2", type:"helper"}]; 430 | helper.load(functionNode, flow, function() { 431 | var n1 = helper.getNode("n1"); 432 | var n2 = helper.getNode("n2"); 433 | n2.on("input", function(msg) { 434 | msg.should.have.property('topic', 'bar'); 435 | msg.should.have.property('payload', 'foo'); 436 | n2.context().global.get("count").should.equal("0"); 437 | done(); 438 | }); 439 | n1.receive({payload:"foo",topic: "bar"}); 440 | }); 441 | }); 442 | 443 | it('should get global context', function(done) { 444 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"msg.payload=global.get('count');return msg;"}, 445 | {id:"n2", type:"helper"}]; 446 | helper.load(functionNode, flow, function() { 447 | var n1 = helper.getNode("n1"); 448 | var n2 = helper.getNode("n2"); 449 | n1.context().global.set("count","0"); 450 | n2.on("input", function(msg) { 451 | msg.should.have.property('topic', 'bar'); 452 | msg.should.have.property('payload', '0'); 453 | done(); 454 | }); 455 | n1.receive({payload:"foo",topic: "bar"}); 456 | }); 457 | }); 458 | 459 | it('should get global context', function(done) { 460 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"msg.payload=context.global.get('count');return msg;"}, 461 | {id:"n2", type:"helper"}]; 462 | helper.load(functionNode, flow, function() { 463 | var n1 = helper.getNode("n1"); 464 | var n2 = helper.getNode("n2"); 465 | n1.context().global.set("count","0"); 466 | n2.on("input", function(msg) { 467 | msg.should.have.property('topic', 'bar'); 468 | msg.should.have.property('payload', '0'); 469 | done(); 470 | }); 471 | n1.receive({payload:"foo",topic: "bar"}); 472 | }); 473 | }); 474 | 475 | it('should handle setTimeout()', function(done) { 476 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"setTimeout(function(){node.send(msg);},1000);"}, 477 | {id:"n2", type:"helper"}]; 478 | helper.load(functionNode, flow, function() { 479 | var n1 = helper.getNode("n1"); 480 | var n2 = helper.getNode("n2"); 481 | n2.on("input", function(msg) { 482 | var endTime = process.hrtime(startTime); 483 | var nanoTime = endTime[0] * 1000000000 + endTime[1]; 484 | msg.should.have.property('topic', 'bar'); 485 | msg.should.have.property('payload', 'foo'); 486 | if (900000000 < nanoTime && nanoTime < 1100000000) { 487 | done(); 488 | } else { 489 | try { 490 | should.fail(null, null, "Delayed time was not between 900 and 1100 ms"); 491 | } catch (err) { 492 | done(err); 493 | } 494 | } 495 | }); 496 | var startTime = process.hrtime(); 497 | n1.receive({payload:"foo",topic: "bar"}); 498 | }); 499 | }); 500 | 501 | it('should handle setInterval()', function(done) { 502 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"setInterval(function(){node.send(msg);},100);"}, 503 | {id:"n2", type:"helper"}]; 504 | helper.load(functionNode, flow, function() { 505 | var n1 = helper.getNode("n1"); 506 | var n2 = helper.getNode("n2"); 507 | var count = 0; 508 | n2.on("input", function(msg) { 509 | msg.should.have.property('topic', 'bar'); 510 | msg.should.have.property('payload', 'foo'); 511 | count++; 512 | if (count > 2) { 513 | done(); 514 | } 515 | }); 516 | n1.receive({payload:"foo",topic: "bar"}); 517 | }); 518 | }); 519 | 520 | it('should handle clearInterval()', function(done) { 521 | var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"var id=setInterval(null,100);setTimeout(function(){clearInterval(id);node.send(msg);},1000);"}, 522 | {id:"n2", type:"helper"}]; 523 | helper.load(functionNode, flow, function() { 524 | var n1 = helper.getNode("n1"); 525 | var n2 = helper.getNode("n2"); 526 | n2.on("input", function(msg) { 527 | msg.should.have.property('topic', 'bar'); 528 | msg.should.have.property('payload', 'foo'); 529 | done(); 530 | }); 531 | n1.receive({payload:"foo",topic: "bar"}); 532 | }); 533 | }); 534 | 535 | describe('Logger', function () { 536 | it('should log an Info Message', function (done) { 537 | var flow = [{id: "n1", type: "function", wires: [["n2"]], func: "node.log('test');"}]; 538 | helper.load(functionNode, flow, function () { 539 | var n1 = helper.getNode("n1"); 540 | n1.receive({payload: "foo", topic: "bar"}); 541 | try { 542 | helper.log().called.should.be.true(); 543 | var logEvents = helper.log().args.filter(function (evt) { 544 | return evt[0].type == "function"; 545 | }); 546 | logEvents.should.have.length(1); 547 | var msg = logEvents[0][0]; 548 | msg.should.have.property('level', helper.log().INFO); 549 | msg.should.have.property('id', 'n1'); 550 | msg.should.have.property('type', 'function'); 551 | msg.should.have.property('msg', 'test'); 552 | done(); 553 | } catch (err) { 554 | done(err); 555 | } 556 | }); 557 | }); 558 | it('should log a Warning Message', function (done) { 559 | var flow = [{id: "n1", type: "function", wires: [["n2"]], func: "node.warn('test');"}]; 560 | helper.load(functionNode, flow, function () { 561 | var n1 = helper.getNode("n1"); 562 | n1.receive({payload: "foo", topic: "bar"}); 563 | try { 564 | helper.log().called.should.be.true(); 565 | var logEvents = helper.log().args.filter(function (evt) { 566 | return evt[0].type == "function"; 567 | }); 568 | logEvents.should.have.length(1); 569 | var msg = logEvents[0][0]; 570 | msg.should.have.property('level', helper.log().WARN); 571 | msg.should.have.property('id', 'n1'); 572 | msg.should.have.property('type', 'function'); 573 | msg.should.have.property('msg', 'test'); 574 | done(); 575 | } catch (err) { 576 | done(err); 577 | } 578 | }); 579 | }); 580 | it('should log an Error Message', function (done) { 581 | var flow = [{id: "n1", type: "function", wires: [["n2"]], func: "node.error('test');"}]; 582 | helper.load(functionNode, flow, function () { 583 | var n1 = helper.getNode("n1"); 584 | n1.receive({payload: "foo", topic: "bar"}); 585 | try { 586 | helper.log().called.should.be.true(); 587 | var logEvents = helper.log().args.filter(function (evt) { 588 | return evt[0].type == "function"; 589 | }); 590 | logEvents.should.have.length(1); 591 | var msg = logEvents[0][0]; 592 | msg.should.have.property('level', helper.log().ERROR); 593 | msg.should.have.property('id', 'n1'); 594 | msg.should.have.property('type', 'function'); 595 | msg.should.have.property('msg', 'test'); 596 | done(); 597 | } catch (err) { 598 | done(err); 599 | } 600 | }); 601 | }); 602 | }); 603 | 604 | }); 605 | --------------------------------------------------------------------------------