feat(component-test): 完善组件测试功能与界面交互

master
钟良源 2 months ago
parent 000d985420
commit bba9318e9d

@ -11,6 +11,7 @@ const BreadcrumbItem = Breadcrumb.Item;
const ComponentTest = () => { const ComponentTest = () => {
const [selectedIdentifier, setSelectedIdentifier] = useState<string>(''); const [selectedIdentifier, setSelectedIdentifier] = useState<string>('');
const [selectedParentId, setSelectedParentId] = useState<string>('');
const [refreshKey, setRefreshKey] = useState<number>(0); const [refreshKey, setRefreshKey] = useState<number>(0);
const [count, setCount] = useState({ total: 0, passed: 0, failed: 0 }); const [count, setCount] = useState({ total: 0, passed: 0, failed: 0 });
const [currentView, setCurrentView] = useState<'list' | 'test'>('list'); const [currentView, setCurrentView] = useState<'list' | 'test'>('list');
@ -24,8 +25,9 @@ const ComponentTest = () => {
]; ];
// 处理子节点选择 // 处理子节点选择
const handleNodeSelect = (identifier: string) => { const handleNodeSelect = (identifier: string, parentId: string) => {
setSelectedIdentifier(identifier); setSelectedIdentifier(identifier);
setSelectedParentId(parentId);
// 每次选择都更新 refreshKey,确保即使选择同一个节点也会触发刷新 // 每次选择都更新 refreshKey,确保即使选择同一个节点也会触发刷新
setRefreshKey(prev => prev + 1); setRefreshKey(prev => prev + 1);
}; };
@ -106,6 +108,7 @@ const ComponentTest = () => {
</> </>
) : ( ) : (
<TestInstance <TestInstance
parentId={selectedParentId}
instance={selectedInstance} instance={selectedInstance}
onBack={handleBackToList} onBack={handleBackToList}
/> />

@ -64,6 +64,7 @@ const SideBar = ({ onNodeSelect, getCount }) => {
classifyMap.set(classify, { classifyMap.set(classify, {
title: classify, title: classify,
key: `classify-${classify}`, key: `classify-${classify}`,
id: item.id,
children: [] children: []
}); });
} }
@ -72,6 +73,7 @@ const SideBar = ({ onNodeSelect, getCount }) => {
classifyMap.get(classify).children.push({ classifyMap.get(classify).children.push({
title: item.name, title: item.name,
key: item.identifier, key: item.identifier,
id: item.id,
data: item data: item
}); });
}); });
@ -116,8 +118,9 @@ const SideBar = ({ onNodeSelect, getCount }) => {
} }
} }
else if (selectedKey) { else if (selectedKey) {
// 子节点的key就是identifier const parentId = info.node.props.id;
onNodeSelect(selectedKey);
onNodeSelect(selectedKey, parentId);
} }
}; };

@ -207,10 +207,32 @@
padding: 16px; padding: 16px;
.node-function { .node-function {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px; font-size: 15px;
font-weight: 500; font-weight: 500;
color: #1d2129; color: #1d2129;
margin-bottom: 10px; margin-bottom: 10px;
opacity: 0.4;
transition: opacity 0.3s;
.function-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #86909c;
flex-shrink: 0;
transition: background 0.3s;
}
&.active {
opacity: 1;
.function-dot {
background: #165dff;
}
}
} }
.node-params { .node-params {
@ -223,18 +245,30 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
opacity: 0.4;
transition: opacity 0.3s;
.param-dot { .param-dot {
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background: #86909c; background: #86909c;
flex-shrink: 0;
transition: background 0.3s;
} }
.param-label { .param-label {
font-size: 14px; font-size: 14px;
color: #4e5969; color: #4e5969;
} }
&.active {
opacity: 1;
.param-dot {
background: #165dff;
}
}
} }
} }
@ -243,18 +277,31 @@
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
gap: 8px; gap: 8px;
margin-bottom: 7px;
opacity: 0.4;
transition: opacity 0.3s;
.param-dot { .param-dot {
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background: #86909c; background: #86909c;
flex-shrink: 0;
transition: background 0.3s;
} }
.param-label { .param-label {
font-size: 14px; font-size: 14px;
color: #4e5969; color: #4e5969;
} }
&.active {
opacity: 1;
.param-dot {
background: #165dff;
}
}
} }
} }
} }

