From 6bccdd015f347e6c3423a50fb02ac549c5a7e531 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 18 Jul 2018 22:17:11 +0100 Subject: [PATCH 01/20] Update changelog --- CHANGELOG.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a73106c4..f9acf83e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,64 @@ +#### 0.19: Milestone Release + +Editor + + - Add Context data sidebar + - Index all node properties when searching Fixes #1446 + - Handle NaN and Infinity properly in debug sidebar Fixes #1778 #1779 + - Prevent horizontal scroll when palette name cannot wrap + - Ignore middle-click on node/ports to enable panning + - Better wire layout when looping back + - fix appearence of retry button of remote branch management dialog + - Handle releasing ctrl when using quick-add node dialog + - Add $env function to JSONata expressions + - Widen support for env var to use ${} or $() syntax + - Add env-var support to TypedInput + - Show unknown node properties in info tab + - Add node icon picker widget + - Only edit nodes on dbl click on primary button with no modifiers + - Allow subflows to be put in any palette category + - Add flow navigator widget + - Cache flow library result to improve response time Fixes #1753 + - Add middle-button-drag to pan the workspace + - allow multi-line category name in editor + - Redesign sidebar tabs + - Do not disable the export-clipboard menu option with empty selection + +Nodes + + - Change: Ensure runtime errors in Change node can be caught Fixes #1769 + - File: Add output to File Out node + - Function: add expandable JavaScript editor pane + - Function: allow id and name reference in function node code (#1731) + - HTTP Request: Move to request module + - HTTP: Ensure apiMaxLength applies to HTTP Nodes Fixes #1278 + - Join: accumulate top level properties + - Join: allow environment variable as reduce init value + - JSON: add JSON schema validation via msg.schema + - Pi: Let nrgpio code work with python 3 + - Pi: let Pi nodes be visible/editable on all platforms + - Switch: add isEmpty rule + - TCP: queue messages while connecting; closes #1414 + - TLS: Add servername option to TLS config node for SNI Fixes #1805 + - UDP: Don't accidentally re-use udp port when set to not do so + +Persistent Context + + - Add persistable context option + - Add default memory store + - Add file-based context store + - Add async mode to evaluateJSONataExpression + - Update RED.util.evaluateNodeProperty to support context stores + +Runtime + + - Support flow.disabled and .info in /flow API + - Node errors should be Strings not Errors Fixes #1781 + - Add detection of connection timeout in git communication Fixes #1770 + - Handle loading empty nodesDir + - Add 'private' property to userDir generated package.json + - Add RED.require to allow nodes to access other modules + #### 0.18.7: Maintenance Release Editor Fixes From 5847f92bef082e47759004930adde0dceae47335 Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Thu, 19 Jul 2018 11:06:57 +0900 Subject: [PATCH 02/20] fix test for template node for persistable context --- test/nodes/core/core/80-template_spec.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/nodes/core/core/80-template_spec.js b/test/nodes/core/core/80-template_spec.js index 1284b7dba..62db71179 100644 --- a/test/nodes/core/core/80-template_spec.js +++ b/test/nodes/core/core/80-template_spec.js @@ -36,6 +36,9 @@ describe('template node', function() { function initContext(done) { Context.init({ contextStorage: { + memory0: { // do not use (for excluding effect fallback) + module: "memory" + }, memory1: { module: "memory" }, @@ -337,7 +340,7 @@ describe('template node', function() { msg.should.have.property('topic', 'bar'); msg.should.have.property('payload', 'foo'); // result is in flow context - n2.context().flow.get("payload", "memory", function (err, val) { + n2.context().flow.get("payload", "memory1", function (err, val) { val.should.equal("payload=foo"); done(); }); @@ -375,7 +378,7 @@ describe('template node', function() { msg.should.have.property('topic', 'bar'); msg.should.have.property('payload', 'foo'); // result is in global context - n2.context().global.get("payload", "memory", function (err, val) { + n2.context().global.get("payload", "memory1", function (err, val) { val.should.equal("payload=foo"); done(); }); From ca3da262da7ce343c02d5a8b1bf7dcb4e72454d8 Mon Sep 17 00:00:00 2001 From: nakanishi Date: Thu, 19 Jul 2018 12:58:42 +0900 Subject: [PATCH 03/20] Add test cases for index.js of context --- test/red/runtime/nodes/context/index_spec.js | 113 ++++++++++++++++++- 1 file changed, 111 insertions(+), 2 deletions(-) diff --git a/test/red/runtime/nodes/context/index_spec.js b/test/red/runtime/nodes/context/index_spec.js index 5ec3ff5d7..46a973001 100644 --- a/test/red/runtime/nodes/context/index_spec.js +++ b/test/red/runtime/nodes/context/index_spec.js @@ -308,13 +308,21 @@ describe('context', function() { }); it('should fail when using invalid store name', function(done) { - Context.init({contextStorage:{'Invalid name':"noexist"}}); + Context.init({contextStorage:{'Invalid name':{module:testPlugin}}}); Context.load().then(function(){ done("An error was not thrown"); }).catch(function(){ done(); }); }); + it('should fail when using invalid sign character', function (done) { + Context.init({ contextStorage:{'abc-123':{module:testPlugin}}}); + Context.load().then(function () { + done("An error was not thrown"); + }).catch(function () { + done(); + }); + }); it('should fail when using invalid default context', function(done) { Context.init({contextStorage:{default:"noexist"}}); Context.load().then(function(){ @@ -339,6 +347,20 @@ describe('context', function() { done(); }); }); + it('should fail to load invalid module', function (done) { + Context.init({contextStorage: { + test: { + module: function (config) { + throw new Error("invalid plugin was loaded."); + } + } + }}); + Context.load().then(function () { + done("An error was not thrown"); + }).catch(function () { + done(); + }); + }); }); describe('close modules',function(){ @@ -419,7 +441,7 @@ describe('context', function() { Context.load().then(function(){ var context = Context.get("1","flow"); var cb = function(){done("An error occurred")} - context.set("foo","bar","defaultt",cb); + context.set("foo","bar","default",cb); context.get("foo","default",cb); context.keys("default",cb); stubGet.called.should.be.false(); @@ -763,6 +785,93 @@ describe('context', function() { }); }).catch(function(err){ done(err); }); }); + + it('should throw an error if callback of context.get is not a function', function (done) { + Context.init({ contextStorage: memoryStorage }); + Context.load().then(function () { + var context = Context.get("1", "flow"); + context.get("foo", "memory", "callback"); + done("should throw an error."); + }).catch(function () { + done(); + }); + }); + + it('should throw an error if callback of context.get is not specified', function (done) { + Context.init({ contextStorage: memoryStorage }); + Context.load().then(function () { + var context = Context.get("1", "flow"); + context.get("foo", "memory"); + done("should throw an error."); + }).catch(function () { + done(); + }); + }); + + it('should throw an error if callback of context.set is not a function', function (done) { + Context.init({ contextStorage: memoryStorage }); + Context.load().then(function () { + var context = Context.get("1", "flow"); + context.set("foo", "bar", "memory", "callback"); + done("should throw an error."); + }).catch(function () { + done(); + }); + }); + + it('should not throw an error if callback of context.set is not specified', function (done) { + Context.init({ contextStorage: memoryStorage }); + Context.load().then(function () { + var context = Context.get("1", "flow"); + context.set("foo", "bar", "memory"); + done(); + }).catch(done); + }); + + it('should throw an error if callback of context.keys is not a function', function (done) { + Context.init({ contextStorage: memoryStorage }); + Context.load().then(function () { + var context = Context.get("1", "flow"); + context.keys("memory", "callback"); + done("should throw an error."); + }).catch(function () { + done(); + }); + }); + + it('should throw an error if callback of context.keys is not specified', function (done) { + Context.init({ contextStorage: memoryStorage }); + Context.load().then(function () { + var context = Context.get("1", "flow"); + context.keys("memory"); + done("should throw an error."); + }).catch(function () { + done(); + }); + }); + + }); + + describe('listStores', function () { + it('should list context storages', function (done) { + Context.init({ contextStorage: contextDefaultStorage }); + Context.load().then(function () { + var list = Context.listStores(); + list.default.should.equal("default"); + list.stores.should.eql(["default", "test"]); + done(); + }).catch(done); + }); + + it('should list context storages without default storage', function (done) { + Context.init({ contextStorage: contextStorage }); + Context.load().then(function () { + var list = Context.listStores(); + list.default.should.equal("test"); + list.stores.should.eql(["test"]); + done(); + }).catch(done); + }); }); describe('delete context',function(){ From 2201c9062fe33ea3eded15d5cd63a6468a77ac08 Mon Sep 17 00:00:00 2001 From: Yuma Matsuura Date: Thu, 19 Jul 2018 13:17:41 +0900 Subject: [PATCH 04/20] Add UItest for http request --- .../nodes/core/core/20-inject_page.js | 9 +- .../nodes/core/core/58-debug_page.js | 19 ++ .../nodes/core/core/80-function_page.js | 35 +++ .../nodes/core/core/80-template_page.js | 55 ++++ .../nodes/core/io/21-httpin_page.js | 51 ++++ .../nodes/core/io/21-httprequest_page.js | 59 ++++ .../nodes/core/io/21-httpresponse_page.js | 27 ++ .../nodes/core/logic/15-change_page.js | 48 +++- .../nodes/core/parsers/70-HTML_page.js | 31 ++ .../pageobjects/nodes/nodefactory_page.js | 13 + .../pageobjects/workspace/palette_page.js | 6 + test/editor/specs/scenario/cookbook_uispec.js | 272 +++++++++++++++++- 12 files changed, 620 insertions(+), 5 deletions(-) create mode 100644 test/editor/pageobjects/nodes/core/core/80-function_page.js create mode 100644 test/editor/pageobjects/nodes/core/core/80-template_page.js create mode 100644 test/editor/pageobjects/nodes/core/io/21-httpin_page.js create mode 100644 test/editor/pageobjects/nodes/core/io/21-httprequest_page.js create mode 100644 test/editor/pageobjects/nodes/core/io/21-httpresponse_page.js create mode 100644 test/editor/pageobjects/nodes/core/parsers/70-HTML_page.js diff --git a/test/editor/pageobjects/nodes/core/core/20-inject_page.js b/test/editor/pageobjects/nodes/core/core/20-inject_page.js index 09248f268..3ba0b26d3 100644 --- a/test/editor/pageobjects/nodes/core/core/20-inject_page.js +++ b/test/editor/pageobjects/nodes/core/core/20-inject_page.js @@ -27,7 +27,7 @@ util.inherits(injectNode, nodePage); var payloadType = { "flow": 1, "global": 2, - "string": 3, + "str": 3, "num": 4, "bool": 5, "json": 6, @@ -43,6 +43,13 @@ var timeType = { "atASpecificTime": 4, }; +var timeType = { + "none": 1, + "interval": 2, + "intervalBetweenTimes": 3, + "atASpecificTime": 4, +}; + injectNode.prototype.setPayload = function(type, value) { // Open a payload type list. browser.clickWithWait('//*[contains(@class, "red-ui-typedInput-container")]'); diff --git a/test/editor/pageobjects/nodes/core/core/58-debug_page.js b/test/editor/pageobjects/nodes/core/core/58-debug_page.js index f87f29646..b04e4a228 100644 --- a/test/editor/pageobjects/nodes/core/core/58-debug_page.js +++ b/test/editor/pageobjects/nodes/core/core/58-debug_page.js @@ -24,4 +24,23 @@ function debugNode(id) { util.inherits(debugNode, nodePage); +var target = { + "msg": 1, + "full": 2 +}; + +debugNode.prototype.setTarget = function(type, value) { + // Open a payload type list. + browser.clickWithWait('//*[contains(@class, "red-ui-typedInput-container")]/button'); + // Select a payload type. + var xPath = '/html/body/div[11]/a[' + target[type] + ']'; + browser.clickWithWait(xPath); + if (value) { + browser.clickWithWait('//*[contains(@class, "red-ui-typedInput-input")]/input'); + browser.keys(['Control', 'a', 'Control']); + browser.keys(['Delete']); + browser.setValue('//*[contains(@class, "red-ui-typedInput-input")]/input', value); + } +} + module.exports = debugNode; diff --git a/test/editor/pageobjects/nodes/core/core/80-function_page.js b/test/editor/pageobjects/nodes/core/core/80-function_page.js new file mode 100644 index 000000000..1cd47f8b9 --- /dev/null +++ b/test/editor/pageobjects/nodes/core/core/80-function_page.js @@ -0,0 +1,35 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var util = require("util"); + +var nodePage = require("../../node_page"); + +function functionNode(id) { + nodePage.call(this, id); +} + +util.inherits(functionNode, nodePage); + +functionNode.prototype.setCode = function(value) { + browser.click('#node-input-func-editor'); + browser.keys(['Control', 'Home', 'Control']); + for (var i=0; iNode-RED'); + }); + + it('set the URL of a request using a template', function() { + var injectNode = workspace.addNode("inject"); + var changeNode = workspace.addNode("change", nodeWidth * 1.5); + var httpRequetNode = workspace.addNode("httpRequest", nodeWidth * 2.5); + var debugNode = workspace.addNode("debug", nodeWidth * 3.5); + + injectNode.edit(); + injectNode.setPayload("str", 'settings'); + injectNode.clickOk(); + + changeNode.edit(); + changeNode.ruleSet("query", "msg", "payload", "msg"); + changeNode.clickOk(); + + httpRequetNode.edit(); + httpRequetNode.setUrl(helper.url() + "/{{{query}}}"); + httpRequetNode.clickOk(); + + injectNode.connect(changeNode); + changeNode.connect(httpRequetNode); + httpRequetNode.connect(debugNode); + + workspace.deploy(); + + debugTab.open(); + debugTab.clearMessage(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.containEql('httpNodeRoot'); + }); + + it('set the query string parameters', function() { + var injectNode = workspace.addNode("inject"); + var changeNode = workspace.addNode("change", nodeWidth); + var httpRequetNode = workspace.addNode("httpRequest", nodeWidth * 2); + var debugNode = workspace.addNode("debug", nodeWidth * 3); + + injectNode.edit(); + injectNode.setPayload("str", 'Nick'); + injectNode.clickOk(); + + changeNode.edit(); + changeNode.ruleSet("query", "msg", "payload", "msg"); + changeNode.clickOk(); + + httpRequetNode.edit(); + httpRequetNode.setUrl(helper.url() + httpNodeRoot + '/set-query?q={{{query}}}'); + httpRequetNode.clickOk(); + + injectNode.connect(changeNode); + changeNode.connect(httpRequetNode); + httpRequetNode.connect(debugNode); + + // The code for confirmation starts from here. + var httpinNode = workspace.addNode("httpin", 0, nodeHeight); + var templateNode = workspace.addNode("template", nodeWidth, nodeHeight); + var httpResponseNode = workspace.addNode("httpResponse", nodeWidth * 2, nodeHeight); + + httpinNode.edit(); + httpinNode.setMethod("get"); + httpinNode.setUrl("/set-query"); + httpinNode.clickOk(); + + templateNode.edit(); + templateNode.setSyntax("mustache"); + templateNode.setFormat("handlebars"); + templateNode.setTemplate("Hello {{req.query.q}}"); + templateNode.clickOk(); + + httpinNode.connect(templateNode); + templateNode.connect(httpResponseNode); + // The code for confirmation ends here. + + workspace.deploy(); + debugTab.open(); + debugTab.clearMessage(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.eql('"Hello Nick"'); + }); + + it('get a parsed JSON response', function() { + var injectNode = workspace.addNode("inject"); + var changeNode_setPost = workspace.addNode("change", nodeWidth); + var httpRequetNode = workspace.addNode("httpRequest", nodeWidth * 2); + var debugNode = workspace.addNode("debug", nodeWidth * 3); + + injectNode.edit(); + injectNode.setPayload("str", "json-response"); + injectNode.clickOk(); + + changeNode_setPost.edit(); + changeNode_setPost.ruleSet("post", "msg", "payload", "msg"); + changeNode_setPost.clickOk(); + + httpRequetNode.edit(); + httpRequetNode.setMethod("get"); + var url = helper.url() + httpNodeRoot + "/{{post}}"; + httpRequetNode.setUrl(url); + httpRequetNode.setRet("obj"); + httpRequetNode.clickOk(); + + debugNode.edit(); + debugNode.setTarget("msg", "payload.title"); + debugNode.clickOk(); + + injectNode.connect(changeNode_setPost); + changeNode_setPost.connect(httpRequetNode); + httpRequetNode.connect(debugNode); + + // The code for confirmation starts from here. + var httpinNode = workspace.addNode("httpin", 0, nodeHeight); + var templateNode = workspace.addNode("template", nodeWidth * 1.5, nodeHeight); + var changeNode_setHeader = workspace.addNode("change", nodeWidth * 2.5, nodeHeight); + var httpResponseNode = workspace.addNode("httpResponse", nodeWidth * 3.5, nodeHeight); + + httpinNode.edit(); + httpinNode.setMethod("get"); + httpinNode.setUrl("/json-response"); + httpinNode.clickOk(); + + templateNode.edit(); + templateNode.setSyntax("mustache"); + templateNode.setFormat("handlebars"); + templateNode.setTemplate("{\"title\": \"Hello\"}"); + templateNode.clickOk(); + + changeNode_setHeader.edit(); + changeNode_setHeader.ruleSet("headers", "msg", "{\"content-type\":\"application/json\"}", "json"); + changeNode_setHeader.clickOk(); + + httpinNode.connect(templateNode); + templateNode.connect(changeNode_setHeader); + changeNode_setHeader.connect(httpResponseNode); + // The code for confirmation ends here. + + workspace.deploy(); + debugTab.open(); + debugTab.clearMessage(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.eql('"Hello"'); + }); + + it('get a binary response', function() { + var injectNode = workspace.addNode("inject"); + var httpRequetNode = workspace.addNode("httpRequest", nodeWidth); + var debugNode = workspace.addNode("debug", nodeWidth * 2); + + httpRequetNode.edit(); + httpRequetNode.setMethod("get"); + httpRequetNode.setUrl(helper.url() + "/settings"); + httpRequetNode.setRet("bin"); + httpRequetNode.clickOk(); + + injectNode.connect(httpRequetNode); + httpRequetNode.connect(debugNode); + + workspace.deploy(); + + debugTab.open(); + debugTab.clearMessage(); + injectNode.clickLeftButton(); + + debugTab.getMessage().should.eql(['123', '34', '104', '116', '116', '112', '78', '111', '100', '101']); + }); + + it('set a request header', function() { + var injectNode = workspace.addNode("inject"); + var functionNode = workspace.addNode("function", nodeWidth); + var httpRequetNode = workspace.addNode("httpRequest", nodeWidth * 2); + var debugNode = workspace.addNode("debug", nodeWidth * 3); + + functionNode.edit(); + functionNode.setCode("msg.payload = \"data to post\";"); + functionNode.clickOk(); + + httpRequetNode.edit(); + httpRequetNode.setMethod("post"); + var url = helper.url() + httpNodeRoot + "/set-header"; + httpRequetNode.setUrl(url); + httpRequetNode.clickOk(); + + injectNode.connect(functionNode); + functionNode.connect(httpRequetNode); + httpRequetNode.connect(debugNode); + + // The code for confirmation starts from here. + var httpinNode = workspace.addNode("httpin", 0, nodeHeight); + var templateNode = workspace.addNode("template", nodeWidth * 1.5, nodeHeight); + var httpResponseNode = workspace.addNode("httpResponse", nodeWidth * 2.5, nodeHeight); + + httpinNode.edit(); + httpinNode.setMethod("post"); + httpinNode.setUrl("/set-header"); + httpinNode.clickOk(); + + templateNode.edit(); + templateNode.setSyntax("mustache"); + templateNode.setFormat("handlebars"); + templateNode.setTemplate("{{ payload }}"); + templateNode.clickOk(); + + httpinNode.connect(templateNode); + templateNode.connect(httpResponseNode); + // The code for confirmation ends here. + + workspace.deploy(); + debugTab.open(); + debugTab.clearMessage(); + injectNode.clickLeftButton(); + debugTab.getMessage().should.eql('"data to post"'); + }); + }); }); From f6c7cb58048a8a2ea445c4dbe346961054f1d74e Mon Sep 17 00:00:00 2001 From: nakanishi Date: Thu, 19 Jul 2018 13:49:36 +0900 Subject: [PATCH 05/20] Add test cases for global context of memory context --- test/red/runtime/nodes/context/memory_spec.js | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/red/runtime/nodes/context/memory_spec.js b/test/red/runtime/nodes/context/memory_spec.js index 00582d986..95bb0674f 100644 --- a/test/red/runtime/nodes/context/memory_spec.js +++ b/test/red/runtime/nodes/context/memory_spec.js @@ -144,6 +144,32 @@ describe('memory',function() { keysY.should.have.length(1); keysY[0].should.equal("hoge"); }); + + it('should enumerate global context keys', function () { + var keys = context.keys("global"); + keys.should.be.an.Array(); + keys.should.be.empty(); + + context.set("global", "foo", "bar"); + keys = context.keys("global"); + keys.should.have.length(1); + keys[0].should.equal("foo"); + + context.set("global", "abc.def", "bar"); + keys = context.keys("global"); + keys.should.have.length(2); + keys[1].should.equal("abc"); + }); + + it('should not return specific keys as global context keys', function () { + var keys = context.keys("global"); + + context.set("global", "set", "bar"); + context.set("global", "get", "bar"); + context.set("global", "keys", "bar"); + keys = context.keys("global"); + keys.should.have.length(0); + }); }); describe('async',function() { @@ -212,6 +238,14 @@ describe('memory',function() { should.not.exist(context.get("nodeY","foo")); }); }); + it('should not clean global context', function () { + context.set("global", "foo", "abc"); + context.get("global", "foo").should.equal("abc"); + + return context.clean(["global"]).then(function () { + should.exist(context.get("global", "foo")); + }); + }); }); }); From e675512fa36b2abf10068fa2aded52e886a0051d Mon Sep 17 00:00:00 2001 From: HirokiUchikawa Date: Thu, 19 Jul 2018 14:44:21 +0900 Subject: [PATCH 06/20] Fix ReferenceError in change node and add a test case --- nodes/core/logic/15-change.js | 8 +++----- test/nodes/core/logic/15-change_spec.js | 27 +++++++++++++++++++++---- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/nodes/core/logic/15-change.js b/nodes/core/logic/15-change.js index 4a1ce33ec..2c1443846 100644 --- a/nodes/core/logic/15-change.js +++ b/nodes/core/logic/15-change.js @@ -143,7 +143,7 @@ module.exports = function(RED) { if (rule.fromt === "msg") { resolve(RED.util.getMessageProperty(msg,rule.from)); } else if (rule.fromt === 'flow' || rule.fromt === 'global') { - var contextKey = RED.util.parseContextStore(rule.from); + var contextKey = RED.util.parseContextStore(rule.from); node.context()[rule.fromt].get(contextKey.key, contextKey.store, (err,fromValue) => { if (err) { reject(err); @@ -166,12 +166,10 @@ module.exports = function(RED) { try { fromRE = new RegExp(fromRE, "g"); } catch (e) { - reject(new Error(RED._("change.errors.invalid-from",{error:e.message}))); - return; + return Promise.reject(new Error(RED._("change.errors.invalid-from",{error:e.message}))); } } else { - reject(new Error(RED._("change.errors.invalid-from",{error:"unsupported type: "+(typeof fromValue)}))); - return; + return Promise.reject(new Error(RED._("change.errors.invalid-from",{error:"unsupported type: "+(typeof fromValue)}))); } return { fromType, diff --git a/test/nodes/core/logic/15-change_spec.js b/test/nodes/core/logic/15-change_spec.js index bbba27dc3..47d93ff92 100644 --- a/test/nodes/core/logic/15-change_spec.js +++ b/test/nodes/core/logic/15-change_spec.js @@ -1176,6 +1176,25 @@ describe('change Node', function() { }); }); + it('reports invalid fromValue', function(done) { + var flow = [{"id":"changeNode1","type":"change",rules:[{"t":"change","p":"payload","from":"null","fromt":"msg","to":"abc","tot":"str"}],"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + setTimeout(function() { + var logEvents = helper.log().args.filter(function (evt) { + return evt[0].type == "change"; + }); + logEvents.should.have.length(1); + var msg = logEvents[0][0]; + msg.should.have.property('level', helper.log().ERROR); + msg.should.have.property('id', 'changeNode1'); + done(); + },25); + changeNode1.receive({payload:"",null:null}); + }); + }); + describe('env var', function() { before(function() { process.env.NR_TEST_A = 'foo'; @@ -1431,7 +1450,7 @@ describe('change Node', function() { }); }); }); - + it('applies multiple rules in order', function(done) { var flow = [{"id":"changeNode1","type":"change","wires":[["helperNode1"]], rules:[ @@ -1487,7 +1506,7 @@ describe('change Node', function() { }); }); }); - + it('can access two persistable global context property', function(done) { var flow = [{"id":"changeNode1", "z":"t1", "type":"change", "wires":[["helperNode1"]], @@ -1518,7 +1537,7 @@ describe('change Node', function() { }); }); }); - + it('can access persistable global & flow context property', function(done) { var flow = [{"id":"changeNode1", "z":"t1", "type":"change", "wires":[["helperNode1"]], @@ -1551,6 +1570,6 @@ describe('change Node', function() { }); }); }); - + }); }); From daf1388a6a95cd66bb40cb18de7d4fce1b944d85 Mon Sep 17 00:00:00 2001 From: HirokiUchikawa Date: Thu, 19 Jul 2018 19:32:53 +0900 Subject: [PATCH 07/20] Update Japanese info text of http request node --- nodes/core/locales/ja/io/21-httprequest.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nodes/core/locales/ja/io/21-httprequest.html b/nodes/core/locales/ja/io/21-httprequest.html index 7740632c1..662367343 100644 --- a/nodes/core/locales/ja/io/21-httprequest.html +++ b/nodes/core/locales/ja/io/21-httprequest.html @@ -30,7 +30,9 @@
payload
リクエストボディとして送るデータ
rejectUnauthorized
-
trueをセットすると、自己署名証明書を使用するhttpsサイトへのリクエストを許可します。
+
falseをセットすると、自己署名証明書を使用するhttpsサイトへのリクエストを許可します。
+
followRedirects
+
falseをセットすると、リダイレクトを行いません。デフォルトはtrueです。

