Merge branch 'upstream-update'

This commit is contained in:
andrew.greene 2022-04-12 15:32:16 -06:00
commit 37b54ab9ae
30 changed files with 2718 additions and 7577 deletions

View File

@ -1,524 +0,0 @@
#### 2.2.2: Maintenance Release
Nodes
- Fix "close timed out" error when performing full deploy or modifying broker node. (#3451) @Steve-Mcl
#### 2.2.1: Maintenance Release
Editor
- Handle mixed-cased filter terms in keyboard shortcut dialog (#3444) @knolleary
- Prevent duplicate links being added between nodes (#3442) @knolleary
- Fix to hide tooltip after removing subflow tab (#3391) @HiroyasuNishiyama
- Fix node validation to be applied to config node (#3397) @HiroyasuNishiyama
- Fix: Dont add wires to undo buffer twice (#3437) @Steve-Mcl
Runtime
- Improve module location parsing (of stack info) when adding hook (#3447) @Steve-Mcl
- Fix substitution of NR_NODE_PATH (#3445) @HiroyasuNishiyama
- Remove console.log when ignoring disabled module (#3439) @knolleary
- Improve "Unexpected Node Error" logging (#3446) @Steve-Mcl
Nodes
- Debug: Fix no-prototype-builtins bug in debug node and utils (#3394) @Alkarex
- Delay: Fix Japanese message of delay node (#3434)
- Allow nbRateUnits to be undefined when validating (#3443) @knolleary
- Coding help for recently added node-red Predefined Environment Variables (#3440) @Steve-Mcl
#### 2.2.0: Milestone Release
Editor
- Add editorTheme.tours property to default settings file (#3375) @knolleary
- Remember Zoom level and Sidebar tab selection between sessions (#3361) @knolleary
- Fix timing issue when merging background changes fixes #3364 (#3373) @Steve-Mcl
- Use a nodes palette label in help tree (#3372) @Steve-Mcl
- Subflow: Add labels to OUTPUT nodes (#3352) @ralphwetzel
- Fix vertical align subflow port (#3370) @knolleary
- Make actions list i18n ready and Japanese translation (#3359) @HiroyasuNishiyama
- Update tour for 2.2.0 (#3378) @knolleary
- Include paletteLabel when building search index (#3380) @Steve-Mcl
- Fix opening/closing subflow template not to make subflow changed (#3382) @HiroyasuNishiyama
- Add Japanese translations for v2.2.0 (#3353, #3381) @kazuhitoyokoi
Runtime
- Add support for accessing node id & name as environment variable (#3356) @HiroyasuNishiyama
- Clear context contents when switching projects (#3243) @knolleary
Nodes
- MQTT: reject invalid topics (#3374) @Steve-Mcl
- Function: Expose node.path property (#3371) @knolleary
- Function: Update `node` declarations in func.d.ts (#3377) @Steve-Mcl
#### 2.2.0-beta.1: Beta Release
Editor
- Add search history to main search box (#3262) @knolleary
- Check availability of type of config node on deploy (#3304) @k-toumura
- Add wire-slice mode to delete wires with Ctrl-RHClick-Drag (#3340) @knolleary
- Wiring keyboard shortcuts (#3288) @knolleary
- Snap nodes on grid using either edge as reference (#3289) @knolleary
- Detach node action (#3338) @knolleary
- Highlight links when selecting nodes (#3323) @knolleary
- Allow multiple links to be selected by ctrl-click (#3294) @knolleary
Nodes
- JSON: Let JSON node attempt to parse buffer if it contains a valid string (#3296) @dceejay
- Remove use of verbose flag in core nodes - and use node.debug level instead (#3300) @dceejay
- TCP: Add TLS option to tcp client nodes (#3307) @dceejay
- WebSocket: Implemented support for Websocket Subprotocols in WS Client Node. (#3333) @tobiasoort
#### 2.1.6: Maintenance Release
Editor
- Revert copy-text change and apply alternative fix (#3363) @knolleary
- Update marked to latest (#3362) @knolleary
- fix to make start of property error tooltip messages aligned (#3358) @HiroyasuNishiyama
Nodes
- Inject: fix JSON propety validation of inject node (#3349) @HiroyasuNishiyama
- Delay: fix unit value validation of delay node (#3351) @HiroyasuNishiyama
#### 2.1.5: Maintenance Release
Runtime
- Handle reporting error location when stack is truncated (#3346) @knolleary
- Initialize passport when only adminAuth.tokens is set (#3343) @knolleary
- Add log logging (#3342) @knolleary
Editor
- Fix copy buttons on the debug window (another method) (#3331) @kazuhitoyokoi
- Add Japanese translations for hidden flow (#3302) @kazuhitoyokoi
- Improve jsonata legacy mode detection regex (#3345) @knolleary
- Fix generating flow name with incrementing number (#3347) @knolleary
- resume focus after import/export dialog close (#3337) @HiroyasuNishiyama
- Fix findPreviousVisibleTab action (#3321) @knolleary
- Fix storing hidden tab state when not hidden via action (#3312) @knolleary
- Avoid adding empty env properties to tabs/groups (#3311) @knolleary
- Fix hide icon in tour guide (#3301) @kazuhitoyokoi
Nodes
- File: Update file node examples according to node name change (#3335) @HiroyasuNishiyama
- Filter (RBE): Fix for filter node narrrowbandEq mode start condition failure (#3339) @dceejay
- Function: Prevent function scrollbar from obscuring expand button (#3348) @knolleary
- Function: load extralibs when expanding monaco. fixes #3319 (#3334) @Steve-Mcl
- Function: Update Function to use correct api to access env vars (#3310) @knolleary
- HTTP Request: Fix basic auth with empty username or password (#3325) @hardillb
- Inject: Fix incorrect clearing of blank payload property in Inject node (#3322) @knolleary
- Link Call: add link call example (#3336) @HiroyasuNishiyama
- WebSocket: Only setup ws client heartbeat once it is connected (#3344) @knolleary
- Update Japanese translations in node help (#3332) @kazuhitoyokoi
#### 2.1.4: Maintenance Release
Runtime
- fix env var access using $parent for groups (#3278) @HiroyasuNishiyama
- Add proper error handling for 404 errors when serving debug files (#3277) @knolleary
- Add Japanese translations for Node-RED v2.1.0-beta.1 (#3179) @kazuhitoyokoi
- Include full user object on login audit events (#3269) @knolleary
- Remove styling from de locale files (#3237) @knolleary
Editor
- Change tab hide button icon to an eye and add search option (#3282) @knolleary
- Fix i18n handling of namespaces with spaces in (#3281) @knolleary
- Trigger change event when autoComplete fills in input (#3280) @knolleary
- Apply CN i18n fix (#3279) @knolleary
- fix select menu label of config node to use paletteLabel (#3273) @HiroyasuNishiyama
- fix removed tab not to cause node conflict (#3275) @HiroyasuNishiyama
- Group diff fix (#3239) @knolleary
- Only toggle disabled workspace flag if on activeWorkspace (#3252) @knolleary
- Do not show status for disabled nodes (#3253) @knolleary
- Set dimension value for tour guide (#3265) @kazuhitoyokoi
- Avoid redundant initialisation of TypedInput type (#3263) @knolleary
- Don't let themes change flow port label color (#3270) @bonanitech
- Fix treeList gutter calculation to handle floating gutters (#3238) @knolleary
Nodes
- Debug: Handle RegExp types in Debug sidebar (#3251) @knolleary
- Delay: fix 2nd output when in rate limit per topic modes (#3261) @dceejay
- Link: fix to show link target when selected (#3267) @HiroyasuNishiyama
- Inject: Do not modify inject node props in oneditprepare (#3242) @knolleary
- HTTP Request: HTTP Basic Auth should always add : between username and password even if empty (#3236) @hardillb
#### 2.1.3: Maintenance Release
Runtime
- Update gen-publish script to update 'next' tag for main releases
- Add environment variable to enable/disable tours (#3221) @hardillb
- Fix loading non-default language files leaving runtime in wrong locale (#3225) @knolleary
Editor
- Refresh editor settings whenever a node is added or enabled (#3227) @knolleary
- Revert spinner css change that made it shrink in some cases (#3229) @knolleary
- Fix import notification message when importing config nodes (#3224) @knolleary
- Handle changing types of TypedInput repeatedly (#3223) @knolleary
#### 2.1.2: Maintenance Release
Runtime
- node-red-pi: Remove bash dependency (#3216) @a16bitsysop
Editor
- Improved regex for markdown renderer (#3213) @GerwinvBeek
- Fix TypedInput initialisation (#3220) @knolleary
Nodes
- MQTT: fix datatype in node config not used. fixes #3215 (#3219) @Steve-Mcl
#### 2.1.1: Maintenance Release
Editor
- Ensure tourGuide popover doesn't fall offscreen (#3212) @knolleary
- Fix issue with old inject nodes that migrated topic to 'string' type (#3210) @knolleary
- Add cache-busting query params to index.mst (#3211) @knolleary
- Fix TypedInput validation of type without options (#3207) @knolleary
#### 2.1.0: Milestone Release
Editor
- Position popover properly on a scrolled page
- Fixes from 2.1.0-beta.2 (#3202) @knolleary
Nodes
- Link Out: Fix saving link out node links (#3201) @knolleary
- Switch: Refix #3170 - copy switch rule type when adding new rule
- TCP Request: Add string option to TCP request node output (#3204) @dceejay
#### 2.1.0-beta.2: Beta Release
Editor
- Fix switching projects (#3199) @knolleary
- Use locale setting when installing/enabling node (#3198) @knolleary
- Do not show projects-wecome dialog until welcome tour completes (#3197) @knolleary
- Fix converting selection to subflow (#3196) @knolleary
- Avoid conflicts with native browser cmd-ctrl type shortcuts (#3195) @knolleary
- Ensure message tools stay attached to top-level entry in Debug/Context (#3186) @knolleary
- Ensure tab state updates properly when toggling enable state (#3175) @knolleary
- Improve handling of long labels in TreeList (#3176) @knolleary
- Shift-click tab scroll arrows to jump to start/end (#3177) @knolleary
Runtime
- Update package dependencies
- Update to latest node-red-admin
Nodes
- Dynamic MQTT connections (#3189)
- Link: Filter out Link Out Return nodes in Link In edit dialog Fixes #3187
- Link: Fix link call label (#3200) @knolleary
- Debug: Redesign debug filter options and make them persistant (#3183) @knolleary
- Inject: Widen Inject interval box for >1 digit (#3184) @knolleary
- Switch: Fix rule focus when switch 'otherwise' rule is used (#3185) @knolleary
#### 2.1.0-beta.1: Beta Release
Editor
- Add Tour Guide component (#3136) @knolleary
- Allow tabs to be hidden (#3120) @knolleary
- Add align actions to editor (#3110) @knolleary
- Add support of environment variable for tab & group (#3112) @HiroyasuNishiyama
- Add autoComplete widget and add to TypedInput for msg. props (#3171) @knolleary
- Render node documentation to node-red style guide when written in markdown. (#3169) @Steve-Mcl
- Allow colouring of tab icon svg (#3140) @harmonic7
- Restore tab selection after merging conflicts (#3151) @GerwinvBeek
- Fix serving of theme files on Windows (#3154) @knolleary
- Ensure config-node select inherits width properly from input (#3155) @knolleary
- Do better remembering TypedInput values whilst switching types (#3159) @knolleary
- Update monaco to 0.28.1 (#3153) @knolleary
- Improve themeing of tourGuide (#3161) @knolleary
- Allow a node to specify a filter for the config nodes it can pick from (#3160) @knolleary
- Allow RED.notify.update to modify any notification setting (#3163) @knolleary
- Fix typo in ko editor.json Fixes #3119
- Improve RED.actions api to ensure actions cannot be overridden
- Ensure treeList row has suitable min-height when no content Fixes #3109
- Refactor edit dialogs to use separate edit panes
- Ensure type select button is not focussable when TypedInput only has one type
- Place close tab link in front of fade
Runtime
- Improve error reporting with oauth login strategies (#3148) @knolleary
- Add allowUpdate feature to externalModules.palette (#3143) @knolleary
- Improve node install error reporting (#3158) @knolleary
- Improve unit test coverage (#3168) @knolleary
- Allow coreNodesDir to be set to false (#3149) @hardillb
- Update package dependencies
- uncaughtException debug improvements (#3146) @renatojuniorrs
Nodes
- Change: Add option to deep-clone properties in Change node (#3156) @knolleary
- Delay: Add push to front of rate limit queue. (#3069) @dceejay
- File: Add paletteLabel to file nodes to make read/write more obvious (#3157) @knolleary
- HTTP Request: Extend HTTP request node to log detailed timing information (#3116) @k-toumura
- HTTP Response: Fix sizing of HTTP Response header fields (#3164) @knolleary
- Join: Support for msg.restartTimeout (#3121) @magma1447
- Link Call: Add Link Call node (#3152) @knolleary
- Switch: Copy previous rule type when adding rule to switch node (#3170) @knolleary
- Delay node: add option to send intermediate messages on separate output (#3166) @knolleary
- Typo in http request set method translation (#3173) @mailsvb
#### 2.0.6: Maintenance Release
Editor
- Fix typo in ko editor.json Fixes #3119
- Change fade color when hovering an inactive tab (#3106) @bonanitech
- Ensure treeList row has suitable min-height when no content Fixes #3109
Runtime
- Update tar to latest (#3128) @aksswami
- Give passport verify callback the same arity as the original callback (#3117) @dschmidt
- Handle HTTPS Key and certificate as string or buffer (#3115) @bartbutenaers
#### 2.0.5: Maintenance Release
Editor
- Remove default ctrl-enter keybinding from monaco editor Fixes #3093
Runtime
- Update tar dependency
- Add support for maintenance streams in generate-publish-script
Nodes
- Fix regression in Join node when manual joining array with msg.parts present Fixes #3096
#### 2.0.4: Maintenance Release
Editor
- Fix tab fade CSS for when using themes (#3085) @bonanitech
- Handle just-copied-but-not-deployed node with credentials in editor Fixes #3090
Nodes
- Filter: Fix RBE node handling of default topi property Fixes #3087
- HTTP Request: Handle partially encoded url query strings in request node
- HTTP Request: Fix support for supplied CA certs (#3089) @hardillb
- HTTP Request: Ensure TLS Cert is used (#3092) @hardillb
- Inject: Fix inject now button unable to send empty props
- Inject: Inject now button success notification should use label with updated props
#### 2.0.3: Maintenance Release
Nodes
- HTML: Fix HTML parsing when body is included in the select tag Fixes #3079
- HTTP Request: Preserve case of user-provided http headers in request node Fixes #3081
- HTTP Request: Set decompress to false for HTTP Request to keep 1.x compatibility Fixes #3083
- HTTP Request: Add unit tests for HTTP Request encodeURI and error response
- HTTP Request: Do not throw HTTP errors in request node Fixes #3082
- HTTP Request: Ensure uri is properly encoded before passing to got module Fixes #3080
#### 2.0.2: Maintenance Release
Runtime
- Use file:// url with dynamic import
- Detect if agent-base has patched https.request and undo it Fixes #3072
Editor
- Fix tab fade css because Safari Fixes #3073
- Fix error closing library dialog with monaco
- Handle other error types in Manage Palette view
Nodes
- HTTP Request node - ignore invalid cookies rather than fail request Fixes #3075
- Fix msg.reset handling in Delay node Fixes #3074
#### 2.0.1: Maintenance Release
Nodes
- Function: Ensure default module export is exposed in Function node
#### 2.0.0: Milestone Release
**Migration from 1.x**
- Node-RED now requires Node.js 12.x or later.
- The following nodes have had significant dependency updates. Unless stated,
they should be fully backward compatible.
- RBE: Relabelled as 'filter' to make it more discoverable and made part of
the core palette, rather than as a separate module.
- Tail: This node has been removed from the default palette. You can reinstall it
from node-red-node-tail
- HTTP Request: Reimplemented with a different underlying module. We have
tried to maintain 100% functional compatibility, but it is possible
some edge cases remain.
- JSON: The schema validation option no longer supports JSON-Schema draft-04
- HTML: Its underlying module has had a major version update. Should be fully
backward compatible.
- `functionExternalModules` is now enabled by default for new installs.
If you have an existing settings file that contains this setting, you will
need to set it to `true` yourself.
The external modules will now get installed in your Node-RED user directory,
(`~/.node-red`) rather than in a subdirectory. This means all dependencies will
be listed in your top-level `package.json`. If you have existing external modules,
they will get reinstalled to the new location when you first run Node-RED 2.0.
Runtime
- Fix missing dependencies (#3052, #2057) @kazuhitoyokoi
- Ensure node.types is defined if node html file missing
- Fix reporting of type_already_registered error
- Move install location of external modules (#3064) @knolleary
Editor
- Update translations (#3063) @kazuhitoyokoi
- Add a slight fade to tab labels that overflow
- Show config node details when selected in outliner
- Fix layout of info outliner for subflow entries
Nodes
- Delay: let `msg.flush` specify how many messages to flush from node (#3059) @dceejay
- Function: external modules is now enabled by default (#3065) @knolleary
- Function: external modules now supports both ES6 and CJS modules (#3065) @knolleary
- WebSocket: add option for client node to send automatic pings (#3056) @knolleary
##### 2.0.0-beta.2: Beta Release
Runtime
- Add `node-red admin init` (via `node-red-admin@2.1.0`)
- Move to GH Actions rather than Travis for build (#3042) @knolleary
Editor
- Include hasUser=false config nodes when exporting whole flow (#3048)
- Emit nodes:change for any updated config node when node deleted/added
- Fix padding of compact notification Closes #3045
- Ensure any html in changelog is escaped before displaying
- Add support for Map/Set property types on Debug (#3040) @knolleary
- Add 'theme' to default settings file
- Add RED.view.annotations api (#3032) @knolleary
- Update monaco editor to V0.25.2 (#3031) @Steve-Mcl
- Lower tray zIndex when overlay tray being opened Fixes #3019
- Reduce z-Index of Function expand buttons to prevent overlap Part of #3019
- Ensure RED.clipboard.import displays the right library Fixes #3021
- Batch messages sent over comms to prevent flooding (#3025) @knolleary
- Allow RED.popover.panel to specify a closeButton to ignore click events on
- Use browser default language for initial page load
- Add css var for node font color
- Fix label padding of toggleButton
- Give sidebar open tab a bit more room for its label
- Various Monaco updates (#3015) @Steve-Mcl
- Log readOnly on startup (#3024) @sammachin
- Translation updates (#3020 #3022) @HiroyasuNishiyama @kazuhitoyokoi
Nodes
- HTTP Request: Fix proxy handling (#3044) @hardillb
- HTTP Request: Handle basic auth with @ in username (#3017) @hardillb
- Add Japanese translation for file-in node (#3037 #3039) @kazuhitoyokoi
- File In: Add option for file-in node to include all properties (default off) (#3035) @dceejay
- Exec: add windowsHide option to hide windows under Windows (#3026) @natcl
- Support loading external module sub path Fixes #3023
##### 2.0.0-beta.1: Beta Release
Runtime
- [MAJOR] Set minimum node version to 12.
- [MAJOR] Fix flowfile name to flows.json in settings (#2951) @dceejay
- [MAJOR] Update to latest i18n in editor and runtime (#2940) @knolleary
- [MAJOR] Deprecate usage of httpRoot (#2953) @knolleary
- Add pre/postInstall hooks to npm install handling (#2936) @knolleary
- Add engine-strict flag to npm install args (#2965) @nileio
- Restructure default settings.js to be more organised (#3012) @knolleary
- Ensure httpServerOptions gets applied to ALL the express apps
- Allow RED.settings.set to replace string property with object property
- Update debug tests to handle compact comms format
- Updates to encode/decode message when passed over debug comms link
- Remove all input event listeners on a node once it is closed
- Move hooks to util package
- Rework hooks structure to be a linkedlist
- Update dependencies (#2922) @knolleary
Editor
- [MAJOR] Change node id generation to give fixed length values without '.' (#2987) @knolleary
- [MAJOR] Add Monaco code editor (#2971) @Steve-Mcl
- Update to latest Monaco (#3007) @Steve-Mcl
- Update Node-RED Function typings in Monaco (#3008) @Steve-Mcl
- Add css named variables for certain key colours (#2994) @knolleary
- Improve contrast of export dialog JSON font color
- Switch editableList buttons from <a> to <button> elements
- Add option to RED.nodes.createCompleteNodeSet to include node dimensions
- Fix css of node help table of contents elements
- Improve red-ui-node-icon css and add red-ui-node-icon-small modifier class
- Add RED.hooks to editor
- Add viewAddPort viewRemovePort viewAddNode viewRemoveNode hooks to view
- Use paletteLabel if set in help sidebar
- Add missing args from JSONata $now signature
Nodes
- [MAJOR] Relabel RBE node as 'filter' and move into core. Also remove tail (#2944) @dceejay
- [MAJOR] HTTP Request: migrate to 'got' module (#2952) @knolleary
- [MAJOR] Move Inject node to CronosJS module (#2959) @knolleary
- [MAJOR] JSON: Update ajv to 8.2.0 - drop support for JSON-Schema draft-04 (#2969) @knolleary
- [MAJOR] HTML node: cheerio update to 1.x (#3011) @knolleary
- Join: change default manual mode to object (#2931) @knolleary
- File node: Add fileWorkingDirectory (#2932) @knolleary
- Delay node enhancements (#2294) @kazuhitoyokoi (#2949) @dceejay
- Add Japanese translations for delay node enhancements (#2958) @kazuhitoyokoi
- Inject node: reorder TypedInput options (#2961) @dceejay
- HTTP Request: update to work with proxies (#2983) @hardillb (#3009) @hardillb
- HTTP Request: fix msg.responseUrl (#2986) @hardillb
- TLS: Add ALPN support to TLS node (#2988) @hardillb
- Inject: add "Inject now" button to edit dialog (#2990) @Steve-Mcl
#### Older Releases
Change logs for older releases are available on GitHub: https://github.com/node-red/node-red/releases

View File

@ -1,16 +0,0 @@
# Contributing to Sparkles Guide
Submit an [issue](https://github.com/defenseunicorns/Sparkles-Guide/issues/new) if unsure.
### To Sparkle Guides
See [this](docs/sparkles-guides-contribution.md) guide, if you are looking to contribute to the guides and flows.
### To Code
See [this](docs/code-contribution.md) guide, if you are looking to add/remove/modify code implementing Sparkle's Guide.
## Copyright and license
Copyright OpenJS Foundation and other contributors, https://openjsf.org under [the Apache 2.0 license](LICENSE).
December 2021 --- this project and modifications within it are a derivative work of [node-red](https://github.com/node-red/node-red)

1869
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -78,6 +78,7 @@
"bcrypt": "5.0.1"
},
"devDependencies": {
"cypress": "^9.3.1",
"dompurify": "2.3.5",
"grunt": "1.4.1",
"grunt-chmod": "~1.1.1",

View File

@ -2135,7 +2135,7 @@ RED.nodes = (function() {
n = new_nodes[i];
if (n.wires) {
for (var w1=0;w1<n.wires.length;w1++) {
var wires = (n.wires[w1] instanceof Array)?n.wires[w1]:[n.wires[w1]];
var wires = (Array.isArray(n.wires[w1]))?n.wires[w1]:[n.wires[w1]];
for (var w2=0;w2<wires.length;w2++) {
if (node_map.hasOwnProperty(wires[w2])) {
if (n.z === node_map[wires[w2]].z) {

View File

@ -252,8 +252,21 @@ var RED = (function() {
if (/^#flow\/.+$/.test(currentHash)) {
RED.workspaces.show(currentHash.substring(6),true);
}
if (RED.workspaces.active() === 0 && RED.workspaces.count() > 0) {
RED.workspaces.show(RED.nodes.getWorkspaceOrder()[0])
if (RED.workspaces.count() > 0) {
const hiddenTabs = JSON.parse(RED.settings.getLocal("hiddenTabs")||"{}");
const workspaces = RED.nodes.getWorkspaceOrder();
if (RED.workspaces.active() === 0) {
for (let index = 0; index < workspaces.length; index++) {
const ws = workspaces[index];
if (!hiddenTabs[ws]) {
RED.workspaces.show(ws);
break;
}
}
}
if (RED.workspaces.active() === 0) {
RED.workspaces.show(workspaces[0]);
}
}
} catch(err) {
console.warn(err);
@ -461,7 +474,7 @@ var RED = (function() {
var parts = topic.split("/");
var node = RED.nodes.node(parts[1]);
if (node) {
if (msg.hasOwnProperty("text") && msg.text !== null && /^[a-zA-Z]/.test(msg.text)) {
if (msg.hasOwnProperty("text") && msg.text !== null && /^[@a-zA-Z]/.test(msg.text)) {
msg.text = node._(msg.text.toString(),{defaultValue:msg.text.toString()});
}
node.status = msg;

View File

@ -315,205 +315,208 @@ RED.deploy = (function() {
},delta);
});
}
function save(skipValidation,force) {
if (!$("#red-ui-header-button-deploy").hasClass("disabled")) {
if (!RED.user.hasPermission("flows.write")) {
RED.notify(RED._("user.errors.deploy"),"error");
function save(skipValidation, force) {
if ($("#red-ui-header-button-deploy").hasClass("disabled")) {
return; //deploy is disabled
}
if ($("#red-ui-header-shade").is(":visible")) {
return; //deploy is shaded
}
if (!RED.user.hasPermission("flows.write")) {
RED.notify(RED._("user.errors.deploy"), "error");
return;
}
if (!skipValidation) {
let hasUnknown = false;
let hasInvalid = false;
let hasUnusedConfig = false;
const unknownNodes = [];
const invalidNodes = [];
RED.nodes.eachConfig(function (node) {
if (node.valid === undefined) {
RED.editor.validateNode(node);
}
if (!node.valid && !node.d) {
invalidNodes.push(getNodeInfo(node));
}
if (node.type === "unknown") {
if (unknownNodes.indexOf(node.name) == -1) {
unknownNodes.push(node.name);
}
}
});
RED.nodes.eachNode(function (node) {
if (!node.valid && !node.d) {
invalidNodes.push(getNodeInfo(node));
}
if (node.type === "unknown") {
if (unknownNodes.indexOf(node.name) == -1) {
unknownNodes.push(node.name);
}
}
});
hasUnknown = unknownNodes.length > 0;
hasInvalid = invalidNodes.length > 0;
const unusedConfigNodes = [];
RED.nodes.eachConfig(function (node) {
if ((node._def.hasUsers !== false) && (node.users.length === 0)) {
unusedConfigNodes.push(getNodeInfo(node));
hasUnusedConfig = true;
}
});
let showWarning = false;
let notificationMessage;
let notificationButtons = [];
let notification;
if (hasUnknown && !ignoreDeployWarnings.unknown) {
showWarning = true;
notificationMessage = "<p>" + RED._('deploy.confirm.unknown') + "</p>" +
'<ul class="red-ui-deploy-dialog-confirm-list"><li>' + cropList(unknownNodes).map(function (n) { return sanitize(n) }).join("</li><li>") + "</li></ul><p>" +
RED._('deploy.confirm.confirm') +
"</p>";
notificationButtons = [
{
id: "red-ui-deploy-dialog-confirm-deploy-deploy",
text: RED._("deploy.confirm.button.confirm"),
class: "primary",
click: function () {
save(true);
notification.close();
}
}
];
} else if (hasInvalid && !ignoreDeployWarnings.invalid) {
showWarning = true;
invalidNodes.sort(sortNodeInfo);
notificationMessage = "<p>" + RED._('deploy.confirm.improperlyConfigured') + "</p>" +
'<ul class="red-ui-deploy-dialog-confirm-list"><li>' + cropList(invalidNodes.map(function (A) { return sanitize((A.tab ? "[" + A.tab + "] " : "") + A.label + " (" + A.type + ")") })).join("</li><li>") + "</li></ul><p>" +
RED._('deploy.confirm.confirm') +
"</p>";
notificationButtons = [
{
id: "red-ui-deploy-dialog-confirm-deploy-deploy",
text: RED._("deploy.confirm.button.confirm"),
class: "primary",
click: function () {
save(true);
notification.close();
}
}
];
}
if (showWarning) {
notificationButtons.unshift(
{
text: RED._("common.label.cancel"),
click: function () {
notification.close();
}
}
);
notification = RED.notify(notificationMessage, {
modal: true,
fixed: true,
buttons: notificationButtons
});
return;
}
if (!skipValidation) {
var hasUnknown = false;
var hasInvalid = false;
var hasUnusedConfig = false;
var unknownNodes = [];
var invalidNodes = [];
RED.nodes.eachConfig(function(node) {
if (node.valid === undefined) {
RED.editor.validateNode(node);
}
if (!node.valid && !node.d) {
invalidNodes.push(getNodeInfo(node));
}
if (node.type === "unknown") {
if (unknownNodes.indexOf(node.name) == -1) {
unknownNodes.push(node.name);
}
}
});
RED.nodes.eachNode(function(node) {
if (!node.valid && !node.d) {
invalidNodes.push(getNodeInfo(node));
}
if (node.type === "unknown") {
if (unknownNodes.indexOf(node.name) == -1) {
unknownNodes.push(node.name);
}
}
});
hasUnknown = unknownNodes.length > 0;
hasInvalid = invalidNodes.length > 0;
var unusedConfigNodes = [];
RED.nodes.eachConfig(function(node) {
if ((node._def.hasUsers !== false) && (node.users.length === 0)) {
unusedConfigNodes.push(getNodeInfo(node));
hasUnusedConfig = true;
}
});
var showWarning = false;
var notificationMessage;
var notificationButtons = [];
var notification;
if (hasUnknown && !ignoreDeployWarnings.unknown) {
showWarning = true;
notificationMessage = "<p>"+RED._('deploy.confirm.unknown')+"</p>"+
'<ul class="red-ui-deploy-dialog-confirm-list"><li>'+cropList(unknownNodes).map(function(n) { return sanitize(n) }).join("</li><li>")+"</li></ul><p>"+
RED._('deploy.confirm.confirm')+
"</p>";
notificationButtons= [
{
id: "red-ui-deploy-dialog-confirm-deploy-deploy",
text: RED._("deploy.confirm.button.confirm"),
class: "primary",
click: function() {
save(true);
notification.close();
}
}
];
} else if (hasInvalid && !ignoreDeployWarnings.invalid) {
showWarning = true;
invalidNodes.sort(sortNodeInfo);
notificationMessage = "<p>"+RED._('deploy.confirm.improperlyConfigured')+"</p>"+
'<ul class="red-ui-deploy-dialog-confirm-list"><li>'+cropList(invalidNodes.map(function(A) { return sanitize( (A.tab?"["+A.tab+"] ":"")+A.label+" ("+A.type+")")})).join("</li><li>")+"</li></ul><p>"+
RED._('deploy.confirm.confirm')+
"</p>";
notificationButtons= [
{
id: "red-ui-deploy-dialog-confirm-deploy-deploy",
text: RED._("deploy.confirm.button.confirm"),
class: "primary",
click: function() {
save(true);
notification.close();
}
}
];
}
if (showWarning) {
notificationButtons.unshift(
{
text: RED._("common.label.cancel"),
click: function() {
notification.close();
}
}
);
notification = RED.notify(notificationMessage,{
modal: true,
fixed: true,
buttons:notificationButtons
});
return;
}
}
var nns = RED.nodes.createCompleteNodeSet();
var startTime = Date.now();
$(".red-ui-deploy-button-content").css('opacity',0);
$(".red-ui-deploy-button-spinner").show();
$("#red-ui-header-button-deploy").addClass("disabled");
var data = {flows:nns};
if (!force) {
data.rev = RED.nodes.version();
}
deployInflight = true;
$("#red-ui-header-shade").show();
$("#red-ui-editor-shade").show();
$("#red-ui-palette-shade").show();
$("#red-ui-sidebar-shade").show();
$.ajax({
url:"flows",
type: "POST",
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
headers: {
"Node-RED-Deployment-Type":deploymentType
}
}).done(function(data,textStatus,xhr) {
RED.nodes.dirty(false);
RED.nodes.version(data.rev);
RED.nodes.originalFlow(nns);
if (hasUnusedConfig) {
RED.notify(
'<p>'+RED._("deploy.successfulDeploy")+'</p>'+
'<p>'+RED._("deploy.unusedConfigNodes")+' <a href="#" onclick="RED.sidebar.config.show(true); return false;">'+RED._("deploy.unusedConfigNodesLink")+'</a></p>',"success",false,6000);
} else {
RED.notify('<p>'+RED._("deploy.successfulDeploy")+'</p>',"success");
}
RED.nodes.eachNode(function(node) {
if (node.changed) {
node.dirty = true;
node.changed = false;
}
if (node.moved) {
node.dirty = true;
node.moved = false;
}
if(node.credentials) {
delete node.credentials;
}
});
RED.nodes.eachConfig(function (confNode) {
confNode.changed = false;
if (confNode.credentials) {
delete confNode.credentials;
}
});
RED.nodes.eachSubflow(function(subflow) {
subflow.changed = false;
});
RED.nodes.eachWorkspace(function(ws) {
ws.changed = false;
});
// Once deployed, cannot undo back to a clean state
RED.history.markAllDirty();
RED.view.redraw();
RED.events.emit("deploy");
}).fail(function(xhr,textStatus,err) {
RED.nodes.dirty(true);
$("#red-ui-header-button-deploy").removeClass("disabled");
if (xhr.status === 401) {
RED.notify(RED._("deploy.deployFailed",{message:RED._("user.notAuthorized")}),"error");
} else if (xhr.status === 409) {
resolveConflict(nns, true);
} else if (xhr.responseText) {
RED.notify(RED._("deploy.deployFailed",{message:xhr.responseText}),"error");
} else {
RED.notify(RED._("deploy.deployFailed",{message:RED._("deploy.errors.noResponse")}),"error");
}
}).always(function() {
deployInflight = false;
var delta = Math.max(0,300-(Date.now()-startTime));
setTimeout(function() {
$(".red-ui-deploy-button-content").css('opacity',1);
$(".red-ui-deploy-button-spinner").hide();
$("#red-ui-header-shade").hide();
$("#red-ui-editor-shade").hide();
$("#red-ui-palette-shade").hide();
$("#red-ui-sidebar-shade").hide();
},delta);
});
}
const nns = RED.nodes.createCompleteNodeSet();
const startTime = Date.now();
$(".red-ui-deploy-button-content").css('opacity', 0);
$(".red-ui-deploy-button-spinner").show();
$("#red-ui-header-button-deploy").addClass("disabled");
const data = { flows: nns };
if (!force) {
data.rev = RED.nodes.version();
}
deployInflight = true;
$("#red-ui-header-shade").show();
$("#red-ui-editor-shade").show();
$("#red-ui-palette-shade").show();
$("#red-ui-sidebar-shade").show();
$.ajax({
url: "flows",
type: "POST",
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
headers: {
"Node-RED-Deployment-Type": deploymentType
}
}).done(function (data, textStatus, xhr) {
RED.nodes.dirty(false);
RED.nodes.version(data.rev);
RED.nodes.originalFlow(nns);
if (hasUnusedConfig) {
RED.notify(
'<p>' + RED._("deploy.successfulDeploy") + '</p>' +
'<p>' + RED._("deploy.unusedConfigNodes") + ' <a href="#" onclick="RED.sidebar.config.show(true); return false;">' + RED._("deploy.unusedConfigNodesLink") + '</a></p>', "success", false, 6000);
} else {
RED.notify('<p>' + RED._("deploy.successfulDeploy") + '</p>', "success");
}
RED.nodes.eachNode(function (node) {
if (node.changed) {
node.dirty = true;
node.changed = false;
}
if (node.moved) {
node.dirty = true;
node.moved = false;
}
if (node.credentials) {
delete node.credentials;
}
});
RED.nodes.eachConfig(function (confNode) {
confNode.changed = false;
if (confNode.credentials) {
delete confNode.credentials;
}
});
RED.nodes.eachSubflow(function (subflow) {
subflow.changed = false;
});
RED.nodes.eachWorkspace(function (ws) {
ws.changed = false;
});
// Once deployed, cannot undo back to a clean state
RED.history.markAllDirty();
RED.view.redraw();
RED.events.emit("deploy");
}).fail(function (xhr, textStatus, err) {
RED.nodes.dirty(true);
$("#red-ui-header-button-deploy").removeClass("disabled");
if (xhr.status === 401) {
RED.notify(RED._("deploy.deployFailed", { message: RED._("user.notAuthorized") }), "error");
} else if (xhr.status === 409) {
resolveConflict(nns, true);
} else if (xhr.responseText) {
RED.notify(RED._("deploy.deployFailed", { message: xhr.responseText }), "error");
} else {
RED.notify(RED._("deploy.deployFailed", { message: RED._("deploy.errors.noResponse") }), "error");
}
}).always(function () {
deployInflight = false;
const delta = Math.max(0, 300 - (Date.now() - startTime));
setTimeout(function () {
$(".red-ui-deploy-button-content").css('opacity', 1);
$(".red-ui-deploy-button-spinner").hide();
$("#red-ui-header-shade").hide();
$("#red-ui-editor-shade").hide();
$("#red-ui-palette-shade").hide();
$("#red-ui-sidebar-shade").hide();
}, delta);
});
}
return {
init: init,

View File

@ -168,7 +168,7 @@
'b': { before:"**", after: "**", tooltip: RED._("markdownEditor.bold")},
'i': { before:"_", after: "_", tooltip: RED._("markdownEditor.italic")},
'code': { before:"`", after: "`", tooltip: RED._("markdownEditor.code")},
'ol': { before:" * ", newline: true, tooltip: RED._("markdownEditor.ordered-list")},
'ol': { before:" 1. ", newline: true, tooltip: RED._("markdownEditor.ordered-list")},
'ul': { before:" - ", newline: true, tooltip: RED._("markdownEditor.unordered-list")},
'bq': { before:"> ", newline: true, tooltip: RED._("markdownEditor.quote")},
'link': { before:"[", after: "]()", tooltip: RED._("markdownEditor.link")},

View File

@ -107,7 +107,7 @@
newValue = "";
}
}
if (node[d] != newValue) {
if (!isEqual(node[d], newValue)) {
if (node._def.defaults[d].type) {
// Change to a related config node
var configNode = RED.nodes.node(node[d]);
@ -139,6 +139,23 @@
}
});
/**
* Compares `newValue` with `originalValue` for equality.
* @param {*} originalValue Original value
* @param {*} newValue New value
* @returns {boolean} true if originalValue equals newValue, otherwise false
*/
function isEqual(originalValue, newValue) {
try {
if(originalValue == newValue) {
return true;
}
return JSON.stringify(originalValue) === JSON.stringify(newValue);
} catch (err) {
return false;
}
}
/**
* Update the node credentials from the edit form
* @param node - the node containing the credentials

View File

@ -346,7 +346,7 @@ RED.sidebar.config = (function() {
refreshConfigNodeList();
}
});
RED.popover.tooltip($('#red-ui-sidebar-config-filter-all'), RED._("sidebar.config.showAllUnusedConfigNodes"));
RED.popover.tooltip($('#red-ui-sidebar-config-filter-all'), RED._("sidebar.config.showAllConfigNodes"));
RED.popover.tooltip($('#red-ui-sidebar-config-filter-unused'), RED._("sidebar.config.showAllUnusedConfigNodes"));
}

View File

@ -5743,7 +5743,7 @@
node.dirty = true;
RED.workspaces.show(node.z);
var screenSize = [chart.width()/scaleFactor,chart.height()/scaleFactor];
var screenSize = [chart[0].clientWidth/scaleFactor,chart[0].clientHeight/scaleFactor];
var scrollPos = [chart.scrollLeft()/scaleFactor,chart.scrollTop()/scaleFactor];
var cx = node.x;
var cy = node.y;

View File

@ -151,6 +151,8 @@ $popover-button-border-color-hover: #666;
$diff-text-header-color: $secondary-text-color;
$diff-text-header-background: #ffd;
$diff-text-header-background-hover: #ffc;
$diff-state-color: $primary-text-color;
$diff-state-prefix-color: $secondary-text-color;
$diff-state-added: #009900;
$diff-state-deleted: #f80000;
$diff-state-changed: #f89406;

View File

@ -562,7 +562,7 @@ ul.red-ui-deploy-dialog-confirm-list {
width: 30px;
display: inline-block;
text-align: center;
color: $secondary-text-color;
color: $diff-state-prefix-color;
}
&.added {
@ -577,9 +577,11 @@ ul.red-ui-deploy-dialog-confirm-list {
}
td.added {
background: $diff-state-added-background;
color: $diff-state-color;
}
td.removed {
background: $diff-state-deleted-background;
color: $diff-state-color;
}
tr.mergeHeader td {
color: $diff-merge-header-color;
@ -652,7 +654,7 @@ ul.red-ui-deploy-dialog-confirm-list {
font-family: $monospace-font;
padding: 5px 10px;
text-align: left;
color: $secondary-text-color;
color: $diff-text-header-color;
background: $diff-text-header-background;
height: 30px;
vertical-align: middle;

View File

@ -1,113 +0,0 @@
[
{
"id": "84222b92.d65d18",
"type": "inject",
"z": "6312c0588348b2d4",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "Hello, World!",
"payloadType": "str",
"x": 190,
"y": 180,
"wires": [
[
"b4b9f603.739598"
]
]
},
{
"id": "7b014430.dfd94c",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "Write string to a file, then read from the file",
"info": "Read file node can read string from a file.",
"x": 220,
"y": 100,
"wires": []
},
{
"id": "b4b9f603.739598",
"type": "file",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "none",
"x": 380,
"y": 180,
"wires": [
[
"6dc01cac.5c4bf4"
]
]
},
{
"id": "2587adb9.7e60f2",
"type": "debug",
"z": "6312c0588348b2d4",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 770,
"y": 180,
"wires": []
},
{
"id": "6dc01cac.5c4bf4",
"type": "file in",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"format": "utf8",
"chunk": false,
"sendError": false,
"encoding": "none",
"x": 580,
"y": 180,
"wires": [
[
"2587adb9.7e60f2"
]
]
},
{
"id": "f4b4309a.3b78a",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↑read result from file",
"info": "",
"x": 590,
"y": 220,
"wires": []
},
{
"id": "672d3693.3cabd8",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↓write to /tmp/hello.txt",
"info": "",
"x": 400,
"y": 140,
"wires": []
}
]

View File

@ -1,114 +0,0 @@
[
{
"id": "8997398f.c5d628",
"type": "inject",
"z": "6312c0588348b2d4",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "😀",
"payloadType": "str",
"x": 170,
"y": 260,
"wires": [
[
"56e32d23.050f44"
]
]
},
{
"id": "4e598e65.1799d",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "Read data in specified encoding",
"info": "Read file node can specify encoding of data read from a file.",
"x": 190,
"y": 180,
"wires": []
},
{
"id": "56e32d23.050f44",
"type": "file",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "none",
"x": 340,
"y": 260,
"wires": [
[
"38fa0579.f2cd8a"
]
]
},
{
"id": "d28c8994.99c0a8",
"type": "debug",
"z": "6312c0588348b2d4",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 730,
"y": 260,
"wires": []
},
{
"id": "38fa0579.f2cd8a",
"type": "file in",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"format": "utf8",
"chunk": false,
"sendError": false,
"encoding": "base64",
"allProps": false,
"x": 540,
"y": 260,
"wires": [
[
"d28c8994.99c0a8"
]
]
},
{
"id": "fa22ca20.ae4528",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↑read data from file as base64 string",
"info": "",
"x": 600,
"y": 300,
"wires": []
},
{
"id": "148e25ad.98891a",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↓write to /tmp/hello.txt",
"info": "",
"x": 360,
"y": 220,
"wires": []
}
]

View File

@ -1,132 +0,0 @@
[
{
"id": "6a0b1d03.d4cee4",
"type": "inject",
"z": "6312c0588348b2d4",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 160,
"y": 220,
"wires": [
[
"d4b00cb7.a5a23"
]
]
},
{
"id": "f17ea1d1.8ecc3",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "Read data breaking lines into individual messages",
"info": "Read file node can break read text into messages with individual lines",
"x": 230,
"y": 140,
"wires": []
},
{
"id": "99ae7806.1d6428",
"type": "file",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "none",
"x": 480,
"y": 220,
"wires": [
[
"70d7892f.d27db8"
]
]
},
{
"id": "7ed8282c.92b338",
"type": "debug",
"z": "6312c0588348b2d4",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 750,
"y": 280,
"wires": []
},
{
"id": "70d7892f.d27db8",
"type": "file in",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"format": "lines",
"chunk": false,
"sendError": false,
"encoding": "none",
"x": 560,
"y": 280,
"wires": [
[
"7ed8282c.92b338"
]
]
},
{
"id": "c1b7e05.1d94b2",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↑read data from file breaking lines into messages",
"info": "",
"x": 660,
"y": 320,
"wires": []
},
{
"id": "a5f647b2.cf27a8",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↓write to /tmp/hello.txt",
"info": "",
"x": 500,
"y": 180,
"wires": []
},
{
"id": "d4b00cb7.a5a23",
"type": "template",
"z": "6312c0588348b2d4",
"name": "data",
"field": "payload",
"fieldType": "msg",
"format": "handlebars",
"syntax": "plain",
"template": "one\ntwo\nthree!",
"output": "str",
"x": 310,
"y": 220,
"wires": [
[
"99ae7806.1d6428"
]
]
}
]

View File

@ -1,113 +0,0 @@
[
{
"id": "84222b92.d65d18",
"type": "inject",
"z": "5132b95f037524f9",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "Hello, World!",
"payloadType": "str",
"x": 150,
"y": 220,
"wires": [
[
"b4b9f603.739598"
]
]
},
{
"id": "7b014430.dfd94c",
"type": "comment",
"z": "5132b95f037524f9",
"name": "Write string to a file, then read from the file",
"info": "Write file node can write string from a file.",
"x": 180,
"y": 140,
"wires": []
},
{
"id": "b4b9f603.739598",
"type": "file",
"z": "5132b95f037524f9",
"name": "",
"filename": "/tmp/hello.txt",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "none",
"x": 340,
"y": 220,
"wires": [
[
"6dc01cac.5c4bf4"
]
]
},
{
"id": "2587adb9.7e60f2",
"type": "debug",
"z": "5132b95f037524f9",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 730,
"y": 220,
"wires": []
},
{
"id": "6dc01cac.5c4bf4",
"type": "file in",
"z": "5132b95f037524f9",
"name": "",
"filename": "/tmp/hello.txt",
"format": "utf8",
"chunk": false,
"sendError": false,
"encoding": "none",
"x": 540,
"y": 220,
"wires": [
[
"2587adb9.7e60f2"
]
]
},
{
"id": "f4b4309a.3b78a",
"type": "comment",
"z": "5132b95f037524f9",
"name": "↑read result from file",
"info": "",
"x": 550,
"y": 260,
"wires": []
},
{
"id": "672d3693.3cabd8",
"type": "comment",
"z": "5132b95f037524f9",
"name": "↓write to /tmp/hello.txt",
"info": "",
"x": 360,
"y": 180,
"wires": []
}
]

View File

@ -1,118 +0,0 @@
[
{
"id": "704479e1.399388",
"type": "inject",
"z": "6312c0588348b2d4",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "filename",
"v": "/tmp/hello.txt",
"vt": "str"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "Hello, World!",
"payloadType": "str",
"x": 190,
"y": 260,
"wires": [
[
"402f3b7e.988014"
]
]
},
{
"id": "8e876a75.e9beb8",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "Write string to a file specied by filename property, the read from the file",
"info": "Write file node can target file using `filename` property.",
"x": 310,
"y": 180,
"wires": []
},
{
"id": "402f3b7e.988014",
"type": "file",
"z": "6312c0588348b2d4",
"name": "",
"filename": "",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "none",
"x": 350,
"y": 260,
"wires": [
[
"26e077d6.bbcd98"
]
]
},
{
"id": "97b6b6b2.a54b38",
"type": "debug",
"z": "6312c0588348b2d4",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 730,
"y": 260,
"wires": []
},
{
"id": "26e077d6.bbcd98",
"type": "file in",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"format": "utf8",
"chunk": false,
"sendError": false,
"encoding": "none",
"x": 540,
"y": 260,
"wires": [
[
"97b6b6b2.a54b38"
]
]
},
{
"id": "85062297.da79",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↑read result from file",
"info": "",
"x": 550,
"y": 300,
"wires": []
},
{
"id": "7316c4fc.b1dcdc",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↓write to file specified by filename property",
"info": "",
"x": 460,
"y": 220,
"wires": []
}
]

View File

@ -1,85 +0,0 @@
[
{
"id": "4ac00fb0.d5f52",
"type": "inject",
"z": "6312c0588348b2d4",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 180,
"y": 220,
"wires": [
[
"542cc2f4.92857c"
]
]
},
{
"id": "671f8295.0e6f6c",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "Delete a file",
"info": "Write file node can delete a file.",
"x": 130,
"y": 160,
"wires": []
},
{
"id": "542cc2f4.92857c",
"type": "file",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"appendNewline": true,
"createDir": false,
"overwriteFile": "delete",
"encoding": "none",
"x": 380,
"y": 220,
"wires": [
[
"a24da523.5babe8"
]
]
},
{
"id": "a24da523.5babe8",
"type": "debug",
"z": "6312c0588348b2d4",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 590,
"y": 220,
"wires": []
},
{
"id": "51157051.2f62",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↓delete a file",
"info": "",
"x": 350,
"y": 180,
"wires": []
}
]

View File

@ -1,113 +0,0 @@
[
{
"id": "e4ef1f5e.7cd82",
"type": "inject",
"z": "6312c0588348b2d4",
"name": "Base64 encoded string",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "8J+YgA==",
"payloadType": "str",
"x": 200,
"y": 220,
"wires": [
[
"72b37cc8.177054"
]
]
},
{
"id": "f5997af4.5a9298",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "Specify encoding of written data",
"info": "Write file node can specify encoding of data.",
"x": 170,
"y": 140,
"wires": []
},
{
"id": "72b37cc8.177054",
"type": "file",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "base64",
"x": 420,
"y": 220,
"wires": [
[
"2da33ec.f45cac2"
]
]
},
{
"id": "2e814354.278c8c",
"type": "debug",
"z": "6312c0588348b2d4",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 810,
"y": 220,
"wires": []
},
{
"id": "2da33ec.f45cac2",
"type": "file in",
"z": "6312c0588348b2d4",
"name": "",
"filename": "/tmp/hello.txt",
"format": "utf8",
"chunk": false,
"sendError": false,
"encoding": "none",
"x": 620,
"y": 220,
"wires": [
[
"2e814354.278c8c"
]
]
},
{
"id": "ec754c99.84bfd",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↓write string with base64 encoding",
"info": "",
"x": 480,
"y": 180,
"wires": []
},
{
"id": "3e6704ff.4ce25c",
"type": "comment",
"z": "6312c0588348b2d4",
"name": "↑read result from file",
"info": "",
"x": 630,
"y": 260,
"wires": []
}
]

View File

@ -582,7 +582,7 @@ class Flow {
reportingNode = node;
}
if (!muteStatusEvent) {
if (statusMessage.hasOwnProperty("text") && typeof(statusMessage.text !== "string")) {
if (statusMessage.hasOwnProperty("text") && typeof statusMessage.text !== "string") {
try {
statusMessage.text = statusMessage.text.toString();
}

View File

@ -1,539 +0,0 @@
var should = require("should");
var helper = require("node-red-node-test-helper");
var testNode = require("nr-test-utils").require("@node-red/nodes/core/function/rbe.js");
describe('rbe node', function() {
"use strict";
beforeEach(function(done) {
helper.startServer(done);
});
afterEach(function(done) {
helper.unload().then(function() {
helper.stopServer(done);
});
});
it("should be loaded with correct defaults", function(done) {
var flow = [{"id":"n1", "type":"rbe", "name":"rbe1", "wires":[[]]}];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
n1.should.have.property("name", "rbe1");
n1.should.have.property("func", "rbe");
n1.should.have.property("gap", "0");
done();
});
});
it('should only send output if payload changes - with multiple topics (rbe)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"rbe", gap:"0", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload", "a");
c+=1;
}
else if (c === 1) {
msg.should.have.a.property("payload", 2);
c+=1;
}
else if (c == 2) {
msg.should.have.a.property("payload");
msg.payload.should.have.a.property("b",1);
msg.payload.should.have.a.property("c",2);
c+=1;
}
else if (c == 3) {
msg.should.have.a.property("payload",true);
c+=1;
}
else if (c == 4) {
msg.should.have.a.property("payload",false);
c+=1;
}
else if (c == 5) {
msg.should.have.a.property("payload",true);
c+=1;
}
else if (c == 6) {
msg.should.have.a.property("topic","a");
msg.should.have.a.property("payload",1);
c+=1;
}
else if (c == 7) {
msg.should.have.a.property("topic","b");
msg.should.have.a.property("payload",1);
c+=1;
}
else {
c += 1;
msg.should.have.a.property("topic","c");
msg.should.have.a.property("payload",1);
done();
}
});
n1.emit("input", {payload:"a"});
n1.emit("input", {payload:"a"});
n1.emit("input", {payload:"a"});
n1.emit("input", {payload:2});
n1.emit("input", {payload:2});
n1.emit("input", {payload:{b:1,c:2}});
n1.emit("input", {payload:{c:2,b:1}});
n1.emit("input", {payload:{c:2,b:1}});
n1.emit("input", {payload:true});
n1.emit("input", {payload:false});
n1.emit("input", {payload:false});
n1.emit("input", {payload:true});
n1.emit("input", {topic:"a",payload:1});
n1.emit("input", {topic:"b",payload:1});
n1.emit("input", {topic:"b",payload:1});
n1.emit("input", {topic:"a",payload:1});
n1.emit("input", {topic:"c",payload:1});
});
});
it('should ignore multiple topics if told to (rbe)', function(done) {
var flow = [{id:"n1", type:"rbe", func:"rbe", gap:"0", septopics:false, wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload", "a");
c+=1;
}
else if (c === 1) {
msg.should.have.a.property("payload", 2);
c+=1;
}
else if (c == 2) {
msg.should.have.a.property("payload");
msg.payload.should.have.a.property("b",1);
msg.payload.should.have.a.property("c",2);
c+=1;
}
else if (c == 3) {
msg.should.have.a.property("payload",true);
c+=1;
}
else if (c == 4) {
msg.should.have.a.property("payload",false);
c+=1;
}
else if (c == 5) {
msg.should.have.a.property("payload",true);
c+=1;
}
else if (c == 6) {
msg.should.have.a.property("topic","a");
msg.should.have.a.property("payload",1);
c+=1;
}
else {
msg.should.have.a.property("topic","a");
msg.should.have.a.property("payload",2);
done();
}
});
n1.emit("input", {topic:"a",payload:"a"});
n1.emit("input", {topic:"b",payload:"a"});
n1.emit("input", {topic:"c",payload:"a"});
n1.emit("input", {topic:"a",payload:2});
n1.emit("input", {topic:"b",payload:2});
n1.emit("input", {payload:{b:1,c:2}});
n1.emit("input", {payload:{c:2,b:1}});
n1.emit("input", {payload:{c:2,b:1}});
n1.emit("input", {topic:"a",payload:true});
n1.emit("input", {topic:"b",payload:false});
n1.emit("input", {topic:"c",payload:false});
n1.emit("input", {topic:"d",payload:true});
n1.emit("input", {topic:"a",payload:1});
n1.emit("input", {topic:"b",payload:1});
n1.emit("input", {topic:"c",payload:1});
n1.emit("input", {topic:"d",payload:1});
n1.emit("input", {topic:"a",payload:2});
});
});
it('should only send output if another chosen property changes - foo (rbe)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"rbe", gap:"0", property:"foo", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("foo", "a");
c+=1;
}
else if (c === 1) {
msg.should.have.a.property("foo", "b");
c+=1;
}
else {
msg.should.have.a.property("foo");
msg.foo.should.have.a.property("b",1);
msg.foo.should.have.a.property("c",2);
done();
}
});
n1.emit("input", {foo:"a"});
n1.emit("input", {payload:"a"});
n1.emit("input", {foo:"a"});
n1.emit("input", {payload:"a"});
n1.emit("input", {foo:"a"});
n1.emit("input", {foo:"b"});
n1.emit("input", {foo:{b:1,c:2}});
n1.emit("input", {foo:{c:2,b:1}});
n1.emit("input", {payload:{c:2,b:1}});
});
});
it('should only send output if payload changes - ignoring first value (rbei)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"rbei", gap:"0", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload", "b");
msg.should.have.a.property("topic", "a");
c+=1;
}
else if (c === 1) {
msg.should.have.a.property("payload", "b");
msg.should.have.a.property("topic", "b");
c+=1;
}
else if (c === 2) {
msg.should.have.a.property("payload", "c");
msg.should.have.a.property("topic", "a");
c+=1;
}
else {
msg.should.have.a.property("payload", "c");
msg.should.have.a.property("topic", "b");
done();
}
});
n1.emit("input", {payload:"a", topic:"a"});
n1.emit("input", {payload:"a", topic:"b"});
n1.emit("input", {payload:"a", topic:"a"});
n1.emit("input", {payload:"b", topic:"a"});
n1.emit("input", {payload:"b", topic:"b"});
n1.emit("input", {payload:"c", topic:"a"});
n1.emit("input", {payload:"c", topic:"b"});
});
});
it('should send output if queue is reset (rbe)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"rbe", gap:"0", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload", "a");
c+=1;
}
else if (c === 1) {
msg.should.have.a.property("payload", "b");
c+=1;
}
else if (c === 2) {
msg.should.have.a.property("payload", "a");
c+=1;
}
else if (c === 3) {
msg.should.have.a.property("payload", "b");
c+=1;
}
else if (c === 4) {
msg.should.have.a.property("payload", "b");
c+=1;
}
else if (c === 5) {
msg.should.have.a.property("payload", "b");
c+=1;
}
else if (c === 6) {
msg.should.have.a.property("payload", "a");
c+=1;
}
else {
msg.should.have.a.property("payload", "c");
done();
}
});
n1.emit("input", {topic:"a", payload:"a"});
n1.emit("input", {topic:"a", payload:"a"});
n1.emit("input", {topic:"b", payload:"b"});
n1.emit("input", {reset:true}); // reset all
n1.emit("input", {topic:"a", payload:"a"});
n1.emit("input", {topic:"b", payload:"b"});
n1.emit("input", {topic:"b", payload:"b"});
n1.emit("input", {topic:"b", reset:""}); // reset b
n1.emit("input", {topic:"b", payload:"b"});
n1.emit("input", {topic:"a", payload:"a"});
n1.emit("input", {reset:""}); // reset all
n1.emit("input", {topic:"b", payload:"b"});
n1.emit("input", {topic:"a", payload:"a"});
n1.emit("input", {topic:"c"}); // don't reset a non topic
n1.emit("input", {topic:"b", payload:"b"});
n1.emit("input", {topic:"a", payload:"a"});
n1.emit("input", {topic:"c", payload:"c"});
});
});
it('should only send output if x away from original value (deadbandEq)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"deadbandEq", gap:"10", inout:"out", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
c = c + 1;
if (c === 1) {
msg.should.have.a.property("payload", 0);
}
else if (c === 2) {
msg.should.have.a.property("payload", 10);
}
else if (c == 3) {
msg.should.have.a.property("payload", 20);
done();
}
});
n1.emit("input", {payload:0});
n1.emit("input", {payload:2});
n1.emit("input", {payload:4});
n1.emit("input", {payload:6});
n1.emit("input", {payload:8});
n1.emit("input", {payload:10});
n1.emit("input", {payload:15});
n1.emit("input", {payload:20});
});
});
it('should only send output if more than x away from original value (deadband)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"deadband", gap:"10", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
c = c + 1;
//console.log(c,msg);
if (c === 1) {
msg.should.have.a.property("payload", 0);
}
else if (c === 2) {
msg.should.have.a.property("payload", 20);
}
else {
msg.should.have.a.property("payload", "5 deg");
done();
}
});
n1.emit("input", {payload:0});
n1.emit("input", {payload:2});
n1.emit("input", {payload:4});
n1.emit("input", {payload:"6 deg"});
n1.emit("input", {payload:8});
n1.emit("input", {payload:20});
n1.emit("input", {payload:15});
n1.emit("input", {payload:"5 deg"});
});
});
it('should only send output if more than x% away from original value (deadband)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"deadband", gap:"10%", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
c = c + 1;
if (c === 1) {
msg.should.have.a.property("payload", 100);
}
else if (c === 2) {
msg.should.have.a.property("payload", 111);
}
else if (c === 3) {
msg.should.have.a.property("payload", 135);
done();
}
});
n1.emit("input", {payload:100});
n1.emit("input", {payload:95});
n1.emit("input", {payload:105});
n1.emit("input", {payload:111});
n1.emit("input", {payload:120});
n1.emit("input", {payload:135});
});
});
it('should warn if no number found in deadband mode', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"deadband", gap:"10", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
c += 1;
});
setTimeout( function() {
c.should.equal(0);
helper.log().called.should.be.true;
var logEvents = helper.log().args.filter(function (evt) {
return evt[0].type == "rbe";
});
logEvents.should.have.length(1);
var msg = logEvents[0][0];
msg.should.have.property('level', helper.log().WARN);
msg.should.have.property('id', 'n1');
msg.should.have.property('type', 'rbe');
msg.should.have.property('msg', 'rbe.warn.nonumber');
done();
},50);
n1.emit("input", {payload:"banana"});
});
});
it('should not send output if x away or greater from original value (narrowbandEq)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"narrowbandEq", gap:"10", inout:"out", start:"1", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
c = c + 1;
//console.log(c,msg);
if (c === 1) {
msg.should.have.a.property("payload", 0);
}
else if (c === 2) {
msg.should.have.a.property("payload", 5);
}
else if (c === 3) {
msg.should.have.a.property("payload", 10);
done();
}
});
n1.emit("input", {payload:100});
n1.emit("input", {payload:0});
n1.emit("input", {payload:10});
n1.emit("input", {payload:5});
n1.emit("input", {payload:15});
n1.emit("input", {payload:10});
n1.emit("input", {payload:20});
n1.emit("input", {payload:25});
});
});
it('should not send output if more than x away from original value (narrowband)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"narrowband", gap:"10", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload", 0);
}
else if (c === 1) {
msg.should.have.a.property("payload","6 deg");
}
else {
msg.should.have.a.property("payload", "5 deg");
done();
}
c += 1;
});
n1.emit("input", {payload:0});
n1.emit("input", {payload:20});
n1.emit("input", {payload:40});
n1.emit("input", {payload:"6 deg"});
n1.emit("input", {payload:18});
n1.emit("input", {payload:20});
n1.emit("input", {payload:50});
n1.emit("input", {payload:"5 deg"});
});
});
it('should send output if gap is 0 and input doesnt change (narrowband)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"narrowband", gap:"0", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload", 1);
}
else if (c === 4) {
msg.should.have.a.property("payload",1);
done();
}
c += 1;
});
n1.emit("input", {payload:1});
n1.emit("input", {payload:1});
n1.emit("input", {payload:1});
n1.emit("input", {payload:1});
n1.emit("input", {payload:0});
n1.emit("input", {payload:1});
});
});
it('should not send output if more than x away from original value (narrowband in step mode)', function(done) {
var flow = [{"id":"n1", "type":"rbe", func:"narrowband", gap:"10", inout:"in", start:"500", wires:[["n2"]] },
{id:"n2", type:"helper"} ];
helper.load(testNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
var c = 0;
n2.on("input", function(msg) {
if (c === 0) {
msg.should.have.a.property("payload", 55);
}
else if (c === 1) {
msg.should.have.a.property("payload", 205);
done();
}
c += 1;
});
n1.emit("input", {payload:50});
n1.emit("input", {payload:55});
n1.emit("input", {payload:200});
n1.emit("input", {payload:205});
});
});
});

View File

@ -0,0 +1,610 @@
/* These tests are only supposed to be executed at development time (for now)*/
"use strict";
const should = require("should");
const helper = require("node-red-node-test-helper");
const mqttNodes = require("nr-test-utils").require("@node-red/nodes/core/network/10-mqtt.js");
const BROKER_HOST = process.env.MQTT_BROKER_SERVER || "localhost";
const BROKER_PORT = process.env.MQTT_BROKER_PORT || 1883;
//By default, MQTT tests are disabled. Set ENV VAR NR_MQTT_TESTS to "1" or "true" to enable
const skipTests = process.env.NR_MQTT_TESTS != "true" && process.env.NR_MQTT_TESTS != "1";
describe('MQTT Nodes', function () {
before(function (done) {
helper.startServer(done);
});
after(function (done) {
helper.stopServer(done);
});
afterEach(function () {
try {
helper.unload();
} catch (error) { }
});
it('should be loaded and have default values', function (done) {
this.timeout = 2000;
const { flow, nodes } = buildBasicMQTTSendRecvFlow({ id: "mqtt.broker", name: "mqtt_broker", autoConnect: false }, { id: "mqtt.in", topic: "in_topic" }, { id: "mqtt.out", topic: "out_topic" });
helper.load(mqttNodes, flow, function () {
try {
const mqttIn = helper.getNode("mqtt.in");
const mqttOut = helper.getNode("mqtt.out");
const mqttBroker = helper.getNode("mqtt.broker");
should(mqttIn).be.type("object", "mqtt in node should be an object")
mqttIn.should.have.property('broker', nodes.mqtt_broker.id); //should be the id of the broker node
mqttIn.should.have.property('datatype', 'utf8'); //default: 'utf8'
mqttIn.should.have.property('isDynamic', false); //default: false
mqttIn.should.have.property('inputs', 0); //default: 0
mqttIn.should.have.property('qos', 2); //default: 2
mqttIn.should.have.property('topic', "in_topic");
mqttIn.should.have.property('wires', [["helper.node"]]);
should(mqttOut).be.type("object", "mqtt out node should be an object")
mqttOut.should.have.property('broker', nodes.mqtt_broker.id); //should be the id of the broker node
mqttOut.should.have.property('topic', "out_topic");
should(mqttBroker).be.type("object", "mqtt broker node should be an object")
mqttBroker.should.have.property('broker', BROKER_HOST);
mqttBroker.should.have.property('port', BROKER_PORT);
mqttBroker.should.have.property('brokerurl');
// mqttBroker.should.have.property('autoUnsubscribe', true);//default: true
mqttBroker.should.have.property('autoConnect', false);//Set "autoConnect:false" in brokerOptions
mqttBroker.should.have.property('options');
mqttBroker.options.should.have.property('clean', true);
mqttBroker.options.should.have.property('clientId');
mqttBroker.options.clientId.should.containEql('nodered_');
mqttBroker.options.should.have.property('keepalive').type("number");
mqttBroker.options.should.have.property('reconnectPeriod').type("number");
done();
} catch (error) {
done(error)
}
});
});
if (skipTests) {
it('skipping MQTT tests. Set env var "NR_MQTT_TESTS=true" to enable. Requires a v5 capable broker running on localhost:1883.', function (done) {
done();
});
}
/** Conditional test runner (only run if skipTests=false) */
function itConditional(title, test) {
return !skipTests ? it(title, test) : it.skip(title, test);
}
//#region ################### BASIC TESTS ################### #//
itConditional('basic send and receive tests', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
const options = {}
options.sendMsg = {
topic: nextTopic(),
payload: "hello",
qos: 0
}
options.expectMsg = Object.assign({}, options.sendMsg);
testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, options, { done: done });
});
itConditional('should send JSON and receive string (auto)', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
const options = {}
options.sendMsg = {
topic: nextTopic(),
payload: '{"prop":"value1", "num":1}',
qos: 1
}
options.expectMsg = Object.assign({}, options.sendMsg);
testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, options, { done: done });
})
itConditional('should send JSON and receive string (utf8)', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
const options = {}
options.sendMsg = {
topic: nextTopic(),
payload: '{"prop":"value2", "num":2}',
qos: 2
}
options.expectMsg = Object.assign({}, options.sendMsg);
testSendRecv({}, { datatype: "utf8", topicType: "static" }, {}, options, { done: done });
});
itConditional('should send JSON and receive Object (json)', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
const options = {}
options.sendMsg = {
topic: nextTopic(),
payload: '{"prop":"value3", "num":3}'// send a string ...
}
options.expectMsg = Object.assign({}, options.sendMsg, { payload: { "prop": "value3", "num": 3 } });//expect an object
testSendRecv({}, { datatype: "json", topicType: "static" }, {}, options, { done: done });
});
itConditional('should send String and receive Buffer (buffer)', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
const options = {}
options.sendMsg = {
topic: nextTopic(),
payload: "a b c" //send string ...
}
options.expectMsg = Object.assign({}, options.sendMsg, { payload: Buffer.from(options.sendMsg.payload) });//expect Buffer.from(msg.payload)
testSendRecv({}, { datatype: "buffer", topicType: "static" }, {}, options, { done: done });
});
itConditional('should send utf8 Buffer and receive String (auto)', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
const options = {}
options.sendMsg = {
topic: nextTopic(),
payload: Buffer.from([0x78, 0x20, 0x79, 0x20, 0x7a]) // "x y z"
}
options.expectMsg = Object.assign({}, options.sendMsg, { payload: "x y z" });//set expected payload to "x y z"
testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, options, { done: done });
});
itConditional('should send non utf8 Buffer and receive Buffer (auto)', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
const options = {}
const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null }
options.sendMsg = {
topic: nextTopic(),
payload: Buffer.from([0xC0, 0xC1, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF]) //non valid UTF8
}
options.expectMsg = Object.assign({}, options.sendMsg);
testSendRecv({}, { datatype: "auto", topicType: "static" }, {}, options, hooks);
});
itConditional('should send/receive all v5 flags and settings', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
const t = nextTopic();
const options = {}
const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null }
options.sendMsg = {
topic: t + "/command", payload: Buffer.from("v5"), qos: 1, retain: true,
responseTopic: t + "/response",
userProperties: { prop1: "val1" },
contentType: "application/json",
correlationData: Buffer.from([1, 2, 3]),
payloadFormatIndicator: true,
messageExpiryInterval: 2000,
}
options.expectMsg = Object.assign({}, options.sendMsg);
options.expectMsg.payload = options.expectMsg.payload.toString(); //auto mode + payloadFormatIndicator should make a string
delete options.expectMsg.payloadFormatIndicator; //Seems mqtt.js only publishes payloadFormatIndicator the will msg
const inOptions = {
datatype: "auto", topicType: "static",
qos: 1, nl: false, rap: true, rh: 1
}
testSendRecv({ protocolVersion: 5 }, inOptions, {}, options, hooks);
});
itConditional('should subscribe dynamically via action', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
const options = {}
const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null }
options.sendMsg = {
topic: nextTopic(), payload: "abc"
}
options.expectMsg = Object.assign({}, options.sendMsg);
testSendRecv({ protocolVersion: 5 }, { datatype: "utf8", topicType: "dynamic" }, {}, options, hooks);
});
//#endregion BASIC TESTS
//#region ################### ADVANCED TESTS ################### #//
itConditional('should connect via "connect" action', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
const options = {}
const hooks = { done: null, beforeLoad: null, afterLoad: null, afterConnect: null }
hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => {
mqttBroker.should.have.property("autoConnect", false);
mqttBroker.should.have.property("connecting", false);//should not attempt to connect (autoConnect:false)
mqttIn.receive({ "action": "connect" }); //now request connect action
return true; //handled
}
hooks.afterConnect = (helperNode, mqttBroker, mqttIn, mqttOut) => {
done();//if we got here, it connected :)
return true;
}
testSendRecv({ protocolVersion: 5, autoConnect: false }, { datatype: "utf8", topicType: "dynamic" }, {}, options, hooks);
});
itConditional('should disconnect via "disconnect" action', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
const options = {}
const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null }
hooks.beforeLoad = (flow) => { //add a status node pointed at MQTT Out node (to watch for connection status change)
flow.push({ "id": "status.node", "type": "status", "name": "status_node", "scope": ["mqtt.out"], "wires": [["helper.node"]] });//add status node to watch mqtt_out
}
hooks.afterLoad = (helperNode, mqttBroker, mqttIn, mqttOut) => {
mqttBroker.should.have.property("autoConnect", true);
mqttBroker.should.have.property("connecting", true);//should be trying to connect (autoConnect:true)
return true; //handled
}
hooks.afterConnect = (helperNode, mqttBroker, mqttIn, mqttOut) => {
//connected - now add the "on" handler then send "disconnect" action
helperNode.on("input", function (msg) {
try {
msg.should.have.property("status");
msg.status.should.have.property("text");
msg.status.text.should.containEql('disconnect');
done(); //it disconnected - yey!
} catch (error) {
done(error)
}
})
mqttOut.receive({ "action": "disconnect" });
return true; //handed
}
testSendRecv({ protocolVersion: 5 }, null, {}, options, hooks);
});
itConditional('should publish birth message', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
const baseTopic = nextTopic();
const brokerOptions = {
protocolVersion: 4,
birthTopic: baseTopic + "/birth",
birthPayload: "broker connected",
birthQos: 2,
}
const options = {};
const hooks = { done: done, beforeLoad: null, afterLoad: null, afterConnect: null };
options.expectMsg = {
topic: brokerOptions.birthTopic,
payload: brokerOptions.birthPayload,
qos: brokerOptions.birthQos
};
testSendRecv(brokerOptions, { topic: brokerOptions.birthTopic }, {}, options, hooks);
});
itConditional('should publish close message', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
const baseTopic = nextTopic();
const broker1Options = { id: "mqtt.broker1" }//Broker 1 - stays connected to receive the close message
const broker2Options = { id: "mqtt.broker2", closeTopic: baseTopic + "/close", closePayload: '{"msg":"close"}', closeQos: 1, }//Broker 2 - connects to same broker but has a LWT message.
const { flow } = buildBasicMQTTSendRecvFlow(broker1Options, { broker: broker1Options.id, topic: broker2Options.closeTopic, datatype: "json" }, { broker: broker2Options.id })
flow.push(buildMQTTBrokerNode(broker2Options.id, broker2Options.name, BROKER_HOST, BROKER_PORT, broker2Options)); //add second broker
helper.load(mqttNodes, flow, function () {
const helperNode = helper.getNode("helper.node");
const mqttOut = helper.getNode("mqtt.out");
const mqttBroker1 = helper.getNode("mqtt.broker1");
const mqttBroker2 = helper.getNode("mqtt.broker2");
waitBrokerConnect([mqttBroker1, mqttBroker2], function connected() {
//connected - add the on handler and call to disconnect
helperNode.on("input", function (msg) {
try {
msg.should.have.property("topic", broker2Options.closeTopic);
msg.should.have.property('payload', JSON.parse(broker2Options.closePayload));
msg.should.have.property('qos', broker2Options.closeQos);
done();
} catch (error) {
done(error)
}
})
mqttOut.receive({ "action": "disconnect" });//close broker2
})
});
});
itConditional('should publish will message', function (done) {
if (skipTests) { return this.skip() }
this.timeout = 2000;
const baseTopic = nextTopic();
const broker1Options = { id: "mqtt.broker1" }//Broker 1 - stays connected to receive the will message
const broker2Options = { id: "mqtt.broker2", willTopic: baseTopic + "/will", willPayload: '{"msg":"will"}', willQos: 2, }//Broker 2 - connects to same broker but has a LWT message.
const { flow } = buildBasicMQTTSendRecvFlow(broker1Options, { broker: broker1Options.id, topic: broker2Options.willTopic, datatype: "utf8" }, { broker: broker2Options.id })
flow.push(buildMQTTBrokerNode(broker2Options.id, broker2Options.name, BROKER_HOST, BROKER_PORT, broker2Options)); //add second broker
helper.load(mqttNodes, flow, function () {
const helperNode = helper.getNode("helper.node");
const mqttBroker1 = helper.getNode("mqtt.broker1");
const mqttBroker2 = helper.getNode("mqtt.broker2");
waitBrokerConnect([mqttBroker1, mqttBroker2], function connected() {
//connected - add the on handler and call to disconnect
helperNode.on("input", function (msg) {
try {
msg.should.have.property("topic", broker2Options.willTopic);
msg.should.have.property('payload', broker2Options.willPayload);
msg.should.have.property('qos', broker2Options.willQos);
done();
} catch (error) {
done(error)
}
});
mqttBroker2.client.end(true); //force closure
})
});
});
itConditional('should publish will message with V5 properties', function (done) {
if (skipTests) { return this.skip() }
// return this.skip(); //Issue receiving v5 props on will msg. Issue raised here: https://github.com/mqttjs/MQTT.js/issues/1455
this.timeout = 2000;
const baseTopic = nextTopic();
//Broker 1 - stays connected to receive the will message when broker 2 is killed
const broker1Options = { id: "mqtt.broker1", name: "mqtt_broker1", protocolVersion: 5, datatype: "utf8" }
//Broker 2 - connects to same broker but has a LWT message. Broker 2 gets killed shortly after connection so that the will message is sent from broker
const broker2Options = {
id: "mqtt.broker2", name: "mqtt_broker2", protocolVersion: 5,
willTopic: baseTopic + "/will",
willPayload: '{"msg":"will"}',
willQos: 2,
willMsg: {
contentType: 'application/json',
userProps: { "will": "value" },
respTopic: baseTopic + "/resp",
correl: Buffer.from("abc"),
expiry: 2000,
payloadFormatIndicator: true
}
}
const expectMsg = {
topic: broker2Options.willTopic,
payload: broker2Options.willPayload,
qos: broker2Options.willQos,
contentType: broker2Options.willMsg.contentType,
userProperties: broker2Options.willMsg.userProps,
responseTopic: broker2Options.willMsg.respTopic,
correlationData: broker2Options.willMsg.correl,
messageExpiryInterval: broker2Options.willMsg.expiry,
// payloadFormatIndicator: broker2Options.willMsg.payloadFormatIndicator,
};
const { flow, nodes } = buildBasicMQTTSendRecvFlow(broker1Options, { broker: broker1Options.id, topic: broker2Options.willTopic, datatype: "utf8" }, { broker: broker2Options.id })
flow.push(buildMQTTBrokerNode(broker2Options.id, broker2Options.name, nodes.mqtt_broker1.broker, nodes.mqtt_broker1.port, broker2Options)) //add second broker with will msg set
helper.load(mqttNodes, flow, function () {
const helperNode = helper.getNode("helper.node");
const mqttBroker1 = helper.getNode("mqtt.broker1");
const mqttBroker2 = helper.getNode("mqtt.broker2");
waitBrokerConnect([mqttBroker1, mqttBroker2], function connected() {
//connected - add the on handler and call to disconnect
helperNode.on("input", function (msg) {
try {
compareMsgToExpected(msg, expectMsg);
done();
} catch (error) {
done(error)
}
});
mqttBroker2.client.end(true); //force closure
})
});
});
//#endregion ADVANCED TESTS
});
//#region ################### HELPERS ################### #//
/**
* A basic unit test that builds a flow containing 1 broker, 1 mqtt-in, one mqtt-out and a helper.
* It performs the following steps: builds flow, loads flow, waits for connection, sends `sendMsg`,
* waits for msg then compares `sendMsg` to `expectMsg`, and finally calls `done`
* @param {object} brokerOptions anything that can be set in an MQTTBrokerNode (e.g. id, name, url, broker, server, port, protocolVersion, ...)
* @param {object} inNodeOptions anything that can be set in an MQTTInNode (e.g. id, name, broker, topic, rh, nl, rap, ... )
* @param {object} outNodeOptions anything that can be set in an MQTTOutNode (e.g. id, name, broker, ...)
* @param {object} options an object for passing in test properties like `sendMsg` and `expectMsg`
* @param {object} hooks an object containing hook functions...
* * [fn] `done()` - the tests done function. If excluded, an error will be thrown upon test error
* * [fn] `beforeLoad(flow)` - provides opportunity to adjust the flow JSON before loading into runtime
* * [fn] `afterLoad(helperNode, mqttBroker, mqttIn, mqttOut)` - called before connection attempt
* * [fn] `afterConnect(helperNode, mqttBroker, mqttIn, mqttOut)` - called before connection attempt
*/
function testSendRecv(brokerOptions, inNodeOptions, outNodeOptions, options, hooks) {
options = options || {};
brokerOptions = brokerOptions || {};
inNodeOptions = inNodeOptions || {};
outNodeOptions = outNodeOptions || {};
const sendMsg = options.sendMsg || {};
sendMsg.topic = sendMsg.topic || nextTopic();
const expectMsg = options.expectMsg || Object.assign({}, sendMsg);
expectMsg.payload = inNodeOptions.payload === undefined ? expectMsg.payload : inNodeOptions.payload;
if (inNodeOptions.topicType != "dynamic") {
inNodeOptions.topic = inNodeOptions.topic || sendMsg.topic;
}
const { flow, nodes } = buildBasicMQTTSendRecvFlow(brokerOptions, inNodeOptions, outNodeOptions);
if (hooks.beforeLoad) { hooks.beforeLoad(flow) }
helper.load(mqttNodes, flow, function () {
try {
const helperNode = helper.getNode("helper.node");
const mqttBroker = helper.getNode(brokerOptions.id);
const mqttIn = helper.getNode(nodes.mqtt_in.id);
const mqttOut = helper.getNode(nodes.mqtt_out.id);
let afterLoadHandled = false;
if (hooks.afterLoad) {
afterLoadHandled = hooks.afterLoad(helperNode, mqttBroker, mqttIn, mqttOut)
}
if (!afterLoadHandled) {
helperNode.on("input", function (msg) {
try {
compareMsgToExpected(msg, expectMsg);
if (hooks.done) { hooks.done(); }
} catch (err) {
if (hooks.done) { hooks.done(err); }
else { throw err; }
}
});
}
waitBrokerConnect(mqttBroker, function () {
//finally, connected!
if (hooks.afterConnect) {
let handled = hooks.afterConnect(helperNode, mqttBroker, mqttIn, mqttOut);
if (handled) { return }
}
if (mqttIn.isDynamic) {
mqttIn.receive({ "action": "subscribe", "topic": sendMsg.topic })
}
mqttOut.receive(sendMsg);
})
} catch (err) {
if (hooks.done) { hooks.done(err); }
else { throw err; }
}
});
}
/**
* Builds a flow containing 2 parts.
* * 1: MQTT Out node (with broker configured).
* * 2: MQTT In node (with broker configured) --> helper node `id:helper.node`
*/
function buildBasicMQTTSendRecvFlow(brokerOptions, inOptions, outOptions) {
brokerOptions = brokerOptions || {};
brokerOptions.broker = brokerOptions.broker || BROKER_HOST;
brokerOptions.port = brokerOptions.port || BROKER_PORT;
brokerOptions.autoConnect = String(brokerOptions.autoConnect) == "false" ? false : true;
const broker = buildMQTTBrokerNode(brokerOptions.id, brokerOptions.name, brokerOptions.broker, brokerOptions.port, brokerOptions);
const inNode = buildMQTTInNode(inOptions.id, inOptions.name, inOptions.broker || broker.id, inOptions.topic, inOptions, ["helper.node"]);
const outNode = buildMQTTOutNode(outOptions.id, outOptions.name, outOptions.broker || broker.id, outOptions.topic, outOptions);
const helper = buildNode("helper", "helper.node", "helper_node", {});
return {
nodes: {
[broker.name]: broker,
[inNode.name]: inNode,
[outNode.name]: outNode,
[helper.name]: helper,
},
flow: [broker, inNode, outNode, helper]
}
}
function buildMQTTBrokerNode(id, name, brokerHost, brokerPort, options) {
// url,broker,port,clientid,autoConnect,usetls,usews,verifyservercert,compatmode,protocolVersion,keepalive,
//cleansession,sessionExpiry,topicAliasMaximum,maximumPacketSize,receiveMaximum,userProperties,userPropertiesType,autoUnsubscribe
options = options || {};
const node = buildNode("mqtt-broker", id || "mqtt.broker", name || "mqtt_broker", options);
node.url = options.url;
node.broker = brokerHost || options.broker || BROKER_HOST;
node.port = brokerPort || options.port || BROKER_PORT;
node.clientid = options.clientid || "";
node.cleansession = String(options.cleansession) == "false" ? false : true;
node.autoUnsubscribe = String(options.autoUnsubscribe) == "false" ? false : true;
node.autoConnect = String(options.autoConnect) == "false" ? false : true;
if (options.birthTopic) {
node.birthTopic = options.birthTopic;
node.birthQos = options.birthQos || "0";
node.birthPayload = options.birthPayload || "";
}
if (options.closeTopic) {
node.closeTopic = options.closeTopic;
node.closeQos = options.closeQos || "0";
node.closePayload = options.closePayload || "";
}
if (options.willTopic) {
node.willTopic = options.willTopic;
node.willQos = options.willQos || "0";
node.willPayload = options.willPayload || "";
}
updateNodeOptions(options, node);
return node;
}
function buildMQTTInNode(id, name, brokerId, topic, options, wires) {
//{ "id": "mqtt.in", "type": "mqtt in", "name": "mqtt_in", "topic": "test/in", "qos": "2", "datatype": "auto", "broker": "mqtt.broker", "nl": false, "rap": true, "rh": 0, "inputs": 0, "wires": [["mqtt.out"]] }
options = options || {};
options.broker = options.broker || "mqtt.broker";
const node = buildNode("mqtt in", id || "mqtt.in", name || "mqtt_in", options);
node.topic = topic || "";
node.broker = brokerId;
node.topicType = options.topicType == "dynamic" ? "dynamic" : "static",
node.inputs = options.topicType == "dynamic" ? 1 : 0,
updateNodeOptions(node, options, wires);
return node;
}
function buildMQTTOutNode(id, name, brokerId, topic, options) {
//{ "id": "mqtt.out", "type": "mqtt out", "name": "mqtt_out", "topic": "test/out", "qos": "", "retain": "", "respTopic": "", "contentType": "", "userProps": "", "correl": "", "expiry": "", "broker": brokerId, "wires": [] },
options = options || {};
options.broker = options.broker || "mqtt.broker";
const node = buildNode("mqtt out", id || "mqtt.out", name || "mqtt_out", options);
node.topic = topic || "";
node.broker = brokerId;
updateNodeOptions(node, options, null);
return node;
}
function buildNode(type, id, name, options, wires) {
//{ "id": "mqtt.in", "type": "mqtt in", "name": "mqtt_in", "topic": "test/in", "qos": "2", "datatype": "auto", "broker": "mqtt.broker", "nl": false, "rap": true, "rh": 0, "inputs": 0, "wires": [["mqtt.out"]] }
options = options || {};
const node = {
"id": id || (type.replace(/[\W]/g, ".")),
"type": type,
"name": name || (type.replace(/[\W]/g, "_")),
"wires": []
}
if (node.id.indexOf(".") == -1) { node.is += ".node" }
updateNodeOptions(node, options, wires);
return node;
}
function updateNodeOptions(node, options, wires) {
let keys = Object.keys(options);
for (let index = 0; index < keys.length; index++) {
const key = keys[index];
const val = options[key];
if (node[key] === undefined) {
node[key] = val;
}
}
if (wires && Array.isArray(wires)) {
node.wires[0] = [...wires];
}
}
function compareMsgToExpected(msg, expectMsg) {
msg.should.have.property("topic", expectMsg.topic);
msg.should.have.property("payload", expectMsg.payload);
if (hasProperty(expectMsg, "retain")) { msg.retain.should.eql(expectMsg.retain); }
if (hasProperty(expectMsg, "qos")) {
msg.qos.should.eql(expectMsg.qos);
} else {
msg.qos.should.eql(0);
}
if (hasProperty(expectMsg, "userProperties")) { msg.should.have.property("userProperties", expectMsg.userProperties); }
if (hasProperty(expectMsg, "contentType")) { msg.should.have.property("contentType", expectMsg.contentType); }
if (hasProperty(expectMsg, "correlationData")) { msg.should.have.property("correlationData", expectMsg.correlationData); }
if (hasProperty(expectMsg, "responseTopic")) { msg.should.have.property("responseTopic", expectMsg.responseTopic); }
if (hasProperty(expectMsg, "payloadFormatIndicator")) { msg.should.have.property("payloadFormatIndicator", expectMsg.payloadFormatIndicator); }
if (hasProperty(expectMsg, "messageExpiryInterval")) { msg.should.have.property("messageExpiryInterval", expectMsg.messageExpiryInterval); }
}
function waitBrokerConnect(broker, callback, timeLimit) {
timeLimit = timeLimit || 2000;
const brokers = Array.isArray(broker) ? broker : [broker];
wait();
function wait() {
if (brokers.every(e => e.connected == true)) {
callback(); //yey - connected!
} else {
timeLimit = timeLimit - 15;
if (timeLimit <= 0) {
throw new Error("Timeout waiting broker connect")
}
setTimeout(wait, 15);
return;
}
}
}
function hasProperty(obj, propName) {
return Object.prototype.hasOwnProperty.call(obj, propName);
}
const base_topic = "nr" + Date.now().toString() + "/";
let topicNo = 0;
function nextTopic(topic) {
topicNo++;
if (!topic) { topic = "unittest" }
if (topic.startsWith("/")) { topic = topic.substring(1); }
if (topic.startsWith(base_topic)) { return topic + String(topicNo) }
return (base_topic + topic + String(topicNo));
}
//#endregion HELPERS

View File

@ -1,592 +0,0 @@
/**
* 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 ws = require("ws");
var should = require("should");
var helper = require("node-red-node-test-helper");
var websocketNode = require("nr-test-utils").require("@node-red/nodes/core/network/22-websocket.js");
var sockets = [];
function getWsUrl(path) {
return helper.url().replace(/http/, "ws") + path;
}
function createClient(listenerid) {
return new Promise(function(resolve, reject) {
var node = helper.getNode(listenerid);
var url = getWsUrl(node.path);
var sock = new ws(url);
sockets.push(sock);
sock.on("open", function() {
resolve(sock);
});
sock.on("error", function(err) {
reject(err);
});
});
}
function closeAll() {
for (var i = 0; i < sockets.length; i++) {
sockets[i].close();
}
sockets = [];
}
function getSocket(listenerid) {
var node = helper.getNode(listenerid);
return node.server;
}
describe('websocket Node', function() {
before(function(done) {
helper.startServer(done);
});
after(function(done) {
helper.stopServer(done);
});
afterEach(function() {
closeAll();
helper.unload();
});
describe('websocket-listener', function() {
it('should load', function(done) {
var flow = [{ id: "n1", type: "websocket-listener", path: "/ws" }];
helper.load(websocketNode, flow, function() {
helper.getNode("n1").should.have.property("path", "/ws");
done();
});
});
it('should be server', function(done) {
var flow = [{ id: "n1", type: "websocket-listener", path: "/ws" }];
helper.load(websocketNode, flow, function() {
helper.getNode("n1").should.have.property('isServer', true);
done();
});
});
it('should handle wholemsg property', function(done) {
var flow = [
{ id: "n1", type: "websocket-listener", path: "/ws" },
{ id: "n2", type: "websocket-listener", path: "/ws2", wholemsg: "true" }];
helper.load(websocketNode, flow, function() {
helper.getNode("n1").should.have.property("wholemsg", false);
helper.getNode("n2").should.have.property("wholemsg", true);
done();
});
});
it('should create socket', function(done) {
var flow = [
{ id: "n1", type: "websocket-listener", path: "/ws" },
{ id: "n2", type: "websocket in", server: "n1" }];
helper.load(websocketNode, flow, function() {
createClient("n1").then(function(sock) {
done();
}).catch(function(err) {
done(err);
});
});
});
it('should close socket on delete', function(done) {
var flow = [{ id: "n1", type: "websocket-listener", path: "/ws" }];
helper.load(websocketNode, flow, function() {
createClient("n1").then(function(sock) {
sock.on("close", function(code, msg) {
done();
});
helper.clearFlows();
}).catch(function(err) {
done(err);
});
});
});
it('should receive data', function(done) {
var flow = [
{ id: "n1", type: "websocket-listener", path: "/ws" },
{ id: "n2", type: "websocket in", server: "n1", wires: [["n3"]] },
{ id: "n3", type: "helper" }];
helper.load(websocketNode, flow, function() {
createClient("n1").then(function(sock) {
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("payload", "hello");
done();
});
sock.send("hello");
}).catch(function(err) {
done(err);
});
});
});
it('should receive wholemsg', function(done) {
var flow = [
{ id: "n1", type: "websocket-listener", path: "/ws", wholemsg: "true" },
{ id: "n2", type: "websocket in", server: "n1", wires: [["n3"]] },
{ id: "n3", type: "helper" }];
helper.load(websocketNode, flow, function() {
createClient("n1").then(function(sock) {
sock.send('{"text":"hello"}');
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("text", "hello");
done();
});
}).catch(function(err) {
done(err);
});
});
});
it('should receive wholemsg when data not JSON', function(done) {
var flow = [
{ id: "n1", type: "websocket-listener", path: "/ws", wholemsg: "true" },
{ id: "n2", type: "websocket in", server: "n1", wires: [["n3"]] },
{ id: "n3", type: "helper" }];
helper.load(websocketNode, flow, function() {
createClient("n1").then(function(sock) {
sock.send('hello');
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("payload", "hello");
done();
});
}).catch(function(err) {
done(err);
});
});
});
it('should receive wholemsg when data not object', function(done) {
var flow = [
{ id: "n1", type: "websocket-listener", path: "/ws", wholemsg: "true" },
{ id: "n2", type: "websocket in", server: "n1", wires: [["n3"]] },
{ id: "n3", type: "helper" }];
helper.load(websocketNode, flow, function() {
createClient("n1").then(function(sock) {
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("payload", 123);
done();
});
sock.send(123);
}).catch(function(err) {
done(err);
});
});
});
it('should send', function(done) {
var flow = [
{ id: "n1", type: "websocket-listener", path: "/ws" },
{ id: "n2", type: "helper", wires: [["n3"]] },
{ id: "n3", type: "websocket out", server: "n1" }];
helper.load(websocketNode, flow, function() {
createClient("n1").then(function(sock) {
sock.on("message", function(msg, flags) {
msg.should.equal("hello");
done();
});
helper.getNode("n2").send({
payload: "hello"
});
}).catch(function(err) {
done(err);
});
});
});
it('should send wholemsg', function(done) {
var flow = [
{ id: "n1", type: "websocket-listener", path: "/ws", wholemsg: "true" },
{ id: "n2", type: "websocket out", server: "n1" },
{ id: "n3", type: "helper", wires: [["n2"]] }];
helper.load(websocketNode, flow, function() {
createClient("n1").then(function(sock) {
sock.on("message", function(msg, flags) {
JSON.parse(msg).should.have.property("text", "hello");
done();
});
helper.getNode("n3").send({
text: "hello"
});
}).catch(function(err) {
done(err);
});
});
});
it('should do nothing if no payload', function(done) {
var flow = [
{ id: "n1", type: "websocket-listener", path: "/ws" },
{ id: "n2", type: "helper", wires: [["n3"]] },
{ id: "n3", type: "websocket out", server: "n1" }];
helper.load(websocketNode, flow, function() {
createClient("n1").then(function(sock) {
setTimeout(function() {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "file";
});
logEvents.should.have.length(0);
done();
},100);
helper.getNode("n2").send({topic: "hello"});
}).catch(function(err) {
done(err);
});
});
});
it('should echo', function(done) {
var flow = [
{ id: "n1", type: "websocket-listener", path: "/ws" },
{ id: "n2", type: "websocket in", server: "n1", wires: [["n3"]] },
{ id: "n3", type: "websocket out", server: "n1" }];
helper.load(websocketNode, flow, function() {
createClient("n1").then(function(sock) {
sock.on("message", function(msg, flags) {
msg.should.equal("hello");
done();
});
sock.send("hello");
}).catch(function(err) {
done(err);
});
});
});
it('should echo wholemsg', function(done) {
var flow = [
{ id: "n1", type: "websocket-listener", path: "/ws", wholemsg: "true" },
{ id: "n2", type: "websocket in", server: "n1", wires: [["n3"]] },
{ id: "n3", type: "websocket out", server: "n1" }];
helper.load(websocketNode, flow, function() {
createClient("n1").then(function(sock) {
sock.on("message", function(msg, flags) {
JSON.parse(msg).should.have.property("text", "hello");
done();
});
sock.send('{"text":"hello"}');
}).catch(function(err) {
done(err);
});
});
});
it('should broadcast', function(done) {
var flow = [
{ id: "n1", type: "websocket-listener", path: "/ws" },
{ id: "n2", type: "websocket out", server: "n1" },
{ id: "n3", type: "helper", wires: [["n2"]] }];
helper.load(websocketNode, flow, function() {
Promise.all([createClient("n1"), createClient("n1")]).then(function(socks) {
var promises = [
new Promise((resolve,reject) => {
socks[0].on("message", function(msg, flags) {
try {
msg.should.equal("hello");
resolve();
} catch(err) {
reject(err);
}
});
}),
new Promise((resolve,reject) => {
socks[1].on("message", function(msg, flags) {
try {
msg.should.equal("hello");
resolve();
} catch(err) {
reject(err);
}
});
})
];
helper.getNode("n3").send({
payload: "hello"
});
return Promise.all(promises).then(() => {done()});
}).catch(function(err) {
done(err);
});
});
});
});
describe('websocket-client', function() {
it('should load', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n1", type: "websocket-client", path: getWsUrl("/ws") }];
helper.load(websocketNode, flow, function() {
helper.getNode("n1").should.have.property('path', getWsUrl("/ws"));
done();
});
});
it('should not be server', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n1", type: "websocket-client", path: getWsUrl("/ws") }];
helper.load(websocketNode, flow, function() {
helper.getNode("n1").should.have.property('isServer', false);
done();
});
});
it('should handle wholemsg property', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n1", type: "websocket-client", path: getWsUrl("/ws") },
{ id: "n2", type: "websocket-client", path: getWsUrl("/ws"), wholemsg: "true" }];
helper.load(websocketNode, flow, function() {
helper.getNode("n1").should.have.property("wholemsg", false);
helper.getNode("n2").should.have.property("wholemsg", true);
done();
});
});
it('should handle protocol property', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n1", type: "websocket-client", path: getWsUrl("/ws") },
{ id: "n2", type: "websocket-client", path: getWsUrl("/ws"), subprotocol: "testprotocol1, testprotocol2" }];
helper.load(websocketNode, flow, function() {
helper.getNode("n1").should.have.property("subprotocol", []);
helper.getNode("n2").should.have.property("subprotocol", ["testprotocol1","testprotocol2"]);
done();
});
});
it('should connect to server', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n2", type: "websocket-client", path: getWsUrl("/ws") }];
helper.load(websocketNode, flow, function() {
getSocket('server').on('connection', function(sock) {
done();
});
});
});
it('should initiate with subprotocol', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n2", type: "websocket-client", path: getWsUrl("/ws"), subprotocol: "testprotocol" }];
helper.load(websocketNode, flow, function() {
getSocket('server').on('connection', function (sock) {
sock.should.have.property("protocol", "testprotocol")
done();
});
});
});
it('should close on delete', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n2", type: "websocket-client", path: getWsUrl("/ws") }];
helper.load(websocketNode, flow, function() {
getSocket('server').on('connection', function(sock) {
sock.on('close', function() {
done();
});
helper.getNode("n2").close();
});
});
});
it('should receive data', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n1", type: "websocket-client", path: getWsUrl("/ws") },
{ id: "n2", type: "websocket in", client: "n1", wires: [["n3"]] },
{ id: "n3", type: "helper" }];
helper.load(websocketNode, flow, function() {
getSocket('server').on('connection', function(sock) {
sock.send('hello');
});
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("payload", "hello");
done();
});
});
});
it('should receive wholemsg data ', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n1", type: "websocket-client", path: getWsUrl("/ws"), wholemsg: "true" },
{ id: "n2", type: "websocket in", client: "n1", wires: [["n3"]] },
{ id: "n3", type: "helper" }];
helper.load(websocketNode, flow, function() {
getSocket('server').on('connection', function(sock) {
sock.send('{"text":"hello"}');
});
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("text", "hello");
done();
});
});
});
it('should receive wholemsg when data not JSON', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n1", type: "websocket-client", path: getWsUrl("/ws"), wholemsg: "true" },
{ id: "n2", type: "websocket in", client: "n1", wires: [["n3"]] },
{ id: "n3", type: "helper" }];
helper.load(websocketNode, flow, function() {
getSocket('server').on('connection', function(sock) {
sock.send('hello');
});
helper.getNode("n3").on("input", function(msg) {
msg.should.have.property("payload", "hello");
done();
});
});
});
it('should send', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n1", type: "websocket-client", path: getWsUrl("/ws") },
{ id: "n2", type: "websocket out", client: "n1" },
{ id: "n3", type: "helper", wires: [["n2"]] }];
helper.load(websocketNode, flow, function() {
getSocket('server').on('connection', function(sock) {
sock.on('message', function(msg) {
msg.should.equal("hello");
done();
});
});
getSocket("n1").on("open", function() {
helper.getNode("n3").send({
payload: "hello"
});
});
});
});
it('should send buffer', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n1", type: "websocket-client", path: getWsUrl("/ws") },
{ id: "n2", type: "websocket out", client: "n1" },
{ id: "n3", type: "helper", wires: [["n2"]] }];
helper.load(websocketNode, flow, function() {
getSocket('server').on('connection', function(sock) {
sock.on('message', function(msg) {
Buffer.isBuffer(msg).should.be.true();
msg.should.have.length(5);
done();
});
});
getSocket("n1").on("open", function() {
helper.getNode("n3").send({
payload: Buffer.from("hello")
});
});
});
});
it('should send wholemsg', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws" },
{ id: "n1", type: "websocket-client", path: getWsUrl("/ws"), wholemsg: "true" },
{ id: "n2", type: "websocket out", client: "n1" },
{ id: "n3", type: "helper", wires: [["n2"]] }];
helper.load(websocketNode, flow, function() {
getSocket('server').on('connection', function(sock) {
sock.on('message', function(msg) {
JSON.parse(msg).should.have.property("text", "hello");
done();
});
});
getSocket("n1").on('open', function(){
helper.getNode("n3").send({
text: "hello"
});
});
});
});
it('should NOT feedback more than once', function(done) {
var flow = [
{ id: "server", type: "websocket-listener", path: "/ws", wholemsg: "true" },
{ id: "client", type: "websocket-client", path: getWsUrl("/ws"), wholemsg: "true" },
{ id: "n1", type: "websocket in", client: "client", wires: [["n2", "output"]] },
{ id: "n2", type: "websocket out", server: "server" },
{ id: "n3", type: "helper", wires: [["n2"]] },
{ id: "output", type: "helper" }];
helper.load(websocketNode, flow, function() {
getSocket('client').on('open', function() {
helper.getNode("n3").send({
payload: "ping"
});
});
var acc = 0;
helper.getNode("output").on("input", function(msg) {
acc = acc + 1;
});
setTimeout( function() {
acc.should.equal(1);
helper.clearFlows();
done();
}, 250);
});
});
});
describe('websocket in node', function() {
it('should report error if no server config', function(done) {
var flow = [{ id: "n1", type: "websocket in", mode: "server" }];
helper.load(websocketNode, flow, function() {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "websocket in";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.toString().should.startWith("websocket.errors.missing-conf");
done();
});
});
});
describe('websocket out node', function() {
it('should report error if no server config', function(done) {
var flow = [{ id: "n1", type: "websocket out", mode: "server" }];
helper.load(websocketNode, flow, function() {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "websocket out";
});
//console.log(logEvents);
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.toString().should.startWith("websocket.errors.missing-conf");
done();
});
});
});
});

View File

@ -1,340 +0,0 @@
/**
* 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 net = require("net");
var should = require("should");
var stoppable = require('stoppable');
var helper = require("node-red-node-test-helper");
var tcpinNode = require("nr-test-utils").require("@node-red/nodes/core/network/31-tcpin.js");
var RED = require("nr-test-utils").require("node-red/lib/red.js");
describe('TCP Request Node', function() {
var server = undefined;
var port = 9000;
function startServer(done) {
port += 1;
server = stoppable(net.createServer(function(c) {
c.on('data', function(data) {
var rdata = "ACK:"+data.toString();
c.write(rdata);
});
c.on('error', function(err) {
startServer(done);
});
})).listen(port, "127.0.0.1", function(err) {
done();
});
}
before(function(done) {
startServer(done);
});
after(function(done) {
server.stop(done);
});
afterEach(function() {
helper.unload();
});
function testTCP(flow, val0, val1, done) {
helper.load(tcpinNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
if (typeof val1 === 'object') {
msg.should.have.properties(Object.assign({}, val1, {payload: Buffer.from(val1.payload)}));
} else {
msg.should.have.property('payload', Buffer.from(val1));
}
done();
} catch(err) {
done(err);
}
});
if((typeof val0) === 'object') {
n1.receive(val0);
} else {
n1.receive({payload:val0});
}
});
}
function testTCPMany(flow, values, result, done) {
helper.load(tcpinNode, flow, () => {
const n1 = helper.getNode("n1");
const n2 = helper.getNode("n2");
n2.on("input", msg => {
try {
if (typeof result === 'object') {
if (flow[0].ret === "string") {
msg.should.have.properties(Object.assign({}, result, {payload: result.payload}));
} else {
msg.should.have.properties(Object.assign({}, result, {payload: Buffer.from(result.payload)}));
}
} else {
if (flow[0].ret === "string") {
msg.should.have.property('payload', result);
} else {
msg.should.have.property('payload', Buffer.from(result));
}
}
done();
} catch(err) {
done(err);
}
});
values.forEach(value => {
n1.receive(typeof value === 'object' ? value : {payload: value});
});
});
}
describe('single message', function () {
it('should send & recv data', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"time", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCP(flow, {
payload: 'foo',
topic: 'bar'
}, {
payload: 'ACK:foo',
topic: 'bar'
}, done);
});
it('should retain complete message', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"time", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCP(flow, {
payload: 'foo',
topic: 'bar'
}, {
payload: 'ACK:foo',
topic: 'bar'
}, done);
});
it('should send & recv data when specified character received', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"char", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCP(flow, {
payload: 'foo0bar0',
topic: 'bar'
}, {
payload: 'ACK:foo0',
topic: 'bar'
}, done);
});
it('should send & recv data after fixed number of chars received', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"count", splitc: "7", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCP(flow, {
payload: 'foo bar',
topic: 'bar'
}, {
payload: 'ACK:foo',
topic: 'bar'
}, done);
});
it('should send & receive, then keep connection', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", splitc: "5", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCP(flow, {
payload: 'foo',
topic: 'bar'
}, {
payload: 'ACK:foo',
topic: 'bar'
}, done);
});
it('should send & recv data to/from server:port from msg', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"", port:"", out:"time", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCP(flow, {
payload: "foo",
host: "localhost",
port: port
}, {
payload: "ACK:foo",
host: 'localhost',
port: port
}, done);
});
});
describe('many messages', function () {
it('should send & recv data', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"time", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [{
payload: 'f',
topic: 'bar'
}, {
payload: 'o',
topic: 'bar'
}, {
payload: 'o',
topic: 'bar'
}], {
payload: 'ACK:foo',
topic: 'bar'
}, done);
});
it('should send & recv data when specified character received', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"char", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [{
payload: "foo0",
topic: 'bar'
}, {
payload: "bar0",
topic: 'bar'
}], {
payload: "ACK:foo0",
topic: 'bar'
}, done);
});
it('should send & recv data after fixed number of chars received', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"count", splitc: "7", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [{
payload: "fo",
topic: 'bar'
}, {
payload: "ob",
topic: 'bar'
}, {
payload: "ar",
topic: 'bar'
}], {
payload: "ACK:foo",
topic: 'bar'
}, done);
});
it('should send & receive, then keep connection', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", splitc: "5", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [{
payload: "foo",
topic: 'bar'
}, {
payload: "bar",
topic: 'bar'
}, {
payload: "baz",
topic: 'bar'
}], {
payload: "ACK:foobarbaz",
topic: 'bar'
}, done);
});
it('should send & receive, then keep connection, and not split return strings', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", ret:"string", newline:"", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [{
payload: "foo",
topic: 'boo'
}, {
payload: "bar<A>\nfoo",
topic: 'boo'
}], {
payload: "ACK:foobar<A>\nfoo",
topic: 'boo'
}, done);
});
it('should send & receive, then keep connection, and split return strings', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", ret:"string", newline:"<A>\\n", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [{
payload: "foo",
topic: 'boo'
}, {
payload: "bar<A>\nfoo",
topic: 'boo'
}], {
payload: "ACK:foobar<A>",
topic: 'boo'
}, done);
});
it('should send & recv data to/from server:port from msg', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"", port:"", out:"time", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [
{
payload: "f",
host: "localhost",
port: port
},
{
payload: "o",
host: "localhost",
port: port
},
{
payload: "o",
host: "localhost",
port: port
}
], {
payload: "ACK:foo",
host: 'localhost',
port: port
}, done);
});
it('should limit the queue size', function (done) {
RED.settings.tcpMsgQueueSize = 10;
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"sit", splitc: "5", wires:[["n2"]] },
{id:"n2", type:"helper"}];
// create one more msg than is allowed
const msgs = new Array(RED.settings.tcpMsgQueueSize + 1).fill('x');
const expected = msgs.slice(0, -1);
testTCPMany(flow, msgs, "ACK:" + expected.join(''), done);
});
it('should only retain the latest message', function(done) {
var flow = [{id:"n1", type:"tcp request", server:"localhost", port:port, out:"time", splitc: "0", wires:[["n2"]] },
{id:"n2", type:"helper"}];
testTCPMany(flow, [{
payload: 'f',
topic: 'bar'
}, {
payload: 'o',
topic: 'baz'
}, {
payload: 'o',
topic: 'quux'
}], {
payload: 'ACK:foo',
topic: 'quux'
}, done);
});
});
});

View File

@ -1,590 +0,0 @@
/**
* 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 should = require("should");
var jsonNode = require("nr-test-utils").require("@node-red/nodes/core/parsers/70-JSON.js");
var helper = require("node-red-node-test-helper");
describe('JSON node', function() {
before(function(done) {
helper.startServer(done);
});
after(function(done) {
helper.stopServer(done);
});
afterEach(function() {
helper.unload();
});
it('should convert a valid json string to a javascript object', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
msg.should.have.property('topic', 'bar');
msg.payload.should.have.property('employees');
msg.payload.employees[0].should.have.property('firstName', 'John');
msg.payload.employees[0].should.have.property('lastName', 'Smith');
done();
});
var jsonString = '{"employees":[{"firstName":"John", "lastName":"Smith"}]}';
jn1.receive({payload:jsonString,topic: "bar"});
});
});
it('should convert a buffer of a valid json string to a javascript object', function(done) {
var flow = [{id:"jn1",type:"json",action:"obj",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
msg.should.have.property('topic', 'bar');
msg.payload.should.have.property('employees');
msg.payload.employees[0].should.have.property('firstName', 'John');
msg.payload.employees[0].should.have.property('lastName', 'Smith');
done();
});
var jsonString = Buffer.from('{"employees":[{"firstName":"John", "lastName":"Smith"}]}');
jn1.receive({payload:jsonString,topic: "bar"});
});
});
it('should convert a javascript object to a json string', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
should.equal(msg.payload, '{"employees":[{"firstName":"John","lastName":"Smith"}]}');
done();
});
var obj = {employees:[{firstName:"John", lastName:"Smith"}]};
jn1.receive({payload:obj});
});
});
it('should convert a array to a json string', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
should.equal(msg.payload, '[1,2,3]');
done();
});
var obj = [1,2,3];
jn1.receive({payload:obj});
});
});
it('should convert a boolean to a json string', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
should.equal(msg.payload, 'true');
done();
});
var obj = true;
jn1.receive({payload:obj});
});
});
it('should convert a json string to a boolean', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
should.equal(msg.payload, true);
done();
});
var obj = "true";
jn1.receive({payload:obj});
});
});
it('should convert a number to a json string', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
should.equal(msg.payload, '2019');
done();
});
var obj = 2019;
jn1.receive({payload:obj});
});
});
it('should convert a json string to a number', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
should.equal(msg.payload, 1962);
done();
});
var obj = '1962';
jn1.receive({payload:obj});
});
});
it('should log an error if asked to parse an invalid json string', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
try {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn1.receive({payload:'foo',topic: "bar"});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.should.startWith("Unexpected token o");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},20);
} catch(err) {
done(err);
}
});
});
it('should log an error if asked to parse an invalid json string in a buffer', function(done) {
var flow = [{id:"jn1",type:"json",action:"obj",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
try {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn1.receive({payload:Buffer.from('{"name":foo}'),topic: "bar"});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.should.startWith("Unexpected token o");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},20);
} catch(err) {
done(err);
}
});
});
// it('should log an error if asked to parse something thats not json or js and not in force object mode', function(done) {
// var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
// {id:"jn2", type:"helper"}];
// helper.load(jsonNode, flow, function() {
// var jn1 = helper.getNode("jn1");
// var jn2 = helper.getNode("jn2");
// setTimeout(function() {
// try {
// var logEvents = helper.log().args.filter(function(evt) {
// return evt[0].type == "json";
// });
// logEvents.should.have.length(1);
// logEvents[0][0].should.have.a.property('msg');
// logEvents[0][0].msg.toString().should.eql('json.errors.dropped');
// done();
// } catch(err) {
// done(err);
// }
// },50);
// jn1.receive({payload:Buffer.from("abcd")});
// });
// });
it('should pass straight through if no payload set', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
msg.should.have.property('topic', 'bar');
msg.should.not.have.property('payload');
done();
});
jn1.receive({topic: "bar"});
});
});
it('should ensure the result is a json string', function(done) {
var flow = [{id:"jn1",type:"json",action:"str",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
var count = 0;
jn2.on("input", function(msg) {
try {
should.equal(msg.payload, '{"employees":[{"firstName":"John","lastName":"Smith"}]}');
count++;
if (count === 2) {
done();
}
} catch(err) {
done(err);
}
});
var obj = {employees:[{firstName:"John", lastName:"Smith"}]};
jn1.receive({payload:obj,topic: "bar"});
jn1.receive({payload:JSON.stringify(obj),topic: "bar"});
});
});
it('should ensure the result is a JS Object', function(done) {
var flow = [{id:"jn1",type:"json",action:"obj",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
var count = 0;
jn2.on("input", function(msg) {
try {
msg.should.have.property('topic', 'bar');
msg.payload.should.have.property('employees');
msg.payload.employees[0].should.have.property('firstName', 'John');
msg.payload.employees[0].should.have.property('lastName', 'Smith');
count++;
if (count === 2) {
done();
}
} catch(err) {
done(err);
}
});
var obj = {employees:[{firstName:"John", lastName:"Smith"}]};
jn1.receive({payload:obj,topic: "bar"});
jn1.receive({payload:JSON.stringify(obj),topic: "bar"});
});
});
it('should handle any msg property - receive existing string', function(done) {
var flow = [{id:"jn1",type:"json",property:"one.two",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
try {
msg.should.have.property('topic', 'bar');
msg.should.have.property('one');
msg.one.should.have.property('two');
msg.one.two.should.have.property('employees');
msg.one.two.employees[0].should.have.property('firstName', 'John');
msg.one.two.employees[0].should.have.property('lastName', 'Smith');
done();
} catch(err) {
done(err);
}
});
var jsonString = '{"employees":[{"firstName":"John", "lastName":"Smith"}]}';
jn1.receive({payload:"",one:{two:jsonString},topic: "bar"});
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
});
});
it('should handle any msg property - receive existing obj', function(done) {
var flow = [{id:"jn1",type:"json",property:"one.two",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
try {
should.equal(msg.one.two, '{"employees":[{"firstName":"John","lastName":"Smith"}]}');
done();
} catch(err) {
done(err);
}
});
var jsonString = '{"employees":[{"firstName":"John", "lastName":"Smith"}]}';
jn1.receive({payload:"",one:{two:JSON.parse(jsonString)},topic: "bar"});
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
});
});
it('should pass an object if provided a valid JSON string and schema', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
should.equal(msg.payload.number, 3);
should.equal(msg.payload.string, "allo");
done();
});
var jsonString = '{"number": 3, "string": "allo"}';
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
jn1.receive({payload:jsonString, schema:schema});
});
});
it('should pass an object if provided a valid object and schema and action is object', function(done) {
var flow = [{id:"jn1",type:"json",action:"obj",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
should.equal(msg.payload.number, 3);
should.equal(msg.payload.string, "allo");
done();
});
var obj = {"number": 3, "string": "allo"};
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
jn1.receive({payload:obj, schema:schema});
});
});
it('should pass a string if provided a valid object and schema', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
should.equal(msg.payload, '{"number":3,"string":"allo"}');
done();
});
var obj = {"number": 3, "string": "allo"};
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
jn1.receive({payload:obj, schema:schema});
});
});
it('should pass a string if provided a valid JSON string and schema and action is string', function(done) {
var flow = [{id:"jn1",type:"json",action:"str",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
should.equal(msg.payload, '{"number":3,"string":"allo"}');
done();
});
var jsonString = '{"number":3,"string":"allo"}';
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
jn1.receive({payload:jsonString, schema:schema});
});
});
it('should log an error if passed an invalid object and valid schema', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
try {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
var obj = {"number": "foo", "string": 3};
jn1.receive({payload:obj, schema:schema});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.should.startWith("json.errors.schema-error");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},50);
} catch(err) {
done(err);
}
});
});
it('should log an error if passed an invalid object and valid schema and action is object', function(done) {
var flow = [{id:"jn1",type:"json",action:"obj",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
try {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
var obj = {"number": "foo", "string": 3};
jn1.receive({payload:obj, schema:schema});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.should.startWith("json.errors.schema-error");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},50);
} catch(err) {
done(err);
}
});
});
it('should log an error if passed an invalid JSON string and valid schema', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
try {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
var jsonString = '{"number":"Hello","string":3}';
jn1.receive({payload:jsonString, schema:schema});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.should.startWith("json.errors.schema-error");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},50);
} catch(err) {
done(err);
}
});
});
it('should log an error if passed an invalid JSON string and valid schema and action is string', function(done) {
var flow = [{id:"jn1",type:"json",action:"str",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
try {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
var jsonString = '{"number":"Hello","string":3}';
jn1.receive({payload:jsonString, schema:schema});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.should.startWith("json.errors.schema-error");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},50);
} catch(err) {
done(err);
}
});
});
it('should log an error if passed a valid object and invalid schema', function(done) {
var flow = [{id:"jn1",type:"json",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
try {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
var schema = "garbage";
var obj = {"number": "foo", "string": 3};
jn1.receive({payload:obj, schema:schema});
setTimeout(function() {
try {
var logEvents = helper.log().args.filter(function(evt) {
return evt[0].type == "json";
});
logEvents.should.have.length(1);
logEvents[0][0].should.have.a.property('msg');
logEvents[0][0].msg.should.equal("json.errors.schema-error-compile");
logEvents[0][0].should.have.a.property('level',helper.log().ERROR);
done();
} catch(err) { done(err) }
},50);
} catch(err) {
done(err);
}
});
});
it('msg.schema property should be deleted before sending to next node (string input)', function(done) {
var flow = [{id:"jn1",type:"json",action:"str",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
should.equal(msg.schema, undefined);
done();
});
var jsonString = '{"number":3,"string":"allo"}';
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
jn1.receive({payload:jsonString, schema:schema});
});
});
it('msg.schema property should be deleted before sending to next node (object input)', function(done) {
var flow = [{id:"jn1",type:"json",action:"str",wires:[["jn2"]]},
{id:"jn2", type:"helper"}];
helper.load(jsonNode, flow, function() {
var jn1 = helper.getNode("jn1");
var jn2 = helper.getNode("jn2");
jn2.on("input", function(msg) {
should.equal(msg.schema, undefined);
done();
});
var jsonObject = {"number":3,"string":"allo"};
var schema = {title: "testSchema", type: "object", properties: {number: {type: "number"}, string: {type: "string" }}};
jn1.receive({payload:jsonObject, schema:schema});
});
});
});

View File

@ -1,609 +0,0 @@
/**
* 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 should = require("should");
var functionNode = require("nr-test-utils").require("@node-red/nodes/core/function/10-function.js");
var helper = require("node-red-node-test-helper");
// Notice:
// - nodes should have x, y, z property when defining subflow.
describe('subflow', function() {
before(function(done) {
helper.startServer(done);
});
after(function(done) {
helper.stopServer(done);
});
afterEach(function() {
helper.unload();
});
it('should define subflow', function(done) {
var flow = [
{id:"t1", type:"tab"},
{id:"n1", z:"t1", type:"subflow:s1", wires:[["n2"]]},
{id:"n2", z:"t1", type:"helper", wires:[]},
// Subflow
{id:"s1", type:"subflow", name:"Subflow", info:"",
in:[{wires:[ {id:"s1-n1"} ]}],
out:[{wires:[ {id:"s1-n1", port:0} ]}]},
{id:"s1-n1", z:"s1", type:"function",
func:"return msg;", wires:[]}
];
helper.load(functionNode, flow, function() {
done();
});
});
it('should pass data to/from subflow', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:""},
{id:"n1", x:10, y:10, z:"t0", type:"subflow:s1", wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow
{id:"s1", type:"subflow", name:"Subflow", info:"",
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n1", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"function",
func:"msg.payload = msg.payload+'bar'; return msg;", wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property("payload", "foobar");
done();
});
n1.receive({payload:"foo"});
});
});
it('should pass data to/from nested subflow', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:""},
{id:"n1", x:10, y:10, z:"t0", type:"subflow:s1", wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow1
{id:"s1", type:"subflow", name:"Subflow1", info:"",
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n2", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"subflow:s2",
wires:[["s1-n2"]]},
{id:"s1-n2", x:10, y:10, z:"s1", type:"function",
func:"msg.payload = msg.payload+'baz'; return msg;", wires:[]},
// Subflow2
{id:"s2", type:"subflow", name:"Subflow2", info:"",
in:[{
x:10, y:10,
wires:[ {id:"s2-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s2-n1", port:0} ]
}]
},
{id:"s2-n1", x:10, y:10, z:"s2", type:"function",
func:"msg.payload=msg.payload+'bar'; return msg;", wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property("payload", "foobarbaz");
done();
});
n1.receive({payload:"foo"});
});
});
it('should access env var of subflow template', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:""},
{id:"n1", x:10, y:10, z:"t0", type:"subflow:s1", wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow
{id:"s1", type:"subflow", name:"Subflow", info:"",
env: [
{name: "K", type: "str", value: "V"}
],
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n1", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"function",
func:"msg.V = env.get('K'); return msg;",
wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("V", "V");
done();
}
catch (e) {
console.log(e);
done(e);
}
});
n1.receive({payload:"foo"});
});
});
it('should access env var of subflow instance', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:""},
{id:"n1", x:10, y:10, z:"t0", type:"subflow:s1",
env: [
{name: "K", type: "str", value: "V"}
],
wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow
{id:"s1", type:"subflow", name:"Subflow", info:"",
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n1", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"function",
func:"msg.V = env.get('K'); return msg;",
wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("V", "V");
done();
}
catch (e) {
console.log(e);
done(e);
}
});
n1.receive({payload:"foo"});
});
});
it('should access last env var with same name', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:""},
{id:"n1", x:10, y:10, z:"t0", type:"subflow:s1",
env: [
{name: "K", type: "str", value: "V0"},
{name: "X", type: "str", value: "VX"},
{name: "K", type: "str", value: "V1"}
],
wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow
{id:"s1", type:"subflow", name:"Subflow", info:"",
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n1", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"function",
func:"msg.V = env.get('K'); return msg;",
wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("V", "V1");
done();
}
catch (e) {
console.log(e);
done(e);
}
});
n1.receive({payload:"foo"});
});
});
it('should access typed value of env var', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:""},
{id:"n1", x:10, y:10, z:"t0", type:"subflow:s1",
env: [
{name: "KN", type: "num", value: "100"},
{name: "KB", type: "bool", value: "true"},
{name: "KJ", type: "json", value: "[1,2,3]"},
{name: "Kb", type: "bin", value: "[65,65]"},
{name: "Ke", type: "env", value: "KS"}
],
wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow
{id:"s1", type:"subflow", name:"Subflow", info:"",
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n1", port:0} ]
}],
env: [
{name: "KS", type: "str", value: "STR"}
]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"function",
func:"msg.VE = env.get('Ke'); msg.VS = env.get('KS'); msg.VN = env.get('KN'); msg.VB = env.get('KB'); msg.VJ = env.get('KJ'); msg.Vb = env.get('Kb'); return msg;",
wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("VS", "STR");
msg.should.have.property("VN", 100);
msg.should.have.property("VB", true);
msg.should.have.property("VJ", [1,2,3]);
msg.should.have.property("Vb");
should.ok(msg.Vb instanceof Buffer);
msg.should.have.property("VE","STR");
done();
}
catch (e) {
done(e);
}
});
n1.receive({payload:"foo"});
});
});
it('should overwrite env var of subflow template by env var of subflow instance', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:""},
{id:"n1", x:10, y:10, z:"t0", type:"subflow:s1",
env: [
{name: "K", type: "str", value: "V"}
],
wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow
{id:"s1", type:"subflow", name:"Subflow", info:"",
env: [
{name: "K", type: "str", value: "TV"}
],
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n1", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"function",
func:"msg.V = env.get('K'); return msg;",
wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("V", "V");
done();
}
catch (e) {
console.log(e);
done(e);
}
});
n1.receive({payload:"foo"});
});
});
it('should access env var of parent subflow template', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:""},
{id:"n1", x:10, y:10, z:"t0", type:"subflow:s1", wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow1
{id:"s1", type:"subflow", name:"Subflow1", info:"",
env: [
{name: "K", type: "str", value: "V"},
],
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n2", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"subflow:s2",
wires:[["s1-n2"]]},
{id:"s1-n2", x:10, y:10, z:"s1", type:"function",
func:"return msg;", wires:[]},
// Subflow2
{id:"s2", type:"subflow", name:"Subflow2", info:"",
in:[{
x:10, y:10,
wires:[ {id:"s2-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s2-n1", port:0} ]
}]
},
{id:"s2-n1", x:10, y:10, z:"s2", type:"function",
func:"msg.V = env.get('K'); return msg;",
wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property("V", "V");
done();
});
n1.receive({payload:"foo"});
});
});
it('should access env var of parent subflow instance', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:""},
{id:"n1", x:10, y:10, z:"t0", type:"subflow:s1",
env: [
{name: "K", type: "str", value: "V"}
],
wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow1
{id:"s1", type:"subflow", name:"Subflow1", info:"",
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n2", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"subflow:s2",
wires:[["s1-n2"]]},
{id:"s1-n2", x:10, y:10, z:"s1", type:"function",
func:"return msg;", wires:[]},
// Subflow2
{id:"s2", type:"subflow", name:"Subflow2", info:"",
in:[{
x:10, y:10,
wires:[ {id:"s2-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s2-n1", port:0} ]
}]
},
{id:"s2-n1", x:10, y:10, z:"s2", type:"function",
func:"msg.V = env.get('K'); return msg;",
wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
msg.should.have.property("V", "V");
done();
});
n1.receive({payload:"foo"});
});
});
it('should access env var of tab', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:"", env: [
{name: "K", type: "str", value: "V"}
]},
{id:"n1", x:10, y:10, z:"t0", type:"subflow:s1", wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow
{id:"s1", type:"subflow", name:"Subflow", info:"", env: [],
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n1", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"function",
func:"msg.V = env.get('K'); return msg;",
wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("V", "V");
done();
}
catch (e) {
console.log(e);
done(e);
}
});
n1.receive({payload:"foo"});
});
});
it('should access env var of group', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:""},
{id:"g1", z:"t0", type:"group", env:[
{name: "K", type: "str", value: "V"}
]},
{id:"n1", x:10, y:10, z:"t0", g:"g1", type:"subflow:s1", wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow
{id:"s1", type:"subflow", name:"Subflow", info:"", env: [],
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n1", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"function",
func:"msg.V = env.get('K'); return msg;",
wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("V", "V");
done();
}
catch (e) {
console.log(e);
done(e);
}
});
n1.receive({payload:"foo"});
});
});
it('should access env var of nested group', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:""},
{id:"g1", z:"t0", type:"group", env:[
{name: "K", type: "str", value: "V"}
]},
{id:"g2", z:"t0", g:"g1", type:"group", env:[]},
{id:"n1", x:10, y:10, z:"t0", g:"g2", type:"subflow:s1", wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow
{id:"s1", type:"subflow", name:"Subflow", info:"", env: [],
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n1", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"function",
func:"msg.V = env.get('K'); return msg;",
wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("V", "V");
done();
}
catch (e) {
console.log(e);
done(e);
}
});
n1.receive({payload:"foo"});
});
});
it('should access NR_NODE_PATH env var within subflow instance', function(done) {
var flow = [
{id:"t0", type:"tab", label:"", disabled:false, info:""},
{id:"n1", x:10, y:10, z:"t0", type:"subflow:s1",
env: [], wires:[["n2"]]},
{id:"n2", x:10, y:10, z:"t0", type:"helper", wires:[]},
// Subflow
{id:"s1", type:"subflow", name:"Subflow", info:"",
in:[{
x:10, y:10,
wires:[ {id:"s1-n1"} ]
}],
out:[{
x:10, y:10,
wires:[ {id:"s1-n1", port:0} ]
}]
},
{id:"s1-n1", x:10, y:10, z:"s1", type:"function",
func:"msg.payload = env.get('NR_NODE_PATH'); return msg;",
wires:[]}
];
helper.load(functionNode, flow, function() {
var n1 = helper.getNode("n1");
var n2 = helper.getNode("n2");
n2.on("input", function(msg) {
try {
msg.should.have.property("payload", "t0/n1/s1-n1");
done();
}
catch (e) {
console.log(e);
done(e);
}
});
n1.receive({payload:"foo"});
});
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff