Merge remote-tracking branch 'upstream/0.19' into json-schema

This commit is contained in:
Nathanaël Lécaudé
2018-07-10 11:11:15 -04:00
31 changed files with 1607 additions and 766 deletions

View File

@@ -62,10 +62,11 @@ exports.config = {
//
browserName: 'chrome',
chromeOptions: {
// Runs tests without opening a broser.
args: ['--headless', '--disable-gpu', 'window-size=1920,1080'],
// Runs tests with opening a broser.
// args: ['--disable-gpu'],
args: process.env.NODE_RED_NON_HEADLESS
// Runs tests with opening a browser.
? ['--disable-gpu']
// Runs tests without opening a browser.
: ['--headless', '--disable-gpu', 'window-size=1920,1080']
},
}],
//

View File

@@ -288,7 +288,7 @@ describe('trigger node', function() {
it('should be able to return things from flow and global context variables', function(done) {
var spy = sinon.stub(RED.util, 'evaluateNodeProperty',
function(arg1, arg2, arg3, arg4) { return arg1; }
function(arg1, arg2, arg3, arg4, arg5) { if (arg5) { arg5(null, arg1) } else { return arg1; } }
);
var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:"foo", op1type:"flow", op2:"bar", op2type:"global", duration:"20", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
@@ -386,7 +386,7 @@ describe('trigger node', function() {
it('should be able to extend the delay', function(done) {
this.timeout(5000); // add extra time for flake
var spy = sinon.stub(RED.util, 'evaluateNodeProperty',
function(arg1, arg2, arg3, arg4) { return arg1; }
function(arg1, arg2, arg3, arg4, arg5) { if (arg5) { arg5(null, arg1) } else { return arg1; } }
);
var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", extend:"true", op1type:"flow", op1:"foo", op2:"bar", op2type:"global", duration:"100", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
@@ -428,12 +428,10 @@ describe('trigger node', function() {
n2.on("input", function(msg) {
try {
if (c === 0) {
console.log(c,Date.now() - ss,msg);
msg.should.have.a.property("payload", "Hello");
c += 1;
}
else {
console.log(c,Date.now() - ss,msg);
msg.should.have.a.property("payload", "World");
(Date.now() - ss).should.be.greaterThan(150);
done();

View File

@@ -460,7 +460,7 @@ describe('switch Node', function() {
} catch(err) {
done(err);
}
},100)
},500)
});
});
@@ -599,7 +599,7 @@ describe('switch Node', function() {
it('should take head of message sequence (w. context)', function(done) {
var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"head","v":"count",vt:"global"}],checkall:false,repair:true,outputs:1,wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
customFlowSequenceSwitchTest(flow, [0, 1, 2, 3, 4], [0, 1, 2], true,
customFlowSequenceSwitchTest(flow, [0, 1, 2, 3, 4], [0, 1, 2], true,
function(node) {
node.context().global.set("count", 3);
}, done);
@@ -642,7 +642,7 @@ describe('switch Node', function() {
{id:"helperNode1", type:"helper", wires:[]}];
customFlowSwitchTest(flow, true, 9, done);
});
it('should be able to use $I in JSONata expression', function(done) {
var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"payload",rules:[{"t":"jsonata_exp","v":"$I % 2 = 1",vt:"jsonata"}],checkall:true,repair:true,outputs:1,wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
@@ -821,4 +821,24 @@ describe('switch Node', function() {
n1.receive({payload:1, parts:{index:0, count:4, id:222}});
});
});
it('should handle invalid jsonata expression', function(done) {
var flow = [{id:"switchNode1",type:"switch",name:"switchNode",property:"$invalidExpression(payload)",propertyType:"jsonata",rules:[{"t":"btwn","v":"$sqrt(16)","vt":"jsonata","v2":"$sqrt(36)","v2t":"jsonata"}],checkall:true,outputs:1,wires:[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
helper.load(switchNode, flow, function() {
var n1 = helper.getNode("switchNode1");
setTimeout(function() {
var logEvents = helper.log().args.filter(function (evt) {
return evt[0].type == "switch";
});
var evt = logEvents[0][0];
evt.should.have.property('id', "switchNode1");
evt.should.have.property('type', "switch");
done();
}, 150);
n1.receive({payload:1});
});
});
});

View File

@@ -15,6 +15,7 @@
**/
var should = require("should");
var sinon = require("sinon");
var changeNode = require("../../../../nodes/core/logic/15-change.js");
var helper = require("node-red-node-test-helper");
@@ -454,6 +455,28 @@ describe('change Node', function() {
});
});
it('reports invalid jsonata expression', function(done) {
var flow = [{"id":"changeNode1","type":"change",rules:[{"t":"set","p":"payload","to":"$invalid(payload)","tot":"jsonata"}],"name":"changeNode","wires":[["helperNode1"]]},
{id:"helperNode1", type:"helper", wires:[]}];
helper.load(changeNode, flow, function() {
var changeNode1 = helper.getNode("changeNode1");
var helperNode1 = helper.getNode("helperNode1");
sinon.spy(changeNode1,"error");
helperNode1.on("input", function(msg) {
done("Invalid jsonata expression passed message through");
});
changeNode1.receive({payload:"Hello World!"});
setTimeout(function() {
try {
changeNode1.error.called.should.be.true();
done();
} catch(err) {
done(err);
}
},50);
});
});
});
describe('#change', function() {
it('changes the value of the message property', function(done) {

View File

@@ -59,7 +59,8 @@ describe("api/editor/settings", function() {
},
nodes: {
paletteEditorEnabled: function() { return true; },
getCredentialKeyType: function() { return "test-key-type"}
getCredentialKeyType: function() { return "test-key-type"},
listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}}
},
log: { error: console.error },
storage: {}
@@ -73,6 +74,7 @@ describe("api/editor/settings", function() {
}
res.body.should.have.property("httpNodeRoot","testHttpNodeRoot");
res.body.should.have.property("version","testVersion");
res.body.should.have.property("context",{default: "foo", stores: ["foo","bar"]});
res.body.should.have.property("paletteCategories",["red","blue","green"]);
res.body.should.have.property("editorTheme",{test:456});
res.body.should.have.property("testNodeSetting","helloWorld");
@@ -95,7 +97,8 @@ describe("api/editor/settings", function() {
},
nodes: {
paletteEditorEnabled: function() { return true; },
getCredentialKeyType: function() { return "test-key-type"}
getCredentialKeyType: function() { return "test-key-type"},
listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}}
},
log: { error: console.error },
storage: {}
@@ -130,7 +133,8 @@ describe("api/editor/settings", function() {
},
nodes: {
paletteEditorEnabled: function() { return true; },
getCredentialKeyType: function() { return "test-key-type"}
getCredentialKeyType: function() { return "test-key-type"},
listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}}
},
log: { error: console.error },
storage: {
@@ -169,7 +173,8 @@ describe("api/editor/settings", function() {
},
nodes: {
paletteEditorEnabled: function() { return true; },
getCredentialKeyType: function() { return "test-key-type"}
getCredentialKeyType: function() { return "test-key-type"},
listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}}
},
log: { error: console.error },
storage: {
@@ -211,7 +216,8 @@ describe("api/editor/settings", function() {
},
nodes: {
paletteEditorEnabled: function() { return true; },
getCredentialKeyType: function() { return "test-key-type"}
getCredentialKeyType: function() { return "test-key-type"},
listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}}
},
log: { error: console.error },
storage: {
@@ -248,8 +254,8 @@ describe("api/editor/settings", function() {
},
nodes: {
paletteEditorEnabled: function() { return false; },
getCredentialKeyType: function() { return "test-key-type"}
getCredentialKeyType: function() { return "test-key-type"},
listContextStores: function() { return {default: "foo", stores: ["foo","bar"]}}
},
log: { error: console.error },
storage: {}

View File

@@ -23,7 +23,7 @@ describe('context', function() {
describe('local memory',function() {
beforeEach(function() {
Context.init({});
return Context.load();
Context.load();
});
afterEach(function() {
Context.clean({allNodes:{}});
@@ -124,7 +124,7 @@ describe('context', function() {
});
});
it('enumerates context keys', function() {
it('enumerates context keys - sync', function() {
var context = Context.get("1","flowA");
var keys = context.keys();
@@ -142,15 +142,63 @@ describe('context', function() {
keys[1].should.equal("abc");
});
it('should enumerate only context keys when GlobalContext was given', function() {
Context.init({functionGlobalContext: {foo:"bar"}});
return Context.load().then(function(){
var context = Context.get("1","flowA");
var keys = context.global.keys();
keys.should.have.length(1);
keys[0].should.equal("foo");
it('enumerates context keys - async', function(done) {
var context = Context.get("1","flowA");
var keys = context.keys(function(err,keys) {
keys.should.be.an.Array();
keys.should.be.empty();
context.set("foo","bar");
keys = context.keys(function(err,keys) {
keys.should.have.length(1);
keys[0].should.equal("foo");
context.set("abc.def","bar");
keys = context.keys(function(err,keys) {
keys.should.have.length(2);
keys[1].should.equal("abc");
done();
});
});
});
});
it('should enumerate only context keys when GlobalContext was given - sync', function() {
Context.init({functionGlobalContext: {foo:"bar"}});
Context.load().then(function(){
var context = Context.get("1","flowA");
context.global.set("foo2","bar2");
var keys = context.global.keys();
keys.should.have.length(2);
keys[0].should.equal("foo");
keys[1].should.equal("foo2");
});
});
it('should enumerate only context keys when GlobalContext was given - async', function(done) {
Context.init({functionGlobalContext: {foo:"bar"}});
Context.load().then(function(){
var context = Context.get("1","flowA");
context.global.set("foo2","bar2");
context.global.keys(function(err,keys) {
keys.should.have.length(2);
keys[0].should.equal("foo");
keys[1].should.equal("foo2");
done();
});
}).catch(done);
});
it('returns functionGlobalContext value if store value undefined', function() {
Context.init({functionGlobalContext: {foo:"bar"}});
Context.load().then(function(){
var context = Context.get("1","flowA");
var v = context.global.get('foo');
v.should.equal('bar');
});
})
});
describe('external context storage',function() {
@@ -215,6 +263,11 @@ describe('context', function() {
config:{}
}
};
var memoryStorage ={
memory:{
module: "memory"
}
};
afterEach(function() {
sandbox.reset();
@@ -226,18 +279,18 @@ describe('context', function() {
describe('load modules',function(){
it('should call open()', function() {
Context.init({contextStorage:contextDefaultStorage});
return Context.load().then(function(){
Context.load().then(function(){
stubOpen.called.should.be.true();
stubOpen2.called.should.be.true();
});
});
it('should load memory module', function() {
Context.init({contextStorage:{memory:{module:"memory"}}});
return Context.load();
Context.load();
});
it('should load localfilesystem module', function() {
Context.init({contextStorage:{file:{module:"localfilesystem",config:{dir:resourcesDir}}}});
return Context.load();
Context.load();
});
it('should accept special storage name', function(done) {
Context.init({
@@ -257,7 +310,7 @@ describe('context', function() {
stubSet.calledWithExactly("1:flow","file","file2",cb).should.be.true();
stubSet.calledWithExactly("1:flow","num","num3",cb).should.be.true();
done();
});
}).catch(done);
});
it('should ignore reserved storage name `_`', function(done) {
Context.init({contextStorage:{_:{module:testPlugin}}});
@@ -271,7 +324,7 @@ describe('context', function() {
stubGet.called.should.be.false();
stubKeys.called.should.be.false();
done();
});
}).catch(done);
});
it('should fail when using invalid default context', function(done) {
Context.init({contextStorage:{default:"noexist"}});
@@ -300,14 +353,15 @@ describe('context', function() {
});
describe('close modules',function(){
it('should call close()', function() {
it('should call close()', function(done) {
Context.init({contextStorage:contextDefaultStorage});
return Context.load().then(function(){
Context.load().then(function(){
return Context.close().then(function(){
stubClose.called.should.be.true();
stubClose2.called.should.be.true();
done();
});
});
}).catch(done);
});
});
@@ -324,7 +378,7 @@ describe('context', function() {
stubGet.calledWithExactly("1:flow","foo",cb).should.be.true();
stubKeys.calledWithExactly("1:flow",cb).should.be.true();
done();
});
}).catch(done);
});
it('should store flow property to external context storage',function(done) {
Context.init({contextStorage:contextStorage});
@@ -338,7 +392,7 @@ describe('context', function() {
stubGet.calledWithExactly("flow","foo",cb).should.be.true();
stubKeys.calledWithExactly("flow",cb).should.be.true();
done();
});
}).catch(done);
});
it('should store global property to external context storage',function(done) {
Context.init({contextStorage:contextStorage});
@@ -349,10 +403,10 @@ describe('context', function() {
context.global.get("foo","test",cb);
context.global.keys("test",cb);
stubSet.calledWithExactly("global","foo","bar",cb).should.be.true();
stubGet.calledWithExactly("global","foo",cb).should.be.true();
stubKeys.calledWithExactly("global",cb).should.be.true();
stubGet.calledWith("global","foo").should.be.true();
stubKeys.calledWith("global").should.be.true();
done();
});
}).catch(done);
});
it('should store data to the default context when non-existent context storage was specified', function(done) {
Context.init({contextStorage:contextDefaultStorage});
@@ -369,7 +423,7 @@ describe('context', function() {
stubGet2.calledWithExactly("1:flow","foo",cb).should.be.true();
stubKeys2.calledWithExactly("1:flow",cb).should.be.true();
done();
});
}).catch(done);
});
it('should use the default context', function(done) {
Context.init({contextStorage:contextDefaultStorage});
@@ -386,7 +440,7 @@ describe('context', function() {
stubGet2.calledWithExactly("1:flow","foo",cb).should.be.true();
stubKeys2.calledWithExactly("1:flow",cb).should.be.true();
done();
});
}).catch(done);
});
it('should use the alias of default context', function(done) {
Context.init({contextStorage:contextDefaultStorage});
@@ -403,7 +457,7 @@ describe('context', function() {
stubGet2.calledWithExactly("1:flow","foo",cb).should.be.true();
stubKeys2.calledWithExactly("1:flow",cb).should.be.true();
done();
});
}).catch(done);
});
it('should use default as the alias of other context', function(done) {
Context.init({contextStorage:contextAlias});
@@ -417,22 +471,16 @@ describe('context', function() {
stubGet.calledWithExactly("1:flow","foo",cb).should.be.true();
stubKeys.calledWithExactly("1:flow",cb).should.be.true();
done();
});
}).catch(done);
});
it('should throw an error using undefined storage for local context', function(done) {
it('should not throw an error using undefined storage for local context', function(done) {
Context.init({contextStorage:contextStorage});
Context.load().then(function(){
var context = Context.get("1","flow");
var cb = function(){done("An error occurred")}
context.get("local","nonexist",cb);
should.fail(null, null, "An error was not thrown using undefined storage for local context");
}).catch(function(err) {
if (err.name === "ContextError") {
done();
} else {
done(err);
}
});
done()
}).catch(done);
});
it('should throw an error using undefined storage for flow context', function(done) {
Context.init({contextStorage:contextStorage});
@@ -440,39 +488,89 @@ describe('context', function() {
var context = Context.get("1","flow");
var cb = function(){done("An error occurred")}
context.flow.get("flow","nonexist",cb);
should.fail(null, null, "An error was not thrown using undefined storage for flow context");
}).catch(function(err) {
if (err.name === "ContextError") {
done();
} else {
done(err);
}
});
done();
}).catch(done);
});
it('should return functionGlobalContext value as a default - synchronous', function(done) {
var fGC = { "foo": 456 };
Context.init({contextStorage:memoryStorage, functionGlobalContext:fGC });
Context.load().then(function() {
var context = Context.get("1","flow");
// Get foo - should be value from fGC
var v = context.global.get("foo");
v.should.equal(456);
// Update foo - should not touch fGC object
context.global.set("foo","new value");
fGC.foo.should.equal(456);
// Get foo - should be the updated value
v = context.global.get("foo");
v.should.equal("new value");
done();
}).catch(done);
})
it('should return functionGlobalContext value as a default - async', function(done) {
var fGC = { "foo": 456 };
Context.init({contextStorage:memoryStorage, functionGlobalContext:fGC });
Context.load().then(function() {
var context = Context.get("1","flow");
// Get foo - should be value from fGC
context.global.get("foo", function(err, v) {
if (err) {
done(err)
} else {
v.should.equal(456);
// Update foo - should not touch fGC object
context.global.set("foo","new value", function(err) {
if (err) {
done(err)
} else {
fGC.foo.should.equal(456);
// Get foo - should be the updated value
context.global.get("foo", function(err, v) {
if (err) {
done(err)
} else {
v.should.equal("new value");
done();
}
});
}
});
}
});
}).catch(done);
})
});
describe('delete context',function(){
it('should not call delete() when external context storage is used', function() {
it('should not call delete() when external context storage is used', function(done) {
Context.init({contextStorage:contextDefaultStorage});
return Context.load().then(function(){
Context.load().then(function(){
Context.get("flowA");
return Context.delete("flowA").then(function(){
stubDelete.called.should.be.false();
stubDelete2.called.should.be.false();
done();
});
});
}).catch(done);
});
});
describe('clean context',function(){
it('should call clean()', function() {
it('should call clean()', function(done) {
Context.init({contextStorage:contextDefaultStorage});
return Context.load().then(function(){
Context.load().then(function(){
return Context.clean({allNodes:{}}).then(function(){
stubClean.calledWithExactly([]).should.be.true();
stubClean2.calledWithExactly([]).should.be.true();
done();
});
});
}).catch(done);
});
});
});

View File

@@ -29,7 +29,7 @@ describe('localfilesystem',function() {
});
beforeEach(function() {
context = LocalFileSystem({dir: resourcesDir});
context = LocalFileSystem({dir: resourcesDir, cache: false});
return context.open();
});
@@ -308,7 +308,7 @@ describe('localfilesystem',function() {
done();
});
});
});
}).catch(done);
});
});
});
@@ -375,4 +375,4 @@ describe('localfilesystem',function() {
});
});
});
});
});

