|
|
import React, { useEffect, useState, useRef } from 'react';
|
|
|
import { Button, Space, Tree, Collapse, Divider, Message, Modal } from '@arco-design/web-react';
|
|
|
import { IconLeft, IconPlus, IconEdit, IconDelete, IconLink, IconSend } from '@arco-design/web-react/icon';
|
|
|
import styles from './style/testInstance.module.less';
|
|
|
import { getComponentDesign } from '@/api/componentDevelopProcess';
|
|
|
import {
|
|
|
getComponentTestCaseList,
|
|
|
submitTestCase,
|
|
|
deleteTestCase,
|
|
|
exportTemplate,
|
|
|
exportTestCases,
|
|
|
importTestCases, startTestCase
|
|
|
} from '@/api/componentTestCase';
|
|
|
import TestCaseModal from './testCaseModal';
|
|
|
import useWebSocket from '@/hooks/useWebSocket';
|
|
|
import { getToken } from '@/utils/auth';
|
|
|
|
|
|
const CollapseItem = Collapse.Item;
|
|
|
|
|
|
const TestInstance = ({ instance, parentId, onBack }: { instance: any; parentId: string; onBack: () => void }) => {
|
|
|
const [activeTab, setActiveTab] = useState('link');
|
|
|
const [testCaseList, setTestCaseList] = useState([]);
|
|
|
const [design, setDesign] = useState(null);
|
|
|
const [modalVisible, setModalVisible] = useState(false);
|
|
|
const [selectedOperationIdent, setSelectedOperationIdent] = useState('');
|
|
|
const [activeOperationIdent, setActiveOperationIdent] = useState('');
|
|
|
const [editingTestCase, setEditingTestCase] = useState(null);
|
|
|
const [logs, setLogs] = useState<string[]>([]);
|
|
|
const [isSocketConnected, setIsSocketConnected] = useState(false);
|
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
// WebSocket hook
|
|
|
const { connect, disconnect, isConnected, sendMessage } = useWebSocket({
|
|
|
onOpen: () => {
|
|
|
console.log('链接成功');
|
|
|
setIsSocketConnected(true);
|
|
|
addLog('连接信息:连接成功!');
|
|
|
},
|
|
|
onClose: () => {
|
|
|
console.log('链接关闭');
|
|
|
setIsSocketConnected(false);
|
|
|
addLog('连接信息:连接已断开');
|
|
|
},
|
|
|
onError: () => {
|
|
|
console.log('链接错误');
|
|
|
addLog('连接错误:连接失败');
|
|
|
},
|
|
|
onMessage: (event) => {
|
|
|
try {
|
|
|
const data = JSON.parse(event.data);
|
|
|
addLog(`收到消息: ${JSON.stringify(data)}`);
|
|
|
} catch (e) {
|
|
|
addLog(`收到消息: ${event.data}`);
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// 添加日志
|
|
|
const addLog = (message: string) => {
|
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
|
setLogs(prev => [...prev, `[${timestamp}] ${message}`]);
|
|
|
};
|
|
|
|
|
|
// 清空日志
|
|
|
const clearLogs = () => {
|
|
|
setLogs([]);
|
|
|
};
|
|
|
|
|
|
const getDesign = async () => {
|
|
|
const res: any = await getComponentDesign(parentId);
|
|
|
if (res.code === 200) {
|
|
|
setDesign(res.data);
|
|
|
// 默认激活第一个 operation
|
|
|
if (res.data?.operates?.length > 0) {
|
|
|
setActiveOperationIdent(res.data.operates[0].ident);
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const getTestCaseList = async () => {
|
|
|
const res: any = await getComponentTestCaseList({ componentBaseId: parentId, identifier: instance.identifier });
|
|
|
if (res.code === 200) setTestCaseList(res.data);
|
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
|
getTestCaseList();
|
|
|
getDesign();
|
|
|
}, [parentId, instance]);
|
|
|
|
|
|
// 组件卸载时断开连接
|
|
|
useEffect(() => {
|
|
|
return () => {
|
|
|
disconnect();
|
|
|
};
|
|
|
}, [disconnect]);
|
|
|
|
|
|
|
|
|
const cryptoRandom = () => {
|
|
|
return new Date().getTime().toString(16) + Math.random().toString(16).substring(2);
|
|
|
};
|
|
|
|
|
|
// 链接实例
|
|
|
const handleLinkInstance = async () => {
|
|
|
if (isSocketConnected) {
|
|
|
disconnect();
|
|
|
Message.info('已断开连接');
|
|
|
}
|
|
|
else {
|
|
|
// WebSocket连接前置
|
|
|
const res: any = await startTestCase(instance.id);
|
|
|
console.log('res:', res);
|
|
|
|
|
|
// 构建WebSocket URL,根据你的实际后端配置调整
|
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
|
const host = window.location.host;
|
|
|
let wsUrl = '';
|
|
|
if (window.location.host.includes('localhost')) {
|
|
|
wsUrl = `${process.env.NEXT_PUBLIC_DEV_SOCKET_HOST}/ws/v1/bpms-workbench/test-case/${instance.id}/${cryptoRandom()}?Authorization=Bearer ${getToken()}`;
|
|
|
}
|
|
|
else {
|
|
|
wsUrl = `${protocol}//${host}/ws/v1/bpms-workbench/test-case/${instance.id}/${cryptoRandom()}?Authorization=Bearer ${getToken()}`;
|
|
|
}
|
|
|
|
|
|
connect(wsUrl);
|
|
|
addLog('正在连接测试实例...');
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const handleAddTestCase = (operationIdent: string) => {
|
|
|
setSelectedOperationIdent(operationIdent);
|
|
|
setEditingTestCase(null);
|
|
|
setModalVisible(true);
|
|
|
};
|
|
|
|
|
|
const handleEditTestCase = (testCase: any, operationIdent: string) => {
|
|
|
setSelectedOperationIdent(operationIdent);
|
|
|
setEditingTestCase(testCase);
|
|
|
setModalVisible(true);
|
|
|
};
|
|
|
|
|
|
const handleDeleteTestCase = (testCase: any) => {
|
|
|
Modal.confirm({
|
|
|
title: '确认删除',
|
|
|
content: `确定要删除测试用例"${testCase.testCaseName}"吗?`,
|
|
|
okText: '确定',
|
|
|
cancelText: '取消',
|
|
|
onOk: async () => {
|
|
|
try {
|
|
|
const res: any = await deleteTestCase(testCase.id);
|
|
|
if (res.code === 200) {
|
|
|
Message.success('删除成功');
|
|
|
getTestCaseList();
|
|
|
}
|
|
|
else {
|
|
|
Message.error(res.msg || '删除失败');
|
|
|
}
|
|
|
} catch (error) {
|
|
|
Message.error('删除失败');
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
};
|
|
|
|
|
|
// 导出测试用例模板
|
|
|
const handleExportTemplate = async () => {
|
|
|
try {
|
|
|
Message.loading('正在生成模板文件...');
|
|
|
const res: any = await exportTemplate(parentId);
|
|
|
|
|
|
// 创建下载链接
|
|
|
const blob = new Blob([res], {
|
|
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
|
});
|
|
|
const url = window.URL.createObjectURL(blob);
|
|
|
const link = document.createElement('a');
|
|
|
link.href = url;
|
|
|
|
|
|
// 设置文件名
|
|
|
const fileName = `测试用例模板_${instance.identifier}_${new Date().getTime()}.xlsx`;
|
|
|
link.setAttribute('download', fileName);
|
|
|
|
|
|
// 触发下载
|
|
|
document.body.appendChild(link);
|
|
|
link.click();
|
|
|
|
|
|
// 清理
|
|
|
document.body.removeChild(link);
|
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
|
|
Message.success('模板下载成功');
|
|
|
} catch (error) {
|
|
|
console.error('导出模板失败:', error);
|
|
|
Message.error('导出模板失败');
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 导出当前测试用例
|
|
|
const handleExportTestCases = async () => {
|
|
|
try {
|
|
|
// 检查是否有测试用例
|
|
|
if (!testCaseList || testCaseList.length === 0) {
|
|
|
Message.warning('当前没有测试用例可导出');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
Message.loading('正在生成测试用例文件...');
|
|
|
const res: any = await exportTestCases(parentId);
|
|
|
|
|
|
// 创建下载链接
|
|
|
const blob = new Blob([res], {
|
|
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
|
});
|
|
|
const url = window.URL.createObjectURL(blob);
|
|
|
const link = document.createElement('a');
|
|
|
link.href = url;
|
|
|
|
|
|
// 设置文件名
|
|
|
const fileName = `测试用例_${instance.identifier}_${new Date().getTime()}.xlsx`;
|
|
|
link.setAttribute('download', fileName);
|
|
|
|
|
|
// 触发下载
|
|
|
document.body.appendChild(link);
|
|
|
link.click();
|
|
|
|
|
|
// 清理
|
|
|
document.body.removeChild(link);
|
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
|
|
Message.success('测试用例导出成功');
|
|
|
} catch (error) {
|
|
|
console.error('导出测试用例失败:', error);
|
|
|
Message.error('导出测试用例失败');
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 触发文件选择
|
|
|
const handleImportClick = () => {
|
|
|
fileInputRef.current?.click();
|
|
|
};
|
|
|
|
|
|
// 处理文件上传
|
|
|
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
const file = event.target.files?.[0];
|
|
|
if (!file) return;
|
|
|
|
|
|
// 验证文件类型
|
|
|
const allowedTypes = [
|
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
|
|
|
'application/vnd.ms-excel' // .xls
|
|
|
];
|
|
|
|
|
|
if (!allowedTypes.includes(file.type) && !file.name.match(/\.(xlsx|xls)$/i)) {
|
|
|
Message.error('请选择Excel文件(.xlsx 或 .xls)');
|
|
|
event.target.value = ''; // 清空input
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
Message.loading('正在导入测试用例...');
|
|
|
|
|
|
const res: any = await importTestCases({
|
|
|
componentBaseId: parentId,
|
|
|
file: file
|
|
|
});
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
Message.success(`导入成功!`);
|
|
|
getTestCaseList(); // 刷新列表
|
|
|
}
|
|
|
else {
|
|
|
Message.error(res.msg || '导入失败');
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('导入测试用例失败:', error);
|
|
|
Message.error('导入测试用例失败');
|
|
|
} finally {
|
|
|
// 清空input,允许重复选择同一文件
|
|
|
event.target.value = '';
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const handleModalOk = async (values: any) => {
|
|
|
const params = {
|
|
|
...values,
|
|
|
componentBaseId: parentId,
|
|
|
identifier: instance.identifier
|
|
|
};
|
|
|
const res: any = await submitTestCase(params);
|
|
|
if (res.code === 200) {
|
|
|
Message.success('添加测试用例成功');
|
|
|
getTestCaseList();
|
|
|
}
|
|
|
else Message.error(res.msg);
|
|
|
setModalVisible(false);
|
|
|
};
|
|
|
|
|
|
const handleModalCancel = () => {
|
|
|
setModalVisible(false);
|
|
|
setEditingTestCase(null);
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
<div className={styles['test-instance']}>
|
|
|
{/* 隐藏的文件input */}
|
|
|
<input
|
|
|
ref={fileInputRef}
|
|
|
type="file"
|
|
|
accept=".xlsx,.xls"
|
|
|
style={{ display: 'none' }}
|
|
|
onChange={handleFileChange}
|
|
|
/>
|
|
|
|
|
|
{/* 顶部标签栏 */}
|
|
|
<div className={styles['tabs']}>
|
|
|
<div className={styles['tab-left']}>
|
|
|
<Button
|
|
|
type="primary"
|
|
|
status={isSocketConnected ?'danger' : 'default'}
|
|
|
icon={<IconLink />}
|
|
|
onClick={handleLinkInstance}
|
|
|
style={{ minWidth: 250 }}
|
|
|
>
|
|
|
{isSocketConnected ? '断开实例' : '链接实例'}
|
|
|
</Button>
|
|
|
</div>
|
|
|
<div className={styles['tab-center']}>
|
|
|
<Button
|
|
|
type="outline"
|
|
|
onClick={handleExportTemplate}
|
|
|
>
|
|
|
下载当前测试用例模板
|
|
|
</Button>
|
|
|
<Button
|
|
|
type="outline"
|
|
|
onClick={handleImportClick}
|
|
|
>
|
|
|
从模板导入用例
|
|
|
</Button>
|
|
|
{/*<Button*/}
|
|
|
{/* type="outline"*/}
|
|
|
{/* onClick={() => setActiveTab('batch')}*/}
|
|
|
{/*>*/}
|
|
|
{/* 一键生成测试用例*/}
|
|
|
{/*</Button>*/}
|
|
|
<Button
|
|
|
type="outline"
|
|
|
onClick={handleExportTestCases}
|
|
|
>
|
|
|
导出当前测试用例
|
|
|
</Button>
|
|
|
</div>
|
|
|
<div className={styles['tab-right']}></div>
|
|
|
</div>
|
|
|
|
|
|
<div className={styles['main-content']}>
|
|
|
{/* 左侧测试用例列表 */}
|
|
|
<div className={styles['left-panel']}>
|
|
|
<div className={styles['panel-header']}>
|
|
|
<span>测试用例</span>
|
|
|
</div>
|
|
|
<div className={styles['tree-container']}>
|
|
|
<Collapse defaultActiveKey={['1']} bordered={false} accordion>
|
|
|
{testCaseList.map((item: any, index: number) => (
|
|
|
<CollapseItem
|
|
|
header={
|
|
|
<div
|
|
|
style={{
|
|
|
width: '100%',
|
|
|
height: '100%',
|
|
|
display: 'flex',
|
|
|
alignItems: 'center'
|
|
|
}}
|
|
|
onClick={() => setActiveOperationIdent(item.operationIdent)}
|
|
|
>
|
|
|
{item.operationIdent}
|
|
|
</div>
|
|
|
}
|
|
|
name={index.toString()}
|
|
|
key={index}
|
|
|
extra={
|
|
|
<IconPlus
|
|
|
onClick={(e) => {
|
|
|
e.stopPropagation();
|
|
|
handleAddTestCase(item.operationIdent);
|
|
|
}}
|
|
|
style={{ cursor: 'pointer' }}
|
|
|
/>
|
|
|
}
|
|
|
>
|
|
|
<div className={styles['tree-list']}>
|
|
|
{item.children.map((child: any, index1: number) => (
|
|
|
<div
|
|
|
className={styles['tree-item']}
|
|
|
key={index1}
|
|
|
onClick={() => setActiveOperationIdent(item.operationIdent)}
|
|
|
>
|
|
|
<span className={styles['item-text']}>{child.testCaseName}</span>
|
|
|
<div className={styles['item-actions']}>
|
|
|
<IconSend />
|
|
|
<IconEdit
|
|
|
onClick={(e) => {
|
|
|
e.stopPropagation();
|
|
|
handleEditTestCase(child, item.operationIdent);
|
|
|
}}
|
|
|
/>
|
|
|
<IconDelete
|
|
|
onClick={(e) => {
|
|
|
e.stopPropagation();
|
|
|
handleDeleteTestCase(child);
|
|
|
}}
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
</CollapseItem>
|
|
|
))}
|
|
|
</Collapse>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
{/* 中间流程图区域 */}
|
|
|
<div className={styles['center-panel']}>
|
|
|
<div className={styles['flow-wrapper']}>
|
|
|
{/* 开始节点 */}
|
|
|
<div className={styles['flow-node']}>
|
|
|
<div className={`${styles['node-box']} ${styles['node-start']}`}>
|
|
|
<div className={styles['node-header']}>
|
|
|
<span className={styles['node-title']}>开始</span>
|
|
|
</div>
|
|
|
<div className={styles['node-label']}>流程协作接口</div>
|
|
|
</div>
|
|
|
<div className={styles['node-connector']}>
|
|
|
<div className={styles['connector-line']}></div>
|
|
|
<div className={styles['connector-dot']}></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
{/* 组件节点 - 单个节点展示所有接口 */}
|
|
|
<div className={styles['flow-node']}>
|
|
|
<div className={`${styles['node-box']} ${styles['node-component']}`}>
|
|
|
<div className={styles['node-header']}>
|
|
|
<span className={styles['node-title']}>{instance.identifier}</span>
|
|
|
</div>
|
|
|
<div className={styles['node-body']}>
|
|
|
{/* 显示所有接口函数 - 固定在上方 */}
|
|
|
{design?.operates?.map((operation: any, index: number) => (
|
|
|
<div
|
|
|
key={index}
|
|
|
className={`${styles['node-function']} ${
|
|
|
activeOperationIdent === operation.ident ? styles['active'] : ''
|
|
|
}`}
|
|
|
>
|
|
|
<span className={styles['function-dot']}></span>
|
|
|
{operation.ident}
|
|
|
</div>
|
|
|
))}
|
|
|
|
|
|
{/* 分割线 */}
|
|
|
<Divider />
|
|
|
|
|
|
{/* 显示激活接口的参数 */}
|
|
|
{design?.operates?.map((operation: any, index: number) => (
|
|
|
activeOperationIdent === operation.ident && (
|
|
|
<div key={index}>
|
|
|
<div className={styles['node-params']}>
|
|
|
{/* Input 参数 - 显示 parameters */}
|
|
|
{operation.parameters?.map((param: any, paramIndex: number) => (
|
|
|
<div className={`${styles['param-item']} ${styles['active']}`} key={paramIndex}>
|
|
|
<span className={styles['param-dot']}></span>
|
|
|
<span className={styles['param-label']}>
|
|
|
{param.ident} {param.type}
|
|
|
</span>
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
{/* Output 参数 - 显示 responses */}
|
|
|
{operation.responses?.map((response: any, respIndex: number) => (
|
|
|
<div className={`${styles['param-item-right']} ${styles['active']}`} key={respIndex}>
|
|
|
<span className={styles['param-label']}>
|
|
|
{response.ident} {response.type}
|
|
|
</span>
|
|
|
<span className={styles['param-dot']}></span>
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
)
|
|
|
))}
|
|
|
</div>
|
|
|
</div>
|
|
|
<div className={styles['node-connector']}>
|
|
|
<div className={styles['connector-line']}></div>
|
|
|
<div className={styles['connector-dot']}></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
{/* 结束节点 */}
|
|
|
<div className={styles['flow-node']}>
|
|
|
<div className={`${styles['node-box']} ${styles['node-end']}`}>
|
|
|
<div className={styles['node-header']}>
|
|
|
<span className={styles['node-title']}>结束</span>
|
|
|
</div>
|
|
|
<div className={styles['node-label']}>流程协作接口</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
{/* 右侧运行日志 */}
|
|
|
<div className={styles['right-panel']}>
|
|
|
<div className={styles['panel-header']}>
|
|
|
<span>运行日志</span>
|
|
|
<div className={styles['header-actions']}>
|
|
|
<span className={styles['icon-btn']} onClick={clearLogs} title="清空日志">
|
|
|
<IconDelete />
|
|
|
</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div className={styles['log-content']}>
|
|
|
{logs.length === 0 ? (
|
|
|
<div className={styles['log-empty']}>
|
|
|
<p>暂无日志信息</p>
|
|
|
<p>请点击"链接实例"按钮连接测试实例</p>
|
|
|
</div>
|
|
|
) : (
|
|
|
<div className={styles['log-list']}>
|
|
|
{logs.map((log, index) => (
|
|
|
<div key={index} className={styles['log-item']}>
|
|
|
{log}
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
)}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
{modalVisible && <TestCaseModal
|
|
|
design={design.operates}
|
|
|
visible={modalVisible}
|
|
|
operationIdent={selectedOperationIdent}
|
|
|
editingData={editingTestCase}
|
|
|
onCancel={handleModalCancel}
|
|
|
onOk={handleModalOk}
|
|
|
/>}
|
|
|
</div>
|
|
|
);
|
|
|
};
|
|
|
|
|
|
export default TestInstance; |