出力

From bb106bfce726c765f46206bfd5ab54449288b59b Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Thu, 19 Jul 2018 20:13:27 +0900 Subject: [PATCH 08/20] small update of Japanese info text of exec node --- nodes/core/locales/ja/core/75-exec.html | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/nodes/core/locales/ja/core/75-exec.html b/nodes/core/locales/ja/core/75-exec.html index bc7d9b5de..7eb75b10a 100644 --- a/nodes/core/locales/ja/core/75-exec.html +++ b/nodes/core/locales/ja/core/75-exec.html @@ -26,7 +26,7 @@
kill 文字列
execノードのプロセスに対して送るシグナルの種別を指定します
pid 数値|文字列
-
シグナル送信対象のexecノードのプロセスID
+
シグナル送信対象のexecノードのプロセスIDを指定します

出力

@@ -60,13 +60,12 @@

詳細

デフォルトでは、execシステムコールを用いてコマンドを呼び出してその完了を待ち、出力を返します。例えば、コマンドの実行が成功した場合には、{ code: 0 }と言う返却値を返します。

-

spawnを使ってコマンドを実行し、 -標準出力および標準エラー出力へ出力を返すようにすることもできます。この場合、通常1行毎に値を返します。コマンドの実行が完了すると、3番目の端子にオブジェクトを出力します。例えば、コマンドの実行が成功した場合には、{ code: 0 }と言う返却値を返します。