View File

@@ -103,19 +103,6 @@ describe('memory',function() {
keysY[0].should.equal("hoge");
});
it('should enumerate only context keys when GlobalContext was given', function() {
var keys = context.keys("global");
keys.should.be.an.Array();
keys.should.be.empty();
var data = {
foo: "bar"
}
context.setGlobalContext(data);
keys = context.keys("global");
keys.should.have.length(1);
keys[0].should.equal("foo");
});
});
describe('#delete',function() {
@@ -163,17 +150,4 @@ describe('memory',function() {
});
});
describe('#setGlobalContext',function() {
it('should initialize global context with argument', function() {
var keys = context.keys("global");
keys.should.be.an.Array();
keys.should.be.empty();
var data = {
foo: "bar"
}
context.setGlobalContext(data);
context.get("global","foo").should.equal("bar");
});
});
});
});

View File

@@ -307,6 +307,10 @@ describe("red/util", function() {
},{});
result.should.eql("123");
});
it('returns null', function() {
var result = util.evaluateNodeProperty(null,'null');
(result === null).should.be.true();
})
describe('environment variable', function() {
before(function() {
process.env.NR_TEST_A = "foo";
@@ -454,6 +458,30 @@ describe("red/util", function() {
var result = util.evaluateJSONataExpression(expr,{payload:"hello"});
should.not.exist(result);
});
it('handles async flow context access', function(done) {
var expr = util.prepareJSONataExpression('$flowContext("foo")',{context:function() { return {flow:{get: function(key,callback) { setTimeout(()=>{callback(null,{'foo':'bar'}[key])},10)}}}}});
util.evaluateJSONataExpression(expr,{payload:"hello"},function(err,value) {
try {
should.not.exist(err);
value.should.eql("bar");
done();
} catch(err2) {
done(err2);
}
});
})
it('handles async global context access', function(done) {
var expr = util.prepareJSONataExpression('$globalContext("foo")',{context:function() { return {global:{get: function(key,callback) { setTimeout(()=>{callback(null,{'foo':'bar'}[key])},10)}}}}});
util.evaluateJSONataExpression(expr,{payload:"hello"},function(err,value) {
try {
should.not.exist(err);
value.should.eql("bar");
done();
} catch(err2) {
done(err2);
}
});
})
});