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}); + } + }); + }); + + }); + +});