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

Support npm subflow modules

This commit is contained in:
Nick O'Leary 2020-08-28 16:36:11 +01:00 committed by Nick O'Leary
parent 6e1466e411
commit 9a660f3fe9
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
13 changed files with 499 additions and 254 deletions

View File

@ -414,18 +414,20 @@ RED.editor = (function() {
for (var cred in credDefinition) {
if (credDefinition.hasOwnProperty(cred)) {
var input = $("#" + prefix + '-' + cred);
var value = input.val();
if (credDefinition[cred].type == 'password') {
node.credentials['has_' + cred] = (value !== "");
if (value == '__PWRD__') {
continue;
}
changed = true;
if (input.length > 0) {
var value = input.val();
if (credDefinition[cred].type == 'password') {
node.credentials['has_' + cred] = (value !== "");
if (value == '__PWRD__') {
continue;
}
changed = true;
}
node.credentials[cred] = value;
if (value != node.credentials._[cred]) {
changed = true;
}
node.credentials[cred] = value;
if (value != node.credentials._[cred]) {
changed = true;
}
}
}
}
@ -465,6 +467,7 @@ RED.editor = (function() {
definition.oneditprepare.call(node);
} catch(err) {
console.log("oneditprepare",node.id,node.type,err.toString());
console.log(err.stack);
}
}
// Now invoke any change handlers added to the fields - passing true
@ -1192,7 +1195,7 @@ RED.editor = (function() {
changed = true;
}
} catch(err) {
console.log("oneditsave",editing_node.id,editing_node.type,err.toString());
console.warn("oneditsave",editing_node.id,editing_node.type,err.toString());
}
for (d in editing_node._def.defaults) {
@ -1890,7 +1893,7 @@ RED.editor = (function() {
try {
configTypeDef.oneditsave.call(editing_config_node);
} catch(err) {
console.log("oneditsave",editing_config_node.id,editing_config_node.type,err.toString());
console.warn("oneditsave",editing_config_node.id,editing_config_node.type,err.toString());
}
}
@ -2457,7 +2460,7 @@ RED.editor = (function() {
changed = true;
}
} catch(err) {
console.log("oneditsave",editing_node.id,editing_node.type,err.toString());
console.warn("oneditsave",editing_node.id,editing_node.type,err.toString());
}
for (d in editing_node._def.defaults) {

View File

@ -1725,22 +1725,54 @@ RED.subflow = (function() {
parentEnv[env.name] = item;
})
}
}
if (node.env) {
for (var i = 0; i < node.env.length; i++) {
var env = node.env[i];
if (parentEnv.hasOwnProperty(env.name)) {
parentEnv[env.name].type = env.type;
parentEnv[env.name].value = env.value;
} else {
// envList.push({
// name: env.name,
// type: env.type,
// value: env.value,
// });
if (node.env) {
for (var i = 0; i < node.env.length; i++) {
var env = node.env[i];
if (parentEnv.hasOwnProperty(env.name)) {
parentEnv[env.name].type = env.type;
parentEnv[env.name].value = env.value;
} else {
// envList.push({
// name: env.name,
// type: env.type,
// value: env.value,
// });
}
}
}
} else if (/^sf-/.test(node.type)) {
var keys = Object.keys(node._def.defaults);
keys.forEach(function(name) {
if (name !== 'name') {
var prop = node._def.defaults[name];
var nodeProp = node[name];
var nodePropType;
var nodePropValue = nodeProp;
if (prop.ui && prop.ui.type === "cred") {
nodePropType = "cred";
} else {
switch(typeof nodeProp) {
case "string": nodePropType = "str"; break;
case "number": nodePropType = "num"; break;
case "boolean": nodePropType = "bool"; nodePropValue = nodeProp?"true":"false"; break;
default:
nodePropType = nodeProp.type;
nodePropValue = nodeProp.value;
}
}
var item = {
name: name,
type: nodePropType,
value: nodePropValue,
parent: {
type: prop.type,
value: prop.value
},
ui: $.extend(true,{},prop.ui)
}
envList.push(item);
}
})
}
return envList;
}

View File

@ -99,6 +99,8 @@ module.exports = {
*/
get: registry.getNodeConstructor,
registerSubflow: registry.registerSubflow,
/**
* Get a node's set information.
*

View File

@ -158,13 +158,10 @@ async function loadNodeTemplate(node) {
}
return node
}).catch(err => {
node.types = [];
if (err.code === 'ENOENT') {
if (!node.types) {
node.types = [];
}
node.err = "Error: "+node.template+" does not exist";
} else {
// ENOENT means no html file. We can live with that. But any other error
// should be fatal
// node.err = "Error: "+node.template+" does not exist";
if (err.code !== 'ENOENT') {
node.types = [];
node.err = err.toString();
}

View File

@ -20,6 +20,7 @@ var fs = require("fs");
var library = require("./library");
const {events} = require("@node-red/util")
var subflows = require("./subflow");
var settings;
var loader;
@ -27,6 +28,8 @@ var nodeConfigCache = {};
var moduleConfigs = {};
var nodeList = [];
var nodeConstructors = {};
var subflowModules = {};
var nodeTypeToId = {};
var moduleNodes = {};
@ -36,6 +39,7 @@ function init(_settings,_loader) {
moduleNodes = {};
nodeTypeToId = {};
nodeConstructors = {};
subflowModules = {};
nodeList = [];
nodeConfigCache = {};
}
@ -225,6 +229,7 @@ function removeNode(id) {
config.types.forEach(function(t) {
var typeId = nodeTypeToId[t];
if (typeId === id) {
delete subflowModules[t];
delete nodeConstructors[t];
delete nodeTypeToId[t];
}
@ -393,6 +398,27 @@ function registerNodeConstructor(nodeSet,type,constructor) {
events.emit("type-registered",type);
}
function registerSubflow(nodeSet, subflow) {
var nodeSetInfo = getFullNodeInfo(nodeSet);
const result = subflows.register(nodeSet,subflow);
if (subflowModules.hasOwnProperty(result.type)) {
throw new Error(result.type+" already registered");
}
if (nodeSetInfo) {
if (nodeSetInfo.types.indexOf(result.type) === -1) {
nodeSetInfo.types.push(result.type);
nodeTypeToId[result.type] = nodeSetInfo.id;
}
nodeSetInfo.config = result.config;
}
subflowModules[result.type] = result;
events.emit("type-registered",result.type);
return result;
}
function getAllNodeConfigs(lang) {
if (!nodeConfigCache[lang]) {
var result = "";
@ -447,7 +473,7 @@ function getNodeConstructor(type) {
}
if (!config || (config.enabled && !config.err)) {
return nodeConstructors[type];
return nodeConstructors[type] || subflowModules[type];
}
return null;
}
@ -457,6 +483,7 @@ function clear() {
moduleConfigs = {};
nodeList = [];
nodeConstructors = {};
subflowModules = {};
nodeTypeToId = {};
}
@ -613,6 +640,7 @@ var registry = module.exports = {
registerNodeConstructor: registerNodeConstructor,
getNodeConstructor: getNodeConstructor,
registerSubflow: registerSubflow,
addModule: addModule,

View File

@ -0,0 +1,117 @@
function getSubflowType(subflow) {
return "sf-"+subflow.id
}
function generateSubflowConfig(subflow) {
const subflowType = getSubflowType(subflow)
const label = subflow.name || subflowType;
const category = subflow.category || "function";
const color = subflow.color || "#C0DEED";
const inputCount = subflow.in?subflow.in.length:0;
const outputCount = subflow.out?subflow.out.length:0;
const icon = subflow.icon || "arrow-in.svg";
const defaults = {
name: {value: ""}
}
const credentials = {}
if (subflow.env) {
subflow.env.forEach(prop => {
var defaultValue;
switch(prop.type) {
case "cred": defaultValue = ""; break;
case "str": defaultValue = prop.value||""; break;
case "bool": defaultValue = (typeof prop.value === 'boolean')?prop.value:prop.value === "true" ; break;
case "num": defaultValue = (typeof prop.value === 'number')?prop.value:Number(prop.value); break;
default:
defaultValue = {
type: prop.type,
value: prop.value||""
}
}
defaults[prop.name] = {
value: defaultValue,
ui: prop.ui
}
if (prop.type === 'cred') {
defaults[prop.name].ui.type = "cred";
credentials[prop.name] = {type:"password"}
}
})
}
const defaultString = JSON.stringify(defaults);
const credentialsString = JSON.stringify(credentials);
return `<script type="text/javascript">
RED.nodes.registerType("${subflowType}",{
category: "${category}",
color: "${color}",
defaults: ${defaultString},
credentials: ${credentialsString},
inputs:${inputCount},
outputs:${outputCount},
icon: "${icon}",
paletteLabel: "${label}",
label: function() {
return this.name||"${label}";
},
labelStyle: function() {
return this.name?"node_label_italic":"";
},
oneditprepare: function() {
RED.subflow.buildEditForm('subflow', this);
},
oneditsave: function() {
var props = RED.subflow.exportSubflowInstanceEnv(this);
var i=0,l=props.length;
for (;i<l;i++) {
var prop = props[i];
if (this._def.defaults[prop.name].ui && this._def.defaults[prop.name].ui.type === "cred") {
this[prop.name] = "";
this.credentials[prop.name] = prop.value || "";
this.credentials['has_' + prop.name] = (this.credentials[prop.name] !== "");
} else {
switch(prop.type) {
case "str": this[prop.name] = prop.value||""; break;
case "bool": this[prop.name] = (typeof prop.value === 'boolean')?prop.value:prop.value === "true" ; break;
case "num": this[prop.name] = (typeof prop.value === 'number')?prop.value:Number(prop.value); break;
default:
this[prop.name] = {
type: prop.type,
value: prop.value||""
}
}
}
}
}
});
</script>
<script type="text/x-red" data-template-name="${subflowType}">
<div class="form-row">
<label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>
<input type="text" id="node-input-name" data-i18n="[placeholder]editor:common.label.name">
</div>
<div id="subflow-input-ui"></div>
</script>
`
}
function register(id,subflow) {
return {
subflow: subflow,
type: getSubflowType(subflow),
config: generateSubflowConfig(subflow)
}
}
module.exports = {
register: register
}

View File

@ -83,6 +83,9 @@ function createNodeApi(node) {
red.nodes.registerType = function(type,constructor,opts) {
runtime.nodes.registerType(node.id,type,constructor,opts);
}
red.nodes.registerSubflow = function(subflowDef) {
runtime.nodes.registerSubflow(node.id,subflowDef)
}
copyObjectProperties(log,red.log,null,["init"]);
copyObjectProperties(runtime.settings,red.settings,null,["init","load","reset"]);
if (runtime.adminApi) {

View File

@ -303,21 +303,13 @@ class Flow {
if (node) {
delete this.activeNodes[stopList[i]];
if (this.subflowInstanceNodes[stopList[i]]) {
try {
(function(subflow) {
promises.push(stopNode(node,false).then(() => subflow.stop()));
})(this.subflowInstanceNodes[stopList[i]]);
} catch(err) {
node.error(err);
}
delete this.subflowInstanceNodes[stopList[i]];
} else {
try {
var removed = removedMap[stopList[i]];
promises.push(stopNode(node,removed).catch(()=>{}));
} catch(err) {
node.error(err);
}
}
try {
var removed = removedMap[stopList[i]];
promises.push(stopNode(node,removed).catch(()=>{}));
} catch(err) {
node.error(err);
}
if (removedMap[stopList[i]]) {
events.emit("node-status",{

View File

@ -208,7 +208,12 @@ class Subflow extends Flow {
this.node = new Node(subflowInstanceConfig);
this.node.on("input", function(msg) { this.send(msg);});
this.node.on("close", function() { this.status({}); })
// Called when the subflow instance node is being stopped
this.node.on("close", function(done) {
this.status({});
// Stop the complete subflow
self.stop().finally(done)
})
this.node.status = status => this.parent.handleStatus(this.node,status);
// Create a context instance
// console.log("Node.context",this.type,"id:",this._alias||this.id,"z:",this.z)

View File

@ -19,6 +19,7 @@ var Log = require("@node-red/util").log;
var subflowInstanceRE = /^subflow:(.+)$/;
var typeRegistry = require("@node-red/registry");
var envVarExcludes = {};
function diffNodes(oldNode,newNode) {
@ -66,6 +67,194 @@ function mapEnvVarProperties(obj,prop,flow) {
}
}
function createNode(flow,config) {
var newNode = null;
var type = config.type;
try {
var nodeTypeConstructor = typeRegistry.get(type);
if (typeof nodeTypeConstructor === "function") {
var conf = clone(config);
delete conf.credentials;
for (var p in conf) {
if (conf.hasOwnProperty(p)) {
mapEnvVarProperties(conf,p,flow);
}
}
try {
Object.defineProperty(conf,'_flow', {value: flow, enumerable: false, writable: true })
newNode = new nodeTypeConstructor(conf);
} catch (err) {
Log.log({
level: Log.ERROR,
id:conf.id,
type: type,
msg: err
});
}
} else if (nodeTypeConstructor) {
// console.log(nodeTypeConstructor)
var subflowConfig = parseConfig([nodeTypeConstructor.subflow].concat(nodeTypeConstructor.subflow.flow));
var instanceConfig = clone(config);
instanceConfig.env = clone(nodeTypeConstructor.subflow.env);
instanceConfig.env = nodeTypeConstructor.subflow.env.map(nodeProp => {
var nodePropType;
var nodePropValue = config[nodeProp.name];
if (nodeProp.type === "cred") {
nodePropType = "cred";
} else {
switch(typeof config[nodeProp.name]) {
case "string": nodePropType = "str"; break;
case "number": nodePropType = "num"; break;
case "boolean": nodePropType = "bool"; nodePropValue = nodeProp?"true":"false"; break;
default:
nodePropType = config[nodeProp.name].type;
nodePropValue = config[nodeProp.name].value;
}
}
return {
name: nodeProp.name,
type: nodePropType,
value: nodePropValue
}
})
var subflow = require("./Subflow").create(
flow,
flow.global,
subflowConfig.subflows[nodeTypeConstructor.subflow.id],
instanceConfig
);
subflow.start();
return subflow.node;
Log.error(Log._("nodes.flow.unknown-type", {type:type}));
}
} catch(err) {
Log.error(err);
}
return newNode;
}
function parseConfig(config) {
var flow = {};
flow.allNodes = {};
flow.subflows = {};
flow.configs = {};
flow.flows = {};
flow.groups = {};
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;
}
});
// 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);
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;
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];
});
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;
}
module.exports = {
init: function(runtime) {
envVarExcludes = {};
@ -79,123 +268,7 @@ module.exports = {
diffNodes: diffNodes,
mapEnvVarProperties: mapEnvVarProperties,
parseConfig: function(config) {
var flow = {};
flow.allNodes = {};
flow.subflows = {};
flow.configs = {};
flow.flows = {};
flow.groups = {};
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;
}
});
// 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);
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;
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];
});
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;
},
parseConfig: parseConfig,
diffConfigs: function(oldConfig, newConfig) {
var id;
@ -475,36 +548,5 @@ module.exports = {
* @param {object} config The node configuration object
* @return {Node} The instance of the node
*/
createNode: function(flow,config) {
var newNode = null;
var type = config.type;
try {
var nodeTypeConstructor = typeRegistry.get(type);
if (nodeTypeConstructor) {
var conf = clone(config);
delete conf.credentials;
for (var p in conf) {
if (conf.hasOwnProperty(p)) {
mapEnvVarProperties(conf,p,flow);
}
}
try {
Object.defineProperty(conf,'_flow', {value: flow, enumerable: false, writable: true })
newNode = new nodeTypeConstructor(conf);
} catch (err) {
Log.log({
level: Log.ERROR,
id:conf.id,
type: type,
msg: err
});
}
} else {
Log.error(Log._("nodes.flow.unknown-type", {type:type}));
}
} catch(err) {
Log.error(err);
}
return newNode;
}
createNode: createNode
}

View File

@ -112,6 +112,25 @@ function createNode(node,def) {
}
}
function registerSubflow(nodeSet, subflow) {
// TODO: extract credentials definition from subflow properties
var registeredType = registry.registerSubflow(nodeSet,subflow);
if (subflow.env) {
var creds = {};
var hasCreds = false;
subflow.env.forEach(e => {
if (e.type === "cred") {
creds[e.name] = {type: "password"};
hasCreds = true;
}
})
if (hasCreds) {
credentials.register(registeredType.type,creds);
}
}
}
function init(runtime) {
settings = runtime.settings;
log = runtime.log;
@ -196,6 +215,7 @@ module.exports = {
// Node type registry
registerType: registerType,
registerSubflow: registerSubflow,
getType: registry.get,
getNodeInfo: registry.getNodeInfo,

View File

@ -414,60 +414,61 @@ describe("red/nodes/registry/loader",function() {
});
});
it("load core node files scanned by lfs - missing html file", function(done) {
stubs.push(sinon.stub(localfilesystem,"getNodeFiles", function(){
var result = {};
result["node-red"] = {
"name": "node-red",
"version": "1.2.3",
"nodes": {
"DuffNode": {
"file": path.join(resourcesDir,"DuffNode","DuffNode.js"),
"module": "node-red",
"name": "DuffNode"
}
}
};
return result;
}));
stubs.push(sinon.stub(registry,"saveNodeList", function(){ return }));
stubs.push(sinon.stub(registry,"addModule", function(){ return }));
// This module isn't already loaded
stubs.push(sinon.stub(registry,"getNodeInfo", function(){ return null; }));
stubs.push(sinon.stub(nodes,"registerType"));
loader.init({nodes:nodes,log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}});
loader.load().then(function(result) {
registry.addModule.called.should.be.true();
var module = registry.addModule.lastCall.args[0];
module.should.have.property("name","node-red");
module.should.have.property("version","1.2.3");
module.should.have.property("nodes");
module.nodes.should.have.property("DuffNode");
module.nodes.DuffNode.should.have.property("id","node-red/DuffNode");
module.nodes.DuffNode.should.have.property("module","node-red");
module.nodes.DuffNode.should.have.property("name","DuffNode");
module.nodes.DuffNode.should.have.property("file");
module.nodes.DuffNode.should.have.property("template");
module.nodes.DuffNode.should.have.property("enabled",true);
module.nodes.DuffNode.should.have.property("loaded",false);
module.nodes.DuffNode.should.have.property("types");
module.nodes.DuffNode.types.should.have.a.length(0);
module.nodes.DuffNode.should.have.property("config","");
module.nodes.DuffNode.should.have.property("help",{});
module.nodes.DuffNode.should.have.property("namespace","node-red");
module.nodes.DuffNode.should.have.property('err');
module.nodes.DuffNode.err.should.endWith("DuffNode.html does not exist");
nodes.registerType.called.should.be.false();
done();
}).catch(function(err) {
done(err);
});
});
// it("load core node files scanned by lfs - missing html file", function(done) {
// // This is now an okay situation
// stubs.push(sinon.stub(localfilesystem,"getNodeFiles", function(){
// var result = {};
// result["node-red"] = {
// "name": "node-red",
// "version": "1.2.3",
// "nodes": {
// "DuffNode": {
// "file": path.join(resourcesDir,"DuffNode","DuffNode.js"),
// "module": "node-red",
// "name": "DuffNode"
// }
// }
// };
// return result;
// }));
//
// stubs.push(sinon.stub(registry,"saveNodeList", function(){ return }));
// stubs.push(sinon.stub(registry,"addModule", function(){ return }));
// // This module isn't already loaded
// stubs.push(sinon.stub(registry,"getNodeInfo", function(){ return null; }));
//
// stubs.push(sinon.stub(nodes,"registerType"));
// loader.init({nodes:nodes,log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}});
// loader.load().then(function(result) {
//
// registry.addModule.called.should.be.true();
// var module = registry.addModule.lastCall.args[0];
// module.should.have.property("name","node-red");
// module.should.have.property("version","1.2.3");
// module.should.have.property("nodes");
// module.nodes.should.have.property("DuffNode");
// module.nodes.DuffNode.should.have.property("id","node-red/DuffNode");
// module.nodes.DuffNode.should.have.property("module","node-red");
// module.nodes.DuffNode.should.have.property("name","DuffNode");
// module.nodes.DuffNode.should.have.property("file");
// module.nodes.DuffNode.should.have.property("template");
// module.nodes.DuffNode.should.have.property("enabled",true);
// module.nodes.DuffNode.should.have.property("loaded",false);
// module.nodes.DuffNode.should.have.property("types");
// module.nodes.DuffNode.types.should.have.a.length(0);
// module.nodes.DuffNode.should.have.property("config","");
// module.nodes.DuffNode.should.have.property("help",{});
// module.nodes.DuffNode.should.have.property("namespace","node-red");
// module.nodes.DuffNode.should.have.property('err');
// module.nodes.DuffNode.err.should.endWith("DuffNode.html does not exist");
//
// nodes.registerType.called.should.be.false();
//
// done();
// }).catch(function(err) {
// done(err);
// });
// });
});
describe("#addModule",function() {

View File

@ -0,0 +1,3 @@
describe("red/nodes/registry/subflow",function() {
it.skip("NEEDS TESTS");
});