diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 0000000..ef79ffe --- /dev/null +++ b/bin/README.md @@ -0,0 +1,27 @@ +Simple command line tool for runnning test cases. + +1. listing list of test cases + +``` +% node flow-test.js [--list|-l] [[--target|-t] URL] +``` + +show list of test cases on Node-RED instance specified by *URL*. + +Example: +``` +% node bin/flow-test.js --list +Sample - Global + 0: test click + 1: test send + 2: test wait + 3: test function +``` + +2. running a test case + +``` +% node flow-test.js [--run|-r] no [[--target|-t] URL] +``` + +run the test case specified by *no* on Node-RED instance specified by *URL*. diff --git a/bin/flow-test.js b/bin/flow-test.js new file mode 100644 index 0000000..a03ca40 --- /dev/null +++ b/bin/flow-test.js @@ -0,0 +1,93 @@ +#!/usr/bin/env node +'use strict' + +const nopt = require("nopt"); +const got = require("got"); + +function parseArgs(args) { + const knownOpts = { + target: String, + list: Boolean, + run: String + }; + const shortHands = { + 't': ["--target"], + 'l': ["--list"], + 'r': ["--run"], + 'j': ["--json"], + }; + return nopt(knownOpts, shortHands, args, 2); +} + + +async function getList(target) { + const url = `${target}/flow-tester/testCase`; + try { + const res = await got(url); + const body = JSON.parse(res.body); + return body; + } + catch (err) { + console.log(err); + } +} + +async function doList(target, jsonOutput) { + const list = await getList(target); + if (jsonOutput) { + console.log(JSON.stringify(list, undefined, "\t")); + return; + } + let id = 0; + list.forEach((tcase) => { + console.log(`${tcase.name}`); + const tests = tcase.tests; + tests.forEach((test) => { + console.log(` ${id}: ${test.name}`); + id++; + }); + }); +} + +async function doRun(target, no) { + try { + const cases = await getList(target); + let id = 0; + for (let tcase of cases) { + const tests = tcase.tests; + for (let test of tests) { + if (id === no) { + const url = `${target}/flow-tester/runTestCase/${tcase.id}/${test.id}`; + const res = await got(url); + const body = JSON.parse(res.body); + return body; + } + id++; + } + } + } + catch (err) { + console.log(err); + return undefined; + } +} + +async function main(args) { + const opts = parseArgs(args); + let target = "http://localhost:1880"; + if (opts.target) { + target = opts.target; + } + if (opts.list) { + await doList(target, opts.json); + } + if (opts.run) { + const res = await doRun(target, Number(opts.run)); + console.log(JSON.stringify(res, null, "\t")); + } +} + + +(async () => { + main(process.argv); +})(); diff --git a/examples/node-red-contrib-flow-tester-addon/.gitignore b/examples/node-red-contrib-flow-tester-addon/.gitignore new file mode 100644 index 0000000..75b9df9 --- /dev/null +++ b/examples/node-red-contrib-flow-tester-addon/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +package-lock.json +.nyc_output +resources diff --git a/examples/node-red-contrib-flow-tester-addon/LICENSE b/examples/node-red-contrib-flow-tester-addon/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/examples/node-red-contrib-flow-tester-addon/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/examples/node-red-contrib-flow-tester-addon/README.md b/examples/node-red-contrib-flow-tester-addon/README.md new file mode 100644 index 0000000..ca2e0fd --- /dev/null +++ b/examples/node-red-contrib-flow-tester-addon/README.md @@ -0,0 +1,55 @@ +# node-red-flow-tester-addon + +An Sample Addon to Flow Testing plugin for Node-RED + +**This is under development and not ready for general use. If you want to contribute, come talk to us in the `#core-dev` channel on https://nodered.org/slack + +## Developing + +To use the development version of Flow Tester Addon you can clone its source code repository +and build it yourself. + +1. Get the source code + +``` +git clone https://github.com/node-red/node-red-flow-tester-addon.git +``` + +2. Install the dependencies + +``` +cd node-red-flow-tester-addon +npm install +``` + +3. Build the plugin + +``` +npm run build +``` + +4. Install into Node-RED + +``` +cd ~/.node-red +npm install +``` + +5. Restart Node-RED to load the plugin. + +### Source code structure + + - `scripts` - build scripts used by `npm run build` + - `src` - source code of the plugins + - `test` - tests material + +After `npm run build` is run, the following directories will be created: + + - `dist` - contains the built plugin files + - `resources` contains the built plugin resource files (eg css) + +## License + +Copyright Node-RED Project Contributors. + +Licensed under the [Apache License, Version 2.0](LICENSE). diff --git a/examples/node-red-contrib-flow-tester-addon/package.json b/examples/node-red-contrib-flow-tester-addon/package.json new file mode 100644 index 0000000..f29a632 --- /dev/null +++ b/examples/node-red-contrib-flow-tester-addon/package.json @@ -0,0 +1,38 @@ +{ + "name": "node-red-contrib-flow-tester-addon", + "version": "0.0.1", + "description": "A test plugin for flow tester addon", + "scripts": { + "build": "node scripts/build.js", + "dev": "nodemon --exec 'npm run build' -i dist -i resources -e 'css js html'", + "test": "nyc mocha" + }, + "keywords": [ + "node-red", + "testing" + ], + "files": [ + "dist", + "resources" + ], + "license": "Apache-2", + "node-red": { + "version": ">=2.0.0", + "plugins": { + "flow-tester-addon": "dist/flow-tester-addon.js" + } + }, + "contributors": [ + { + "name": "Hiroyasu Nishiyama", + "email": "hiroyasu.nishiyama.uq@hitachi.com" + } + ], + "devDependencies": { + "fs-extra": "^10.0.0", + "html-minifier": "^4.0.0", + "nodemon": "^2.0.7", + "nyc": "^15.1.0", + "mocha": "^8.4.0" + } +} diff --git a/examples/node-red-contrib-flow-tester-addon/scripts/build.js b/examples/node-red-contrib-flow-tester-addon/scripts/build.js new file mode 100644 index 0000000..169b5c5 --- /dev/null +++ b/examples/node-red-contrib-flow-tester-addon/scripts/build.js @@ -0,0 +1,51 @@ +const minify = require("html-minifier").minify; +const fs = require("fs-extra"); +const path = require("path"); + + +const projectRoot = path.join(__dirname,"..") +const resources = path.join(projectRoot,"resources"); +const dist = path.join(projectRoot,"dist"); +const src = path.join(projectRoot,"src"); + +const assets = {} +assets[dist] = [ + "locales", + "flow-tester-addon.html", + "flow-tester-addon.js" +] +assets[resources] = [ + "style.css" +] + +async function copyStaticAssets(dist,assets) { + await fs.mkdir(dist,{recursive: true}); + for (let i=0; i"+rawCSS+"", {minifyCSS: true}); + const finalCSS = minifiedCSS.substring(7,minifiedCSS.length-8) + await fs.writeFile(path.join(dist,assets[i]), finalCSS) + } else { + await fs.mkdir(path.join(dist,assets[i]), {recursive: true}); + await fs.copy(path.join(src,assets[i]),path.join(dist,assets[i])) + } + } +} + + + +(async function() { + const destinations = Object.keys(assets); + for (let i=0, l=destinations.length; i { + console.error(err); + process.exit(1); +}); diff --git a/examples/node-red-contrib-flow-tester-addon/src/flow-tester-addon.html b/examples/node-red-contrib-flow-tester-addon/src/flow-tester-addon.html new file mode 100644 index 0000000..a2afe29 --- /dev/null +++ b/examples/node-red-contrib-flow-tester-addon/src/flow-tester-addon.html @@ -0,0 +1,142 @@ +// Example addon for flow tester action + + + diff --git a/examples/node-red-contrib-flow-tester-addon/src/flow-tester-addon.js b/examples/node-red-contrib-flow-tester-addon/src/flow-tester-addon.js new file mode 100644 index 0000000..9ffc684 --- /dev/null +++ b/examples/node-red-contrib-flow-tester-addon/src/flow-tester-addon.js @@ -0,0 +1,62 @@ +module.exports = (RED) => { + "use strict"; + + console.log("; Initialize flow tester addon"); + RED.plugins.registerPlugin('node-red-flow-tester-addon', { + type: "flow-tester-addon", + actions: function () { + return [ + { + name: "addon:example1", + onTestStart: function () { + let promise = Promise.resolve(); + promise = promise.then(() => { + console.log("; start: addon:example1"); + }); + return promise; + }, + onTestEnd: function () { + let promise = Promise.resolve(); + promise = promise.then(() => { + console.log("; end: addon:example1"); + }); + return promise; + }, + execute: function(opt) { + // opt object contains: + // - data: optional data for action + // - node: target node + // - msg: message object + return new Promise((resolve, reject) => { + const action = opt.action; + console.log(action.value); + resolve(); + }); + } + }, + { + name: "addon:example2", + execute: function(opt) { + return new Promise((resolve, reject) => { + const msg = opt.msg; + const payload = msg.payload; + if (payload) { + try { + const action = opt.action; + const rex = new RegExp(action.value); + if (rex.test(payload)) { + resolve(); + } + } + catch (e) { + console.log("error:", e); + } + } + reject(); + }); + } + } + ]; + } + }); +}; diff --git a/examples/node-red-contrib-flow-tester-addon/src/locales/en-US/flow-tester-addon.json b/examples/node-red-contrib-flow-tester-addon/src/locales/en-US/flow-tester-addon.json new file mode 100644 index 0000000..3f29600 --- /dev/null +++ b/examples/node-red-contrib-flow-tester-addon/src/locales/en-US/flow-tester-addon.json @@ -0,0 +1,4 @@ +{ + "label": { + } +} diff --git a/examples/node-red-contrib-flow-tester-addon/src/style.css b/examples/node-red-contrib-flow-tester-addon/src/style.css new file mode 100644 index 0000000..e69de29 diff --git a/examples/node-red-contrib-flow-tester-addon/test/test.js b/examples/node-red-contrib-flow-tester-addon/test/test.js new file mode 100644 index 0000000..be2c5fa --- /dev/null +++ b/examples/node-red-contrib-flow-tester-addon/test/test.js @@ -0,0 +1,2 @@ +describe("Flow tester addon tests", function() { +}); diff --git a/examples/node-red-contrib-flow-tester-gui-addon/LICENSE b/examples/node-red-contrib-flow-tester-gui-addon/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/examples/node-red-contrib-flow-tester-gui-addon/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/examples/node-red-contrib-flow-tester-gui-addon/README.md b/examples/node-red-contrib-flow-tester-gui-addon/README.md new file mode 100644 index 0000000..9052440 --- /dev/null +++ b/examples/node-red-contrib-flow-tester-gui-addon/README.md @@ -0,0 +1,56 @@ +# node-red-flow-tester-gui-addon + +An GUI Test Addon to Flow Testing plugin for Node-RED + +**This is under development and not ready for general use. If you want to contribute, come talk to us in the `#core-dev` channel on https://nodered.org/slack + +## Developing + +To use the development version of Flow Tester Addon you can clone its source code repository +and build it yourself. + +1. Get the source code + +``` +git clone https://github.com/node-red/node-red-flow-tester-gui-addon.git +``` + +2. Install the dependencies + +``` +cd node-red-flow-tester-gui-addon +npm install +``` + +3. Build the plugin + +``` +npm run build +``` + +4. Install into Node-RED + +``` +cd ~/.node-red +npm install +``` + +5. Restart Node-RED to load the plugin. + +### Source code structure + + - `scripts` - build scripts used by `npm run build` + - `src` - source code of the plugins + - `test` - tests material + - `examples` - test flow example + +After `npm run build` is run, the following directories will be created: + + - `dist` - contains the built plugin files + - `resources` contains the built plugin resource files (eg css) + +## License + +Copyright Node-RED Project Contributors. + +Licensed under the [Apache License, Version 2.0](LICENSE). diff --git a/examples/node-red-contrib-flow-tester-gui-addon/examples/GUI-Test-Example.json b/examples/node-red-contrib-flow-tester-gui-addon/examples/GUI-Test-Example.json new file mode 100644 index 0000000..afad3ad --- /dev/null +++ b/examples/node-red-contrib-flow-tester-gui-addon/examples/GUI-Test-Example.json @@ -0,0 +1 @@ +[{"id":"9b3db84fb21d2ae9","type":"tab","label":"フロー 1","disabled":false,"info":"","env":[]},{"id":"bcdef1528a38ab01","type":"flow-test-config","name":"Test Suite 0","tests":[{"id":"T1dqpqj7o1mo","name":"01-GUI-URL","suite":"bcdef1528a38ab01","timeout":"10","max_actions":"500","actions":{"setup":{"_global_":[{"target":"test1","url":"http://localhost:1880/test1-1","size":"1024,768","performCheck":false,"kind":"addon:GUI-URL"},{"target":"test2","url":"http://localhost:1880/test1-2","size":"800,600","performCheck":false,"kind":"addon:GUI-URL"},{"target":"test1","url":"","filepath":"./save/01-GUI-URL_1.png","performCheck":false,"kind":"addon:GUI-saveScreenshot"},{"target":"test2","url":"","filepath":"./save/01-GUI-URL_2.png","performCheck":false,"kind":"addon:GUI-saveScreenshot"},{"kind":"send","target":"193db0840f528392","value":"01-GUI-URL"}],"fe5f34a07f8fa02d":[],"33f0acbced5f2f2f":[],"3f1bc77ebad6fb7d":[],"89abec8dc8dcd928":[],"3433732293b0efe6":[],"4cbdf19859bd3673":[],"a57d5145d9cc69f8":[],"3a204cb1e4e7e798":[],"3b1b52e7950d2d96":[],"a8b9279ac2b7bb34":[],"3d80cb23855d1f23":[],"ea2689d761c694d9":[],"df63e899f76ec37d":[],"73b88c9ce8eb419b":[],"e2cc4ae0d72b963b":[],"fe3ad091b0d72fa6":[],"01a33fd443f37a9d":[],"5a88bb5ee5f74048":[],"de20304a494f612c":[],"46a0c697fa6f3aa9":[],"e53fcf4665e96479":[],"1851347e41085efa":[],"2135f0ed4da03687":[],"ecbdd8f3f5d1a43f":[],"6b5edcefaf047657":[],"252be38b87101a0c":[],"d9631503adada096":[],"f96271d463f3d005":[],"e9568cd8a3881f7a":[],"7deb2f295e4cf286":[],"b15db00ea98ccdb8":[],"da44efa0625b0c46":[],"b2abfc59dfd6747f":[],"9ad3814da7eb0bf8":[],"e0e8d9808cbefc12":[],"57489d4a6843ed06":[],"bf6dc3e2d4bba78a":[],"08cab205b3caa944":[],"67123a9c0d3e0d6b":[],"2ab47c47fb5f303e":[],"c7e554e9a8a3c157":[],"2486e1ca0a98c73f":[],"71f6558f1dbc157d":[],"0aac7206f22fd418":[],"2c3680f456d44179":[],"334278a4d04eef8e":[],"46fd6974c4ab4dd4":[],"006bc52baad3b791":[],"f79c8d74d708debb":[],"c5143318646fb9fd":[],"640d91fab434d070":[],"553dac4c5471d519":[],"cb0a6d52972bcc02":[],"193db0840f528392":[],"3d690e3169d2d483":[],"6717a5b7fb237bd1":[],"bf0d97744cf36e4c":[],"8e35628ea9d99957":[],"2b0fd9cc8afbdcd6":[],"b6aa9aef3d3647a3":[],"db5b2d254533ef8b":[],"46d010cd56ba8914":[],"29f54a1ad235097e":[],"a4d57d3ae7aff215":[],"f470657e71958b8f":[],"42419f5d347b6c64":[]},"cleanup":{"_global_":[],"fe5f34a07f8fa02d":[],"33f0acbced5f2f2f":[],"3f1bc77ebad6fb7d":[],"89abec8dc8dcd928":[],"3433732293b0efe6":[],"4cbdf19859bd3673":[],"a57d5145d9cc69f8":[],"3a204cb1e4e7e798":[],"3b1b52e7950d2d96":[],"a8b9279ac2b7bb34":[],"3d80cb23855d1f23":[],"ea2689d761c694d9":[],"df63e899f76ec37d":[],"73b88c9ce8eb419b":[],"e2cc4ae0d72b963b":[],"fe3ad091b0d72fa6":[],"01a33fd443f37a9d":[],"5a88bb5ee5f74048":[],"de20304a494f612c":[],"46a0c697fa6f3aa9":[],"e53fcf4665e96479":[],"1851347e41085efa":[],"2135f0ed4da03687":[],"ecbdd8f3f5d1a43f":[],"6b5edcefaf047657":[],"252be38b87101a0c":[],"d9631503adada096":[],"f96271d463f3d005":[],"e9568cd8a3881f7a":[],"7deb2f295e4cf286":[],"b15db00ea98ccdb8":[],"da44efa0625b0c46":[],"b2abfc59dfd6747f":[],"9ad3814da7eb0bf8":[],"e0e8d9808cbefc12":[],"57489d4a6843ed06":[],"bf6dc3e2d4bba78a":[],"08cab205b3caa944":[],"67123a9c0d3e0d6b":[],"2ab47c47fb5f303e":[],"c7e554e9a8a3c157":[],"2486e1ca0a98c73f":[],"71f6558f1dbc157d":[],"0aac7206f22fd418":[],"2c3680f456d44179":[],"334278a4d04eef8e":[],"46fd6974c4ab4dd4":[],"006bc52baad3b791":[],"f79c8d74d708debb":[],"c5143318646fb9fd":[],"640d91fab434d070":[],"553dac4c5471d519":[],"cb0a6d52972bcc02":[],"193db0840f528392":[],"3d690e3169d2d483":[],"6717a5b7fb237bd1":[],"bf0d97744cf36e4c":[],"8e35628ea9d99957":[],"2b0fd9cc8afbdcd6":[],"b6aa9aef3d3647a3":[],"db5b2d254533ef8b":[],"46d010cd56ba8914":[],"29f54a1ad235097e":[],"a4d57d3ae7aff215":[],"f470657e71958b8f":[],"42419f5d347b6c64":[]},"recv":{"fe5f34a07f8fa02d":[],"33f0acbced5f2f2f":[],"3f1bc77ebad6fb7d":[],"89abec8dc8dcd928":[],"a57d5145d9cc69f8":[],"3a204cb1e4e7e798":[],"3b1b52e7950d2d96":[],"a8b9279ac2b7bb34":[],"3d80cb23855d1f23":[],"ea2689d761c694d9":[],"fe3ad091b0d72fa6":[],"01a33fd443f37a9d":[],"5a88bb5ee5f74048":[],"e9568cd8a3881f7a":[],"7deb2f295e4cf286":[],"b15db00ea98ccdb8":[],"b2abfc59dfd6747f":[],"e0e8d9808cbefc12":[],"57489d4a6843ed06":[],"08cab205b3caa944":[],"67123a9c0d3e0d6b":[],"c7e554e9a8a3c157":[],"0aac7206f22fd418":[],"2c3680f456d44179":[],"334278a4d04eef8e":[],"46fd6974c4ab4dd4":[],"006bc52baad3b791":[],"f79c8d74d708debb":[],"c5143318646fb9fd":[],"640d91fab434d070":[],"553dac4c5471d519":[],"193db0840f528392":[{"kind":"match","value":"01-GUI-URL","performCheck":true}],"3d690e3169d2d483":[],"bf0d97744cf36e4c":[],"8e35628ea9d99957":[],"2b0fd9cc8afbdcd6":[],"b6aa9aef3d3647a3":[],"db5b2d254533ef8b":[],"46d010cd56ba8914":[],"29f54a1ad235097e":[],"a4d57d3ae7aff215":[]},"stub":{"fe5f34a07f8fa02d":[],"33f0acbced5f2f2f":[],"3f1bc77ebad6fb7d":[],"89abec8dc8dcd928":[],"a57d5145d9cc69f8":[],"3a204cb1e4e7e798":[],"3b1b52e7950d2d96":[],"a8b9279ac2b7bb34":[],"3d80cb23855d1f23":[],"ea2689d761c694d9":[],"fe3ad091b0d72fa6":[],"01a33fd443f37a9d":[],"5a88bb5ee5f74048":[],"e9568cd8a3881f7a":[],"7deb2f295e4cf286":[],"b15db00ea98ccdb8":[],"b2abfc59dfd6747f":[],"e0e8d9808cbefc12":[],"57489d4a6843ed06":[],"08cab205b3caa944":[],"67123a9c0d3e0d6b":[],"c7e554e9a8a3c157":[],"0aac7206f22fd418":[],"2c3680f456d44179":[],"334278a4d04eef8e":[],"46fd6974c4ab4dd4":[],"006bc52baad3b791":[],"f79c8d74d708debb":[],"c5143318646fb9fd":[],"640d91fab434d070":[],"553dac4c5471d519":[],"193db0840f528392":[],"3d690e3169d2d483":[],"bf0d97744cf36e4c":[],"8e35628ea9d99957":[],"2b0fd9cc8afbdcd6":[],"b6aa9aef3d3647a3":[],"db5b2d254533ef8b":[],"46d010cd56ba8914":[],"29f54a1ad235097e":[],"a4d57d3ae7aff215":[]},"send":{"fe5f34a07f8fa02d":[],"33f0acbced5f2f2f":[],"3f1bc77ebad6fb7d":[],"89abec8dc8dcd928":[],"3433732293b0efe6":[],"4cbdf19859bd3673":[],"a57d5145d9cc69f8":[],"3a204cb1e4e7e798":[],"3d80cb23855d1f23":[],"fe3ad091b0d72fa6":[],"01a33fd443f37a9d":[],"5a88bb5ee5f74048":[],"f96271d463f3d005":[],"e9568cd8a3881f7a":[],"b15db00ea98ccdb8":[],"da44efa0625b0c46":[],"b2abfc59dfd6747f":[],"9ad3814da7eb0bf8":[],"e0e8d9808cbefc12":[],"57489d4a6843ed06":[],"bf6dc3e2d4bba78a":[],"08cab205b3caa944":[],"67123a9c0d3e0d6b":[],"2ab47c47fb5f303e":[],"c7e554e9a8a3c157":[],"2486e1ca0a98c73f":[],"71f6558f1dbc157d":[],"0aac7206f22fd418":[],"2c3680f456d44179":[],"334278a4d04eef8e":[],"46fd6974c4ab4dd4":[],"f79c8d74d708debb":[],"c5143318646fb9fd":[],"640d91fab434d070":[],"3d690e3169d2d483":[],"6717a5b7fb237bd1":[],"8e35628ea9d99957":[],"2b0fd9cc8afbdcd6":[],"b6aa9aef3d3647a3":[],"db5b2d254533ef8b":[],"46d010cd56ba8914":[],"29f54a1ad235097e":[],"a4d57d3ae7aff215":[]}}},{"id":"Tr3j3798bung","name":"02-GUI-Keys","suite":"bcdef1528a38ab01","timeout":"10","max_actions":"500","actions":{"setup":{"_global_":[{"target":"test","url":"http://localhost:1880/ui/#!/0","size":"","performCheck":false,"kind":"addon:GUI-URL"},{"kind":"send","target":"3a204cb1e4e7e798","value":"0"},{"target":"test","selector":"#\\30 2_GUI-Keys_Group_1_cards > md-card.nr-dashboard-slider._md.layout-align-space-between-center.layout-row.flex.visible","type":"str","value":"slider","timeout":"5000","performCheck":false,"kind":"addon:GUI-waitUntil"},{"target":"test","key":"Tab,Tab","performCheck":false,"kind":"addon:GUI-Keys"},{"target":"test","key":"ArrowRight,ArrowRight,ArrowRight,ArrowRight,ArrowRight,ArrowRight,ArrowRight,ArrowRight,ArrowRight,ArrowRight,ArrowRight,ArrowRight","performCheck":false,"kind":"addon:GUI-Keys"},{"kind":"wait","wait":"1000"},{"target":"test","url":"","filepath":"./save/02-GUI-Keys.png","performCheck":false,"kind":"addon:GUI-saveScreenshot"},{"kind":"send","target":"193db0840f528392","value":"02-GUI-Keys"}],"193db0840f528392":[]},"cleanup":{"_global_":[],"193db0840f528392":[]},"recv":{"193db0840f528392":[{"kind":"match","value":"02-GUI-Keys","performCheck":true}]},"stub":{"193db0840f528392":[]},"send":{}}},{"id":"Tm3obhhm6vug","name":"03-GUI-saveScreenshot","suite":"bcdef1528a38ab01","timeout":"10","max_actions":"500","actions":{"setup":{"_global_":[{"target":"test","url":"http://localhost:1880/test3","filepath":"./save/03-GUI-saveScreenshot.png","performCheck":false,"kind":"addon:GUI-saveScreenshot"},{"kind":"send","target":"193db0840f528392","value":"03-GUI-saveScreenshot"}],"193db0840f528392":[]},"cleanup":{"_global_":[],"193db0840f528392":[]},"recv":{"193db0840f528392":[{"kind":"match","value":"03-GUI-saveScreenshot","performCheck":true}]},"stub":{"193db0840f528392":[]},"send":{}}},{"id":"T8ehjolrb0og","name":"04-GUI-setCookies","suite":"bcdef1528a38ab01","timeout":"10","max_actions":"500","actions":{"setup":{"_global_":[{"target":"test","url":"http://localhost:1880/test4","size":"","performCheck":false,"kind":"addon:GUI-URL"},{"target":"test","cookie":"[{\"name\":\"test1\",\"value\":\"123\"},{\"name\":\"test2\",\"value\":\"456\"},{\"name\":\"test3\",\"value\":\"789\"}]","performCheck":false,"kind":"addon:GUI-setCookies"},{"target":"test","url":"http://localhost:1880/test4","size":"","performCheck":false,"kind":"addon:GUI-URL"},{"target":"test","url":"","filepath":"./save/04-GUI-setCookies.png","performCheck":false,"kind":"addon:GUI-saveScreenshot"},{"kind":"send","target":"193db0840f528392","value":"04-GUI-setCookies"}],"193db0840f528392":[]},"cleanup":{"_global_":[],"193db0840f528392":[]},"recv":{"193db0840f528392":[{"kind":"match","value":"04-GUI-setCookies","performCheck":true}]},"stub":{"193db0840f528392":[]},"send":{}}},{"id":"Tqicqojskkb","name":"05-GUI-setTimeout","suite":"bcdef1528a38ab01","timeout":"10","max_actions":"500","actions":{"setup":{"_global_":[{"target":"test1","timeout":"{\"pageLoad\":5000}","performCheck":false,"kind":"addon:GUI-setTimeout"},{"target":"test1","url":"http://localhost:1880/test5-1","size":"","performCheck":false,"kind":"addon:GUI-URL"},{"target":"test2","timeout":"{\"pageLoad\":5000}","performCheck":false,"kind":"addon:GUI-setTimeout"},{"target":"test2","url":"http://localhost:1880/test5-2","size":"","performCheck":false,"kind":"addon:GUI-URL"},{"kind":"send","target":"193db0840f528392","value":"05-GUI-setTimeout"}],"193db0840f528392":[]},"cleanup":{"_global_":[],"193db0840f528392":[]},"recv":{"193db0840f528392":[{"kind":"match","value":"05-GUI-setTimeout","performCheck":true}]},"stub":{"193db0840f528392":[]},"send":{}}},{"id":"Tktn10o59iug","name":"06-GUI-waitUntil","suite":"bcdef1528a38ab01","timeout":"10","max_actions":"500","actions":{"setup":{"_global_":[{"target":"test1","url":"http://localhost:1880/test6-1","size":"","performCheck":false,"kind":"addon:GUI-URL"},{"target":"test1","selector":"#ID_TEXT","type":"re","value":"\\d{2}","timeout":"10000","performCheck":false,"kind":"addon:GUI-waitUntil"},{"target":"test2","url":"http://localhost:1880/test6-2","size":"","performCheck":false,"kind":"addon:GUI-URL"},{"target":"test2","selector":"#ID_TEXT","type":"re","value":"\\d{2}","timeout":"10000","performCheck":false,"kind":"addon:GUI-waitUntil"},{"kind":"send","target":"193db0840f528392","value":"06-GUI-waitUntil"}],"193db0840f528392":[]},"cleanup":{"_global_":[],"193db0840f528392":[]},"recv":{"193db0840f528392":[{"kind":"match","value":"06-GUI-waitUntil","performCheck":true}]},"stub":{"193db0840f528392":[]},"send":{}}},{"id":"Taqk7q2rj9ng","name":"07-GUI-addValue","suite":"bcdef1528a38ab01","timeout":"10","max_actions":"500","actions":{"setup":{"_global_":[{"kind":"send","target":"640d91fab434d070","value":"test addValue "},{"target":"test","url":"http://localhost:1880/ui/#!/1","size":"","performCheck":false,"kind":"addon:GUI-URL"},{"target":"test","selector":"#input_0","type":"str","value":"","timeout":"5000","performCheck":false,"kind":"addon:GUI-waitUntil"},{"target":"test","selector":"#input_0","value":"add1 ","performCheck":false,"kind":"addon:GUI-addValue"},{"target":"test","selector":"#input_0","value":"add2 ","performCheck":false,"kind":"addon:GUI-addValue"},{"target":"test","selector":"#input_0","value":"add3","performCheck":false,"kind":"addon:GUI-addValue"},{"target":"test","url":"","filepath":"./save/07-GUI-addValue.png","performCheck":false,"kind":"addon:GUI-saveScreenshot"},{"kind":"send","target":"193db0840f528392","value":"07-GUI-addValue"}],"553dac4c5471d519":[],"193db0840f528392":[]},"cleanup":{"_global_":[],"553dac4c5471d519":[],"193db0840f528392":[]},"recv":{"553dac4c5471d519":[{"kind":"match","value":"test addValue add1 add2 add3","performCheck":true}],"193db0840f528392":[{"kind":"match","value":"07-GUI-addValue","performCheck":true}]},"stub":{"553dac4c5471d519":[],"193db0840f528392":[]},"send":{}}},{"id":"T1f2t37fo80o","name":"08-GUI-clearValue","suite":"bcdef1528a38ab01","timeout":"10","max_actions":"500","actions":{"setup":{"_global_":[{"kind":"send","target":"640d91fab434d070","value":"test clearValue"},{"target":"test","url":"http://localhost:1880/ui/#!/1","size":"","performCheck":false,"kind":"addon:GUI-URL"},{"target":"test","selector":"#input_0","type":"str","value":"","timeout":"5000","performCheck":false,"kind":"addon:GUI-waitUntil"},{"target":"test","selector":"#input_0","performCheck":false,"kind":"addon:GUI-clearValue"},{"target":"test","selector":"#input_0","value":"Test 08-GUI-clearValue","performCheck":false,"kind":"addon:GUI-addValue"},{"target":"test","url":"","filepath":"./save/08-GUI-clearValue.png","performCheck":false,"kind":"addon:GUI-saveScreenshot"},{"kind":"send","target":"193db0840f528392","value":"08-GUI-clearValue"}],"553dac4c5471d519":[],"193db0840f528392":[]},"cleanup":{"_global_":[],"553dac4c5471d519":[],"193db0840f528392":[]},"recv":{"553dac4c5471d519":[{"kind":"match","value":"Test 08-GUI-clearValue","performCheck":true}],"193db0840f528392":[{"kind":"match","value":"08-GUI-clearValue","performCheck":true}]},"stub":{"553dac4c5471d519":[],"193db0840f528392":[]},"send":{}}},{"id":"Tgbjs7hefb9","name":"09-GUI-click","suite":"bcdef1528a38ab01","timeout":"10","max_actions":"500","actions":{"setup":{"_global_":[{"kind":"send","target":"ea2689d761c694d9","value":""},{"target":"test","url":"http://localhost:1880/ui/#!/2","size":"","performCheck":false,"kind":"addon:GUI-URL"},{"target":"test","selector":"#\\30 9_GUI-click_Group_1_cards > md-card.nr-dashboard-button._md.visible > button","type":"str","value":"BUTTON","timeout":"5000","performCheck":false,"kind":"addon:GUI-waitUntil"},{"target":"test","selector":"#\\30 9_GUI-click_Group_1_cards > md-card.nr-dashboard-button._md.visible > button","clickkind":"single","performCheck":false,"kind":"addon:GUI-click"},{"kind":"wait","wait":"1000"},{"target":"test","url":"","filepath":"./save/09-GUI-click.png","performCheck":false,"kind":"addon:GUI-saveScreenshot"},{"kind":"send","target":"193db0840f528392","value":"09-GUI-click"}],"5a88bb5ee5f74048":[],"193db0840f528392":[]},"cleanup":{"_global_":[],"5a88bb5ee5f74048":[],"193db0840f528392":[]},"recv":{"5a88bb5ee5f74048":[],"193db0840f528392":[{"kind":"match","value":"09-GUI-click","performCheck":true}]},"stub":{"5a88bb5ee5f74048":[],"193db0840f528392":[]},"send":{"5a88bb5ee5f74048":[{"kind":"match","value":"Button was clicked.","performCheck":true}]}}},{"id":"Tcacec5v6mao","name":"10-GUI-getValue","suite":"bcdef1528a38ab01","timeout":"10","max_actions":"500","actions":{"setup":{"_global_":[{"target":"test","url":"http://localhost:1880/test10","size":"","performCheck":false,"kind":"addon:GUI-URL"},{"kind":"click","target":"29e3ad51752ad855"}],"193db0840f528392":[],"a4d57d3ae7aff215":[],"bf0d97744cf36e4c":[]},"cleanup":{"_global_":[],"193db0840f528392":[],"a4d57d3ae7aff215":[],"bf0d97744cf36e4c":[]},"recv":{"193db0840f528392":[{"kind":"match","value":"10-GUI-getValue","performCheck":true}],"a4d57d3ae7aff215":[{"target":"test","selector":"#ID_TAG","getkind":"tag","getname":"","type":"global","value":"tag","performCheck":false,"kind":"addon:GUI-getValue"},{"target":"test","selector":"#ID_TEXT","getkind":"text","getname":"","type":"global","value":"text","performCheck":false,"kind":"addon:GUI-getValue"},{"target":"test","selector":"#ID_PROP","getkind":"prop","getname":"testprop","type":"flow","value":"property","performCheck":false,"kind":"addon:GUI-getValue"},{"target":"test","selector":"#ID_VALUE","getkind":"val","getname":"","type":"global","value":"value","performCheck":false,"kind":"addon:GUI-getValue"},{"target":"test","selector":"#ID_ATTR","getkind":"attr","getname":"attribute","type":"flow","value":"attribute","performCheck":false,"kind":"addon:GUI-getValue"},{"target":"test","selector":"#ID_VALUE","getkind":"prop","getname":"type","type":"flow","value":"property","performCheck":false,"kind":"addon:GUI-getValue"}],"bf0d97744cf36e4c":[]},"stub":{"193db0840f528392":[],"a4d57d3ae7aff215":[],"bf0d97744cf36e4c":[]},"send":{"a4d57d3ae7aff215":[{"kind":"wait","wait":"2000"},{"kind":"match","value":"h1TEST-TextTEST-ValueTEST-Attributetext","performCheck":true},{"kind":"send","target":"193db0840f528392","value":"10-GUI-getValue"}]}}},{"id":"T2cbqvf3cgc8","name":"11-GUI-moveTo","suite":"bcdef1528a38ab01","timeout":"10","max_actions":"500","actions":{"setup":{"_global_":[{"target":"test","url":"http://localhost:1880/ui/#!/3","size":"","performCheck":false,"kind":"addon:GUI-URL"},{"target":"test","selector":"#\\31 1_GUI-moveTo_Group_1_cards > md-card:nth-child(1) > button","type":"str","value":"BUTTON 1","timeout":"5000","performCheck":false,"kind":"addon:GUI-waitUntil"},{"target":"test","selector":"#\\31 1_GUI-moveTo_Group_1_cards > md-card:nth-child(1) > button","performCheck":false,"kind":"addon:GUI-moveTo"},{"kind":"wait","wait":"1500"},{"target":"test","url":"","filepath":"./save/11-GUI-moveTo_1.png","performCheck":false,"kind":"addon:GUI-saveScreenshot"},{"target":"test","selector":"#\\31 1_GUI-moveTo_Group_1_cards > md-card:nth-child(2) > button","performCheck":false,"kind":"addon:GUI-moveTo"},{"kind":"wait","wait":"1500"},{"target":"test","url":"","filepath":"./save/11-GUI-moveTo_2.png","performCheck":false,"kind":"addon:GUI-saveScreenshot"},{"target":"test","selector":"#\\31 1_GUI-moveTo_Group_1_cards > md-card:nth-child(3) > button","performCheck":false,"kind":"addon:GUI-moveTo"},{"kind":"wait","wait":"1500"},{"target":"test","url":"","filepath":"./save/11-GUI-moveTo_3.png","performCheck":false,"kind":"addon:GUI-saveScreenshot"},{"kind":"send","target":"193db0840f528392","value":"11-GUI-moveTo"}],"193db0840f528392":[]},"cleanup":{"_global_":[],"193db0840f528392":[]},"recv":{"193db0840f528392":[{"kind":"match","value":"11-GUI-moveTo","performCheck":true}]},"stub":{"193db0840f528392":[]},"send":{}}},{"id":"Tn9de8bpkfa8","name":"12-GUI-setValue","suite":"bcdef1528a38ab01","timeout":"10","max_actions":"500","actions":{"setup":{"_global_":[{"kind":"send","target":"a8b9279ac2b7bb34","value":""},{"target":"test","url":"http://localhost:1880/ui/#!/4","size":"","performCheck":false,"kind":"addon:GUI-URL"},{"target":"test","selector":"#input_0","type":"str","value":"","timeout":"5000","performCheck":false,"kind":"addon:GUI-waitUntil"},{"target":"test","selector":"#input_0","value":"Test text","performCheck":false,"kind":"addon:GUI-setValue"},{"kind":"wait","wait":"1000"},{"target":"test","url":"","filepath":"./save/12-GUI-setValue.png","performCheck":false,"kind":"addon:GUI-saveScreenshot"},{"kind":"send","target":"193db0840f528392","value":"12-GUI-setValue"}],"fe3ad091b0d72fa6":[],"193db0840f528392":[]},"cleanup":{"_global_":[],"fe3ad091b0d72fa6":[],"193db0840f528392":[]},"recv":{"fe3ad091b0d72fa6":[],"193db0840f528392":[{"kind":"match","value":"12-GUI-setValue","performCheck":true}]},"stub":{"fe3ad091b0d72fa6":[],"193db0840f528392":[]},"send":{"fe3ad091b0d72fa6":[{"kind":"match","value":"Test text has been entered.","performCheck":true}]}}}]},{"id":"ea7e40d01b3c9e92","type":"ui_base","theme":{"name":"theme-light","lightTheme":{"default":"#0094CE","baseColor":"#0094CE","baseFont":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif","edited":true,"reset":false},"darkTheme":{"default":"#097479","baseColor":"#097479","baseFont":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif","edited":false},"customTheme":{"name":"Untitled Theme 1","default":"#4B7930","baseColor":"#4B7930","baseFont":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"},"themeState":{"base-color":{"default":"#0094CE","value":"#0094CE","edited":false},"page-titlebar-backgroundColor":{"value":"#0094CE","edited":false},"page-backgroundColor":{"value":"#fafafa","edited":false},"page-sidebar-backgroundColor":{"value":"#ffffff","edited":false},"group-textColor":{"value":"#1bbfff","edited":false},"group-borderColor":{"value":"#ffffff","edited":false},"group-backgroundColor":{"value":"#ffffff","edited":false},"widget-textColor":{"value":"#111111","edited":false},"widget-backgroundColor":{"value":"#0094ce","edited":false},"widget-borderColor":{"value":"#ffffff","edited":false},"base-font":{"value":"-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"}},"angularTheme":{"primary":"indigo","accents":"blue","warn":"red","background":"grey","palette":"light"}},"site":{"name":"Node-RED ダッシュボード","hideToolbar":"false","allowSwipe":"false","lockMenu":"false","allowTempTheme":"true","dateFormat":"YYYY/MM/DD","sizes":{"sx":48,"sy":48,"gx":6,"gy":6,"cx":6,"cy":6,"px":0,"py":0}}},{"id":"29571e93d0cd3bd8","type":"ui_group","name":"Group 1","tab":"","order":1,"disp":true,"width":6},{"id":"add392ac2ced5ed7","type":"ui_tab","name":"02 GUI-Keys","icon":"dashboard","order":1,"disabled":false,"hidden":false},{"id":"0779fd07b585202b","type":"ui_group","name":"Group 1","tab":"add392ac2ced5ed7","order":1,"disp":true,"width":"6","collapse":false,"className":""},{"id":"44d046383f6e170c","type":"ui_group","name":"Group 1","tab":"26c328d9fd99402b","order":1,"disp":true,"width":6},{"id":"26c328d9fd99402b","type":"ui_tab","name":"09 GUI-click","icon":"dashboard","order":3,"disabled":false,"hidden":false},{"id":"69ef5cd81fbe23c4","type":"ui_tab","name":"12 GUI-setValue","icon":"dashboard","order":5,"disabled":false,"hidden":false},{"id":"2b4a3bdb043675fd","type":"ui_group","name":"Group 1","tab":"69ef5cd81fbe23c4","order":1,"disp":true,"width":6},{"id":"51a7624fadecbb11","type":"ui_group","name":"Group 1","tab":"","order":1,"disp":true,"width":6},{"id":"785e79a6406a34f0","type":"ui_tab","name":"07 GUI-addValue 08-clearValue","icon":"dashboard","order":2,"disabled":false,"hidden":false},{"id":"2c037ff2393ce306","type":"ui_group","name":"Group 1","tab":"785e79a6406a34f0","order":1,"disp":true,"width":6},{"id":"d7a48474c86fb492","type":"ui_tab","name":"11 GUI-moveTo","icon":"dashboard","order":4,"disabled":false,"hidden":false},{"id":"a94a995999abb6ff","type":"ui_group","name":"Group 1","tab":"d7a48474c86fb492","order":1,"disp":true,"width":6},{"id":"3433732293b0efe6","type":"http in","z":"9b3db84fb21d2ae9","name":"","url":"/test1-1","method":"get","upload":false,"swaggerDoc":"","x":110,"y":100,"wires":[["89abec8dc8dcd928"]]},{"id":"9c703e8de2e3c532","type":"http response","z":"9b3db84fb21d2ae9","name":"","statusCode":"","headers":{},"x":470,"y":100,"wires":[]},{"id":"89abec8dc8dcd928","type":"template","z":"9b3db84fb21d2ae9","name":"Size: 1024x768","field":"payload","fieldType":"msg","format":"html","syntax":"plain","template":"\n\n\n\n \n 01 GUI-URL TEST\n\n\n\n