+

spawnを使ってコマンドを実行し、標準出力および標準エラー出力へ出力を返すようにすることもできます。この場合、通常1行毎に値を返します。コマンドの実行が完了すると、3番目の端子にオブジェクトを出力します。例えば、コマンドの実行が成功した場合には、{ code: 0 }という返却値を返します。

エラー発生時には、3番目の端子のmsg.payloadmessagesignalなど付加情報を返します。

実行対象のコマンドはノード設定で定義します。msg.payloadや追加引数をコマンドに追加することもできます。

-

コマンドもしくはパラメータが空白を含む場合には、引用符で囲みます。- "This is a single parameter"

+

コマンドもしくはパラメータが空白を含む場合には、引用符で囲みます。- "これは一つのパラメータです"

返却するpayloadは通常文字列ですが、UTF8文字以外が存在するとバッファとなります。

-

ノードが実行中の場合、ステータスアイコンとPIDを表示します。この状態変化はstatusノードで検知できます。

+

ノードが実行中の場合、ステータスアイコンとPIDを表示します。この状態変化はStatusノードで検知できます。

プロセスの停止

msg.killを受信すると、実行中のプロセスを停止することができます。msg.killには送出するシグナルの種別を指定します。例えば、SIGINTSIGQUITSIGHUPなどです。空の文字列を指定した場合には、SIGTERMを指定したものとみなします。

