1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Add node done API

This commit is contained in:
Nick O'Leary 2019-07-08 23:23:33 +01:00
parent f0a51bafbe
commit 3b5ea0f15f
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
17 changed files with 1039 additions and 498 deletions

View File

@ -0,0 +1,136 @@
<script type="text/x-red" data-template-name="complete">
<div class="form-row node-input-target-row">
<button id="node-input-complete-target-select" class="red-ui-button" data-i18n="common.label.selectNodes"></button>
</div>
<div class="form-row node-input-target-row node-input-target-list-row" style="min-height: 100px">
<div id="node-input-complete-target-container-div"></div>
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
<input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
</div>
</script>
<script type="text/javascript">
RED.nodes.registerType('complete',{
category: 'input',
color:"#c0edc0",
defaults: {
name: {value:""},
scope: {value:[]},
uncaught: {value:false}
},
inputs:0,
outputs:1,
icon: "alert.svg",
label: function() {
if (this.name) {
return this.name;
}
return this._("complete.completeNodes",{number:this.scope.length});
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
var node = this;
var scope = node.scope || [];
this._resize = function() {
var rows = $("#dialog-form>div:not(.node-input-target-list-row)");
var height = $("#dialog-form").height();
for (var i=0;i<rows.length;i++) {
height -= $(rows[i]).outerHeight(true);
}
var editorRow = $("#dialog-form>div.node-input-target-list-row");
editorRow.css("height",height+"px");
};
var dirList = $("#node-input-complete-target-container-div").css({width: "100%", height: "100%"})
.treeList({multi:true}).on("treelistitemmouseover", function(e, item) {
item.node.highlighted = true;
item.node.dirty = true;
RED.view.redraw();
}).on("treelistitemmouseout", function(e, item) {
item.node.highlighted = false;
item.node.dirty = true;
RED.view.redraw();
})
var candidateNodes = RED.nodes.filterNodes({z:node.z});
var allChecked = true;
var items = [];
var nodeItemMap = {};
candidateNodes.forEach(function(n) {
if (n.id === node.id) {
return;
}
var isChecked = scope.indexOf(n.id) !== -1;
allChecked = allChecked && isChecked;
var nodeDef = RED.nodes.getType(n.type);
var label;
var sublabel;
if (nodeDef) {
var l = nodeDef.label;
label = (typeof l === "function" ? l.call(n) : l)||"";
sublabel = n.type;
if (sublabel.indexOf("subflow:") === 0) {
var subflowId = sublabel.substring(8);
var subflow = RED.nodes.subflow(subflowId);
sublabel = "subflow : "+subflow.name;
}
}
if (!nodeDef || !label) {
label = n.type;
}
nodeItemMap[n.id] = {
node: n,
label: label,
sublabel: sublabel,
selected: isChecked
};
items.push(nodeItemMap[n.id]);
});
dirList.treeList('data',items);
$("#node-input-complete-target-select").on("click", function(e) {
e.preventDefault();
var preselected = dirList.treeList('selected').map(function(n) {return n.node.id});
RED.tray.hide();
RED.view.selectNodes({
selected: preselected,
onselect: function(selection) {
RED.tray.show();
var newlySelected = {};
selection.forEach(function(n) {
newlySelected[n.id] = true;
if (nodeItemMap[n.id]) {
nodeItemMap[n.id].treeList.select(true);
}
})
preselected.forEach(function(id) {
if (!newlySelected[id]) {
nodeItemMap[id].treeList.select(false);
}
})
},
oncancel: function() {
RED.tray.show();
},
filter: function(n) {
return n.id !== node.id;
}
});
})
},
oneditsave: function() {
this.scope = $("#node-input-complete-target-container-div").treeList('selected').map(function(i) { return i.node.id})
},
oneditresize: function(size) {
this._resize();
}
});
</script>

View File

@ -0,0 +1,30 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
module.exports = function(RED) {
"use strict";
function CompleteNode(n) {
RED.nodes.createNode(this,n);
var node = this;
this.scope = n.scope;
this.on("input",function(msg) {
this.send(msg);
});
}
RED.nodes.registerType("complete",CompleteNode);
}

View File