01 GUI-URL TEST1

\n

Size: 1024x768

\n

test test test test test test test test test test test test test test test test test test test test test test test test\n test test test test test test test test test test test test test test test test test test test test test test test test\n test test test test test test test test test test test test test test test test test test test test test test test test\n test test test test test test test test test test test test test test test

\n\n\n","output":"str","x":300,"y":100,"wires":[["9c703e8de2e3c532"]]},{"id":"4cbdf19859bd3673","type":"http in","z":"9b3db84fb21d2ae9","name":"","url":"/test1-2","method":"get","upload":false,"swaggerDoc":"","x":110,"y":160,"wires":[["a57d5145d9cc69f8"]]},{"id":"741bea42aff840bc","type":"http response","z":"9b3db84fb21d2ae9","name":"","statusCode":"","headers":{},"x":470,"y":160,"wires":[]},{"id":"a57d5145d9cc69f8","type":"template","z":"9b3db84fb21d2ae9","name":"Size: 800x600","field":"payload","fieldType":"msg","format":"html","syntax":"plain","template":"\n\n\n\n \n 01 GUI-URL TEST\n\n\n\n

01 GUI-URL TEST2

\n

Size: 800x600

\n

test test test test test test test test test test test test test test test test test test test test test test test test\n test test test test test test test test test test test test test test test test test test test test test test test test\n test test test test test test test test test test test test test test test test test test test test test test test test\n test test test test test test test test test test test test test test test

