diff --git a/editor/icons/batch.png b/editor/icons/batch.png
new file mode 100644
index 000000000..44803d185
Binary files /dev/null and b/editor/icons/batch.png differ
diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js
index 8b50da49d..31133dc36 100644
--- a/editor/js/ui/editor.js
+++ b/editor/js/ui/editor.js
@@ -170,7 +170,7 @@ RED.editor = (function() {
}
}
}
- if (!node._def.defaults.hasOwnProperty("icon") && node.icon) {
+ if (node._def.hasOwnProperty("defaults") && !node._def.defaults.hasOwnProperty("icon") && node.icon) {
var iconPath = RED.utils.separateIconPath(node.icon);
var iconSets = RED.nodes.getIconSets();
var iconFileList = iconSets[iconPath.module];
diff --git a/editor/js/ui/view.js b/editor/js/ui/view.js
index 8e47e0e5a..c685e852e 100644
--- a/editor/js/ui/view.js
+++ b/editor/js/ui/view.js
@@ -81,6 +81,9 @@ RED.view = (function() {
.style("cursor","crosshair")
.on("mousedown", function() {
focusView();
+ })
+ .on("contextmenu", function(){
+ d3.event.preventDefault();
});
var vis = outer
@@ -1392,7 +1395,7 @@ RED.view = (function() {
function portMouseUp(d,portType,portIndex) {
var i;
- if (mouse_mode === RED.state.QUICK_JOINING) {
+ if (mouse_mode === RED.state.QUICK_JOINING && drag_lines.length > 0) {
if (drag_lines[0].node===d) {
return
}
diff --git a/nodes/core/core/89-trigger.html b/nodes/core/core/89-trigger.html
index f90bda1a4..0b1194fe0 100644
--- a/nodes/core/core/89-trigger.html
+++ b/nodes/core/core/89-trigger.html
@@ -18,7 +18,7 @@
@@ -45,7 +45,7 @@
-
+
diff --git a/nodes/core/core/89-trigger.js b/nodes/core/core/89-trigger.js
index 72cdf59aa..f1df7ea8b 100644
--- a/nodes/core/core/89-trigger.js
+++ b/nodes/core/core/89-trigger.js
@@ -80,7 +80,7 @@ module.exports = function(RED) {
var topic = msg.topic || "_none";
if (node.bytopic === "all") { topic = "_none"; }
node.topics[topic] = node.topics[topic] || {};
- if (msg.hasOwnProperty("reset") || ((node.reset !== '') && (msg.payload == node.reset)) ) {
+ if (msg.hasOwnProperty("reset") || ((node.reset !== '') && msg.hasOwnProperty("payload") && (msg.payload !== null) && msg.payload.toString && (msg.payload.toString() == node.reset)) ) {
if (node.loop === true) { clearInterval(node.topics[topic].tout); }
else { clearTimeout(node.topics[topic].tout); }
delete node.topics[topic];
@@ -104,7 +104,9 @@ module.exports = function(RED) {
if (node.duration === 0) { node.topics[topic].tout = 0; }
else if (node.loop === true) {
+ /* istanbul ignore else */
if (node.topics[topic].tout) { clearInterval(node.topics[topic].tout); }
+ /* istanbul ignore else */
if (node.op1type !== "nul") {
var msg2 = RED.util.cloneMessage(msg);
node.topics[topic].tout = setInterval(function() { node.send(RED.util.cloneMessage(msg2)); }, node.duration);
@@ -128,7 +130,9 @@ module.exports = function(RED) {
node.status({fill:"blue",shape:"dot",text:" "});
}
else if ((node.extend === "true" || node.extend === true) && (node.duration > 0)) {
+ /* istanbul ignore else */
if (node.op2type === "payl") { node.topics[topic].m2 = RED.util.cloneMessage(msg.payload); }
+ /* istanbul ignore else */
if (node.topics[topic].tout) { clearTimeout(node.topics[topic].tout); }
node.topics[topic].tout = setTimeout(function() {
var msg2 = null;
@@ -153,6 +157,7 @@ module.exports = function(RED) {
});
this.on("close", function() {
for (var t in node.topics) {
+ /* istanbul ignore else */
if (node.topics[t]) {
if (node.loop === true) { clearInterval(node.topics[t].tout); }
else { clearTimeout(node.topics[t].tout); }
diff --git a/nodes/core/locales/en-US/messages.json b/nodes/core/locales/en-US/messages.json
index 6665387ae..e524b78f3 100644
--- a/nodes/core/locales/en-US/messages.json
+++ b/nodes/core/locales/en-US/messages.json
@@ -541,7 +541,8 @@
"switch": {
"label": {
"property": "Property",
- "rule": "rule"
+ "rule": "rule",
+ "repair" : "repair sequence (reconstruct parts property of outgoing messages)"
},
"and": "and",
"checkall": "checking all rules",
@@ -555,10 +556,15 @@
"false":"is false",
"null":"is null",
"nnull":"is not null",
+ "head":"head",
+ "tail":"tail",
+ "index":"is between",
+ "exp":"JSONata exp",
"else":"otherwise"
},
"errors": {
- "invalid-expr": "Invalid JSONata expression: __error__"
+ "invalid-expr": "Invalid JSONata expression: __error__",
+ "too-many" : "too many pending messages in switch node"
}
},
"change": {
@@ -840,6 +846,8 @@
"mode":{
"mode":"Mode",
"auto":"automatic",
+ "merge":"merge sequence",
+ "reduce":"reduce sequence",
"custom":"manual"
},
"combine":"Combine each",
@@ -861,19 +869,63 @@
"afterTimeout":"After a timeout following the first message",
"seconds":"seconds",
"complete":"After a message with the msg.complete
property set",
- "tip":"This mode assumes this node is either paired with a split node or the received messages will have a properly configured msg.parts
property."
+ "tip":"This mode assumes this node is either paired with a split node or the received messages will have a properly configured msg.parts
property.",
+ "too-many" : "too many pending messages in join node",
+ "merge": {
+ "topics-label":"Merged Topics",
+ "topics":"topics",
+ "topic" : "topic",
+ "on-change":"Send merged message on arrival of a new topic"
+ },
+ "reduce": {
+ "exp": "Reduce exp",
+ "exp-value": "exp",
+ "init": "Initial value",
+ "right": "Evaluate in reverse order (right to left)",
+ "fixup": "Fixup exp"
+ },
+ "errors": {
+ "invalid-expr": "Invalid JSONata expression: __error__"
+ }
},
"sort" : {
- "key-type" : "Key type",
- "payload" : "payload or element",
- "exp" : "expression",
- "key-exp" : "Key exp.",
- "order" : "Order",
- "ascending" : "ascending",
- "descending" : "descending",
- "as-number" : "as number",
+ "target" : "Sort",
+ "seq" : "message sequence",
+ "key" : "Key",
+ "elem" : "element value",
+ "order" : "Order",
+ "ascending" : "ascending",
+ "descending" : "descending",
+ "as-number" : "as number",
"invalid-exp" : "invalid JSONata expression in sort node",
"too-many" : "too many pending messages in sort node",
"clear" : "clear pending message in sort node"
+ },
+ "batch" : {
+ "mode": {
+ "label" : "Mode",
+ "num-msgs" : "number of messages",
+ "interval" : "interval in seconds",
+ "concat" : "concatenate sequences"
+ },
+ "count": {
+ "label" : "Number of msgs",
+ "overwrap" : "Overwrap",
+ "count" : "count",
+ "invalid" : "Invalid count and overwrap"
+ },
+ "interval": {
+ "label" : "Interval (sec)",
+ "seconds" : "seconds",
+ "sec" : "sec",
+ "empty" : "send empty message when no message arrives"
+ },
+ "concat": {
+ "topics-label": "Topics",
+ "topic" : "topic"
+ },
+ "too-many" : "too many pending messages in batch node",
+ "unexpected" : "unexpected mode",
+ "no-parts" : "no parts property in message"
}
}
diff --git a/nodes/core/logic/10-switch.html b/nodes/core/logic/10-switch.html
index 553bdbbf6..5a39c2bee 100644
--- a/nodes/core/logic/10-switch.html
+++ b/nodes/core/logic/10-switch.html
@@ -33,6 +33,10 @@
+
+
+
+
diff --git a/nodes/core/logic/17-split.js b/nodes/core/logic/17-split.js
index d4fdfc55b..803131671 100644
--- a/nodes/core/logic/17-split.js
+++ b/nodes/core/logic/17-split.js
@@ -232,6 +232,229 @@ module.exports = function(RED) {
RED.nodes.registerType("split",SplitNode);
+ var _max_kept_msgs_count = undefined;
+
+ function max_kept_msgs_count(node) {
+ if (_max_kept_msgs_count === undefined) {
+ var name = "joinMaxKeptMsgsCount";
+ if (RED.settings.hasOwnProperty(name)) {
+ _max_kept_msgs_count = RED.settings[name];
+ }
+ else {
+ _max_kept_msgs_count = 0;
+ }
+ }
+ return _max_kept_msgs_count;
+ }
+
+ function add_to_topic(node, pending, topic, msg) {
+ var merge_on_change = node.merge_on_change;
+ if (!pending.hasOwnProperty(topic)) {
+ pending[topic] = [];
+ }
+ var topics = pending[topic];
+ topics.push(msg);
+ if (merge_on_change) {
+ var counts = node.topic_counts;
+ if (topics.length > counts[topic]) {
+ topics.shift();
+ node.pending_count--;
+ }
+ }
+ }
+
+ function compute_topic_counts(topics) {
+ var counts = {};
+ for (var topic of topics) {
+ counts[topic] = (counts.hasOwnProperty(topic) ? counts[topic] : 0) +1;
+ }
+ return counts;
+ }
+
+ function try_merge(node, pending, merge_on_change) {
+ var topics = node.topics;
+ var counts = node.topic_counts;
+ for(var topic of topics) {
+ if(!pending.hasOwnProperty(topic) ||
+ (pending[topic].length < counts[topic])) {
+ return;
+ }
+ }
+ var merge_on_change = node.merge_on_change;
+ var msgs = [];
+ var vals = [];
+ var new_msg = {payload: vals};
+ for (var topic of topics) {
+ var pmsgs = pending[topic];
+ var msg = pmsgs.shift();
+ if (merge_on_change) {
+ pmsgs.push(msg);
+ }
+ var pval = msg.payload;
+ var val = new_msg[topic];
+ msgs.push(msg);
+ vals.push(pval);
+ if (val instanceof Array) {
+ new_msg[topic].push(pval);
+ }
+ else if (val === undefined) {
+ new_msg[topic] = pval;
+ }
+ else {
+ new_msg[topic] = [val, pval]
+ }
+ }
+ node.send(new_msg);
+ if (!merge_on_change) {
+ node.pending_count -= topics.length;
+ }
+ }
+
+ function merge_msg(node, msg) {
+ var topics = node.topics;
+ var topic = msg.topic;
+ if(node.topics.indexOf(topic) >= 0) {
+ var pending = node.pending;
+ if(node.topic_counts == undefined) {
+ node.topic_counts = compute_topic_counts(topics)
+ }
+ add_to_topic(node, pending, topic, msg);
+ node.pending_count++;
+ var max_msgs = max_kept_msgs_count(node);
+ if ((max_msgs > 0) && (node.pending_count > max_msgs)) {
+ node.pending = {};
+ node.pending_count = 0;
+ node.error(RED._("join.too-many"), msg);
+ }
+ try_merge(node, pending);
+ }
+ }
+
+ function apply_r(exp, accum, msg, index, count) {
+ exp.assign("I", index);
+ exp.assign("N", count);
+ exp.assign("A", accum);
+ return RED.util.evaluateJSONataExpression(exp, msg);
+ }
+
+ function apply_f(exp, accum, count) {
+ exp.assign("N", count);
+ exp.assign("A", accum);
+ return RED.util.evaluateJSONataExpression(exp, {});
+ }
+
+ function exp_or_undefined(exp) {
+ if((exp === "") ||
+ (exp === null)) {
+ return undefined;
+ }
+ return exp
+ }
+
+ function reduce_and_send_group(node, group) {
+ var is_right = node.reduce_right;
+ var flag = is_right ? -1 : 1;
+ var msgs = group.msgs;
+ var accum = node.reduce_init;
+ var reduce_exp = node.reduce_exp;
+ var reduce_fixup = node.reduce_fixup;
+ var count = group.count;
+ msgs.sort(function(x,y) {
+ var ix = x.parts.index;
+ var iy = y.parts.index;
+ if (ix < iy) return -flag;
+ if (ix > iy) return flag;
+ return 0;
+ });
+ for(var msg of msgs) {
+ accum = apply_r(reduce_exp, accum, msg, msg.parts.index, count);
+ }
+ if(reduce_fixup !== undefined) {
+ accum = apply_f(reduce_fixup, accum, count);
+ }
+ node.send({payload: accum});
+ }
+
+ function reduce_msg(node, msg) {
+ if(msg.hasOwnProperty('parts')) {
+ var parts = msg.parts;
+ var pending = node.pending;
+ var pending_count = node.pending_count;
+ var gid = msg.parts.id;
+ if(!pending.hasOwnProperty(gid)) {
+ var count = undefined;
+ if(parts.hasOwnProperty('count')) {
+ count = msg.parts.count;
+ }
+ pending[gid] = {
+ count: count,
+ msgs: []
+ };
+ }
+ var group = pending[gid];
+ var msgs = group.msgs;
+ if(parts.hasOwnProperty('count') &&
+ (group.count === undefined)) {
+ group.count = count;
+ }
+ msgs.push(msg);
+ pending_count++;
+ if(msgs.length === group.count) {
+ delete pending[gid];
+ try {
+ pending_count -= msgs.length;
+ reduce_and_send_group(node, group);
+ } catch(e) {
+ node.error(RED._("join.errors.invalid-expr",{error:e.message})); }
+ }
+ node.pending_count = pending_count;
+ var max_msgs = max_kept_msgs_count(node);
+ if ((max_msgs > 0) && (pending_count > max_msgs)) {
+ node.pending = {};
+ node.pending_count = 0;
+ node.error(RED._("join.too-many"), msg);
+ }
+ }
+ else {
+ node.send(msg);
+ }
+ }
+
+ function eval_exp(node, exp, exp_type) {
+ if(exp_type === "flow") {
+ return node.context().flow.get(exp);
+ }
+ else if(exp_type === "global") {
+ return node.context().global.get(exp);
+ }
+ else if(exp_type === "str") {
+ return exp;
+ }
+ else if(exp_type === "num") {
+ return Number(exp);
+ }
+ else if(exp_type === "bool") {
+ if (exp === 'true') {
+ return true;
+ }
+ else if (exp === 'false') {
+ return false;
+ }
+ }
+ else if ((exp_type === "bin") ||
+ (exp_type === "json")) {
+ return JSON.parse(exp);
+ }
+ else if(exp_type === "date") {
+ return Date.now();
+ }
+ else if(exp_type === "jsonata") {
+ var jexp = RED.util.prepareJSONataExpression(exp, node);
+ return RED.util.evaluateJSONataExpression(jexp, {});
+ }
+ throw new Error("unexpected initial value type");
+ }
+
function JoinNode(n) {
RED.nodes.createNode(this,n);
this.mode = n.mode||"auto";
@@ -246,6 +469,22 @@ module.exports = function(RED) {
this.joiner = n.joiner||"";
this.joinerType = n.joinerType||"str";
+ this.reduce = (this.mode === "reduce");
+ if (this.reduce) {
+ var exp_init = n.reduceInit;
+ var exp_init_type = n.reduceInitType;
+ var exp_reduce = n.reduceExp;
+ var exp_fixup = exp_or_undefined(n.reduceFixup);
+ this.reduce_right = n.reduceRight;
+ try {
+ this.reduce_init = eval_exp(this, exp_init, exp_init_type);
+ this.reduce_exp = RED.util.prepareJSONataExpression(exp_reduce, this);
+ this.reduce_fixup = (exp_fixup !== undefined) ? RED.util.prepareJSONataExpression(exp_fixup, this) : undefined;
+ } catch(e) {
+ this.error(RED._("join.errors.invalid-expr",{error:e.message}));
+ }
+ }
+
if (this.joinerType === "str") {
this.joiner = this.joiner.replace(/\\n/g,"\n").replace(/\\r/g,"\r").replace(/\\t/g,"\t").replace(/\\e/g,"\e").replace(/\\f/g,"\f").replace(/\\0/g,"\0");
} else if (this.joinerType === "bin") {
@@ -259,6 +498,14 @@ module.exports = function(RED) {
this.build = n.build || "array";
this.accumulate = n.accumulate || "false";
+
+ this.topics = (n.topics || []).map(function(x) { return x.topic; });
+ this.merge_on_change = n.mergeOnChange || false;
+ this.topic_counts = undefined;
+ this.output = n.output || "stream";
+ this.pending = {};
+ this.pending_count = 0;
+
//this.topic = n.topic;
var node = this;
var inflight = {};
@@ -321,6 +568,10 @@ module.exports = function(RED) {
node.warn("Message missing msg.parts property - cannot join in 'auto' mode")
return;
}
+ if (node.mode === 'merge' && !msg.hasOwnProperty("topic")) {
+ node.warn("Message missing msg.topic property - cannot join in 'merge' mode");
+ return;
+ }
if (node.propertyType == "full") {
property = msg;
@@ -351,6 +602,14 @@ module.exports = function(RED) {
arrayLen = msg.parts.len;
propertyIndex = msg.parts.index;
}
+ else if (node.mode === 'merge') {
+ merge_msg(node, msg);
+ return;
+ }
+ else if (node.mode === 'reduce') {
+ reduce_msg(node, msg);
+ return;
+ }
else {
// Use the node configuration to identify all of the group information
partId = "_";
diff --git a/nodes/core/logic/18-sort.html b/nodes/core/logic/18-sort.html
index 3aacd46a1..5ce1a9f82 100644
--- a/nodes/core/logic/18-sort.html
+++ b/nodes/core/logic/18-sort.html
@@ -19,17 +19,24 @@
@@ -86,9 +93,13 @@
defaults: {
name: { value:"" },
order: { value:"ascending" },
- as_num : { value:false },
- keyType : { value:"payload" },
- key : { value:"" }
+ as_num: { value:false },
+ target: { value:"payload" },
+ targetType: { value:"msg" },
+ msgKey: { value:"payload" },
+ msgKeyType: { value:"elem" },
+ seqKey: { value:"payload" },
+ seqKeyType: { value:"msg" }
},
inputs:1,
outputs:1,
@@ -100,13 +111,37 @@
return this.name ? "node_label_italic" : "";
},
oneditprepare: function() {
- $("#node-input-key").typedInput({default:'jsonata', types:['jsonata']});
- $("#node-input-keyType").change(function(e) {
- var val = $(this).val();
- $(".node-row-sort-key").toggle(val === 'exp');
+ var seq = {
+ value: "seq",
+ label: RED._("node-red:sort.seq"),
+ hasValue: false
+ };
+ var elem = {
+ value: "elem",
+ label: RED._("node-red:sort.elem"),
+ hasValue: false
+ };
+ $("#node-input-target").typedInput({
+ default:'msg',
+ typeField: $("#node-input-targetType"),
+ types:['msg', seq]
});
- $("#node-input-keyType").change();
- $("#node-input-order").change();
+ $("#node-input-msgKey").typedInput({
+ default:'elem',
+ typeField: $("#node-input-msgKeyType"),
+ types:[elem, 'jsonata']
+ });
+ $("#node-input-seqKey").typedInput({
+ default:'msg',
+ typeField: $("#node-input-seqKeyType"),
+ types:['msg', 'jsonata']
+ });
+ $("#node-input-target").change(function(e) {
+ var val = $("#node-input-target").typedInput('type');
+ $(".node-row-sort-msg-key").toggle(val === "msg");
+ $(".node-row-sort-seq-key").toggle(val === "seq");
+ });
+ $("#node-input-target").change();
}
});
diff --git a/nodes/core/logic/18-sort.js b/nodes/core/logic/18-sort.js
index 529e83f37..65d8e6c45 100644
--- a/nodes/core/logic/18-sort.js
+++ b/nodes/core/logic/18-sort.js
@@ -21,7 +21,7 @@ module.exports = function(RED) {
function max_kept_msgs_count(node) {
if (_max_kept_msgs_count === undefined) {
- var name = "sortMaxKeptMsgsCount";
+ var name = "maxKeptMsgsCount";
if (RED.settings.hasOwnProperty(name)) {
_max_kept_msgs_count = RED.settings[name];
}
@@ -60,11 +60,15 @@ module.exports = function(RED) {
var pending_id = 0;
var order = n.order || "ascending";
var as_num = n.as_num || false;
- var key_is_payload = (n.keyType === 'payload');
- var key_exp = undefined;
- if (!key_is_payload) {
+ var target_prop = n.target || "payload";
+ var target_is_prop = (n.targetType === 'msg');
+ var key_is_exp = target_is_prop ? (n.msgKeyType === "jsonata") : (n.seqKeyType === "jsonata");
+ var key_prop = n.seqKey || "payload";
+ var key_exp = target_is_prop ? n.msgKey : n.seqKey;
+
+ if (key_is_exp) {
try {
- key_exp = RED.util.prepareJSONataExpression(n.key, this);
+ key_exp = RED.util.prepareJSONataExpression(key_exp, this);
}
catch (e) {
node.error(RED._("sort.invalid-exp"));
@@ -87,10 +91,12 @@ module.exports = function(RED) {
}
function send_group(group) {
- var key = key_is_payload
- ? function(msg) { return msg.payload; }
- : function(msg) {
+ var key = key_is_exp
+ ? function(msg) {
return eval_jsonata(node, key_exp, msg);
+ }
+ : function(msg) {
+ return RED.util.getMessageProperty(msg, key_prop);
};
var comp = gen_comp(key);
var msgs = group.msgs;
@@ -108,16 +114,16 @@ module.exports = function(RED) {
}
function sort_payload(msg) {
- var payload = msg.payload;
- if (Array.isArray(payload)) {
- var key = key_is_payload
- ? function(elem) { return elem; }
- : function(elem) {
+ var data = RED.util.getMessageProperty(msg, target_prop);
+ if (Array.isArray(data)) {
+ var key = key_is_exp
+ ? function(elem) {
return eval_jsonata(node, key_exp, elem);
- };
+ }
+ : function(elem) { return elem; };
var comp = gen_comp(key);
try {
- payload.sort(comp);
+ data.sort(comp);
}
catch (e) {
return false;
@@ -162,7 +168,7 @@ module.exports = function(RED) {
}
function process_msg(msg) {
- if (!msg.hasOwnProperty("parts")) {
+ if (target_is_prop) {
if (sort_payload(msg)) {
node.send(msg);
}
diff --git a/nodes/core/logic/19-batch.html b/nodes/core/logic/19-batch.html
new file mode 100644
index 000000000..07fda4915
--- /dev/null
+++ b/nodes/core/logic/19-batch.html
@@ -0,0 +1,192 @@
+
+
+
+
+
+
+
+
+
diff --git a/nodes/core/logic/19-batch.js b/nodes/core/logic/19-batch.js
new file mode 100644
index 000000000..bdd5b5408
--- /dev/null
+++ b/nodes/core/logic/19-batch.js
@@ -0,0 +1,246 @@
+/**
+ * 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";
+
+ var _max_kept_msgs_count = undefined;
+
+ function max_kept_msgs_count(node) {
+ if (_max_kept_msgs_count === undefined) {
+ var name = "batchMaxKeptMsgsCount";
+ if (RED.settings.hasOwnProperty(name)) {
+ _max_kept_msgs_count = RED.settings[name];
+ }
+ else {
+ _max_kept_msgs_count = 0;
+ }
+ }
+ return _max_kept_msgs_count;
+ }
+
+ function send_msgs(node, msgs, clone_msg) {
+ var count = msgs.length;
+ var msg_id = msgs[0]._msgid;
+ for (var i = 0; i < count; i++) {
+ var msg = clone_msg ? RED.util.cloneMessage(msgs[i]) : msgs[i];
+ if (!msg.hasOwnProperty("parts")) {
+ msg.parts = {};
+ }
+ var parts = msg.parts;
+ parts.id = msg_id;
+ parts.index = i;
+ parts.count = count;
+ node.send(msg);
+ }
+ }
+
+ function send_interval(node, allow_empty_seq) {
+ let msgs = node.pending;
+ if (msgs.length > 0) {
+ send_msgs(node, msgs, false);
+ node.pending = [];
+ }
+ else {
+ if (allow_empty_seq) {
+ let mid = RED.util.generateId();
+ let msg = {
+ payload: null,
+ parts: {
+ id: mid,
+ index: 0,
+ count: 1
+ }
+ };
+ node.send(msg);
+ }
+ }
+ }
+
+ function is_complete(pending, topic) {
+ if (pending.hasOwnProperty(topic)) {
+ var p_topic = pending[topic];
+ var gids = p_topic.gids;
+ if (gids.length > 0) {
+ var gid = gids[0];
+ var groups = p_topic.groups;
+ var group = groups[gid];
+ return (group.count === group.msgs.length);
+ }
+ }
+ return false;
+ }
+
+ function get_msgs_of_topic(pending, topic) {
+ var p_topic = pending[topic];
+ var groups = p_topic.groups;
+ var gids = p_topic.gids;
+ var gid = gids[0];
+ var group = groups[gid];
+ return group.msgs;
+ }
+
+ function remove_topic(pending, topic) {
+ var p_topic = pending[topic];
+ var groups = p_topic.groups;
+ var gids = p_topic.gids;
+ var gid = gids.shift();
+ delete groups[gid];
+ }
+
+ function try_concat(node, pending) {
+ var topics = node.topics;
+ for (var topic of topics) {
+ if (!is_complete(pending, topic)) {
+ return;
+ }
+ }
+ var msgs = [];
+ for (var topic of topics) {
+ var t_msgs = get_msgs_of_topic(pending, topic);
+ msgs = msgs.concat(t_msgs);
+ }
+ for (var topic of topics) {
+ remove_topic(pending, topic);
+ }
+ send_msgs(node, msgs, false);
+ node.pending_count -= msgs.length;
+ }
+
+ function add_to_topic_group(pending, topic, gid, msg) {
+ if (!pending.hasOwnProperty(topic)) {
+ pending[topic] = { groups: {}, gids: [] };
+ }
+ var p_topic = pending[topic];
+ var groups = p_topic.groups;
+ var gids = p_topic.gids;
+ if (!groups.hasOwnProperty(gid)) {
+ groups[gid] = { msgs: [], count: undefined };
+ gids.push(gid);
+ }
+ var group = groups[gid];
+ group.msgs.push(msg);
+ if ((group.count === undefined) &&
+ msg.parts.hasOwnProperty('count')) {
+ group.count = msg.parts.count;
+ }
+ }
+
+ function concat_msg(node, msg) {
+ var topic = msg.topic;
+ if(node.topics.indexOf(topic) >= 0) {
+ if (!msg.hasOwnProperty("parts") ||
+ !msg.parts.hasOwnProperty("id") ||
+ !msg.parts.hasOwnProperty("index") ||
+ !msg.parts.hasOwnProperty("count")) {
+ node.error(RED._("batch.no-parts"), msg);
+ return;
+ }
+ var gid = msg.parts.id;
+ var pending = node.pending;
+ add_to_topic_group(pending, topic, gid, msg);
+ node.pending_count++;
+ var max_msgs = max_kept_msgs_count(node);
+ if ((max_msgs > 0) && (node.pending_count > max_msgs)) {
+ node.pending = {};
+ node.pending_count = 0;
+ node.error(RED._("batch.too-many"), msg);
+ }
+ try_concat(node, pending);
+ }
+ }
+
+ function BatchNode(n) {
+ RED.nodes.createNode(this,n);
+ var node = this;
+ var mode = n.mode || "count";
+
+ node.pending_count = 0;
+ if (mode === "count") {
+ var count = Number(n.count || 1);
+ var overwrap = Number(n.overwrap || 0);
+ var is_overwrap = (overwrap > 0);
+ if (count <= overwrap) {
+ node.error(RED._("batch.count.invalid"));
+ return;
+ }
+ node.pending = [];
+ this.on("input", function(msg) {
+ var queue = node.pending;
+ queue.push(msg);
+ node.pending_count++;
+ if (queue.length === count) {
+ send_msgs(node, queue, is_overwrap);
+ node.pending =
+ (overwrap === 0) ? [] : queue.slice(-overwrap);
+ node.pending_count = 0;
+ }
+ var max_msgs = max_kept_msgs_count(node);
+ if ((max_msgs > 0) && (node.pending_count > max_msgs)) {
+ node.pending = [];
+ node.pending_count = 0;
+ node.error(RED._("batch.too-many"), msg);
+ }
+ });
+ this.on("close", function() {
+ node.pending_count = 0;
+ node.pending = [];
+ });
+ }
+ else if (mode === "interval") {
+ var interval = Number(n.interval || "0") *1000;
+ var allow_empty_seq = n.allowEmptySequence;
+ node.pending = []
+ var timer = setInterval(function() {
+ send_interval(node, allow_empty_seq);
+ node.pending_count = 0;
+ }, interval);
+ this.on("input", function(msg) {
+ node.pending.push(msg);
+ node.pending_count++;
+ var max_msgs = max_kept_msgs_count(node);
+ if ((max_msgs > 0) && (node.pending_count > max_msgs)) {
+ node.pending = [];
+ node.pending_count = 0;
+ node.error(RED._("batch.too-many"), msg);
+ }
+ });
+ this.on("close", function() {
+ clearInterval(timer);
+ node.pending = [];
+ node.pending_count = 0;
+ });
+ }
+ else if(mode === "concat") {
+ node.topics = (n.topics || []).map(function(x) {
+ return x.topic;
+ });
+ node.pending = {};
+ this.on("input", function(msg) {
+ concat_msg(node, msg);
+ });
+ this.on("close", function() {
+ node.pending = {};
+ node.pending_count = 0;
+ });
+ }
+ else {
+ node.error(RED._("batch.unexpected"));
+ }
+ }
+
+ RED.nodes.registerType("batch", BatchNode);
+}
diff --git a/settings.js b/settings.js
index cb634d47d..eca108e96 100644
--- a/settings.js
+++ b/settings.js
@@ -47,9 +47,9 @@ module.exports = {
// The maximum length, in characters, of any message sent to the debug sidebar tab
debugMaxLength: 1000,
- // The maximum number of messages kept internally in sort node.
+ // The maximum number of messages kept internally in nodes.
// Zero or undefined value means not restricting number of messages.
- //sortMaxKeptMsgsCount: 0,
+ //maxKeptMsgsCount: 0,
// To disable the option for using local files for storing keys and certificates in the TLS configuration
// node, set this to true
diff --git a/test/editor/editor_helper.js b/test/editor/editor_helper.js
index 733df8a87..281988484 100644
--- a/test/editor/editor_helper.js
+++ b/test/editor/editor_helper.js
@@ -14,67 +14,125 @@
* limitations under the License.
**/
-var RED = require("../../red/red.js");
-
var when = require("when");
var http = require('http');
var express = require("express");
var fs = require('fs-extra');
+var path = require('path');
var app = express();
+var RED = require("../../red/red.js");
+
+var utilPage = require("./pageobjects/util/util_page");
+
var server;
var homeDir = './test/resources/home';
var address = '127.0.0.1';
var listenPort = 0; // use ephemeral port
var url;
+/*
+ * Set false when you need a flow to reproduce the failed test case.
+ * The flow file is under "homeDir" defined above.
+ */
+var isDeleteFlow = true;
-function cleanup() {
- var flowsFile = homeDir + '/flows_'+require('os').hostname()+'.json';
+function getFlowFilename() {
+ var orig = Error.prepareStackTrace;
+ var err = new Error();
+ Error.prepareStackTrace = function (err, stack) {
+ return stack;
+ };
+ // Two level higher caller is the actual caller (e.g. a caller of startServer).
+ var filepath = err.stack[2].getFileName();
+ var filename = path.basename(filepath, ".js");
+ Error.prepareStackTrace = orig;
+
+ var flowFile = 'flows_' + filename + '.json';
+ return flowFile;
+}
+
+function cleanup(flowFile) {
+ deleteFile(homeDir+"/"+flowFile);
+ deleteFile(homeDir+"/."+flowFile+".backup");
+}
+
+function deleteFile(flowFile) {
try {
- fs.statSync(flowsFile);
- fs.unlinkSync(flowsFile);
- } catch (err) {
- }
+ fs.statSync(flowFile);
+ if (isDeleteFlow) {
+ fs.unlinkSync(flowFile);
+ }
+ } catch (e) {}
};
module.exports = {
- startServer: function(done) {
- cleanup();
- app.use("/",express.static("public"));
- server = http.createServer(app);
- var settings = {
- httpAdminRoot: "/",
- httpNodeRoot: "/api",
- userDir: homeDir,
- functionGlobalContext: { }, // enables global context
- SKIP_BUILD_CHECK: true,
- logging: {console: {level:'off'}}
- };
- RED.init(server, settings);
- app.use(settings.httpAdminRoot,RED.httpAdmin);
- app.use(settings.httpNodeRoot,RED.httpNode);
- server.listen(listenPort, address);
- server.on('listening', function() {
- var port = server.address().port;
- url = 'http://' + address + ':' + port;
- });
- RED.start().then(function() {
- done();
- });
+ startServer: function() {
+ try{
+ utilPage.init();
+
+ // Name a flow file including caller filename so that multiple Node-RED servers can run simultaneously.
+ // Call this method here because retrieving the caller filename by call stack.
+ var flowFilename = getFlowFilename();
+ browser.windowHandleMaximize();
+ browser.call(function () {
+ // return when.promise(function(resolve, reject) {
+ return new Promise(function(resolve, reject) {
+ cleanup(flowFilename);
+ app.use("/",express.static("public"));
+ server = http.createServer(app);
+ var settings = {
+ httpAdminRoot: "/",
+ httpNodeRoot: "/api",
+ userDir: homeDir,
+ flowFile: flowFilename,
+ functionGlobalContext: { }, // enables global context
+ SKIP_BUILD_CHECK: true,
+ logging: {console: {level:'off'}}
+ };
+ RED.init(server, settings);
+ app.use(settings.httpAdminRoot,RED.httpAdmin);
+ app.use(settings.httpNodeRoot,RED.httpNode);
+ server.listen(listenPort, address);
+ server.on('listening', function() {
+ var port = server.address().port;
+ url = 'http://' + address + ':' + port;
+ });
+ RED.start().then(function() {
+ resolve();
+ });
+ });
+ });
+ browser.url(url);
+ browser.waitForExist('#palette_node_inject');
+ } catch (err) {
+ console.log(err);
+ throw err;
+ }
},
stopServer: function(done) {
- if (server) {
- try {
- RED.stop().then(function() {
- server.close(done);
- cleanup();
- done();
+ try {
+ // Call this method here because retrieving the caller filename by call stack.
+ var flowFilename = getFlowFilename();
+ browser.call(function () {
+ browser.close(); // need to call this inside browser.call().
+ return when.promise(function(resolve, reject) {
+ if (server) {
+ RED.stop().then(function() {
+ server.close(function() {
+ cleanup(flowFilename);
+ resolve();
+ });
+ });
+ } else {
+ cleanup(flowFilename);
+ resolve();
+ }
});
- } catch(e) {
- cleanup();
- done();
- }
+ });
+ } catch (err) {
+ console.log(err);
+ throw err;
}
},
diff --git a/test/editor/pageobjects/nodes/core/core/20-inject_page.js b/test/editor/pageobjects/nodes/core/core/20-inject_page.js
new file mode 100644
index 000000000..09aa8cca1
--- /dev/null
+++ b/test/editor/pageobjects/nodes/core/core/20-inject_page.js
@@ -0,0 +1,52 @@
+/**
+ * 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.
+ **/
+
+var util = require("util");
+
+var nodePage = require("../../node_page");
+
+function injectNode(id) {
+ nodePage.call(this, id);
+}
+
+util.inherits(injectNode, nodePage);
+
+var payloadType = {
+ "flow": 1,
+ "global": 2,
+ "string": 3,
+ "num": 4,
+ "bool": 5,
+ "json": 6,
+ "bin": 7,
+ "date": 8,
+};
+
+injectNode.prototype.setPayload = function(type, value) {
+ // Open a payload type list.
+ browser.clickWithWait('//*[contains(@class, "red-ui-typedInput-container")]');
+ // Select a payload type.
+ var payloadTypeXPath = '//*[@class="red-ui-typedInput-options"]/a[' + payloadType[type] + ']';
+ browser.clickWithWait(payloadTypeXPath);
+ // Input a value.
+ browser.setValue('#node-input-payload', value);
+}
+
+injectNode.prototype.setTopic = function(value) {
+ browser.setValue('#node-input-topic', value);
+}
+
+module.exports = injectNode;
diff --git a/test/editor/pageobjects/workspace/editWindow_page.js b/test/editor/pageobjects/nodes/core/core/58-debug_page.js
similarity index 76%
rename from test/editor/pageobjects/workspace/editWindow_page.js
rename to test/editor/pageobjects/nodes/core/core/58-debug_page.js
index 25ceecc8f..f87f29646 100644
--- a/test/editor/pageobjects/workspace/editWindow_page.js
+++ b/test/editor/pageobjects/nodes/core/core/58-debug_page.js
@@ -14,11 +14,14 @@
* limitations under the License.
**/
-function clickOk() {
- browser.click('#node-dialog-ok');
- browser.pause(300);
+var util = require("util");
+
+var nodePage = require("../../node_page");
+
+function debugNode(id) {
+ nodePage.call(this, id);
}
-module.exports = {
- clickOk: clickOk,
-};
+util.inherits(debugNode, nodePage);
+
+module.exports = debugNode;
diff --git a/test/editor/pageobjects/nodes/core/logic/15-change_page.js b/test/editor/pageobjects/nodes/core/logic/15-change_page.js
new file mode 100644
index 000000000..aa8b8a73b
--- /dev/null
+++ b/test/editor/pageobjects/nodes/core/logic/15-change_page.js
@@ -0,0 +1,53 @@
+/**
+ * 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.
+ **/
+
+var util = require("util");
+
+var nodePage = require("../../node_page");
+
+function changeNode(id) {
+ nodePage.call(this, id);
+}
+
+util.inherits(changeNode, nodePage);
+
+function setT(rule, index) {
+ browser.selectByValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/select', rule);
+}
+
+changeNode.prototype.ruleSet = function(to, index) {
+ index = index ? index : 1;
+ setT("set", index);
+ browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[2]/div[2]/div/input', to);
+}
+
+changeNode.prototype.ruleDelete = function(index) {
+ index = index ? index : 1;
+ setT("delete", index);
+}
+
+changeNode.prototype.ruleMove = function(p, to, index) {
+ index = index ? index : 1;
+ setT("move", index);
+ browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[1]/div/div/input', p);
+ browser.setValue('//*[@id="node-input-rule-container"]/li[' + index + ']/div/div[4]/div[2]/div/input', to);
+}
+
+changeNode.prototype.addRule = function() {
+ browser.clickWithWait('//*[@id="dialog-form"]/div[3]/div/a');
+}
+
+module.exports = changeNode;
diff --git a/test/editor/pageobjects/nodes/core/logic/16-range_page.js b/test/editor/pageobjects/nodes/core/logic/16-range_page.js
new file mode 100644
index 000000000..630abc86a
--- /dev/null
+++ b/test/editor/pageobjects/nodes/core/logic/16-range_page.js
@@ -0,0 +1,38 @@
+/**
+ * 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.
+ **/
+
+var util = require("util");
+
+var nodePage = require("../../node_page");
+
+function rangeNode(id) {
+ nodePage.call(this, id);
+}
+
+util.inherits(rangeNode, nodePage);
+
+rangeNode.prototype.setAction = function(value) {
+ browser.selectByValue('#node-input-action', value);
+}
+
+rangeNode.prototype.setRange = function(minin, maxin, minout, maxout) {
+ browser.setValue('#node-input-minin', minin);
+ browser.setValue('#node-input-maxin', maxin);
+ browser.setValue('#node-input-minout', minout);
+ browser.setValue('#node-input-maxout', maxout);
+}
+
+module.exports = rangeNode;
diff --git a/test/editor/pageobjects/workspace/node_page.js b/test/editor/pageobjects/nodes/node_page.js
similarity index 59%
rename from test/editor/pageobjects/workspace/node_page.js
rename to test/editor/pageobjects/nodes/node_page.js
index 3fd5ba630..3937f15b7 100644
--- a/test/editor/pageobjects/workspace/node_page.js
+++ b/test/editor/pageobjects/nodes/node_page.js
@@ -14,29 +14,21 @@
* limitations under the License.
**/
-var icons = {
- // input
- "inject": "icons/node-red/inject.png",
- // output
- "debug": "icons/node-red/debug.png",
- // function
- "change": "icons/node-red/swap.png",
-};
-
-function getIdWithIcon(icon) {
- //*[name()="image" and @*="icons/node-red/inject.png"]/../..
- var id = browser.getAttribute('//*[name()="image" and @*="' + icon + '"]/../..', 'id');
- return id;
-}
-
-function Node(type) {
- this.id = '//*[@id="' + getIdWithIcon(icons[type]) + '"]';
+function Node(id) {
+ this.id = '//*[@id="' + id + '"]';
}
Node.prototype.edit = function() {
- browser.click(this.id);
- browser.click(this.id);
- browser.pause(500); // Necessary for headless mode.
+ browser.clickWithWait(this.id);
+ browser.clickWithWait(this.id);
+ // Wait until an edit dialog opens.
+ browser.waitForVisible('#node-dialog-ok', 2000);
+}
+
+Node.prototype.clickOk = function() {
+ browser.clickWithWait('#node-dialog-ok');
+ // Wait untile an edit dialog closes.
+ browser.waitForVisible('#node-dialog-ok', 2000, true);
}
Node.prototype.connect = function(targetNode) {
@@ -46,7 +38,7 @@ Node.prototype.connect = function(targetNode) {
}
Node.prototype.clickLeftButton = function() {
- browser.click(this.id + '/*[@class="node_button node_left_button"]');
+ browser.clickWithWait(this.id + '/*[@class="node_button node_left_button"]');
}
module.exports = Node;
diff --git a/test/editor/pageobjects/nodes/nodefactory_page.js b/test/editor/pageobjects/nodes/nodefactory_page.js
new file mode 100644
index 000000000..8d06d3342
--- /dev/null
+++ b/test/editor/pageobjects/nodes/nodefactory_page.js
@@ -0,0 +1,39 @@
+/**
+ * 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.
+ **/
+
+var injectNode = require('./core/core/20-inject_page');
+var debugNode = require('./core/core/58-debug_page');
+var changeNode = require('./core/logic/15-change_page');
+var rangeNode = require('./core/logic/16-range_page');
+
+var nodeCatalog = {
+ // input
+ "inject": injectNode,
+ // output
+ "debug": debugNode,
+ // function
+ "change": changeNode,
+ "range": rangeNode,
+}
+
+function create(type, id) {
+ var node = nodeCatalog[type];
+ return new node(id);
+}
+
+module.exports = {
+ create: create,
+};
diff --git a/test/editor/pageobjects/util/util_page.js b/test/editor/pageobjects/util/util_page.js
new file mode 100644
index 000000000..e2b6c3177
--- /dev/null
+++ b/test/editor/pageobjects/util/util_page.js
@@ -0,0 +1,45 @@
+/**
+ * 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.
+ **/
+
+function init() {
+ browser.addCommand("clickWithWait", function(selector) {
+ browser.waitForVisible(selector);
+ // Wait at most 10 seconds.
+ for (var i = 0; i < 50; i++) {
+ try {
+ var ret = browser.click(selector);
+ return ret;
+ } catch (err) {
+ if (err.message.indexOf('is not clickable') !== -1) {
+ browser.pause(200);
+ } else {
+ throw err;
+ }
+ }
+ }
+ }, false);
+
+ browser.addCommand("getTextWithWait", function(selector) {
+ browser.waitForExist(selector);
+ browser.waitForValue(selector);
+ var ret = browser.getText(selector);
+ return ret;
+ }, false);
+}
+
+ module.exports = {
+ init: init,
+ };
diff --git a/test/editor/pageobjects/workspace/debugTab_page.js b/test/editor/pageobjects/workspace/debugTab_page.js
index c46314b27..c66122d70 100644
--- a/test/editor/pageobjects/workspace/debugTab_page.js
+++ b/test/editor/pageobjects/workspace/debugTab_page.js
@@ -15,17 +15,17 @@
**/
function open() {
- browser.click('#red-ui-tab-debug');
+ browser.clickWithWait('#red-ui-tab-debug');
}
-function getMessage() {
- var debugMessagePath = '//div[@class="debug-content debug-content-list"]//span[contains(@class, "debug-message-type")]';
- browser.waitForExist(debugMessagePath);
- return browser.getText(debugMessagePath);
+function getMessage(index) {
+ index = index ? index : 1;
+ var debugMessagePath = '//div[@class="debug-content debug-content-list"]/div[contains(@class,"debug-message")][' + index + ']//span[contains(@class, "debug-message-type")]';
+ return browser.getTextWithWait(debugMessagePath);
}
function clearMessage() {
- browser.click('//a[@id="debug-tab-clear"]');
+ browser.clickWithWait('//a[@id="debug-tab-clear"]');
}
module.exports = {
diff --git a/test/editor/pageobjects/workspace/palette_page.js b/test/editor/pageobjects/workspace/palette_page.js
new file mode 100644
index 000000000..764f5e55c
--- /dev/null
+++ b/test/editor/pageobjects/workspace/palette_page.js
@@ -0,0 +1,33 @@
+/**
+ * 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.
+ **/
+
+var idMap = {
+ // input
+ "inject": "#palette_node_inject",
+ // output
+ "debug": "#palette_node_debug",
+ // function
+ "change": "#palette_node_change",
+ "range": "#palette_node_range",
+};
+
+function getId(type) {
+ return idMap[type];
+}
+
+module.exports = {
+ getId: getId,
+};
diff --git a/test/editor/pageobjects/workspace/workspace_page.js b/test/editor/pageobjects/workspace/workspace_page.js
index 95b8d2260..6c802486d 100644
--- a/test/editor/pageobjects/workspace/workspace_page.js
+++ b/test/editor/pageobjects/workspace/workspace_page.js
@@ -14,26 +14,25 @@
* limitations under the License.
**/
- var when = require('when');
+ var when = require("when");
var events = require("../../../../red/runtime/events.js");
-var node = require('./node_page');
-
-var palette = {
- "inject": "#palette_node_inject",
- "debug": "#palette_node_debug",
- "change": "#palette_node_change",
-};
+var palette = require("./palette_page");
+var nodeFactory = require("../nodes/nodefactory_page");
function addNode(type, x, y) {
var offsetX = x ? x : 0;
var offsetY = y ? y : 0;
- browser.moveToObject(palette[type]);
+ browser.moveToObject(palette.getId(type));
browser.buttonDown();
browser.moveToObject("#palette-search", offsetX + 300, offsetY + 100); // adjust to the top-left corner of workspace.
browser.buttonUp();
- return new node(type);
+ // Last node is the one that has been created right now.
+ var nodeElement = browser.elements('//*[@class="node nodegroup"][last()]');
+ var nodeId = nodeElement.getAttribute('id');
+ var node = nodeFactory.create(type, nodeId);
+ return node;
}
function deleteAllNodes() {
@@ -49,10 +48,10 @@ function deploy() {
resolve();
}
});
- browser.click('#btn-deploy');
+ browser.clickWithWait('#btn-deploy');
});
});
- browser.pause(500); // Necessary for headless mode.
+ browser.waitForText('#btn-deploy', 2000);
}
module.exports = {
diff --git a/test/editor/specs/editor_uispec.js b/test/editor/specs/editor_uispec.js
deleted file mode 100644
index e14b89250..000000000
--- a/test/editor/specs/editor_uispec.js
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * 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.
- **/
-
-var when = require('when');
-var should = require("should");
-var fs = require('fs-extra');
-
-var helper = require("../editor_helper");
-var editWindow = require('../pageobjects/workspace/editWindow_page');
-var debugTab = require('../pageobjects/workspace/debugTab_page');
-var workspace = require('../pageobjects/workspace/workspace_page');
-
-var nodeWidth = 200;
-
-describe('Node-RED main page', function() {
- beforeEach(function() {
- workspace.deleteAllNodes();
- });
-
- before(function() {
- browser.windowHandleMaximize();
- browser.call(function () {
- return when.promise(function(resolve, reject) {
- helper.startServer(function() {
- resolve();
- });
- });
- });
- browser.url(helper.url());
- browser.waitForExist('#palette_node_inject');
- });
-
- after(function() {
- browser.call(function () {
- return when.promise(function(resolve, reject) {
- helper.stopServer(function() {
- resolve();
- });
- });
- });
- });
-
- it('should have a right title', function () {
- browser.getTitle().should.equal('Node-RED');
- });
-
- it('should output a timestamp', function() {
- var injectNode = workspace.addNode("inject");
- var debugNode = workspace.addNode("debug", nodeWidth);
- injectNode.connect(debugNode);
-
- workspace.deploy();
-
- debugTab.open();
- debugTab.clearMessage();
- injectNode.clickLeftButton();
- debugTab.getMessage().should.within(1500000000000, 3000000000000);
- });
-
- it('should set a message property to a fixed value', function() {
- var injectNode = workspace.addNode("inject");
- var changeNode = workspace.addNode("change", nodeWidth);
- var debugNode = workspace.addNode("debug", nodeWidth * 2);
-
- changeNode.edit();
- browser.setValue('.node-input-rule-property-value', 'Hello World!');
- editWindow.clickOk();
-
- injectNode.connect(changeNode);
- changeNode.connect(debugNode);
-
- workspace.deploy();
-
- debugTab.open();
- debugTab.clearMessage();
- injectNode.clickLeftButton();
- debugTab.getMessage().should.be.equal('"Hello World!"');
- });
-});
diff --git a/test/editor/specs/scenario/cookbook_uispec.js b/test/editor/specs/scenario/cookbook_uispec.js
new file mode 100644
index 000000000..fb3b5705e
--- /dev/null
+++ b/test/editor/specs/scenario/cookbook_uispec.js
@@ -0,0 +1,145 @@
+/**
+ * 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.
+ **/
+
+var when = require('when');
+var should = require("should");
+var fs = require('fs-extra');
+
+var helper = require("../../editor_helper");
+var debugTab = require('../../pageobjects/workspace/debugTab_page');
+var workspace = require('../../pageobjects/workspace/workspace_page');
+
+var nodeWidth = 200;
+
+// https://cookbook.nodered.org/
+describe('cookbook', function() {
+ beforeEach(function() {
+ workspace.deleteAllNodes();
+ });
+
+ before(function() {
+ helper.startServer();
+ });
+
+ after(function() {
+ helper.stopServer();
+ });
+
+ describe('messages', function() {
+ it('set a message property to a fixed value', function() {
+ var injectNode = workspace.addNode("inject");
+ var changeNode = workspace.addNode("change", nodeWidth);
+ var debugNode = workspace.addNode("debug", nodeWidth * 2);
+
+ changeNode.edit();
+ changeNode.ruleSet("Hello World!");
+ changeNode.clickOk();
+
+ injectNode.connect(changeNode);
+ changeNode.connect(debugNode);
+
+ workspace.deploy();
+
+ debugTab.open();
+ debugTab.clearMessage();
+ injectNode.clickLeftButton();
+ debugTab.getMessage().should.be.equal('"Hello World!"');
+ });
+
+ it('delete a message property', function() {
+ var injectNode = workspace.addNode("inject");
+ var changeNode = workspace.addNode("change", nodeWidth);
+ var debugNode = workspace.addNode("debug", nodeWidth * 2);
+
+ changeNode.edit();
+ changeNode.ruleDelete();
+ changeNode.clickOk();
+
+ injectNode.connect(changeNode);
+ changeNode.connect(debugNode);
+
+ workspace.deploy();
+
+ debugTab.open();
+ debugTab.clearMessage();
+ injectNode.clickLeftButton();
+ debugTab.getMessage().should.be.equal("undefined");
+ });
+
+ it('move a message property', function() {
+ var injectNode = workspace.addNode("inject");
+ var changeNode = workspace.addNode("change", nodeWidth);
+ var debugNode = workspace.addNode("debug", nodeWidth * 2);
+
+ injectNode.edit();
+ injectNode.setTopic("Hello");
+ injectNode.clickOk();
+
+ changeNode.edit();
+ changeNode.ruleMove("topic", "payload");
+ changeNode.clickOk();
+
+ injectNode.connect(changeNode);
+ changeNode.connect(debugNode);
+
+ workspace.deploy();
+
+ debugTab.open();
+ debugTab.clearMessage();
+ injectNode.clickLeftButton();
+ debugTab.getMessage().should.be.equal('"Hello"');
+ });
+
+ it('map a property between different numeric ranges', function() {
+ var injectNode1 = workspace.addNode("inject");
+ var injectNode2 = workspace.addNode("inject", 0, 50);
+ var injectNode3 = workspace.addNode("inject", 0, 100);
+ var rangeNode = workspace.addNode("range", nodeWidth);
+ var debugNode = workspace.addNode("debug", nodeWidth * 2);
+
+ injectNode1.edit();
+ injectNode1.setPayload("num", 0);
+ injectNode1.clickOk();
+ injectNode2.edit();
+ injectNode2.setPayload("num", 512);
+ injectNode2.clickOk();
+ injectNode3.edit();
+ injectNode3.setPayload("num", 1023);
+ injectNode3.clickOk();
+
+ rangeNode.edit();
+ rangeNode.setAction("clamp");
+ rangeNode.setRange(0, 1023, 0, 5);
+ rangeNode.clickOk();
+
+ injectNode1.connect(rangeNode);
+ injectNode2.connect(rangeNode);
+ injectNode3.connect(rangeNode);
+ rangeNode.connect(debugNode);
+
+ workspace.deploy();
+
+ debugTab.open();
+ debugTab.clearMessage();
+ injectNode1.clickLeftButton();
+ debugTab.getMessage(1).should.be.equal('0');
+ injectNode2.clickLeftButton();
+ debugTab.getMessage(2).should.be.equal('2.5024437927663734');
+ injectNode3.clickLeftButton();
+ debugTab.getMessage(3).should.be.equal('5');
+ });
+ });
+});
diff --git a/test/editor/specs/workspace/workspace_uispec.js b/test/editor/specs/workspace/workspace_uispec.js
new file mode 100644
index 000000000..0b901cc1f
--- /dev/null
+++ b/test/editor/specs/workspace/workspace_uispec.js
@@ -0,0 +1,57 @@
+/**
+ * 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.
+ **/
+
+var when = require('when');
+var should = require("should");
+var fs = require('fs-extra');
+
+var helper = require("../../editor_helper");
+var debugTab = require('../../pageobjects/workspace/debugTab_page');
+var workspace = require('../../pageobjects/workspace/workspace_page');
+
+var nodeWidth = 200;
+
+describe('Workspace', function() {
+ beforeEach(function() {
+ workspace.deleteAllNodes();
+ });
+
+ before(function() {
+ helper.startServer();
+ });
+
+ after(function() {
+ helper.stopServer();
+ });
+
+ it('should have a right title', function () {
+ browser.getTitle().should.equal('Node-RED');
+ });
+
+ it('should output a timestamp', function() {
+ var injectNode = workspace.addNode("inject");
+ var debugNode = workspace.addNode("debug", nodeWidth);
+ injectNode.connect(debugNode);
+
+ workspace.deploy();
+
+ debugTab.open();
+ debugTab.clearMessage();
+ injectNode.clickLeftButton();
+ debugTab.getMessage().should.within(1500000000000, 3000000000000);
+ });
+
+});
diff --git a/test/nodes/core/core/89-trigger_spec.js b/test/nodes/core/core/89-trigger_spec.js
index 53b13564a..9faff3bb3 100644
--- a/test/nodes/core/core/89-trigger_spec.js
+++ b/test/nodes/core/core/89-trigger_spec.js
@@ -609,11 +609,70 @@ describe('trigger node', function() {
n1.emit("input", {payload:null}); // trigger
n1.emit("input", {payload:null}); // blocked
n1.emit("input", {payload:null}); // blocked
+ n1.emit("input", {payload:"foo"}); // don't clear the blockage
n1.emit("input", {payload:"boo"}); // clear the blockage
n1.emit("input", {payload:null}); // trigger
});
});
+ it('should be able to set infinite timeout, and clear timeout by boolean true', function(done) {
+ var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", reset:"true", duration:"0", wires:[["n2"]] },
+ {id:"n2", type:"helper"} ];
+ helper.load(triggerNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ var c = 0;
+ n2.on("input", function(msg) {
+ try {
+ c += 1;
+ msg.should.have.a.property("payload", "1");
+ }
+ catch(err) { done(err); }
+ });
+ setTimeout( function() {
+ if (c === 2) { done(); }
+ else {
+ done(new Error("Too many messages received"));
+ }
+ },20);
+ n1.emit("input", {payload:null}); // trigger
+ n1.emit("input", {payload:null}); // blocked
+ n1.emit("input", {payload:null}); // blocked
+ n1.emit("input", {payload:false}); // don't clear the blockage
+ n1.emit("input", {payload:true}); // clear the blockage
+ n1.emit("input", {payload:null}); // trigger
+ });
+ });
+
+ it('should be able to set infinite timeout, and clear timeout by boolean false', function(done) {
+ var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", reset:"false", duration:"0", wires:[["n2"]] },
+ {id:"n2", type:"helper"} ];
+ helper.load(triggerNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ var c = 0;
+ n2.on("input", function(msg) {
+ try {
+ c += 1;
+ msg.should.have.a.property("payload", "1");
+ }
+ catch(err) { done(err); }
+ });
+ setTimeout( function() {
+ if (c === 2) { done(); }
+ else {
+ done(new Error("Too many messages received"));
+ }
+ },20);
+ n1.emit("input", {payload:null}); // trigger
+ n1.emit("input", {payload:null}); // blocked
+ n1.emit("input", {payload:null}); // blocked
+ n1.emit("input", {payload:"foo"}); // don't clear the blockage
+ n1.emit("input", {payload:false}); // clear the blockage
+ n1.emit("input", {payload:null}); // trigger
+ });
+ });
+
it('should be able to set a repeat, and clear loop by reset', function(done) {
var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", reset:"boo", op1:"", op1type:"pay", duration:-25, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
diff --git a/test/nodes/core/logic/10-switch_spec.js b/test/nodes/core/logic/10-switch_spec.js
index c910c37e4..a34f0947e 100644
--- a/test/nodes/core/logic/10-switch_spec.js
+++ b/test/nodes/core/logic/10-switch_spec.js
@@ -18,6 +18,7 @@ var should = require("should");
var switchNode = require("../../../../nodes/core/logic/10-switch.js");
var helper = require("../../helper.js");
+var RED = require("../../../../red/red.js");
describe('switch Node', function() {
@@ -28,6 +29,7 @@ describe('switch Node', function() {
afterEach(function(done) {
helper.unload();
helper.stopServer(done);
+ RED.settings.switchMaxKeptMsgsCount = 0;
});
it('should be loaded with some defaults', function(done) {
@@ -119,6 +121,44 @@ describe('switch Node', function() {
});
}
+ function customFlowSequenceSwitchTest(flow, seq_in, seq_out, done) {
+ helper.load(switchNode, flow, function() {
+ var switchNode1 = helper.getNode("switchNode1");
+ var helperNode1 = helper.getNode("helperNode1");
+ var sid = undefined;
+ var count = 0;
+ helperNode1.on("input", function(msg) {
+ try {
+ msg.should.have.property("payload", seq_out[count]);
+ msg.should.have.property("parts");
+ var parts = msg.parts;
+ parts.should.have.property("id");
+ var id = parts.id;
+ if (sid === undefined) {
+ sid = id;
+ }
+ else {
+ id.should.equal(sid);
+ }
+ parts.should.have.property("index", count);
+ parts.should.have.property("count", seq_out.length);
+ count++;
+ if (count === seq_out.length) {
+ done();
+ }
+ } catch (e) {
+ done(e);
+ }
+ });
+ var len = seq_in.length;
+ for (var i = 0; i < len; i++) {
+ var parts = {index:i, count:len, id:222};
+ var msg = {payload:seq_in[i], parts:parts};
+ switchNode1.receive(msg);
+ }
+ });
+ }
+
it('should check if payload equals given value', function(done) {
genericSwitchTest("eq", "Hello", true, true, "Hello", done);
});
@@ -498,4 +538,176 @@ describe('switch Node', function() {
{id:"helperNode1", type:"helper", wires:[]}];
customFlowSwitchTest(flow, true, -5, done);
});
+
+ it('should take head of message sequence', function(done) {
+ var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"head","v":3}],checkall:true,outputs:1,wires:[["helperNode1"]]},
+ {id:"helperNode1", type:"helper", wires:[]}];
+ customFlowSequenceSwitchTest(flow, [0, 1, 2, 3, 4], [0, 1, 2], done);
+ });
+
+ it('should take tail of message sequence', function(done) {
+ var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"tail","v":3}],checkall:true,outputs:1,wires:[["helperNode1"]]},
+ {id:"helperNode1", type:"helper", wires:[]}];
+ customFlowSequenceSwitchTest(flow, [0, 1, 2, 3, 4], [2, 3, 4], done);
+ });
+
+ it('should take slice of message sequence', function(done) {
+ var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"index","v":1,"v2":3}],checkall:true,outputs:1,wires:[["helperNode1"]]},
+ {id:"helperNode1", type:"helper", wires:[]}];
+ customFlowSequenceSwitchTest(flow, [0, 1, 2, 3, 4], [1,2, 3], done);
+ });
+
+ it('should check JSONata expression is true', function(done) {
+ var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",
+ rules:[{"t":"jsonata_exp","v":"payload%2 = 1","vt":"jsonata"}],
+ checkall:true,outputs:1,wires:[["helperNode1"]]},
+ {id:"helperNode1", type:"helper", wires:[]}];
+ customFlowSwitchTest(flow, true, 9, done);
+ });
+
+ it('should repair message sequence', function(done) {
+ var flow = [{id:"n1",type:"switch",name:"switchNode",property:"payload",
+ rules:[{"t":"gt","v":0},{"t":"lt","v":0},{"t":"else"}],
+ checkall:true,repair:true,
+ outputs:3,wires:[["n2"],["n3"],["n4"]]},
+ {id:"n2", type:"helper", wires:[]},
+ {id:"n3", type:"helper", wires:[]},
+ {id:"n4", type:"helper", wires:[]}
+ ];
+ helper.load(switchNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ var n3 = helper.getNode("n3");
+ var n4 = helper.getNode("n4");
+ var data = { "0":1, "1":-2, "2":2, "3":0, "4":-1 };
+ var count = 0;
+ function check_msg(msg, vf) {
+ try {
+ msg.should.have.property("payload");
+ var payload = msg.payload;
+ msg.should.have.property("parts");
+ vf(payload).should.be.ok;
+ var parts = msg.parts;
+ parts.should.have.property("id", 222);
+ parts.should.have.property("count", 5);
+ parts.should.have.property("index");
+ var index = parts.index;
+ payload.should.equal(data[index]);
+ count++;
+ if (count == 5) {
+ done();
+ }
+ }
+ catch (e) {
+ done(e);
+ }
+ }
+ n2.on("input", function(msg) {
+ check_msg(msg, function(x) { return(x < 0); });
+ });
+ n3.on("input", function(msg) {
+ check_msg(msg, function(x) { return(x === 0); });
+ });
+ n4.on("input", function(msg) {
+ check_msg(msg, function(x) { return(x > 0); });
+ });
+ for(var i in data) {
+ n1.receive({payload: data[i], parts:{index:i,count:5,id:222}});
+ }
+ });
+ });
+
+ it('should create message sequence for each port', function(done) {
+ var flow = [{id:"n1",type:"switch",name:"switchNode",property:"payload",
+ rules:[{"t":"gt","v":0},{"t":"lt","v":0},{"t":"else"}],
+ checkall:true,repair:false,
+ outputs:3,wires:[["n2"],["n3"],["n4"]]},
+ {id:"n2", type:"helper", wires:[]}, // >0
+ {id:"n3", type:"helper", wires:[]}, // <0
+ {id:"n4", type:"helper", wires:[]} // ==0
+ ];
+ helper.load(switchNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ var n3 = helper.getNode("n3");
+ var n4 = helper.getNode("n4");
+ var data = [ 1, -2, 2, 0, -1 ];
+ var vals = [[1, 2], [-2, -1], [0]];
+ var ids = [undefined, undefined, undefined];
+ var counts = [0, 0, 0];
+ var count = 0;
+ function check_msg(msg, ix, vf) {
+ try {
+ msg.should.have.property("payload");
+ var payload = msg.payload;
+ msg.should.have.property("parts");
+ vf(payload).should.be.ok;
+ var parts = msg.parts;
+ var evals = vals[ix];
+ parts.should.have.property("count", evals.length);
+ parts.should.have.property("id");
+ var id = parts.id;
+ if (ids[ix] === undefined) {
+ ids[ix] = id;
+ }
+ else {
+ ids[ix].should.equal(id);
+ }
+ parts.should.have.property("index");
+ var index = parts.index;
+ var eindex = counts[ix];
+ var eval = evals[eindex];
+ payload.should.equal(eval);
+ counts[ix]++;
+ count++;
+ if (count == 5) {
+ done();
+ }
+ }
+ catch (e) {
+ done(e);
+ }
+ }
+ n2.on("input", function(msg) {
+ check_msg(msg, 0, function(x) { return(x > 0); });
+ });
+ n3.on("input", function(msg) {
+ check_msg(msg, 1, function(x) { return(x < 0); });
+ });
+ n4.on("input", function(msg) {
+ check_msg(msg, 2, function(x) { return(x === 0); });
+ });
+ for(var i in data) {
+ n1.receive({payload: data[i], parts:{index:i,count:5,id:222}});
+ }
+ });
+ });
+
+ it('should handle too many pending messages', function(done) {
+ var flow = [{id:"n1",type:"switch",name:"switchNode",property:"payload",
+ rules:[{"t":"tail","v":2}],
+ checkall:true,repair:false,
+ outputs:3,wires:[["n2"]]},
+ {id:"n2", type:"helper", wires:[]}
+ ];
+ helper.load(switchNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ RED.settings.switchMaxKeptMsgsCount = 2;
+ setTimeout(function() {
+ var logEvents = helper.log().args.filter(function (evt) {
+ return evt[0].type == "switch";
+ });
+ var evt = logEvents[0][0];
+ evt.should.have.property('id', "n1");
+ evt.should.have.property('type', "switch");
+ evt.should.have.property('msg', "switch.errors.too-many");
+ done();
+ }, 150);
+ n1.receive({payload:3, parts:{index:2, count:4, id:222}});
+ n1.receive({payload:2, parts:{index:1, count:4, id:222}});
+ n1.receive({payload:4, parts:{index:3, count:4, id:222}});
+ n1.receive({payload:1, parts:{index:0, count:4, id:222}});
+ });
+ });
+
});
diff --git a/test/nodes/core/logic/17-split_spec.js b/test/nodes/core/logic/17-split_spec.js
index db092658e..e2c8aaa97 100644
--- a/test/nodes/core/logic/17-split_spec.js
+++ b/test/nodes/core/logic/17-split_spec.js
@@ -18,6 +18,7 @@ var should = require("should");
var splitNode = require("../../../../nodes/core/logic/17-split.js");
var joinNode = require("../../../../nodes/core/logic/17-split.js");
var helper = require("../../helper.js");
+var RED = require("../../../../red/red.js");
describe('SPLIT node', function() {
@@ -269,6 +270,7 @@ describe('JOIN node', function() {
afterEach(function() {
helper.unload();
+ RED.settings.joinMaxKeptMsgsCount = 0;
});
it('should be loaded', function(done) {
@@ -727,6 +729,394 @@ describe('JOIN node', function() {
});
s1.receive({payload:[[1,2,3],"a\nb\nc",[7,8,9]]});
});
- })
+ });
+
+ it('should merge messages with topics (single)', function(done) {
+ var flow = [{id:"n1", type:"join", mode:"merge",
+ topics:[{topic:"TA"}, {topic:"TB"}],
+ wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ helper.load(joinNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ var count = 0;
+ n2.on("input", function(msg) {
+ try {
+ msg.should.have.property("TA");
+ msg.should.have.property("TB");
+ msg.should.have.property("payload");
+ msg.payload.should.be.an.Array();
+ msg.payload.length.should.equal(2);
+ count++;
+ if (count === 1) {
+ msg.TA.should.equal("a");
+ msg.TB.should.equal("b");
+ msg.payload[0].should.equal("a");
+ msg.payload[1].should.equal("b");
+ }
+ if (count === 2) {
+ msg.TA.should.equal("d");
+ msg.TB.should.equal("c");
+ msg.payload[0].should.equal("d");
+ msg.payload[1].should.equal("c");
+ done();
+ }
+ }
+ catch(e) { done(e); }
+ });
+ n1.receive({payload:"a", topic:"TA"});
+ n1.receive({payload:"b", topic:"TB"});
+ n1.receive({payload:"c", topic:"TB"});
+ n1.receive({payload:"d", topic:"TA"});
+ });
+ });
+
+ it('should merge messages with topics (multiple)', function(done) {
+ var flow = [{id:"n1", type:"join", mode:"merge",
+ topics:[{topic:"TA"}, {topic:"TB"}, {topic:"TA"}],
+ wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ helper.load(joinNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ var count = 0;
+ n2.on("input", function(msg) {
+ try {
+ msg.should.have.property("TA");
+ msg.TA.should.be.an.Array();
+ msg.TA.length.should.equal(2);
+ msg.should.have.property("TB");
+ msg.should.have.property("payload");
+ msg.payload.should.be.an.Array();
+ msg.payload.length.should.equal(3);
+ count++;
+ if (count === 1) {
+ msg.TA[0].should.equal("a");
+ msg.TA[1].should.equal("d");
+ msg.TB.should.equal("b");
+ msg.payload[0].should.equal("a");
+ msg.payload[1].should.equal("b");
+ msg.payload[2].should.equal("d");
+ }
+ if (count === 2) {
+ msg.TA[0].should.equal("e");
+ msg.TA[1].should.equal("f");
+ msg.TB.should.equal("c");
+ msg.payload[0].should.equal("e");
+ msg.payload[1].should.equal("c");
+ msg.payload[2].should.equal("f");
+ done();
+ }
+ }
+ catch(e) { done(e); }
+ });
+ n1.receive({payload:"a", topic:"TA"});
+ n1.receive({payload:"b", topic:"TB"});
+ n1.receive({payload:"c", topic:"TB"});
+ n1.receive({payload:"d", topic:"TA"});
+ n1.receive({payload:"e", topic:"TA"});
+ n1.receive({payload:"f", topic:"TA"});
+ });
+ });
+
+ it('should merge messages with topics (single, send on new topic)', function(done) {
+ var flow = [{id:"n1", type:"join", mode:"merge",
+ topics:[{topic:"TA"}, {topic:"TB"}],
+ mergeOnChange:true,
+ wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ helper.load(joinNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ var count = 0;
+ n2.on("input", function(msg) {
+ try {
+ msg.should.have.property("TA");
+ msg.should.have.property("TB");
+ msg.should.have.property("payload");
+ msg.payload.should.be.an.Array();
+ msg.payload.length.should.equal(2);
+ count++;
+ if (count === 1) {
+ msg.TA.should.equal("a");
+ msg.TB.should.equal("b");
+ msg.payload[0].should.equal("a");
+ msg.payload[1].should.equal("b");
+ }
+ if (count === 2) {
+ msg.TA.should.equal("a");
+ msg.TB.should.equal("c");
+ msg.payload[0].should.equal("a");
+ msg.payload[1].should.equal("c");
+ }
+ if (count === 3) {
+ msg.TA.should.equal("d");
+ msg.TB.should.equal("c");
+ msg.payload[0].should.equal("d");
+ msg.payload[1].should.equal("c");
+ done();
+ }
+ }
+ catch(e) { done(e); }
+ });
+ n1.receive({payload:"a", topic:"TA"});
+ n1.receive({payload:"b", topic:"TB"});
+ n1.receive({payload:"c", topic:"TB"});
+ n1.receive({payload:"d", topic:"TA"});
+ });
+ });
+
+ it('should merge messages with topics (multiple, send on new topic)', function(done) {
+ var flow = [{id:"n1", type:"join", mode:"merge",
+ topics:[{topic:"TA"}, {topic:"TB"}, {topic:"TA"}],
+ mergeOnChange:true,
+ wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ helper.load(joinNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ var count = 0;
+ n2.on("input", function(msg) {
+ try {
+ msg.should.have.property("TA");
+ msg.TA.should.be.an.Array();
+ msg.TA.length.should.equal(2);
+ msg.should.have.property("TB");
+ msg.should.have.property("payload");
+ msg.payload.should.be.an.Array();
+ msg.payload.length.should.equal(3);
+ count++;
+ if (count === 1) {
+ msg.TA[0].should.equal("a");
+ msg.TA[1].should.equal("c");
+ msg.TB.should.equal("b");
+ msg.payload[0].should.equal("a");
+ msg.payload[1].should.equal("b");
+ msg.payload[2].should.equal("c");
+ }
+ if (count === 2) {
+ msg.TA[0].should.equal("c");
+ msg.TA[1].should.equal("d");
+ msg.TB.should.equal("b");
+ msg.payload[0].should.equal("c");
+ msg.payload[1].should.equal("b");
+ msg.payload[2].should.equal("d");
+ }
+ if (count === 3) {
+ msg.TA[0].should.equal("c");
+ msg.TA[1].should.equal("d");
+ msg.TB.should.equal("e");
+ msg.payload[0].should.equal("c");
+ msg.payload[1].should.equal("e");
+ msg.payload[2].should.equal("d");
+ done();
+ }
+ }
+ catch(e) { done(e); }
+ });
+ n1.receive({payload:"a", topic:"TA"});
+ n1.receive({payload:"b", topic:"TB"});
+ n1.receive({payload:"c", topic:"TA"});
+ n1.receive({payload:"d", topic:"TA"});
+ n1.receive({payload:"e", topic:"TB"});
+ });
+ });
+
+ it('should redece messages', function(done) {
+ var flow = [{id:"n1", type:"join", mode:"reduce",
+ reduceRight:false,
+ reduceExp:"$A+payload",
+ reduceInit:"0",
+ reduceInitType:"num",
+ reduceFixup:undefined,
+ wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ helper.load(joinNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ var count = 0;
+ n2.on("input", function(msg) {
+ try {
+ msg.should.have.property("payload");
+ msg.payload.should.equal(10);
+ done();
+ }
+ catch(e) { done(e); }
+ });
+ n1.receive({payload:3, parts:{index:2, count:4, id:222}});
+ n1.receive({payload:2, parts:{index:1, count:4, id:222}});
+ n1.receive({payload:4, parts:{index:3, count:4, id:222}});
+ n1.receive({payload:1, parts:{index:0, count:4, id:222}});
+ });
+ });
+
+ it('should redece messages using $I', function(done) {
+ var flow = [{id:"n1", type:"join", mode:"reduce",
+ reduceRight:false,
+ reduceExp:"$A+$I",
+ reduceInit:"0",
+ reduceInitType:"num",
+ reduceFixup:undefined,
+ wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ helper.load(joinNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ var count = 0;
+ n2.on("input", function(msg) {
+ try {
+ msg.should.have.property("payload");
+ msg.payload.should.equal(6);
+ done();
+ }
+ catch(e) { done(e); }
+ });
+ n1.receive({payload:3, parts:{index:2, count:4, id:222}});
+ n1.receive({payload:2, parts:{index:1, count:4, id:222}});
+ n1.receive({payload:4, parts:{index:3, count:4, id:222}});
+ n1.receive({payload:1, parts:{index:0, count:4, id:222}});
+ });
+ });
+
+ it('should redece messages with fixup', function(done) {
+ var flow = [{id:"n1", type:"join", mode:"reduce",
+ reduceRight:false,
+ reduceExp:"$A+payload",
+ reduceInit:"0",
+ reduceInitType:"num",
+ reduceFixup:"$A/$N",
+ wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ helper.load(joinNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ var count = 0;
+ n2.on("input", function(msg) {
+ try {
+ msg.should.have.property("payload");
+ msg.payload.should.equal(2);
+ done();
+ }
+ catch(e) { done(e); }
+ });
+ n1.receive({payload:3, parts:{index:2, count:5, id:222}});
+ n1.receive({payload:2, parts:{index:1, count:5, id:222}});
+ n1.receive({payload:4, parts:{index:3, count:5, id:222}});
+ n1.receive({payload:1, parts:{index:0, count:5, id:222}});
+ n1.receive({payload:0, parts:{index:4, count:5, id:222}});
+ });
+ });
+
+ it('should redece messages (left)', function(done) {
+ var flow = [{id:"n1", type:"join", mode:"reduce",
+ reduceRight:false,
+ reduceExp:"'(' & $A & '+' & payload & ')'",
+ reduceInit:"0",
+ reduceInitType:"str",
+ reduceFixup:undefined,
+ wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ helper.load(joinNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ var count = 0;
+ n2.on("input", function(msg) {
+ try {
+ msg.should.have.property("payload");
+ msg.payload.should.be.an.String();
+ msg.payload.should.equal("((((0+1)+2)+3)+4)");
+ done();
+ }
+ catch(e) { done(e); }
+ });
+ n1.receive({payload:'3', parts:{index:2, count:4, id:222}});
+ n1.receive({payload:'2', parts:{index:1, count:4, id:222}});
+ n1.receive({payload:'4', parts:{index:3, count:4, id:222}});
+ n1.receive({payload:'1', parts:{index:0, count:4, id:222}});
+ });
+ });
+
+ it('should redece messages (right)', function(done) {
+ var flow = [{id:"n1", type:"join", mode:"reduce",
+ reduceRight:true,
+ reduceExp:"'(' & $A & '+' & payload & ')'",
+ reduceInit:"0",
+ reduceInitType:"str",
+ reduceFixup:undefined,
+ wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ helper.load(joinNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ var count = 0;
+ n2.on("input", function(msg) {
+ try {
+ msg.should.have.property("payload");
+ msg.payload.should.be.an.String();
+ msg.payload.should.equal("((((0+4)+3)+2)+1)");
+ done();
+ }
+ catch(e) { done(e); }
+ });
+ n1.receive({payload:'3', parts:{index:2, count:4, id:222}});
+ n1.receive({payload:'2', parts:{index:1, count:4, id:222}});
+ n1.receive({payload:'4', parts:{index:3, count:4, id:222}});
+ n1.receive({payload:'1', parts:{index:0, count:4, id:222}});
+ });
+ });
+
+ it('should handle too many pending messages for merge mode', function(done) {
+ var flow = [{id:"n1", type:"join", mode:"merge",
+ topics:[{topic:"TA"}, {topic:"TA"}, {topic:"TB"}],
+ wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ helper.load(joinNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ RED.settings.joinMaxKeptMsgsCount = 2;
+ setTimeout(function() {
+ var logEvents = helper.log().args.filter(function (evt) {
+ return evt[0].type == "join";
+ });
+ var evt = logEvents[0][0];
+ evt.should.have.property('id', "n1");
+ evt.should.have.property('type', "join");
+ evt.should.have.property('msg', "join.too-many");
+ done();
+ }, 150);
+ n1.receive({payload:"a", topic:"TA"});
+ n1.receive({payload:"b", topic:"TB"});
+ n1.receive({payload:"c", topic:"TB"});
+ n1.receive({payload:"d", topic:"TA"});
+ });
+ });
+
+ it('should handle too many pending messages for reduce mode', function(done) {
+ var flow = [{id:"n1", type:"join", mode:"reduce",
+ reduceRight:false,
+ reduceExp:"$A+payload",
+ reduceInit:"0",
+ reduceInitType:"num",
+ reduceFixup:undefined,
+ wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ helper.load(joinNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ RED.settings.joinMaxKeptMsgsCount = 2;
+ setTimeout(function() {
+ var logEvents = helper.log().args.filter(function (evt) {
+ return evt[0].type == "join";
+ });
+ var evt = logEvents[0][0];
+ evt.should.have.property('id', "n1");
+ evt.should.have.property('type', "join");
+ evt.should.have.property('msg', "join.too-many");
+ done();
+ }, 150);
+ n1.receive({payload:3, parts:{index:2, count:4, id:222}});
+ n1.receive({payload:2, parts:{index:1, count:4, id:222}});
+ n1.receive({payload:4, parts:{index:3, count:4, id:222}});
+ n1.receive({payload:1, parts:{index:0, count:4, id:222}});
+ });
+ });
});
diff --git a/test/nodes/core/logic/18-sort_spec.js b/test/nodes/core/logic/18-sort_spec.js
index 1695a7439..98e299f6f 100644
--- a/test/nodes/core/logic/18-sort_spec.js
+++ b/test/nodes/core/logic/18-sort_spec.js
@@ -27,10 +27,11 @@ describe('SORT node', function() {
afterEach(function() {
helper.unload();
+ RED.settings.maxKeptMsgsCount = 0;
});
it('should be loaded', function(done) {
- var flow = [{id:"n1", type:"sort", order:"ascending", as_num:false, keyType:"payload", name: "SortNode", wires:[["n2"]]},
+ var flow = [{id:"n1", type:"sort", order:"ascending", as_num:false, name: "SortNode", wires:[["n2"]]},
{id:"n2", type:"helper"}];
helper.load(sortNode, flow, function() {
var n1 = helper.getNode("n1");
@@ -39,16 +40,58 @@ describe('SORT node', function() {
});
});
- function check_sort0(flow, data_in, data_out, done) {
+ function check_sort0(flow, target, key, key_type, data_in, data_out, done) {
+ var sort = flow[0];
+ sort.target = target;
+ sort.targetType = "msg";
+ sort.msgKey = key;
+ sort.msgKeyType = key_type;
+ helper.load(sortNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ n2.on("input", function(msg) {
+ msg.should.have.property(target);
+ var data = msg[target];
+ data.length.should.equal(data_out.length);
+ for(var i = 0; i < data_out.length; i++) {
+ data[i].should.equal(data_out[i]);
+ }
+ done();
+ });
+ var msg = {};
+ msg[target] = data_in;
+ n1.receive(msg);
+ });
+ }
+
+ function check_sort0A(flow, data_in, data_out, done) {
+ check_sort0(flow, "payload", "", "elem", data_in, data_out, done);
+ }
+
+ function check_sort0B(flow, data_in, data_out, done) {
+ check_sort0(flow, "data", "", "elem", data_in, data_out, done);
+ }
+
+ function check_sort0C(flow, exp, data_in, data_out, done) {
+ check_sort0(flow, "data", exp, "jsonata", data_in, data_out, done);
+ }
+
+ function check_sort1(flow, key, key_type, data_in, data_out, done) {
+ var sort = flow[0];
+ var prop = (key_type === "msg") ? key : "payload";
+ sort.targetType = "seq";
+ sort.seqKey = key;
+ sort.seqKeyType = key_type;
helper.load(sortNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var count = 0;
n2.on("input", function(msg) {
- msg.should.have.property("payload");
+ msg.should.have.property(prop);
msg.should.have.property("parts");
msg.parts.should.have.property("count", data_out.length);
- var index = data_out.indexOf(msg.payload);
+ var data = msg[prop];
+ var index = data_out.indexOf(data);
msg.parts.should.have.property("index", index);
count++;
if (count === data_out.length) {
@@ -58,111 +101,132 @@ describe('SORT node', function() {
var len = data_in.length;
for(var i = 0; i < len; i++) {
var parts = { id: "X", index: i, count: len };
- n1.receive({payload:data_in[i], parts: parts});
+ var msg = {parts: parts};
+ msg[prop] = data_in[i];
+ n1.receive(msg);
}
});
}
- function check_sort1(flow, data_in, data_out, done) {
- helper.load(sortNode, flow, function() {
- var n1 = helper.getNode("n1");
- var n2 = helper.getNode("n2");
- n2.on("input", function(msg) {
- msg.should.have.property("payload");
- msg.payload.length.should.equal(data_out.length);
- for(var i = 0; i < data_out.length; i++) {
- msg.payload[i].should.equal(data_out[i]);
- }
- done();
- });
- n1.receive({payload:data_in});
- });
+ function check_sort1A(flow, data_in, data_out, done) {
+ check_sort1(flow, "payload", "msg", data_in, data_out, done);
}
+ function check_sort1B(flow, data_in, data_out, done) {
+ check_sort1(flow, "data", "msg", data_in, data_out, done);
+ }
+
+ function check_sort1C(flow, exp, data_in, data_out, done) {
+ check_sort1(flow, exp, "jsonata", data_in, data_out, done);
+ }
+
+
(function() {
- var flow = [{id:"n1", type:"sort", order:"ascending", as_num:false, keyType:"payload", wires:[["n2"]]},
+ var flow = [{id:"n1", type:"sort", order:"ascending", as_num:false, wires:[["n2"]]},
{id:"n2", type:"helper"}];
var data_in = [ "200", "4", "30", "1000" ];
var data_out = [ "1000", "200", "30", "4" ];
- it('should sort message group (payload, not number, ascending)', function(done) {
- check_sort0(flow, data_in, data_out, done);
+ it('should sort payload (elem, not number, ascending)', function(done) {
+ check_sort0A(flow, data_in, data_out, done);
});
- it('should sort payload (payload, not number, ascending)', function(done) {
- check_sort1(flow, data_in, data_out, done);
+ it('should sort msg prop (elem, not number, ascending)', function(done) {
+ check_sort0B(flow, data_in, data_out, done);
});
- })();
-
- (function() {
- var flow = [{id:"n1", type:"sort", order:"descending", as_num:false, keyType:"payload", wires:[["n2"]]},
- {id:"n2", type:"helper"}];
- var data_in = [ "200", "4", "30", "1000" ];
- var data_out = [ "4", "30", "200", "1000" ];
- it('should sort message group (payload, not number, descending)', function(done) {
- check_sort0(flow, data_in, data_out, done);
+ it('should sort message group/payload (not number, ascending)', function(done) {
+ check_sort1A(flow, data_in, data_out, done);
});
- it('should sort payload (payload, not number, descending)', function(done) {
- check_sort1(flow, data_in, data_out, done);
- });
- })();
-
- (function() {
- var flow = [{id:"n1", type:"sort", order:"ascending", as_num:true, keyType:"payload", wires:[["n2"]]},
- {id:"n2", type:"helper"}];
- var data_in = [ "200", "4", "30", "1000" ];
- var data_out = [ "4", "30", "200", "1000" ];
- it('should sort message group (payload, number, ascending)', function(done) {
- check_sort0(flow, data_in, data_out, done);
- });
- it('should sort payload (payload, number, ascending)', function(done) {
- check_sort1(flow, data_in, data_out, done);
- });
- })();
-
- (function() {
- var flow = [{id:"n1", type:"sort", order:"descending", as_num:true, keyType:"payload", wires:[["n2"]]},
- {id:"n2", type:"helper"}];
- var data_in = [ "200", "4", "30", "1000" ];
- var data_out = [ "1000", "200", "30", "4" ];
- it('should sort message group (payload, number, descending)', function(done) {
- check_sort0(flow, data_in, data_out, done);
- });
- it('should sort payload (payload, number, descending)', function(done) {
- check_sort1(flow, data_in, data_out, done);
+ it('should sort message group/prop (not number, ascending)', function(done) {
+ check_sort1B(flow, data_in, data_out, done);
});
})();
(function() {
+ var flow = [{id:"n1", type:"sort", order:"descending", as_num:false, wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ var data_in = [ "200", "4", "30", "1000" ];
+ var data_out = [ "4", "30", "200", "1000" ];
+ it('should sort payload (elem, not number, descending)', function(done) {
+ check_sort0A(flow, data_in, data_out, done);
+ });
+ it('should sort msg prop (elem, not number, descending)', function(done) {
+ check_sort0B(flow, data_in, data_out, done);
+ });
+ it('should sort message group/payload (not number, descending)', function(done) {
+ check_sort1A(flow, data_in, data_out, done);
+ });
+ it('should sort message group/prop (not number, descending)', function(done) {
+ check_sort1B(flow, data_in, data_out, done);
+ });
+ })();
+
+ (function() {
+ var flow = [{id:"n1", type:"sort", order:"ascending", as_num:true, wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ var data_in = [ "200", "4", "30", "1000" ];
+ var data_out = [ "4", "30", "200", "1000" ];
+ it('should sort payload (elem, number, ascending)', function(done) {
+ check_sort0A(flow, data_in, data_out, done);
+ });
+ it('should sort msg prop (elem, number, ascending)', function(done) {
+ check_sort0B(flow, data_in, data_out, done);
+ });
+ it('should sort message group/payload (number, ascending)', function(done) {
+ check_sort1A(flow, data_in, data_out, done);
+ });
+ it('should sort message group/prop (number, ascending)', function(done) {
+ check_sort1B(flow, data_in, data_out, done);
+ });
+ })();
+
+ (function() {
+ var flow = [{id:"n1", type:"sort", order:"descending", as_num:true, wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ var data_in = [ "200", "4", "30", "1000" ];
+ var data_out = [ "1000", "200", "30", "4" ];
+ it('should sort payload (elem, number, descending)', function(done) {
+ check_sort0A(flow, data_in, data_out, done);
+ });
+ it('should sort msg prop (elem, number, descending)', function(done) {
+ check_sort0B(flow, data_in, data_out, done);
+ });
+ it('should sort message group/payload (number, descending)', function(done) {
+ check_sort1A(flow, data_in, data_out, done);
+ });
+ it('should sort message group/prop (number, descending)', function(done) {
+ check_sort1B(flow, data_in, data_out, done);
+ });
+ })();
+
+ (function() {
+ var flow = [{id:"n1", type:"sort", order:"ascending", as_num:false, wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
var data_in = [ "C200", "A4", "B30", "D1000" ];
var data_out = [ "D1000", "C200", "B30", "A4" ];
- it('should sort message group (exp, not number, ascending)', function(done) {
- var flow = [{id:"n1", type:"sort", order:"ascending", as_num:false, keyType:"exp", key:"$substring(payload, 1)", wires:[["n2"]]},
- {id:"n2", type:"helper"}];
- check_sort0(flow, data_in, data_out, done);
- });
it('should sort payload (exp, not number, ascending)', function(done) {
- var flow = [{id:"n1", type:"sort", order:"ascending", as_num:false, keyType:"exp", key:"$substring($, 1)", wires:[["n2"]]},
- {id:"n2", type:"helper"}];
- check_sort1(flow, data_in, data_out, done);
+ check_sort0C(flow, "$substring($,1)", data_in, data_out, done);
+ });
+ it('should sort message group (exp, not number, ascending)', function(done) {
+ check_sort1C(flow, "$substring(payload,1)", data_in, data_out, done);
});
})();
+ return;
+
(function() {
+ var flow = [{id:"n1", type:"sort", order:"descending", as_num:false, wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
var data_in = [ "C200", "A4", "B30", "D1000" ];
var data_out = [ "A4", "B30", "C200", "D1000" ];
it('should sort message group (exp, not number, descending)', function(done) {
- var flow = [{id:"n1", type:"sort", order:"descending", as_num:false, keyType:"exp", key:"$substring(payload, 1)", wires:[["n2"]]},
- {id:"n2", type:"helper"}];
- check_sort0(flow, data_in, data_out, done);
+ check_sort0C(flow, "$substring($,1)", data_in, data_out, done);
});
it('should sort payload (exp, not number, descending)', function(done) {
- var flow = [{id:"n1", type:"sort", order:"descending", as_num:false, keyType:"exp", key:"$substring($, 1)", wires:[["n2"]]},
- {id:"n2", type:"helper"}];
- check_sort1(flow, data_in, data_out, done);
+ check_sort1C(flow, "$substring(payload,1)", data_in, data_out, done);
});
})();
it('should handle JSONata script error', function(done) {
- var flow = [{id:"n1", type:"sort", order:"ascending", as_num:false, keyType:"exp", key:"$unknown()", wires:[["n2"]]},
+ var flow = [{id:"n1", type:"sort", order:"ascending", as_num:false, target:"payload", targetType:"seq", seqKey:"$unknown()", seqKeyType:"jsonata", wires:[["n2"]]},
{id:"n2", type:"helper"}];
helper.load(sortNode, flow, function() {
var n1 = helper.getNode("n1");
@@ -183,12 +247,14 @@ describe('SORT node', function() {
});
});
+ return;
+
it('should handle too many pending messages', function(done) {
- var flow = [{id:"n1", type:"sort", order:"ascending", as_num:false, keyType:"payload", wires:[["n2"]]},
+ var flow = [{id:"n1", type:"sort", order:"ascending", as_num:false, target:"payload", targetType:"seq", seqKey:"payload", seqKeyType:"msg", wires:[["n2"]]},
{id:"n2", type:"helper"}];
helper.load(sortNode, flow, function() {
var n1 = helper.getNode("n1");
- RED.settings.sortMaxKeptMsgsCount = 2;
+ RED.settings.maxKeptMsgsCount = 2;
setTimeout(function() {
var logEvents = helper.log().args.filter(function (evt) {
return evt[0].type == "sort";
@@ -208,7 +274,7 @@ describe('SORT node', function() {
});
it('should clear pending messages on close', function(done) {
- var flow = [{id:"n1", type:"sort", order:"ascending", as_num:false, keyType:"payload", wires:[["n2"]]},
+ var flow = [{id:"n1", type:"sort", order:"ascending", as_num:false, target:"payload", targetType:"seq", seqKey:"payload", seqKeyType:"msg", wires:[["n2"]]},
{id:"n2", type:"helper"}];
helper.load(sortNode, flow, function() {
var n1 = helper.getNode("n1");
diff --git a/test/nodes/core/logic/19-batch_spec.js b/test/nodes/core/logic/19-batch_spec.js
new file mode 100644
index 000000000..aa806f5bf
--- /dev/null
+++ b/test/nodes/core/logic/19-batch_spec.js
@@ -0,0 +1,338 @@
+/**
+ * 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.
+ **/
+
+var should = require("should");
+var batchNode = require("../../../../nodes/core/logic/19-batch.js");
+var helper = require("../../helper.js");
+var RED = require("../../../../red/red.js");
+
+describe('BATCH node', function() {
+ this.timeout(8000);
+
+ before(function(done) {
+ helper.startServer(done);
+ });
+
+ afterEach(function() {
+ helper.unload();
+ RED.settings.batchMaxKeptMsgsCount = 0;
+ });
+
+ it('should be loaded with defaults', function(done) {
+ var flow = [{id:"n1", type:"batch", name: "BatchNode", wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ helper.load(batchNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ n1.should.have.property('name', 'BatchNode');
+ done();
+ });
+ });
+
+ function check_parts(msg, id, idx, count) {
+ msg.should.have.property("parts");
+ var parts = msg.parts;
+ parts.should.have.property("id", id);
+ parts.should.have.property("index", idx);
+ parts.should.have.property("count", count);
+ }
+
+ function check_data(n1, n2, results, done) {
+ var id = undefined;
+ var ix0 = 0; // seq no
+ var ix1 = 0; // loc. in seq
+ var seq = undefined;
+ n2.on("input", function(msg) {
+ try {
+ if (seq === undefined) {
+ seq = results[ix0];
+ }
+ var val = seq[ix1];
+ msg.should.have.property("payload", val);
+ if (id === undefined) {
+ id = msg.parts.id;
+ }
+ check_parts(msg, id, ix1, seq.length);
+ ix1++;
+ if (ix1 === seq.length) {
+ ix0++;
+ ix1 = 0;
+ seq = undefined;
+ id = undefined;
+ if (ix0 === results.length) {
+ done();
+ }
+ }
+ }
+ catch (e) {
+ done(e);
+ }
+ });
+ }
+
+ function check_count(flow, results, done) {
+ try {
+ helper.load(batchNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ check_data(n1, n2, results, done);
+ for(var i = 0; i < 6; i++) {
+ n1.receive({payload: i});
+ }
+ });
+ }
+ catch (e) {
+ done(e);
+ }
+ }
+
+ function delayed_send(receiver, index, count, delay) {
+ if (index < count) {
+ setTimeout(function() {
+ receiver.receive({payload: index});
+ delayed_send(receiver, index+1, count, delay);
+ }, delay);
+ }
+ }
+
+ function check_interval(flow, results, delay, done) {
+ helper.load(batchNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ check_data(n1, n2, results, done);
+ delayed_send(n1, 0, 4, delay);
+ });
+ }
+
+ function check_concat(flow, results, inputs, done) {
+ try {
+ helper.load(batchNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ check_data(n1, n2, results, done);
+ for(var data of inputs) {
+ var msg = {
+ topic: data[0],
+ payload: data[1],
+ parts: {
+ id: data[0],
+ index: data[2],
+ count: data[3]
+ }
+ };
+ n1.receive(msg);
+ }
+ });
+ }
+ catch (e) {
+ done(e);
+ }
+ }
+
+ describe('mode: count', function() {
+
+ it('should create seq. with count', function(done) {
+ var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 2, overwrap: 0, interval: 10, allowEmptySequence: false, topics: [], wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ var results = [
+ [0, 1],
+ [2, 3],
+ [4, 5]
+ ];
+ check_count(flow, results, done);
+ });
+
+ it('should create seq. with count and overwrap', function(done) {
+ var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 3, overwrap: 2, interval: 10, allowEmptySequence: false, topics: [], wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ var results = [
+ [0, 1, 2],
+ [1, 2, 3],
+ [2, 3, 4],
+ [3, 4, 5]
+ ];
+ check_count(flow, results, done);
+ });
+
+ it('should handle too many pending messages', function(done) {
+ var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "count", count: 5, overwrap: 0, interval: 10, allowEmptySequence: false, topics: [], wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ helper.load(batchNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ RED.settings.batchMaxKeptMsgsCount = 2;
+ setTimeout(function() {
+ var logEvents = helper.log().args.filter(function (evt) {
+ return evt[0].type == "batch";
+ });
+ var evt = logEvents[0][0];
+ evt.should.have.property('id', "n1");
+ evt.should.have.property('type', "batch");
+ evt.should.have.property('msg', "batch.too-many");
+ done();
+ }, 150);
+ for(var i = 0; i < 3; i++) {
+ n1.receive({payload: i});
+ }
+ });
+ });
+
+ });
+
+ describe('mode: interval', function() {
+
+ it('should create seq. with interval', function(done) {
+ var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overwrap: 0, interval: 1, allowEmptySequence: false, topics: [], wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ var results = [
+ [0, 1],
+ [2, 3]
+ ];
+ check_interval(flow, results, 450, done);
+ });
+
+ it('should create seq. with interval (in float)', function(done) {
+ var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overwrap: 0, interval: 0.5, allowEmptySequence: false, topics: [], wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ var results = [
+ [0, 1],
+ [2, 3]
+ ];
+ check_interval(flow, results, 225, done);
+ });
+
+ it('should create seq. with interval & not send empty seq', function(done) {
+ var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overwrap: 0, interval: 1, allowEmptySequence: false, topics: [], wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ var results = [
+ // 1300, 2600, 3900, 5200,
+ [0], [1], [2], [3]
+ ];
+ check_interval(flow, results, 1300, done);
+ });
+
+ it('should create seq. with interval & send empty seq', function(done) {
+ var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overwrap: 0, interval: 1, allowEmptySequence: true, topics: [], wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ var results = [
+ // 1300, 2600, 3900, 5200,
+ [null], [0], [1], [2], [null], [3]
+ ];
+ check_interval(flow, results, 1300, done);
+ });
+
+ it('should handle too many pending messages', function(done) {
+ var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "interval", count: 0, overwrap: 0, interval: 1, allowEmptySequence: false, topics: [], wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ helper.load(batchNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ RED.settings.batchMaxKeptMsgsCount = 2;
+ setTimeout(function() {
+ var logEvents = helper.log().args.filter(function (evt) {
+ return evt[0].type == "batch";
+ });
+ var evt = logEvents[0][0];
+ evt.should.have.property('id', "n1");
+ evt.should.have.property('type', "batch");
+ evt.should.have.property('msg', "batch.too-many");
+ done();
+ }, 150);
+ for(var i = 0; i < 3; i++) {
+ n1.receive({payload: i});
+ }
+ });
+ });
+
+ });
+
+ describe('mode: concat', function() {
+
+ it('should concat two seq. (series)', function(done) {
+ var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overwrap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}], wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ var results = [
+ [2, 3, 0, 1]
+ ];
+ var inputs = [
+ ["TB", 0, 0, 2],
+ ["TB", 1, 1, 2],
+ ["TA", 2, 0, 2],
+ ["TA", 3, 1, 2]
+ ];
+ check_concat(flow, results, inputs, done);
+ });
+
+ it('should concat two seq. (mixed)', function(done) {
+ var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overwrap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}], wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ var results = [
+ [2, 3, 0, 1]
+ ];
+ var inputs = [
+ ["TA", 2, 0, 2],
+ ["TB", 0, 0, 2],
+ ["TA", 3, 1, 2],
+ ["TB", 1, 1, 2]
+ ];
+ check_concat(flow, results, inputs, done);
+ });
+
+ it('should concat three seq.', function(done) {
+ var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overwrap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}, {topic: "TC"}], wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ var results = [
+ [2, 3, 0, 1, 4]
+ ];
+ var inputs = [
+ ["TC", 4, 0, 1],
+ ["TB", 0, 0, 2],
+ ["TB", 1, 1, 2],
+ ["TA", 2, 0, 2],
+ ["TA", 3, 1, 2]
+ ];
+ check_concat(flow, results, inputs, done);
+ });
+
+ it('should handle too many pending messages', function(done) {
+ var flow = [{id:"n1", type:"batch", name: "BatchNode", mode: "concat", count: 0, overwrap: 0, interval: 1, allowEmptySequence: false, topics: [{topic: "TA"}, {topic: "TB"}], wires:[["n2"]]},
+ {id:"n2", type:"helper"}];
+ helper.load(batchNode, flow, function() {
+ var n1 = helper.getNode("n1");
+ var n2 = helper.getNode("n2");
+ RED.settings.batchMaxKeptMsgsCount = 2;
+ setTimeout(function() {
+ var logEvents = helper.log().args.filter(function (evt) {
+ return evt[0].type == "batch";
+ });
+ var evt = logEvents[0][0];
+ evt.should.have.property('id', "n1");
+ evt.should.have.property('type', "batch");
+ evt.should.have.property('msg', "batch.too-many");
+ done();
+ }, 150);
+ var C = 3;
+ for(var i = 0; i < C; i++) {
+ var parts_a = {index:i, count:C, id:"A"};
+ var parts_b = {index:i, count:C, id:"B"};
+ n1.receive({payload: i, topic: "TA", parts:parts_a});
+ n1.receive({payload: i, topic: "TB", parts:parts_b});
+ }
+ });
+ });
+
+ });
+
+});