@ -81,7 +81,7 @@ module.exports = function(RED) {
}
}
this.on("input", function(msg) {
this.on("input", function(msg, done) {
if (this.complete === "true") {
// debug complete msg object
if (this.console === "true") {
@ -90,13 +90,14 @@ module.exports = function(RED) {
if (this.active && this.tosidebar) {
sendDebug({id:node.id, name:node.name, topic:msg.topic, msg:msg, _path:msg._path});
}
done();
} else {
prepareValue(msg,function(err,msg) {
prepareValue(msg,function(err,debugMsg) {
if (err) {
node.error(err);
return;
}
var output = msg.msg;
var output = debugMsg.msg;
if (node.console === "true") {
if (typeof output === "string") {
node.log((output.indexOf("\n") !== -1 ? "\n" : "") + output);
@ -114,9 +115,10 @@ module.exports = function(RED) {
}
if (node.active) {
if (node.tosidebar == true) {
sendDebug(msg);
sendDebug(debugMsg);
}
}
done();
});
}
})

View File

@ -328,13 +328,14 @@ module.exports = function(RED) {
}
if (valid) {
this.on('input', function(msg) {
this.on('input', function(msg, done) {
applyRules(msg, 0, (err,msg) => {
if (err) {
node.error(err,msg);
} else if (msg) {
node.send(msg);
}
done();
})
});
}

View File

@ -0,0 +1,29 @@
<!--
Copyright JS Foundation and other contributors, http://js.foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script type="text/x-red" data-help-name="complete">
<p>Trigger a flow when another node completes its handling of a message.</p>
<h3>Details</h3>
<p>If a node tells the runtime when it has finished handling a message,
this node can be used to trigger a second flow.</p>
<p>For example, this can be used alongside a node with no output port,
such as the Email sending node, to continue the flow.</p>
<p>This node must be configured to handle the event for selected nodes in the
flow. Unlike the Catch node, it does not provide a 'handle all' mode automatically
applies to all nodes in the flow.</p>
<p>Not all nodes will trigger this event - it will depend on whether they
have been implemented to support this feature as introduced in Node-RED 1.0.</p>
</script>

View File

@ -112,6 +112,9 @@
"selected": "selected nodes"
}
},
"complete": {
"completeNodes": "complete: __number__"
},
"debug": {
"output": "Output",
"none": "None",

View File

@ -27,6 +27,8 @@ function Node(n) {
this.type = n.type;
this.z = n.z;
this._closeCallbacks = [];
this._inputCallback = null;
this._inputCallbacks = null;
if (n.name) {
this.name = n.name;
@ -43,6 +45,10 @@ function Node(n) {
// as part of its constructure - config._flow will overwrite this._flow
// which we can tolerate as they are the same object.
Object.defineProperty(this,'_flow', {value: n._flow, enumerable: false, writable: true })
this._asyncDelivery = n._flow.asyncMessageDelivery;
}
if (this._asyncDelivery === undefined) {
this._asyncDelivery = true;
}
this.updateWires(n.wires);
}
@ -79,17 +85,95 @@ Node.prototype.context = function() {
return this._context;
}
Node.prototype._on = Node.prototype.on;
Node.prototype._complete = function(msg,error) {
if (error) {
// For now, delegate this to this.error
// But at some point, the timeout handling will need to know about
// this as well.
this.error(error,msg);
} else {
this._flow.handleComplete(this,msg);
}
}
Node.prototype._on = Node.prototype.on;
Node.prototype.on = function(event, callback) {
var node = this;
if (event == "close") {
this._closeCallbacks.push(callback);
} else if (event === "input") {
if (this._inputCallback) {
this._inputCallbacks = [this._inputCallback, callback];
this._inputCallback = null;
} else if (this._inputCallbacks) {
this._inputCallbacks.push(callback);
} else {
this._inputCallback = callback;
}
} else {
this._on(event, callback);
}
};
Node.prototype._emit = Node.prototype.emit;
Node.prototype.emit = function(event,arg) {
var node = this;
if (event === "input") {
// When Pluggable Message Routing arrives, this will be called from
// that and will already be sync/async depending on the router.
if (this._asyncDelivery) {
setImmediate(function() {
node._emitInput(arg);
});
} else {
this._emitInput(arg);
}
} else {
this._emit(event,arg);
}
}
Node.prototype._emitInput = function(arg) {
var node = this;
if (node._inputCallback) {
try {
node._inputCallback(arg,function(err) { node._complete(arg,err); });
} catch(err) {
node.error(err,arg);
}
} else if (node._inputCallbacks) {
var c = node._inputCallbacks.length;
for (var i=0;i<c;i++) {
var cb = node._inputCallbacks[i];
if (cb.length === 2) {
c++;
}
try {
node._inputCallbacks[i](arg,function(err) {
c--;
if (c === 0) {
node._complete(arg,err);
}
});
} catch(err) {
node.error(err,msg);
}
}
}
}
Node.prototype._removeAllListeners = Node.prototype.removeAllListeners;
Node.prototype.removeAllListeners = function(name) {
if (name === "input") {
this._inputCallback = null;
this._inputCallbacks = null;
} else if (name === "close") {
this._closeCallbacks = [];
} else {
this._removeAllListeners(name);
}
}
Node.prototype.close = function(removed) {
//console.log(this.type,this.id,removed);
var promises = [];
@ -233,11 +317,7 @@ Node.prototype.receive = function(msg) {
msg._msgid = redUtil.generateId();
}
this.metric("receive",msg);
try {
this.emit("input", msg);
} catch(err) {
this.error(err,msg);
}
this.emit("input",msg);
};
function log_helper(self, level, msg) {

View File

@ -23,6 +23,7 @@ var Subflow;
var Log;
var nodeCloseTimeout = 15000;
var asyncMessageDelivery = true;
/**
* This class represents a flow within the runtime. It is responsible for
@ -125,6 +126,7 @@ class Flow {
var id;
this.catchNodes = [];
this.statusNodes = [];
this.completeNodeMap = {};
var configNodes = Object.keys(this.flow.configs);
var configNodeAttempts = {};
@ -228,7 +230,7 @@ class Flow {
this.trace(" id | type | alias");
this.trace("------------------|--------------|-----------------");
}
// Build the map of catch/status nodes.
// Build the map of catch/status/complete nodes.
for (id in this.activeNodes) {
if (this.activeNodes.hasOwnProperty(id)) {
node = this.activeNodes[id];
@ -237,6 +239,13 @@ class Flow {
this.catchNodes.push(node);
} else if (node.type === "status") {
this.statusNodes.push(node);
} else if (node.type === "complete") {
if (node.scope) {
node.scope.forEach(id => {
this.completeNodeMap[id] = this.completeNodeMap[id] || [];
this.completeNodeMap[id].push(node);
})
}
}
}
}
@ -516,6 +525,20 @@ class Flow {
return handled;
}
handleComplete(node,msg) {
if (this.completeNodeMap[node.id]) {
let toSend = msg;
this.completeNodeMap[node.id].forEach((completeNode,index) => {
toSend = redUtil.cloneMessage(msg);
completeNode.receive(toSend);
})
}
}
get asyncMessageDelivery() {
return asyncMessageDelivery
}
dump() {
console.log("==================")
console.log(this.TYPE, this.id);
@ -562,6 +585,7 @@ function stopNode(node,removed) {
module.exports = {
init: function(runtime) {
nodeCloseTimeout = runtime.settings.nodeCloseTimeout || 15000;
asyncMessageDelivery = !runtime.settings.runtimeSyncDelivery
Log = runtime.log;
Subflow = require("./Subflow");
Subflow.init(runtime);

View File

@ -729,6 +729,10 @@ module.exports = {
updateFlow: updateFlow,
removeFlow: removeFlow,
disableFlow:null,
enableFlow:null
enableFlow:null,
isDeliveryModeAsync: function() {
// If settings is null, this is likely being run by unit tests
return !settings || !settings.runtimeSyncDelivery
}
};

View File

@ -268,6 +268,7 @@ describe('function node', function() {
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
n1.receive({payload:"foo",topic: "bar"});
setTimeout(function() {
try {
helper.log().called.should.be.true();
var logEvents = helper.log().args.filter(function(evt) {
@ -283,15 +284,17 @@ describe('function node', function() {
} catch(err) {
done(err);
}
},50);
});
});
it('should handle node.on()', function(done) {
var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"node.on('close',function(){node.log('closed')});"}];
var flow = [{id:"n1",type:"function",wires:[["n2"]],func:"node.on('close',function(){ node.log('closed')});"}];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
n1.receive({payload:"foo",topic: "bar"});
helper.getNode("n1").close();
setTimeout(function() {
n1.close().then(function() {
try {
helper.log().called.should.be.true();
var logEvents = helper.log().args.filter(function(evt) {
@ -308,6 +311,8 @@ describe('function node', function() {
done(err);
}
});
},1500);
});
});
it('should set node context', function(done) {
@ -532,6 +537,7 @@ describe('function node', function() {
});
function checkCallbackError(name, done) {
setTimeout(function() {
try {
helper.log().called.should.be.true();
var logEvents = helper.log().args.filter(function (evt) {
@ -548,6 +554,7 @@ describe('function node', function() {
catch (e) {
done(e);
}
},50);
}
it('should get persistable node context (w/o callback)', function(done) {
@ -1267,12 +1274,12 @@ describe('function node', function() {
n1.receive({payload:"foo",topic: "bar"});
} else {
msg.should.have.property('payload', "hello");
delete process.env._TEST_FOO_;
done();
}
} catch(err) {
done(err);
} finally {
delete process.env._TEST_FOO_;
done(err);
}
});
n1.receive({payload:"foo",topic: "bar"});
@ -1285,6 +1292,7 @@ describe('function node', function() {
helper.load(functionNode, flow, function () {
var n1 = helper.getNode("n1");
n1.receive({payload: "foo", topic: "bar"});
setTimeout(function() {
try {
helper.log().called.should.be.true();
var logEvents = helper.log().args.filter(function (evt) {
@ -1300,6 +1308,7 @@ describe('function node', function() {
} catch (err) {
done(err);
}
},50);
});
});
it('should log a Debug Message', function (done) {
@ -1307,6 +1316,7 @@ describe('function node', function() {
helper.load(functionNode, flow, function () {
var n1 = helper.getNode("n1");
n1.receive({payload: "foo", topic: "bar"});
setTimeout(function() {
try {
helper.log().called.should.be.true();
var logEvents = helper.log().args.filter(function (evt) {
@ -1322,6 +1332,7 @@ describe('function node', function() {
} catch (err) {
done(err);
}
},50);
});
});
it('should log a Trace Message', function (done) {
@ -1329,6 +1340,7 @@ describe('function node', function() {
helper.load(functionNode, flow, function () {
var n1 = helper.getNode("n1");
n1.receive({payload: "foo", topic: "bar"});
setTimeout(function() {
try {
helper.log().called.should.be.true();
var logEvents = helper.log().args.filter(function (evt) {
@ -1344,6 +1356,7 @@ describe('function node', function() {
} catch (err) {
done(err);
}
},50);
});
});
it('should log a Warning Message', function (done) {
@ -1351,6 +1364,7 @@ describe('function node', function() {
helper.load(functionNode, flow, function () {
var n1 = helper.getNode("n1");
n1.receive({payload: "foo", topic: "bar"});
setTimeout(function() {
try {
helper.log().called.should.be.true();
var logEvents = helper.log().args.filter(function (evt) {
@ -1366,6 +1380,7 @@ describe('function node', function() {
} catch (err) {
done(err);
}
},50);
});
});
it('should log an Error Message', function (done) {
@ -1373,6 +1388,7 @@ describe('function node', function() {
helper.load(functionNode, flow, function () {
var n1 = helper.getNode("n1");
n1.receive({payload: "foo", topic: "bar"});
setTimeout(function() {
try {
helper.log().called.should.be.true();
var logEvents = helper.log().args.filter(function (evt) {
@ -1388,6 +1404,7 @@ describe('function node', function() {
} catch (err) {
done(err);
}
},50);
});
});
});

View File

@ -475,7 +475,11 @@ describe('SORT node', function() {
{id:"n2", type:"helper"}];
helper.load(sortNode, flow, function() {
var n1 = helper.getNode("n1");
var msg = { payload: 0,
parts: { id: "X", index: 0, count: 2} };
n1.receive(msg);
setTimeout(function() {
n1.close().then(function() {
var logEvents = helper.log().args.filter(function (evt) {
return evt[0].type == "sort";
});
@ -484,11 +488,8 @@ describe('SORT node', function() {
evt.should.have.property('type', "sort");
evt.should.have.property('msg', "sort.clear");
done();
});
}, 150);
var msg = { payload: 0,
parts: { id: "X", index: 0, count: 2} };
n1.receive(msg);
n1.close();
});
});

View File

@ -21,7 +21,7 @@ var fs = require('fs-extra');
var htmlNode = require("nr-test-utils").require("@node-red/nodes/core/parsers/70-HTML.js");
var helper = require("node-red-node-test-helper");
describe('html node', function() {
describe('HTML node', function() {
var resourcesDir = __dirname+ path.sep + ".." + path.sep + ".." + path.sep + ".." + path.sep + "resources" + path.sep;
var file = path.join(resourcesDir, "70-HTML-test-file.html");
@ -228,6 +228,8 @@ describe('html node', function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n1.receive({payload:null,topic: "bar"});
setTimeout(function() {
try {
helper.log().called.should.be.true();
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "html";
@ -238,6 +240,8 @@ describe('html node', function() {
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},50);
} catch(err) {
done(err);
}

View File

@ -148,6 +148,8 @@ describe('JSON node', function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn1.receive({payload:'foo',topic: "bar"});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
@ -156,6 +158,8 @@ describe('JSON node', function() {
logEvents[0][0].msg.should.startWith("Unexpected token o");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},20);
} catch(err) {
done(err);
}
@ -378,6 +382,8 @@ describe('JSON node', function() {
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
var obj = {"number": "foo", "string": 3};
jn1.receive({payload:obj, schema:schema});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
@ -386,6 +392,8 @@ describe('JSON node', function() {
logEvents[0][0].msg.should.equal("json.errors.schema-error: data.number should be number, data.string should be string");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},50);
} catch(err) {
done(err);
}
@ -402,6 +410,8 @@ describe('JSON node', function() {
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
var obj = {"number": "foo", "string": 3};
jn1.receive({payload:obj, schema:schema});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
@ -410,6 +420,8 @@ describe('JSON node', function() {
logEvents[0][0].msg.should.equal("json.errors.schema-error: data.number should be number, data.string should be string");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},50);
} catch(err) {
done(err);
}
@ -426,6 +438,8 @@ describe('JSON node', function() {
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
var jsonString = '{"number":"Hello","string":3}';
jn1.receive({payload:jsonString, schema:schema});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
@ -434,6 +448,8 @@ describe('JSON node', function() {
logEvents[0][0].msg.should.equal("json.errors.schema-error: data.number should be number, data.string should be string");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},50);
} catch(err) {
done(err);
}
@ -450,6 +466,8 @@ describe('JSON node', function() {
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
var jsonString = '{"number":"Hello","string":3}';
jn1.receive({payload:jsonString, schema:schema});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
@ -458,6 +476,8 @@ describe('JSON node', function() {
logEvents[0][0].msg.should.equal("json.errors.schema-error: data.number should be number, data.string should be string");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},50);
} catch(err) {
done(err);
}
@ -474,6 +494,8 @@ describe('JSON node', function() {
var schema = "garbage";
var obj = {"number": "foo", "string": 3};
jn1.receive({payload:obj, schema:schema});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
@ -482,6 +504,8 @@ describe('JSON node', function() {
logEvents[0][0].msg.should.equal("json.errors.schema-error-compile");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},50);
} catch(err) {
done(err);
}

View File

@ -130,6 +130,8 @@ describe('YAML node', function() {
var yn1 = helper.getNode("yn1");
var yn2 = helper.getNode("yn2");
yn1.receive({payload:'employees:\n-firstName: John\n- lastName: Smith\n',topic: "bar"});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "yaml";
});
@ -138,6 +140,8 @@ describe('YAML node', function() {
logEvents[0][0].msg.should.startWith("end of the stream");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},50);
} catch(err) {
done(err);
}

View File

@ -156,10 +156,50 @@ describe('Node', function() {
throw new Error("test error");
});
n.receive(message);
setTimeout(function() {
n.error.called.should.be.true();
n.error.firstCall.args[1].should.equal(message);
done();
},50);
});
it('calls parent flow handleComplete when callback provided', function(done) {
var n = new RedNode({id:'123',type:'abc', _flow: {
handleComplete: function(node,msg) {
try {
msg.should.deepEqual(message);
done();
} catch(err) {
done(err);
}
}
}});
var message = {payload:"hello world"};
n.on('input',function(msg, nodeDone) {
nodeDone();
});
n.receive(message);
});
it('logs error if callback provides error', function(done) {
var n = new RedNode({id:'123',type:'abc'});
sinon.stub(n,"error",function(err,msg) {});
var message = {payload:"hello world"};
n.on('input',function(msg, nodeDone) {
nodeDone(new Error("test error"));
setTimeout(function() {
try {
n.error.called.should.be.true();
n.error.firstCall.args[0].toString().should.equal("Error: test error");
n.error.firstCall.args[1].should.equal(message);
done();
} catch(err) {
done(err);
}
},50);
});
n.receive(message);
});
});
@ -172,15 +212,45 @@ describe('Node', function() {
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var message = {payload:"hello world"};
var messageReceived = false;
n2.on('input',function(msg) {
// msg equals message, and is not a new copy
messageReceived = true;
should.deepEqual(msg,message);
should.strictEqual(msg,message);
done();
});
n1.send(message);
messageReceived.should.be.false();
});
it('emits a single message - synchronous mode', function(done) {
var flow = {
getNode: (id) => { return {'n1':n1,'n2':n2}[id]},
asyncMessageDelivery: false
};
var n1 = new RedNode({_flow:flow,id:'n1',type:'abc',wires:[['n2']]});
var n2 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var message = {payload:"hello world"};
var messageReceived = false;
var notSyncErr;
n2.on('input',function(msg) {
try {
// msg equals message, and is not a new copy
messageReceived = true;
should.deepEqual(msg,message);
should.strictEqual(msg,message);
done(notSyncErr);
} catch(err) {
done(err);
}
});
n1.send(message);
try {
messageReceived.should.be.true();
} catch(err) {
notSyncErr = err;
}
});
it('emits multiple messages on a single output', function(done) {
@ -356,12 +426,13 @@ describe('Node', function() {
it("logs the uuid for all messages sent", function(done) {
var logHandler = {
msgIds:[],
messagesSent: 0,
emit: function(event, msg) {
if (msg.event == "node.abc.send" && msg.level == Log.METRIC) {
this.messagesSent++;
this.msgIds.push(msg.msgid);
(typeof msg.msgid).should.not.be.equal("undefined");
done();
}
}
};
@ -375,6 +446,17 @@ describe('Node', function() {
var receiver1 = new RedNode({_flow:flow,id:'n2',type:'abc'});
var receiver2 = new RedNode({_flow:flow,id:'n3',type:'abc'});
sender.send({"some": "message"});
setTimeout(function() {
try {
logHandler.messagesSent.should.equal(1);
should.exist(logHandler.msgIds[0])
Log.removeHandler(logHandler);
done();
} catch(err) {
Log.removeHandler(logHandler);
done(err);
}
},50)
})
});

View File

@ -122,6 +122,7 @@ describe('Flow', function() {
currentNodes[node.id] = node;
this.on('input',function(msg) {
node.handled++;
msg.handled = node.handled;
node.messages.push(msg);
node.send(msg);
});
@ -136,12 +137,42 @@ describe('Flow', function() {
}
util.inherits(TestAsyncNode,Node);
var TestDoneNode = function(n) {
Node.call(this,n);
var node = this;
this.scope = n.scope;
this.uncaught = n.uncaught;
this.foo = n.foo;
this.handled = 0;
this.messages = [];
this.stopped = false;
this.closeDelay = n.closeDelay || 50;
currentNodes[node.id] = node;
this.on('input',function(msg, done) {
node.handled++;
node.messages.push(msg);
node.send(msg);
done();
});
this.on('close',function(done) {
setTimeout(function() {
node.stopped = true;
stoppedNodes[node.id] = node;
delete currentNodes[node.id];
done();
},node.closeDelay);
});
}
util.inherits(TestDoneNode,Node);
before(function() {
getType = sinon.stub(typeRegistry,"get",function(type) {
if (type=="test") {
return TestNode;
} else if (type=="testError"){
return TestErrorNode;
} else if (type=="testDone"){
return TestDoneNode;
} else {
return TestAsyncNode;
}
@ -189,9 +220,7 @@ describe('Flow', function() {
currentNodes["2"].should.have.a.property("handled",0);
currentNodes["3"].should.have.a.property("handled",0);
currentNodes["1"].receive({payload:"test"});
currentNodes["3"].on("input", function() {
currentNodes["1"].should.have.a.property("handled",1);
currentNodes["2"].should.have.a.property("handled",1);
currentNodes["3"].should.have.a.property("handled",1);
@ -212,6 +241,8 @@ describe('Flow', function() {
}
});
});
currentNodes["1"].receive({payload:"test"});
});
it("instantiates config nodes in the right order",function(done) {
var config = flowUtils.parseConfig([
@ -350,6 +381,7 @@ describe('Flow', function() {
currentNodes["1"].receive({payload:"test"});
setTimeout(function() {
currentNodes["1"].should.have.a.property("handled",1);
// Message doesn't reach 3 as 2 is disabled
currentNodes["3"].should.have.a.property("handled",0);
@ -369,6 +401,7 @@ describe('Flow', function() {
done(err);
}
});
},50);
});
});
@ -551,6 +584,8 @@ describe('Flow', function() {
flow.handleStatus(config.flows["t1"].nodes["1"],{text:"my-status",random:"otherProperty"});
setTimeout(function() {
currentNodes["sn"].should.have.a.property("handled",1);
var statusMessage = currentNodes["sn"].messages[0];
@ -576,6 +611,7 @@ describe('Flow', function() {
flow.stop().then(function() {
done();
});
},50)
});
it("passes a status event to the adjacent scoped status node ",function(done) {
var config = flowUtils.parseConfig([
@ -596,6 +632,7 @@ describe('Flow', function() {
flow.handleStatus(config.flows["t1"].nodes["1"],{text:"my-status"});
setTimeout(function() {
currentNodes["sn"].should.have.a.property("handled",0);
currentNodes["sn2"].should.have.a.property("handled",1);
var statusMessage = currentNodes["sn2"].messages[0];
@ -611,6 +648,7 @@ describe('Flow', function() {
flow.stop().then(function() {
done();
});
},50);
});
});
@ -636,6 +674,7 @@ describe('Flow', function() {
flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo"});
setTimeout(function() {
currentNodes["sn"].should.have.a.property("handled",1);
var statusMessage = currentNodes["sn"].messages[0];
@ -663,6 +702,7 @@ describe('Flow', function() {
flow.stop().then(function() {
done();
});
},50);
});
it("passes an error event to the adjacent scoped catch node ",function(done) {
var config = flowUtils.parseConfig([
@ -684,6 +724,7 @@ describe('Flow', function() {
flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo"});
setTimeout(function() {
currentNodes["sn"].should.have.a.property("handled",0);
currentNodes["sn2"].should.have.a.property("handled",1);
var statusMessage = currentNodes["sn2"].messages[0];
@ -701,7 +742,7 @@ describe('Flow', function() {
// Inject error that sn1/2 will ignore - so should get picked up by sn3
flow.handleError(config.flows["t1"].nodes["3"],"my-error-2",{a:"foo-2"});
setTimeout(function() {
currentNodes["sn"].should.have.a.property("handled",0);
currentNodes["sn2"].should.have.a.property("handled",1);
currentNodes["sn3"].should.have.a.property("handled",1);
@ -717,6 +758,8 @@ describe('Flow', function() {
flow.stop().then(function() {
done();
});
},50);
},50);
});
it("moves any existing error object sideways",function(done){
var config = flowUtils.parseConfig([
@ -733,7 +776,7 @@ describe('Flow', function() {
var activeNodes = flow.getActiveNodes();
flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo",error:"existing"});
setTimeout(function() {
currentNodes["sn"].should.have.a.property("handled",1);
var statusMessage = currentNodes["sn"].messages[0];
@ -748,7 +791,39 @@ describe('Flow', function() {
flow.stop().then(function() {
done();
});
},50);
});
it("prevents an error looping more than 10 times",function(){});
});
describe("#handleComplete",function() {
it("passes a complete event to the adjacent Complete node",function(done) {
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id:"1",x:10,y:10,z:"t1",type:"testDone",name:"a",wires:["2"]},
{id:"2",x:10,y:10,z:"t1",type:"test",wires:["3"]},
{id:"3",x:10,y:10,z:"t1",type:"testDone",foo:"a",wires:[]},
{id:"cn",x:10,y:10,z:"t1",type:"complete",scope:["1","3"],foo:"a",wires:[]}
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
var activeNodes = flow.getActiveNodes();
Object.keys(activeNodes).should.have.length(4);
var msg = {payload: "hello world"}
var n1 = currentNodes["1"].receive(msg);
setTimeout(function() {
currentNodes["cn"].should.have.a.property("handled",2);
currentNodes["cn"].messages[0].should.have.a.property("handled",1);
currentNodes["cn"].messages[1].should.have.a.property("handled",2);
flow.stop().then(function() {
done();
});
},50);
});
});
});

View File

@ -271,6 +271,7 @@ describe('Subflow', function() {
currentNodes["1"].receive({payload:"test"});
setTimeout(function() {
currentNodes["1"].should.have.a.property("handled",1);
// currentNodes[sfInstanceId].should.have.a.property("handled",1);
// currentNodes[sfInstanceId2].should.have.a.property("handled",1);
@ -297,6 +298,7 @@ describe('Subflow', function() {
// // stoppedNodes.should.have.a.property(sfConfigId);
done();
});
},50);
});
it("instantiates a subflow inside a subflow and stops it",function(done) {
var config = flowUtils.parseConfig([
@ -322,17 +324,14 @@ describe('Subflow', function() {
currentNodes["1"].receive({payload:"test"});
setTimeout(function() {
currentNodes["1"].should.have.a.property("handled",1);
currentNodes["3"].should.have.a.property("handled",1);
flow.stop().then(function() {
Object.keys(currentNodes).should.have.length(0);
done();
});
},50);
});
it("rewires a subflow node on update/start",function(done){
@ -369,6 +368,7 @@ describe('Subflow', function() {
currentNodes["1"].receive({payload:"test"});
setTimeout(function() {
currentNodes["1"].should.have.a.property("handled",1);
// currentNodes[sfInstanceId].should.have.a.property("handled",1);
// currentNodes[sfInstanceId2].should.have.a.property("handled",1);
@ -379,6 +379,7 @@ describe('Subflow', function() {
flow.start(diff)
currentNodes["1"].receive({payload:"test2"});
setTimeout(function() {
currentNodes["1"].should.have.a.property("handled",2);
// currentNodes[sfInstanceId].should.have.a.property("handled",2);
@ -390,6 +391,8 @@ describe('Subflow', function() {
flow.stop().then(function() {
done();
});
},50);
},50);
});
});
describe('#stop', function() {
@ -436,7 +439,7 @@ describe('Subflow', function() {
var activeNodes = flow.getActiveNodes();
activeNodes["1"].receive({payload:"test"});
setTimeout(function() {
currentNodes["sn"].should.have.a.property("handled",1);
var statusMessage = currentNodes["sn"].messages[0];
@ -447,9 +450,9 @@ describe('Subflow', function() {
statusMessage.status.source.should.have.a.property("name","test-status-node");
flow.stop().then(function() {
done();
});
},50);
});
it("passes a status event to the subflow's parent tab status node - targetted scope",function(done) {
var config = flowUtils.parseConfig([
@ -472,6 +475,7 @@ describe('Subflow', function() {
activeNodes["1"].receive({payload:"test"});
setTimeout(function() {
parentFlowStatusCalled.should.be.false();
currentNodes["sn"].should.have.a.property("handled",1);
@ -487,6 +491,7 @@ describe('Subflow', function() {
done();
});
},50);
});
});
@ -517,6 +522,7 @@ describe('Subflow', function() {
activeNodes["1"].receive({payload:"test-payload"});
setTimeout(function() {
currentNodes["sn"].should.have.a.property("handled",1);
var statusMessage = currentNodes["sn"].messages[0];
@ -530,6 +536,7 @@ describe('Subflow', function() {
done();
});
},50);
});
it("emits a status event when a message is passed to a subflow-status node - msg.payload as status obj", function(done) {
var config = flowUtils.parseConfig([
@ -557,6 +564,7 @@ describe('Subflow', function() {
activeNodes["1"].receive({payload:{text:"payload-obj"}});
setTimeout(function() {
currentNodes["sn"].should.have.a.property("handled",1);
var statusMessage = currentNodes["sn"].messages[0];
@ -570,6 +578,7 @@ describe('Subflow', function() {
done();
});
},50);
});
it("emits a status event when a message is passed to a subflow-status node - msg.status", function(done) {
var config = flowUtils.parseConfig([
@ -597,6 +606,7 @@ describe('Subflow', function() {
activeNodes["1"].receive({status:{text:"status-obj"}});
setTimeout(function() {
currentNodes["sn"].should.have.a.property("handled",1);
var statusMessage = currentNodes["sn"].messages[0];
@ -610,6 +620,7 @@ describe('Subflow', function() {
done();
});
},50);
});
it("does not emit a regular status event if it contains a subflow-status node", function(done) {
var config = flowUtils.parseConfig([
@ -666,6 +677,7 @@ describe('Subflow', function() {
activeNodes["1"].receive({payload:"test"});
setTimeout(function() {
currentNodes["sn"].should.have.a.property("handled",1);
var statusMessage = currentNodes["sn"].messages[0];
@ -678,6 +690,7 @@ describe('Subflow', function() {
flow.stop().then(function() {
done();
});
},50);
});
it("passes an error event to the subflow's parent tab catch node - targetted scope",function(done) {
var config = flowUtils.parseConfig([
@ -699,6 +712,7 @@ describe('Subflow', function() {
activeNodes["1"].receive({payload:"test"});
setTimeout(function() {
parentFlowErrorCalled.should.be.false();
currentNodes["sn"].should.have.a.property("handled",1);
@ -713,6 +727,7 @@ describe('Subflow', function() {
flow.stop().then(function() {
done();
});
},50);
});
});
@ -756,11 +771,13 @@ describe('Subflow', function() {
process.env["__KEY__"] = "__VAL__";
currentNodes["1"].receive({payload: "test"});
setTimeout(function() {
currentNodes["3"].should.have.a.property("received", "__VAL__");
flow.stop().then(function() {
done();
});
},50);
});
it("can access subflow env var", function(done) {
@ -794,11 +811,13 @@ describe('Subflow', function() {
setEnv(testenv_node, "__KEY__", "__VAL1__");
currentNodes["1"].receive({payload: "test"});
setTimeout(function() {
currentNodes["3"].should.have.a.property("received", "__VAL1__");
flow.stop().then(function() {
done();
});
},50);
});
it("can access nested subflow env var", function(done) {
@ -840,19 +859,25 @@ describe('Subflow', function() {
process.env["__KEY__"] = "__VAL0__";
currentNodes["1"].receive({payload: "test"});
setTimeout(function() {
currentNodes["3"].should.have.a.property("received", "__VAL0__");
setEnv(node_sf1_1, "__KEY__", "__VAL1__");
currentNodes["1"].receive({payload: "test"});
setTimeout(function() {
currentNodes["3"].should.have.a.property("received", "__VAL1__");
setEnv(node_sf2_1, "__KEY__", "__VAL2__");
currentNodes["1"].receive({payload: "test"});
setTimeout(function() {
currentNodes["3"].should.have.a.property("received", "__VAL2__");
flow.stop().then(function() {
done();
});
},50);
},50);
},50);
});
});