From bca5840712278874048193f269ce189fae10e29d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=98=84=EC=84=B1=ED=95=84?= Date: Sat, 25 Jan 2025 09:51:11 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=9C=EB=82=98=EB=A6=AC=EC=98=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TestMasterUI/src/components/APITester.jsx | 21 ++ .../src/components/MainContentArea.jsx | 9 +- .../api-tester/AddScenarioDialog.jsx | 111 +++++++ .../api-tester/DeleteScenarioDialog.jsx | 44 +++ .../src/components/api-tester/DiagramArea.jsx | 154 +++++----- .../api-tester/DraggableApiItem.jsx | 13 +- .../components/api-tester/ScenarioEditor.jsx | 137 +++++++-- .../api-tester/ScenarioNavigation.jsx | 44 ++- .../components/client-views/TCPClientView.jsx | 11 +- .../components/layout/LayoutTreeDataGrid.jsx | 2 +- TestMasterUI/src/lib/api-client.js | 272 ----------------- TestMasterUI/src/providers/APIProvider.jsx | 2 +- .../src/providers/LoadTestProvider.jsx | 218 +++++++------- TestMasterUI/src/services/APIClient.js | 71 +++++ TestMasterUI/src/services/ScenarioExecutor.js | 273 ++++++++++++++++++ TestMasterUI/src/services/loadTestService.js | 72 +++++ TestMasterUI/src/services/scenarioService.js | 50 ++++ 17 files changed, 989 insertions(+), 515 deletions(-) create mode 100644 TestMasterUI/src/components/api-tester/AddScenarioDialog.jsx create mode 100644 TestMasterUI/src/components/api-tester/DeleteScenarioDialog.jsx delete mode 100644 TestMasterUI/src/lib/api-client.js create mode 100644 TestMasterUI/src/services/APIClient.js create mode 100644 TestMasterUI/src/services/ScenarioExecutor.js create mode 100644 TestMasterUI/src/services/loadTestService.js create mode 100644 TestMasterUI/src/services/scenarioService.js diff --git a/TestMasterUI/src/components/APITester.jsx b/TestMasterUI/src/components/APITester.jsx index 9f4d945..a9b5dcd 100644 --- a/TestMasterUI/src/components/APITester.jsx +++ b/TestMasterUI/src/components/APITester.jsx @@ -4,6 +4,8 @@ import MainContentArea from '@/components/MainContentArea.jsx'; import {useAPI} from '@/providers/APIProvider.jsx'; import AddCollectionDialog from '@/components/api-tester/AddCollectionDialog.jsx'; import AddAPIDialog from '@/components/api-tester/AddAPIDialog.jsx'; +import AddScenarioDialog from '@/components/api-tester/AddScenarioDialog.jsx'; +import {useLoadTest} from '@/providers/LoadTestProvider.jsx'; const menuConfig = [ { @@ -78,7 +80,9 @@ const APITester = () => { const [isApiMode, setIsApiMode] = useState(true); const [showAddCollection, setShowAddCollection] = useState(false); const [showAddApi, setShowAddApi] = useState(false); + const [showAddScenario, setShowAddScenario] = useState(false); const { handleLogout, createCollection, createAPIRequest } = useAPI(); + const {createScenario} = useLoadTest(); // 모드 전환 핸들러 const toggleMode = () => { @@ -104,6 +108,15 @@ const APITester = () => { } }; + const handleAddScenario = async (name) => { + try { + await createScenario(name); + return true; + } catch (error) { + console.error('시나리오 생성에 실패했습니다.'); + } + } + const handleMenuClick = async (action) => { // Handle menu item clicks @@ -115,6 +128,9 @@ const APITester = () => { case 'collection-add': setShowAddCollection(true); break; + case 'scenario-add': + setShowAddScenario(true); + break; case 'logout': const success = await handleLogout(); if (success) { @@ -176,6 +192,11 @@ const APITester = () => { onOpenChange={setShowAddApi} onSubmit={handleAddApi} /> + ); }; diff --git a/TestMasterUI/src/components/MainContentArea.jsx b/TestMasterUI/src/components/MainContentArea.jsx index 8e3b695..7a690e1 100644 --- a/TestMasterUI/src/components/MainContentArea.jsx +++ b/TestMasterUI/src/components/MainContentArea.jsx @@ -7,7 +7,7 @@ import ScenarioEditor from '@/components/api-tester/ScenarioEditor.jsx'; const MainContentArea = ({ isApiMode, toggleMode }) => { return ( -
+
{isApiMode ? : }
); @@ -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 ( + <> + + + + 새 시나리오 추가 + + 새로운 시나리오의 이름을 입력해주세요 + + +
+
+
+ + { + setScenarioName(e.target.value); + setError(''); + }} + placeholder="시나리오 이름을 입력하세요" + /> +
+
+ + + + +
+
+
+ + + + ); +}; + +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 }) => { /> ))}
- + Start
); }); -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 }) => { ))}
-
+
{data.method}
@@ -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 }) => { />
-
+
Condition
@@ -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 }) => { ))}
-
+
Delay
@@ -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; + } + }; +};