);
@@ -128,22 +128,19 @@ const APIMode = ({ isApiMode, onToggleMode }) => {
// 시나리오 모드
const ScenarioMode = ({ isApiMode, onToggleMode }) => (
-
+
{/* 왼쪽 고정: ScenarioNavigation */}
{
- // Handle scenario selection
- }}
isApiMode={isApiMode}
onToggleMode={onToggleMode}
/>
{/* 중앙: ScenarioEditor */}
-
+
diff --git a/TestMasterUI/src/components/api-tester/AddScenarioDialog.jsx b/TestMasterUI/src/components/api-tester/AddScenarioDialog.jsx
new file mode 100644
index 0000000..6708574
--- /dev/null
+++ b/TestMasterUI/src/components/api-tester/AddScenarioDialog.jsx
@@ -0,0 +1,111 @@
+import React, { useState } from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import ErrorAlertDialog from '@/components/shared/ErrorAlertDialog.jsx';
+
+const AddScenarioDialog = ({ open, onOpenChange, onSubmit }) => {
+ const [scenarioName, setScenarioName] = useState('');
+ const [error, setError] = useState('');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [showErrorDialog, setShowErrorDialog] = useState(false);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!scenarioName.trim()) {
+ setError('시나리오 이름을 입력해주세요');
+ return;
+ }
+
+ setIsSubmitting(true);
+ try {
+ const success = await onSubmit(scenarioName.trim());
+ if (success) {
+ setScenarioName('');
+ setError('');
+ onOpenChange(false);
+ } else {
+ setError('시나리오 생성에 실패했습니다');
+ setShowErrorDialog(true);
+ }
+ } catch (err) {
+ setError(err.message || '시나리오 생성에 실패했습니다');
+ setShowErrorDialog(true);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleOpenChange = (open) => {
+ if (!open) {
+ setScenarioName('');
+ setError('');
+ }
+ onOpenChange(open);
+ };
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+export default AddScenarioDialog;
diff --git a/TestMasterUI/src/components/api-tester/DeleteScenarioDialog.jsx b/TestMasterUI/src/components/api-tester/DeleteScenarioDialog.jsx
new file mode 100644
index 0000000..ca3b216
--- /dev/null
+++ b/TestMasterUI/src/components/api-tester/DeleteScenarioDialog.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from '@/components/ui/alert-dialog';
+
+const DeleteScenarioDialog = ({ open, onOpenChange, onConfirm, scenarioName }) => {
+ const handleConfirm = async () => {
+ await onConfirm();
+ onOpenChange(false);
+ };
+
+ return (
+
+
+
+ 시나리오 삭제
+
+ '{scenarioName}' 시나리오를 삭제하시겠습니까?
+
+ 이 작업은 되돌릴 수 없으며, 시나리오의 모든 구성요소가 함께 삭제됩니다.
+
+
+
+ 취소
+
+ 삭제
+
+
+
+
+ );
+};
+
+export default DeleteScenarioDialog;
diff --git a/TestMasterUI/src/components/api-tester/DiagramArea.jsx b/TestMasterUI/src/components/api-tester/DiagramArea.jsx
index 29ffb1b..37f8986 100644
--- a/TestMasterUI/src/components/api-tester/DiagramArea.jsx
+++ b/TestMasterUI/src/components/api-tester/DiagramArea.jsx
@@ -1,12 +1,13 @@
-import { Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState, Position, Handle, ReactFlowProvider } from '@xyflow/react';
-import React, { useCallback, memo, useMemo, useRef, useState, useEffect } from 'react';
-import { Play } from 'lucide-react';
+import {Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState, Position, Handle, ReactFlowProvider} from '@xyflow/react';
+import React, {useCallback, memo, useMemo, useRef, useState, useEffect, forwardRef, useImperativeHandle} from 'react';
+import {Play} from 'lucide-react';
+import {useLoadTest} from '@/providers/LoadTestProvider.jsx';
// Optimized node components with proper prop types and memoization
-const StartNode = memo(({ data, isConnectable }) => {
+const StartNode = memo(({data, isConnectable}) => {
// Memoize handles configuration
const handles = useMemo(() => ([
- { id: 'start-right', type: 'source', position: Position.Right }
+ {id: 'start-right', type: 'source', position: Position.Right}
]), []);
return (
@@ -21,18 +22,18 @@ const StartNode = memo(({ data, isConnectable }) => {
/>
))}
);
});
-const ApiNode = memo(({ data, isConnectable }) => {
+const ApiNode = memo(({data, isConnectable}) => {
// Memoize handles and colors
const handles = useMemo(() => ([
- { id: `${data.id}-left`, type: 'target', position: Position.Left },
- { id: `${data.id}-right`, type: 'source', position: Position.Right }
+ {id: `${data.id}-left`, type: 'target', position: Position.Left},
+ {id: `${data.id}-right`, type: 'source', position: Position.Right}
]), [data.id]);
const methodColor = useMemo(() => {
@@ -59,7 +60,7 @@ const ApiNode = memo(({ data, isConnectable }) => {
))}
@@ -71,7 +72,7 @@ const ApiNode = memo(({ data, isConnectable }) => {
);
});
-const ConditionNode = memo(({ data, isConnectable }) => {
+const ConditionNode = memo(({data, isConnectable}) => {
// Memoize handle styles
const handleStyles = useMemo(() => ({
true: {
@@ -113,7 +114,7 @@ const ConditionNode = memo(({ data, isConnectable }) => {
/>
@@ -124,10 +125,10 @@ const ConditionNode = memo(({ data, isConnectable }) => {
);
});
-const DelayNode = memo(({ data, isConnectable }) => {
+const DelayNode = memo(({data, isConnectable}) => {
const handles = useMemo(() => ([
- { id: `${data.id}-left`, type: 'target', position: Position.Left },
- { id: `${data.id}-right`, type: 'source', position: Position.Right }
+ {id: `${data.id}-left`, type: 'target', position: Position.Left},
+ {id: `${data.id}-right`, type: 'source', position: Position.Right}
]), [data.id]);
return (
@@ -143,7 +144,7 @@ const DelayNode = memo(({ data, isConnectable }) => {
))}
@@ -160,18 +161,6 @@ ApiNode.displayName = 'ApiNode';
ConditionNode.displayName = 'ConditionNode';
DelayNode.displayName = 'DelayNode';
-// Memoize initial nodes and edges
-const initialNodes = [
- {
- id: 'start',
- type: 'startNode',
- position: { x: 100, y: 50 },
- data: { id: 'start' }
- }
-];
-
-const initialEdges = [];
-
// Memoize node types
const nodeTypes = {
startNode: StartNode,
@@ -180,11 +169,40 @@ const nodeTypes = {
delayNode: DelayNode
};
-const DiagramArea = () => {
+const DiagramArea = forwardRef(({onSave}, ref) => {
+ const {currentFlowData} = useLoadTest();
const reactFlowWrapper = useRef(null);
- const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
- const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const [selectedNodeId, setSelectedNodeId] = useState(null);
+ const [edges, setEdges, onEdgesChange] = useEdgesState(currentFlowData?.edges || []);
+ const [nodes, setNodes, onNodesChange] = useNodesState(currentFlowData?.nodes || [
+ {
+ id: 'start',
+ type: 'startNode',
+ position: {x: 100, y: 50},
+ data: {id: 'start'}
+ }]);
+
+ useEffect(() => {
+ if (currentFlowData) {
+ const {nodes: scenarioNodes, edges: scenarioEdges} = currentFlowData;
+ setNodes(scenarioNodes?.length ? scenarioNodes : [
+ {
+ id: 'start',
+ type: 'startNode',
+ position: {x: 100, y: 50},
+ data: {id: 'start'}
+ }]);
+ setEdges(scenarioEdges || []);
+ }
+ }, [currentFlowData, setNodes, setEdges]);
+
+ useImperativeHandle(ref, () => ({
+ getFlowData: () => ({
+ nodes,
+ edges,
+ viewport: reactFlowWrapper.current?.getViewport()
+ })
+ }));
// Click handlers for node selection
const onNodeClick = useCallback((event, node) => {
@@ -206,9 +224,11 @@ const DiagramArea = () => {
);
}, [selectedNodeId, setNodes]);
- // Memoize callback functions
const onConnect = useCallback((params) => {
- setEdges(prev => [...prev, { ...params, id: `e${prev.length + 1}` }]);
+ setEdges(prev => {
+ const newId = `edge-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ return [...prev, {...params, id: newId}];
+ });
}, []);
const onInit = useCallback((instance) => {
@@ -223,25 +243,24 @@ const DiagramArea = () => {
const onDrop = useCallback((event) => {
event.preventDefault();
- if (!reactFlowWrapper.current) return;
+ if (!reactFlowWrapper.current) {
+ return;
+ }
try {
const apiData = JSON.parse(event.dataTransfer.getData('application/json'));
const position = reactFlowWrapper.current.screenToFlowPosition({
x: event.clientX,
- y: event.clientY,
+ y: event.clientY
});
const newNode = {
- id: `api-${Date.now()}`,
+ id: `node-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
type: 'apiNode',
position,
data: {
- id: apiData.id,
- name: apiData.name,
- method: apiData.method,
- path: apiData.path,
- collectionId: apiData.collectionId
+ ...apiData,
+ id: `api-${Date.now()}` // Ensure unique API ID
}
};
@@ -252,34 +271,35 @@ const DiagramArea = () => {
}, [setNodes]);
// Default edge options memoized
- const defaultEdgeOptions = useMemo(() => ({ type: 'smoothstep' }), []);
+ const defaultEdgeOptions = useMemo(() => ({type: 'smoothstep'}), []);
return (
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
-};
+});
+
+DiagramArea.displayName = 'DiagramArea';
export default memo(DiagramArea);
diff --git a/TestMasterUI/src/components/api-tester/DraggableApiItem.jsx b/TestMasterUI/src/components/api-tester/DraggableApiItem.jsx
index b94f995..6d6c67e 100644
--- a/TestMasterUI/src/components/api-tester/DraggableApiItem.jsx
+++ b/TestMasterUI/src/components/api-tester/DraggableApiItem.jsx
@@ -37,18 +37,7 @@ const DraggableApiItem = ({ api, collectionId, onApiAction }) => {
const handleDragStart = (event) => {
// Prepare API data for drag operation
- const apiData = {
- id: api.id,
- name: api.name,
- method: api.method || 'GET',
- path: api.path || '/',
- collectionId: collectionId,
- headers: api.headers || [],
- parameters: api.parameters || [],
- body: api.body || '',
- description: api.description || '',
- type: api.type || 'HTTP'
- };
+ const apiData = {...api};
// Create drag image for better visual feedback
const dragImage = document.createElement('div');
diff --git a/TestMasterUI/src/components/api-tester/ScenarioEditor.jsx b/TestMasterUI/src/components/api-tester/ScenarioEditor.jsx
index 63a8b1b..40c26bd 100644
--- a/TestMasterUI/src/components/api-tester/ScenarioEditor.jsx
+++ b/TestMasterUI/src/components/api-tester/ScenarioEditor.jsx
@@ -1,48 +1,97 @@
-import React, {useState} from 'react';
+import React, {useCallback, useEffect, useRef, useState} from 'react';
import '@xyflow/react/dist/style.css';
import MonacoEditor from '@/components/shared/MonacoEditor';
import ScenarioMenuBar from '@/components/api-tester/ScenarioMenuBar.jsx';
import DiagramArea from '@/components/api-tester/DiagramArea.jsx';
+import {useLoadTest} from '@/providers/LoadTestProvider.jsx';
+import {useScenarioExecutor} from '@/services/ScenarioExecutor';
+import APIClient from '@/services/APIClient.js';
+import {Button} from '@/components/ui/button.jsx';
+
+const apiClient = new APIClient();
// ConsoleArea Component
-const ConsoleArea = () => {
- const [consoleOutput, setConsoleOutput] = useState('');
+const ConsoleArea = ({value}) => (
- return (
-
-
-
- );
-};
+
+
+);
// Main ScenarioEditor Component
const ScenarioEditor = () => {
- const handleSave = () => {
- console.log('Saving scenario...');
+ const diagramRef = useRef(null);
+ const {currentScenario, currentFlowData, updateScenario} = useLoadTest();
+ const [isExecuting, setIsExecuting] = useState(false);
+
+ const {
+ logs,
+ clearLogs,
+ executeScenario,
+ executeStep,
+ resetExecutor,
+ pauseExecution,
+ isRunning
+ } = useScenarioExecutor(
+ currentFlowData?.nodes || [],
+ currentFlowData?.edges || [],
+ apiClient
+ );
+
+ useEffect(() => {
+ resetExecutor();
+ clearLogs();
+ }, [currentFlowData]);
+
+ const handleSave = async () => {
+ try {
+ console.log(currentScenario);
+ const flowData = diagramRef.current?.getFlowData();
+ await updateScenario({
+ ...currentScenario,
+ scenario: JSON.stringify(flowData)
+ });
+ } catch (error) {
+ console.error('Failed to save scenario:', error);
+ }
};
- const handleStepRun = () => {
- console.log('Running step...');
- };
+ const handleStepRun = useCallback(async () => {
+ setIsExecuting(true);
+ try {
+ await executeStep();
+ } finally {
+ setIsExecuting(false);
+ }
+ }, [executeStep]);
- const handleFullRun = () => {
- console.log('Running all steps...');
- };
+ const handleFullRun = useCallback(async () => {
+ setIsExecuting(true);
+ try {
+ await executeScenario();
+ } finally {
+ setIsExecuting(false);
+ }
+ }, [executeScenario]);
- const handlePause = () => {
- console.log('Pausing execution...');
- };
+ const handlePause = useCallback(() => {
+ pauseExecution();
+ }, [pauseExecution]);
- const handleReset = () => {
- console.log('Resetting scenario...');
- };
+ const handleReset = useCallback(() => {
+ resetExecutor();
+ clearLogs();
+ }, [resetExecutor, clearLogs]);
+
+ if (!currentFlowData) {
+ return
Select a scenario to edit
;
+ }
return (
@@ -52,9 +101,35 @@ const ScenarioEditor = () => {
onFullRun={handleFullRun}
onPause={handlePause}
onReset={handleReset}
+ disabled={isExecuting}
+ isRunning={isRunning}
/>
-
-
+
+
+
+
+
+
+
Output:
+
+
+
+
+
+
);
};
diff --git a/TestMasterUI/src/components/api-tester/ScenarioNavigation.jsx b/TestMasterUI/src/components/api-tester/ScenarioNavigation.jsx
index 6bbca8d..45286e4 100644
--- a/TestMasterUI/src/components/api-tester/ScenarioNavigation.jsx
+++ b/TestMasterUI/src/components/api-tester/ScenarioNavigation.jsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, {useEffect, useState} from 'react';
import { FileText, MoreHorizontal } from 'lucide-react';
import {
Sidebar,
@@ -23,6 +23,7 @@ import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip
import { Button } from '@/components/ui/button';
import { useLoadTest } from '@/providers/LoadTestProvider';
import { SidebarProvider } from '@/components/ui/sidebar';
+import DeleteScenarioDialog from '@/components/api-tester/DeleteScenarioDialog.jsx';
const ScenarioDropdownMenu = ({ onOpen, onRename, onDelete }) => (
@@ -64,16 +65,21 @@ const ErrorMessage = ({ message }) => (
const ScenarioNavigation = ({
contextPath = '/',
- onScenarioSelect,
side,
isApiMode,
onToggleMode
}) => {
+
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [scenarioToDelete, setScenarioToDelete] = useState(null);
+
const {
scenarios,
loading,
error,
- loadScenarioList
+ deleteScenario,
+ loadScenarioList,
+ selectScenario
} = useLoadTest();
useEffect(() => {
@@ -81,14 +87,18 @@ const ScenarioNavigation = ({
}, [loadScenarioList]);
const handleScenarioAction = {
- open: (scenarioId) => {
- if (onScenarioSelect) {
- onScenarioSelect(scenarioId);
+ open: async (scenarioId) => {
+ try {
+ console.log({scenarioId});
+ selectScenario(scenarioId);
+ } catch (err) {
+ console.error('Failed to select scenario:', err);
}
},
- delete: async (scenarioId) => {
- // TODO: Implement delete functionality
- console.log('Delete scenario:', scenarioId);
+ delete: async (scenario) => {
+ console.log('delete', scenario);
+ setScenarioToDelete(scenario);
+ setDeleteDialogOpen(true);
},
rename: (scenarioId) => {
// TODO: Implement rename functionality
@@ -96,6 +106,14 @@ const ScenarioNavigation = ({
}
};
+ const handleDeleteConfirm = async () => {
+ if (scenarioToDelete) {
+ await deleteScenario(scenarioToDelete);
+ setDeleteDialogOpen(false);
+ setScenarioToDelete(null);
+ }
+ };
+
const renderScenarioItem = (scenario) => (
handleScenarioAction.open(scenario.id)}
onRename={() => handleScenarioAction.rename(scenario.id)}
- onDelete={() => handleScenarioAction.delete(scenario.id)}
+ onDelete={() => handleScenarioAction.delete(scenario)}
/>
@@ -161,6 +179,12 @@ const ScenarioNavigation = ({
{renderContent()}
+
);
diff --git a/TestMasterUI/src/components/client-views/TCPClientView.jsx b/TestMasterUI/src/components/client-views/TCPClientView.jsx
index d10d3f1..c9a76ef 100644
--- a/TestMasterUI/src/components/client-views/TCPClientView.jsx
+++ b/TestMasterUI/src/components/client-views/TCPClientView.jsx
@@ -1,15 +1,14 @@
-import React, { useState, useEffect, useRef, useCallback } from 'react';
+import React, {useCallback, useEffect, useRef, useState} from 'react';
import * as monaco from 'monaco-editor';
-import APIClient from '../../lib/api-client';
-import { Button } from "@/components/ui/button";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { Card, CardContent } from "@/components/ui/card";
+import {Button} from '@/components/ui/button';
+import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs';
+import {Card, CardContent} from '@/components/ui/card';
import LayoutGrid from '../layout/LayoutGrid';
-import LayoutTreeGrid from '../layout/LayoutTreeGrid';
import LayoutTreeDataGrid from '../layout/LayoutTreeDataGrid';
import ApiLayoutItemTreeBuilder from '../../lib/ApiLayoutItemTreeBuilder';
import ApiLayoutItemTreeDataBuilder from '../../lib/ApiLayoutItemTreeDataBuilder';
import VariableSnippet from '../shared/VariableSnippet';
+import APIClient from '@/services/APIClient.js';
export const TCPClientView = ({ parent, serverManager, model, index }) => {
const [apiClient] = useState(() => new APIClient());
diff --git a/TestMasterUI/src/components/layout/LayoutTreeDataGrid.jsx b/TestMasterUI/src/components/layout/LayoutTreeDataGrid.jsx
index 51ee243..148570e 100644
--- a/TestMasterUI/src/components/layout/LayoutTreeDataGrid.jsx
+++ b/TestMasterUI/src/components/layout/LayoutTreeDataGrid.jsx
@@ -1,8 +1,8 @@
import React, { useState, useEffect, useCallback } from 'react';
import { cn } from "@/lib/utils";
import EditableInput from '../shared/EditableInput';
-import APIClient from '@/lib/api-client';
import _ from 'lodash';
+import APIClient from '@/services/APIClient.js';
// Helper function to get indentation for tree visualization
const getIndent = (depth) => {
diff --git a/TestMasterUI/src/lib/api-client.js b/TestMasterUI/src/lib/api-client.js
deleted file mode 100644
index 8ce1a85..0000000
--- a/TestMasterUI/src/lib/api-client.js
+++ /dev/null
@@ -1,272 +0,0 @@
-export const fillPadding = async (apiRequest, value, dataLength) => {
- let paddedValue = value; // In real app, this would be processed by APIClient
- while (paddedValue.length < dataLength) {
- paddedValue = ' ' + paddedValue;
- }
- return paddedValue;
-};
-
-class APIClient {
- constructor() {
- // Initialize global variables store
- window.globals = window.globals || {};
- this.abortController = null;
- this.contextPath = document.querySelector('meta[name="context-path"]')?.getAttribute('content') || '/';
-
- if (!this.contextPath.endsWith('/')) {
- this.contextPath += '/';
- }
- }
-
- getClientUrl() {
- return `${this.contextPath}mgmt/api/test.do`;
- }
-
- // Format response for consistency
- formatResponse(data) {
- return {
- body: data.body,
- status: data.status,
- headers: data.headers.reduce((acc, header) => {
- acc[header.key] = header.value;
- return acc;
- }, {}),
- size: data.size
- };
- }
-
- // Abort ongoing requests
- abort() {
- if (this.abortController) {
- this.abortController.abort();
- this.abortController = null;
- }
- }
-
- // Make the actual API request
- async makeAjaxRequest(processedRequest) {
- this.abortController = new AbortController();
-
- try {
- const response = await fetch(this.getClientUrl(), {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
- },
- body: JSON.stringify(processedRequest),
- signal: this.abortController.signal
- });
-
- if (!response.ok) {
- throw new Error('Network request failed');
- }
-
- return response;
-
- } catch (error) {
- if (error.name === 'AbortError') {
- // Handle abort silently
- return null;
- }
- throw error;
- }
- }
-
- // Process pre-script and variables
- async processPreScriptAndVariables(model, value, consoleOutput) {
- const variables = {};
- const updatedModel = await this.executePreRequestScript(model, variables, consoleOutput);
- return this.replacePlaceholderValueWithVariables(updatedModel, value, variables);
- }
-
- // Replace placeholder values with actual variables
- replacePlaceholderValueWithVariables(model, originalValue, variables = {}) {
- const mergedVariables = {
- ...window.globals,
- ...variables
- };
-
- return Object.entries(mergedVariables).reduce((value, [key, replacement]) => {
- const placeholder = `{{${key}}}`;
- return value.replace(new RegExp(placeholder, 'g'), replacement);
- }, originalValue);
- }
-
- // Main method to send API requests
- async sendAPIRequest(model, preProcessCallback, consoleOutput) {
- try {
- // Execute pre-request script and get variables
- const variables = {};
- const updatedModel = await this.executePreRequestScript(model, variables, consoleOutput);
-
- // Process request with variables
- const processedRequest = this.replacePlaceholdersWithVariables(updatedModel, variables);
-
- // Allow pre-processing if callback provided
- if (preProcessCallback) {
- preProcessCallback(processedRequest);
- }
-
- // Make the request
- const response = await this.makeAjaxRequest(processedRequest, consoleOutput);
- if (!response) return null;
-
- const responseData = await response.json();
-
- if (!response.ok) {
- throw new Error(responseData.error || 'Unknown error');
- }
-
- // Format response
- const finalResponse = this.formatResponse(responseData);
-
- // Execute post-request script
- return this.executePostRequestScript(
- model.postRequestScript,
- variables,
- processedRequest,
- finalResponse,
- consoleOutput
- ).catch(error => {
- console.error('Post-request script error:', error);
- return finalResponse;
- });
-
- } catch (error) {
- throw new Error(`API 호출 중 오류가 발생했습니다. 사유[${error.message}]`);
- }
- }
-
- // Replace placeholders in request with variables
- replacePlaceholdersWithVariables(original, variables = {}) {
- const request = JSON.parse(JSON.stringify(original));
- const mergedVariables = {
- ...window.globals,
- ...variables
- };
-
- Object.entries(mergedVariables).forEach(([key, value]) => {
- const placeholder = new RegExp(`{{${key}}}`, 'g');
-
- // Replace in path
- request.path = request.path.replace(placeholder, value);
-
- // Replace in request body
- if (request.requestBody) {
- request.requestBody = request.requestBody.replace(placeholder, value);
- }
-
- // Replace in headers
- if (request.headers?.length) {
- request.headers = JSON.parse(
- JSON.stringify(request.headers).replace(placeholder, value)
- );
- }
-
- // Replace in query params
- if (request.queryParams?.length) {
- request.queryParams = JSON.parse(
- JSON.stringify(request.queryParams).replace(placeholder, value)
- );
- }
- });
-
- return request;
- }
-
- // Execute pre-request script in an iframe
- executePreRequestScript(model, variables, consoleOutput) {
- return new Promise((resolve, reject) => {
- const resultFrame = document.getElementById('preRequest');
- if (!resultFrame) {
- console.warn('preRequest iframe not found');
- resolve(model);
- return;
- }
-
- window.preRequestComplete = (updatedModel, error) => {
- if (error) {
- reject(error);
- } else {
- resolve(updatedModel);
- }
- };
-
- window.temp_variable = variables;
- window.currentRequest = model;
- window.consoleOutput = consoleOutput;
-
- resultFrame.srcdoc = `
-
- `;
- });
- }
-
- // Execute post-request script in an iframe
- executePostRequestScript(script, variables, request, response, consoleOutput) {
- return new Promise((resolve, reject) => {
- const resultFrame = document.getElementById('postRequest');
- if (!resultFrame) {
- console.warn('postRequest iframe not found');
- resolve(response);
- return;
- }
-
- window.request = request;
- window.response = response;
- window.temp_variable = variables;
- window.postRequestComplete = (response, error) => {
- if (error) {
- reject(error);
- } else {
- resolve(response);
- }
- };
- window.consoleOutput = consoleOutput;
-
- resultFrame.srcdoc = `
-
- `;
- });
- }
-}
-
-export default APIClient;
diff --git a/TestMasterUI/src/providers/APIProvider.jsx b/TestMasterUI/src/providers/APIProvider.jsx
index 937ebde..f4d402a 100644
--- a/TestMasterUI/src/providers/APIProvider.jsx
+++ b/TestMasterUI/src/providers/APIProvider.jsx
@@ -1,9 +1,9 @@
import React, {createContext, useState, useContext, useCallback, useEffect} from 'react';
-import APIClient from '../lib/api-client';
import {collectionService} from '@/services/collectionService';
import {apiRequestService} from '@/services/apiRequestService';
import {serverService} from '@/services/serverService';
import {authService} from '@/services/authService';
+import APIClient from '@/services/APIClient.js';
const APIContext = createContext(null);
diff --git a/TestMasterUI/src/providers/LoadTestProvider.jsx b/TestMasterUI/src/providers/LoadTestProvider.jsx
index 8cb5941..0e550a0 100644
--- a/TestMasterUI/src/providers/LoadTestProvider.jsx
+++ b/TestMasterUI/src/providers/LoadTestProvider.jsx
@@ -1,51 +1,110 @@
+// providers/LoadTestProvider.jsx
import React, { createContext, useState, useContext, useCallback } from 'react';
+import { loadTestService } from '@/services/loadTestService';
+import { scenarioService } from '@/services/scenarioService';
-// Create the context
const LoadTestContext = createContext();
-// Provider component
export const LoadTestProvider = ({ children, contextPath = '/' }) => {
const [tests, setTests] = useState([]);
const [scenarios, setScenarios] = useState([]);
const [currentTest, setCurrentTest] = useState(null);
+ const [currentScenario, setCurrentScenario] = useState(null);
+ const [currentFlowData, setCurrentFlowData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
- // Load scenario list
+ // Initialize services
+ const loadTests$ = loadTestService(contextPath);
+ const scenarios$ = scenarioService(contextPath);
+
const loadScenarioList = useCallback(async () => {
+ setLoading(true);
+ setError(null);
try {
- const response = await fetch(`${contextPath}mgmt/api_scenario/list.do`, {
- headers: {
- 'Content-Type': 'application/json',
- 'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
- }
- });
-
- if (!response.ok) throw new Error('Failed to load scenarios');
-
- const data = await response.json();
+ const data = await scenarios$.loadScenarios();
setScenarios(data);
return data;
} catch (err) {
setError(err.message);
return [];
+ } finally {
+ setLoading(false);
}
- }, [contextPath]);
+ }, []);
+
+ const selectScenario = useCallback((scenarioId) => {
+ const scenario = scenarios.find(s => s.id === scenarioId);
+ if (!scenario) {
+ setError('Scenario not found');
+ return null;
+ }
+
+ let flowData = { nodes: [], edges: [] };
+ if (scenario.scenario) {
+ try {
+ flowData = JSON.parse(scenario.scenario);
+ } catch (e) {
+ console.warn('Failed to parse scenario data:', e);
+ }
+ }
+ console.log(scenario);
+ console.log(flowData);
+ setCurrentScenario(scenario);
+ setCurrentFlowData(flowData);
+ return flowData;
+ }, [scenarios]);
+
+ const createScenario = useCallback(async (name) => {
+ setLoading(true);
+ setError(null);
+ try {
+ const newScenario = await scenarios$.createScenario(name);
+ await loadScenarioList();
+ return newScenario;
+ } catch (err) {
+ setError(err.message);
+ return null;
+ } finally {
+ setLoading(false);
+ }
+ }, [loadScenarioList]);
+
+ const updateScenario = useCallback(async (scenario) => {
+ setLoading(true);
+ setError(null);
+ try {
+ const updated = await scenarios$.updateScenario(scenario);
+ await loadScenarioList();
+ return updated;
+ } catch (err) {
+ setError(err.message);
+ return null;
+ } finally {
+ setLoading(false);
+ }
+ }, [loadScenarioList]);
+
+ const deleteScenario = useCallback(async (scenario) => {
+ setLoading(true);
+ setError(null);
+ try {
+ const newScenario = await scenarios$.deleteScenario(scenario);
+ await loadScenarioList();
+ return newScenario;
+ } catch (err) {
+ setError(err.message);
+ return null;
+ } finally {
+ setLoading(false);
+ }
+ }, [loadScenarioList]);
- // Load test list
const loadTestList = useCallback(async () => {
setLoading(true);
+ setError(null);
try {
- const response = await fetch(`${contextPath}mgmt/load_test/list.do`, {
- headers: {
- 'Content-Type': 'application/json',
- 'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
- }
- });
-
- if (!response.ok) throw new Error('Failed to load tests');
-
- const data = await response.json();
+ const data = await loadTests$.loadTests();
setTests(data);
return data;
} catch (err) {
@@ -54,24 +113,13 @@ export const LoadTestProvider = ({ children, contextPath = '/' }) => {
} finally {
setLoading(false);
}
- }, [contextPath]);
+ }, []);
- // Create a new test
const createTest = useCallback(async (testName) => {
setLoading(true);
+ setError(null);
try {
- const response = await fetch(`${contextPath}mgmt/load_test/create.do`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
- },
- body: JSON.stringify({ name: testName })
- });
-
- if (!response.ok) throw new Error('Failed to create test');
-
- const newTest = await response.json();
+ const newTest = await loadTests$.createTest(testName);
await loadTestList();
return newTest;
} catch (err) {
@@ -80,60 +128,32 @@ export const LoadTestProvider = ({ children, contextPath = '/' }) => {
} finally {
setLoading(false);
}
- }, [contextPath, loadTestList]);
+ }, [loadTestList]);
- // Update a test
const updateTest = useCallback(async (test) => {
setLoading(true);
+ setError(null);
try {
- const response = await fetch(`${contextPath}mgmt/load_test/update.do`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
- },
- body: JSON.stringify(test)
- });
-
- if (!response.ok) throw new Error('Failed to update test');
-
- // Update local state
- setTests(prevTests =>
- prevTests.map(t => t.id === test.id ? { ...t, ...test } : t)
- );
-
- return test;
+ const updatedTest = await loadTests$.updateTest(test);
+ setTests(prev => prev.map(t => t.id === test.id ? updatedTest : t));
+ return updatedTest;
} catch (err) {
setError(err.message);
return null;
} finally {
setLoading(false);
}
- }, [contextPath]);
+ }, []);
- // Delete a test
const deleteTest = useCallback(async (testId) => {
setLoading(true);
+ setError(null);
try {
- const response = await fetch(`${contextPath}mgmt/load_test/delete.do`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
- },
- body: JSON.stringify({ id: testId })
- });
-
- if (!response.ok) throw new Error('Failed to delete test');
-
- // Remove test from local state
- setTests(prevTests => prevTests.filter(t => t.id !== testId));
-
- // Clear current test if deleted
+ await loadTests$.deleteTest(testId);
+ setTests(prev => prev.filter(t => t.id !== testId));
if (currentTest?.id === testId) {
setCurrentTest(null);
}
-
return true;
} catch (err) {
setError(err.message);
@@ -141,60 +161,43 @@ export const LoadTestProvider = ({ children, contextPath = '/' }) => {
} finally {
setLoading(false);
}
- }, [contextPath, currentTest]);
+ }, [currentTest?.id]);
- // Start a load test
const startTest = useCallback(async (test) => {
setLoading(true);
+ setError(null);
try {
- const response = await fetch(`${contextPath}startLoadTest`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
- },
- body: JSON.stringify(test)
- });
-
- if (!response.ok) throw new Error('Failed to start test');
-
- return true;
+ return await loadTests$.startTest(test);
} catch (err) {
setError(err.message);
return false;
} finally {
setLoading(false);
}
- }, [contextPath]);
+ }, []);
- // Stop a load test
const stopTest = useCallback(async (test) => {
setLoading(true);
+ setError(null);
try {
- const response = await fetch(`${contextPath}stopLoadTest`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-XSRF-TOKEN': document.querySelector('meta[name="_csrf"]')?.getAttribute('content')
- },
- body: JSON.stringify(test)
- });
-
- if (!response.ok) throw new Error('Failed to stop test');
-
- return true;
+ return await loadTests$.stopTest(test);
} catch (err) {
setError(err.message);
return false;
} finally {
setLoading(false);
}
- }, [contextPath]);
+ }, []);
- // Prepare context value
const contextValue = {
tests,
scenarios,
+ currentScenario,
+ currentFlowData,
+ createScenario,
+ updateScenario,
+ deleteScenario,
+ selectScenario,
currentTest,
loading,
error,
@@ -216,14 +219,11 @@ export const LoadTestProvider = ({ children, contextPath = '/' }) => {
);
};
-// Custom hook to use the LoadTestContext
export const useLoadTest = () => {
const context = useContext(LoadTestContext);
-
if (!context) {
throw new Error('useLoadTest must be used within a LoadTestProvider');
}
-
return context;
};
diff --git a/TestMasterUI/src/services/APIClient.js b/TestMasterUI/src/services/APIClient.js
new file mode 100644
index 0000000..6e25926
--- /dev/null
+++ b/TestMasterUI/src/services/APIClient.js
@@ -0,0 +1,71 @@
+import { getCSRFToken } from '@/services/token.js';
+
+class APIClient {
+ constructor() {
+ this.globals = {};
+ this.abortController = null;
+ this.contextPath = document.querySelector('meta[name="context-path"]')?.getAttribute('content') || '/';
+ this.contextPath = this.contextPath.endsWith('/') ? this.contextPath : this.contextPath + '/';
+ this.CLIENT_URL = 'mgmt/api/test.do';
+ }
+
+ abort() {
+ if (this.abortController) {
+ this.abortController.abort();
+ this.abortController = null;
+ }
+ }
+
+ async sendAPIRequest(model, onLog = () => {}) {
+ try {
+ onLog(`Request: ${model.method} ${model.path}`);
+
+ const response = await this.makeRequest(model);
+ const formattedResponse = this.formatResponse(response);
+ return formattedResponse;
+ } catch (error) {
+ if (error.name === 'AbortError') {
+ throw new Error('Request was cancelled');
+ }
+ throw new Error(`API Error: ${error.message}`);
+ } finally {
+ this.abortController = null;
+ }
+ }
+
+ async makeRequest(request) {
+ this.abortController = new AbortController();
+
+ const response = await fetch(this.getClientUrl(), {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-XSRF-TOKEN': getCSRFToken()
+ },
+ body: JSON.stringify(request),
+ signal: this.abortController.signal
+ });
+
+ if (!response.ok) {
+ const data = await response.json();
+ throw new Error(data.error || 'Unknown error');
+ }
+
+ return response.json();
+ }
+
+ formatResponse(data) {
+ return {
+ body: data.body,
+ status: data.status,
+ headers: Object.fromEntries(data.headers.map(h => [h.key, h.value])),
+ size: data.size
+ };
+ }
+
+ getClientUrl() {
+ return this.contextPath + this.CLIENT_URL;
+ }
+}
+
+export default APIClient;
diff --git a/TestMasterUI/src/services/ScenarioExecutor.js b/TestMasterUI/src/services/ScenarioExecutor.js
new file mode 100644
index 0000000..84a72c3
--- /dev/null
+++ b/TestMasterUI/src/services/ScenarioExecutor.js
@@ -0,0 +1,273 @@
+import { useCallback, useRef, useState } from 'react';
+
+export class ScenarioExecutor {
+ constructor(nodes, edges, apiClient, onLogAppend, onNodeStatusChange) {
+ this.nodes = nodes;
+ this.edges = edges;
+ this.apiClient = apiClient;
+ this.onLogAppend = onLogAppend;
+ this.onNodeStatusChange = onNodeStatusChange;
+ this.reset();
+ }
+
+ reset() {
+ this.currentNodeId = this.findStartNode()?.id;
+ this.visitedNodes = new Set();
+ this.isRunning = false;
+ this.isPaused = false;
+ this.isDone = false;
+ this.nodes.forEach(node => {
+ this.onNodeStatusChange?.(node.id, 'default');
+ });
+ }
+
+ pause() {
+ this.isPaused = true;
+ this.isRunning = false;
+ }
+
+ resume() {
+ this.isPaused = false;
+ this.isRunning = true;
+ }
+
+ findStartNode() {
+ return this.nodes.find(node => node.type === 'startNode');
+ }
+
+ findNextNodes(nodeId) {
+ return this.edges
+ .filter(edge => edge.source === nodeId)
+ .map(edge => ({
+ nodeId: edge.target,
+ condition: edge.sourceHandle
+ }));
+ }
+
+ async executeCurrentNode() {
+ if (!this.currentNodeId || this.isPaused) return;
+ console.log(this.currentNodeId);
+ const currentNode = this.nodes.find(n => n.id === this.currentNodeId);
+ console.log({currentNode});
+ if (!currentNode) return;
+
+ this.isRunning = true;
+ this.visitedNodes.add(this.currentNodeId);
+ this.onNodeStatusChange?.(this.currentNodeId, 'running');
+
+ try {
+ let result;
+ switch (currentNode.type) {
+ case 'startNode':
+ this.appendLog('Starting API scenario\n');
+ break;
+
+ case 'apiNode':
+ result = await this.executeApiNode(currentNode);
+ break;
+
+ case 'delayNode':
+ await this.executeDelayNode(currentNode);
+ break;
+
+ case 'conditionNode':
+ result = await this.executeConditionNode(currentNode);
+ break;
+ }
+
+ this.onNodeStatusChange?.(this.currentNodeId, 'success');
+ return result;
+ } catch (error) {
+ console.error(error);
+ this.onNodeStatusChange?.(this.currentNodeId, 'error');
+ this.appendLog(`Error executing node ${currentNode.id}: ${error.message}`);
+ throw error;
+ }
+ }
+
+ async executeApiNode(node) {
+ const { data } = node;
+ this.appendLog(`Executing API [${data.name}]`);
+
+ try {
+ const response = await this.apiClient.sendAPIRequest(data, this.appendLog.bind(this));
+ console.log({response});
+ this.logApiResponse(response, data.type);
+ return response;
+ } catch (error) {
+ throw new Error(`API Error: ${error.message}`);
+ }
+ }
+
+ async executeDelayNode(node) {
+ const duration = node.data.duration || 1000;
+ this.appendLog(`Delay: ${duration}ms`);
+ await new Promise(resolve => setTimeout(resolve, duration));
+ }
+
+ async executeConditionNode(node) {
+ const { condition, script } = node.data;
+ this.appendLog(`Evaluating condition: ${condition}`);
+
+ try {
+ const result = await this.evaluateCondition(script || condition);
+ this.appendLog(`Condition result: ${result}`);
+ return result;
+ } catch (error) {
+ throw new Error(`Condition evaluation failed: ${error.message}`);
+ }
+ }
+
+ async evaluateCondition(script) {
+ return new Promise((resolve, reject) => {
+ const scriptExecutor = document.createElement('iframe');
+ scriptExecutor.style.display = 'none';
+ document.body.appendChild(scriptExecutor);
+
+ window.conditionComplete = (result, error) => {
+ document.body.removeChild(scriptExecutor);
+ if (error) reject(error);
+ else resolve(!!result);
+ };
+
+ const scriptContent = `
+
+ `;
+
+ scriptExecutor.srcdoc = scriptContent;
+ });
+ }
+
+ appendLog(text) {
+ this.onLogAppend(text + '\n\n');
+ }
+
+ logApiResponse(response, type) {
+ const responseLog = type === 'HTTP'
+ ? `Response:\nStatus: ${response.status}\nHeaders:\n${this.formatHeaders(response.headers)}\nBody:\n${response.body}`
+ : `Response:\n${response.body}`;
+
+ this.appendLog(responseLog);
+ }
+
+ formatHeaders(headers) {
+ return Object.entries(headers)
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([key, value]) => `${key}: ${value}`)
+ .join('\n');
+ }
+
+ async executeAll() {
+ while (this.currentNodeId && !this.isDone && !this.isPaused) {
+ try {
+ await this.executeCurrentNode();
+ await this.moveToNextNode();
+ } catch (error) {
+ this.appendLog(`Execution stopped: ${error.message}`);
+ break;
+ }
+ }
+
+ if (this.isDone) {
+ this.appendLog('Scenario execution completed');
+ } else if (this.isPaused) {
+ this.appendLog('Scenario execution paused');
+ }
+ }
+
+ async moveToNextNode() {
+ const nextNodes = this.findNextNodes(this.currentNodeId);
+
+ if (nextNodes.length === 0) {
+ this.isDone = true;
+ this.currentNodeId = null;
+ return;
+ }
+
+ const currentNode = this.nodes.find(n => n.id === this.currentNodeId);
+
+ if (currentNode.type === 'conditionNode') {
+ const result = await this.evaluateCondition(currentNode.data.script || currentNode.data.condition);
+ const nextNode = nextNodes.find(n => n.condition === (result ? 'true' : 'false'));
+ this.currentNodeId = nextNode?.nodeId;
+ } else {
+ const nextNode = nextNodes.find(n => !this.visitedNodes.has(n.nodeId));
+ this.currentNodeId = nextNode?.nodeId;
+ }
+
+ if (!this.currentNodeId) {
+ this.isDone = true;
+ }
+ }
+}
+
+export const useScenarioExecutor = (nodes, edges, apiClient) => {
+ const [logs, setLogs] = useState('');
+ const [nodeStatuses, setNodeStatuses] = useState({});
+ const executorRef = useRef(null);
+
+ const appendLog = useCallback((text) => {
+ setLogs(prev => prev + text);
+ }, []);
+
+ const updateNodeStatus = useCallback((nodeId, status) => {
+ setNodeStatuses(prev => ({ ...prev, [nodeId]: status }));
+ }, []);
+
+ const initExecutor = useCallback(() => {
+ executorRef.current = new ScenarioExecutor(
+ nodes,
+ edges,
+ apiClient,
+ appendLog,
+ updateNodeStatus
+ );
+ }, [nodes, edges, apiClient]);
+
+ const executeStep = useCallback(async () => {
+ if (!executorRef.current) initExecutor();
+ if (!executorRef.current.isDone) {
+ await executorRef.current.executeCurrentNode();
+ await executorRef.current.moveToNextNode();
+ }
+ }, [initExecutor]);
+
+ const executeScenario = useCallback(async () => {
+ if (!executorRef.current) initExecutor();
+ await executorRef.current.executeAll();
+ }, [initExecutor]);
+
+ const pauseExecution = useCallback(() => {
+ if (executorRef.current) {
+ executorRef.current.pause();
+ }
+ }, []);
+
+ const resumeExecution = useCallback(async () => {
+ if (executorRef.current) {
+ executorRef.current.resume();
+ await executorRef.current.executeAll();
+ }
+ }, []);
+
+ return {
+ logs,
+ nodeStatuses,
+ clearLogs: () => setLogs(''),
+ executeScenario,
+ executeStep,
+ pauseExecution,
+ resumeExecution,
+ resetExecutor: initExecutor,
+ isRunning: executorRef.current?.isRunning ?? false,
+ isPaused: executorRef.current?.isPaused ?? false,
+ isDone: executorRef.current?.isDone ?? false
+ };
+};
diff --git a/TestMasterUI/src/services/loadTestService.js b/TestMasterUI/src/services/loadTestService.js
new file mode 100644
index 0000000..0ec675d
--- /dev/null
+++ b/TestMasterUI/src/services/loadTestService.js
@@ -0,0 +1,72 @@
+// services/loadTestService.js
+import { getCSRFToken } from '@/services/token.js';
+
+export const loadTestService = (contextPath = '/') => {
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'X-XSRF-TOKEN': getCSRFToken()
+ };
+
+ return {
+ async loadTests() {
+ const response = await fetch(`${contextPath}mgmt/load_test/list.do`, { headers });
+ if (!response.ok) throw new Error('Failed to fetch load tests');
+ return await response.json();
+ },
+
+ async createTest(testName) {
+ const response = await fetch(`${contextPath}mgmt/load_test/create.do`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({ name: testName })
+ });
+
+ if (!response.ok) throw new Error('Failed to create test');
+ return await response.json();
+ },
+
+ async updateTest(test) {
+ const response = await fetch(`${contextPath}mgmt/load_test/update.do`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(test)
+ });
+
+ if (!response.ok) throw new Error('Failed to update test');
+ return test;
+ },
+
+ async deleteTest(testId) {
+ const response = await fetch(`${contextPath}mgmt/load_test/delete.do`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({ id: testId })
+ });
+
+ if (!response.ok) throw new Error('Failed to delete test');
+ return true;
+ },
+
+ async startTest(test) {
+ const response = await fetch(`${contextPath}startLoadTest`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(test)
+ });
+
+ if (!response.ok) throw new Error('Failed to start test');
+ return true;
+ },
+
+ async stopTest(test) {
+ const response = await fetch(`${contextPath}stopLoadTest`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(test)
+ });
+
+ if (!response.ok) throw new Error('Failed to stop test');
+ return true;
+ }
+ };
+};
diff --git a/TestMasterUI/src/services/scenarioService.js b/TestMasterUI/src/services/scenarioService.js
new file mode 100644
index 0000000..f8ba637
--- /dev/null
+++ b/TestMasterUI/src/services/scenarioService.js
@@ -0,0 +1,50 @@
+// services/scenarioService.js
+import { getCSRFToken } from '@/services/token.js';
+
+export const scenarioService = (contextPath = '/') => {
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'X-XSRF-TOKEN': getCSRFToken()
+ };
+
+ return {
+ async loadScenarios() {
+ const response = await fetch(`${contextPath}mgmt/api_scenario/list.do`, { headers });
+ if (!response.ok) throw new Error('Failed to fetch scenarios');
+ return await response.json();
+ },
+
+ async createScenario(name) {
+ const response = await fetch(`${contextPath}mgmt/api_scenario/create.do`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({ name })
+ });
+
+ if (!response.ok) throw new Error('Failed to create scenario');
+ return await response.json();
+ },
+
+ async updateScenario(scenario) {
+ const response = await fetch(`${contextPath}mgmt/api_scenario/update.do`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(scenario)
+ });
+
+ if (!response.ok) throw new Error('Failed to update scenario');
+ return scenario;
+ },
+
+ async deleteScenario(scenario) {
+ const response = await fetch(`${contextPath}mgmt/api_scenario/delete.do`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(scenario)
+ });
+
+ if (!response.ok) throw new Error('Failed to delete scenario');
+ return true;
+ }
+ };
+};