Merge branch 'master' into 0.16

This commit is contained in:
Nick O'Leary 2017-01-06 14:30:13 +00:00 committed by GitHub
commit e73216d4c1
27 changed files with 354 additions and 160 deletions

View File

@ -1,3 +1,38 @@
#### 0.15.3: Maintenance Release
- Tcpgetfix: Another small check (#1070)
- TCPGet: Ensure done() is called only once (#1068)
- Allow $ and _ at start of property identifiers Fixes #1063
- TCPGet: Separated the node.connected property for each instance (#1062)
- Corrected 'overide' typo in XML node help (#1061)
- TCPGet: Last property check (hopefully) (#1059)
- Add additional safety checks to avoid acting on non-existent objects (#1057)
- add --title for process name to command line options
- add indicator for fire once on inject node
- reimplement $(env var) replace to share common code.
- Fix error message for missing node html file, and add test.
- Let credentials also use $(...) substitutions from ENV
- Rename insecureRedirect to requireHttps
- Add setting to cause insecure redirect (#1054)
- Palette editor fixes (#1033)
- Close comms on stopServer in test helper (#1020)
- Tcpgetfix (#1050)
- TCPget: Store incoming messages alongside the client object to keep reference
- Merge remote-tracking branch 'upstream/master' into tcpgetfix
- TCPget can now handle concurrent sessions (#1042)
- Better scope handling
- Add security checks
- small change to udp httpadmin
- Fix comparison to "" in tcpin
- Change scope of clients object
- Works when connection is left open
- First release of multi connection tcpget
- Fix node.error() not printing when passed false (#1037)
- fix test for CSV array input
- different test for Pi (rather than use serial port name)
- Fix missing 0 handling for css node with array input
#### 0.15.2: Maintenance Release
- Revert bidi changes to nodes and hide menu option until fixed Fixes #1024

View File

@ -38,21 +38,13 @@ If you want to raise a pull-request with a new feature, or a refactoring
of existing code, it may well get rejected if you haven't discussed it on
the [mailing list](https://groups.google.com/forum/#!forum/node-red) first.
### Contributor License Agreement
All contributors need to sign the JS Foundation's Contributor License Agreement.
It is an online process and quick to do. You can read the details of the agreement
here: https://cla.js.foundation/node-red/node-red.
In order for us to accept pull-requests, the contributor must first complete
a Contributor License Agreement (CLA). This clarifies the intellectual
property license granted with any contribution. It is for your protection as a
Contributor as well as the protection of IBM and its customers; it does not
change your rights to use your own Contributions for any other purpose.
If you raise a pull-request without having signed the CLA, you will be prompted
to do so automatically.
You can download the CLAs here:
- [individual](http://nodered.org/cla/node-red-cla-individual.pdf)
- [corporate](http://nodered.org/cla/node-red-cla-corporate.pdf)
If you are an IBMer, please contact us directly as the contribution process is
slightly different.
### Coding standards

View File

@ -14,9 +14,6 @@
* limitations under the License.
**/
(function($) {
var allOptions = {
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression},
flow: {value:"flow",label:"flow.",validate:RED.utils.validatePropertyExpression},

View File

@ -255,7 +255,9 @@ RED.palette.editor = (function() {
nodeEntry.removeButton.hide();
} else {
nodeEntry.enableButton.removeClass('disabled');
if (moduleInfo.local) {
nodeEntry.removeButton.show();
}
if (activeTypeCount === 0) {
nodeEntry.enableButton.html(RED._('palette.editor.enableall'));
} else {

View File

@ -201,6 +201,11 @@
icon: "inject.png",
label: function() {
var suffix = "";
// if fire once then add small indication
if (this.once) {
suffix = " ¹";
}
// but replace with repeat one if set to repeat
if (this.repeat || this.crontab) {
suffix = " ↻";
}

View File

@ -23,9 +23,9 @@ module.exports = function(RED) {
var gpioCommand = __dirname+'/nrgpio';
try {
fs.statSync("/dev/ttyAMA0"); // unlikely if not on a Pi
var cpuinfo = fs.readFileSync("/proc/cpuinfo").toString();
if (cpuinfo.indexOf(": BCM") === -1) { throw "Info : "+RED._("rpi-gpio.errors.ignorenode"); }
} catch(err) {
//RED.log.info(RED._("rpi-gpio.errors.ignorenode"));
throw "Info : "+RED._("rpi-gpio.errors.ignorenode");
}

View File

@ -83,7 +83,7 @@ module.exports = function(RED) {
}
});
client.on('end', function() {
if (!node.stream || (node.datatype == "utf8" && node.newline != "" && buffer.length > 0)) {
if (!node.stream || (node.datatype == "utf8" && node.newline !== "" && buffer.length > 0)) {
var msg = {topic:node.topic, payload:buffer};
msg._session = {type:"tcp",id:id};
if (buffer.length !== 0) {
@ -407,6 +407,28 @@ module.exports = function(RED) {
} // jshint ignore:line
}
var node = this;
var clients = {};
this.on("input", function(msg) {
var i = 0;
if ((!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
msg.payload = msg.payload.toString();
}
var host = node.server || msg.host;
var port = node.port || msg.port;
// Store client information independently
// the clients object will have:
// clients[id].client, clients[id].msg, clients[id].timeout
var connection_id = host + ":" + port;
clients[connection_id] = clients[connection_id] || {};
clients[connection_id].msg = msg;
clients[connection_id].connected = clients[connection_id].connected || false;
if (!clients[connection_id].connected) {
var buf;
if (this.out == "count") {
if (this.splitc === 0) { buf = new Buffer(1); }
@ -414,136 +436,178 @@ module.exports = function(RED) {
}
else { buf = new Buffer(65536); } // set it to 64k... hopefully big enough for most TCP packets.... but only hopefully
this.connected = false;
var node = this;
var client;
var m;
this.on("input", function(msg) {
m = msg;
var i = 0;
if ((!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
msg.payload = msg.payload.toString();
}
if (!node.connected) {
client = net.Socket();
if (socketTimeout !== null) { client.setTimeout(socketTimeout); }
var host = node.server || msg.host;
var port = node.port || msg.port;
clients[connection_id].client = net.Socket();
if (socketTimeout !== null) { clients[connection_id].client.setTimeout(socketTimeout);}
if (host && port) {
client.connect(port, host, function() {
clients[connection_id].client.connect(port, host, function() {
//node.log(RED._("tcpin.errors.client-connected"));
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
node.connected = true;
client.write(msg.payload);
if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].connected = true;
clients[connection_id].client.write(clients[connection_id].msg.payload);
}
});
}
else {
node.warn(RED._("tcpin.errors.no-host"));
}
client.on('data', function(data) {
clients[connection_id].client.on('data', function(data) {
if (node.out == "sit") { // if we are staying connected just send the buffer
m.payload = data;
node.send(m);
if (clients[connection_id]) {
clients[connection_id].msg.payload = data;
node.send(clients[connection_id].msg);
}
}
else if (node.splitc === 0) {
msg.payload = data;
node.send(msg);
clients[connection_id].msg.payload = data;
node.send(clients[connection_id].msg);
}
else {
for (var j = 0; j < data.length; j++ ) {
if (node.out === "time") {
if (clients[connection_id]) {
// do the timer thing
if (node.tout) {
if (clients[connection_id].timeout) {
i += 1;
buf[i] = data[j];
}
else {
node.tout = setTimeout(function () {
node.tout = null;
msg.payload = new Buffer(i+1);
buf.copy(msg.payload,0,0,i+1);
node.send(msg);
if (client) { node.status({}); client.destroy(); }
clients[connection_id].timeout = setTimeout(function () {
if (clients[connection_id]) {
clients[connection_id].timeout = null;
clients[connection_id].msg.payload = new Buffer(i+1);
buf.copy(clients[connection_id].msg.payload,0,0,i+1);
node.send(clients[connection_id].msg);
if (clients[connection_id].client) {
node.status({}); clients[connection_id].client.destroy();
delete clients[connection_id];
}
}
}, node.splitc);
i = 0;
buf[0] = data[j];
}
}
}
// count bytes into a buffer...
else if (node.out == "count") {
buf[i] = data[j];
i += 1;
if ( i >= node.splitc) {
msg.payload = new Buffer(i);
buf.copy(msg.payload,0,0,i);
node.send(msg);
if (client) { node.status({}); client.destroy(); }
if (clients[connection_id]) {
clients[connection_id].msg.payload = new Buffer(i);
buf.copy(clients[connection_id].msg.payload,0,0,i);
node.send(clients[connection_id].msg);
if (clients[connection_id].client) {
node.status({}); clients[connection_id].client.destroy();
delete clients[connection_id];
}
i = 0;
}
}
}
// look for a char
else {
buf[i] = data[j];
i += 1;
if (data[j] == node.splitc) {
msg.payload = new Buffer(i);
buf.copy(msg.payload,0,0,i);
node.send(msg);
if (client) { node.status({}); client.destroy(); }
if (clients[connection_id]) {
clients[connection_id].msg.payload = new Buffer(i);
buf.copy(clients[connection_id].msg.payload,0,0,i);
node.send(clients[connection_id].msg);
if (clients[connection_id].client) {
node.status({}); clients[connection_id].client.destroy();
delete clients[connection_id];
}
i = 0;
}
}
}
}
}
});
client.on('end', function() {
clients[connection_id].client.on('end', function() {
//console.log("END");
node.connected = false;
node.status({fill:"grey",shape:"ring",text:"common.status.disconnected"});
client = null;
if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].connected = false;
clients[connection_id].client = null;
}
});
client.on('close', function() {
clients[connection_id].client.on('close', function() {
//console.log("CLOSE");
node.connected = false;
if (node.done) { node.done(); }
if (clients[connection_id]) {
clients[connection_id].connected = false;
}
var anyConnected = false;
for (var client in clients) {
if (clients[client].connected) {
anyConnected = true;
break;
}
}
if (node.done && !anyConnected) {
clients = {};
node.done();
}
});
client.on('error', function() {
clients[connection_id].client.on('error', function() {
//console.log("ERROR");
node.connected = false;
node.status({fill:"red",shape:"ring",text:"common.status.error"});
node.error(RED._("tcpin.errors.connect-fail"),msg);
if (client) { client.destroy(); }
node.error(RED._("tcpin.errors.connect-fail") + " " + connection_id, msg);
if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].connected = false;
clients[connection_id].client.destroy();
delete clients[connection_id];
}
});
client.on('timeout',function() {
clients[connection_id].client.on('timeout',function() {
//console.log("TIMEOUT");
node.connected = false;
clients[connection_id].connected = false;
node.status({fill:"grey",shape:"dot",text:"tcpin.errors.connect-timeout"});
//node.warn(RED._("tcpin.errors.connect-timeout"));
if (client) {
client.connect(port, host, function() {
node.connected = true;
if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].client.connect(port, host, function() {
clients[connection_id].connected = true;
node.status({fill:"green",shape:"dot",text:"common.status.connected"});
});
}
});
}
else { client.write(msg.payload); }
else {
if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].client.write(clients[connection_id].msg.payload);
}
}
});
this.on("close", function(done) {
node.done = done;
if (client) {
client.destroy();
for (var client in clients) {
clients[client].client.destroy();
}
node.status({});
if (!node.connected) { done(); }
var anyConnected = false;
for (var c in clients) {
if (clients[c].connected) {
anyConnected = true;
break;
}
}
if (!anyConnected) {
clients = {};
done();
}
});
}

View File

@ -102,7 +102,7 @@ module.exports = function(RED) {
try { server.bind(node.port,node.iface); }
catch(e) { } // Don't worry if already bound
}
RED.httpAdmin.get('/udp-ports/:id', RED.auth.needsPermission('udp-in.read'), function(req,res) {
RED.httpAdmin.get('/udp-ports/:id', RED.auth.needsPermission('udp-ports.read'), function(req,res) {
res.json(Object.keys(udpInputPortsInUse));
});
RED.nodes.registerType("udp in",UDPin);

View File

@ -36,7 +36,7 @@
<p>A function that parses the <code>msg.payload</code> to convert xml to/from a javascript object. Places the result in the payload.</p>
<p>If the input is a string it tries to parse it as XML and creates a javascript object.</p>
<p>If the input is a javascript object it tries to build an XML string.</p>
<p>You can also pass in a <code>msg.options</code> object to overide all the multitude of parameters. See
<p>You can also pass in a <code>msg.options</code> object to override all the multitude of parameters. See
<a href="https://github.com/Leonidas-from-XIV/node-xml2js/blob/master/README.md#options" target="_blank">the xml2js docs</a>
for more information.</p>
<p>If set, options in the edit dialogue override those passed in on the msg.options object.</p>

View File

@ -1,6 +1,6 @@
{
"name" : "node-red",
"version" : "0.15.2",
"version" : "0.15.3",
"description" : "A visual tool for wiring the Internet of Things",
"homepage" : "http://nodered.org",
"license" : "Apache-2.0",

32
red.js
View File

@ -33,17 +33,20 @@ var settingsFile;
var flowFile;
var knownOpts = {
"settings":[path],
"userDir":[path],
"help": Boolean,
"port": Number,
"v": Boolean,
"help": Boolean
"settings": [path],
"title": String,
"userDir": [path],
"verbose": Boolean
};
var shortHands = {
"s":["--settings"],
"u":["--userDir"],
"?":["--help"],
"p":["--port"],
"?":["--help"]
"s":["--settings"],
"t":["--help"],
"u":["--userDir"],
"v":["--verbose"]
};
nopt.invalidHandler = function(k,v,t) {
// TODO: console.log(k,v,t);
@ -54,14 +57,15 @@ var parsedArgs = nopt(knownOpts,shortHands,process.argv,2)
if (parsedArgs.help) {
console.log("Node-RED v"+RED.version());
console.log("Usage: node-red [-v] [-?] [--settings settings.js] [--userDir DIR]");
console.log(" [--port PORT] [flows.json]");
console.log(" [--port PORT] [--title TITLE] [flows.json]");
console.log("");
console.log("Options:");
console.log(" -s, --settings FILE use specified settings file");
console.log(" -u, --userDir DIR use specified user directory");
console.log(" -p, --port PORT port to listen on");
console.log(" -v enable verbose output");
console.log(" -?, --help show usage");
console.log(" -s, --settings FILE use specified settings file");
console.log(" --title TITLE process window title");
console.log(" -u, --userDir DIR use specified user directory");
console.log(" -v, --verbose enable verbose output");
console.log(" -?, --help show this help");
console.log("");
console.log("Documentation can be found at http://nodered.org");
process.exit();
@ -224,7 +228,6 @@ if (settings.httpNodeRoot !== false && settings.httpNodeAuth) {
if (settings.httpNodeRoot !== false) {
app.use(settings.httpNodeRoot,RED.httpNode);
}
if (settings.httpStatic) {
settings.httpStaticAuth = settings.httpStaticAuth || settings.httpAuth;
if (settings.httpStaticAuth) {
@ -265,7 +268,7 @@ RED.start().then(function() {
if (settings.httpAdminRoot === false) {
RED.log.info(RED.log._("server.admin-ui-disabled"));
}
process.title = 'node-red';
process.title = parsedArgs.title || 'node-red';
RED.log.info(RED.log._("server.now-running", {listenpath:getListenPath()}));
});
} else {
@ -280,7 +283,6 @@ RED.start().then(function() {
}
});
process.on('uncaughtException',function(err) {
util.log('[red] Uncaught Exception:');
if (err.stack) {

View File

@ -87,6 +87,16 @@ function init(_server,_runtime) {
if (!settings.disableEditor) {
ui.init(runtime);
var editorApp = express();
if (settings.requireHttps === true) {
editorApp.enable('trust proxy');
editorApp.use(function (req, res, next) {
if (req.secure) {
next();
} else {
res.redirect('https://' + req.headers.host + req.originalUrl);
}
});
}
editorApp.get("/",ensureRuntimeStarted,ui.ensureSlash,ui.editor);
editorApp.get("/icons/:icon",ui.icon);
theme.init(runtime);

View File

@ -236,7 +236,9 @@ Node.prototype.warn = function(msg) {
};
Node.prototype.error = function(logMessage,msg) {
if (typeof logMessage != 'boolean') {
logMessage = logMessage || "";
}
log_helper(this, Log.ERROR, logMessage);
/* istanbul ignore else */
if (msg) {

View File

@ -259,32 +259,6 @@ function Flow(global,flow) {
}
}
}
}
var EnvVarPropertyRE = /^\$\((\S+)\)$/;
function mapEnvVarProperties(obj,prop) {
if (Buffer.isBuffer(obj[prop])) {
return;
} else if (Array.isArray(obj[prop])) {
for (var i=0;i<obj[prop].length;i++) {
mapEnvVarProperties(obj[prop],i);
}
} else if (typeof obj[prop] === 'string') {
var m;
if ( (m = EnvVarPropertyRE.exec(obj[prop])) !== null) {
if (process.env.hasOwnProperty(m[1])) {
obj[prop] = process.env[m[1]];
}
}
} else {
for (var p in obj[prop]) {
if (obj[prop].hasOwnProperty(p)) {
mapEnvVarProperties(obj[prop],p);
}
}
}
}
function createNode(type,config) {
@ -295,7 +269,7 @@ function createNode(type,config) {
delete conf.credentials;
for (var p in conf) {
if (conf.hasOwnProperty(p)) {
mapEnvVarProperties(conf,p);
flowUtil.mapEnvVarProperties(conf,p);
}
}
try {

View File

@ -37,9 +37,35 @@ function diffNodes(oldNode,newNode) {
return false;
}
var EnvVarPropertyRE = /^\$\((\S+)\)$/;
function mapEnvVarProperties(obj,prop) {
if (Buffer.isBuffer(obj[prop])) {
return;
} else if (Array.isArray(obj[prop])) {
for (var i=0;i<obj[prop].length;i++) {
mapEnvVarProperties(obj[prop],i);
}
} else if (typeof obj[prop] === 'string') {
var m;
if ( (m = EnvVarPropertyRE.exec(obj[prop])) !== null) {
if (process.env.hasOwnProperty(m[1])) {
obj[prop] = process.env[m[1]];
}
}
} else {
for (var p in obj[prop]) {
if (obj[prop].hasOwnProperty(p)) {
mapEnvVarProperties(obj[prop],p);
}
}
}
}
module.exports = {
diffNodes: diffNodes,
mapEnvVarProperties: mapEnvVarProperties,
parseConfig: function(config) {
var flow = {};

View File

@ -21,6 +21,7 @@ var fs = require("fs");
var registry = require("./registry");
var credentials = require("./credentials");
var flows = require("./flows");
var flowUtil = require("./flows/util")
var context = require("./context");
var Node = require("./Node");
var log = require("../log");
@ -69,6 +70,12 @@ function createNode(node,def) {
var creds = credentials.get(id);
if (creds) {
//console.log("Attaching credentials to ",node.id);
// allow $(foo) syntax to substitute env variables for credentials also...
for (var p in creds) {
if (creds.hasOwnProperty(p)) {
flowUtil.mapEnvVarProperties(creds,p);
}
}
node.credentials = creds;
} else if (credentials.getDefinition(node.type)) {
node.credentials = {};
@ -146,7 +153,6 @@ module.exports = {
// disableFlow: flows.disableFlow,
// enableFlow: flows.enableFlow,
// Credentials
addCredentials: credentials.add,
getCredentials: credentials.get,

View File

@ -203,7 +203,7 @@ function loadNodeConfig(fileInfo) {
if (!node.types) {
node.types = [];
}
node.err = "Error: "+file+" does not exist";
node.err = "Error: "+node.template+" does not exist";
} else {
node.types = [];
node.err = err.toString();
@ -382,7 +382,6 @@ function getNodeHelp(node,lang) {
} else {
node.help[lang] = node.help[runtime.i18n.defaultLang];
}
}
return node.help[lang];
}

View File

@ -281,6 +281,7 @@ function getModuleFiles(module) {
}
nodeModuleFiles.forEach(function(node) {
nodeList[moduleFile.package.name].nodes[node.name] = node;
nodeList[moduleFile.package.name].nodes[node.name].local = moduleFile.local || false;
});
});
return nodeList;

View File

@ -165,7 +165,7 @@ function normalisePropertyExpression(str) {
throw new Error("Invalid property expression: unterminated expression");
}
// Next char is a-z
if (!/[a-z0-9]/i.test(str[i+1])) {
if (!/[a-z0-9\$\_]/i.test(str[i+1])) {
throw new Error("Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1));
}
start = i+1;

View File

@ -129,6 +129,10 @@ module.exports = {
// cert: fs.readFileSync('certificate.pem')
//},
// The following property can be used to cause insecure HTTP connections to
// be redirected to HTTPS.
//requireHttps: true
// The following property can be used to disable the editor. The admin API
// is not affected by this option. To disable both the editor and the admin
// API, use either the httpRoot or httpAdminRoot properties

View File

@ -227,12 +227,12 @@ describe('CSV node', function() {
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property('payload', '1,2,3,4\n');
msg.should.have.property('payload', '0,1,2,3,4\n');
done();
}
catch(e) { done(e); }
});
var testJson = [1,2,3,4];
var testJson = [0,1,2,3,4];
n1.emit("input", {payload:testJson});
});
});
@ -245,12 +245,12 @@ describe('CSV node', function() {
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property('payload', '1,2,3,4\n4,3,2,1\n');
msg.should.have.property('payload', '0,1,2,3,4\n4,3,2,1,0\n');
done();
}
catch(e) { done(e); }
});
var testJson = [[1,2,3,4],[4,3,2,1]];
var testJson = [[0,1,2,3,4],[4,3,2,1,0]];
n1.emit("input", {payload:testJson});
});
});

View File

@ -149,6 +149,9 @@ module.exports = {
stopServer: function(done) {
if (server) {
try {
server.on('close', function() {
comms.stop();
});
server.close(done);
} catch(e) {
done();

View File

@ -34,7 +34,20 @@ describe('flows/util', function() {
getType.restore();
});
describe('#mapEnvVarProperties',function() {
it('handles ENV substitutions in an object', function() {
process.env.foo1 = "bar1";
process.env.foo2 = "bar2";
process.env.foo3 = "bar3";
var foo = {a:"$(foo1)",b:"$(foo2)",c:{d:"$(foo3)"}};
for (var p in foo) {
if (foo.hasOwnProperty(p)) {
flowUtil.mapEnvVarProperties(foo,p);
}
}
foo.should.eql({ a: 'bar1', b: 'bar2', c: { d: 'bar3' } } );
});
});
describe('#diffNodes',function() {
it('handles a null old node', function() {

View File

@ -38,8 +38,9 @@ describe("red/nodes/index", function() {
index.clearRegistry();
});
process.env.foo="bar";
var testFlows = [{"type":"test","id":"tab1","label":"Sheet 1"}];
var testCredentials = {"tab1":{"b":1,"c":2}};
var testCredentials = {"tab1":{"b":1, "c":"2", "d":"$(foo)"}};
var storage = {
getFlows: function() {
return when({red:123,flows:testFlows,credentials:testCredentials});
@ -75,7 +76,8 @@ describe("red/nodes/index", function() {
index.loadFlows().then(function() {
var testnode = new TestNode({id:'tab1',type:'test',name:'barney'});
testnode.credentials.should.have.property('b',1);
testnode.credentials.should.have.property('c',2);
testnode.credentials.should.have.property('c',"2");
testnode.credentials.should.have.property('d',"bar");
done();
}).otherwise(function(err) {
done(err);

View File

@ -317,6 +317,53 @@ describe("red/nodes/registry/loader",function() {
done(err);
});
});
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",
"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,"addNodeSet", 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,i18n:{defaultLang:"en-US"},events:{on:function(){},removeListener:function(){}},log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}});
loader.load().then(function(result) {
registry.addNodeSet.called.should.be.true();
registry.addNodeSet.lastCall.args[0].should.eql("node-red/DuffNode");
registry.addNodeSet.lastCall.args[1].should.have.a.property('id',"node-red/DuffNode");
registry.addNodeSet.lastCall.args[1].should.have.a.property('module',"node-red");
registry.addNodeSet.lastCall.args[1].should.have.a.property('enabled',true);
registry.addNodeSet.lastCall.args[1].should.have.a.property('loaded',false);
registry.addNodeSet.lastCall.args[1].should.have.a.property('version',undefined);
registry.addNodeSet.lastCall.args[1].should.have.a.property('types');
registry.addNodeSet.lastCall.args[1].types.should.have.a.length(0);
registry.addNodeSet.lastCall.args[1].should.not.have.a.property('config');
registry.addNodeSet.lastCall.args[1].should.not.have.a.property('help');
registry.addNodeSet.lastCall.args[1].should.not.have.a.property('namespace','node-red');
registry.addNodeSet.lastCall.args[1].should.have.a.property('err');
registry.addNodeSet.lastCall.args[1].err.should.endWith("DuffNode.html does not exist");
nodes.registerType.calledOnce.should.be.false();
done();
}).otherwise(function(err) {
done(err);
});
});
});
describe("#addModule",function() {

View File

@ -0,0 +1,5 @@
// A test node that exports a function
module.exports = function(RED) {
function DuffNode(n) {}
RED.nodes.registerType("duff-node",DuffNode);
}

View File

@ -337,6 +337,11 @@ describe("red/util", function() {
it("pass 'a.b'.c",function() { testABC("'a.b'.c",['a.b','c']); })
it('pass a.$b.c',function() { testABC('a.$b.c',['a','$b','c']); })
it('pass a["$b"].c',function() { testABC('a["$b"].c',['a','$b','c']); })
it('pass a._b.c',function() { testABC('a._b.c',['a','_b','c']); })
it('pass a["_b"].c',function() { testABC('a["_b"].c',['a','_b','c']); })
it("fail a'b'.c",function() { testInvalid("a'b'.c"); })
it("fail a['b'.c",function() { testInvalid("a['b'.c"); })
it("fail a[]",function() { testInvalid("a[]"); })