@ -1,22 +1,65 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Space, Tree, Collapse } from '@arco-design/web-react'; import { Button, Space, Tree, Collapse, Divider, Message } from '@arco-design/web-react';
import { IconLeft, IconPlus, IconEdit, IconDelete, IconLink, IconSend } from '@arco-design/web-react/icon'; import { IconLeft, IconPlus, IconEdit, IconDelete, IconLink, IconSend } from '@arco-design/web-react/icon';
import styles from './style/testInstance.module.less'; import styles from './style/testInstance.module.less';
import { getComponentDesign } from '@/api/componentDevelopProcess'; import { getComponentDesign } from '@/api/componentDevelopProcess';
import { getComponentTestCaseList, submitTestCase } from '@/api/componentTestCase';
import TestCaseModal from './TestCaseModal';
const CollapseItem = Collapse.Item; const CollapseItem = Collapse.Item;
const TestInstance = ({ instance, onBack }: { instance: any; onBack: () => void }) => { const TestInstance = ({ instance, parentId, onBack }: { instance: any; parentId: string; onBack: () => void }) => {
const [activeTab, setActiveTab] = useState('link'); 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 getDesign = async () => { const getDesign = async () => {
const res: any = await getComponentDesign(instance.id); const res: any = await getComponentDesign(parentId);
console.log('res:', res); 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(() => { useEffect(() => {
getTestCaseList();
getDesign(); getDesign();
}, [instance]); }, [parentId, instance]);
const handleAddTestCase = (operationIdent: string) => {
setSelectedOperationIdent(operationIdent);
setModalVisible(true);
};
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);
};
return ( return (
<div className={styles['test-instance']}> <div className={styles['test-instance']}>
@ -68,35 +111,44 @@ const TestInstance = ({ instance, onBack }: { instance: any; onBack: () => void
<span></span> <span></span>
</div> </div>
<div className={styles['tree-container']}> <div className={styles['tree-container']}>
<Collapse defaultActiveKey={['1']} bordered={false}> <Collapse defaultActiveKey={['1']} bordered={false} accordion>
<CollapseItem header="监听组件" name="1" extra={<IconPlus />}> {testCaseList.map((item: any, index: number) => (
<div className={styles['tree-list']}> <CollapseItem
<div className={styles['tree-item']}> header={
<span className={styles['item-text']}></span> <div onClick={() => setActiveOperationIdent(item.operationIdent)}>
<div className={styles['item-actions']}> {item.operationIdent}
<IconSend />
<IconEdit />
<IconDelete />
</div> </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 />
<IconDelete />
</div>
</div>
))}
</div> </div>
<div className={styles['tree-item']}> </CollapseItem>
<span className={styles['item-text']}>jck|ssss</span> ))}
<div className={styles['item-actions']}>
<IconSend />
<IconEdit />
<IconDelete />
</div>
</div>
<div className={styles['tree-item']}>
<span className={styles['item-text']}></span>
<div className={styles['item-actions']}>
<IconSend />
<IconEdit />
<IconDelete />
</div>
</div>
</div>
</CollapseItem>
</Collapse> </Collapse>
</div> </div>
</div> </div>
@ -108,7 +160,6 @@ const TestInstance = ({ instance, onBack }: { instance: any; onBack: () => void
<div className={styles['flow-node']}> <div className={styles['flow-node']}>
<div className={`${styles['node-box']} ${styles['node-start']}`}> <div className={`${styles['node-box']} ${styles['node-start']}`}>
<div className={styles['node-header']}> <div className={styles['node-header']}>
<span className={styles['node-icon']}>🔧</span>
<span className={styles['node-title']}></span> <span className={styles['node-title']}></span>
</div> </div>
<div className={styles['node-label']}></div> <div className={styles['node-label']}></div>
@ -119,38 +170,56 @@ const TestInstance = ({ instance, onBack }: { instance: any; onBack: () => void
</div> </div>
</div> </div>
{/* 组件节点 */} {/* 组件节点 - 单个节点展示所有接口 */}
<div className={styles['flow-node']}> <div className={styles['flow-node']}>
<div className={`${styles['node-box']} ${styles['node-component']}`}> <div className={`${styles['node-box']} ${styles['node-component']}`}>
<div className={styles['node-header']}> <div className={styles['node-header']}>
<span className={styles['node-icon']}></span> <span className={styles['node-title']}>{instance.identifier}</span>
<span className={styles['node-title']}>#1</span>
</div> </div>
<div className={styles['node-body']}> <div className={styles['node-body']}>
<div className={styles['node-function']}>lineMove</div> {/* 显示所有接口函数 - 固定在上方 */}
<div className={styles['node-function']}>circleMove</div> {design?.operates?.map((operation: any, index: number) => (
<div className={styles['node-params']}> <div
<div className={styles['param-item']}> key={index}
<span className={styles['param-dot']}></span> className={`${styles['node-function']} ${
<span className={styles['param-label']}>input</span> activeOperationIdent === operation.ident ? styles['active'] : ''
</div> }`}
<div className={styles['param-item']}> >
<span className={styles['param-dot']}></span> <span className={styles['function-dot']}></span>
<span className={styles['param-label']}>strpos ARR</span> {operation.ident}
</div>
<div className={styles['param-item']}>
<span className={styles['param-dot']}></span>
<span className={styles['param-label']}>ufNum INT</span>
</div>
<div className={styles['param-item']}>
<span className={styles['param-dot']}></span>
<span className={styles['param-label']}>ufNum INT</span>
</div> </div>
</div> ))}
<div className={styles['param-item-right']}>
<span className={styles['param-label']}>output</span> {/* 分割线 */}
<span className={styles['param-dot']}></span> <Divider />
</div>
{/* 显示激活接口的参数 */}
{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> </div>
<div className={styles['node-connector']}> <div className={styles['node-connector']}>
@ -163,7 +232,6 @@ const TestInstance = ({ instance, onBack }: { instance: any; onBack: () => void
<div className={styles['flow-node']}> <div className={styles['flow-node']}>
<div className={`${styles['node-box']} ${styles['node-end']}`}> <div className={`${styles['node-box']} ${styles['node-end']}`}>
<div className={styles['node-header']}> <div className={styles['node-header']}>
<span className={styles['node-icon']}>📍</span>
<span className={styles['node-title']}></span> <span className={styles['node-title']}></span>
</div> </div>
<div className={styles['node-label']}></div> <div className={styles['node-label']}></div>
@ -192,6 +260,14 @@ const TestInstance = ({ instance, onBack }: { instance: any; onBack: () => void
</div> </div>
</div> </div>
</div> </div>
{modalVisible && <TestCaseModal
design={design.operates}
visible={modalVisible}
operationIdent={selectedOperationIdent}
onCancel={handleModalCancel}
onOk={handleModalOk}
/>}
</div> </div>
); );
}; };

Loading…
Cancel
Save