From 1c5fdb6ab6d625af398e365cf8e036cf6bf2cbb1 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 23 Jun 2023 02:11:57 +0100 Subject: [PATCH] Evaluate all env vars as part of async flow start --- package.json | 2 +- .../@node-red/runtime/lib/flows/Flow.js | 214 +--- .../@node-red/runtime/lib/flows/Group.js | 55 + .../@node-red/runtime/lib/flows/Subflow.js | 71 +- .../@node-red/runtime/lib/flows/index.js | 2 +- .../@node-red/runtime/lib/flows/util.js | 285 +++-- .../node_modules/@node-red/util/lib/util.js | 6 +- test/node_modules/nr-test-utils/index.js | 6 +- test/nodes/core/common/20-inject_spec.js | 67 +- test/nodes/core/function/15-change_spec.js | 16 +- test/nodes/subflow/subflow_spec.js | 53 +- .../@node-red/runtime/lib/flows/Flow_spec.js | 1109 ++++++++--------- .../@node-red/runtime/lib/flows/Group_spec.js | 48 + .../runtime/lib/flows/Subflow_spec.js | 461 +++---- .../@node-red/runtime/lib/flows/index_spec.js | 22 +- .../@node-red/runtime/lib/flows/util_spec.js | 14 +- 16 files changed, 1141 insertions(+), 1290 deletions(-) create mode 100644 packages/node_modules/@node-red/runtime/lib/flows/Group.js create mode 100644 test/unit/@node-red/runtime/lib/flows/Group_spec.js diff --git a/package.json b/package.json index 986c88375..8f367b842 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "mermaid": "^9.4.3", "minami": "1.2.3", "mocha": "9.2.2", - "node-red-node-test-helper": "^0.3.1", + "node-red-node-test-helper": "^0.3.2", "nodemon": "2.0.20", "proxy": "^1.0.2", "sass": "1.62.1", diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 489ec42cb..2e03f5165 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -14,19 +14,20 @@ * limitations under the License. **/ -var clone = require("clone"); -var redUtil = require("@node-red/util").util; +const clone = require("clone"); +const redUtil = require("@node-red/util").util; const events = require("@node-red/util").events; -var flowUtil = require("./util"); +const flowUtil = require("./util"); const context = require('../nodes/context'); const hooks = require("@node-red/util").hooks; const credentials = require("../nodes/credentials"); -var Subflow; -var Log; +let Subflow; +let Log; +let Group; -var nodeCloseTimeout = 15000; -var asyncMessageDelivery = true; +let nodeCloseTimeout = 15000; +let asyncMessageDelivery = true; /** * This class represents a flow within the runtime. It is responsible for @@ -52,6 +53,27 @@ class Flow { this.isGlobalFlow = false; } this.id = this.flow.id || "global"; + + // Initialise the group objects. These must be done in the right order + // starting from outer-most to inner-most so that the parent hierarchy + // is maintained. + this.groups = {} + this.groupOrder = [] + const groupIds = Object.keys(this.flow.groups || {}) + while (groupIds.length > 0) { + const id = groupIds.shift() + const groupDef = this.flow.groups[id] + if (!groupDef.g || this.groups[groupDef.g]) { + // The parent of this group is available - either another group + // or the top-level flow (this) + const parent = this.groups[groupDef.g] || this + this.groups[groupDef.id] = new Group(parent, groupDef) + this.groupOrder.push(groupDef.id) + } else { + // Try again once we've processed the other groups + groupIds.push(id) + } + } this.activeNodes = {}; this.subflowInstanceNodes = {}; this.catchNodes = []; @@ -59,6 +81,11 @@ class Flow { this.path = this.id; // Ensure a context exists for this flow this.context = context.getFlowContext(this.id,this.parent.id); + + // env is an array of env definitions + // _env is an object for direct lookup of env name -> value + this.env = this.flow.env + this._env = {} } /** @@ -136,7 +163,7 @@ class Flow { * @param {[type]} msg [description] * @return {[type]} [description] */ - start(diff) { + async start(diff) { this.trace("start "+this.TYPE+" ["+this.path+"]"); var node; var newNode; @@ -145,6 +172,18 @@ class Flow { this.statusNodes = []; this.completeNodeMap = {}; + if (this.env) { + this._env = await flowUtil.evaluateEnvProperties(this, this.env, credentials.get(this.id)) + // console.log('env', this.env) + // console.log('_env', this._env) + } + for (let i = 0; i < this.groupOrder.length; i++) { + // Start the groups in the right order so they + // can setup their env vars knowning their parent + // will have been started + await this.groups[this.groupOrder[i]].start() + } + var configNodes = Object.keys(this.flow.configs); var configNodeAttempts = {}; while (configNodes.length > 0) { @@ -177,7 +216,7 @@ class Flow { } } if (readyToCreate) { - newNode = flowUtil.createNode(this,node); + newNode = await flowUtil.createNode(this,node); if (newNode) { this.activeNodes[id] = newNode; } @@ -203,7 +242,7 @@ class Flow { if (node.d !== true) { if (!node.subflow) { if (!this.activeNodes[id]) { - newNode = flowUtil.createNode(this,node); + newNode = await flowUtil.createNode(this,node); if (newNode) { this.activeNodes[id] = newNode; } @@ -221,7 +260,7 @@ class Flow { node ); this.subflowInstanceNodes[id] = subflow; - subflow.start(); + await subflow.start(); this.activeNodes[id] = subflow.node; // this.subflowInstanceNodes[id] = nodes.map(function(n) { return n.id}); @@ -404,8 +443,7 @@ class Flow { * @return {Node} group node */ getGroupNode(id) { - const groups = this.global.groups; - return groups[id]; + return this.groups[id]; } /** @@ -416,95 +454,8 @@ class Flow { return this.activeNodes; } - /*! - * Get value of environment variable defined in group node. - * @param {String} group - group node - * @param {String} name - name of variable - * @return {Object} object containing the value in val property or null if not defined - */ - getGroupEnvSetting(node, group, name) { - if (group) { - if (name === "NR_GROUP_NAME") { - return [{ - val: group.name - }, null]; - } - if (name === "NR_GROUP_ID") { - return [{ - val: group.id - }, null]; - } - - if (group.credentials === undefined) { - group.credentials = credentials.get(group.id) || {}; - } - if (!name.startsWith("$parent.")) { - if (group.env) { - if (!group._env) { - const envs = group.env; - const entries = envs.map((env) => { - if (env.type === "cred") { - const cred = group.credentials; - if (cred.hasOwnProperty(env.name)) { - env.value = cred[env.name]; - } - } - return [env.name, env]; - }); - group._env = Object.fromEntries(entries); - } - const env = group._env[name]; - if (env) { - let value = env.value; - const type = env.type; - if ((type !== "env") || - (value !== name)) { - if (type === "env") { - value = value.replace(new RegExp("\\${"+name+"}","g"),"${$parent."+name+"}"); - } - if (type === "bool") { - const val - = ((value === "true") || - (value === true)); - return [{ - val: val - }, null]; - } - if (type === "cred") { - return [{ - val: value - }, null]; - } - try { - var val = redUtil.evaluateNodeProperty(value, type, node, null, null); - return [{ - val: val - }, null]; - } - catch (e) { - this.error(e); - return [null, null]; - } - } - } - } - } - else { - name = name.substring(8); - } - if (group.g) { - const parent = this.getGroupNode(group.g); - return this.getGroupEnvSetting(node, parent, name); - } - } - return [null, name]; - } - - /** - * Get a flow setting value. This currently automatically defers to the parent - * flow which, as defined in ./index.js returns `process.env[key]`. - * This lays the groundwork for Subflow to have instance-specific settings + * Get a flow setting value. * @param {[type]} key [description] * @return {[type]} [description] */ @@ -516,54 +467,14 @@ class Flow { if (key === "NR_FLOW_ID") { return flow.id; } - if (flow.credentials === undefined) { - flow.credentials = credentials.get(flow.id) || {}; - } - if (flow.env) { - if (!key.startsWith("$parent.")) { - if (!flow._env) { - const envs = flow.env; - const entries = envs.map((env) => { - if (env.type === "cred") { - const cred = flow.credentials; - if (cred.hasOwnProperty(env.name)) { - env.value = cred[env.name]; - } - } - return [env.name, env] - }); - flow._env = Object.fromEntries(entries); - } - const env = flow._env[key]; - if (env) { - let value = env.value; - const type = env.type; - if ((type !== "env") || (value !== key)) { - if (type === "env") { - value = value.replace(new RegExp("\\${"+key+"}","g"),"${$parent."+key+"}"); - } - try { - if (type === "bool") { - const val = ((value === "true") || - (value === true)); - return val; - } - if (type === "cred") { - return value; - } - var val = redUtil.evaluateNodeProperty(value, type, null, null, null); - return val; - } - catch (e) { - this.error(e); - } - } - } + if (!key.startsWith("$parent.")) { + if (this._env.hasOwnProperty(key)) { + return this._env[key] } - else { + } else { key = key.substring(8); - } } + // Delegate to the parent flow. return this.parent.getSetting(key); } @@ -618,10 +529,10 @@ class Flow { let distance = 0 if (reportingNode.g) { // Reporting node inside a group. Calculate the distance between it and the status node - let containingGroup = this.global.groups[reportingNode.g] + let containingGroup = this.groups[reportingNode.g] while (containingGroup && containingGroup.id !== targetStatusNode.g) { distance++ - containingGroup = this.global.groups[containingGroup.g] + containingGroup = this.groups[containingGroup.g] } if (!containingGroup && targetStatusNode.g && targetStatusNode.scope === 'group') { // This status node is in a group, but not in the same hierachy @@ -706,10 +617,10 @@ class Flow { let distance = 0 if (reportingNode.g) { // Reporting node inside a group. Calculate the distance between it and the catch node - let containingGroup = this.global.groups[reportingNode.g] + let containingGroup = this.groups[reportingNode.g] while (containingGroup && containingGroup.id !== targetCatchNode.g) { distance++ - containingGroup = this.global.groups[containingGroup.g] + containingGroup = this.groups[containingGroup.g] } if (!containingGroup && targetCatchNode.g && targetCatchNode.scope === 'group') { // This catch node is in a group, but not in the same hierachy @@ -909,9 +820,10 @@ module.exports = { asyncMessageDelivery = !runtime.settings.runtimeSyncDelivery Log = runtime.log; Subflow = require("./Subflow"); + Group = require("./Group").Group }, create: function(parent,global,conf) { - return new Flow(parent,global,conf); + return new Flow(parent,global,conf) }, Flow: Flow } diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Group.js b/packages/node_modules/@node-red/runtime/lib/flows/Group.js new file mode 100644 index 000000000..dc05211a1 --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/flows/Group.js @@ -0,0 +1,55 @@ +const flowUtil = require("./util"); +const credentials = require("../nodes/credentials"); + +/** + * This class represents a group within the runtime. + */ +class Group { + + /** + * Create a Group object. + * @param {[type]} parent The parent flow/group + * @param {[type]} groupDef This group's definition + */ + constructor(parent, groupDef) { + this.TYPE = 'group' + this.name = groupDef.name + this.parent = parent + this.group = groupDef + this.id = this.group.id + this.g = this.group.g + this.env = this.group.env + this._env = {} + } + + async start() { + if (this.env) { + this._env = await flowUtil.evaluateEnvProperties(this, this.env, credentials.get(this.id)) + } + } + /** + * Get a group setting value. + * @param {[type]} key [description] + * @return {[type]} [description] + */ + getSetting(key) { + if (key === "NR_GROUP_NAME") { + return this.name; + } + if (key === "NR_GROUP_ID") { + return this.id; + } + if (!key.startsWith("$parent.")) { + if (this._env.hasOwnProperty(key)) { + return this._env[key] + } + } else { + key = key.substring(8); + } + return this.parent.getSetting(key); + } +} + +module.exports = { + Group +} diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js b/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js index 78f0c7550..4588e3d9b 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Subflow.js @@ -119,7 +119,7 @@ class Subflow extends Flow { this.templateCredentials = credentials.get(subflowDef.id) || {}; this.instanceCredentials = credentials.get(id) || {}; - var env = []; + var env = {}; if (this.subflowDef.env) { this.subflowDef.env.forEach(e => { env[e.name] = e; @@ -145,7 +145,7 @@ class Subflow extends Flow { } }); } - this.env = env; + this.env = Object.values(env); } /** @@ -156,7 +156,7 @@ class Subflow extends Flow { * @param {[type]} diff [description] * @return {[type]} [description] */ - start(diff) { + async start(diff) { var self = this; // Create a subflow node to accept inbound messages and route appropriately var Node = require("../nodes/Node"); @@ -310,7 +310,7 @@ class Subflow extends Flow { } } } - super.start(diff); + return super.start(diff); } /** @@ -335,68 +335,35 @@ class Subflow extends Flow { } /** * Get environment variable of subflow - * @param {String} name name of env var + * @param {String} key name of env var * @return {Object} val value of env var */ - getSetting(name) { - if (!/^\$parent\./.test(name)) { - var env = this.env; - if (env && env.hasOwnProperty(name)) { - var val = env[name]; - // If this is an env type property we need to be careful not - // to get into lookup loops. - // 1. if the value to lookup is the same as this one, go straight to parent - // 2. otherwise, check if it is a compound env var ("foo $(bar)") - // and if so, substitute any instances of `name` with $parent.name - // See https://github.com/node-red/node-red/issues/2099 - if (val.type !== 'env' || val.value !== name) { - let value = val.value; - var type = val.type; - if (type === 'env') { - value = value.replace(new RegExp("\\${"+name+"}","g"),"${$parent."+name+"}"); - } - try { - return evaluateInputValue(value, type, this.node); - } - catch (e) { - this.error(e); - return undefined; - } - } else { - // This _is_ an env property pointing at itself - go to parent - } - } - } else { - // name starts $parent. ... so delegate to parent automatically - name = name.substring(8); - } + getSetting(key) { const node = this.subflowInstance; if (node) { - if (name === "NR_NODE_NAME") { + if (key === "NR_NODE_NAME") { return node.name; } - if (name === "NR_NODE_ID") { + if (key === "NR_NODE_ID") { return node.id; } - if (name === "NR_NODE_PATH") { + if (key === "NR_NODE_PATH") { return node._path; } } - if (node.g) { - const group = this.getGroupNode(node.g); - const [result, newName] = this.getGroupEnvSetting(node, group, name); - if (result) { - return result.val; + if (!key.startsWith("$parent.")) { + if (this._env.hasOwnProperty(key)) { + return this._env[key] } - name = newName; + } else { + key = key.substring(8); } - - var parent = this.parent; - if (parent) { - var val = parent.getSetting(name); - return val; + // Push the request up to the parent. + // Unlike a Flow, the parent of a Subflow could be a Group + if (node.g) { + return this.parent.getGroupNode(node.g).getSetting(key) } - return undefined; + return this.parent.getSetting(key) } /** diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js index e18861f17..c121e469c 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js @@ -391,7 +391,7 @@ async function start(type,diff,muteLog,isDeploy) { for (id in activeFlows) { if (activeFlows.hasOwnProperty(id)) { try { - activeFlows[id].start(diff); + await activeFlows[id].start(diff); // Create a map of node id to flow id and also a subflowInstance lookup map var activeNodes = activeFlows[id].getActiveNodes(); Object.keys(activeNodes).forEach(function(nid) { diff --git a/packages/node_modules/@node-red/runtime/lib/flows/util.js b/packages/node_modules/@node-red/runtime/lib/flows/util.js index 868657c7b..85c1381b3 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/util.js @@ -70,8 +70,58 @@ function mapEnvVarProperties(obj,prop,flow,config) { } } +async function evaluateEnvProperties(flow, env, credentials) { + const pendingEvaluations = [] + const evaluatedEnv = {} + const envTypes = [] + for (let i = 0; i < env.length; i++) { + let { name, value, type } = env[i] + if (type === "env") { + // Do env types last as they may include references to other env vars + // at this level which need to be resolved before they can be looked-up + envTypes.push(env[i]) + } else if (type === "bool") { + value = (value === "true") || (value === true); + } else if (type === "cred") { + if (credentials.hasOwnProperty(name)) { + value = credentials[name]; + } + } else if (type ==='jsonata') { + pendingEvaluations.push(new Promise((resolve, _) => { + redUtil.evaluateNodeProperty(value, 'jsonata', {_flow: flow}, null, (err, result) => { + if (!err) { + evaluatedEnv[name] = result + } + resolve() + }); + })) + } else { + value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null); + } + evaluatedEnv[name] = value + } + if (pendingEvaluations.length > 0) { + await Promise.all(pendingEvaluations) + } + for (let i = 0; i < envTypes.length; i++) { + let { name, value, type } = envTypes[i] + // If an env-var wants to lookup itself, delegate straight to the parent + // https://github.com/node-red/node-red/issues/2099 + if (value === name) { + value = `$parent.${name}` + } + if (evaluatedEnv.hasOwnProperty(value)) { + value = evaluatedEnv[value] + } else { + value = redUtil.evaluateNodeProperty(value, type, {_flow: flow}, null, null); + } + evaluatedEnv[name] = value + } -function createNode(flow,config) { + return evaluatedEnv +} + +async function createNode(flow,config) { var newNode = null; var type = config.type; try { @@ -140,7 +190,7 @@ function createNode(flow,config) { // This allows nodes inside the subflow to get ahold of each other // such as a node accessing its config node flow.subflowInstanceNodes[config.id] = subflow - subflow.start(); + await subflow.start(); return subflow.node; } } catch(err) { @@ -150,123 +200,122 @@ function createNode(flow,config) { } function parseConfig(config) { - var flow = {}; - flow.allNodes = {}; - flow.subflows = {}; - flow.configs = {}; - flow.flows = {}; - flow.groups = {}; - flow.missingTypes = []; + var flow = {}; + flow.allNodes = {}; + flow.subflows = {}; + flow.configs = {}; + flow.flows = {}; + flow.missingTypes = []; - config.forEach(function(n) { - flow.allNodes[n.id] = clone(n); - if (n.type === 'tab') { - flow.flows[n.id] = n; - flow.flows[n.id].subflows = {}; - flow.flows[n.id].configs = {}; - flow.flows[n.id].nodes = {}; - } - if (n.type === 'group') { - flow.groups[n.id] = n; - } - }); + config.forEach(function (n) { + flow.allNodes[n.id] = clone(n); + if (n.type === 'tab') { + flow.flows[n.id] = n; + flow.flows[n.id].subflows = {}; + flow.flows[n.id].configs = {}; + flow.flows[n.id].nodes = {}; + flow.flows[n.id].groups = {}; + } else if (n.type === 'subflow') { + flow.subflows[n.id] = n; + flow.subflows[n.id].configs = {}; + flow.subflows[n.id].nodes = {}; + flow.subflows[n.id].groups = {}; + flow.subflows[n.id].instances = []; + } + }); - // TODO: why a separate forEach? this can be merged with above - config.forEach(function(n) { - if (n.type === 'subflow') { - flow.subflows[n.id] = n; - flow.subflows[n.id].configs = {}; - flow.subflows[n.id].nodes = {}; - flow.subflows[n.id].instances = []; - } - }); - var linkWires = {}; - var linkOutNodes = []; - config.forEach(function(n) { - if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') { - var subflowDetails = subflowInstanceRE.exec(n.type); + var linkWires = {}; + var linkOutNodes = []; + config.forEach(function (n) { + if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') { + var subflowDetails = subflowInstanceRE.exec(n.type); - if ( (subflowDetails && !flow.subflows[subflowDetails[1]]) || (!subflowDetails && !typeRegistry.get(n.type)) ) { - if (flow.missingTypes.indexOf(n.type) === -1) { - flow.missingTypes.push(n.type); - } - } - var container = null; - if (flow.flows[n.z]) { - container = flow.flows[n.z]; - } else if (flow.subflows[n.z]) { - container = flow.subflows[n.z]; - } - if (n.hasOwnProperty('x') && n.hasOwnProperty('y')) { - if (subflowDetails) { - var subflowType = subflowDetails[1] - n.subflow = subflowType; - if (flow.subflows[subflowType]) { - flow.subflows[subflowType].instances.push(n) - } - } - if (container) { - container.nodes[n.id] = n; - } - } else { - if (container) { - container.configs[n.id] = n; - } else { - flow.configs[n.id] = n; - flow.configs[n.id]._users = []; - } - } - if (n.type === 'link in' && n.links) { - // Ensure wires are present in corresponding link out nodes - n.links.forEach(function(id) { - linkWires[id] = linkWires[id]||{}; - linkWires[id][n.id] = true; - }) - } else if (n.type === 'link out' && n.links) { - linkWires[n.id] = linkWires[n.id]||{}; - n.links.forEach(function(id) { - linkWires[n.id][id] = true; - }) - linkOutNodes.push(n); - } - } - }); - linkOutNodes.forEach(function(n) { - var links = linkWires[n.id]; - var targets = Object.keys(links); - n.wires = [targets]; - }); + if ((subflowDetails && !flow.subflows[subflowDetails[1]]) || (!subflowDetails && !typeRegistry.get(n.type))) { + if (flow.missingTypes.indexOf(n.type) === -1) { + flow.missingTypes.push(n.type); + } + } + var container = null; + if (flow.flows[n.z]) { + container = flow.flows[n.z]; + } else if (flow.subflows[n.z]) { + container = flow.subflows[n.z]; + } + if (n.hasOwnProperty('x') && n.hasOwnProperty('y')) { + if (subflowDetails) { + var subflowType = subflowDetails[1] + n.subflow = subflowType; + if (flow.subflows[subflowType]) { + flow.subflows[subflowType].instances.push(n) + } + } + if (container) { + container.nodes[n.id] = n; + } + } else { + if (container) { + container.configs[n.id] = n; + } else { + flow.configs[n.id] = n; + flow.configs[n.id]._users = []; + } + } + if (n.type === 'link in' && n.links) { + // Ensure wires are present in corresponding link out nodes + n.links.forEach(function (id) { + linkWires[id] = linkWires[id] || {}; + linkWires[id][n.id] = true; + }) + } else if (n.type === 'link out' && n.links) { + linkWires[n.id] = linkWires[n.id] || {}; + n.links.forEach(function (id) { + linkWires[n.id][id] = true; + }) + linkOutNodes.push(n); + } + } else if (n.type === 'group') { + const parentContainer = flow.flows[n.z] || flow.subflows[n.z] + if (parentContainer) { + parentContainer.groups[n.id] = n + } + } + }); + linkOutNodes.forEach(function (n) { + var links = linkWires[n.id]; + var targets = Object.keys(links); + n.wires = [targets]; + }); - var addedTabs = {}; - config.forEach(function(n) { - if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') { - for (var prop in n) { - if (n.hasOwnProperty(prop) && prop !== 'id' && prop !== 'wires' && prop !== 'type' && prop !== '_users' && flow.configs.hasOwnProperty(n[prop])) { - // This property references a global config node - flow.configs[n[prop]]._users.push(n.id) - } - } - if (n.z && !flow.subflows[n.z]) { + var addedTabs = {}; + config.forEach(function (n) { + if (n.type !== 'subflow' && n.type !== 'tab' && n.type !== 'group') { + for (var prop in n) { + if (n.hasOwnProperty(prop) && prop !== 'id' && prop !== 'wires' && prop !== 'type' && prop !== '_users' && flow.configs.hasOwnProperty(n[prop])) { + // This property references a global config node + flow.configs[n[prop]]._users.push(n.id) + } + } + if (n.z && !flow.subflows[n.z]) { - if (!flow.flows[n.z]) { - flow.flows[n.z] = {type:'tab',id:n.z}; - flow.flows[n.z].subflows = {}; - flow.flows[n.z].configs = {}; - flow.flows[n.z].nodes = {}; - addedTabs[n.z] = flow.flows[n.z]; - } - if (addedTabs[n.z]) { - if (n.hasOwnProperty('x') && n.hasOwnProperty('y')) { - addedTabs[n.z].nodes[n.id] = n; - } else { - addedTabs[n.z].configs[n.id] = n; - } - } - } - } - }); - return flow; + if (!flow.flows[n.z]) { + flow.flows[n.z] = { type: 'tab', id: n.z }; + flow.flows[n.z].subflows = {}; + flow.flows[n.z].configs = {}; + flow.flows[n.z].nodes = {}; + addedTabs[n.z] = flow.flows[n.z]; + } + if (addedTabs[n.z]) { + if (n.hasOwnProperty('x') && n.hasOwnProperty('y')) { + addedTabs[n.z].nodes[n.id] = n; + } else { + addedTabs[n.z].configs[n.id] = n; + } + } + } + } + }); + return flow; } function getGlobalEnv(name) { @@ -319,10 +368,11 @@ module.exports = { } return undefined; }, - diffNodes: diffNodes, - mapEnvVarProperties: mapEnvVarProperties, - parseConfig: parseConfig, + diffNodes, + mapEnvVarProperties, + evaluateEnvProperties, + parseConfig, diffConfigs: function(oldConfig, newConfig) { var id; @@ -615,5 +665,6 @@ module.exports = { * @param {object} config The node configuration object * @return {Node} The instance of the node */ - createNode: createNode + createNode: createNode, + evaluateEnvProperties } diff --git a/packages/node_modules/@node-red/util/lib/util.js b/packages/node_modules/@node-red/util/lib/util.js index ad05d45f1..4ae0032b0 100644 --- a/packages/node_modules/@node-red/util/lib/util.js +++ b/packages/node_modules/@node-red/util/lib/util.js @@ -548,11 +548,9 @@ function getSetting(node, name, flow_) { if (flow) { if (node && node.g) { const group = flow.getGroupNode(node.g); - const [result, newName] = flow.getGroupEnvSetting(node, group, name); - if (result) { - return result.val; + if (group) { + return group.getSetting(name) } - name = newName; } return flow.getSetting(name); } diff --git a/test/node_modules/nr-test-utils/index.js b/test/node_modules/nr-test-utils/index.js index 56bc369e6..08a0b3d89 100644 --- a/test/node_modules/nr-test-utils/index.js +++ b/test/node_modules/nr-test-utils/index.js @@ -16,7 +16,6 @@ const path = require("path"); -const fs = require("fs"); const PACKAGE_ROOT = "../../../packages/node_modules"; @@ -27,5 +26,10 @@ module.exports = { }, resolve: function(file) { return path.resolve(path.join(__dirname,PACKAGE_ROOT,file)); + }, + sleep: async (time) => { + return new Promise(resolve => { + setTimeout(resolve, time) + }) } } diff --git a/test/nodes/core/common/20-inject_spec.js b/test/nodes/core/common/20-inject_spec.js index 3e9a35391..056ca9693 100644 --- a/test/nodes/core/common/20-inject_spec.js +++ b/test/nodes/core/common/20-inject_spec.js @@ -22,7 +22,9 @@ var helper = require("node-red-node-test-helper"); describe('inject node', function() { beforeEach(function(done) { - helper.startServer(done); + helper.startServer(() => { + done() + }); }); function initContext(done) { @@ -41,7 +43,7 @@ describe('inject node', function() { }); } - afterEach(function(done) { + afterEach(async function() { helper.unload().then(function () { return Context.clean({allNodes: {}}); }).then(function () { @@ -53,8 +55,11 @@ describe('inject node', function() { function basicTest(type, val, rval) { it('inject value ('+type+')', function (done) { - var flow = [{id: "n1", type: "inject", topic: "t1", payload: val, payloadType: type, wires: [["n2"]], z: "flow"}, - {id: "n2", type: "helper"}]; + var flow = [ + {id:'flow', type:'tab'}, + {id: "n1", type: "inject", topic: "t1", payload: val, payloadType: type, wires: [["n2"]], z: "flow"}, + {id: "n2", type: "helper", z:'flow'} + ]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -93,6 +98,7 @@ describe('inject node', function() { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); n2.on("input", function (msg) { + delete process.env.NR_TEST try { msg.should.have.property("topic", "t1"); msg.should.have.property("payload", "foo"); @@ -101,13 +107,13 @@ describe('inject node', function() { done(err); } }); - process.env.NR_TEST = 'foo'; + process.env.NR_TEST = 'foo'; n1.receive({}); }); }); it('inject name of node as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_NODE_NAME", payloadType: "env", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "NR_NODE_NAME", payloadType: "env", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -125,7 +131,7 @@ describe('inject node', function() { }); it('inject id of node as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_NODE_ID", payloadType: "env", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "NR_NODE_ID", payloadType: "env", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -143,7 +149,7 @@ describe('inject node', function() { }); it('inject path of node as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_NODE_PATH", payloadType: "env", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "NR_NODE_PATH", payloadType: "env", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -162,7 +168,7 @@ describe('inject node', function() { it('inject name of flow as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_FLOW_NAME", payloadType: "env", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "NR_FLOW_NAME", payloadType: "env", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}, {id: "flow", type: "tab", label: "FLOW" }, ]; @@ -182,7 +188,7 @@ describe('inject node', function() { }); it('inject id of flow as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_FLOW_ID", payloadType: "env", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "NR_FLOW_ID", payloadType: "env", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}, {id: "flow", type: "tab", name: "FLOW" }, ]; @@ -202,9 +208,10 @@ describe('inject node', function() { }); it('inject name of group as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_GROUP_NAME", payloadType: "env", wires: [["n2"]], z: "flow", g: "g0"}, - {id: "n2", type: "helper"}, - {id: "g0", type: "group", name: "GROUP" }, + var flow = [{id: "flow", type: "tab" }, + {id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "NR_GROUP_NAME", payloadType: "env", wires: [["n2"]], z: "flow", g: "g0"}, + {id: "n2", type: "helper", z: "flow"}, + {id: "g0", type: "group", name: "GROUP", z: "flow" }, ]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -222,9 +229,10 @@ describe('inject node', function() { }); it('inject id of group as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "NR_GROUP_ID", payloadType: "env", wires: [["n2"]], z: "flow", g: "g0"}, - {id: "n2", type: "helper"}, - {id: "g0", type: "group", name: "GROUP" }, + var flow = [{id: "flow", type: "tab" }, + {id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "NR_GROUP_ID", payloadType: "env", wires: [["n2"]], z: "flow", g: "g0"}, + {id: "n2", type: "helper", z: "flow"}, + {id: "g0", type: "group", name: "GROUP", z: "flow" }, ]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -243,8 +251,9 @@ describe('inject node', function() { it('inject name of node as environment variable by substitution ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_NODE_NAME}", payloadType: "str", wires: [["n2"]], z: "flow"}, - {id: "n2", type: "helper"}]; + var flow = [{id: "flow", type: "tab" }, + {id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "${NR_NODE_NAME}", payloadType: "str", wires: [["n2"]], z: "flow"}, + {id: "n2", type: "helper", z: "flow"}]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); var n2 = helper.getNode("n2"); @@ -261,7 +270,7 @@ describe('inject node', function() { }); it('inject id of node as environment variable by substitution ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_NODE_ID}", payloadType: "str", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "${NR_NODE_ID}", payloadType: "str", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -279,7 +288,7 @@ describe('inject node', function() { }); it('inject path of node as environment variable by substitution ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_NODE_PATH}", payloadType: "str", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "${NR_NODE_PATH}", payloadType: "str", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -298,7 +307,7 @@ describe('inject node', function() { it('inject name of flow as environment variable by substitution ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_FLOW_NAME}", payloadType: "str", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "${NR_FLOW_NAME}", payloadType: "str", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}, {id: "flow", type: "tab", label: "FLOW" }, ]; @@ -318,7 +327,7 @@ describe('inject node', function() { }); it('inject id of flow as environment variable ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_FLOW_ID}", payloadType: "str", wires: [["n2"]], z: "flow"}, + var flow = [{id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "${NR_FLOW_ID}", payloadType: "str", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}, {id: "flow", type: "tab", name: "FLOW" }, ]; @@ -338,9 +347,10 @@ describe('inject node', function() { }); it('inject name of group as environment variable by substitution ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_GROUP_NAME}", payloadType: "str", wires: [["n2"]], z: "flow", g: "g0"}, - {id: "n2", type: "helper"}, - {id: "g0", type: "group", name: "GROUP" }, + var flow = [{id: "flow", type: "tab" }, + {id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "${NR_GROUP_NAME}", payloadType: "str", wires: [["n2"]], z: "flow", g: "g0"}, + {id: "n2", type: "helper", z: "flow"}, + {id: "g0", type: "group", name: "GROUP", z: "flow" }, ]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); @@ -358,9 +368,10 @@ describe('inject node', function() { }); it('inject id of group as environment variable by substitution ', function (done) { - var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_GROUP_ID}", payloadType: "str", wires: [["n2"]], z: "flow", g: "g0"}, - {id: "n2", type: "helper"}, - {id: "g0", type: "group", name: "GROUP" }, + var flow = [{id: "flow", type: "tab" }, + {id: "n1", type: "inject", name: "NAME", topic: "t1", payload: "${NR_GROUP_ID}", payloadType: "str", wires: [["n2"]], z: "flow", g: "g0"}, + {id: "n2", type: "helper", z: "flow"}, + {id: "g0", type: "group", name: "GROUP", z: "flow" }, ]; helper.load(injectNode, flow, function () { var n1 = helper.getNode("n1"); diff --git a/test/nodes/core/function/15-change_spec.js b/test/nodes/core/function/15-change_spec.js index 66f02d6d1..b23c1994a 100644 --- a/test/nodes/core/function/15-change_spec.js +++ b/test/nodes/core/function/15-change_spec.js @@ -568,11 +568,12 @@ describe('change Node', function() { it('sets the value using env property from group', function(done) { var flow = [ + {"id": "flow", type:"tab"}, {"id":"group1","type":"group","env":[ {"name":"NR_TEST_A", "value":"bar", "type": "str"} - ]}, - {"id":"changeNode1","type":"change","g":"group1",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]]}, - {id:"helperNode1", type:"helper", wires:[]} + ], z: "flow"}, + {"id":"changeNode1","type":"change","g":"group1",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]], z: "flow"}, + {id:"helperNode1", type:"helper", wires:[], z: "flow"} ]; helper.load(changeNode, flow, function() { var changeNode1 = helper.getNode("changeNode1"); @@ -591,12 +592,13 @@ describe('change Node', function() { it('sets the value using env property from nested group', function(done) { var flow = [ + {"id": "flow", type:"tab"}, {"id":"group1","type":"group","env":[ {"name":"NR_TEST_A", "value":"bar", "type": "str"} - ]}, - {"id":"group2","type":"group","g":"group1","env":[]}, - {"id":"changeNode1","type":"change","g":"group2",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]]}, - {id:"helperNode1", type:"helper", wires:[]} + ], z: "flow"}, + {"id":"group2","type":"group","g":"group1","env":[], z: "flow"}, + {"id":"changeNode1","type":"change","g":"group2",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]], z: "flow"}, + {id:"helperNode1", type:"helper", wires:[], z: "flow"} ]; helper.load(changeNode, flow, function() { var changeNode1 = helper.getNode("changeNode1"); diff --git a/test/nodes/subflow/subflow_spec.js b/test/nodes/subflow/subflow_spec.js index f4b8257d8..e25b45102 100644 --- a/test/nodes/subflow/subflow_spec.js +++ b/test/nodes/subflow/subflow_spec.js @@ -253,35 +253,32 @@ describe('subflow', function() { it('should access typed value of env var', function(done) { var flow = [ - {id:"t0", type:"tab", label:"", disabled:false, info:""}, - {id:"n1", x:10, y:10, z:"t0", type:"subflow:s1", - env: [ - {name: "KN", type: "num", value: "100"}, - {name: "KB", type: "bool", value: "true"}, - {name: "KJ", type: "json", value: "[1,2,3]"}, - {name: "Kb", type: "bin", value: "[65,65]"}, - {name: "Ke", type: "env", value: "KS"}, - {name: "Kj", type: "jsonata", value: "1+2"}, - ], - wires:[["n2"]]}, - {id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]}, - // Subflow - {id:"s1", type:"subflow", name:"Subflow", info:"", - in:[{ - x:10, y:10, - wires:[ {id:"s1-n1"} ] - }], - out:[{ - x:10, y:10, - wires:[ {id:"s1-n1", port:0} ] - }], - env: [ - {name: "KS", type: "str", value: "STR"} - ] + { id: "t0", type: "tab", label: "", disabled: false, info: "" }, + { + id: "n1", x: 10, y: 10, z: "t0", type: "subflow:s1", + env: [ + { name: "KN", type: "num", value: "100" }, + { name: "KB", type: "bool", value: "true" }, + { name: "KJ", type: "json", value: "[1,2,3]" }, + { name: "Kb", type: "bin", value: "[65,65]" }, + { name: "Ke", type: "env", value: "KS" }, + { name: "Kj", type: "jsonata", value: "1+2" }, + ], + wires: [["n2"]] }, - {id:"s1-n1", x:10, y:10, z:"s1", type:"function", - func:"msg.VE = env.get('Ke'); msg.VS = env.get('KS'); msg.VN = env.get('KN'); msg.VB = env.get('KB'); msg.VJ = env.get('KJ'); msg.Vb = env.get('Kb'); msg.Vj = env.get('Kj'); return msg;", - wires:[]} + { id: "n2", x: 10, y: 10, z: "t0", type: "helper", wires: [] }, + // Subflow + { + id: "s1", type: "subflow", name: "Subflow", info: "", + in: [{ x: 10, y: 10, wires: [{ id: "s1-n1" }] }], + out: [{ x: 10, y: 10, wires: [{ id: "s1-n1", port: 0 }] }], + env: [{ name: "KS", type: "str", value: "STR" }] + }, + { + id: "s1-n1", x: 10, y: 10, z: "s1", type: "function", + func: "msg.VE = env.get('Ke'); msg.VS = env.get('KS'); msg.VN = env.get('KN'); msg.VB = env.get('KB'); msg.VJ = env.get('KJ'); msg.Vb = env.get('Kb'); msg.Vj = env.get('Kj'); return msg;", + wires: [] + } ]; helper.load(functionNode, flow, function() { var n1 = helper.getNode("n1"); diff --git a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js index 9f99830b0..9f115c058 100644 --- a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js @@ -29,7 +29,6 @@ var Node = NR_TEST_UTILS.require("@node-red/runtime/lib/nodes/Node"); var hooks = NR_TEST_UTILS.require("@node-red/util/lib/hooks"); var typeRegistry = NR_TEST_UTILS.require("@node-red/registry"); - describe('Flow', function() { var getType; @@ -200,7 +199,7 @@ describe('Flow', function() { }); describe('#start',function() { - it("instantiates an initial configuration and stops it",function(done) { + it("instantiates an initial configuration and stops it", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -209,49 +208,50 @@ describe('Flow', function() { {id:"4",z:"t1",type:"test",foo:"a"} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); + return new Promise((done) => { + Object.keys(flow.getActiveNodes()).should.have.length(4); - Object.keys(flow.getActiveNodes()).should.have.length(4); + flow.getNode('1').should.have.a.property('id','1'); + flow.getNode('2').should.have.a.property('id','2'); + flow.getNode('3').should.have.a.property('id','3'); + flow.getNode('4').should.have.a.property('id','4'); - flow.getNode('1').should.have.a.property('id','1'); - flow.getNode('2').should.have.a.property('id','2'); - flow.getNode('3').should.have.a.property('id','3'); - flow.getNode('4').should.have.a.property('id','4'); + currentNodes.should.have.a.property("1"); + currentNodes.should.have.a.property("2"); + currentNodes.should.have.a.property("3"); + currentNodes.should.have.a.property("4"); - currentNodes.should.have.a.property("1"); - currentNodes.should.have.a.property("2"); - currentNodes.should.have.a.property("3"); - currentNodes.should.have.a.property("4"); + currentNodes["1"].should.have.a.property("handled",0); + currentNodes["2"].should.have.a.property("handled",0); + currentNodes["3"].should.have.a.property("handled",0); - currentNodes["1"].should.have.a.property("handled",0); - currentNodes["2"].should.have.a.property("handled",0); - currentNodes["3"].should.have.a.property("handled",0); + currentNodes["3"].on("input", function() { + currentNodes["1"].should.have.a.property("handled",1); + currentNodes["2"].should.have.a.property("handled",1); + currentNodes["3"].should.have.a.property("handled",1); - currentNodes["3"].on("input", function() { - currentNodes["1"].should.have.a.property("handled",1); - currentNodes["2"].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",1); - - flow.stop().then(function() { - try { - currentNodes.should.not.have.a.property("1"); - currentNodes.should.not.have.a.property("2"); - currentNodes.should.not.have.a.property("3"); - currentNodes.should.not.have.a.property("4"); - stoppedNodes.should.have.a.property("1"); - stoppedNodes.should.have.a.property("2"); - stoppedNodes.should.have.a.property("3"); - stoppedNodes.should.have.a.property("4"); - done(); - } catch(err) { - done(err); - } + flow.stop().then(function() { + try { + currentNodes.should.not.have.a.property("1"); + currentNodes.should.not.have.a.property("2"); + currentNodes.should.not.have.a.property("3"); + currentNodes.should.not.have.a.property("4"); + stoppedNodes.should.have.a.property("1"); + stoppedNodes.should.have.a.property("2"); + stoppedNodes.should.have.a.property("3"); + stoppedNodes.should.have.a.property("4"); + done(); + } catch(err) { + done(err); + } + }); }); - }); - currentNodes["1"].receive({payload:"test"}); + currentNodes["1"].receive({payload:"test"}); + }) }); - it("instantiates config nodes in the right order",function(done) { + it("instantiates config nodes in the right order",async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -261,11 +261,8 @@ describe('Flow', function() { {id:"5",z:"t1",type:"test"} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); - + await flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(5); - - currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); @@ -278,7 +275,7 @@ describe('Flow', function() { currentNodes["4"].should.have.a.property("_index",1); currentNodes["5"].should.have.a.property("_index",0); - flow.stop().then(function() { + return flow.stop().then(function() { currentNodes.should.not.have.a.property("1"); currentNodes.should.not.have.a.property("2"); currentNodes.should.not.have.a.property("3"); @@ -289,12 +286,11 @@ describe('Flow', function() { stoppedNodes.should.have.a.property("3"); stoppedNodes.should.have.a.property("4"); stoppedNodes.should.have.a.property("5"); - done(); }); }); - it("detects dependency loops in config nodes",function() { + it("detects dependency loops in config nodes",async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"node1",z:"t1",type:"test",foo:"node2"}, // This node depends on #5 @@ -302,12 +298,12 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); /*jshint immed: false */ - (function(){ - flow.start(); - }).should.throw("Circular config node dependency detected: node1"); + return flow.start().catch(err => { + err.toString().should.equal("Error: Circular config node dependency detected: node1") + }) }); - it("rewires nodes specified by diff",function(done) { + it("rewires nodes specified by diff", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -317,16 +313,15 @@ describe('Flow', function() { var flow = Flow.create({},config,config.flows["t1"]); createCount.should.equal(0); - flow.start(); + await flow.start(); //TODO: use update to pass in new wiring and verify the change createCount.should.equal(3); flow.start({rewired:["2"]}); createCount.should.equal(3); rewiredNodes.should.have.a.property("2"); - done(); }); - it("instantiates a node with environment variable property values",function(done) { + it("instantiates a node with environment variable property values", async function() { after(function() { delete process.env.NODE_RED_TEST_VALUE; }) @@ -341,7 +336,7 @@ describe('Flow', function() { {id:"6",x:10,y:10,z:"t1",type:"test",foo:["$(NODE_RED_TEST_VALUE)"],wires:[]} ]); var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); @@ -352,12 +347,10 @@ describe('Flow', function() { activeNodes["5"].foo.should.equal("$(NODE_RED_TEST_VALUE_NONE)"); activeNodes["6"].foo[0].should.equal("a-value"); - flow.stop().then(function() { - done(); - }); + await flow.stop() }); - it("ignores disabled nodes",function(done) { + it("ignores disabled nodes", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -368,7 +361,7 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(3); @@ -387,28 +380,20 @@ describe('Flow', function() { currentNodes["3"].should.have.a.property("handled",0); currentNodes["1"].receive({payload:"test"}); + await NR_TEST_UTILS.sleep(50) + currentNodes["1"].should.have.a.property("handled",1); + // Message doesn't reach 3 as 2 is disabled + currentNodes["3"].should.have.a.property("handled",0); - setTimeout(function() { - currentNodes["1"].should.have.a.property("handled",1); - // Message doesn't reach 3 as 2 is disabled - currentNodes["3"].should.have.a.property("handled",0); - - flow.stop().then(function() { - try { - currentNodes.should.not.have.a.property("1"); - currentNodes.should.not.have.a.property("2"); - currentNodes.should.not.have.a.property("3"); - currentNodes.should.not.have.a.property("4"); - stoppedNodes.should.have.a.property("1"); - stoppedNodes.should.not.have.a.property("2"); - stoppedNodes.should.have.a.property("3"); - stoppedNodes.should.have.a.property("4"); - done(); - } catch(err) { - done(err); - } - }); - },50); + await flow.stop() + currentNodes.should.not.have.a.property("1"); + currentNodes.should.not.have.a.property("2"); + currentNodes.should.not.have.a.property("3"); + currentNodes.should.not.have.a.property("4"); + stoppedNodes.should.have.a.property("1"); + stoppedNodes.should.not.have.a.property("2"); + stoppedNodes.should.have.a.property("3"); + stoppedNodes.should.have.a.property("4"); }); }); @@ -416,7 +401,7 @@ describe('Flow', function() { describe('#stop', function() { - it("stops all nodes",function(done) { + it("stops all nodes", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -424,25 +409,21 @@ describe('Flow', function() { {id:"3",x:10,y:10,z:"t1",type:"asyncTest",foo:"a",wires:[]} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); - - + await flow.start(); currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); - flow.stop().then(function() { - currentNodes.should.not.have.a.property("1"); - currentNodes.should.not.have.a.property("2"); - currentNodes.should.not.have.a.property("3"); - stoppedNodes.should.have.a.property("1"); - stoppedNodes.should.have.a.property("2"); - stoppedNodes.should.have.a.property("3"); - done(); - }).catch(done); + await flow.stop() + currentNodes.should.not.have.a.property("1"); + currentNodes.should.not.have.a.property("2"); + currentNodes.should.not.have.a.property("3"); + stoppedNodes.should.have.a.property("1"); + stoppedNodes.should.have.a.property("2"); + stoppedNodes.should.have.a.property("3"); }); - it("stops specified nodes",function(done) { + it("stops specified nodes", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -450,24 +431,21 @@ describe('Flow', function() { {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); - + await flow.start(); currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); - flow.stop(["2"]).then(function() { - currentNodes.should.have.a.property("1"); - currentNodes.should.not.have.a.property("2"); - currentNodes.should.have.a.property("3"); - stoppedNodes.should.not.have.a.property("1"); - stoppedNodes.should.have.a.property("2"); - stoppedNodes.should.not.have.a.property("3"); - done(); - }); + await flow.stop(["2"]) + currentNodes.should.have.a.property("1"); + currentNodes.should.not.have.a.property("2"); + currentNodes.should.have.a.property("3"); + stoppedNodes.should.not.have.a.property("1"); + stoppedNodes.should.have.a.property("2"); + stoppedNodes.should.not.have.a.property("3"); }); - it("stops config nodes last",function(done) { + it("stops config nodes last", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -478,7 +456,7 @@ describe('Flow', function() { {id:"c3",z:"t1",type:"test"} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); @@ -488,14 +466,12 @@ describe('Flow', function() { currentNodes.should.have.a.property("c3"); stoppedOrder.should.have.a.length(0); - flow.stop().then(function() { - stoppedOrder.should.eql([ '1', '2', '3', 'c1', 'c2', 'c3' ]); - done(); - }).catch(done); + await flow.stop() + stoppedOrder.should.eql([ '1', '2', '3', 'c1', 'c2', 'c3' ]); }); - it("Times out a node that fails to close", function(done) { + it("Times out a node that fails to close", async function() { Flow.init({settings:{nodeCloseTimeout:50},log:{ log: sinon.stub(), debug: sinon.stub(), @@ -512,31 +488,28 @@ describe('Flow', function() { {id:"3",x:10,y:10,z:"t1",type:"test",foo:"a",wires:[]} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); currentNodes.should.have.a.property("1"); currentNodes.should.have.a.property("2"); currentNodes.should.have.a.property("3"); - flow.stop().then(function() { - currentNodes.should.have.a.property("1"); - currentNodes.should.not.have.a.property("2"); - currentNodes.should.not.have.a.property("3"); - stoppedNodes.should.not.have.a.property("1"); - stoppedNodes.should.have.a.property("2"); - stoppedNodes.should.have.a.property("3"); - setTimeout(function() { - currentNodes.should.not.have.a.property("1"); - stoppedNodes.should.have.a.property("1"); - done(); - },40) - }); + await flow.stop() + currentNodes.should.have.a.property("1"); + currentNodes.should.not.have.a.property("2"); + currentNodes.should.not.have.a.property("3"); + stoppedNodes.should.not.have.a.property("1"); + stoppedNodes.should.have.a.property("2"); + stoppedNodes.should.have.a.property("3"); + await NR_TEST_UTILS.sleep(40) + currentNodes.should.not.have.a.property("1"); + stoppedNodes.should.have.a.property("1"); }); }); describe('#getNode',function() { - it("gets a node known to the flow",function(done) { + it("gets a node known to the flow", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -545,16 +518,13 @@ describe('Flow', function() { {id:"4",z:"t1",type:"test",foo:"a"} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); - + await flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(4); - flow.getNode('1').should.have.a.property('id','1'); - - flow.stop().then(() => { done() }); + await flow.stop(); }); - it("passes to parent if node not known locally",function(done) { + it("passes to parent if node not known locally", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -565,19 +535,14 @@ describe('Flow', function() { var flow = Flow.create({ getNode: id => { return {id:id}} },config,config.flows["t1"]); - flow.start(); - + await flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(4); - flow.getNode('1').should.have.a.property('id','1'); - flow.getNode('parentNode').should.have.a.property('id','parentNode'); - - - flow.stop().then(() => { done() }); + await flow.stop() }); - it("does not pass to parent if cancelBubble set",function(done) { + it("does not pass to parent if cancelBubble set", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -588,19 +553,16 @@ describe('Flow', function() { var flow = Flow.create({ getNode: id => { return {id:id}} },config,config.flows["t1"]); - flow.start(); - + await flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(4); - flow.getNode('1').should.have.a.property('id','1'); - should.not.exist(flow.getNode('parentNode',true)); - flow.stop().then(() => { done() }); + await flow.stop() }); }); describe("#handleStatus",function() { - it("passes a status event to the adjacent status node",function(done) { + it("passes a status event to the adjacent status node", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -611,44 +573,34 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(5); flow.handleStatus(config.flows["t1"].nodes["1"],{text:"my-status",random:"otherProperty"}); - - setTimeout(function() { - - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; - - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","my-status"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id","1"); - statusMessage.status.source.should.have.a.property("type","test"); - statusMessage.status.source.should.have.a.property("name","a"); - - currentNodes["sn2"].should.have.a.property("handled",1); - statusMessage = currentNodes["sn2"].messages[0]; - - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","my-status"); - statusMessage.status.should.have.a.property("random","otherProperty"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id","1"); - statusMessage.status.source.should.have.a.property("type","test"); - statusMessage.status.source.should.have.a.property("name","a"); - - - flow.stop().then(function() { - done(); - }); - },50) + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","my-status"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id","1"); + statusMessage.status.source.should.have.a.property("type","test"); + statusMessage.status.source.should.have.a.property("name","a"); + currentNodes["sn2"].should.have.a.property("handled",1); + statusMessage = currentNodes["sn2"].messages[0]; + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","my-status"); + statusMessage.status.should.have.a.property("random","otherProperty"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id","1"); + statusMessage.status.source.should.have.a.property("type","test"); + statusMessage.status.source.should.have.a.property("name","a"); + await flow.stop() }); - it("passes a status event to the adjacent scoped status node ",function(done) { + it("passes a status event to the adjacent scoped status node ", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -659,39 +611,32 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(5); - flow.handleStatus(config.flows["t1"].nodes["1"],{text:"my-status"}); - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",0); - currentNodes["sn2"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn2"].messages[0]; - - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","my-status"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id","1"); - statusMessage.status.source.should.have.a.property("type","test"); - statusMessage.status.source.should.have.a.property("name","a"); - - - flow.stop().then(function() { - done(); - }); - },50); + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",0); + currentNodes["sn2"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn2"].messages[0]; + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","my-status"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id","1"); + statusMessage.status.source.should.have.a.property("type","test"); + statusMessage.status.source.should.have.a.property("name","a"); + await flow.stop() }); - it("passes a status event to the group scoped status node",function(done) { + it("passes a status event to the group scoped status node", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, - {id: "g1", type: "group", g: "g3" }, - {id: "g2", type: "group" }, - {id: "g3", type: "group" }, + {id: "g1", type: "group", g: "g3", z:"t1" }, + {id: "g2", type: "group", z:"t1" }, + {id: "g3", type: "group", z:"t1" }, {id:"1",x:10,y:10,z:"t1",g:"g1", type:"test",name:"a",wires:["2"]}, // sn - in the same group as source node {id:"sn",x:10,y:10,z:"t1",g:"g1", type:"status",scope:"group",wires:[]}, @@ -705,29 +650,21 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); flow.handleStatus(config.flows["t1"].nodes["1"],{text:"my-status"}); - setTimeout(function() { - try { - currentNodes["sn"].should.have.a.property("handled",1); - currentNodes["sn2"].should.have.a.property("handled",0); - currentNodes["sn3"].should.have.a.property("handled",1); - currentNodes["sn3"].should.have.a.property("handled",1); - done() - } catch(err) { - done(err) - } - },50); + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",1); + currentNodes["sn2"].should.have.a.property("handled",0); + currentNodes["sn3"].should.have.a.property("handled",1); + currentNodes["sn3"].should.have.a.property("handled",1); + await flow.stop() }); - - - }); describe("#handleError",function() { - it("passes an error event to the adjacent catch node",function(done) { + it("passes an error event to the adjacent catch node", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -739,45 +676,38 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(6); - - flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo"}); + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","my-error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("id","1"); + statusMessage.error.source.should.have.a.property("type","test"); + statusMessage.error.source.should.have.a.property("name","a"); - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","my-error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("id","1"); - statusMessage.error.source.should.have.a.property("type","test"); - statusMessage.error.source.should.have.a.property("name","a"); + currentNodes["sn2"].should.have.a.property("handled",1); + statusMessage = currentNodes["sn2"].messages[0]; - currentNodes["sn2"].should.have.a.property("handled",1); - statusMessage = currentNodes["sn2"].messages[0]; + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","my-error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("id","1"); + statusMessage.error.source.should.have.a.property("type","test"); + statusMessage.error.source.should.have.a.property("name","a"); - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","my-error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("id","1"); - statusMessage.error.source.should.have.a.property("type","test"); - statusMessage.error.source.should.have.a.property("name","a"); - - // Node sn3 has uncaught:true - so should not get called - currentNodes["sn3"].should.have.a.property("handled",0); - - - flow.stop().then(function() { - done(); - }); - },50); + // Node sn3 has uncaught:true - so should not get called + currentNodes["sn3"].should.have.a.property("handled",0); + await flow.stop() }); - it("passes an error event to the adjacent scoped catch node ",function(done) { + + it("passes an error event to the adjacent scoped catch node ", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -790,56 +720,50 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(7); flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo"}); + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",0); + currentNodes["sn2"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn2"].messages[0]; - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",0); - currentNodes["sn2"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn2"].messages[0]; + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","my-error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("id","1"); + statusMessage.error.source.should.have.a.property("type","test"); + statusMessage.error.source.should.have.a.property("name","a"); - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","my-error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("id","1"); - statusMessage.error.source.should.have.a.property("type","test"); - statusMessage.error.source.should.have.a.property("name","a"); + // Node sn3/4 have uncaught:true - so should not get called + currentNodes["sn3"].should.have.a.property("handled",0); + currentNodes["sn4"].should.have.a.property("handled",0); - // Node sn3/4 have uncaught:true - so should not get called - currentNodes["sn3"].should.have.a.property("handled",0); - currentNodes["sn4"].should.have.a.property("handled",0); + // Inject error that sn1/2 will ignore - so should get picked up by sn3 + flow.handleError(config.flows["t1"].nodes["3"],"my-error-2",{a:"foo-2"}); - // Inject error that sn1/2 will ignore - so should get picked up by sn3 - flow.handleError(config.flows["t1"].nodes["3"],"my-error-2",{a:"foo-2"}); - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",0); - currentNodes["sn2"].should.have.a.property("handled",1); - currentNodes["sn3"].should.have.a.property("handled",1); - currentNodes["sn4"].should.have.a.property("handled",1); - - statusMessage = currentNodes["sn3"].messages[0]; - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","my-error-2"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("id","3"); - statusMessage.error.source.should.have.a.property("type","test"); - - flow.stop().then(function() { - done(); - }); - },50); - },50); + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",0); + currentNodes["sn2"].should.have.a.property("handled",1); + currentNodes["sn3"].should.have.a.property("handled",1); + currentNodes["sn4"].should.have.a.property("handled",1); + statusMessage = currentNodes["sn3"].messages[0]; + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","my-error-2"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("id","3"); + statusMessage.error.source.should.have.a.property("type","test"); + await flow.stop() }); - it("passes an error event to the group scoped catch node",function(done) { + it("passes an error event to the group scoped catch node",async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, - {id: "g1", type: "group", g: "g3" }, - {id: "g2", type: "group" }, - {id: "g3", type: "group" }, + {id: "g1", type: "group", g: "g3", z:"t1" }, + {id: "g2", type: "group", z:"t1" }, + {id: "g3", type: "group", z:"t1" }, {id:"1",x:10,y:10,z:"t1",g:"g1", type:"test",name:"a",wires:["2"]}, // sn - in the same group as source node {id:"sn",x:10,y:10,z:"t1",g:"g1", type:"catch",scope:"group",wires:[]}, @@ -853,24 +777,20 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo"}); - setTimeout(function() { - try { - currentNodes["sn"].should.have.a.property("handled",1); - currentNodes["sn2"].should.have.a.property("handled",0); - currentNodes["sn3"].should.have.a.property("handled",1); - currentNodes["sn3"].should.have.a.property("handled",1); - done() - } catch(err) { - done(err) - } - },50); + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",1); + currentNodes["sn2"].should.have.a.property("handled",0); + currentNodes["sn3"].should.have.a.property("handled",1); + currentNodes["sn3"].should.have.a.property("handled",1); + await flow.stop() }); - it("moves any existing error object sideways",function(done){ + + it("moves any existing error object sideways", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -880,33 +800,30 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo",error:"existing"}); - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + await NR_TEST_UTILS.sleep(50) + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - statusMessage.should.have.a.property("_error","existing"); - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","my-error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("id","1"); - statusMessage.error.source.should.have.a.property("type","test"); - statusMessage.error.source.should.have.a.property("name","a"); + statusMessage.should.have.a.property("_error","existing"); + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","my-error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("id","1"); + statusMessage.error.source.should.have.a.property("type","test"); + statusMessage.error.source.should.have.a.property("name","a"); - flow.stop().then(function() { - done(); - }); - },50); + await flow.stop() }); it("prevents an error looping more than 10 times",function(){}); }); describe("#handleComplete",function() { - it("passes a complete event to the adjacent Complete node",function(done) { + it("passes a complete event to the adjacent Complete node",async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"testDone",name:"a",wires:["2"]}, @@ -916,143 +833,154 @@ describe('Flow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(4); var msg = {payload: "hello world"} var n1 = currentNodes["1"].receive(msg); - setTimeout(function() { - currentNodes["cn"].should.have.a.property("handled",2); - currentNodes["cn"].messages[0].should.have.a.property("handled",1); - currentNodes["cn"].messages[1].should.have.a.property("handled",2); - flow.stop().then(function() { - done(); - }); - },50); + await NR_TEST_UTILS.sleep(50) + + currentNodes["cn"].should.have.a.property("handled",2); + currentNodes["cn"].messages[0].should.have.a.property("handled",1); + currentNodes["cn"].messages[1].should.have.a.property("handled",2); + await flow.stop() }); }); describe("#send", function() { - it("sends a message - no cloning", function(done) { - var shutdownTest = function(err) { - hooks.clear(); - flow.stop().then(() => { done(err) }); - } + it("sends a message - no cloning", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(2); var n1 = flow.getNode('1'); var n2 = flow.getNode('2'); var messageReceived = false; - n2.receive = function(msg) { - messageReceived = true; - try { - msg.should.be.exactly(message); - shutdownTest(); - } catch(err) { - shutdownTest(err); + + return new Promise((resolve, reject) => { + const shutdownTest = async function(err) { + hooks.clear(); + await flow.stop() + if (err) { reject(err) } + else { resolve() } } - } - - var message = {payload:"hello"} - flow.send([{ - msg: message, - source: { id:"1", node: n1 }, - destination: { id:"2", node: undefined }, - cloneMessage: false - }]) - messageReceived.should.be.false() - }) - it("sends a message - cloning", function(done) { - var shutdownTest = function(err) { - hooks.clear(); - flow.stop().then(() => { done(err) }); - } - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab"}, - {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, - {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} - ]); - var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); - - Object.keys(flow.getActiveNodes()).should.have.length(2); - - var n1 = flow.getNode('1'); - var n2 = flow.getNode('2'); - - n2.receive = function(msg) { - try { - // Message should be cloned - msg.should.be.eql(message); - msg.should.not.be.exactly(message); - shutdownTest(); - } catch(err) { - shutdownTest(err); - } - } - - var message = {payload:"hello"} - flow.send([{ - msg: message, - source: { id:"1", node: n1 }, - destination: { id:"2", node: undefined }, - cloneMessage: true - }]) - }) - it("sends multiple messages", function(done) { - var shutdownTest = function(err) { - hooks.clear(); - flow.stop().then(() => { done(err) }); - } - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab"}, - {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, - {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} - ]); - var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); - - Object.keys(flow.getActiveNodes()).should.have.length(2); - - var n1 = flow.getNode('1'); - var n2 = flow.getNode('2'); - - var messageCount = 0; - n2.receive = function(msg) { - try { - msg.should.be.exactly(messages[messageCount++]); - if (messageCount === 2) { + n2.receive = function(msg) { + messageReceived = true; + try { + msg.should.be.exactly(message); shutdownTest(); + } catch(err) { + shutdownTest(err); } - } catch(err) { - shutdownTest(err); } - } - var messages = [{payload:"hello"},{payload:"world"}]; - - flow.send([{ - msg: messages[0], - source: { id:"1", node: n1 }, - destination: { id:"2", node: undefined } - },{ - msg: messages[1], - source: { id:"1", node: n1 }, - destination: { id:"2", node: undefined } - }]) + var message = {payload:"hello"} + flow.send([{ + msg: message, + source: { id:"1", node: n1 }, + destination: { id:"2", node: undefined }, + cloneMessage: false + }]) + messageReceived.should.be.false() + }) }) - it("sends a message - triggers hooks", function(done) { + it("sends a message - cloning", async function() { + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} + ]); + var flow = Flow.create({},config,config.flows["t1"]); + await flow.start(); + + Object.keys(flow.getActiveNodes()).should.have.length(2); + + var n1 = flow.getNode('1'); + var n2 = flow.getNode('2'); + + return new Promise((resolve, reject) => { + const shutdownTest = async function(err) { + hooks.clear(); + await flow.stop() + if (err) { reject(err) } + else { resolve() } + } + n2.receive = function(msg) { + try { + // Message should be cloned + msg.should.be.eql(message); + msg.should.not.be.exactly(message); + shutdownTest(); + } catch(err) { + shutdownTest(err); + } + } + + var message = {payload:"hello"} + flow.send([{ + msg: message, + source: { id:"1", node: n1 }, + destination: { id:"2", node: undefined }, + cloneMessage: true + }]) + }) + }) + it("sends multiple messages", async function() { + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab"}, + {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, + {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} + ]); + var flow = Flow.create({},config,config.flows["t1"]); + await flow.start(); + + Object.keys(flow.getActiveNodes()).should.have.length(2); + + var n1 = flow.getNode('1'); + var n2 = flow.getNode('2'); + return new Promise((resolve, reject) => { + const shutdownTest = async function(err) { + hooks.clear(); + await flow.stop() + if (err) { reject(err) } + else { resolve() } + } + var messageCount = 0; + n2.receive = function(msg) { + try { + msg.should.be.exactly(messages[messageCount++]); + if (messageCount === 2) { + shutdownTest(); + } + } catch(err) { + shutdownTest(err); + } + } + + var messages = [{payload:"hello"},{payload:"world"}]; + + flow.send([{ + msg: messages[0], + source: { id:"1", node: n1 }, + destination: { id:"2", node: undefined } + },{ + msg: messages[1], + source: { id:"1", node: n1 }, + destination: { id:"2", node: undefined } + }]) + }) + }) + it("sends a message - triggers hooks", async function() { + const message = {payload:"hello"} var hookErrors = []; var messageReceived = false; var hooksCalled = []; @@ -1100,45 +1028,49 @@ describe('Flow', function() { } }) - var shutdownTest = function(err) { - hooks.clear(); - flow.stop().then(() => { done(err) }); - } var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); Object.keys(flow.getActiveNodes()).should.have.length(2); var n1 = flow.getNode('1'); var n2 = flow.getNode('2'); - n2.receive = function(msg) { - messageReceived = true; - try { - msg.should.be.eql(message); - msg.should.not.be.exactly(message); - hooksCalled.should.eql(["onSend","preRoute","preDeliver","postDeliver"]) - if (hookErrors.length > 0) { - shutdownTest(hookErrors[0]) - } else { - shutdownTest(); - } - } catch(err) { - shutdownTest(err); + return new Promise((resolve, reject) => { + const shutdownTest = async function(err) { + hooks.clear(); + await flow.stop() + if (err) { reject(err) } + else { resolve() } + } + n2.receive = function(msg) { + messageReceived = true; + try { + msg.should.be.eql(message); + msg.should.not.be.exactly(message); + hooksCalled.should.eql(["onSend","preRoute","preDeliver","postDeliver"]) + if (hookErrors.length > 0) { + shutdownTest(hookErrors[0]) + } else { + shutdownTest(); + } + } catch(err) { + shutdownTest(err); + } } - } - var message = {payload:"hello"} - flow.send([{ - msg: message, - source: { id:"1", node: n1 }, - destination: { id:"2", node: undefined }, - cloneMessage: true - }]) + + flow.send([{ + msg: message, + source: { id:"1", node: n1 }, + destination: { id:"2", node: undefined }, + cloneMessage: true + }]) + }) }) describe("errors thrown by hooks are reported to the sending node", function() { @@ -1146,7 +1078,7 @@ describe('Flow', function() { var n1,n2; var messageReceived = false; var errorReceived = null; - before(function() { + before(async function() { hooks.add("onSend", function(sendEvents) { if (sendEvents[0].msg.payload === "trigger-onSend") { throw new Error("onSend Error"); @@ -1173,7 +1105,7 @@ describe('Flow', function() { {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} ]); flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); n1 = flow.getNode('1'); n2 = flow.getNode('2'); n2.receive = function(msg) { @@ -1184,9 +1116,9 @@ describe('Flow', function() { } }) - after(function(done) { + after(async function() { hooks.clear(); - flow.stop().then(() => { done() }); + await flow.stop() }) beforeEach(function() { messageReceived = false; @@ -1223,7 +1155,7 @@ describe('Flow', function() { var n1,n2; var messageReceived = false; var errorReceived = false; - before(function() { + before(async function() { hooks.add("onSend", function(sendEvents) { if (sendEvents[0].msg.payload === "trigger-onSend") { return false @@ -1250,7 +1182,7 @@ describe('Flow', function() { {id:"2",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["3"]} ]); flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); n1 = flow.getNode('1'); n2 = flow.getNode('2'); n2.receive = function(msg) { @@ -1261,9 +1193,9 @@ describe('Flow', function() { } }) - after(function(done) { + after(async function() { hooks.clear(); - flow.stop().then(() => { done() }); + await flow.stop() }) function testSend(payload,messageReceivedExpected,errorReceivedExpected,done) { messageReceived = false; @@ -1303,170 +1235,139 @@ describe('Flow', function() { }) describe("#env", function () { - it("can instantiate a node with environment variable property values of group and tab", function (done) { - try { - after(function() { - delete process.env.V0; - delete process.env.V1; - }) - process.env.V0 = "gv0"; - process.env.V1 = "gv1"; - process.env.V3 = "gv3"; - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab",env:[ - {"name": "V0", value: "t1v0", type: "str"}, - {"name": "V2", value: "t1v2", type: "str"} - ]}, - {id:"g1",type:"group",z:"t1",env:[ - {"name": "V0", value: "g1v0", type: "str"}, - {"name": "V1", value: "g1v1", type: "str"} - ]}, - {id:"g2",type:"group",z:"t1",g:"g1",env:[ - {"name": "V1", value: "g2v1", type: "str"} - ]}, - {id:"t1__V0",x:10,y:10,z:"t1",type:"test",foo:"${V0}",wires:[]}, // V0 will come from tab env V0 - {id:"t1g1V0",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"${V0}",wires:[]}, // V0 will come from group 1 env V0 - {id:"t1g1V1",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"${V1}",wires:[]}, // V1 will come from group 1 env V1 - {id:"t1g2V0",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${V0}",wires:[]}, // V0 will come from group 1 env V0 - {id:"t1g2V1",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${V1}",wires:[]}, // V1 will come from group 2 env V1 - {id:"t1g2V2",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${V2}",wires:[]}, // V2 will come from tab 1 env V2 - {id:"t1g2V3",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${V3}",wires:[]}, // V3 will come from process env V3 + it("can instantiate a node with environment variable property values of group and tab", async function () { + after(function() { + delete process.env.V0; + delete process.env.V1; + }) + process.env.V0 = "gv0"; + process.env.V1 = "gv1"; + process.env.V3 = "gv3"; + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab",env:[ + {"name": "V0", value: "t1v0", type: "str"}, + {"name": "V2", value: "t1v2", type: "str"} + ]}, + {id:"g1",type:"group",z:"t1",env:[ + {"name": "V0", value: "g1v0", type: "str"}, + {"name": "V1", value: "g1v1", type: "str"} + ]}, + {id:"g2",type:"group",z:"t1",g:"g1",env:[ + {"name": "V1", value: "g2v1", type: "str"} + ]}, + {id:"t1__V0",x:10,y:10,z:"t1",type:"test",foo:"${V0}",wires:[]}, // V0 will come from tab env V0 + {id:"t1g1V0",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"${V0}",wires:[]}, // V0 will come from group 1 env V0 + {id:"t1g1V1",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"${V1}",wires:[]}, // V1 will come from group 1 env V1 + {id:"t1g2V0",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${V0}",wires:[]}, // V0 will come from group 1 env V0 + {id:"t1g2V1",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${V1}",wires:[]}, // V1 will come from group 2 env V1 + {id:"t1g2V2",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${V2}",wires:[]}, // V2 will come from tab 1 env V2 + {id:"t1g2V3",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${V3}",wires:[]}, // V3 will come from process env V3 - {id:"t1__V1",x:10,y:10,z:"t1",type:"test",foo:"${V1}",wires:[]}, - ]); - var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]); - flow.start(); + {id:"t1__V1",x:10,y:10,z:"t1",type:"test",foo:"${V1}",wires:[]}, + ]); + var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]); + await flow.start(); - var activeNodes = flow.getActiveNodes(); + var activeNodes = flow.getActiveNodes(); - activeNodes.t1__V0.foo.should.equal("t1v0"); // node in tab 1, get tab 1 env V0 - activeNodes.t1__V1.foo.should.equal("gv1"); // node in tab 1, get V1, (tab 1 no V1) --> parent (global has V1) - activeNodes.t1g1V0.foo.should.equal("g1v0"); // node in group 1, get V0, (group 1 has V0) - activeNodes.t1g1V1.foo.should.equal("g1v1"); // node in group 1, get V1, (group 1 has V1) - activeNodes.t1g2V0.foo.should.equal("g1v0"); // node in group 2, get V0, (group 2 no V0) --> parent (group 1 has V0) - activeNodes.t1g2V1.foo.should.equal("g2v1"); // node in group 2, get V1, (group 2 has V1) - activeNodes.t1g2V2.foo.should.equal("t1v2"); // node in group 2, get V2, (group 2 no V2) --> parent (tab 1 has V2) - activeNodes.t1g2V3.foo.should.equal("gv3"); // node in group 2, get V3, (group 2 no V3) --> parent (tab 1 no V2) --> parent (global has V3) + activeNodes.t1__V0.foo.should.equal("t1v0"); // node in tab 1, get tab 1 env V0 + activeNodes.t1__V1.foo.should.equal("gv1"); // node in tab 1, get V1, (tab 1 no V1) --> parent (global has V1) + activeNodes.t1g1V0.foo.should.equal("g1v0"); // node in group 1, get V0, (group 1 has V0) + activeNodes.t1g1V1.foo.should.equal("g1v1"); // node in group 1, get V1, (group 1 has V1) + activeNodes.t1g2V0.foo.should.equal("g1v0"); // node in group 2, get V0, (group 2 no V0) --> parent (group 1 has V0) + activeNodes.t1g2V1.foo.should.equal("g2v1"); // node in group 2, get V1, (group 2 has V1) + activeNodes.t1g2V2.foo.should.equal("t1v2"); // node in group 2, get V2, (group 2 no V2) --> parent (tab 1 has V2) + activeNodes.t1g2V3.foo.should.equal("gv3"); // node in group 2, get V3, (group 2 no V3) --> parent (tab 1 no V2) --> parent (global has V3) - flow.stop().then(function() { - done(); - }); - } - catch (e) { - console.log(e.stack); - done(e); - } - }); - it("can access environment variable property using $parent", function (done) { - try { - after(function() { - delete process.env.V0; - delete process.env.V1; - }) - process.env.V0 = "gv0"; - process.env.V1 = "gv1"; - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab",env:[ - {"name": "V0", value: "v0", type: "str"} - ]}, - {id:"g1",type:"group",z:"t1",env:[ - {"name": "V0", value: "v1", type: "str"}, - {"name": "V1", value: "v2", type: "str"} - ]}, - {id:"g2",type:"group",z:"t1",g:"g1",env:[ - {"name": "V1", value: "v3", type: "str"} - ]}, - {id:"1",x:10,y:10,z:"t1",type:"test",foo:"${$parent.V0}",wires:[]}, - {id:"2",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"${$parent.V0}",wires:[]}, - {id:"3",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"${$parent.V1}",wires:[]}, - {id:"4",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${$parent.V1}",wires:[]}, - {id:"5",x:10,y:10,z:"t1",type:"test",foo:"${$parent.V1}",wires:[]}, - ]); - var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]); - flow.start(); - - var activeNodes = flow.getActiveNodes(); - - activeNodes["1"].foo.should.equal("gv0"); - activeNodes["2"].foo.should.equal("v0"); - activeNodes["3"].foo.should.equal("gv1"); - activeNodes["4"].foo.should.equal("v2"); - activeNodes["5"].foo.should.equal("gv1"); - - flow.stop().then(function() { - done(); - }); - } - catch (e) { - console.log(e.stack); - done(e); - } + await flow.stop() }); - it("can define environment variable using JSONata", function (done) { - try { - after(function() { - delete process.env.V0; - }) - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab",env:[ - {"name": "V0", value: "1+2", type: "jsonata"} - ]}, - {id:"g1",type:"group",z:"t1",env:[ - {"name": "V1", value: "2+3", type: "jsonata"}, - ]}, - {id:"1",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V0)",wires:[]}, - {id:"2",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V1)",wires:[]}, - ]); - var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]); - flow.start(); + it("can access environment variable property using $parent", async function () { + after(function() { + delete process.env.V0; + delete process.env.V1; + }) + process.env.V0 = "gv0"; + process.env.V1 = "gv1"; + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab",env:[ + {"name": "V0", value: "v0", type: "str"} + ]}, + {id:"g1",type:"group",z:"t1",env:[ + {"name": "V0", value: "v1", type: "str"}, + {"name": "V1", value: "v2", type: "str"} + ]}, + {id:"g2",type:"group",z:"t1",g:"g1",env:[ + {"name": "V1", value: "v3", type: "str"} + ]}, + {id:"1",x:10,y:10,z:"t1",type:"test",foo:"${$parent.V0}",wires:[]}, + {id:"2",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"${$parent.V0}",wires:[]}, + {id:"3",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"${$parent.V1}",wires:[]}, + {id:"4",x:10,y:10,z:"t1",g:"g2",type:"test",foo:"${$parent.V1}",wires:[]}, + {id:"5",x:10,y:10,z:"t1",type:"test",foo:"${$parent.V1}",wires:[]}, + ]); + var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]); + await flow.start(); - var activeNodes = flow.getActiveNodes(); + var activeNodes = flow.getActiveNodes(); - activeNodes["1"].foo.should.equal(3); - activeNodes["2"].foo.should.equal(5); + activeNodes["1"].foo.should.equal("gv0"); + activeNodes["2"].foo.should.equal("v0"); + activeNodes["3"].foo.should.equal("gv1"); + activeNodes["4"].foo.should.equal("v2"); + activeNodes["5"].foo.should.equal("gv1"); - flow.stop().then(function() { - done(); - }); - } - catch (e) { - console.log(e.stack); - done(e); - } + await flow.stop() }); - it("can access global environment variables defined as JSONata values", function (done) { - try { - after(function() { - delete process.env.V0; - }) - var config = flowUtils.parseConfig([ - {id:"t1",type:"tab",env:[ - {"name": "V0", value: "1+2", type: "jsonata"} - ]}, - {id:"g1",type:"group",z:"t1",env:[ - {"name": "V1", value: "2+3", type: "jsonata"}, - ]}, - {id:"1",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V0)",wires:[]}, - {id:"2",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V1)",wires:[]}, - ]); - var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]); - flow.start(); + it("can define environment variable using JSONata", async function () { + after(function() { + delete process.env.V0; + }) + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab",env:[ + {"name": "V0", value: "1+2", type: "jsonata"} + ]}, + {id:"g1",type:"group",z:"t1",env:[ + {"name": "V1", value: "2+3", type: "jsonata"}, + ]}, + {id:"1",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V0)",wires:[]}, + {id:"2",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V1)",wires:[]}, + ]); + var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]); + await flow.start(); - var activeNodes = flow.getActiveNodes(); + var activeNodes = flow.getActiveNodes(); - activeNodes["1"].foo.should.equal(3); - activeNodes["2"].foo.should.equal(5); + activeNodes["1"].foo.should.equal(3); + activeNodes["2"].foo.should.equal(5); - flow.stop().then(function() { - done(); - }); - } - catch (e) { - console.log(e.stack); - done(e); - } + await flow.stop() + }); + + it("can access global environment variables defined as JSONata values", async function () { + after(function() { + delete process.env.V0; + }) + var config = flowUtils.parseConfig([ + {id:"t1",type:"tab",env:[ + {"name": "V0", value: "1+2", type: "jsonata"} + ]}, + {id:"g1",type:"group",z:"t1",env:[ + {"name": "V1", value: "2+3", type: "jsonata"}, + ]}, + {id:"1",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V0)",wires:[]}, + {id:"2",x:10,y:10,z:"t1",g:"g1",type:"test",foo:"$(V1)",wires:[]}, + ]); + var flow = Flow.create({getSetting:v=>process.env[v]},config,config.flows["t1"]); + await flow.start(); + + var activeNodes = flow.getActiveNodes(); + + activeNodes["1"].foo.should.equal(3); + activeNodes["2"].foo.should.equal(5); + + await flow.stop() }); }); diff --git a/test/unit/@node-red/runtime/lib/flows/Group_spec.js b/test/unit/@node-red/runtime/lib/flows/Group_spec.js new file mode 100644 index 000000000..b547b1c77 --- /dev/null +++ b/test/unit/@node-red/runtime/lib/flows/Group_spec.js @@ -0,0 +1,48 @@ +const should = require("should"); +const NR_TEST_UTILS = require("nr-test-utils"); +const { Group } = NR_TEST_UTILS.require("@node-red/runtime/lib/flows/Group"); + +describe('Group', function () { + describe('getSetting', function () { + it("returns group name/id", async function () { + const group = new Group({ + getSetting: v => v+v + }, { + name: "g1", + id: "group1" + }) + await group.start() + + group.getSetting("NR_GROUP_NAME").should.equal("g1") + group.getSetting("NR_GROUP_ID").should.equal("group1") + }) + it("delegates to parent if not found", async function () { + const group = new Group({ + getSetting: v => v+v + }, { + name: "g1", + id: "group1" + }) + await group.start() + + group.getSetting("123").should.equal("123123") + }) + it("delegates to parent if explicit requested", async function () { + const parentGroup = new Group({ + getSetting: v => v+v + }, { + name: "g0", + id: "group0" + }) + const group = new Group(parentGroup, { + name: "g1", + id: "group1" + }) + await parentGroup.start() + await group.start() + + group.getSetting("$parent.NR_GROUP_NAME").should.equal("g0") + group.getSetting("$parent.NR_GROUP_ID").should.equal("group0") + }) + }) +}) diff --git a/test/unit/@node-red/runtime/lib/flows/Subflow_spec.js b/test/unit/@node-red/runtime/lib/flows/Subflow_spec.js index c2e251447..618205f6d 100644 --- a/test/unit/@node-red/runtime/lib/flows/Subflow_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/Subflow_spec.js @@ -68,11 +68,13 @@ describe('Subflow', function() { this.handled = 0; this.stopped = false; this.received = null; + this.receivedEnv = null; currentNodes[node.id] = node; this.on('input',function(msg) { // console.log(this.id,msg.payload); node.handled++; node.received = msg.payload; + node.receivedEnv = msg.receivedEnv; node.send(msg); }); this.on('close',function() { @@ -185,7 +187,15 @@ describe('Subflow', function() { var flow = node._flow; var val = flow.getSetting("__KEY__"); node.received = val; - node.send({payload: val}); + const receivedEnv = {} + try { + ['__KEY__','__KEY1__','__KEY2__','__KEY3__','__KEY4__'].forEach(k => { + receivedEnv[k] = flow.getSetting(k) + }) + } catch (err) { + console.log(err) + } + node.send({payload: val, receivedEnv}); }); this.on('close',function() { node.stopped = true; @@ -282,7 +292,7 @@ describe('Subflow', function() { getType.restore(); }); describe('#start',function() { - it("instantiates a subflow and stops it",function(done) { + it("instantiates a subflow and stops it", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -297,7 +307,7 @@ describe('Subflow', function() { ]); var flow = Flow.create({handleError: (a,b,c) => { console.log(a,b,c); }},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(4); @@ -332,37 +342,21 @@ describe('Subflow', function() { // currentNodes[sfInstanceId2].should.have.a.property("handled",0); currentNodes["1"].receive({payload:"test"}); + + await NR_TEST_UTILS.sleep(150) - setTimeout(function() { - currentNodes["1"].should.have.a.property("handled",1); - // currentNodes[sfInstanceId].should.have.a.property("handled",1); - // currentNodes[sfInstanceId2].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",1); - currentNodes["4"].should.have.a.property("handled",1); + currentNodes["1"].should.have.a.property("handled",1); + // currentNodes[sfInstanceId].should.have.a.property("handled",1); + // currentNodes[sfInstanceId2].should.have.a.property("handled",1); + currentNodes["3"].should.have.a.property("handled",1); + currentNodes["4"].should.have.a.property("handled",1); - - - flow.stop().then(function() { - Object.keys(currentNodes).should.have.length(0); - Object.keys(stoppedNodes).should.have.length(6); - - // currentNodes.should.not.have.a.property("1"); - // currentNodes.should.not.have.a.property("3"); - // currentNodes.should.not.have.a.property("4"); - // // currentNodes.should.not.have.a.property(sfInstanceId); - // // currentNodes.should.not.have.a.property(sfInstanceId2); - // // currentNodes.should.not.have.a.property(sfConfigId); - // stoppedNodes.should.have.a.property("1"); - // stoppedNodes.should.have.a.property("3"); - // stoppedNodes.should.have.a.property("4"); - // // stoppedNodes.should.have.a.property(sfInstanceId); - // // stoppedNodes.should.have.a.property(sfInstanceId2); - // // stoppedNodes.should.have.a.property(sfConfigId); - done(); - }); - },150); + await flow.stop() + Object.keys(currentNodes).should.have.length(0); + Object.keys(stoppedNodes).should.have.length(6); }); - it("instantiates a subflow inside a subflow and stops it",function(done) { + + it("instantiates a subflow inside a subflow and stops it", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -379,24 +373,20 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); currentNodes["1"].should.have.a.property("handled",0); currentNodes["3"].should.have.a.property("handled",0); currentNodes["1"].receive({payload:"test"}); - - setTimeout(function() { - currentNodes["1"].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",1); - flow.stop().then(function() { - Object.keys(currentNodes).should.have.length(0); - done(); - }); - },150); + await NR_TEST_UTILS.sleep(150) + currentNodes["1"].should.have.a.property("handled",1); + currentNodes["3"].should.have.a.property("handled",1); + await flow.stop() + Object.keys(currentNodes).should.have.length(0); }); - it("rewires a subflow node on update/start",function(done){ + it("rewires a subflow node on update/start", async function(){ var rawConfig = [ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -417,7 +407,7 @@ describe('Subflow', function() { var diff = flowUtils.diffConfigs(config,newConfig); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(4); @@ -429,36 +419,28 @@ describe('Subflow', function() { currentNodes["4"].should.have.a.property("handled",0); currentNodes["1"].receive({payload:"test"}); + await NR_TEST_UTILS.sleep(150) + currentNodes["1"].should.have.a.property("handled",1); + // currentNodes[sfInstanceId].should.have.a.property("handled",1); + // currentNodes[sfInstanceId2].should.have.a.property("handled",1); + currentNodes["3"].should.have.a.property("handled",1); + currentNodes["4"].should.have.a.property("handled",0); - setTimeout(function() { - currentNodes["1"].should.have.a.property("handled",1); - // currentNodes[sfInstanceId].should.have.a.property("handled",1); - // currentNodes[sfInstanceId2].should.have.a.property("handled",1); - currentNodes["3"].should.have.a.property("handled",1); - currentNodes["4"].should.have.a.property("handled",0); + flow.update(newConfig,newConfig.flows["t1"]); + await flow.start(diff) + currentNodes["1"].receive({payload:"test2"}); + await NR_TEST_UTILS.sleep(150) + currentNodes["1"].should.have.a.property("handled",2); + // currentNodes[sfInstanceId].should.have.a.property("handled",2); + // currentNodes[sfInstanceId2].should.have.a.property("handled",2); + currentNodes["3"].should.have.a.property("handled",1); + currentNodes["4"].should.have.a.property("handled",1); - flow.update(newConfig,newConfig.flows["t1"]); - flow.start(diff) - - currentNodes["1"].receive({payload:"test2"}); - setTimeout(function() { - - currentNodes["1"].should.have.a.property("handled",2); - // currentNodes[sfInstanceId].should.have.a.property("handled",2); - // currentNodes[sfInstanceId2].should.have.a.property("handled",2); - currentNodes["3"].should.have.a.property("handled",1); - currentNodes["4"].should.have.a.property("handled",1); - - - flow.stop().then(function() { - done(); - }); - },150); - },150); + await flow.stop() }); }); describe('#stop', function() { - it("stops subflow instance nodes",function(done) { + it("stops subflow instance nodes", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"a",wires:["2"]}, @@ -470,20 +452,18 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); Object.keys(activeNodes).should.have.length(3); Object.keys(stoppedNodes).should.have.length(0); - flow.stop(["2"]).then(function() { - Object.keys(currentNodes).should.have.length(2); - Object.keys(stoppedNodes).should.have.length(1); - done(); - }).catch(done); + await flow.stop(["2"]) + Object.keys(currentNodes).should.have.length(2); + Object.keys(stoppedNodes).should.have.length(1); }); }); describe("#handleStatus",function() { - it("passes a status event to the subflow's parent tab status node - all scope",function(done) { + it("passes a status event to the subflow's parent tab status node - all scope", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -496,27 +476,24 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); activeNodes["1"].receive({payload:"test"}); - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + await NR_TEST_UTILS.sleep(150) + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","test status"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("type","testStatus"); - statusMessage.status.source.should.have.a.property("name","test-status-node"); + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","test status"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("type","testStatus"); + statusMessage.status.source.should.have.a.property("name","test-status-node"); - flow.stop().then(function() { - done(); - }); - },150); + await flow.stop() }); - it("passes a status event to the subflow's parent tab status node - targetted scope",function(done) { + it("passes a status event to the subflow's parent tab status node - targetted scope", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -531,34 +508,30 @@ describe('Subflow', function() { var flow = Flow.create({handleStatus:() => { parentFlowStatusCalled = true} },config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); activeNodes["1"].receive({payload:"test"}); - setTimeout(function() { - parentFlowStatusCalled.should.be.false(); + await NR_TEST_UTILS.sleep(150) + parentFlowStatusCalled.should.be.false(); - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","test status"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("type","testStatus"); - statusMessage.status.source.should.have.a.property("name","test-status-node"); + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","test status"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("type","testStatus"); + statusMessage.status.source.should.have.a.property("name","test-status-node"); - flow.stop().then(function() { - - done(); - }); - },150); + await flow.stop() }); }); describe("status node", function() { - it("emits a status event when a message is passed to a subflow-status node - msg.payload as string", function(done) { + it("emits a status event when a message is passed to a subflow-status node - msg.payload as string", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -578,29 +551,24 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); activeNodes["1"].receive({payload:"test-payload"}); + await NR_TEST_UTILS.sleep(150) - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","test-payload"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id","2"); - statusMessage.status.source.should.have.a.property("type","subflow:sf1"); - - flow.stop().then(function() { - - done(); - }); - },150); + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","test-payload"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id","2"); + statusMessage.status.source.should.have.a.property("type","subflow:sf1"); + await flow.stop() }); - it("emits a status event when a message is passed to a subflow-status node - msg.payload as status obj", function(done) { + it("emits a status event when a message is passed to a subflow-status node - msg.payload as status obj", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -620,29 +588,26 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); activeNodes["1"].receive({payload:{text:"payload-obj"}}); - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + await NR_TEST_UTILS.sleep(150) - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","payload-obj"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id","2"); - statusMessage.status.source.should.have.a.property("type","subflow:sf1"); + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - flow.stop().then(function() { + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","payload-obj"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id","2"); + statusMessage.status.source.should.have.a.property("type","subflow:sf1"); - done(); - }); - },150); + await flow.stop() }); - it("emits a status event when a message is passed to a subflow-status node - msg.status", function(done) { + it("emits a status event when a message is passed to a subflow-status node - msg.status", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -662,29 +627,26 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); activeNodes["1"].receive({status:{text:"status-obj"}}); - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + await NR_TEST_UTILS.sleep(150) - statusMessage.should.have.a.property("status"); - statusMessage.status.should.have.a.property("text","status-obj"); - statusMessage.status.should.have.a.property("source"); - statusMessage.status.source.should.have.a.property("id","2"); - statusMessage.status.source.should.have.a.property("type","subflow:sf1"); + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - flow.stop().then(function() { + statusMessage.should.have.a.property("status"); + statusMessage.status.should.have.a.property("text","status-obj"); + statusMessage.status.should.have.a.property("source"); + statusMessage.status.source.should.have.a.property("id","2"); + statusMessage.status.source.should.have.a.property("type","subflow:sf1"); - done(); - }); - },150); + flow.stop() }); - it("does not emit a regular status event if it contains a subflow-status node", function(done) { + it("does not emit a regular status event if it contains a subflow-status node", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -704,7 +666,7 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); @@ -712,15 +674,12 @@ describe('Subflow', function() { currentNodes["sn"].should.have.a.property("handled",0); - flow.stop().then(function() { - - done(); - }); + await flow.stop() }); }) describe("#handleError",function() { - it("passes an error event to the subflow's parent tab catch node - all scope",function(done) { + it("passes an error event to the subflow's parent tab catch node - all scope",async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -733,28 +692,26 @@ describe('Subflow', function() { ]); var flow = Flow.create({},config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); activeNodes["1"].receive({payload:"test"}); + + await NR_TEST_UTILS.sleep(150) - setTimeout(function() { - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","test error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("type","testError"); - statusMessage.error.source.should.have.a.property("name","test-error-node"); + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","test error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("type","testError"); + statusMessage.error.source.should.have.a.property("name","test-error-node"); - flow.stop().then(function() { - done(); - }); - },150); + await flow.stop() }); - it("passes an error event to the subflow's parent tab catch node - targetted scope",function(done) { + it("passes an error event to the subflow's parent tab catch node - targetted scope", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",name:"a",wires:["2"]}, @@ -768,50 +725,31 @@ describe('Subflow', function() { var parentFlowErrorCalled = false; var flow = Flow.create({handleError:() => { parentFlowErrorCalled = true} },config,config.flows["t1"]); - flow.start(); + await flow.start(); var activeNodes = flow.getActiveNodes(); activeNodes["1"].receive({payload:"test"}); - setTimeout(function() { - parentFlowErrorCalled.should.be.false(); + await NR_TEST_UTILS.sleep(150) + + parentFlowErrorCalled.should.be.false(); - currentNodes["sn"].should.have.a.property("handled",1); - var statusMessage = currentNodes["sn"].messages[0]; + currentNodes["sn"].should.have.a.property("handled",1); + var statusMessage = currentNodes["sn"].messages[0]; - statusMessage.should.have.a.property("error"); - statusMessage.error.should.have.a.property("message","test error"); - statusMessage.error.should.have.a.property("source"); - statusMessage.error.source.should.have.a.property("type","testError"); - statusMessage.error.source.should.have.a.property("name","test-error-node"); - - flow.stop().then(function() { - done(); - }); - },150); + statusMessage.should.have.a.property("error"); + statusMessage.error.should.have.a.property("message","test error"); + statusMessage.error.should.have.a.property("source"); + statusMessage.error.source.should.have.a.property("type","testError"); + statusMessage.error.source.should.have.a.property("name","test-error-node"); + await flow.stop() }); }); describe("#env var", function() { - // should be changed according to internal env var representation - function setEnv(node, key, val) { - var flow = node._flow; - if (flow) { - var env = flow.env; - if (!env) { - env = flow.env = {}; - } - env[key] = { - name: key, - type: "str", - value: val - }; - } - } - - it("can access process env var", function(done) { + it("can access process env var", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"t1.1",wires:["2"]}, @@ -828,29 +766,25 @@ describe('Subflow', function() { handleError: (a,b,c) => { console.log(a,b,c); } },config,config.flows["t1"]); - flow.start(); + await flow.start(); process.env["__KEY__"] = "__VAL__"; currentNodes["1"].receive({payload: "test"}); - setTimeout(function() { - currentNodes["3"].should.have.a.property("received", "__VAL__"); - - flow.stop().then(function() { - done(); - }); - },150); + await NR_TEST_UTILS.sleep(150) + currentNodes["3"].should.have.a.property("received", "__VAL__"); + await flow.stop() }); - it("can access subflow env var", function(done) { + it("can access subflow env var", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"t1.1",wires:["2"]}, {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, {id:"3",x:10,y:10,z:"t1",type:"test",foo:"t1.3",wires:[]}, - {id:"sf1",type:"subflow",name:"Subflow 2",info:"", - "in":[ {wires:[{id:"sf1-1"}]} ], - "out":[ {wires:[{id:"sf1-2",port:0}]} ]}, + {id:"sf1",type:"subflow",name:"Subflow 2",info:"",env: [{name: '__KEY__', value: '__VAL1__', type: 'str'}], + "in":[ {wires:[{id:"sf1-1"}]} ], + "out":[ {wires:[{id:"sf1-2",port:0}]} ]}, {id:"sf1-1",type:"test",z:"sf1",foo:"sf1.1",x:166,y:99,wires:[["sf1-2"]]}, {id:"sf1-2",type:"testEnv",z:"sf1",foo:"sf1.2",x:166,y:99,wires:[[]]} ]); @@ -859,7 +793,7 @@ describe('Subflow', function() { handleError: (a,b,c) => { console.log(a,b,c); } },config,config.flows["t1"]); - flow.start(); + await flow.start(); var testenv_node = null; for (var n in currentNodes) { @@ -870,32 +804,30 @@ describe('Subflow', function() { } } process.env["__KEY__"] = "__VAL0__"; - setEnv(testenv_node, "__KEY__", "__VAL1__"); currentNodes["1"].receive({payload: "test"}); - setTimeout(function() { - currentNodes["3"].should.have.a.property("received", "__VAL1__"); + await NR_TEST_UTILS.sleep(150) - flow.stop().then(function() { - done(); - }); - },150); + currentNodes["3"].should.have.a.property("received", "__VAL1__"); + await flow.stop() }); - it("can access nested subflow env var", function(done) { + it("can access nested subflow env var", async function() { var config = flowUtils.parseConfig([ - {id:"t1",type:"tab"}, + {id:"t1",type:"tab", env: [{name: '__KEY1__', value: 't1', type: 'str'}]}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"t1.1",wires:["2"]}, {id:"2",x:10,y:10,z:"t1",type:"subflow:sf1",wires:["3"]}, {id:"3",x:10,y:10,z:"t1",type:"test",foo:"t1.3",wires:[]}, {id:"sf1",type:"subflow",name:"Subflow 1",info:"", - in:[{wires:[{id:"sf1-1"}]}], - out:[{wires:[{id:"sf1-2",port:0}]}]}, + env: [{name: '__KEY2__', value: 'sf1', type: 'str'}], + in:[{wires:[{id:"sf1-1"}]}], + out:[{wires:[{id:"sf1-2",port:0}]}]}, {id:"sf2",type:"subflow",name:"Subflow 2",info:"", - in:[{wires:[{id:"sf2-1"}]}], - out:[{wires:[{id:"sf2-2",port:0}]}]}, + env: [{name: '__KEY3__', value: 'sf2', type: 'str'}], + in:[{wires:[{id:"sf2-1"}]}], + out:[{wires:[{id:"sf2-2",port:0}]}]}, {id:"sf1-1",type:"test",z:"sf1",foo:"sf1.1",x:166,y:99,wires:[["sf1-2"]]}, - {id:"sf1-2",type:"subflow:sf2",z:"sf1",x:166,y:99,wires:[[]]}, + {id:"sf1-2",type:"subflow:sf2",z:"sf1",x:166,y:99,wires:[[]], env: [{name: '__KEY4__', value: 'sf1-2', type: 'str'}] }, {id:"sf2-1",type:"test",z:"sf2",foo:"sf2.1",x:166,y:99,wires:[["sf2-2"]]}, {id:"sf2-2",type:"testEnv",z:"sf2",foo:"sf2.2",x:166,y:99,wires:[[]]}, ]); @@ -904,45 +836,22 @@ describe('Subflow', function() { handleError: (a,b,c) => { console.log(a,b,c); } },config,config.flows["t1"]); - flow.start(); - - var node_sf1_1 = null; - var node_sf2_1 = null; - var testenv_node = null; - for (var n in currentNodes) { - var node = currentNodes[n]; - if (node.foo === "sf1.1") { - node_sf1_1 = node; - } - if (node.foo === "sf2.1") { - node_sf2_1 = node; - } - } + await flow.start(); process.env["__KEY__"] = "__VAL0__"; currentNodes["1"].receive({payload: "test"}); - setTimeout(function() { - currentNodes["3"].should.have.a.property("received", "__VAL0__"); - - setEnv(node_sf1_1, "__KEY__", "__VAL1__"); - currentNodes["1"].receive({payload: "test"}); - setTimeout(function() { - currentNodes["3"].should.have.a.property("received", "__VAL1__"); - - setEnv(node_sf2_1, "__KEY__", "__VAL2__"); - currentNodes["1"].receive({payload: "test"}); - setTimeout(function() { - currentNodes["3"].should.have.a.property("received", "__VAL2__"); - - flow.stop().then(function() { - done(); - }); - },150); - },150); - },150); + await NR_TEST_UTILS.sleep(150) + currentNodes["3"].should.have.a.property("receivedEnv"); + currentNodes["3"].receivedEnv.should.have.a.property('__KEY__', '__VAL0__') + currentNodes["3"].receivedEnv.should.have.a.property('__KEY1__', 't1') + currentNodes["3"].receivedEnv.should.have.a.property('__KEY2__', 'sf1') + currentNodes["3"].receivedEnv.should.have.a.property('__KEY3__', 'sf2') + currentNodes["3"].receivedEnv.should.have.a.property('__KEY4__', 'sf1-2') + + await flow.stop() }); - it("can access name of subflow as env var", function(done) { + it("can access name of subflow as env var", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"t1.1",wires:["2"]}, @@ -959,19 +868,15 @@ describe('Subflow', function() { handleError: (a,b,c) => { console.log(a,b,c); } },config,config.flows["t1"]); - flow.start(); + await flow.start(); currentNodes["1"].receive({payload: "test"}); - setTimeout(function() { - currentNodes["3"].should.have.a.property("received", "SFN"); - - flow.stop().then(function() { - done(); - }); - },150); + await NR_TEST_UTILS.sleep(150) + currentNodes["3"].should.have.a.property("received", "SFN"); + await flow.stop() }); - it("can access id of subflow as env var", function(done) { + it("can access id of subflow as env var", async function() { var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, {id:"1",x:10,y:10,z:"t1",type:"test",foo:"t1.1",wires:["2"]}, @@ -988,19 +893,13 @@ describe('Subflow', function() { handleError: (a,b,c) => { console.log(a,b,c); } },config,config.flows["t1"]); - flow.start(); + await flow.start(); currentNodes["1"].receive({payload: "test"}); - setTimeout(function() { - currentNodes["3"].should.have.a.property("received", "2"); - - flow.stop().then(function() { - done(); - }); - },150); + await NR_TEST_UTILS.sleep(150) + currentNodes["3"].should.have.a.property("received", "2"); + await flow.stop() }); - - }); }); diff --git a/test/unit/@node-red/runtime/lib/flows/index_spec.js b/test/unit/@node-red/runtime/lib/flows/index_spec.js index 1a0f2a73c..a07ab7b42 100644 --- a/test/unit/@node-red/runtime/lib/flows/index_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/index_spec.js @@ -93,7 +93,7 @@ describe('flows/index', function() { flowCreate.flows[id] = { flow: flow, global: global, - start: sinon.spy(), + start: sinon.spy(async() => {}), update: sinon.spy(), stop: sinon.spy(), getActiveNodes: function() { @@ -221,13 +221,18 @@ describe('flows/index', function() { return Promise.resolve({flows:originalConfig}); } events.once('flows:started',function() { - flows.setFlows(newConfig,"nodes").then(function() { - flows.getFlows().flows.should.eql(newConfig); - flowCreate.flows['t1'].update.called.should.be.true(); - flowCreate.flows['t2'].start.called.should.be.true(); - flowCreate.flows['_GLOBAL_'].update.called.should.be.true(); - done(); + events.once('flows:started', function() { + try { + flows.getFlows().flows.should.eql(newConfig); + flowCreate.flows['t1'].update.called.should.be.true(); + flowCreate.flows['t2'].start.called.should.be.true(); + flowCreate.flows['_GLOBAL_'].update.called.should.be.true(); + done(); + } catch(err) { + done(err) + } }) + flows.setFlows(newConfig,"nodes") }); flows.init({log:mockLog, settings:{},storage:storage}); @@ -250,13 +255,14 @@ describe('flows/index', function() { } events.once('flows:started',function() { - flows.setFlows(newConfig,"nodes").then(function() { + events.once('flows:started',function() { flows.getFlows().flows.should.eql(newConfig); flowCreate.flows['t1'].update.called.should.be.true(); flowCreate.flows['t2'].start.called.should.be.true(); flowCreate.flows['_GLOBAL_'].update.called.should.be.true(); flows.stopFlows().then(done); }) + flows.setFlows(newConfig,"nodes") }); flows.init({log:mockLog, settings:{},storage:storage}); diff --git a/test/unit/@node-red/runtime/lib/flows/util_spec.js b/test/unit/@node-red/runtime/lib/flows/util_spec.js index 6a4571e87..99747f6b4 100644 --- a/test/unit/@node-red/runtime/lib/flows/util_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/util_spec.js @@ -149,7 +149,7 @@ describe('flows/util', function() { {id:"t1",type:"tab"} ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}}},"groups":{},"missingTypes":[]}; + var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"groups":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}}},"missingTypes":[]}; parsedConfig.should.eql(expectedConfig); }); @@ -160,7 +160,7 @@ describe('flows/util', function() { {id:"t1",type:"tab"} ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]},"cn":{"id":"cn","type":"test"},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{"cn":{"id":"cn","type":"test","_users":["t1-1"]}},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]}}}},"groups":{},"missingTypes":[]}; + var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]},"cn":{"id":"cn","type":"test"},"t1":{"id":"t1","type":"tab"}},"subflows":{},"configs":{"cn":{"id":"cn","type":"test","_users":["t1-1"]}},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"groups":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]}}}},"missingTypes":[]}; parsedConfig.should.eql(expectedConfig); }); @@ -172,7 +172,7 @@ describe('flows/util', function() { {id:"t2-1",x:10,y:10,z:"t2",type:"test",wires:[]} ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t2":{"id":"t2","type":"tab"},"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}},"t2":{"id":"t2","type":"tab","subflows":{},"configs":{},"nodes":{"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}}}},"groups":{},"missingTypes":[]}; + var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"t2":{"id":"t2","type":"tab"},"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"groups":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}},"t2":{"id":"t2","type":"tab","subflows":{},"configs":{},"groups":{},"nodes":{"t2-1":{"id":"t2-1","x":10,"y":10,"z":"t2","type":"test","wires":[]}}}},"missingTypes":[]}; parsedConfig.should.eql(expectedConfig); }); @@ -184,7 +184,7 @@ describe('flows/util', function() { {id:"sf1-1",x:10,y:10,z:"sf1",type:"test",wires:[]} ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[]},"sf1":{"id":"sf1","type":"subflow"},"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"subflows":{"sf1":{"id":"sf1","type":"subflow","configs":{},"nodes":{"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"instances":[{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}]}},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}}}},"groups":{},"missingTypes":[]}; + var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[]},"sf1":{"id":"sf1","type":"subflow"},"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"subflows":{"sf1":{"id":"sf1","type":"subflow","configs":{},"groups":{},"nodes":{"sf1-1":{"id":"sf1-1","x":10,"y":10,"z":"sf1","type":"test","wires":[]}},"instances":[{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}]}},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"groups":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"subflow:sf1","wires":[],"subflow":"sf1"}}}},"missingTypes":[]}; parsedConfig.should.eql(expectedConfig); }); @@ -196,7 +196,7 @@ describe('flows/util', function() { ]; var parsedConfig = flowUtil.parseConfig(originalConfig); parsedConfig.missingTypes.should.eql(['missing']); - var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]},"t1-2":{"id":"t1-2","x":10,"y":10,"z":"t1","type":"missing","wires":[]}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]},'t1-2': { id: 't1-2', x: 10, y: 10, z: 't1', type: 'missing', wires: [] }}}},"groups":{},"missingTypes":["missing"]}; + var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]},"t1-2":{"id":"t1-2","x":10,"y":10,"z":"t1","type":"missing","wires":[]}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"groups":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"sf1","wires":[]},'t1-2': { id: 't1-2', x: 10, y: 10, z: 't1', type: 'missing', wires: [] }}}},"missingTypes":["missing"]}; redUtil.compareObjects(parsedConfig,expectedConfig).should.be.true(); }); @@ -206,7 +206,7 @@ describe('flows/util', function() { {id:"cn",type:"test"}, ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]},"cn":{"id":"cn","type":"test"}},"subflows":{},"configs":{"cn":{"id":"cn","type":"test","_users":["t1-1"]}},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]}}}},"groups":{},"missingTypes":[]}; + var expectedConfig = {"allNodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]},"cn":{"id":"cn","type":"test"}},"subflows":{},"configs":{"cn":{"id":"cn","type":"test","_users":["t1-1"]}},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","foo":"cn","wires":[]}}}},"missingTypes":[]}; parsedConfig.should.eql(expectedConfig); }); @@ -217,7 +217,7 @@ describe('flows/util', function() { {id:"g1",type:"group",z:"t1"} ]; var parsedConfig = flowUtil.parseConfig(originalConfig); - var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"g1":{"id":"g1","type":"group","z":"t1"}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}}},"groups":{"g1":{"id":"g1","type":"group","z":"t1"}},"missingTypes":[]} + var expectedConfig = {"allNodes":{"t1":{"id":"t1","type":"tab"},"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]},"g1":{"id":"g1","type":"group","z":"t1"}},"subflows":{},"configs":{},"flows":{"t1":{"id":"t1","type":"tab","subflows":{},"configs":{},"groups":{"g1":{"id":"g1","type":"group","z":"t1"}},"nodes":{"t1-1":{"id":"t1-1","x":10,"y":10,"z":"t1","type":"test","wires":[]}}}},"missingTypes":[]} parsedConfig.should.eql(expectedConfig); });