diff --git a/TestMasterUI/src/components/api-tester/DiagramArea.jsx b/TestMasterUI/src/components/api-tester/DiagramArea.jsx index 91813e8..b584b20 100644 --- a/TestMasterUI/src/components/api-tester/DiagramArea.jsx +++ b/TestMasterUI/src/components/api-tester/DiagramArea.jsx @@ -264,7 +264,7 @@ const DiagramArea = forwardRef(({onSave, onFlowChange, isExecuting}, ref) => { position, data: { ...apiData, - id: `api-${Date.now()}` // Ensure unique API ID + id: apiData.id, } }; diff --git a/src/main/java/com/eactive/testmaster/client/dto/ReactFlowEdge.java b/src/main/java/com/eactive/testmaster/client/dto/ReactFlowEdge.java new file mode 100644 index 0000000..841736c --- /dev/null +++ b/src/main/java/com/eactive/testmaster/client/dto/ReactFlowEdge.java @@ -0,0 +1,59 @@ +package com.eactive.testmaster.client.dto; + + +public class ReactFlowEdge { + private String id; + private String source; + private String target; + private String sourceHandle; + private String targetHandle; + private String type = "smoothstep"; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public String getTarget() { + return target; + } + + public void setTarget(String target) { + this.target = target; + } + + public String getSourceHandle() { + return sourceHandle; + } + + public void setSourceHandle(String sourceHandle) { + this.sourceHandle = sourceHandle; + } + + public String getTargetHandle() { + return targetHandle; + } + + public void setTargetHandle(String targetHandle) { + this.targetHandle = targetHandle; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/src/main/java/com/eactive/testmaster/client/dto/ReactFlowGraph.java b/src/main/java/com/eactive/testmaster/client/dto/ReactFlowGraph.java new file mode 100644 index 0000000..cd7a222 --- /dev/null +++ b/src/main/java/com/eactive/testmaster/client/dto/ReactFlowGraph.java @@ -0,0 +1,25 @@ +package com.eactive.testmaster.client.dto; + +import java.util.ArrayList; +import java.util.List; + +public class ReactFlowGraph { + private List nodes = new ArrayList<>(); + private List edges = new ArrayList<>(); + + public List getNodes() { + return nodes; + } + + public void setNodes(List nodes) { + this.nodes = nodes; + } + + public List getEdges() { + return edges; + } + + public void setEdges(List edges) { + this.edges = edges; + } +} diff --git a/src/main/java/com/eactive/testmaster/client/dto/ReactFlowNode.java b/src/main/java/com/eactive/testmaster/client/dto/ReactFlowNode.java new file mode 100644 index 0000000..c5059d2 --- /dev/null +++ b/src/main/java/com/eactive/testmaster/client/dto/ReactFlowNode.java @@ -0,0 +1,135 @@ +package com.eactive.testmaster.client.dto; + +import java.util.Map; + +public class ReactFlowNode { + private String id; + private String type; + private Position position; + private NodeData data; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Position getPosition() { + return position; + } + + public void setPosition(Position position) { + this.position = position; + } + + public NodeData getData() { + return data; + } + + public void setData(NodeData data) { + this.data = data; + } + + public static class Position { + private double x; + private double y; + + public Position() { + } + + public double getX() { + return x; + } + + public void setX(double x) { + this.x = x; + } + + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + } + + public static class NodeData { + private String id; + private String name; + private String method; + private String path; + private String condition; + private Long duration; + private Map additionalData; + + public NodeData() { + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getCondition() { + return condition; + } + + public void setCondition(String condition) { + this.condition = condition; + } + + public Long getDuration() { + return duration; + } + + public void setDuration(Long duration) { + this.duration = duration; + } + + public Map getAdditionalData() { + return additionalData; + } + + public void setAdditionalData(Map additionalData) { + this.additionalData = additionalData; + } + } +} diff --git a/src/main/java/com/eactive/testmaster/loadtest/service/LoadTestService.java b/src/main/java/com/eactive/testmaster/loadtest/service/LoadTestService.java index 148f468..3d9244d 100644 --- a/src/main/java/com/eactive/testmaster/loadtest/service/LoadTestService.java +++ b/src/main/java/com/eactive/testmaster/loadtest/service/LoadTestService.java @@ -29,6 +29,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; +import org.thymeleaf.util.StringUtils; @Service //@Scope("session") @@ -49,7 +50,7 @@ public class LoadTestService extends TextWebSocketHandler { private final RingBuffer ringBuffer; - public LoadTestService(SimpMessagingTemplate template, LoadTestMonitorMap loadTestMonitorMap,ApiClientFactory apiClientFactory, ApiScenarioMapper apiScenarioMapper, ApiRequestMapper apiRequestMapper) { + public LoadTestService(SimpMessagingTemplate template, LoadTestMonitorMap loadTestMonitorMap, ApiClientFactory apiClientFactory, ApiScenarioMapper apiScenarioMapper, ApiRequestMapper apiRequestMapper) { this.template = template; this.loadTestMonitorMap = loadTestMonitorMap; this.apiScenarioMapper = apiScenarioMapper; @@ -130,7 +131,15 @@ public class LoadTestService extends TextWebSocketHandler { taskScheduler.setPoolSize(numberOfUsers + 1); // Add extra space for cancellation tasks taskScheduler.initialize(); ApiScenario apiScenario = apiScenarioMapper.map(loadTestDTO.getScenarioId()); - ScenarioNode scenario = LoadTestGraphBuilder.getTopNode(apiScenario.getScenario()); + + ScenarioNode scenario = null; + if (apiScenario != null) { + if (!StringUtils.isEmpty(apiScenario.getScenario()) && apiScenario.getScenario().contains("nodes")) { + scenario = ReactFlowGraphBuilder.convertToScenarioNode(apiScenario.getScenario()); + } else { + scenario = LoadTestGraphBuilder.getTopNode(apiScenario.getScenario()); + } + } traverseDFS(scenario); List> scheduledFutures = new ArrayList<>(); diff --git a/src/main/java/com/eactive/testmaster/loadtest/service/ReactFlowGraphBuilder.java b/src/main/java/com/eactive/testmaster/loadtest/service/ReactFlowGraphBuilder.java new file mode 100644 index 0000000..12f9a96 --- /dev/null +++ b/src/main/java/com/eactive/testmaster/loadtest/service/ReactFlowGraphBuilder.java @@ -0,0 +1,222 @@ +package com.eactive.testmaster.loadtest.service; + +import com.eactive.testmaster.client.dto.ReactFlowGraph; +import com.eactive.testmaster.client.dto.internal.ScenarioNode; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.HashMap; +import java.util.Map; + + +public class ReactFlowGraphBuilder { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + static { + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + public static ReactFlowGraph loadData(String data) throws JsonProcessingException { + return objectMapper.readValue(data, new TypeReference() { + }); + } + + public static ScenarioNode convertToScenarioNode(String data) { + try { + return convertToScenarioNode(loadData(data)); + } catch (JsonProcessingException e) { + return new ScenarioNode(); + } + } + + public static ScenarioNode convertToScenarioNode(ReactFlowGraph flowGraph) { + Map nodeMap = new HashMap<>(); + + // First pass: Create all nodes + flowGraph.getNodes().forEach(flowNode -> { + ScenarioNode scenarioNode = new ScenarioNode(); + scenarioNode.setId(flowNode.getId()); + scenarioNode.setName(flowNode.getData().getName()); + scenarioNode.setClassName(getScenarioNodeClass(flowNode.getType())); + + // Convert node-specific data + Map data = new HashMap<>(); + switch (flowNode.getType()) { + case "apiNode": + data.put("id", flowNode.getData().getId()); + data.put("method", flowNode.getData().getMethod()); + data.put("path", flowNode.getData().getPath()); + break; + case "conditionNode": + data.put("condition", flowNode.getData().getCondition()); + break; + case "delayNode": + data.put("duration", String.valueOf(flowNode.getData().getDuration())); + break; + } + scenarioNode.setData(data); + nodeMap.put(flowNode.getId(), scenarioNode); + }); + + // Second pass: Connect nodes + flowGraph.getEdges().forEach(edge -> { + ScenarioNode sourceNode = nodeMap.get(edge.getSource()); + if (sourceNode != null) { + sourceNode.addNode(nodeMap.get(edge.getTarget())); + } + }); + + // Find and return the start node + return nodeMap.values().stream() + .filter(node -> "start".equals(node.getClassName())) + .findFirst() + .orElse(null); + } + + private static String getScenarioNodeClass(String nodeType) { + switch (nodeType) { + case "startNode": + return "start"; + case "apiNode": + return "api"; + case "conditionNode": + return "condition"; + case "delayNode": + return "delay"; + default: + return "default"; + } + } + + public static String testData = "{\n" + + " \"nodes\": [\n" + + " {\n" + + " \"id\": \"start\",\n" + + " \"type\": \"startNode\",\n" + + " \"position\": {\n" + + " \"x\": 100,\n" + + " \"y\": 50\n" + + " },\n" + + " \"data\": {\n" + + " \"id\": \"start\"\n" + + " },\n" + + " \"measured\": {\n" + + " \"width\": 100,\n" + + " \"height\": 56\n" + + " },\n" + + " \"className\": null\n" + + " },\n" + + " {\n" + + " \"id\": \"node-1738370266933-9tienpcqk\",\n" + + " \"type\": \"apiNode\",\n" + + " \"position\": {\n" + + " \"x\": 360,\n" + + " \"y\": 195\n" + + " },\n" + + " \"data\": {\n" + + " \"id\": \"c240c9da-17a4-4dc7-bc93-a29b54aa324e\",\n" + + " \"name\": \"api1\",\n" + + " \"path\": \"api1?test={{test}}\",\n" + + " \"method\": \"POST\",\n" + + " \"type\": \"HTTP\",\n" + + " \"server\": 53,\n" + + " \"headers\": [\n" + + " {\n" + + " \"enabled\": true,\n" + + " \"key\": \"hhaa\",\n" + + " \"value\": \"{{test}}\"\n" + + " }\n" + + " ],\n" + + " \"queryParams\": [\n" + + " {\n" + + " \"enabled\": true,\n" + + " \"key\": \"test\",\n" + + " \"value\": \"{{test}}\"\n" + + " }\n" + + " ],\n" + + " \"variables\": [],\n" + + " \"requestBody\": \"{{test}}\",\n" + + " \"preRequestScript\": \"console.log(11);\",\n" + + " \"postRequestScript\": \"console.log(22);\",\n" + + " \"layout\": null,\n" + + " \"collectionId\": \"d4088731-685e-41ea-b557-61d7ac3d4ab3\"\n" + + " },\n" + + " \"measured\": {\n" + + " \"width\": 200,\n" + + " \"height\": 106\n" + + " }\n" + + " },\n" + + " {\n" + + " \"id\": \"node-1738370270698-hj4f76knw\",\n" + + " \"type\": \"apiNode\",\n" + + " \"position\": {\n" + + " \"x\": 720,\n" + + " \"y\": 315\n" + + " },\n" + + " \"data\": {\n" + + " \"id\": \"c240c9da-17a4-4dc7-bc93-a29b54aa324e\",\n" + + " \"name\": \"api1\",\n" + + " \"path\": \"api1?test={{test}}\",\n" + + " \"method\": \"POST\",\n" + + " \"type\": \"HTTP\",\n" + + " \"server\": 53,\n" + + " \"headers\": [\n" + + " {\n" + + " \"enabled\": true,\n" + + " \"key\": \"hhaa\",\n" + + " \"value\": \"{{test}}\"\n" + + " }\n" + + " ],\n" + + " \"queryParams\": [\n" + + " {\n" + + " \"enabled\": true,\n" + + " \"key\": \"test\",\n" + + " \"value\": \"{{test}}\"\n" + + " }\n" + + " ],\n" + + " \"variables\": [],\n" + + " \"requestBody\": \"{{test}}\",\n" + + " \"preRequestScript\": \"console.log(11);\",\n" + + " \"postRequestScript\": \"console.log(22);\",\n" + + " \"layout\": null,\n" + + " \"collectionId\": \"d4088731-685e-41ea-b557-61d7ac3d4ab3\"\n" + + " },\n" + + " \"measured\": {\n" + + " \"width\": 200,\n" + + " \"height\": 106\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"edges\": [\n" + + " {\n" + + " \"type\": \"smoothstep\",\n" + + " \"source\": \"start\",\n" + + " \"sourceHandle\": null,\n" + + " \"target\": \"node-1738370266933-9tienpcqk\",\n" + + " \"targetHandle\": null,\n" + + " \"id\": \"edge-1738370269180-dqspjlvej\"\n" + + " },\n" + + " {\n" + + " \"type\": \"smoothstep\",\n" + + " \"source\": \"node-1738370266933-9tienpcqk\",\n" + + " \"sourceHandle\": null,\n" + + " \"target\": \"node-1738370270698-hj4f76knw\",\n" + + " \"targetHandle\": null,\n" + + " \"id\": \"edge-1738370272761-db6f2xim6\"\n" + + " }\n" + + " ],\n" + + " \"viewport\": {\n" + + " \"x\": 0,\n" + + " \"y\": 0,\n" + + " \"zoom\": 1\n" + + " }\n" + + "}"; + + public static void main(String[] args) throws JsonProcessingException { + ScenarioNode node = ReactFlowGraphBuilder.convertToScenarioNode(testData); + + System.out.println(node); + } +}