ノードが1つ以上のプロセスを実行している場合、msg.pidに停止対象のPIDを指定しなければなりません。

From b9733e3dfa1b5b3d2d3f7a460547b0222811acfe Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Thu, 19 Jul 2018 21:20:02 +0900 Subject: [PATCH 09/20] add support of bin, data, and env to trigger node --- nodes/core/core/89-trigger.html | 4 +- test/nodes/core/core/89-trigger_spec.js | 103 ++++++++++++++++++++---- 2 files changed, 89 insertions(+), 18 deletions(-) diff --git a/nodes/core/core/89-trigger.html b/nodes/core/core/89-trigger.html index 57b8e4ba8..3bbdfe603 100644 --- a/nodes/core/core/89-trigger.html +++ b/nodes/core/core/89-trigger.html @@ -162,7 +162,7 @@ $("#node-input-op1").typedInput({ default: 'str', typeField: $("#node-input-op1type"), - types:['flow','global','str','num','bool','json', + types:['flow','global','str','num','bool','json','bin','date','env', optionPayload, optionNothing ] @@ -170,7 +170,7 @@ $("#node-input-op2").typedInput({ default: 'str', typeField: $("#node-input-op2type"), - types:['flow','global','str','num','bool','json', + types:['flow','global','str','num','bool','json','bin','date','env', optionOriginalPayload, optionLatestPayload, optionNothing diff --git a/test/nodes/core/core/89-trigger_spec.js b/test/nodes/core/core/89-trigger_spec.js index 08a1757ed..5055518fa 100644 --- a/test/nodes/core/core/89-trigger_spec.js +++ b/test/nodes/core/core/89-trigger_spec.js @@ -35,6 +35,9 @@ describe('trigger node', function() { }, memory1: { module: "memory" + }, + memory2: { + module: "memory" } } }); @@ -96,6 +99,74 @@ describe('trigger node', function() { }); }); + function basicTest(type, val, rval) { + it('should output 1st value when triggered ('+type+')', function(done) { + var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:val, op1type:type, op2:"", op2type:"null", duration:"20", wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; + process.env[val] = rval; + helper.load(triggerNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + if (rval) { + msg.should.have.property("payload"); + should.deepEqual(msg.payload, rval); + } + else { + msg.should.have.property("payload", val); + } + delete process.env[val]; + done(); + } + catch(err) { done(err); } + }); + n1.emit("input", {payload:null}); + }); + }); + + it('should output 2st value when triggered ('+type+')', function(done) { + var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", op1:"foo", op1type:"str", op2:val, op2type:type, duration:"20", wires:[["n2"]] }, + {id:"n2", type:"helper"} ]; + process.env[val] = rval; + helper.load(triggerNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var c = 0; + n2.on("input", function(msg) { + try { + if (c === 0) { + msg.should.have.property("payload", "foo"); + c++; + } + else { + if (rval) { + msg.should.have.property("payload"); + should.deepEqual(msg.payload, rval); + } + else { + msg.should.have.property("payload", val); + } + delete process.env[val]; + done(); + } + } + catch(err) { done(err); } + }); + n1.emit("input", {payload:null}); + }); + }); + } + + basicTest("num", 10); + basicTest("str", "10"); + basicTest("bool", true); + var val_json = '{ "x":"vx", "y":"vy", "z":"vz" }'; + basicTest("json", val_json, JSON.parse(val_json)); + var val_buf = "[1,2,3,4,5]"; + basicTest("bin", val_buf, Buffer.from(JSON.parse(val_buf))); + basicTest("env", "NR-TEST", "env-val"); + it('should output 1 then 0 when triggered (default)', function(done) { var flow = [{"id":"n1", "type":"trigger", "name":"triggerNode", duration:"20", wires:[["n2"]] }, {id:"n2", type:"helper"} ]; @@ -336,8 +407,8 @@ describe('trigger node', function() { }); it('should be able to return things from persistable flow and global context variables', function (done) { - var flow = [{"id": "n1", "type": "trigger", "name": "triggerNode", "op1": "#:(memory0)::foo", "op1type": "flow", - "op2": "#:(memory0)::bar", "op2type": "global", "duration": "20", "wires": [["n2"]], "z": "flow" }, + var flow = [{"id": "n1", "type": "trigger", "name": "triggerNode", "op1": "#:(memory1)::foo", "op1type": "flow", + "op2": "#:(memory1)::bar", "op2type": "global", "duration": "20", "wires": [["n2"]], "z": "flow" }, {"id": "n2", "type": "helper"}]; helper.load(triggerNode, flow, function () { initContext(function () { @@ -360,8 +431,8 @@ describe('trigger node', function() { var context = n1.context(); var flow = context.flow; var global = context.global; - flow.set("foo", "foo", "memory0", function (err) { - global.set("bar", "bar", "memory0", function (err) { + flow.set("foo", "foo", "memory1", function (err) { + global.set("bar", "bar", "memory1", function (err) { n1.emit("input", { payload: null }); }); }); @@ -372,8 +443,8 @@ describe('trigger node', function() { it('should be able to return things from multiple persistable global context variables', function (done) { var flow = [{"id": "n1", "z": "flow", "type": "trigger", "duration": "20", "wires": [["n2"]], - "op1": "#:(memory0)::val", "op1type": "global", - "op2": "#:(memory1)::val", "op2type": "global" + "op1": "#:(memory1)::val", "op1type": "global", + "op2": "#:(memory2)::val", "op2type": "global" }, {"id": "n2", "type": "helper"}]; helper.load(triggerNode, flow, function () { @@ -399,8 +470,8 @@ describe('trigger node', function() { } }); var global = n1.context().global; - global.set("val", "foo", "memory0", function (err) { - global.set("val", "bar", "memory1", function (err) { + global.set("val", "foo", "memory1", function (err) { + global.set("val", "bar", "memory2", function (err) { n1.emit("input", { payload: null }); }); }); @@ -411,8 +482,8 @@ describe('trigger node', function() { it('should be able to return things from multiple persistable flow context variables', function (done) { var flow = [{"id": "n1", "z": "flow", "type": "trigger", "duration": "20", "wires": [["n2"]], - "op1": "#:(memory0)::val", "op1type": "flow", - "op2": "#:(memory1)::val", "op2type": "flow" + "op1": "#:(memory1)::val", "op1type": "flow", + "op2": "#:(memory2)::val", "op2type": "flow" }, {"id": "n2", "type": "helper"}]; helper.load(triggerNode, flow, function () { @@ -438,8 +509,8 @@ describe('trigger node', function() { } }); var flow = n1.context().flow; - flow.set("val", "foo", "memory0", function (err) { - flow.set("val", "bar", "memory1", function (err) { + flow.set("val", "foo", "memory1", function (err) { + flow.set("val", "bar", "memory2", function (err) { n1.emit("input", { payload: null }); }); }); @@ -450,8 +521,8 @@ describe('trigger node', function() { it('should be able to return things from multiple persistable flow & global context variables', function (done) { var flow = [{"id": "n1", "z": "flow", "type": "trigger", "duration": "20", "wires": [["n2"]], - "op1": "#:(memory0)::val", "op1type": "flow", - "op2": "#:(memory1)::val", "op2type": "global" + "op1": "#:(memory1)::val", "op1type": "flow", + "op2": "#:(memory2)::val", "op2type": "global" }, {"id": "n2", "type": "helper"}]; helper.load(triggerNode, flow, function () { @@ -479,8 +550,8 @@ describe('trigger node', function() { var context = n1.context(); var flow = context.flow; var global = context.flow; - flow.set("val", "foo", "memory0", function (err) { - global.set("val", "bar", "memory1", function (err) { + flow.set("val", "foo", "memory1", function (err) { + global.set("val", "bar", "memory2", function (err) { n1.emit("input", { payload: null }); }); }); From 2acc31a4e7a93ec0c8f50ee0a8c07cd0462e0c0e Mon Sep 17 00:00:00 2001 From: HirokiUchikawa Date: Thu, 19 Jul 2018 21:35:00 +0900 Subject: [PATCH 10/20] Update Japanese info text of json node --- nodes/core/locales/ja/parsers/70-JSON.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nodes/core/locales/ja/parsers/70-JSON.html b/nodes/core/locales/ja/parsers/70-JSON.html index 2199d7f89..80595789f 100644 --- a/nodes/core/locales/ja/parsers/70-JSON.html +++ b/nodes/core/locales/ja/parsers/70-JSON.html @@ -20,6 +20,8 @@
payloadオブジェクト | 文字列
JavaScriptオブジェクトもしくはJSON文字列
+
schemaオブジェクト
+
JSONの検証に利用するJSONスキーマ。設定されていない場合は検証を行いません。

出力

@@ -30,9 +32,12 @@
  • 入力がJavaScriptオブジェクトの場合、JSON文字列に変換します。JSON文字列は整形することも可能です。
  • +
    schemaError配列
    +
    JSONの検証でエラーが発生した場合、Catchノードを利用し、エラーを配列としてschemaErrorプロパティから取得することができます。

    詳細

    デフォルトの変換対象はmsg.payloadですが、他のメッセージプロパティを変換対象とすることも可能です。

    双方向の変換を自動選択するのではなく、特定の変換のみ行うように設定できます。この機能は、例えば、HTTP Inノードに対するリクエストがcontent-typeを正しく設定していない場合であっても、JSONノードによる変換結果がJavaScriptオブジェクトであることを保証するために利用します。

    JSON文字列への変換が指定されている場合、受信した文字列に対してさらなるチェックは行いません。すなわち、文字列がJSONとして正しいかどうかの検査や、整形オプションを指定していたとしても整形処理を実施しません。

    +

    JSONスキーマの詳細については、こちらを参照してください。

    From 991c68c394f923b069183be698480e5cf8485ffb Mon Sep 17 00:00:00 2001 From: Kazuhito Yokoi Date: Tue, 17 Jul 2018 17:24:02 +0900 Subject: [PATCH 11/20] Update Japanese translation (editor.json) --- red/api/editor/locales/ja/editor.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/red/api/editor/locales/ja/editor.json b/red/api/editor/locales/ja/editor.json index 892d3e04c..081eb8f13 100644 --- a/red/api/editor/locales/ja/editor.json +++ b/red/api/editor/locales/ja/editor.json @@ -456,7 +456,13 @@ }, "context": { "name": "コンテキスト", - "label": "コンテキスト" + "label": "コンテキスト", + "none": "選択されていません", + "refresh": "読み込みのため更新してください", + "empty": "空", + "node": "なし", + "flow": "フロー", + "global": "グローバル" }, "palette": { "name": "パレットの管理", @@ -637,6 +643,9 @@ "eval": "表現評価エラー:\n __message__" } }, + "jsEditor": { + "title": "JavaScriptエディタ" + }, "jsonEditor": { "title": "JSONエディタ", "format": "JSONフォーマット" From ed20327c4190fd04ad515b4a11068ccdf36b627c Mon Sep 17 00:00:00 2001 From: HirokiUchikawa Date: Fri, 20 Jul 2018 10:13:58 +0900 Subject: [PATCH 12/20] Update Japanese text of editor.json --- red/api/editor/locales/ja/editor.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/red/api/editor/locales/ja/editor.json b/red/api/editor/locales/ja/editor.json index 081eb8f13..70a25caa4 100644 --- a/red/api/editor/locales/ja/editor.json +++ b/red/api/editor/locales/ja/editor.json @@ -455,14 +455,14 @@ "filtered": "__count__ 個が無効" }, "context": { - "name": "コンテキスト", - "label": "コンテキスト", + "name": "コンテキストデータ", + "label": "コンテキストデータ", "none": "選択されていません", "refresh": "読み込みのため更新してください", - "empty": "空", - "node": "なし", - "flow": "フロー", - "global": "グローバル" + "empty": "データが存在しません", + "node": "Node", + "flow": "Flow", + "global": "Global" }, "palette": { "name": "パレットの管理", From 39b751acf58de75e85609fbb7c3a7375cb93e6f8 Mon Sep 17 00:00:00 2001 From: nakanishi Date: Fri, 20 Jul 2018 11:23:37 +0900 Subject: [PATCH 13/20] Add test cases for localfilesystem context --- red/runtime/nodes/context/localfilesystem.js | 2 + .../nodes/context/localfilesystem_spec.js | 230 ++++++++++++++++++ 2 files changed, 232 insertions(+) diff --git a/red/runtime/nodes/context/localfilesystem.js b/red/runtime/nodes/context/localfilesystem.js index 551289a95..74e1f30c0 100644 --- a/red/runtime/nodes/context/localfilesystem.js +++ b/red/runtime/nodes/context/localfilesystem.js @@ -203,6 +203,8 @@ LocalFileSystem.prototype.set = function(scope, key, value, callback) { var storagePath = getStoragePath(this.storageBaseDir ,scope); fs.outputFile(storagePath + ".json", JSON.stringify(newContext, undefined, 4), "utf8").catch(function(err) { }); + } else if (callback && typeof callback !== 'function') { + throw new Error("Callback must be a function"); } else { this._set(scope,key,value,callback); } diff --git a/test/red/runtime/nodes/context/localfilesystem_spec.js b/test/red/runtime/nodes/context/localfilesystem_spec.js index c462c389f..267ef3dd9 100644 --- a/test/red/runtime/nodes/context/localfilesystem_spec.js +++ b/test/red/runtime/nodes/context/localfilesystem_spec.js @@ -63,6 +63,15 @@ describe('localfilesystem',function() { }); }); + it('should store local scope property', function (done) { + context.set("abc:def", "foo.bar", "test", function (err) { + context.get("abc:def", "foo", function (err, value) { + value.should.be.eql({ bar: "test" }); + done(); + }); + }); + }); + it('should delete property',function(done) { context.set("nodeX","foo.abc.bar1","test1",function(err){ context.set("nodeX","foo.abc.bar2","test2",function(err){ @@ -240,6 +249,81 @@ describe('localfilesystem',function() { }); }); }); + + it('should throw an error when getting a value with invalid key', function (done) { + context.set("nodeX","foo","bar",function(err) { + context.get("nodeX"," ",function(err,value) { + should.exist(err); + done(); + }); + }); + }); + + it('should throw an error when setting a value with invalid key',function (done) { + context.set("nodeX"," ","bar",function (err) { + should.exist(err); + done(); + }); + }); + + it('should throw an error when callback of get() is not a function',function (done) { + try { + context.get("nodeX","foo","callback"); + done("should throw an error."); + } catch (err) { + done(); + }; + }); + + it('should throw an error when callback of get() is not specified',function (done) { + try { + context.get("nodeX","foo"); + done("should throw an error."); + } catch (err) { + done(); + }; + }); + + it('should throw an error when callback of set() is not a function',function (done) { + try { + context.set("nodeX","foo","bar","callback"); + done("should throw an error."); + } catch (err) { + done(); + }; + }); + + it('should not throw an error when callback of set() is not specified', function (done) { + try { + context.set("nodeX"," ","bar"); + done(); + } catch (err) { + done("should not throw an error."); + }; + }); + + it('should handle empty context file', function (done) { + fs.outputFile(path.join(resourcesDir,"contexts","nodeX","flow.json"),"",function(){ + context.get("nodeX", "foo", function (err, value) { + should.not.exist(value); + context.set("nodeX", "foo", "test", function (err) { + context.get("nodeX", "foo", function (err, value) { + value.should.be.equal("test"); + done(); + }); + }); + }); + }); + }); + + it('should throw an error when reading corrupt context file', function (done) { + fs.outputFile(path.join(resourcesDir, "contexts", "nodeX", "flow.json"),"{abc",function(){ + context.get("nodeX", "foo", function (err, value) { + should.exist(err); + done(); + }); + }); + }); }); describe('#keys',function() { @@ -286,6 +370,24 @@ describe('localfilesystem',function() { }); }); }); + + it('should throw an error when callback of keys() is not a function', function (done) { + try { + context.keys("nodeX", "callback"); + done("should throw an error."); + } catch (err) { + done(); + }; + }); + + it('should throw an error when callback of keys() is not specified', function (done) { + try { + context.keys("nodeX"); + done("should throw an error."); + } catch (err) { + done(); + }; + }); }); describe('#delete',function() { @@ -496,4 +598,132 @@ describe('localfilesystem',function() { }); }); }); + + describe('Configuration', function () { + it('should change a base directory', function (done) { + var differentBaseContext = LocalFileSystem({ + base: "contexts2", + dir: resourcesDir, + cache: false + }); + differentBaseContext.open().then(function () { + differentBaseContext.set("node2", "foo2", "bar2", function (err) { + differentBaseContext.get("node2", "foo2", function (err, value) { + value.should.be.equal("bar2"); + context.get("node2", "foo2", function(err, value) { + should.not.exist(value); + done(); + }); + }); + }); + }); + }); + + it('should use userDir', function (done) { + var userDirContext = LocalFileSystem({ + base: "contexts2", + cache: false, + settings: { + userDir: resourcesDir + } + }); + userDirContext.open().then(function () { + userDirContext.set("node2", "foo2", "bar2", function (err) { + userDirContext.get("node2", "foo2", function (err, value) { + value.should.be.equal("bar2"); + context.get("node2", "foo2", function (err, value) { + should.not.exist(value); + done(); + }); + }); + }); + }); + }); + + it('should use NODE_RED_HOME', function (done) { + var oldNRH = process.env.NODE_RED_HOME; + process.env.NODE_RED_HOME = resourcesDir; + fs.mkdirSync(resourcesDir); + fs.writeFileSync(path.join(resourcesDir,".config.json"),""); + var nrHomeContext = LocalFileSystem({ + base: "contexts2", + cache: false + }); + try { + nrHomeContext.open().then(function () { + nrHomeContext.set("node2", "foo2", "bar2", function (err) { + nrHomeContext.get("node2", "foo2", function (err, value) { + value.should.be.equal("bar2"); + context.get("node2", "foo2", function (err, value) { + should.not.exist(value); + done(); + }); + }); + }); + }); + } finally { + process.env.NODE_RED_HOME = oldNRH; + } + }); + + it('should use HOME_PATH', function (done) { + var oldNRH = process.env.NODE_RED_HOME; + var oldHOMEPATH = process.env.HOMEPATH; + process.env.NODE_RED_HOME = resourcesDir; + process.env.HOMEPATH = resourcesDir; + var homePath = path.join(resourcesDir, ".node-red"); + fs.outputFile(path.join(homePath, ".config.json"),"",function(){ + var homeContext = LocalFileSystem({ + base: "contexts2", + cache: false + }); + try { + homeContext.open().then(function () { + homeContext.set("node2", "foo2", "bar2", function (err) { + homeContext.get("node2", "foo2", function (err, value) { + value.should.be.equal("bar2"); + context.get("node2", "foo2", function (err, value) { + should.not.exist(value); + done(); + }); + }); + }); + }); + } finally { + process.env.NODE_RED_HOME = oldNRH; + process.env.HOMEPATH = oldHOMEPATH; + } + }); + }); + + it('should use HOME_PATH', function (done) { + var oldNRH = process.env.NODE_RED_HOME; + var oldHOMEPATH = process.env.HOMEPATH; + var oldHOME = process.env.HOME; + process.env.NODE_RED_HOME = resourcesDir; + process.env.HOMEPATH = resourcesDir; + process.env.HOME = resourcesDir; + var homeContext = LocalFileSystem({ + base: "contexts2", + cache: false + }); + try { + homeContext.open().then(function () { + homeContext.set("node2", "foo2", "bar2", function (err) { + homeContext.get("node2", "foo2", function (err, value) { + value.should.be.equal("bar2"); + context.get("node2", "foo2", function (err, value) { + should.not.exist(value); + done(); + }); + }); + }); + }); + } finally { + process.env.NODE_RED_HOME = oldNRH; + process.env.HOMEPATH = oldHOMEPATH; + process.env.HOME = oldHOME; + } + }); + }); }); From 8f34f4e80bc38adbf0f7b9015b4cfa37622235e2 Mon Sep 17 00:00:00 2001 From: nakanishi Date: Fri, 20 Jul 2018 13:35:24 +0900 Subject: [PATCH 14/20] Update Japanese translation for switch and batch nodes --- nodes/core/locales/ja/logic/10-switch.html | 6 +++++- nodes/core/locales/ja/logic/19-batch.html | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/nodes/core/locales/ja/logic/10-switch.html b/nodes/core/locales/ja/logic/10-switch.html index bb0e20ac8..2417ad164 100644 --- a/nodes/core/locales/ja/logic/10-switch.html +++ b/nodes/core/locales/ja/logic/10-switch.html @@ -29,7 +29,11 @@
  • その他 - これより前のルールにマッチするものがなかった場合に適用
  • +

    注釈

    +

    is true/falseis nullのルールは、型に対して厳密な比較を行います。型変換した上での比較はしません。

    +

    is emptyのルールは、長さ0の文字列・配列・バッファ、またはプロパティを持たないオブジェクトを出力します。nullundefinedは出力しません。

    +

    メッセージ列の扱い

    switchノードは入力メッセージの列に関する情報を保持するmsg.partsをデフォルトでは変更しません。

    -

    メッセージ列の補正」オプションを指定すると、マッチした各ルールに対して新しいメッセージ列を生成します。このモードでは、switchノードは新たなメッセージ列を送信する前に、入力メッセージ列全体を内部に蓄積します。nodeMessageBufferMaxLengthを設定すると、蓄積するメッセージ数を制限できます。

    +

    メッセージ列の補正」オプションを指定すると、マッチした各ルールに対して新しいメッセージ列を生成します。このモードでは、switchノードは新たなメッセージ列を送信する前に、入力メッセージ列全体を内部に蓄積します。settings.jsnodeMessageBufferMaxLengthを設定すると、蓄積するメッセージ数を制限できます。

    diff --git a/nodes/core/locales/ja/logic/19-batch.html b/nodes/core/locales/ja/logic/19-batch.html index 6109866ff..41cef364c 100644 --- a/nodes/core/locales/ja/logic/19-batch.html +++ b/nodes/core/locales/ja/logic/19-batch.html @@ -30,5 +30,5 @@

    メッセージの蓄積

    -

    このノードの処理ではメッセージ列の処理のためメッセージを内部に蓄積します。nodeMessageBufferMaxLengthを指定することで蓄積するメッセージの最大値を制限することができます。

    +

    このノードの処理ではメッセージ列の処理のためメッセージを内部に蓄積します。settings.jsnodeMessageBufferMaxLengthを指定することで蓄積するメッセージの最大値を制限することができます。

    From d432edaed2dd2a41f60f55a8f40e0e9969aa0393 Mon Sep 17 00:00:00 2001 From: nakanishi Date: Fri, 20 Jul 2018 13:36:59 +0900 Subject: [PATCH 15/20] Translate icon palette parts into Japanese --- editor/js/ui/editor.js | 4 ++-- red/api/editor/locales/en-US/editor.json | 2 ++ red/api/editor/locales/ja/editor.json | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/editor/js/ui/editor.js b/editor/js/ui/editor.js index b1b688b24..16b6b65f5 100644 --- a/editor/js/ui/editor.js +++ b/editor/js/ui/editor.js @@ -706,7 +706,7 @@ RED.editor = (function() { pickerBackground.on("mousedown", hide); var searchDiv = $("
    ",{class:"red-ui-search-container"}).appendTo(picker); - searchInput = $('').attr("placeholder","Search icons").appendTo(searchDiv).searchBox({ + searchInput = $('').attr("placeholder",RED._("editor.searchIcons")).appendTo(searchDiv).searchBox({ delay: 50, change: function() { var searchTerm = $(this).val().trim(); @@ -730,7 +730,7 @@ RED.editor = (function() { var iconList = $('
    ').appendTo(picker); var metaRow = $('
    ').appendTo(picker); var summary = $('').appendTo(metaRow); - var resetButton = $('').appendTo(metaRow).click(function(e) { + var resetButton = $('').appendTo(metaRow).click(function(e) { e.preventDefault(); hide(); done(null); diff --git a/red/api/editor/locales/en-US/editor.json b/red/api/editor/locales/en-US/editor.json index d0bc7bf1e..cfd88fd2e 100644 --- a/red/api/editor/locales/en-US/editor.json +++ b/red/api/editor/locales/en-US/editor.json @@ -265,6 +265,8 @@ "settingIcon": "Icon", "noDefaultLabel": "none", "defaultLabel": "use default label", + "searchIcons": "Search icons", + "useDefault": "use default", "errors": { "scopeChange": "Changing the scope will make it unavailable to nodes in other flows that use it" } diff --git a/red/api/editor/locales/ja/editor.json b/red/api/editor/locales/ja/editor.json index 892d3e04c..7ab8ea714 100644 --- a/red/api/editor/locales/ja/editor.json +++ b/red/api/editor/locales/ja/editor.json @@ -264,6 +264,8 @@ "settingIcon": "アイコン", "noDefaultLabel": "なし", "defaultLabel": "既定の名前を使用", + "searchIcons": "アイコンを検索", + "useDefault": "デフォルトを使用", "errors": { "scopeChange": "スコープの変更は、他のフローで使われているノードを無効にします" } From e066a154a16510b0f952600ab0e63ea4d2c5de34 Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Fri, 20 Jul 2018 14:59:52 +0900 Subject: [PATCH 16/20] update info text of file node (English & Japanese) --- nodes/core/locales/ja/storage/50-file.html | 2 ++ nodes/core/storage/50-file.html | 2 ++ 2 files changed, 4 insertions(+) diff --git a/nodes/core/locales/ja/storage/50-file.html b/nodes/core/locales/ja/storage/50-file.html index 2e9bb839b..8bd5cdbaf 100644 --- a/nodes/core/locales/ja/storage/50-file.html +++ b/nodes/core/locales/ja/storage/50-file.html @@ -21,6 +21,8 @@
    filename 文字列
    対象ファイル名をノードに設定していない場合、このプロパティでファイルを指定できます
    +

    出力

    +

    書き込みの完了時、入力メッセージを出力端子に送出します。

    詳細

    入力メッセージのペイロードをファイルの最後に追記します。改行(\n)を各データの最後に追加することもできます。

    msg.filenameを使う場合、書き込みを行う毎にファイルをクローズします。より良い性能を得るためにはファイル名をノードに設定してください。

    diff --git a/nodes/core/storage/50-file.html b/nodes/core/storage/50-file.html index eabf56a76..00a698cee 100644 --- a/nodes/core/storage/50-file.html +++ b/nodes/core/storage/50-file.html @@ -37,6 +37,8 @@
    filename string
    If not configured in the node, this optional property sets the name of the file to be updated.
    +

    Output

    +

    On completion of write, input message is sent to output port.

    Details

    Each message payload will be added to the end of the file, optionally appending a newline (\n) character between each one.

    From 5148c62d1c3f64d0972c0ce36257c12698372340 Mon Sep 17 00:00:00 2001 From: HirokiUchikawa Date: Fri, 20 Jul 2018 16:53:30 +0900 Subject: [PATCH 17/20] Fix appearance about `Empty` rules. --- nodes/core/logic/10-switch.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nodes/core/logic/10-switch.html b/nodes/core/logic/10-switch.html index de1acbae3..4b8c8405b 100644 --- a/nodes/core/logic/10-switch.html +++ b/nodes/core/logic/10-switch.html @@ -147,7 +147,7 @@ } if ((rule.t === 'btwn') || (rule.t === 'index')) { label += " "+getValueLabel(rule.vt,rule.v)+" & "+getValueLabel(rule.v2t,rule.v2); - } else if (rule.t !== 'true' && rule.t !== 'false' && rule.t !== 'null' && rule.t !== 'nnull' && rule.t !== 'else' ) { + } else if (rule.t !== 'true' && rule.t !== 'false' && rule.t !== 'null' && rule.t !== 'nnull' && rule.t !== 'empty' && rule.t !== 'nempty' && rule.t !== 'else' ) { label += " "+getValueLabel(rule.vt,rule.v); } return label; @@ -199,7 +199,7 @@ } else if (type === "istype") { typeField.typedInput("width",(newWidth-selectWidth-70)); } else { - if (type === "true" || type === "false" || type === "null" || type === "nnull" || type === "else") { + if (type === "true" || type === "false" || type === "null" || type === "nnull" || type === "empty" || type === "nempty" || type === "else") { // valueField.hide(); } else { valueField.typedInput("width",(newWidth-selectWidth-70)); @@ -295,7 +295,7 @@ numValueField.typedInput('hide'); typeValueField.typedInput('hide'); valueField.typedInput('hide'); - if (type === "true" || type === "false" || type === "null" || type === "nnull" || type === "else") { + if (type === "true" || type === "false" || type === "null" || type === "nnull" || type === "empty" || type === "nempty" || type === "else") { valueField.typedInput('hide'); typeValueField.typedInput('hide'); } @@ -396,7 +396,7 @@ var rule = $(this); var type = rule.find("select").val(); var r = {t:type}; - if (!(type === "true" || type === "false" || type === "null" || type === "nnull" || type === "else")) { + if (!(type === "true" || type === "false" || type === "null" || type === "nnull" || type === "empty" || type === "nempty" || type === "else")) { if ((type === "btwn") || (type === "index")) { r.v = rule.find(".node-input-rule-btwn-value").typedInput('value'); r.vt = rule.find(".node-input-rule-btwn-value").typedInput('type'); From a6a4620374691ae03de0ec2d79d6b027bc788cf6 Mon Sep 17 00:00:00 2001 From: HirokiUchikawa Date: Fri, 20 Jul 2018 17:03:23 +0900 Subject: [PATCH 18/20] Fix an error that occurs when evaluating `null` on `isEmpty` rule. --- nodes/core/logic/10-switch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nodes/core/logic/10-switch.js b/nodes/core/logic/10-switch.js index e7f5cc841..89593047e 100644 --- a/nodes/core/logic/10-switch.js +++ b/nodes/core/logic/10-switch.js @@ -34,7 +34,7 @@ module.exports = function(RED) { 'empty': function(a) { if (typeof a === 'string' || Array.isArray(a) || Buffer.isBuffer(a)) { return a.length === 0; - } else if (typeof a === 'object') { + } else if (typeof a === 'object' && a !== null) { return Object.keys(a).length === 0; } return false; @@ -42,7 +42,7 @@ module.exports = function(RED) { 'nempty': function(a) { if (typeof a === 'string' || Array.isArray(a) || Buffer.isBuffer(a)) { return a.length !== 0; - } else if (typeof a === 'object') { + } else if (typeof a === 'object' && a !== null) { return Object.keys(a).length !== 0; } return false; From c7f3b77aacb5759ec6716e46f94e10e5c5f811c4 Mon Sep 17 00:00:00 2001 From: HirokiUchikawa Date: Fri, 20 Jul 2018 17:04:49 +0900 Subject: [PATCH 19/20] Fix test cases of `empty` rule --- test/nodes/core/logic/10-switch_spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/nodes/core/logic/10-switch_spec.js b/test/nodes/core/logic/10-switch_spec.js index d53ad3f49..f0e884fce 100644 --- a/test/nodes/core/logic/10-switch_spec.js +++ b/test/nodes/core/logic/10-switch_spec.js @@ -379,7 +379,7 @@ describe('switch Node', function() { singularSwitchTest("empty", true, false, undefined, done); }); it('should check if payload is empty (0)', function(done) { - singularSwitchTest("empty", true, false, null, done); + singularSwitchTest("empty", true, false, 0, done); }); it('should check if payload is not empty (string)', function(done) { @@ -413,7 +413,7 @@ describe('switch Node', function() { singularSwitchTest("nempty", true, false, undefined, done); }); it('should check if payload is not empty (0)', function(done) { - singularSwitchTest("nempty", true, false, null, done); + singularSwitchTest("nempty", true, false, 0, done); }); From 054c7a76a4bfe0460e00bf5bb227850515cc41d8 Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Fri, 20 Jul 2018 18:28:49 +0900 Subject: [PATCH 20/20] update test for file node for new output port --- test/nodes/core/storage/50-file_spec.js | 223 ++++++++++++++---------- 1 file changed, 132 insertions(+), 91 deletions(-) diff --git a/test/nodes/core/storage/50-file_spec.js b/test/nodes/core/storage/50-file_spec.js index f6eb41d56..9f0c48761 100644 --- a/test/nodes/core/storage/50-file_spec.js +++ b/test/nodes/core/storage/50-file_spec.js @@ -53,75 +53,143 @@ describe('file Nodes', function() { }); it('should write to a file', function(done) { - var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":false, "overwriteFile":true}]; + var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":false, "overwriteFile":true, wires: [["helperNode1"]]}, + {id:"helperNode1", type:"helper"}]; helper.load(fileNode, flow, function() { var n1 = helper.getNode("fileNode1"); - n1.emit("input", {payload:"test"}); - setTimeout(function() { + var n2 = helper.getNode("helperNode1"); + n2.on("input", function(msg) { var f = fs.readFileSync(fileToTest); f.should.have.length(4); fs.unlinkSync(fileToTest); + msg.should.have.property("payload", "test"); done(); - },wait); + }); + n1.receive({payload:"test"}); }); }); it('should append to a file and add newline', function(done) { - var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":true, "overwriteFile":false}]; + var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":true, "overwriteFile":false, wires: [["helperNode1"]]}, + {id:"helperNode1", type:"helper"}]; try { fs.unlinkSync(fileToTest); - }catch(err) {} + } catch(err) { + } helper.load(fileNode, flow, function() { var n1 = helper.getNode("fileNode1"); - n1.emit("input", {payload:"test2"}); // string + var n2 = helper.getNode("helperNode1"); + var count = 0; + var data = ["test2", true, 999, [2]]; + + n2.on("input", function (msg) { + msg.should.have.property("payload", data[count]); + if (count === 3) { + var f = fs.readFileSync(fileToTest).toString(); + if (os.type() !== "Windows_NT") { + f.should.have.length(19); + f.should.equal("test2\ntrue\n999\n[2]\n"); + } + else { + f.should.have.length(23); + f.should.equal("test2\r\ntrue\r\n999\r\n[2]\r\n"); + } + done(); + } + count++; + }); + + n1.receive({payload:"test2"}); // string setTimeout(function() { - n1.emit("input", {payload:true}); // boolean + n1.receive({payload:true}); // boolean },30); setTimeout(function() { - n1.emit("input", {payload:999}); // number + n1.receive({payload:999}); // number },60); setTimeout(function() { - n1.emit("input", {payload:[2]}); // object (array) + n1.receive({payload:[2]}); // object (array) },90); - setTimeout(function() { - var f = fs.readFileSync(fileToTest).toString(); - if (os.type() !== "Windows_NT") { - f.should.have.length(19); - f.should.equal("test2\ntrue\n999\n[2]\n"); - } - else { - f.should.have.length(23); - f.should.equal("test2\r\ntrue\r\n999\r\n[2]\r\n"); - } - done(); - },wait); }); }); it('should append to a file after it has been deleted ', function(done) { - var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":false, "overwriteFile":false}]; + var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":false, "overwriteFile":false, wires: [["helperNode1"]]}, + {id:"helperNode1", type:"helper"}]; try { fs.unlinkSync(fileToTest); - } catch(err) {} + } catch(err) { + } helper.load(fileNode, flow, function() { var n1 = helper.getNode("fileNode1"); - // Send two messages to the file - n1.emit("input", {payload:"one"}); - n1.emit("input", {payload:"two"}); - setTimeout(function() { + var n2 = helper.getNode("helperNode1"); + var data = ["one", "two", "three", "four"]; + var count = 0; + + n2.on("input", function (msg) { + msg.should.have.property("payload", data[count]); try { - // Check they got appended as expected - var f = fs.readFileSync(fileToTest).toString(); - f.should.equal("onetwo"); + if (count === 1) { + // Check they got appended as expected + var f = fs.readFileSync(fileToTest).toString(); + f.should.equal("onetwo"); - // Delete the file - fs.unlinkSync(fileToTest); + // Delete the file + fs.unlinkSync(fileToTest); + setTimeout(function() { + // Send two more messages to the file + n1.receive({payload:"three"}); + n1.receive({payload:"four"}); + }, wait); + } + if (count === 3) { + var f = fs.readFileSync(fileToTest).toString(); + f.should.equal("threefour"); + fs.unlinkSync(fileToTest); + done(); + } + } catch(err) { + done(err); + } + count++; + }); + + // Send two messages to the file + n1.receive({payload:"one"}); + n1.receive({payload:"two"}); + }); + }); - // Send two more messages to the file - n1.emit("input", {payload:"three"}); - n1.emit("input", {payload:"four"}); + it('should append to a file after it has been recreated ', function(done) { + var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":false, "overwriteFile":false, wires: [["helperNode1"]]}, + {id:"helperNode1", type:"helper"}]; + try { + fs.unlinkSync(fileToTest); + } catch(err) { + } + helper.load(fileNode, flow, function() { + var n1 = helper.getNode("fileNode1"); + var n2 = helper.getNode("helperNode1"); + var data = ["one", "two", "three", "four"]; + var count = 0; + + n2.on("input", function (msg) { + try { + msg.should.have.property("payload", data[count]); + if (count == 1) { + // Check they got appended as expected + var f = fs.readFileSync(fileToTest).toString(); + f.should.equal("onetwo"); - setTimeout(function() { + if (os.type() === "Windows_NT") { + var dummyFile = path.join(resourcesDir,"50-file-test-dummy.txt"); + fs.rename(fileToTest, dummyFile, function() { + recreateTest(n1, dummyFile); + }); + } else { + recreateTest(n1, fileToTest); + } + } + if (count == 3) { // Check the file was updated try { var f = fs.readFileSync(fileToTest).toString(); @@ -131,42 +199,16 @@ describe('file Nodes', function() { } catch(err) { done(err); } - },wait); - } catch(err) { - done(err); - } - },wait); - }); - }); - - it('should append to a file after it has been recreated ', function(done) { - var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":false, "overwriteFile":false}]; - try { - fs.unlinkSync(fileToTest); - } catch(err) {} - helper.load(fileNode, flow, function() { - var n1 = helper.getNode("fileNode1"); - // Send two messages to the file - n1.emit("input", {payload:"one"}); - n1.emit("input", {payload:"two"}); - setTimeout(function() { - try { - // Check they got appended as expected - var f = fs.readFileSync(fileToTest).toString(); - f.should.equal("onetwo"); - - if (os.type() === "Windows_NT") { - var dummyFile = path.join(resourcesDir,"50-file-test-dummy.txt"); - fs.rename(fileToTest, dummyFile, function() { - recreateTest(n1, dummyFile); - }); - } else { - recreateTest(n1, fileToTest); } } catch(err) { done(err); } - },wait); + count++; + }); + + // Send two messages to the file + n1.receive({payload:"one"}); + n1.receive({payload:"two"}); }); function recreateTest(n1, fileToDelete) { @@ -177,30 +219,23 @@ describe('file Nodes', function() { fs.writeFileSync(fileToTest,""); // Send two more messages to the file - n1.emit("input", {payload:"three"}); - n1.emit("input", {payload:"four"}); - - setTimeout(function() { - // Check the file was updated - try { - var f = fs.readFileSync(fileToTest).toString(); - f.should.equal("threefour"); - fs.unlinkSync(fileToTest); - done(); - } catch(err) { - done(err); - } - },wait); + n1.receive({payload:"three"}); + n1.receive({payload:"four"}); } }); it('should use msg.filename if filename not set in node', function(done) { - var flow = [{id:"fileNode1", type:"file", name: "fileNode", "appendNewline":true, "overwriteFile":true}]; + var flow = [{id:"fileNode1", type:"file", name: "fileNode", "appendNewline":true, "overwriteFile":true, wires: [["helperNode1"]]}, + {id:"helperNode1", type:"helper"}]; helper.load(fileNode, flow, function() { var n1 = helper.getNode("fileNode1"); - n1.emit("input", {payload:"fine", filename:fileToTest}); - setTimeout(function() { + var n2 = helper.getNode("helperNode1"); + + n2.on("input", function (msg) { + msg.should.have.property("payload", "fine"); + msg.should.have.property("filename", fileToTest); + var f = fs.readFileSync(fileToTest).toString(); if (os.type() !== "Windows_NT") { f.should.have.length(5); @@ -211,16 +246,20 @@ describe('file Nodes', function() { f.should.equal("fine\r\n"); } done(); - },wait); + }); + + n1.receive({payload:"fine", filename:fileToTest}); }); }); it('should be able to delete the file', function(done) { - var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":false, "overwriteFile":"delete"}]; + var flow = [{id:"fileNode1", type:"file", name: "fileNode", "filename":fileToTest, "appendNewline":false, "overwriteFile":"delete", wires: [["helperNode1"]]}, + {id:"helperNode1", type:"helper"}]; helper.load(fileNode, flow, function() { var n1 = helper.getNode("fileNode1"); - n1.emit("input", {payload:"fine"}); - setTimeout(function() { + var n2 = helper.getNode("helperNode1"); + + n2.on("input", function (msg) { try { var f = fs.readFileSync(fileToTest).toString(); f.should.not.equal("fine"); @@ -230,7 +269,9 @@ describe('file Nodes', function() { e.code.should.equal("ENOENT"); done(); } - },wait); + }); + + n1.receive({payload:"fine"}); }); });