\n\n\n","output":"str","x":300,"y":160,"wires":[["741bea42aff840bc"]]},{"id":"3a204cb1e4e7e798","type":"ui_slider","z":"9b3db84fb21d2ae9","name":"","label":"slider","tooltip":"","group":"0779fd07b585202b","order":0,"width":0,"height":0,"passthru":true,"outs":"all","topic":"topic","topicType":"msg","min":0,"max":10,"step":1,"className":"","x":90,"y":260,"wires":[["3b1b52e7950d2d96"]]},{"id":"3b1b52e7950d2d96","type":"ui_gauge","z":"9b3db84fb21d2ae9","name":"","group":"0779fd07b585202b","order":1,"width":0,"height":0,"gtype":"gage","title":"gauge","label":"units","format":"{{value}}","min":0,"max":10,"colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","className":"","x":230,"y":260,"wires":[]},{"id":"01a33fd443f37a9d","type":"ui_button","z":"9b3db84fb21d2ae9","name":"","group":"44d046383f6e170c","order":1,"width":0,"height":0,"passthru":false,"label":"button","tooltip":"","color":"","bgcolor":"","className":"","icon":"","payload":"Button","payloadType":"str","topic":"topic","topicType":"msg","x":90,"y":1080,"wires":[["5a88bb5ee5f74048"]]},{"id":"ea2689d761c694d9","type":"ui_text","z":"9b3db84fb21d2ae9","group":"44d046383f6e170c","order":2,"width":0,"height":0,"name":"text","label":"","format":"{{msg.payload}}","layout":"row-center","className":"","x":490,"y":1080,"wires":[]},{"id":"5a88bb5ee5f74048","type":"function","z":"9b3db84fb21d2ae9","name":"","func":"if (msg.payload != \"\") {\n msg.payload += \" was clicked.\"\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":300,"y":1080,"wires":[["ea2689d761c694d9"]]},{"id":"3d80cb23855d1f23","type":"ui_text_input","z":"9b3db84fb21d2ae9","name":"","label":"","tooltip":"","group":"2b4a3bdb043675fd","order":1,"width":0,"height":0,"passthru":true,"mode":"text","delay":300,"topic":"topic","sendOnBlur":true,"className":"","topicType":"msg","x":100,"y":1520,"wires":[["fe3ad091b0d72fa6"]]},{"id":"fe3ad091b0d72fa6","type":"function","z":"9b3db84fb21d2ae9","name":"","func":"if (msg.payload != \"\") {\n msg.payload += \" has been entered.\"\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":300,"y":1520,"wires":[["a8b9279ac2b7bb34"]]},{"id":"a8b9279ac2b7bb34","type":"ui_text","z":"9b3db84fb21d2ae9","group":"2b4a3bdb043675fd","order":2,"width":0,"height":0,"name":"text","label":"","format":"{{msg.payload}}","layout":"row-center","className":"","x":510,"y":1520,"wires":[]},{"id":"df63e899f76ec37d","type":"comment","z":"9b3db84fb21d2ae9","name":"09 GUI-click","info":"","x":110,"y":1040,"wires":[]},{"id":"73b88c9ce8eb419b","type":"comment","z":"9b3db84fb21d2ae9","name":"12 GUI-setValue","info":"","x":120,"y":1480,"wires":[]},{"id":"e2cc4ae0d72b963b","type":"comment","z":"9b3db84fb21d2ae9","name":"02 GUI-Keys","info":"","x":110,"y":220,"wires":[]},{"id":"de20304a494f612c","type":"comment","z":"9b3db84fb21d2ae9","name":"01 GUI-URL","info":"","x":110,"y":40,"wires":[]},{"id":"46a0c697fa6f3aa9","type":"comment","z":"9b3db84fb21d2ae9","name":"03 GUI-saveScreenshot","info":"","x":140,"y":340,"wires":[]},{"id":"e53fcf4665e96479","type":"comment","z":"9b3db84fb21d2ae9","name":"04 GUI-setCookies","info":"","x":130,"y":460,"wires":[]},{"id":"1851347e41085efa","type":"comment","z":"9b3db84fb21d2ae9","name":"05 GUI-setTimeout","info":"タイムアウトしてもテストフローが停止しない。\n用途が不明","x":130,"y":580,"wires":[]},{"id":"2135f0ed4da03687","type":"comment","z":"9b3db84fb21d2ae9","name":"06 GUI-waitUntil","info":"","x":120,"y":760,"wires":[]},{"id":"ecbdd8f3f5d1a43f","type":"comment","z":"9b3db84fb21d2ae9","name":"07 GUI-addValue","info":"","x":120,"y":940,"wires":[]},{"id":"6b5edcefaf047657","type":"comment","z":"9b3db84fb21d2ae9","name":"08 GUI-clearValue","info":"","x":310,"y":940,"wires":[]},{"id":"252be38b87101a0c","type":"comment","z":"9b3db84fb21d2ae9","name":"10 GUI-getValue","info":"","x":120,"y":1140,"wires":[]},{"id":"d9631503adada096","type":"comment","z":"9b3db84fb21d2ae9","name":"11 GUI-moveTo","info":"","x":120,"y":1300,"wires":[]},{"id":"f96271d463f3d005","type":"http in","z":"9b3db84fb21d2ae9","name":"","url":"/test3","method":"get","upload":false,"swaggerDoc":"","x":100,"y":380,"wires":[["e9568cd8a3881f7a"]]},{"id":"3646275cbe45db61","type":"http response","z":"9b3db84fb21d2ae9","name":"","statusCode":"","headers":{},"x":550,"y":380,"wires":[]},{"id":"e9568cd8a3881f7a","type":"template","z":"9b3db84fb21d2ae9","name":"URL: http://localhost:1880/test3","field":"payload","fieldType":"msg","format":"html","syntax":"plain","template":"\n\n\n\n \n 03 GUI-savaScreenshot TEST\n\n\n\n

03 GUI-savaScreenshot TEST

\n

URL: http://localhost:1880/test3

\n\n\n","output":"str","x":330,"y":380,"wires":[["3646275cbe45db61"]]},{"id":"b15db00ea98ccdb8","type":"function","z":"9b3db84fb21d2ae9","name":"","func":"msg.payload = JSON.stringify(msg.req.cookies, null, 4);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":260,"y":500,"wires":[["b2abfc59dfd6747f"]]},{"id":"da44efa0625b0c46","type":"http in","z":"9b3db84fb21d2ae9","name":"","url":"/test4","method":"get","upload":false,"swaggerDoc":"","x":100,"y":500,"wires":[["b15db00ea98ccdb8"]]},{"id":"b2abfc59dfd6747f","type":"template","z":"9b3db84fb21d2ae9","name":"Cookies","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"\n\n \n 04 GUI-setCookies TEST\n\n\n

04 GUI-setCookies TEST

\n
{{ payload }}
\n\n","output":"str","x":420,"y":500,"wires":[["c3c9bd6b22cdf5c4"]]},{"id":"c3c9bd6b22cdf5c4","type":"http response","z":"9b3db84fb21d2ae9","name":"","statusCode":"","headers":{},"x":580,"y":500,"wires":[]},{"id":"9ad3814da7eb0bf8","type":"http in","z":"9b3db84fb21d2ae9","name":"","url":"/test5-1","method":"get","upload":false,"swaggerDoc":"","x":110,"y":620,"wires":[["57489d4a6843ed06"]]},{"id":"e0e8d9808cbefc12","type":"template","z":"9b3db84fb21d2ae9","name":"wait page","field":"payload","fieldType":"msg","format":"html","syntax":"plain","template":"\n\n\n \n 05 GUI-setTimeout TEST\n\n\n\n

05 GUI-setTimeout TEST1

\n\n\n","output":"str","x":460,"y":620,"wires":[["273bd1c5fba89419"]]},{"id":"57489d4a6843ed06","type":"delay","z":"9b3db84fb21d2ae9","name":"","pauseType":"delay","timeout":"4","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":280,"y":620,"wires":[["e0e8d9808cbefc12"]]},{"id":"bf6dc3e2d4bba78a","type":"http in","z":"9b3db84fb21d2ae9","name":"","url":"/test5-2","method":"get","upload":false,"swaggerDoc":"","x":110,"y":680,"wires":[["08cab205b3caa944"]]},{"id":"08cab205b3caa944","type":"delay","z":"9b3db84fb21d2ae9","name":"depay 6s (time out)","pauseType":"delay","timeout":"6","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":310,"y":680,"wires":[["e0e8d9808cbefc12"]]},{"id":"2486e1ca0a98c73f","type":"http in","z":"9b3db84fb21d2ae9","name":"","url":"/test6-1","method":"get","upload":false,"swaggerDoc":"","x":110,"y":800,"wires":[["f79c8d74d708debb"]]},{"id":"71f6558f1dbc157d","type":"http in","z":"9b3db84fb21d2ae9","name":"","url":"/test6-2","method":"get","upload":false,"swaggerDoc":"","x":110,"y":860,"wires":[["c5143318646fb9fd"]]},{"id":"46fd6974c4ab4dd4","type":"template","z":"9b3db84fb21d2ae9","name":"wait text","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"\n\n\n \n 06 GUI-waitUntil TEST\n\n\n\n

06 GUI-waitUntil TEST

\n

Regular expression match test.

\n\n\n","output":"str","x":460,"y":800,"wires":[["6075415beb6878a0"]]},{"id":"6075415beb6878a0","type":"http response","z":"9b3db84fb21d2ae9","name":"","statusCode":"","headers":{},"x":610,"y":800,"wires":[]},{"id":"f79c8d74d708debb","type":"change","z":"9b3db84fb21d2ae9","name":"delay 9s","rules":[{"t":"set","p":"payload","pt":"msg","to":"9000","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":280,"y":800,"wires":[["46fd6974c4ab4dd4"]]},{"id":"c5143318646fb9fd","type":"change","z":"9b3db84fb21d2ae9","name":"delay 11s (time out)","rules":[{"t":"set","p":"payload","pt":"msg","to":"10000","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":310,"y":860,"wires":[["46fd6974c4ab4dd4"]]},{"id":"640d91fab434d070","type":"ui_text_input","z":"9b3db84fb21d2ae9","name":"","label":"","tooltip":"","group":"2c037ff2393ce306","order":0,"width":0,"height":0,"passthru":false,"mode":"text","delay":300,"topic":"topic","sendOnBlur":true,"className":"","topicType":"msg","x":100,"y":980,"wires":[["553dac4c5471d519"]]},{"id":"553dac4c5471d519","type":"debug","z":"9b3db84fb21d2ae9","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":290,"y":980,"wires":[]},{"id":"cb0a6d52972bcc02","type":"comment","z":"9b3db84fb21d2ae9","name":"99 TestEnd","info":"","x":100,"y":1600,"wires":[]},{"id":"193db0840f528392","type":"debug","z":"9b3db84fb21d2ae9","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":310,"y":1640,"wires":[]},{"id":"a009dbfb3c8dcba9","type":"inject","z":"9b3db84fb21d2ae9","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":1640,"wires":[["193db0840f528392"]]},{"id":"3d690e3169d2d483","type":"template","z":"9b3db84fb21d2ae9","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"\n\n\n\n \n 10 GUI-getValue TEST\n\n\n\n

10 GUI-getValue TEST

\n

TEST-Text

\n
\n \n\n\n\n","output":"str","x":300,"y":1180,"wires":[["d7fe4424960b4eba"]]},{"id":"6717a5b7fb237bd1","type":"http in","z":"9b3db84fb21d2ae9","name":"","url":"/test10","method":"get","upload":false,"swaggerDoc":"","x":110,"y":1180,"wires":[["3d690e3169d2d483"]]},{"id":"d7fe4424960b4eba","type":"http response","z":"9b3db84fb21d2ae9","name":"","statusCode":"","headers":{},"x":490,"y":1180,"wires":[]},{"id":"29e3ad51752ad855","type":"inject","z":"9b3db84fb21d2ae9","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":1220,"wires":[["a4d57d3ae7aff215"]]},{"id":"bf0d97744cf36e4c","type":"debug","z":"9b3db84fb21d2ae9","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":510,"y":1220,"wires":[]},{"id":"b6aa9aef3d3647a3","type":"ui_button","z":"9b3db84fb21d2ae9","name":"","group":"a94a995999abb6ff","order":1,"width":0,"height":0,"passthru":false,"label":"Button 1","tooltip":"Button 1","color":"","bgcolor":"","className":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":100,"y":1340,"wires":[[]]},{"id":"db5b2d254533ef8b","type":"ui_button","z":"9b3db84fb21d2ae9","name":"","group":"a94a995999abb6ff","order":2,"width":0,"height":0,"passthru":false,"label":"Button 2","tooltip":"Button 2","color":"","bgcolor":"","className":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":100,"y":1380,"wires":[[]]},{"id":"46d010cd56ba8914","type":"ui_button","z":"9b3db84fb21d2ae9","name":"","group":"a94a995999abb6ff","order":3,"width":0,"height":0,"passthru":false,"label":"Button 3","tooltip":"Button 3","color":"","bgcolor":"","className":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":100,"y":1420,"wires":[[]]},{"id":"a4d57d3ae7aff215","type":"change","z":"9b3db84fb21d2ae9","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"$globalContext(\"tag\") & $globalContext(\"text\") & $globalContext(\"value\") & $flowContext(\"attribute\") & $flowContext(\"property\") \t","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":320,"y":1220,"wires":[["bf0d97744cf36e4c"]]},{"id":"f470657e71958b8f","type":"comment","z":"9b3db84fb21d2ae9","name":"Check the console log for the result.","info":"","x":580,"y":680,"wires":[]},{"id":"42419f5d347b6c64","type":"comment","z":"9b3db84fb21d2ae9","name":"Check the console log for the result.","info":"","x":580,"y":860,"wires":[]},{"id":"273bd1c5fba89419","type":"http response","z":"9b3db84fb21d2ae9","name":"","statusCode":"","headers":{},"x":620,"y":620,"wires":[]}] \ No newline at end of file diff --git a/examples/node-red-contrib-flow-tester-gui-addon/package.json b/examples/node-red-contrib-flow-tester-gui-addon/package.json new file mode 100644 index 0000000..d18ea08 --- /dev/null +++ b/examples/node-red-contrib-flow-tester-gui-addon/package.json @@ -0,0 +1,47 @@ +{ + "name": "node-red-contrib-flow-tester-gui-addon", + "version": "0.0.1", + "description": "A test plugin for flow tester gui addon", + "scripts": { + "build": "node scripts/build.js", + "dev": "nodemon --exec 'npm run build' -i dist -i resources -e 'css js html'", + "test": "nyc mocha" + }, + "keywords": [ + "node-red", + "testing", + "dashboard", + "gui" + ], + "files": [ + "dist", + "resources" + ], + "license": "Apache-2", + "node-red": { + "version": ">=2.0.0", + "plugins": { + "flow-tester-gui-addon": "dist/flow-tester-gui-addon.js" + } + }, + "contributors": [ + { + "name": "Kazuhiro Ito", + "email": "kazuhiro.ito.ek@hitachi-solutions.com" + } + ], + "devDependencies": { + "@wdio/cli": "^7.16.10", + "@wdio/local-runner": "^7.16.10", + "@wdio/mocha-framework": "^7.16.6", + "@wdio/spec-reporter": "^7.16.9", + "chromedriver": "^96.0.0", + "fs-extra": "^10.0.0", + "global-agent": "^3.0.0", + "html-minifier": "^4.0.0", + "mocha": "^8.4.0", + "nodemon": "^2.0.7", + "nyc": "^15.1.0", + "wdio-chromedriver-service": "^7.2.2" + } +} diff --git a/examples/node-red-contrib-flow-tester-gui-addon/scripts/build.js b/examples/node-red-contrib-flow-tester-gui-addon/scripts/build.js new file mode 100644 index 0000000..96c50bb --- /dev/null +++ b/examples/node-red-contrib-flow-tester-gui-addon/scripts/build.js @@ -0,0 +1,51 @@ +const minify = require("html-minifier").minify; +const fs = require("fs-extra"); +const path = require("path"); + + +const projectRoot = path.join(__dirname,"..") +const resources = path.join(projectRoot,"resources"); +const dist = path.join(projectRoot,"dist"); +const src = path.join(projectRoot,"src"); + +const assets = {} +assets[dist] = [ + "locales", + "flow-tester-gui-addon.html", + "flow-tester-gui-addon.js" +] +assets[resources] = [ + "style.css" +] + +async function copyStaticAssets(dist,assets) { + await fs.mkdir(dist,{recursive: true}); + for (let i=0; i"+rawCSS+"", {minifyCSS: true}); + const finalCSS = minifiedCSS.substring(7,minifiedCSS.length-8) + await fs.writeFile(path.join(dist,assets[i]), finalCSS) + } else { + await fs.mkdir(path.join(dist,assets[i]), {recursive: true}); + await fs.copy(path.join(src,assets[i]),path.join(dist,assets[i])) + } + } +} + + + +(async function() { + const destinations = Object.keys(assets); + for (let i=0, l=destinations.length; i { + console.error(err); + process.exit(1); +}); diff --git a/examples/node-red-contrib-flow-tester-gui-addon/src/flow-tester-gui-addon.html b/examples/node-red-contrib-flow-tester-gui-addon/src/flow-tester-gui-addon.html new file mode 100644 index 0000000..2d73dc7 --- /dev/null +++ b/examples/node-red-contrib-flow-tester-gui-addon/src/flow-tester-gui-addon.html @@ -0,0 +1,1663 @@ +// GUI test addon for flow tester action + + + diff --git a/examples/node-red-contrib-flow-tester-gui-addon/src/flow-tester-gui-addon.js b/examples/node-red-contrib-flow-tester-gui-addon/src/flow-tester-gui-addon.js new file mode 100644 index 0000000..faac1ad --- /dev/null +++ b/examples/node-red-contrib-flow-tester-gui-addon/src/flow-tester-gui-addon.js @@ -0,0 +1,617 @@ +module.exports = (RED) => { + "use strict"; + + // WebDriver object + const { remote } = require('webdriverio'); + const promisify = require("util").promisify; + + + // Browser instance + var browser_instances = []; + + // Get a browser instance + async function getBrowser(target) { + let key = "_default"; + if (target) { + key = target; + } + let browser = browser_instances[key] || null; + if (!browser) { + browser = await remote({ + capabilities: { + browserName: 'chrome' + } + }) + browser_instances[key] = browser; + } + return browser; + } + + async function testClose(){ + for (let key in browser_instances) { + let browser = browser_instances[key]; + await browser.deleteSession(); + delete browser_instances[key]; + } + } + + function testClear(){ + for (let key in browser_instances) { + delete browser_instances[key]; + } + } + + console.log("; Initialize flow tester gui addon"); + RED.plugins.registerPlugin('node-red-flow-tester-gui-addon', { + type: "flow-tester-addon", + actions: function () { + return [ + { + name: "addon:GUI-URL", + onTestStart: function(opt) { + return new Promise((resolve, reject) => { + testClear(); + resolve(); + }); + }, + onTestEnd: function(opt) { + return new Promise((resolve, reject) => { + // Close all browser instances after test + ;(async () => { + try { + await testClose(); + resolve(); + }catch(err){ + console.log(err); + resolve(); + //reject(err.message); + } + })() + }) + }, + execute: function(opt) { + // opt object contains: + // - data: optional data for action + // - node: target node + // - msg: message object + return new Promise((resolve, reject) => { + const action = opt.action; + // Perform an operation on an object with the specified instance name + (async () => { + try { + const browser = await getBrowser(action.target); + if (action.size) { + const size = action.size.split(','); + await browser.setWindowSize(Number(size[0]),Number(size[1])); + } + await browser.url(action.url); + resolve(); + }catch(err){ + console.log(err); + reject(err.message); + } + })() + }); + }, + }, + { + name: "addon:GUI-Keys", + onTestStart: function(opt) { + return new Promise((resolve, reject) => { + testClear(); + resolve(); + }); + }, + onTestEnd: function(opt) { + return new Promise((resolve, reject) => { + (async () => { + try { + await testClose(); + resolve(); + }catch(err){ + console.log(err); + resolve(); + } + })() + }); + }, + execute: function(opt) { + // opt object contains: + // - data: optional data for action + // - node: target node + // - msg: message object + return new Promise((resolve, reject) => { + const action = opt.action; + (async () => { + try { + const browser = await getBrowser(action.target); + if (action.key) { + const keys = action.key.split(','); + await browser.keys(keys); + } + resolve(); + }catch(err){ + console.log(err); + reject(err.message); + } + })() + }); + } + }, + { + name: "addon:GUI-saveScreenshot", + onTestStart: function(opt) { + return new Promise((resolve, reject) => { + testClear(); + resolve(); + }); + }, + onTestEnd: function(opt) { + return new Promise((resolve, reject) => { + (async () => { + try { + await testClose(); + resolve(); + }catch(err){ + console.log(err); + resolve(); + } + })() + }); + }, + execute: function(opt) { + // opt object contains: + // - data: optional data for action + // - node: target node + // - msg: message object + return new Promise((resolve, reject) => { + const action = opt.action; + (async () => { + try { + const browser = await getBrowser(action.target); + if (action.url) { + // Optional: Open the specified URL + await browser.url(action.url) + } + await browser.saveScreenshot(action.filepath); + resolve(); + }catch(err){ + console.log(err); + reject(err.message); + } + })() + }); + } + }, + { + name: "addon:GUI-setCookies", + onTestStart: function(opt) { + return new Promise((resolve, reject) => { + testClear(); + resolve(); + }); + }, + onTestEnd: function(opt) { + return new Promise((resolve, reject) => { + (async () => { + try { + await testClose(); + resolve(); + }catch(err){ + console.log(err); + resolve(); + } + })() + }); + }, + execute: function(opt) { + // opt object contains: + // - data: optional data for action + // - node: target node + // - msg: message object + return new Promise((resolve, reject) => { + const action = opt.action; + (async () => { + try { + const browser = await getBrowser(action.target); + const cookies = JSON.parse(action.cookie); + await browser.setCookies(cookies); + resolve(); + }catch(err){ + console.log(err); + reject(err.message); + } + })() + }); + } + }, + { + name: "addon:GUI-setTimeout", + onTestStart: function(opt) { + return new Promise((resolve, reject) => { + testClear(); + resolve(); + }); + }, + onTestEnd: function(opt) { + return new Promise((resolve, reject) => { + (async () => { + try { + await testClose(); + resolve(); + }catch(err){ + console.log(err); + resolve(); + } + })() + }); + }, + execute: function(opt) { + // opt object contains: + // - data: optional data for action + // - node: target node + // - msg: message object + return new Promise((resolve, reject) => { + const action = opt.action; + (async () => { + try { + const browser = await getBrowser(action.target); + const timeout = JSON.parse(action.timeout); + await browser.setTimeout(timeout); + resolve(); + }catch(err){ + console.log(err); + reject(err.message); + } + })() + }); + } + }, + { + name: "addon:GUI-waitUntil", + onTestStart: function(opt) { + return new Promise((resolve, reject) => { + testClear(); + resolve(); + }); + }, + onTestEnd: function(opt) { + return new Promise((resolve, reject) => { + (async () => { + try { + await testClose(); + resolve(); + }catch(err){ + console.log(err); + resolve(); + } + })() + }); + }, + execute: function(opt) { + // opt object contains: + // - data: optional data for action + // - node: target node + // - msg: message object + return new Promise((resolve, reject) => { + const action = opt.action; + let timeout = Number(action.timeout); + (async () => { + try { + const browser = await getBrowser(action.target); + await browser.waitUntil( + async () => ( + action.type === "str" && (await browser.$(action.selector).getText() === action.value) || + action.type === "re" && (new RegExp(action.value).test(await browser.$(action.selector).getText()))), + { + //timeout: Number(action.timeout), + timeout: timeout, + timeoutMsg: 'expected text to be different after ' + action.timeout + "ms" + } + ); + resolve(); + }catch(err){ + console.log(err); + reject(err.message); + } + })() + }); + } + }, + { + name: "addon:GUI-addValue", + onTestStart: function(opt) { + return new Promise((resolve, reject) => { + testClear(); + resolve(); + }); + }, + onTestEnd: function(opt) { + return new Promise((resolve, reject) => { + (async () => { + try { + await testClose(); + resolve(); + }catch(err){ + console.log(err); + resolve(); + } + })() + }); + }, + execute: function(opt) { + // opt object contains: + // - data: optional data for action + // - node: target node + // - msg: message object + return new Promise((resolve, reject) => { + const action = opt.action; + (async () => { + try { + const browser = await getBrowser(action.target); + await browser.$(action.selector).addValue(action.value); + resolve(); + }catch(err){ + console.log(err); + reject(err.message); + } + })() + }); + } + }, + { + name: "addon:GUI-clearValue", + onTestStart: function(opt) { + return new Promise((resolve, reject) => { + testClear(); + resolve(); + }); + }, + onTestEnd: function(opt) { + return new Promise((resolve, reject) => { + (async () => { + try { + await testClose(); + resolve(); + }catch(err){ + console.log(err); + resolve(); + } + })() + }); + }, + execute: function(opt) { + // opt object contains: + // - data: optional data for action + // - node: target node + // - msg: message object + return new Promise((resolve, reject) => { + const action = opt.action; + (async () => { + try { + const browser = await getBrowser(action.target); + await browser.$(action.selector).clearValue(); + resolve(); + }catch(err){ + console.log(err); + reject(err.message); + } + })() + }); + } + }, + { + name: "addon:GUI-click", + onTestStart: function(opt) { + return new Promise((resolve, reject) => { + testClear(); + resolve(); + }); + }, + onTestEnd: function(opt) { + return new Promise((resolve, reject) => { + (async () => { + try { + await testClose(); + resolve(); + }catch(err){ + console.log(err); + resolve(); + } + })() + }); + }, + execute: function(opt) { + // opt object contains: + // - data: optional data for action + // - node: target node + // - msg: message object + return new Promise((resolve, reject) => { + const action = opt.action; + (async () => { + try { + const browser = await getBrowser(action.target); + if (action.clickkind === 'single') { + await browser.$(action.selector).click(); + } else { + await browser.$(action.selector).doubleClick(); + } + resolve(); + }catch(err){ + console.log(err); + reject(err.message); + } + })() + }); + } + }, + { + name: "addon:GUI-getValue", + onTestStart: function(opt) { + return new Promise((resolve, reject) => { + testClear(); + resolve(); + }); + }, + onTestEnd: function(opt) { + return new Promise((resolve, reject) => { + (async () => { + try { + await testClose(); + resolve(); + }catch(err){ + console.log(err); + resolve(); + } + })() + }); + }, + execute: function(opt) { + // opt object contains: + // - data: optional data for action + // - node: target node + // - msg: message object + + return new Promise((resolve, reject) => { + const action = opt.action; + const nid = opt.node; + const msg = opt.msg; + const node = nid ? RED.nodes.getNode(nid) : null; + let val = "" + if (!node) { + console.log("Must be run in a node instance test."); + reject("Must be run in a node instance test."); + return; + } + + (async () => { + try { + const browser = await getBrowser(action.target); + if (action.getkind === 'attr') { + val = await browser.$(action.selector).getAttribute(action.getname); + } else if (action.getkind === 'prop') { + val = await browser.$(action.selector).getProperty(action.getname); + } else if (action.getkind === 'tag') { + val = await browser.$(action.selector).getTagName(); + } else if (action.getkind === 'text') { + val = await browser.$(action.selector).getText(); + } else if (action.getkind === 'val') { + val = await browser.$(action.selector).getValue(); + } + + switch (action.type) { + //case "msg": + // if (msg) { + // RED.util.setMessageProperty(msg, action.value, value); + // } + // break; + case "global": + case "flow": + const contextKey = RED.util.parseContextStore(action.value); + if (/\[msg/.test(contextKey.key)) { + // The key has a nest msg. reference to evaluate first + contextKey.key = RED.util.normalisePropertyExpression(contextKey.key, msg, true) + } + const target = node.context()[action.type]; + const set = promisify(target.set); + set(contextKey.key, val, contextKey.store); + break; + default: + console.log("unexpected value type: "+ action.type); + reject("unexpected value type: "+ action.type); + } + resolve(); + }catch(err){ + console.log(err); + reject(err.message); + } + })() + }); + } + }, + { + name: "addon:GUI-moveTo", + onTestStart: function(opt) { + return new Promise((resolve, reject) => { + testClear(); + resolve(); + }); + }, + onTestEnd: function(opt) { + return new Promise((resolve, reject) => { + (async () => { + try { + await testClose(); + resolve(); + }catch(err){ + console.log(err); + resolve(); + } + })() + }); + }, + execute: function(opt) { + // opt object contains: + // - data: optional data for action + // - node: target node + // - msg: message object + return new Promise((resolve, reject) => { + const action = opt.action; + (async () => { + try { + const browser = await getBrowser(action.target); + await browser.$(action.selector).moveTo(); + resolve(); + }catch(err){ + console.log(err); + reject(err.message); + } + })() + }); + } + }, + { + name: "addon:GUI-setValue", + onTestStart: function(opt) { + return new Promise((resolve, reject) => { + testClear(); + resolve(); + }); + }, + onTestEnd: function(opt) { + return new Promise((resolve, reject) => { + (async () => { + try { + await testClose(); + resolve(); + }catch(err){ + console.log(err); + resolve(); + } + })() + }); + }, + execute: function(opt) { + // opt object contains: + // - data: optional data for action + // - node: target node + // - msg: message object + return new Promise((resolve, reject) => { + const action = opt.action; + (async () => { + try { + const browser = await getBrowser(action.target); + await browser.$(action.selector).setValue(action.value); + resolve(); + }catch(err){ + console.log(err); + reject(err.message); + } + })() + }); + } + } + ]; + } + }); +}; diff --git a/examples/node-red-contrib-flow-tester-gui-addon/src/locales/en-US/flow-tester-gui-addon.json b/examples/node-red-contrib-flow-tester-gui-addon/src/locales/en-US/flow-tester-gui-addon.json new file mode 100644 index 0000000..3f29600 --- /dev/null +++ b/examples/node-red-contrib-flow-tester-gui-addon/src/locales/en-US/flow-tester-gui-addon.json @@ -0,0 +1,4 @@ +{ + "label": { + } +} diff --git a/examples/node-red-contrib-flow-tester-gui-addon/src/style.css b/examples/node-red-contrib-flow-tester-gui-addon/src/style.css new file mode 100644 index 0000000..e69de29 diff --git a/examples/node-red-contrib-flow-tester-gui-addon/test/test.js b/examples/node-red-contrib-flow-tester-gui-addon/test/test.js new file mode 100644 index 0000000..2a4de75 --- /dev/null +++ b/examples/node-red-contrib-flow-tester-gui-addon/test/test.js @@ -0,0 +1,2 @@ +describe("Flow tester gui addon tests", function() { +}); diff --git a/nodes/flow-test-config.html b/nodes/flow-test-config.html new file mode 100644 index 0000000..5f9141c --- /dev/null +++ b/nodes/flow-test-config.html @@ -0,0 +1,46 @@ + + + + + diff --git a/nodes/flow-test-config.js b/nodes/flow-test-config.js new file mode 100644 index 0000000..81c3303 --- /dev/null +++ b/nodes/flow-test-config.js @@ -0,0 +1,13 @@ +module.exports = function(RED) { + "use strict"; + + function FlowTestConfig(n) { + RED.nodes.createNode(this, n); + const node = this; + node.name = n.name; + node.tests = n.tests; + node.currentTest = n.currentTest; + } + + RED.nodes.registerType("flow-test-config", FlowTestConfig); +}; diff --git a/package.json b/package.json index a4fa2e9..8425ee9 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,17 @@ "version": ">=2.0.0", "plugins": { "flow-tester": "dist/flow-tester.js" - } + }, + "nodes": { + "flow-test-config": "nodes/flow-test-config.js" + } }, + "contributors": [ + { + "name": "Hiroyasu Nishiyama", + "email": "hiroyasu.nishiyama.uq@hitachi.com" + } + ], "devDependencies": { "fs-extra": "^10.0.0", "html-minifier": "^4.0.0", diff --git a/src/flow-tester.html b/src/flow-tester.html index 34150b7..932594f 100644 --- a/src/flow-tester.html +++ b/src/flow-tester.html @@ -1,17 +1,1929 @@ - + diff --git a/src/flow-tester.js b/src/flow-tester.js index fc1c93d..7f3d538 100644 --- a/src/flow-tester.js +++ b/src/flow-tester.js @@ -1,7 +1,867 @@ module.exports = (RED) => { + "use strict"; + + const vm = require("vm"); + const assert = require("assert"); + const promisify = require("util").promisify; + + let maxActions = 0; // max number of actions + let actionCount = 0; // currently executed actions count + + let numberOfChecks = 0; // number of checks in current test case + let successActions = []; // info. of successful actions + let failActions = []; // info. of failed actions + + let resolveWait = (() => {}); // resolve function for wait promise + let currentWait = null; // handle for wait action + const TIMEOUT = 10; // timeout period of flow execution + + // actions map: event name -> node id -> action info + const actionMap = { + setup: [], // executed on test start + cleanup: [], // executed on test end + + recv: [], // executed on message receive + stub: [], // executed on message receive to make + // stubbed execution of target node + send: [], // executed on message send + }; + + var addonList = []; + + /** + * Add event log to editor sidebar log area + * @param {string} msg - message to be added + */ + function log(msg) { + RED.comms.publish("flow-test:log", msg); + } + + /** + * Show notification on editor + * @param {string} msg - message to be shown + */ + function notify(msg) { + RED.comms.publish("flow-test:notify", msg); + } + + /** + * Notify that execution reached the maxnimum number + */ + function reportMaxActions() { + RED.comms.publish("flow-test:maxActions", {}); + } + + /** + * Notify result of match action + * @param {Object} result - result information object with: + * result: true/false + * suiteID: test suite id + * testID: test case id + */ + function sendMatchResult(result) { + RED.comms.publish("flow-test:match-result", result); + } + + /** + * Execute send action: send value to target node + * @param {string} target - target node id + * @param {any} value - payload value + * @returns {Promise} + */ + function executeSend(target, value) { + log(`send: ${JSON.stringify(value)} to ${target}`); + const node = RED.nodes.getNode(target); + node.receive({ + payload: value + }); + return Promise.resolve(true); + } + + /** + * Execute click action: click button of target node + * @param {string} target - target node id + * @returns {Promise} + */ + function executeClick(target) { + log(`click: ${target}`); + RED.comms.publish("flow-test:click", target); + return Promise.resolve(true); + } + + /** + * Execute log action: log message + * @param {string} value - logged string + * @returns {Promise} + */ + function executeLog(value) { + log(`log: ${value}`); + RED.log.info(value); + return Promise.resolve(true); + } + + /** + * Execute set action: set value to msg, flow context, or global context + * @param {Object} dst - destination location (type, value) + * @param {Object} src - source value (type, value) + * @param {string} nid - target node id + * @param {Object} msg - target message + * @returns {Promise} + */ + function executeSet(dst, src, nid, msg) { + log(`set: ${dst.type}.${dst.value} = ${JSON.stringify(src.value)}`); + const node = nid ? RED.nodes.getNode(nid) : null; + let val = src.value || ""; + switch (src.type) { + case "str": + break; + case "num": + val = Number(val); + break; + case "bool": + val = ((val === true) || (val === "true")); + break; + case "json": + val = JSON.parse(val); + break; + case "bin": + val = Buffer.from(JSON.parse(val)); + break; + case "re": + break; + case "date": + val = Date.now(); + break; + case "jsonata": + if (!src.exp) { + src.exp = RED.util.prepareJSONataExpression(val, node); + } + val = RED.util.evaluateJSONataExpression(src.exp, msg); + break; + case "env": + val = RED.util.evaluateNodeProperty(val, "env", node); + break; + } + switch (dst.type) { + case "msg": + if (msg) { + RED.util.setMessageProperty(msg, dst.value, value); + } + break; + case "global": + case "flow": + const contextKey = RED.util.parseContextStore(dst.value); + if (/\[msg/.test(contextKey.key)) { + // The key has a nest msg. reference to evaluate first + contextKey.key = RED.util.normalisePropertyExpression(contextKey.key, msg, true) + } + const target = node.context()[dst.type]; + const set = promisify(target.set); + return set(contextKey.key, val, contextKey.store); + default: + throw new Error("unexpected value type: "+src.type); + } + return Promise.resolve(true); + } + + /** + * Report result of validation + * @param {boolean} result - result of validation + * @param {Number} index - index in actions list + * @param {string} sid - test suite id + * @param {string} tid - test case id + */ + function reportResult(result, index, sid, tid) { + const info = { + result: result, + suiteID: sid, + testID: tid, + }; + var actInfo = { + index: index, + suiteID: sid, + testID: tid, + }; + if (result) { + successActions.push(actInfo); + } + else { + failActions.push(actInfo); + } + sendMatchResult(info); + if ((successActions.length +failActions.length) >= numberOfChecks) { + if (currentWait) { + // clear timeout if all validations processed + clearTimeout(currentWait); + } + resolveWait(true); + resolveWait = (()=>{}); + } + } + + /** + * Execute match action: validates message value + * @param {any} val - expected value + * @param {Object} msg - message to be checked + * @param {Number} index - index in actions list + * @param {string} sid - test suite id + * @param {string} tid - test case id + * @returns {Promise} + */ + function executeMatch(val, msg, index, sid, tid) { + log(`match: ${val}`); + let result = true; + try { + assert.deepEqual(val, msg.payload); + } + catch (e) { + result = false; + } + reportResult(result, index, sid, tid); + return Promise.resolve(true); + } + + /** + * Execute wait action: wait for specified period + * @param {Number} wait - wait period in ms + * @returns {Promise} + */ + function executeWait(wait) { + log(`wait: ${wait}`); + return new Promise((resolve) => { + resolveWait = resolve; + currentWait = setTimeout(function () { + currentWait = null; + resolve(true); + }, wait); + }); + } + + /** + * Execute function action: execute JavaScript code + * @param {string} code - JavaScript code text + * @param {boolean} performCheck - true if the code performs check + * @param {string} nid - target node id + * @param {Object} msg - message + * @param {Number} index - index in actions list + * @param {string} sid - test suid id + * @param {string} tid - test case id + * @returns {Promise} + */ + function executeFunction(code, performCheck, nid, msg, + index, sid, tid) { + log(`function: ${code}`); + const node = nid ? RED.nodes.getNode(nid) : null; + const ctxDef = { + console: console, + node: node, + msg: msg + }; + if (performCheck) { + // `check` function for validation + ctxDef.check = ((result) => { + reportResult(result, index, sid, tid); + }); + } + const ctx = vm.createContext(ctxDef); + const script = ` +(function () { + try { + ${code}; + } + catch (e) { + return Promise.reject(e); + } + return Promise.resolve(true); +})()`; + return vm.runInContext(script, ctx); + } + + /** + * Find addon action definition + * @param {string} name - name of action + * @returns {Object} action definition + */ + function findAddonAction(name) { + for (let addon of addonList) { + for (let def of addon.actions()) { + if (def.name === name) { + return def; + } + } + } + return null; + } + + /** + * Execute action + * @param {Object} action - action description object + * @param {Object} node - target node + * @param {Object} msg - message + * @returns {Promise} + */ + function executeAction(action, node, msg) { + actionCount++; + if (actionCount >= maxActions) { + reportMaxActions(); + throw new Error("action count exceeded limit"); + } + try { + switch (action.kind) { + case "send": + return executeSend(action.target, action.value); + case "click": + return executeClick(action.target); + case "log": + return executeLog(action.value); + case "set": + return executeSet(action.dst, action.src, node, msg); + case "match": + return executeMatch(action.value, msg, + action.index, action.suiteID, action.testID); + case "wait": + return executeWait(action.wait); + case "function": + return executeFunction(action.code, action.performCheck, + node, msg, + action.index, action.suiteID, action.testID); + default: + const addon = findAddonAction(action.kind); + if (addon) { + return executeAddonAction(addon, action, node, msg); + } + else { + return Promise.reject(new Error("unexpected kind of action: " +action.kind)); + } + } + } + catch (e) { + const msg = "error: "+ e; + log(msg); + console.log(msg); + } + return Promise.resolve(true); + } + + /** + * Process actions defined for event + * @param {array} actions - array of actions + * @param {Object} node - target node + * @param {Object} msg - message + * @returns {Promise} + */ + function processEvent(actions, node, msg) { + let promise = Promise.resolve(true); + if (actions) { + for (let action of actions) { + promise = promise.then(() => + executeAction(action, node, msg) + ); + } + } + return promise; + } + + /** + * Clear hooks and actions map + */ + function clearActions() { + RED.hooks.remove("onReceive.flow-test"); + RED.hooks.remove("preRoute.flow-test"); + RED.hooks.remove("onSend.flow-test"); + + actionMap.setup = []; + actionMap.cleanup = []; + + actionMap.recv = []; + actionMap.stub = []; + actionMap.send = []; + } + + /** + * Initialize flow testing plugin + * @returns {Promise} + */ + function init() { + if (currentWait) { + clearTimeout(currentWait); + } + clearActions(); + + // register hook for recv event + RED.hooks.add("onReceive.flow-test", (event, done) => { + const dst = event.destination.id; + const actions = actionMap.recv[dst]; + + if (actions) { + const msg = event.msg; + const promise = processEvent(actions, dst, msg); + promise.then(() => { + done(); + }).catch((err) => { + log(err.toString()); + done(); + }); + } + else { + done(); + } + }); + + // register hook for stub event + RED.hooks.add("preRoute.flow-test", (event, done) => { + const src = event.source.id; + const actions = actionMap.stub[src]; + + if (actions) { + const msg = event.msg; + const promise = processEvent(actions, src, msg); + promise.then(() => { + done(false); + }).catch((err) => { + log(err); + done(false); + }); + } + else { + done(); + } + }); + + // register hook for send event + RED.hooks.add("onSend.flow-test", (events, done) => { + let promise = Promise.resolve(); + for (let event of events) { + const src = event.source.id; + const msg = event.msg; + const actions = actionMap.send[src]; + + if (actions) { + promise = promise.then(() => { + return processEvent(actions, src, msg); + }); + }; + } + promise.then(() => { + done(); + }).catch((err) => { + log(err); + done(false); + }); + }); + + + return Promise.resolve(true); + } + + + /** + * Register actions to actionMap + * @param {string} event - event name + * @param {array} actions - list of actions + * @param {string} sid - test suite id + * @param {string} tid - test case id + * @returns {Promise} + */ + function registerActions(event, actions, sid, tid) { + switch (event) { + case "setup": + case "cleanup": + case "recv": + case "stub": + case "send": + Object.entries(actions).forEach(([node, acts]) => { + if (acts.length > 0) { + actionMap[event][node] = acts; + let i = 0; + acts.forEach((act) => { + act.index = i; + act.suiteID = sid; + act.testID = tid; + i++; + }); + } + }); + break; + default: + throw new Error("unexpected event: " +event); + } + return Promise.resolve(true); + } + + function executeTestStartAddon() { + let promise = Promise.resolve(); + for (let addon of addonList) { + for (let action of addon.actions()) { + const cb = action.onTestStart; + if (cb) { + promise = promise.then(() => { + return cb(); + }); + } + } + } + return promise; + } + + + function executeTestEndAddon() { + let promise = Promise.resolve(); + for (let addon of addonList) { + for (let action of addon.actions()) { + const cb = action.onTestEnd; + if (cb) { + promise = promise.then(() => { + return cb(); + }); + } + } + } + return promise; + } + + /** + * Initialize & process actions for setup event + * @param {Number} maxActs - max number of actions + * @returns {Promise} + */ + function setup(maxActs) { + maxActions = maxActs; + actionCount = 0; + successActions = []; + failActions = []; + // execute addon start callbacks + let promise = executeTestStartAddon(); + // execute global actions first + const actions = actionMap.setup["_global_"]; + promise = promise.then(() => { + return processEvent(actions, null, null); + }); + // then, execute nodes events + for (let id of Object.keys(actionMap.setup)) { + if (id !== "_global_") { + const actions = actionMap.setup[id]; + promise = promise.then(() => { + const node = RED.nodes.getNode(id); + return processEvent(actions, node, null); + }); + } + } + return promise; + } + + /** + * Process actions for cleanup event + * @param {Number} maxActs - max number of actions + * @returns {Promise} + */ + function cleanup() { + // execute global actions first + const actions = actionMap.cleanup["_global_"]; + let promise = processEvent(actions, null, null); + // then, execute nodes events + for (let id of Object.keys(actionMap.setup)) { + if (id !== "_global_") { + const actions = actionMap.cleanup[id]; + promise = promise.then(() => { + const node = RED.nodes.getNode(id); + return processEvent(actions, node, null); + }); + } + } + promise = promise.then(() => { + return executeTestEndAddon(); + }); + // finally, clear actions + return promise.then(() => { + clearActions(); + }); + } + + function registerAddon(addon) { + addonList.push(addon); + } + + function executeAddonAction(addon, action, node, msg) { + const exec = addon.execute; + const report = action.performCheck; + const index = action.index; + const sid = action.suiteID; + const tid = action.testID;; + if (exec) { + try { + return exec({ + action: action, + node: node, + msg: msg + }).then(() => { + if (report) { + reportResult(true, index, sid, tid); + } + }).catch(() => { + if (report) { + reportResult(false, index, sid, tid); + } + });; + } + catch (e) { + const msg = "error:"+ e; + log(msg); + console.log(msg); + if (report) { + reportResult(false, index, sid, tid); + } + return Promise.reject(); + } + }; + return Promise.resolve(); + } + + // Register flow testing plugin RED.plugins.registerPlugin('node-red-flow-tester', { settings: { "*": { exportable: true } + }, + onadd: () => { + const routeAuthHandler = RED.auth.needsPermission("flow-tester.write"); + + var plugins = RED.plugins.getByType('flow-tester-addon'); + plugins.forEach(function(plugin) { + registerAddon(plugin); + }) + RED.events.on("registry:plugin-added", function (id) { + var plugin = RED.plugins.get(id); + if (plugin.type === "flow-tester-addon") { + registerAddon(plugin); + } + }); + + + // Internal APIs for flow testing + RED.httpAdmin.post( + "/flow-tester/executeAction/:action", + routeAuthHandler, + (req, res) => { + try { + const kind = req.params.action; + const opt = req.body; + let promise = Promise.resolve(true); + switch (kind) { + case "init": + // initialize runtime part of flow-tester plugin + promise = init(); + break; + case "registerActions": + promise = registerActions(opt.event, opt.actions, opt.suiteID, opt.testID); + break; + case "setup": + promise = setup(opt.maxActions); + break; + case "cleanup": + // cleanup runtime part of flow-tester plugin + promise = cleanup(); + break; + case "send": + // send a message to specified node + promise = executeSend(opt.target, opt.value); + break; + case "click": + // click the button of target node + // nothing to do + break; + case "log": + // log message + promise = executeLog(opt.value); + break; + case "set": + // set context or environment variable + promise = executeSet(opt.dst, opt.src); + break; + case "wait": + // wait for specified time + promise = executeWait(opt.wait); + break; + case "function": + // execute JavaScript code + promise = executeFunction(opt.code); + break; + default: + const addon = findAddonAction(action.kind); + if (addon) { + promise = executeAddonAction(addon, action, node, msg); + } + else { + console.log("unexpected action kind: ", kind); + } + break; + } + promise.then((result) => { + res.json(result || {}); + }).catch((err) => { + const msg = "error: " +err; + console.log(msg); + notify(msg); + res.sendStatus(400); + }); + } + catch (e) { + console.log(e.stack); + res.sendStatus(400); + } + }); + + // API for getting list of test cases + RED.httpAdmin.get( + "/flow-tester/testCase", + routeAuthHandler, + (req, res) => { + const suiteList = []; + const nodes = []; + RED.nodes.eachNode((node) => { + if (node.type === "flow-test-config") { + nodes.push(node); + } + }); + nodes.forEach((node) => { + const testList = []; + const suite = { + id: node.id, + name: node.name, + tests: testList + }; + suiteList.push(suite); + const cases = node.tests; + cases.forEach((testCase) => { + const test = { + id: testCase.id, + name: testCase.name + }; + testList.push(test); + }); + }); + res.json(suiteList); + } + ); + + /** + * Find test information for specified ids + * @param {Object} suite - test suite information + * @param {string} tid - test case id + * @returns {Object} test information + */ + function findTest(suite, tid) { + if (suite) { + const tests = suite.tests; + const test = tests.find((t) => { + return (t.id === tid); + }); + if (test) { + return test; + } + } + return null; + } + + /** + * Register actions for a test case + * @param {Object} suite - test suite information + * @param {Object} test - test case information + * @returns {Promise} + */ + function registerTestActions(suite, test) { + log("Running test: " +test.name); + const sid = suite.id; + const tid = test.id; + let promise = Promise.resolve(); + const actions = test.actions; + var events = Object.keys(actions); + events.forEach(function (event) { + promise = promise.then(() => { + return registerActions(event, + actions[event], + sid, tid); + }); + }); + return promise; + } + + /** + * Count number of expected checks for test case + * @param {Object} map - action definition + * @returns {Number} number of checks + */ + function countNumberOfChecks(map) { + var count = 0; + var events = Object.keys(map); + events.forEach(function (event) { + var evMap = map[event]; + var nodes = Object.keys(evMap); + nodes.forEach(function (node) { + var acts = evMap[node]; + acts.forEach(function (act) { + if (act.performCheck) { + count++; + } + }); + }); + }); + return count; + } + + function clearMatchResults() { + RED.comms.publish("flow-test:clear-match-result", {}); + } + + // API for executing a test case + RED.httpAdmin.get( + "/flow-tester/runTestCase/:suite/:test", + routeAuthHandler, + (req, res) => { + const params = req.params; + const sid = params.suite; + const tid = params.test; + const suite = RED.nodes.getNode(sid); + const test = findTest(suite, tid); + + numberOfChecks = countNumberOfChecks(test.actions); + clearMatchResults(); + + if (test) { + init().then(() => { + return registerTestActions(suite, test); + }).then(() => { + return setup(suite.maxActions) + }).then(() => { + var timeout = (test.timeout || TIMEOUT) *1000; + return executeWait(timeout); + }).then(() => { + return cleanup(); + }).then(() => { + const result = { + result: { + all: numberOfChecks, + success: successActions.length, + fail: failActions.length + }, + info: { + success: successActions, + fail: failActions + } + }; + res.json(result); + }).catch((err) => { + const msg = "error: " +err; + console.log(msg); + res.sendStatus(400); + }); + return; + } + res.sendStatus(400); + } + ); + + }, + onremove: () => { } }); }; diff --git a/src/style.css b/src/style.css index a4c0fb7..160bedc 100644 --- a/src/style.css +++ b/src/style.css @@ -3,5 +3,21 @@ height: 100%; display: flex; flex-direction: column; - background: red; } + +.red-ui-suite-label { +} + +.red-ui-suite-label-control { + position: absolute; + top: 0px; + right: 0px; + margin-top: 5px; + margin-right: 5px; + display: none; +} + +.red-ui-suite-label:hover .red-ui-suite-label-control { + display: inline; +} +