├── .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 |
--------------------------------------------------------------------------------