diff --git a/packages/flowtest-cli/LICENSE.md b/packages/flowtest-cli/LICENSE.md new file mode 100644 index 0000000..da25496 --- /dev/null +++ b/packages/flowtest-cli/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Sajal Jain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/flowtest-cli/README.md b/packages/flowtest-cli/README.md new file mode 100644 index 0000000..7000c18 --- /dev/null +++ b/packages/flowtest-cli/README.md @@ -0,0 +1,64 @@ +# flowtestai-cli + +With FlowTestAI CLI, you can now run your end to end flows, constructed using FlowTestAI, directly from command line. + +This makes it easier to run your tests in different environments, automate your testing process, and integrate your tests with your continuous integration and deployment workflows. + +## Installation + +To install the FlowTestAI CLI, use the node package manager of your choice, such as NPM: + +```bash +npm install -g flowtestai +``` + +## Getting started + +Navigate to the root directory of your collection, and then run: + +```bash +flow run help +``` + +This command will give you various options you can use to run a flow. You can also run a single flow by specifying its filename with the `--file` or `-f` option: + +```bash +flow run -f test.flow +``` + +Or run a requests inside a subfolder: + +```bash +flow run -f folder/subfolder/test.flow +``` + +If you need to use an environment, you can specify it with the `--env` or `-e` option: + +```bash +flow run -f test.flow -e environments/test.env +``` + +If you need to publish the results of your flow runs for further analysis, you can specify the `-s` option. Request your access key pairs from https://flowtest-ai.vercel.app/ and then run export $FLOWTEST_ACCESS_ID and $FLOWTEST_ACCESS_KEY before publishing: + +```bash +flow run -f test.flow -e environments/test.env -s +``` + +## Demo + +![demo1](assets/demo1.png) +![demo2](assets/demo2.png) + +## Support + +If you encounter any issues or have any feedback or suggestions, please raise them on our [GitHub repository](https://github.com/FlowTestAI/FlowTest) + +Thank you for using FlowTestAI CLI! + +## Changelog + +See [https://github.com/FlowTestAI/FlowTest/releases](https://github.com/FlowTestAI/FlowTest/releases) + +## License + +[MIT](LICENSE.md) diff --git a/packages/flowtest-cli/assets/demo1.png b/packages/flowtest-cli/assets/demo1.png new file mode 100644 index 0000000..923b3de Binary files /dev/null and b/packages/flowtest-cli/assets/demo1.png differ diff --git a/packages/flowtest-cli/assets/demo2.png b/packages/flowtest-cli/assets/demo2.png new file mode 100644 index 0000000..6b77abd Binary files /dev/null and b/packages/flowtest-cli/assets/demo2.png differ diff --git a/packages/flowtest-cli/bin/index.js b/packages/flowtest-cli/bin/index.js index f719dc1..ac79027 100755 --- a/packages/flowtest-cli/bin/index.js +++ b/packages/flowtest-cli/bin/index.js @@ -3,8 +3,8 @@ const yargs = require('yargs/yargs'); const { hideBin } = require('yargs/helpers'); const chalk = require('chalk'); -const readFile = require('../../flowtest-electron/src/utils/filemanager/readfile'); -const { serialize } = require('../../flowtest-electron/src/utils/flowparser/parser'); +const readFile = require('../utils/readfile'); +const { serialize } = require('../utils/flowparser/parser'); const { Graph } = require('../graph/Graph'); const { cloneDeep } = require('lodash'); const dotenv = require('dotenv'); diff --git a/packages/flowtest-cli/graph/Graph.js b/packages/flowtest-cli/graph/Graph.js index 533efd9..9d830db 100644 --- a/packages/flowtest-cli/graph/Graph.js +++ b/packages/flowtest-cli/graph/Graph.js @@ -7,10 +7,10 @@ const requestNode = require('./compute/requestNode'); const setVarNode = require('./compute/setvarnode'); const chalk = require('chalk'); const path = require('path'); -const readFile = require('../../flowtest-electron/src/utils/filemanager/readfile'); -const { serialize } = require('../../flowtest-electron/src/utils/flowparser/parser'); const Node = require('./compute/node'); const { LogLevel } = require('./GraphLogger'); +const readFile = require('../utils/readfile'); +const { serialize } = require('../utils/flowparser/parser'); class nestedFlowNode extends Node { constructor(nodes, edges, startTime, timeout, initialEnvVars, logger) { diff --git a/packages/flowtest-cli/package.json b/packages/flowtest-cli/package.json index 65dd835..d776262 100644 --- a/packages/flowtest-cli/package.json +++ b/packages/flowtest-cli/package.json @@ -1,13 +1,20 @@ { - "name": "flowtest-cli", - "version": "1.0.0", + "name": "flowtestai", + "version": "1.0.2", "description": "CLI to run flow from command line", "main": "bin/index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "bin": { - "flow": "./bin/index.js" + "flow": "bin/index.js" + }, + "bugs": { + "url": "https://github.com/FlowTestAI/FlowTest/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/FlowTestAI/FlowTest.git" }, "author": "Sajal Jain ", "license": "MIT", diff --git a/packages/flowtest-cli/utils/flowparser/AssertNode.js b/packages/flowtest-cli/utils/flowparser/AssertNode.js new file mode 100644 index 0000000..45895f4 --- /dev/null +++ b/packages/flowtest-cli/utils/flowparser/AssertNode.js @@ -0,0 +1,34 @@ +const { Node } = require('./Node'); + +class AssertNode extends Node { + constructor() { + super('assertNode'); + } + + serialize(id, data, metadata) { + return { + id, + type: this.type, + data, + ...metadata, + }; + } + + deserialize(node) { + const id = node.id; + const data = node.data; + delete node.id; + delete node.data; + const metadata = node; + + return { + id, + data, + metadata, + }; + } +} + +module.exports = { + AssertNode, +}; diff --git a/packages/flowtest-cli/utils/flowparser/AuthNode.js b/packages/flowtest-cli/utils/flowparser/AuthNode.js new file mode 100644 index 0000000..a3f4e21 --- /dev/null +++ b/packages/flowtest-cli/utils/flowparser/AuthNode.js @@ -0,0 +1,34 @@ +const { Node } = require('./Node'); + +class AuthNode extends Node { + constructor() { + super('authNode'); + } + + serialize(id, data, metadata) { + return { + id, + type: this.type, + data, + ...metadata, + }; + } + + deserialize(node) { + const id = node.id; + const data = node.data; + delete node.id; + delete node.data; + const metadata = node; + + return { + id, + data, + metadata, + }; + } +} + +module.exports = { + AuthNode, +}; diff --git a/packages/flowtest-cli/utils/flowparser/DelayNode.js b/packages/flowtest-cli/utils/flowparser/DelayNode.js new file mode 100644 index 0000000..72dc9a4 --- /dev/null +++ b/packages/flowtest-cli/utils/flowparser/DelayNode.js @@ -0,0 +1,34 @@ +const { Node } = require('./Node'); + +class DelayNode extends Node { + constructor() { + super('delayNode'); + } + + serialize(id, data, metadata) { + return { + id, + type: this.type, + data, + ...metadata, + }; + } + + deserialize(node) { + const id = node.id; + const data = node.data; + delete node.id; + delete node.data; + const metadata = node; + + return { + id, + data, + metadata, + }; + } +} + +module.exports = { + DelayNode, +}; diff --git a/packages/flowtest-cli/utils/flowparser/NestedFlowNode.js b/packages/flowtest-cli/utils/flowparser/NestedFlowNode.js new file mode 100644 index 0000000..4088e0e --- /dev/null +++ b/packages/flowtest-cli/utils/flowparser/NestedFlowNode.js @@ -0,0 +1,34 @@ +const { Node } = require('./Node'); + +class NestedFlowNode extends Node { + constructor() { + super('flowNode'); + } + + serialize(id, data, metadata) { + return { + id, + type: this.type, + data, + ...metadata, + }; + } + + deserialize(node) { + const id = node.id; + const data = node.data; + delete node.id; + delete node.data; + const metadata = node; + + return { + id, + data, + metadata, + }; + } +} + +module.exports = { + NestedFlowNode, +}; diff --git a/packages/flowtest-cli/utils/flowparser/Node.js b/packages/flowtest-cli/utils/flowparser/Node.js new file mode 100644 index 0000000..6b079e8 --- /dev/null +++ b/packages/flowtest-cli/utils/flowparser/Node.js @@ -0,0 +1,17 @@ +class Node { + constructor(type) { + this.type = type; + } + + serialize(id, data, metadata) { + throw new Error('Serialize method must be implemented by subclasses'); + } + + deserialize(node) { + throw new Error('Deserialize method must be implemented by subclasses'); + } +} + +module.exports = { + Node, +}; diff --git a/packages/flowtest-cli/utils/flowparser/OutputNode.js b/packages/flowtest-cli/utils/flowparser/OutputNode.js new file mode 100644 index 0000000..e329477 --- /dev/null +++ b/packages/flowtest-cli/utils/flowparser/OutputNode.js @@ -0,0 +1,34 @@ +const { Node } = require('./Node'); + +class OutputNode extends Node { + constructor() { + super('outputNode'); + } + + serialize(id, data, metadata) { + return { + id, + type: this.type, + data, + ...metadata, + }; + } + + deserialize(node) { + const id = node.id; + const { ['output']: _, ...data } = node.data; + delete node.id; + delete node.data; + const metadata = node; + + return { + id, + data, + metadata, + }; + } +} + +module.exports = { + OutputNode, +}; diff --git a/packages/flowtest-cli/utils/flowparser/RequestNode.js b/packages/flowtest-cli/utils/flowparser/RequestNode.js new file mode 100644 index 0000000..91c083a --- /dev/null +++ b/packages/flowtest-cli/utils/flowparser/RequestNode.js @@ -0,0 +1,34 @@ +const { Node } = require('./Node'); + +class RequestNode extends Node { + constructor() { + super('requestNode'); + } + + serialize(id, data, metadata) { + return { + id, + type: this.type, + data, + ...metadata, + }; + } + + deserialize(node) { + const id = node.id; + const data = node.data; + delete node.id; + delete node.data; + const metadata = node; + + return { + id, + data, + metadata, + }; + } +} + +module.exports = { + RequestNode, +}; diff --git a/packages/flowtest-cli/utils/flowparser/SetVarNode.js b/packages/flowtest-cli/utils/flowparser/SetVarNode.js new file mode 100644 index 0000000..08686a5 --- /dev/null +++ b/packages/flowtest-cli/utils/flowparser/SetVarNode.js @@ -0,0 +1,34 @@ +const { Node } = require('./Node'); + +class SetVarNode extends Node { + constructor() { + super('setVarNode'); + } + + serialize(id, data, metadata) { + return { + id, + type: this.type, + data, + ...metadata, + }; + } + + deserialize(node) { + const id = node.id; + const data = node.data; + delete node.id; + delete node.data; + const metadata = node; + + return { + id, + data, + metadata, + }; + } +} + +module.exports = { + SetVarNode, +}; diff --git a/packages/flowtest-cli/utils/flowparser/StartNode.js b/packages/flowtest-cli/utils/flowparser/StartNode.js new file mode 100644 index 0000000..e67d176 --- /dev/null +++ b/packages/flowtest-cli/utils/flowparser/StartNode.js @@ -0,0 +1,29 @@ +const { Node } = require('./Node'); + +class StartNode extends Node { + constructor() { + super('startNode'); + } + + serialize(id, data, metadata) { + return { + id, + ...metadata, + }; + } + + deserialize(node) { + const id = node.id; + delete node.id; + const metadata = node; + + return { + id, + metadata, + }; + } +} + +module.exports = { + StartNode, +}; diff --git a/packages/flowtest-cli/utils/flowparser/parser.js b/packages/flowtest-cli/utils/flowparser/parser.js new file mode 100644 index 0000000..01004fb --- /dev/null +++ b/packages/flowtest-cli/utils/flowparser/parser.js @@ -0,0 +1,265 @@ +const { cloneDeep } = require('lodash'); +const { AuthNode } = require('./AuthNode'); +const { NestedFlowNode } = require('./NestedFlowNode'); +const { DelayNode } = require('./DelayNode'); +const { AssertNode } = require('./AssertNode'); +const { OutputNode } = require('./OutputNode'); +const { RequestNode } = require('./RequestNode'); +const { StartNode } = require('./StartNode'); +const { SetVarNode } = require('./SetVarNode'); + +const VERSION = 1; + +const deserialize = (flowData) => { + // we don't want to modify original object + const flowDataCopy = cloneDeep(flowData); + + const textData = {}; + textData.version = VERSION; + textData.graph = {}; + + if (flowData) { + if (flowData.nodes) { + const nodes = flowDataCopy.nodes; + textData.graph.data = {}; + textData.graph.data.nodes = {}; + textData.graph.metadata = {}; + textData.graph.metadata.nodes = {}; + + nodes.forEach((node) => { + if (node.type === 'startNode') { + const sNode = new StartNode(); + const result = sNode.deserialize(node); + + textData.graph.data.nodes[result.id] = { + type: 'startNode', + }; + + textData.graph.metadata.nodes[result.id] = { + type: 'startNode', + ...result.metadata, + }; + } + + if (node.type === 'authNode') { + const aNode = new AuthNode(); + const result = aNode.deserialize(node); + textData.graph.data.nodes[result.id] = { + type: 'authNode', + auth: result.data, + }; + + textData.graph.metadata.nodes[result.id] = { + type: 'authNode', + ...result.metadata, + }; + } + + if (node.type === 'requestNode') { + const rNode = new RequestNode(); + const result = rNode.deserialize(node); + textData.graph.data.nodes[result.id] = { + type: 'requestNode', + ...result.data, + }; + + textData.graph.metadata.nodes[result.id] = { + type: 'requestNode', + ...result.metadata, + }; + } + + if (node.type === 'outputNode') { + const oNode = new OutputNode(); + const result = oNode.deserialize(node); + textData.graph.data.nodes[result.id] = { + type: 'outputNode', + ...result.data, + }; + + textData.graph.metadata.nodes[result.id] = { + type: 'outputNode', + ...result.metadata, + }; + } + + if (node.type === 'delayNode') { + const dNode = new DelayNode(); + const result = dNode.deserialize(node); + textData.graph.data.nodes[result.id] = { + type: 'delayNode', + ...result.data, + }; + + textData.graph.metadata.nodes[result.id] = { + type: 'delayNode', + ...result.metadata, + }; + } + + if (node.type === 'assertNode') { + const eNode = new AssertNode(); + const result = eNode.deserialize(node); + textData.graph.data.nodes[result.id] = { + type: 'assertNode', + ...result.data, + }; + + textData.graph.metadata.nodes[result.id] = { + type: 'assertNode', + ...result.metadata, + }; + } + + if (node.type === 'flowNode') { + const fNode = new NestedFlowNode(); + const result = fNode.deserialize(node); + textData.graph.data.nodes[result.id] = { + type: 'flowNode', + ...result.data, + }; + + textData.graph.metadata.nodes[result.id] = { + type: 'flowNode', + ...result.metadata, + }; + } + + if (node.type === 'setVarNode') { + const sNode = new SetVarNode(); + const result = sNode.deserialize(node); + textData.graph.data.nodes[result.id] = { + type: 'setVarNode', + ...result.data, + }; + + textData.graph.metadata.nodes[result.id] = { + type: 'setVarNode', + ...result.metadata, + }; + } + }); + } + + if (flowData.edges) { + const edges = flowDataCopy.edges; + textData.graph.data.edges = []; + textData.graph.metadata.edges = {}; + + edges.forEach((edge) => { + textData.graph.data.edges.push(`${edge.source} -> ${edge.target}`); + + const { ['id']: _, ..._edge } = edge; + textData.graph.metadata.edges[edge.id] = _edge; + }); + } + + if (flowData.viewport) { + textData.graph.metadata.viewport = flowDataCopy.viewport; + } + } + + return textData; +}; + +const serialize = (textData) => { + const flowData = {}; + flowData.nodes = []; + flowData.edges = []; + flowData.viewport = { x: 0, y: 0, zoom: 1 }; + + // we don't want to modify original object + const textDataCopy = cloneDeep(textData); + const version = textDataCopy.version; + if (version === 1) { + if (textDataCopy.graph.data) { + Object.entries(textDataCopy.graph.data.nodes).map(([key, value], index) => { + const id = key; + + if (value.type === 'startNode') { + const metadata = textDataCopy.graph.metadata.nodes[id]; + const sNode = new StartNode(); + const result = sNode.serialize(id, undefined, metadata); + + flowData.nodes.push(result); + } + + if (value.type === 'authNode') { + const data = value.auth; + const metadata = textDataCopy.graph.metadata.nodes[id]; + const aNode = new AuthNode(); + const result = aNode.serialize(id, data, metadata); + flowData.nodes.push(result); + } + + if (value.type === 'requestNode') { + const data = value; + const metadata = textDataCopy.graph.metadata.nodes[id]; + const rNode = new RequestNode(); + const result = rNode.serialize(id, data, metadata); + flowData.nodes.push(result); + } + + if (value.type === 'outputNode') { + const data = value; + const metadata = textDataCopy.graph.metadata.nodes[id]; + const oNode = new OutputNode(); + const result = oNode.serialize(id, data, metadata); + flowData.nodes.push(result); + } + + if (value.type === 'delayNode') { + const data = value; + const metadata = textDataCopy.graph.metadata.nodes[id]; + const dNode = new DelayNode(); + const result = dNode.serialize(id, data, metadata); + flowData.nodes.push(result); + } + + if (value.type === 'assertNode') { + const data = value; + const metadata = textDataCopy.graph.metadata.nodes[id]; + const dNode = new AssertNode(); + const result = dNode.serialize(id, data, metadata); + flowData.nodes.push(result); + } + + if (value.type === 'flowNode') { + const data = value; + const metadata = textDataCopy.graph.metadata.nodes[id]; + const fNode = new NestedFlowNode(); + const result = fNode.serialize(id, data, metadata); + flowData.nodes.push(result); + } + + if (value.type === 'setVarNode') { + const data = value; + const metadata = textDataCopy.graph.metadata.nodes[id]; + const cNode = new SetVarNode(); + const result = cNode.serialize(id, data, metadata); + flowData.nodes.push(result); + } + }); + + Object.entries(textDataCopy.graph.metadata.edges).map(([key, value], index) => { + flowData.edges.push({ + id: key, + ...value, + }); + }); + + if (textDataCopy.graph.metadata.viewport) { + flowData.viewport = textDataCopy.graph.metadata.viewport; + } + } + } else { + throw new Error('Version not recognized'); + } + + return flowData; +}; + +module.exports = { + deserialize, + serialize, +}; diff --git a/packages/flowtest-cli/utils/readfile.js b/packages/flowtest-cli/utils/readfile.js new file mode 100644 index 0000000..099035b --- /dev/null +++ b/packages/flowtest-cli/utils/readfile.js @@ -0,0 +1,26 @@ +const fs = require('fs'); + +const pathExists = (path) => { + try { + fs.accessSync(path); + return true; + } catch (error) { + return false; + } +}; + +const readFile = (path) => { + if (!path) { + throw new Error('File path is required'); + } + + // check if file exists + if (!pathExists(path)) { + throw new Error('File does not exist'); + } + + // now delete the file + return fs.readFileSync(path, 'utf8'); +}; + +module.exports = readFile;