Browse Source
- 修复 ScenarioType 枚举类型找不到的问题 - 重命名 TestScenarioTestCase 为 ScenarioTestCase,避免重复前缀 - 简化 ScenarioTestCase 实体,保留核心字段 - 添加执行循环次数 (LoopCount) 字段 - 更新 TestScenario 导航属性命名 - 删除旧的 TestScenarioTestCase 文件 改进: - 更符合领域驱动设计的命名规范 - 简化实体结构,避免过度设计 - 支持测试用例在场景中的循环执行release/web-ui-v1.0.0
31 changed files with 4060 additions and 546 deletions
@ -0,0 +1,307 @@ |
|||
# ScenarioType 与 TestFlowType 作用范围对比分析 |
|||
|
|||
## 概述 |
|||
|
|||
本文档详细分析了测试场景类型(ScenarioType)和测试用例流程类型(TestFlowType)的作用范围、业务价值以及在实际项目中的应用差异。 |
|||
|
|||
## 1. 概念层次对比 |
|||
|
|||
### 1.1 ScenarioType(场景类型)- 业务层面 |
|||
|
|||
**定义**:场景类型关注的是**业务目标和测试目的**,定义了为什么要进行测试以及测试要达到什么目标。 |
|||
|
|||
```csharp |
|||
public enum ScenarioType |
|||
{ |
|||
Functional = 1, // 功能测试 - 验证业务功能 |
|||
Performance = 2, // 性能测试 - 验证系统性能 |
|||
Stress = 3, // 压力测试 - 验证系统极限 |
|||
Regression = 4, // 回归测试 - 验证修改不影响现有功能 |
|||
Integration = 5, // 集成测试 - 验证系统间集成 |
|||
UAT = 6, // 用户验收测试 - 用户验收 |
|||
Other = 99 // 其他类型 |
|||
} |
|||
``` |
|||
|
|||
### 1.2 TestFlowType(用例流程类型)- 技术层面 |
|||
|
|||
**定义**:用例流程类型关注的是**技术实现和具体操作**,定义了如何进行测试以及具体的测试操作流程。 |
|||
|
|||
```csharp |
|||
public enum TestFlowType |
|||
{ |
|||
Registration = 1, // 注册测试 - 网络注册流程 |
|||
Voice = 2, // 语音测试 - 语音通话流程 |
|||
Data = 3 // 数据测试 - 数据传输流程 |
|||
} |
|||
``` |
|||
|
|||
## 2. 作用范围对比 |
|||
|
|||
| 维度 | ScenarioType | TestFlowType | |
|||
|------|-------------|--------------| |
|||
| **关注点** | 业务目标和测试目的 | 技术实现和具体操作 | |
|||
| **层次** | 宏观业务层面 | 微观技术层面 | |
|||
| **范围** | 整个测试场景的目标 | 单个用例流程的操作 | |
|||
| **决策** | 为什么测试 | 怎么测试 | |
|||
| **时间维度** | 长期测试策略 | 短期执行计划 | |
|||
| **资源需求** | 整体资源规划 | 具体资源分配 | |
|||
| **成功标准** | 业务目标达成 | 技术指标完成 | |
|||
|
|||
## 3. 详细作用范围分析 |
|||
|
|||
### 3.1 ScenarioType 的作用范围 |
|||
|
|||
#### 3.1.1 测试策略制定 |
|||
- **功能测试**:验证业务流程的完整性和正确性 |
|||
- **性能测试**:验证系统在特定负载下的性能表现 |
|||
- **压力测试**:验证系统在极限条件下的稳定性 |
|||
- **回归测试**:验证代码修改对现有功能的影响 |
|||
- **集成测试**:验证系统组件间的协作 |
|||
- **UAT测试**:验证系统是否满足用户需求 |
|||
|
|||
#### 3.1.2 资源分配决策 |
|||
- **设备选择**:根据测试类型选择合适数量的设备 |
|||
- **环境配置**:配置适合的测试环境 |
|||
- **人员安排**:分配相应的测试人员 |
|||
- **时间规划**:制定合理的测试时间计划 |
|||
|
|||
#### 3.1.3 质量评估标准 |
|||
- **功能测试**:功能正确性、完整性、易用性 |
|||
- **性能测试**:响应时间、吞吐量、资源使用率 |
|||
- **压力测试**:系统稳定性、故障恢复能力 |
|||
- **回归测试**:现有功能稳定性、兼容性 |
|||
|
|||
### 3.2 TestFlowType 的作用范围 |
|||
|
|||
#### 3.2.1 用例流程分类 |
|||
- **注册流程**:设备网络注册相关的测试操作 |
|||
- **语音流程**:语音通话相关的测试操作 |
|||
- **数据流程**:数据传输相关的测试操作 |
|||
|
|||
#### 3.2.2 执行环境配置 |
|||
- **注册环境**:网络注册所需的网络环境 |
|||
- **语音环境**:语音通话所需的通信环境 |
|||
- **数据环境**:数据传输所需的网络环境 |
|||
|
|||
#### 3.2.3 表单类型映射 |
|||
- **注册流程** → 设备注册表单 |
|||
- **语音流程** → 语音通话表单 |
|||
- **数据流程** → 网络性能表单 |
|||
|
|||
## 4. 业务价值体现 |
|||
|
|||
### 4.1 ScenarioType 的业务价值 |
|||
|
|||
#### 4.1.1 测试策略指导 |
|||
```csharp |
|||
// 根据场景类型制定不同的测试策略 |
|||
public ITestStrategy GetTestStrategy(ScenarioType scenarioType) |
|||
{ |
|||
return scenarioType switch |
|||
{ |
|||
ScenarioType.Functional => new FunctionalTestStrategy(), |
|||
ScenarioType.Performance => new PerformanceTestStrategy(), |
|||
ScenarioType.Stress => new StressTestStrategy(), |
|||
ScenarioType.Regression => new RegressionTestStrategy(), |
|||
ScenarioType.Integration => new IntegrationTestStrategy(), |
|||
ScenarioType.UAT => new UATTestStrategy(), |
|||
_ => new DefaultTestStrategy() |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
#### 4.1.2 资源分配决策 |
|||
```csharp |
|||
// 根据场景类型分配不同的测试资源 |
|||
public TestResource AllocateResource(TestScenario scenario) |
|||
{ |
|||
return scenario.Type switch |
|||
{ |
|||
ScenarioType.Performance => new PerformanceTestResource(), // 高性能设备 |
|||
ScenarioType.Stress => new StressTestResource(), // 多设备并发 |
|||
ScenarioType.Functional => new FunctionalTestResource(), // 标准设备 |
|||
_ => new DefaultTestResource() |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
#### 4.1.3 质量评估标准 |
|||
```csharp |
|||
// 根据场景类型设定不同的质量评估标准 |
|||
public QualityCriteria GetQualityCriteria(TestScenario scenario) |
|||
{ |
|||
return scenario.Type switch |
|||
{ |
|||
ScenarioType.Functional => new FunctionalQualityCriteria(), // 功能正确性 |
|||
ScenarioType.Performance => new PerformanceQualityCriteria(), // 性能指标 |
|||
ScenarioType.Stress => new StressQualityCriteria(), // 稳定性指标 |
|||
_ => new DefaultQualityCriteria() |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
### 4.2 TestFlowType 的业务价值 |
|||
|
|||
#### 4.2.1 用例流程分类管理 |
|||
```csharp |
|||
// 根据流程类型进行用例管理 |
|||
public List<TestCaseFlow> GetTestCaseFlowsByType(TestFlowType type) |
|||
{ |
|||
return context.TestCaseFlows |
|||
.Where(f => f.Type == type && f.IsEnabled) |
|||
.OrderBy(f => f.Name) |
|||
.ToList(); |
|||
} |
|||
``` |
|||
|
|||
#### 4.2.2 执行环境配置 |
|||
```csharp |
|||
// 根据流程类型配置不同的执行环境 |
|||
public TestEnvironment ConfigureEnvironment(TestFlowType flowType) |
|||
{ |
|||
return flowType switch |
|||
{ |
|||
TestFlowType.Registration => new RegistrationEnvironment(), // 注册环境 |
|||
TestFlowType.Voice => new VoiceEnvironment(), // 语音环境 |
|||
TestFlowType.Data => new DataEnvironment(), // 数据环境 |
|||
_ => new DefaultEnvironment() |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
#### 4.2.3 表单类型映射 |
|||
```csharp |
|||
// 根据流程类型确定需要的表单类型 |
|||
public FormType GetRequiredFormType(TestFlowType flowType) |
|||
{ |
|||
return flowType switch |
|||
{ |
|||
TestFlowType.Registration => FormType.DeviceRegistrationForm, |
|||
TestFlowType.Voice => FormType.VoiceCallForm, |
|||
TestFlowType.Data => FormType.NetworkPerformanceForm, |
|||
_ => FormType.NoForm |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
## 5. 实际应用场景 |
|||
|
|||
### 5.1 功能测试场景示例 |
|||
|
|||
```csharp |
|||
// 功能测试场景:验证用户注册完整流程 |
|||
var functionalScenario = TestScenario.Create( |
|||
"SC_FUNC_001", |
|||
"用户注册功能测试", |
|||
ScenarioType.Functional, |
|||
"admin", |
|||
"验证用户注册的完整业务流程" |
|||
); |
|||
|
|||
// 包含注册、语音、数据三个流程 |
|||
functionalScenario.TestScenarioTestCases.Add(new TestScenarioTestCase |
|||
{ |
|||
TestCaseFlowId = "flow_registration", // Registration 类型 |
|||
ExecutionOrder = 1 |
|||
}); |
|||
|
|||
functionalScenario.TestScenarioTestCases.Add(new TestScenarioTestCase |
|||
{ |
|||
TestCaseFlowId = "flow_voice_call", // Voice 类型 |
|||
ExecutionOrder = 2 |
|||
}); |
|||
|
|||
functionalScenario.TestScenarioTestCases.Add(new TestScenarioTestCase |
|||
{ |
|||
TestCaseFlowId = "flow_data_test", // Data 类型 |
|||
ExecutionOrder = 3 |
|||
}); |
|||
``` |
|||
|
|||
### 5.2 性能测试场景示例 |
|||
|
|||
```csharp |
|||
// 性能测试场景:验证系统并发处理能力 |
|||
var performanceScenario = TestScenario.Create( |
|||
"SC_PERF_001", |
|||
"系统性能测试", |
|||
ScenarioType.Performance, |
|||
"admin", |
|||
"验证系统在高并发下的性能表现" |
|||
); |
|||
|
|||
// 主要包含数据流程的性能测试 |
|||
performanceScenario.TestScenarioTestCases.Add(new TestScenarioTestCase |
|||
{ |
|||
TestCaseFlowId = "flow_data_performance", // Data 类型 |
|||
ExecutionOrder = 1 |
|||
}); |
|||
``` |
|||
|
|||
### 5.3 回归测试场景示例 |
|||
|
|||
```csharp |
|||
// 回归测试场景:验证修改不影响现有功能 |
|||
var regressionScenario = TestScenario.Create( |
|||
"SC_REGR_001", |
|||
"回归测试", |
|||
ScenarioType.Regression, |
|||
"admin", |
|||
"验证代码修改后现有功能正常" |
|||
); |
|||
|
|||
// 包含所有类型的流程进行回归测试 |
|||
regressionScenario.TestScenarioTestCases.Add(new TestScenarioTestCase |
|||
{ |
|||
TestCaseFlowId = "flow_regression_registration", // Registration 类型 |
|||
ExecutionOrder = 1 |
|||
}); |
|||
|
|||
regressionScenario.TestScenarioTestCases.Add(new TestScenarioTestCase |
|||
{ |
|||
TestCaseFlowId = "flow_regression_voice", // Voice 类型 |
|||
ExecutionOrder = 2 |
|||
}); |
|||
|
|||
regressionScenario.TestScenarioTestCases.Add(new TestScenarioTestCase |
|||
{ |
|||
TestCaseFlowId = "flow_regression_data", // Data 类型 |
|||
ExecutionOrder = 3 |
|||
}); |
|||
``` |
|||
|
|||
## 6. 设计优势 |
|||
|
|||
### 6.1 职责分离 |
|||
- **ScenarioType**:专注于业务目标和测试策略 |
|||
- **TestFlowType**:专注于技术实现和具体操作 |
|||
|
|||
### 6.2 灵活组合 |
|||
- 同一个场景可以包含不同类型的流程 |
|||
- 不同类型的场景可以使用相同的流程 |
|||
|
|||
### 6.3 可扩展性 |
|||
- 可以轻松添加新的场景类型 |
|||
- 可以轻松添加新的流程类型 |
|||
|
|||
### 6.4 维护性 |
|||
- 清晰的分类便于维护和管理 |
|||
- 降低代码复杂度和耦合度 |
|||
|
|||
## 7. 总结 |
|||
|
|||
### 7.1 核心差异 |
|||
- **ScenarioType**:回答"为什么测试"的问题 |
|||
- **TestFlowType**:回答"怎么测试"的问题 |
|||
|
|||
### 7.2 业务价值 |
|||
- **ScenarioType**:指导测试策略、资源分配、质量评估 |
|||
- **TestFlowType**:指导用例管理、环境配置、表单映射 |
|||
|
|||
### 7.3 应用建议 |
|||
- 在创建测试场景时,首先确定场景类型 |
|||
- 在创建用例流程时,根据具体操作确定流程类型 |
|||
- 在组合场景和用例时,考虑业务目标和技术实现的匹配 |
|||
|
|||
这种分层设计很好地体现了"场景是用例集合,用例是具体操作"的核心思想,既保证了业务目标的清晰性,又保证了技术实现的灵活性。 |
|||
@ -0,0 +1,348 @@ |
|||
using Microsoft.Extensions.Logging; |
|||
using X1.Domain.Entities.TestCase; |
|||
using X1.Domain.Repositories.TestCase; |
|||
using X1.Domain.Repositories.Base; |
|||
using X1.Domain.Models; |
|||
using Newtonsoft.Json; |
|||
|
|||
namespace X1.Application.Features.TestCaseFlow.Commands.CreateTestCaseFlow; |
|||
|
|||
/// <summary>
|
|||
/// 测试用例流程构建器
|
|||
/// </summary>
|
|||
public class TestCaseFlowBuilder |
|||
{ |
|||
private readonly ITestCaseNodeRepository _testCaseNodeRepository; |
|||
private readonly ITestCaseEdgeRepository _testCaseEdgeRepository; |
|||
private readonly IImsiRegistrationRecordRepository _imsiRegistrationRecordRepository; |
|||
private readonly ILogger<TestCaseFlowBuilder> _logger; |
|||
|
|||
/// <summary>
|
|||
/// 初始化测试用例流程构建器
|
|||
/// </summary>
|
|||
public TestCaseFlowBuilder( |
|||
ITestCaseNodeRepository testCaseNodeRepository, |
|||
ITestCaseEdgeRepository testCaseEdgeRepository, |
|||
IImsiRegistrationRecordRepository imsiRegistrationRecordRepository, |
|||
ILogger<TestCaseFlowBuilder> logger) |
|||
{ |
|||
_testCaseNodeRepository = testCaseNodeRepository; |
|||
_testCaseEdgeRepository = testCaseEdgeRepository; |
|||
_imsiRegistrationRecordRepository = imsiRegistrationRecordRepository; |
|||
_logger = logger; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 构建测试用例流程(创建节点和连线)
|
|||
/// </summary>
|
|||
/// <param name="testCaseId">测试用例ID</param>
|
|||
/// <param name="nodes">节点数据</param>
|
|||
/// <param name="edges">连线数据</param>
|
|||
/// <param name="cancellationToken">取消令牌</param>
|
|||
public async Task BuildAsync(string testCaseId, List<NodeData>? nodes, List<EdgeData>? edges, CancellationToken cancellationToken) |
|||
{ |
|||
// 创建节点
|
|||
if (nodes != null && nodes.Any()) |
|||
{ |
|||
await CreateNodesAsync(testCaseId, nodes, cancellationToken); |
|||
} |
|||
|
|||
// 创建连线
|
|||
if (edges != null && edges.Any()) |
|||
{ |
|||
await CreateEdgesAsync(testCaseId, edges, cancellationToken); |
|||
} |
|||
|
|||
_logger.LogInformation("测试用例流程构建完成,ID: {TestCaseId}, 节点数量: {NodeCount}, 连线数量: {EdgeCount}", |
|||
testCaseId, nodes?.Count ?? 0, edges?.Count ?? 0); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 创建测试用例节点
|
|||
/// </summary>
|
|||
private async Task CreateNodesAsync(string testCaseId, List<NodeData> nodes, CancellationToken cancellationToken) |
|||
{ |
|||
// 预处理节点数据
|
|||
var processedNodes = PreprocessNodes(nodes); |
|||
|
|||
// 创建节点实体
|
|||
await CreateNodeEntitiesAsync(testCaseId, processedNodes, cancellationToken); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 预处理节点数据
|
|||
/// </summary>
|
|||
/// <param name="nodes">原始节点数据</param>
|
|||
/// <returns>预处理后的节点数据</returns>
|
|||
private List<ProcessedNodeData> PreprocessNodes(List<NodeData> nodes) |
|||
{ |
|||
return nodes.Select(nodeData => new ProcessedNodeData |
|||
{ |
|||
NodeData = nodeData, |
|||
FormDataJson = nodeData.FormData?.ToString(), |
|||
HasFormData = nodeData.IsFormEnabled == true && nodeData.FormData is not null |
|||
}).ToList(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 创建节点实体
|
|||
/// </summary>
|
|||
/// <param name="testCaseId">测试用例ID</param>
|
|||
/// <param name="processedNodes">预处理后的节点数据</param>
|
|||
/// <param name="cancellationToken">取消令牌</param>
|
|||
private async Task CreateNodeEntitiesAsync(string testCaseId, List<ProcessedNodeData> processedNodes, CancellationToken cancellationToken) |
|||
{ |
|||
var sequenceNumber = 1; |
|||
var nodeIdMapping = new Dictionary<string, string>(); // 前端节点ID -> 数据库节点ID的映射
|
|||
|
|||
foreach (var processedNode in processedNodes) |
|||
{ |
|||
var nodeData = processedNode.NodeData; |
|||
|
|||
var testCaseNode = TestCaseNode.Create( |
|||
testCaseId: testCaseId, |
|||
nodeId: nodeData.Id, |
|||
sequenceNumber: sequenceNumber++, |
|||
positionX: nodeData.PositionX, |
|||
positionY: nodeData.PositionY, |
|||
stepId: nodeData.StepId, |
|||
width: nodeData.Width ?? 100, |
|||
height: nodeData.Height ?? 50, |
|||
isSelected: nodeData.Selected ?? false, |
|||
positionAbsoluteX: nodeData.PositionAbsoluteX, |
|||
positionAbsoluteY: nodeData.PositionAbsoluteY, |
|||
isDragging: nodeData.Dragging ?? false); |
|||
|
|||
await _testCaseNodeRepository.AddTestCaseNodeAsync(testCaseNode, cancellationToken); |
|||
|
|||
// 保存前端节点ID到数据库节点ID的映射
|
|||
nodeIdMapping[nodeData.Id] = testCaseNode.Id; |
|||
} |
|||
|
|||
// 将映射传递给表单数据处理方法
|
|||
await ProcessFormDataBatchAsync(testCaseId, processedNodes, nodeIdMapping, cancellationToken); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 批量处理表单数据
|
|||
/// </summary>
|
|||
private async Task ProcessFormDataBatchAsync(string testCaseId, List<ProcessedNodeData> processedNodes, Dictionary<string, string> nodeIdMapping, CancellationToken cancellationToken) |
|||
{ |
|||
// 同步处理:组装所有需要保存的实体
|
|||
var entities = BuildFormDataEntities(testCaseId, processedNodes, nodeIdMapping); |
|||
|
|||
// 异步处理:批量保存到数据库
|
|||
if (entities.HasAnyEntities()) |
|||
{ |
|||
await SaveFormDataEntitiesAsync(entities, cancellationToken); |
|||
_logger.LogInformation("成功处理表单数据,测试用例ID: {TestCaseId}", testCaseId); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 组装表单数据实体(同步操作)
|
|||
/// </summary>
|
|||
private FormDataEntities BuildFormDataEntities(string testCaseId, List<ProcessedNodeData> processedNodes, Dictionary<string, string> nodeIdMapping) |
|||
{ |
|||
var entities = new FormDataEntities(); |
|||
|
|||
foreach (var processedNode in processedNodes) |
|||
{ |
|||
if (!processedNode.HasFormData || string.IsNullOrEmpty(processedNode.FormDataJson)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
var nodeData = processedNode.NodeData; |
|||
|
|||
// 获取数据库节点ID
|
|||
if (!nodeIdMapping.TryGetValue(nodeData.Id, out var databaseNodeId)) |
|||
{ |
|||
_logger.LogWarning("未找到节点ID映射,前端节点ID: {NodeId}", nodeData.Id); |
|||
continue; |
|||
} |
|||
|
|||
switch (nodeData.FormType) |
|||
{ |
|||
case (int)FormType.DeviceRegistrationForm: |
|||
_logger.LogDebug("开始反序列化设备注册表单数据,JSON: {Json}", processedNode.FormDataJson); |
|||
|
|||
// 使用 Newtonsoft.Json 处理转义的 JSON 字符串
|
|||
var deviceRegistrationData = JsonConvert.DeserializeObject<DeviceRegistrationFormData>( |
|||
System.Text.Json.Nodes.JsonObject.Parse(processedNode.FormDataJson).ToString()); |
|||
|
|||
if (deviceRegistrationData != null) |
|||
{ |
|||
_logger.LogDebug("设备注册表单数据反序列化成功,前端节点ID: {FrontendNodeId}, 数据库节点ID: {DatabaseNodeId}", |
|||
deviceRegistrationData.NodeId, databaseNodeId); |
|||
var imsiRecord = BuildImsiRegistrationRecord(testCaseId, databaseNodeId, deviceRegistrationData); |
|||
entities.ImsiRegistrationRecords.Add(imsiRecord); |
|||
} |
|||
else |
|||
{ |
|||
_logger.LogWarning("设备注册表单数据反序列化返回null,前端节点ID: {NodeId}", nodeData.Id); |
|||
} |
|||
break; |
|||
|
|||
// 可以在这里添加其他表单类型的处理
|
|||
// case (int)FormType.NetworkConnectivityForm:
|
|||
// var networkData = JsonConvert.DeserializeObject<NetworkConnectivityFormData>(
|
|||
// System.Text.Json.Nodes.JsonObject.Parse(processedNode.FormDataJson).ToString());
|
|||
// if (networkData != null)
|
|||
// {
|
|||
// var networkEntity = BuildNetworkConnectivityEntity(testCaseId, databaseNodeId, networkData);
|
|||
// entities.NetworkConnectivityEntities.Add(networkEntity);
|
|||
// }
|
|||
// break;
|
|||
|
|||
default: |
|||
_logger.LogInformation("未处理的表单类型: {FormType}, 前端节点ID: {NodeId}", nodeData.FormType, nodeData.Id); |
|||
break; |
|||
} |
|||
} |
|||
catch (JsonException jsonEx) |
|||
{ |
|||
_logger.LogError(jsonEx, "JSON反序列化失败,前端节点ID: {NodeId}, JSON: {Json}", |
|||
processedNode.NodeData.Id, processedNode.FormDataJson); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "组装表单数据实体失败,前端节点ID: {NodeId}, 表单类型: {FormType}", |
|||
processedNode.NodeData.Id, processedNode.NodeData.FormType); |
|||
} |
|||
} |
|||
|
|||
return entities; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 批量保存表单数据实体(异步操作)
|
|||
/// </summary>
|
|||
private async Task SaveFormDataEntitiesAsync(FormDataEntities entities, CancellationToken cancellationToken) |
|||
{ |
|||
|
|||
// 保存 IMSI 注册记录
|
|||
if (entities.ImsiRegistrationRecords.Any()) |
|||
{ |
|||
foreach (var imsiRecord in entities.ImsiRegistrationRecords) |
|||
{ |
|||
await _imsiRegistrationRecordRepository.AddAsync(imsiRecord, cancellationToken); |
|||
} |
|||
} |
|||
|
|||
// 可以在这里添加其他实体类型的保存
|
|||
// if (entities.NetworkConnectivityEntities.Any())
|
|||
// {
|
|||
// foreach (var networkEntity in entities.NetworkConnectivityEntities)
|
|||
// {
|
|||
// saveTasks.Add(_networkConnectivityRepository.AddAsync(networkEntity, cancellationToken));
|
|||
// }
|
|||
// }
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 组装 IMSI 注册记录
|
|||
/// </summary>
|
|||
private static ImsiRegistrationRecord BuildImsiRegistrationRecord(string testCaseId, string nodeId, DeviceRegistrationFormData deviceRegistrationData) |
|||
{ |
|||
return ImsiRegistrationRecord.Create( |
|||
testCaseId: testCaseId, |
|||
nodeId: nodeId, |
|||
isDualSim: deviceRegistrationData.IsDualSim, |
|||
sim1Plmn: deviceRegistrationData.Sim1Plmn, |
|||
sim1CellId: deviceRegistrationData.Sim1CellId, |
|||
sim1RegistrationWaitTime: deviceRegistrationData.Sim1RegistrationWaitTime, |
|||
sim2Plmn: deviceRegistrationData.Sim2Plmn, |
|||
sim2CellId: deviceRegistrationData.Sim2CellId, |
|||
sim2RegistrationWaitTime: deviceRegistrationData.Sim2RegistrationWaitTime |
|||
); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 创建测试用例连线
|
|||
/// </summary>
|
|||
private async Task CreateEdgesAsync(string testCaseId, List<EdgeData> edges, CancellationToken cancellationToken) |
|||
{ |
|||
foreach (var edgeData in edges) |
|||
{ |
|||
var testCaseEdge = TestCaseEdge.Create( |
|||
testCaseId: testCaseId, |
|||
edgeId: edgeData.Id, |
|||
sourceNodeId: edgeData.Source, |
|||
targetNodeId: edgeData.Target, |
|||
sourceHandle: edgeData.SourceHandle, |
|||
targetHandle: edgeData.TargetHandle, |
|||
edgeType: edgeData.Type, |
|||
condition: edgeData.Condition, |
|||
isAnimated: edgeData.Animated ?? false, |
|||
style: edgeData.Style); |
|||
|
|||
await _testCaseEdgeRepository.AddTestCaseEdgeAsync(testCaseEdge, cancellationToken); |
|||
} |
|||
|
|||
_logger.LogInformation("成功创建 {Count} 个测试用例连线,测试用例ID: {TestCaseId}", edges.Count, testCaseId); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 预处理后的节点数据
|
|||
/// </summary>
|
|||
public class ProcessedNodeData |
|||
{ |
|||
/// <summary>
|
|||
/// 原始节点数据
|
|||
/// </summary>
|
|||
public NodeData NodeData { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 序列化后的表单数据JSON字符串
|
|||
/// </summary>
|
|||
public string? FormDataJson { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 是否有表单数据需要处理
|
|||
/// </summary>
|
|||
public bool HasFormData { get; set; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 表单数据实体集合
|
|||
/// </summary>
|
|||
public class FormDataEntities |
|||
{ |
|||
/// <summary>
|
|||
/// IMSI 注册记录集合
|
|||
/// </summary>
|
|||
public List<ImsiRegistrationRecord> ImsiRegistrationRecords { get; set; } = new(); |
|||
|
|||
// 可以在这里添加其他实体类型的集合
|
|||
// public List<NetworkConnectivityEntity> NetworkConnectivityEntities { get; set; } = new();
|
|||
// public List<NetworkPerformanceEntity> NetworkPerformanceEntities { get; set; } = new();
|
|||
// public List<VoiceCallEntity> VoiceCallEntities { get; set; } = new();
|
|||
|
|||
/// <summary>
|
|||
/// 检查是否有任何实体需要保存
|
|||
/// </summary>
|
|||
/// <returns>是否有实体</returns>
|
|||
public bool HasAnyEntities() |
|||
{ |
|||
return ImsiRegistrationRecords.Any(); |
|||
// || NetworkConnectivityEntities.Any()
|
|||
// || NetworkPerformanceEntities.Any()
|
|||
// || VoiceCallEntities.Any();
|
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取所有实体的总数量
|
|||
/// </summary>
|
|||
/// <returns>实体总数量</returns>
|
|||
public int GetTotalEntityCount() |
|||
{ |
|||
return ImsiRegistrationRecords.Count; |
|||
// + NetworkConnectivityEntities.Count
|
|||
// + NetworkPerformanceEntities.Count
|
|||
// + VoiceCallEntities.Count;
|
|||
} |
|||
} |
|||
@ -0,0 +1,101 @@ |
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using X1.Domain.Abstractions; |
|||
|
|||
namespace X1.Domain.Entities.TestCase |
|||
{ |
|||
/// <summary>
|
|||
/// 场景测试用例实体 - 表示场景中包含的测试用例
|
|||
/// </summary>
|
|||
public class ScenarioTestCase : AuditableEntity |
|||
{ |
|||
/// <summary>
|
|||
/// 场景ID
|
|||
/// </summary>
|
|||
[Required] |
|||
public string ScenarioId { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 测试用例流程ID
|
|||
/// </summary>
|
|||
[Required] |
|||
public string TestCaseFlowId { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 是否启用
|
|||
/// </summary>
|
|||
public bool IsEnabled { get; set; } = true; |
|||
|
|||
/// <summary>
|
|||
/// 执行顺序 - 在场景中的执行顺序
|
|||
/// </summary>
|
|||
public int ExecutionOrder { get; set; } = 0; |
|||
|
|||
/// <summary>
|
|||
/// 执行循环次数 - 测试用例在场景中的执行循环次数
|
|||
/// </summary>
|
|||
public int LoopCount { get; set; } = 1; |
|||
|
|||
// 导航属性
|
|||
public virtual TestScenario Scenario { get; set; } = null!; |
|||
public virtual TestCaseFlow TestCaseFlow { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 创建场景测试用例
|
|||
/// </summary>
|
|||
/// <param name="scenarioId">场景ID</param>
|
|||
/// <param name="testCaseFlowId">测试用例流程ID</param>
|
|||
/// <param name="createdBy">创建人ID</param>
|
|||
/// <param name="executionOrder">执行顺序</param>
|
|||
/// <param name="isEnabled">是否启用</param>
|
|||
/// <param name="loopCount">执行循环次数</param>
|
|||
public static ScenarioTestCase Create( |
|||
string scenarioId, |
|||
string testCaseFlowId, |
|||
string createdBy, |
|||
int executionOrder = 0, |
|||
bool isEnabled = true, |
|||
int loopCount = 1) |
|||
{ |
|||
return new ScenarioTestCase |
|||
{ |
|||
Id = Guid.NewGuid().ToString(), |
|||
ScenarioId = scenarioId, |
|||
TestCaseFlowId = testCaseFlowId, |
|||
ExecutionOrder = executionOrder, |
|||
IsEnabled = isEnabled, |
|||
LoopCount = loopCount, |
|||
CreatedAt = DateTime.UtcNow, |
|||
UpdatedAt = DateTime.UtcNow, |
|||
CreatedBy = createdBy, |
|||
UpdatedBy = createdBy |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 更新场景测试用例
|
|||
/// </summary>
|
|||
/// <param name="updatedBy">更新人ID</param>
|
|||
/// <param name="executionOrder">执行顺序</param>
|
|||
/// <param name="isEnabled">是否启用</param>
|
|||
/// <param name="loopCount">执行循环次数</param>
|
|||
public void Update( |
|||
string updatedBy, |
|||
int? executionOrder = null, |
|||
bool? isEnabled = null, |
|||
int? loopCount = null) |
|||
{ |
|||
if (executionOrder.HasValue) |
|||
ExecutionOrder = executionOrder.Value; |
|||
|
|||
if (isEnabled.HasValue) |
|||
IsEnabled = isEnabled.Value; |
|||
|
|||
if (loopCount.HasValue) |
|||
LoopCount = loopCount.Value; |
|||
|
|||
UpdatedAt = DateTime.UtcNow; |
|||
UpdatedBy = updatedBy; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.ComponentModel; |
|||
|
|||
namespace X1.Domain.Entities.TestCase; |
|||
|
|||
/// <summary>
|
|||
/// 测试场景类型枚举
|
|||
/// </summary>
|
|||
public enum ScenarioType |
|||
{ |
|||
/// <summary>
|
|||
/// 功能测试
|
|||
/// </summary>
|
|||
[Display(Name = "功能测试")] |
|||
[Description("验证系统功能是否按预期工作的测试场景")] |
|||
Functional = 1, |
|||
|
|||
/// <summary>
|
|||
/// 性能测试
|
|||
/// </summary>
|
|||
[Display(Name = "性能测试")] |
|||
[Description("验证系统性能指标是否满足要求的测试场景")] |
|||
Performance = 2, |
|||
|
|||
/// <summary>
|
|||
/// 压力测试
|
|||
/// </summary>
|
|||
[Display(Name = "压力测试")] |
|||
[Description("验证系统在高负载条件下的稳定性的测试场景")] |
|||
Stress = 3, |
|||
|
|||
/// <summary>
|
|||
/// 兼容性测试
|
|||
/// </summary>
|
|||
[Display(Name = "兼容性测试")] |
|||
[Description("验证系统与不同设备和环境的兼容性的测试场景")] |
|||
Compatibility = 4, |
|||
|
|||
/// <summary>
|
|||
/// 回归测试
|
|||
/// </summary>
|
|||
[Display(Name = "回归测试")] |
|||
[Description("验证系统修改后原有功能是否正常的测试场景")] |
|||
Regression = 5, |
|||
|
|||
/// <summary>
|
|||
/// 集成测试
|
|||
/// </summary>
|
|||
[Display(Name = "集成测试")] |
|||
[Description("验证系统各模块间集成是否正常的测试场景")] |
|||
Integration = 6, |
|||
|
|||
/// <summary>
|
|||
/// 安全测试
|
|||
/// </summary>
|
|||
[Display(Name = "安全测试")] |
|||
[Description("验证系统安全性和防护能力的测试场景")] |
|||
Security = 7, |
|||
|
|||
/// <summary>
|
|||
/// 用户体验测试
|
|||
/// </summary>
|
|||
[Display(Name = "用户体验测试")] |
|||
[Description("验证系统用户体验和界面友好性的测试场景")] |
|||
UserExperience = 8 |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using X1.Domain.Abstractions; |
|||
|
|||
namespace X1.Domain.Entities.TestCase |
|||
{ |
|||
/// <summary>
|
|||
/// 测试场景实体 - 定义用例集合
|
|||
/// </summary>
|
|||
public class TestScenario : AuditableEntity |
|||
{ |
|||
/// <summary>
|
|||
/// 场景编码
|
|||
/// </summary>
|
|||
[Required] |
|||
[MaxLength(50)] |
|||
public string ScenarioCode { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 场景名称
|
|||
/// </summary>
|
|||
[Required] |
|||
[MaxLength(200)] |
|||
public string ScenarioName { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 场景类型
|
|||
/// </summary>
|
|||
[Required] |
|||
public ScenarioType Type { get; set; } = ScenarioType.Functional; |
|||
|
|||
/// <summary>
|
|||
/// 场景说明
|
|||
/// </summary>
|
|||
[MaxLength(1000)] |
|||
public string? Description { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 是否启用
|
|||
/// </summary>
|
|||
public bool IsEnabled { get; set; } = true; |
|||
|
|||
// 导航属性 - 场景包含的测试用例
|
|||
public virtual ICollection<ScenarioTestCase> ScenarioTestCases { get; set; } = new List<ScenarioTestCase>(); |
|||
|
|||
/// <summary>
|
|||
/// 创建测试场景
|
|||
/// </summary>
|
|||
/// <param name="scenarioCode">场景编码</param>
|
|||
/// <param name="scenarioName">场景名称</param>
|
|||
/// <param name="type">场景类型</param>
|
|||
/// <param name="createdBy">创建人ID</param>
|
|||
/// <param name="description">场景说明</param>
|
|||
/// <param name="isEnabled">是否启用</param>
|
|||
public static TestScenario Create( |
|||
string scenarioCode, |
|||
string scenarioName, |
|||
ScenarioType type, |
|||
string createdBy, |
|||
string? description = null, |
|||
bool isEnabled = true) |
|||
{ |
|||
return new TestScenario |
|||
{ |
|||
Id = Guid.NewGuid().ToString(), |
|||
ScenarioCode = scenarioCode, |
|||
ScenarioName = scenarioName, |
|||
Type = type, |
|||
Description = description, |
|||
IsEnabled = isEnabled, |
|||
CreatedAt = DateTime.UtcNow, |
|||
UpdatedAt = DateTime.UtcNow, |
|||
CreatedBy = createdBy, |
|||
UpdatedBy = createdBy |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 更新测试场景
|
|||
/// </summary>
|
|||
/// <param name="scenarioCode">场景编码</param>
|
|||
/// <param name="scenarioName">场景名称</param>
|
|||
/// <param name="type">场景类型</param>
|
|||
/// <param name="updatedBy">更新人ID</param>
|
|||
/// <param name="description">场景说明</param>
|
|||
/// <param name="isEnabled">是否启用</param>
|
|||
public void Update( |
|||
string scenarioCode, |
|||
string scenarioName, |
|||
ScenarioType type, |
|||
string updatedBy, |
|||
string? description = null, |
|||
bool? isEnabled = null) |
|||
{ |
|||
ScenarioCode = scenarioCode; |
|||
ScenarioName = scenarioName; |
|||
Type = type; |
|||
Description = description; |
|||
|
|||
if (isEnabled.HasValue) |
|||
IsEnabled = isEnabled.Value; |
|||
|
|||
UpdatedAt = DateTime.UtcNow; |
|||
UpdatedBy = updatedBy; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
using System.Text.Json.Serialization; |
|||
|
|||
namespace X1.Domain.Models; |
|||
|
|||
/// <summary>
|
|||
/// 设备注册表单数据模型
|
|||
/// </summary>
|
|||
public class DeviceRegistrationFormData |
|||
{ |
|||
/// <summary>
|
|||
/// 节点ID
|
|||
/// </summary>
|
|||
[JsonPropertyName("nodeId")] |
|||
public string NodeId { get; set; } = string.Empty; |
|||
|
|||
/// <summary>
|
|||
/// 是否双卡
|
|||
/// </summary>
|
|||
[JsonPropertyName("isDualSim")] |
|||
public bool IsDualSim { get; set; } = false; |
|||
|
|||
/// <summary>
|
|||
/// 卡1 PLMN
|
|||
/// </summary>
|
|||
[JsonPropertyName("sim1Plmn")] |
|||
public string? Sim1Plmn { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 卡1 CellId
|
|||
/// </summary>
|
|||
[JsonPropertyName("sim1CellId")] |
|||
public string? Sim1CellId { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 卡1 注册等待时间(毫秒)
|
|||
/// </summary>
|
|||
[JsonPropertyName("sim1RegistrationWaitTime")] |
|||
public int? Sim1RegistrationWaitTime { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 卡2 PLMN
|
|||
/// </summary>
|
|||
[JsonPropertyName("sim2Plmn")] |
|||
public string? Sim2Plmn { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 卡2 CellId
|
|||
/// </summary>
|
|||
[JsonPropertyName("sim2CellId")] |
|||
public string? Sim2CellId { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 卡2 注册等待时间(毫秒)
|
|||
/// </summary>
|
|||
[JsonPropertyName("sim2RegistrationWaitTime")] |
|||
public int? Sim2RegistrationWaitTime { get; set; } |
|||
} |
|||
@ -0,0 +1,185 @@ |
|||
# ReactFlow 连接点类型配置说明 |
|||
|
|||
## 概述 |
|||
|
|||
本文档详细说明了 ReactFlowDesigner 中连接点(Handle)的类型配置规则,包括不同类型节点的连接点配置、连线规则和业务逻辑。 |
|||
|
|||
## 连接点类型 |
|||
|
|||
### 1. 输出连接点(Source Handle) |
|||
- **类型**:`type="source"` |
|||
- **颜色**:绿色(`bg-green-500`) |
|||
- **功能**:可以发起连线,作为连线的起点 |
|||
- **样式**:`hover:bg-green-600` |
|||
|
|||
### 2. 输入连接点(Target Handle) |
|||
- **类型**:`type="target"` |
|||
- **颜色**:蓝色(`bg-blue-500`) |
|||
- **功能**:只能接收连线,作为连线的终点 |
|||
- **样式**:`hover:bg-blue-600` |
|||
|
|||
## 节点类型与连接点配置 |
|||
|
|||
### 1. 开始节点(stepType = 1) |
|||
**业务逻辑**:流程的起始点,只能输出,不能接收连线 |
|||
|
|||
**连接点配置**: |
|||
- **右边连接点**:`type="source"` - 可以发起连线 |
|||
- **底部连接点**:`type="source"` - 可以发起连线 |
|||
|
|||
**连线规则**: |
|||
- ✅ 可以连线到:处理节点、判断节点 |
|||
- ❌ 不能连线到:开始节点、结束节点 |
|||
|
|||
### 2. 结束节点(stepType = 2) |
|||
**业务逻辑**:流程的终止点,只能接收连线,不能输出 |
|||
|
|||
**连接点配置**: |
|||
- **上边连接点**:`type="target"` - 只能接收连线 |
|||
- **左边连接点**:`type="target"` - 只能接收连线 |
|||
|
|||
**连线规则**: |
|||
- ✅ 可以接收来自:处理节点、判断节点的连线 |
|||
- ❌ 不能接收来自:开始节点、结束节点的连线 |
|||
|
|||
### 3. 处理节点(stepType = 3) |
|||
**业务逻辑**:流程的处理步骤,可以接收和输出连线 |
|||
|
|||
**连接点配置**: |
|||
- **上边连接点**:`type="target"` - 只能接收连线 |
|||
- **右边连接点**:`type="source"` - 可以发起连线 |
|||
- **底部连接点**:`type="source"` - 可以发起连线 |
|||
- **左边连接点**:`type="source"` - 可以发起连线 |
|||
|
|||
**连线规则**: |
|||
- ✅ 可以接收来自:开始节点、处理节点、判断节点的连线 |
|||
- ✅ 可以连线到:处理节点、判断节点、结束节点 |
|||
- ❌ 不能连线到:开始节点 |
|||
|
|||
### 4. 判断节点(stepType = 4) |
|||
**业务逻辑**:流程的分支判断,可以接收和输出连线 |
|||
|
|||
**连接点配置**: |
|||
- **上边连接点**:`type="target"` - 只能接收连线 |
|||
- **右边连接点**:`type="source"` - 可以发起连线 |
|||
- **底部连接点**:`type="source"` - 可以发起连线 |
|||
- **左边连接点**:`type="source"` - 可以发起连线 |
|||
|
|||
**连线规则**: |
|||
- ✅ 可以接收来自:开始节点、处理节点、判断节点的连线 |
|||
- ✅ 可以连线到:处理节点、判断节点、结束节点 |
|||
- ❌ 不能连线到:开始节点 |
|||
|
|||
## 连线验证规则 |
|||
|
|||
### 基本验证 |
|||
1. **不能从结束节点连出**:结束节点只能接收连线 |
|||
2. **不能连到开始节点**:开始节点只能输出连线 |
|||
3. **不能自己连自己**:节点不能连线到自己 |
|||
4. **不能重复连线**:相同的源节点和目标节点之间只能有一条连线 |
|||
|
|||
### 业务逻辑验证 |
|||
1. **流程完整性**:确保有开始节点和结束节点 |
|||
2. **连接点类型匹配**:源连接点必须是 `source` 类型,目标连接点必须是 `target` 类型 |
|||
3. **节点类型限制**:根据节点类型限制连线的方向 |
|||
|
|||
## 样式配置 |
|||
|
|||
### 连接点样式 |
|||
```typescript |
|||
// 输出连接点(绿色) |
|||
className="w-3 h-3 bg-green-500 border-2 border-white rounded-full hover:bg-green-600" |
|||
style={{ position: -6, zIndex: 10, pointerEvents: 'all' }} |
|||
|
|||
// 输入连接点(蓝色) |
|||
className="w-3 h-3 bg-blue-500 border-2 border-white rounded-full hover:bg-blue-600" |
|||
style={{ position: -6, zIndex: 10, pointerEvents: 'all' }} |
|||
``` |
|||
|
|||
### 样式说明 |
|||
- **尺寸**:`w-3 h-3` - 12px × 12px |
|||
- **边框**:`border-2 border-white` - 2px 白色边框 |
|||
- **形状**:`rounded-full` - 圆形 |
|||
- **层级**:`zIndex: 10` - 确保不被其他元素遮挡 |
|||
- **事件**:`pointerEvents: 'all'` - 确保正常接收鼠标事件 |
|||
|
|||
## 连线类型配置 |
|||
|
|||
### 支持的连线类型 |
|||
1. **default**:贝塞尔曲线(默认)- 创建自然、流畅的曲线连接 |
|||
2. **straight**:直线 - 简单的直线连接 |
|||
3. **smoothstep**:平滑曲线 - 带拐角的平滑曲线 |
|||
4. **step**:折线 - 直角拐角的折线 |
|||
|
|||
### 箭头配置 |
|||
所有连线都配置了箭头指向,清晰地显示流程方向: |
|||
|
|||
#### 箭头类型 |
|||
- **type**: `'arrowclosed'` - 闭合箭头,视觉效果更好 |
|||
- **width**: `8` - 箭头宽度(像素) |
|||
- **height**: `8` - 箭头高度(像素) |
|||
- **color**: `'#3b82f6'` - 箭头颜色,与连线颜色一致 |
|||
|
|||
#### 箭头位置 |
|||
- **markerEnd**: 箭头位于连线的终点(目标节点端) |
|||
- 箭头指向目标节点,清晰显示流程方向 |
|||
|
|||
#### 其他箭头类型(可选) |
|||
React Flow 还支持其他箭头类型: |
|||
- `'arrow'` - 开放箭头 |
|||
- `'arrowclosed'` - 闭合箭头(当前使用) |
|||
- `'default'` - 默认箭头 |
|||
- `'triangle'` - 三角形箭头 |
|||
|
|||
### 连线样式 |
|||
```typescript |
|||
style: { |
|||
stroke: '#3b82f6', // 连线颜色 |
|||
strokeWidth: 2 // 连线宽度 |
|||
}, |
|||
markerEnd: { |
|||
type: 'arrowclosed', // 箭头类型:闭合箭头 |
|||
width: 8, // 箭头宽度 |
|||
height: 8, // 箭头高度 |
|||
color: '#3b82f6', // 箭头颜色 |
|||
} |
|||
``` |
|||
|
|||
## 调试和日志 |
|||
|
|||
### 连线调试日志 |
|||
在连线验证函数中添加了详细的调试信息: |
|||
```typescript |
|||
console.log('=== 连线尝试 ==='); |
|||
console.log('连接参数:', params); |
|||
console.log('源节点ID:', params.source); |
|||
console.log('目标节点ID:', params.target); |
|||
console.log('源连接点:', params.sourceHandle); |
|||
console.log('目标连接点:', params.targetHandle); |
|||
``` |
|||
|
|||
### 常见问题排查 |
|||
1. **连接点不可见**:检查 z-index 和 pointerEvents 配置 |
|||
2. **无法连线**:检查连接点类型是否正确(source/target) |
|||
3. **连线验证失败**:查看控制台调试日志,检查连线规则 |
|||
|
|||
## 扩展和维护 |
|||
|
|||
### 添加新节点类型 |
|||
1. 定义节点类型的业务逻辑 |
|||
2. 配置相应的连接点类型和位置 |
|||
3. 更新连线验证规则 |
|||
4. 添加相应的样式配置 |
|||
|
|||
### 修改连线规则 |
|||
1. 更新 `isValidConnection` 函数中的验证逻辑 |
|||
2. 测试所有可能的连线组合 |
|||
3. 更新文档说明 |
|||
|
|||
## 总结 |
|||
|
|||
连接点类型配置是 ReactFlow 流程图设计器的核心功能,正确的配置确保了: |
|||
- **业务逻辑正确性**:符合流程图的业务规则 |
|||
- **用户体验友好**:直观的视觉反馈和交互 |
|||
- **系统稳定性**:防止无效连线和数据错误 |
|||
- **扩展性**:便于后续功能扩展和维护 |
|||
@ -0,0 +1,404 @@ |
|||
import React, { useState, useEffect } from 'react'; |
|||
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import { Label } from '@/components/ui/label'; |
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; |
|||
import { Checkbox } from '@/components/ui/checkbox'; |
|||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; |
|||
import { FileText, UserCheck, Network, Activity, PhoneIncoming } from 'lucide-react'; |
|||
|
|||
// 表单类型枚举
|
|||
export enum FormType { |
|||
NoForm = 0, |
|||
DeviceRegistrationForm = 1, |
|||
NetworkConnectivityForm = 2, |
|||
NetworkPerformanceForm = 3, |
|||
VoiceCallForm = 4 |
|||
} |
|||
|
|||
interface FormTypeDrawerProps { |
|||
open: boolean; |
|||
onOpenChange: (open: boolean) => void; |
|||
nodeId: string; |
|||
formType: number; |
|||
onSave: (data: any) => void; |
|||
loading?: boolean; |
|||
initialData?: any; |
|||
} |
|||
|
|||
export default function FormTypeDrawer({ |
|||
open, |
|||
onOpenChange, |
|||
nodeId, |
|||
formType, |
|||
onSave, |
|||
loading = false, |
|||
initialData |
|||
}: FormTypeDrawerProps) { |
|||
const [formData, setFormData] = useState<any>({}); |
|||
|
|||
useEffect(() => { |
|||
if (open) { |
|||
if (initialData) { |
|||
setFormData({ ...initialData, nodeId }); |
|||
} else { |
|||
setFormData(getDefaultFormData(formType, nodeId)); |
|||
} |
|||
} |
|||
}, [open, initialData, nodeId, formType]); |
|||
|
|||
const getDefaultFormData = (formType: number, nodeId: string) => { |
|||
switch (formType) { |
|||
case FormType.DeviceRegistrationForm: |
|||
return { |
|||
nodeId, |
|||
isDualSim: false, |
|||
sim1Plmn: '', |
|||
sim1CellId: '', |
|||
sim1RegistrationWaitTime: 5000, |
|||
sim2Plmn: '', |
|||
sim2CellId: '', |
|||
sim2RegistrationWaitTime: 5000 |
|||
}; |
|||
case FormType.NetworkConnectivityForm: |
|||
return { |
|||
nodeId, |
|||
targetHost: '8.8.8.8', |
|||
pingCount: 4, |
|||
timeout: 5000, |
|||
packetSize: 64, |
|||
interval: 1000 |
|||
}; |
|||
case FormType.NetworkPerformanceForm: |
|||
return { |
|||
nodeId, |
|||
serverHost: 'localhost', |
|||
serverPort: 5201, |
|||
testDuration: 10, |
|||
bandwidth: 1000, |
|||
protocol: 'tcp', |
|||
parallel: 1 |
|||
}; |
|||
case FormType.VoiceCallForm: |
|||
return { |
|||
nodeId, |
|||
phoneNumber: '', |
|||
callDuration: 30, |
|||
callType: 'mo', |
|||
waitTime: 5000, |
|||
hangupDelay: 2000 |
|||
}; |
|||
default: |
|||
return { nodeId }; |
|||
} |
|||
}; |
|||
|
|||
const getFormTypeIcon = (formType: number) => { |
|||
switch (formType) { |
|||
case FormType.DeviceRegistrationForm: |
|||
return <UserCheck className="h-4 w-4" />; |
|||
case FormType.NetworkConnectivityForm: |
|||
return <Network className="h-4 w-4" />; |
|||
case FormType.NetworkPerformanceForm: |
|||
return <Activity className="h-4 w-4" />; |
|||
case FormType.VoiceCallForm: |
|||
return <PhoneIncoming className="h-4 w-4" />; |
|||
default: |
|||
return <FileText className="h-4 w-4" />; |
|||
} |
|||
}; |
|||
|
|||
const getFormTypeTitle = (formType: number) => { |
|||
switch (formType) { |
|||
case FormType.DeviceRegistrationForm: |
|||
return '设备注册表单'; |
|||
case FormType.NetworkConnectivityForm: |
|||
return '网络连通性测试表单'; |
|||
case FormType.NetworkPerformanceForm: |
|||
return '网络性能测试表单'; |
|||
case FormType.VoiceCallForm: |
|||
return '语音通话测试表单'; |
|||
default: |
|||
return '无表单'; |
|||
} |
|||
}; |
|||
|
|||
const handleSubmit = (e: React.FormEvent) => { |
|||
e.preventDefault(); |
|||
if (loading) return; |
|||
onSave(formData); |
|||
}; |
|||
|
|||
const handleCancel = () => { |
|||
onOpenChange(false); |
|||
}; |
|||
|
|||
const renderDeviceRegistrationForm = () => ( |
|||
<div className="space-y-4"> |
|||
<div className="flex items-center space-x-2"> |
|||
<Checkbox |
|||
id="isDualSim" |
|||
checked={formData.isDualSim || false} |
|||
onCheckedChange={(checked) => { |
|||
setFormData(prev => ({ |
|||
...prev, |
|||
isDualSim: checked, |
|||
...(checked ? {} : { |
|||
sim2Plmn: '', |
|||
sim2CellId: '', |
|||
sim2RegistrationWaitTime: 5000 |
|||
}) |
|||
})); |
|||
}} |
|||
/> |
|||
<Label htmlFor="isDualSim">双卡模式</Label> |
|||
</div> |
|||
|
|||
<Card> |
|||
<CardHeader> |
|||
<CardTitle className="text-sm">SIM卡1配置</CardTitle> |
|||
</CardHeader> |
|||
<CardContent className="space-y-3"> |
|||
<div> |
|||
<Label htmlFor="sim1Plmn">PLMN *</Label> |
|||
<Input |
|||
id="sim1Plmn" |
|||
value={formData.sim1Plmn || ''} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, sim1Plmn: e.target.value }))} |
|||
placeholder="例如: 46001" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<Label htmlFor="sim1CellId">小区ID</Label> |
|||
<Input |
|||
id="sim1CellId" |
|||
value={formData.sim1CellId || ''} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, sim1CellId: e.target.value }))} |
|||
placeholder="例如: 12345" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<Label htmlFor="sim1RegistrationWaitTime">注册等待时间(ms)</Label> |
|||
<Input |
|||
id="sim1RegistrationWaitTime" |
|||
type="number" |
|||
value={formData.sim1RegistrationWaitTime || 5000} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, sim1RegistrationWaitTime: parseInt(e.target.value) }))} |
|||
min="1000" |
|||
max="30000" |
|||
/> |
|||
</div> |
|||
</CardContent> |
|||
</Card> |
|||
|
|||
{formData.isDualSim && ( |
|||
<Card> |
|||
<CardHeader> |
|||
<CardTitle className="text-sm">SIM卡2配置</CardTitle> |
|||
</CardHeader> |
|||
<CardContent className="space-y-3"> |
|||
<div> |
|||
<Label htmlFor="sim2Plmn">PLMN *</Label> |
|||
<Input |
|||
id="sim2Plmn" |
|||
value={formData.sim2Plmn || ''} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, sim2Plmn: e.target.value }))} |
|||
placeholder="例如: 46002" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<Label htmlFor="sim2CellId">小区ID</Label> |
|||
<Input |
|||
id="sim2CellId" |
|||
value={formData.sim2CellId || ''} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, sim2CellId: e.target.value }))} |
|||
placeholder="例如: 12346" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<Label htmlFor="sim2RegistrationWaitTime">注册等待时间(ms)</Label> |
|||
<Input |
|||
id="sim2RegistrationWaitTime" |
|||
type="number" |
|||
value={formData.sim2RegistrationWaitTime || 5000} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, sim2RegistrationWaitTime: parseInt(e.target.value) }))} |
|||
min="1000" |
|||
max="30000" |
|||
/> |
|||
</div> |
|||
</CardContent> |
|||
</Card> |
|||
)} |
|||
</div> |
|||
); |
|||
|
|||
const renderNetworkConnectivityForm = () => ( |
|||
<div className="space-y-4"> |
|||
<div> |
|||
<Label htmlFor="targetHost">目标主机地址 *</Label> |
|||
<Input |
|||
id="targetHost" |
|||
value={formData.targetHost || ''} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, targetHost: e.target.value }))} |
|||
placeholder="例如: 8.8.8.8" |
|||
/> |
|||
</div> |
|||
<div className="grid grid-cols-2 gap-4"> |
|||
<div> |
|||
<Label htmlFor="pingCount">Ping次数</Label> |
|||
<Input |
|||
id="pingCount" |
|||
type="number" |
|||
value={formData.pingCount || 4} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, pingCount: parseInt(e.target.value) }))} |
|||
min="1" |
|||
max="10" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<Label htmlFor="timeout">超时时间(ms)</Label> |
|||
<Input |
|||
id="timeout" |
|||
type="number" |
|||
value={formData.timeout || 5000} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, timeout: parseInt(e.target.value) }))} |
|||
min="1000" |
|||
max="30000" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
|
|||
const renderNetworkPerformanceForm = () => ( |
|||
<div className="space-y-4"> |
|||
<div> |
|||
<Label htmlFor="serverHost">服务器地址 *</Label> |
|||
<Input |
|||
id="serverHost" |
|||
value={formData.serverHost || ''} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, serverHost: e.target.value }))} |
|||
placeholder="例如: localhost" |
|||
/> |
|||
</div> |
|||
<div className="grid grid-cols-2 gap-4"> |
|||
<div> |
|||
<Label htmlFor="serverPort">服务器端口</Label> |
|||
<Input |
|||
id="serverPort" |
|||
type="number" |
|||
value={formData.serverPort || 5201} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, serverPort: parseInt(e.target.value) }))} |
|||
min="1" |
|||
max="65535" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<Label htmlFor="testDuration">测试时长(秒)</Label> |
|||
<Input |
|||
id="testDuration" |
|||
type="number" |
|||
value={formData.testDuration || 10} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, testDuration: parseInt(e.target.value) }))} |
|||
min="1" |
|||
max="3600" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
|
|||
const renderVoiceCallForm = () => ( |
|||
<div className="space-y-4"> |
|||
<div> |
|||
<Label htmlFor="phoneNumber">电话号码 *</Label> |
|||
<Input |
|||
id="phoneNumber" |
|||
value={formData.phoneNumber || ''} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, phoneNumber: e.target.value }))} |
|||
placeholder="例如: 13800138000" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<Label htmlFor="callType">通话类型</Label> |
|||
<Select |
|||
value={formData.callType || 'mo'} |
|||
onValueChange={(value) => setFormData(prev => ({ ...prev, callType: value }))} |
|||
> |
|||
<SelectTrigger> |
|||
<SelectValue /> |
|||
</SelectTrigger> |
|||
<SelectContent> |
|||
<SelectItem value="mo">主叫(MO)</SelectItem> |
|||
<SelectItem value="mt">被叫(MT)</SelectItem> |
|||
</SelectContent> |
|||
</Select> |
|||
</div> |
|||
<div> |
|||
<Label htmlFor="callDuration">通话时长(秒)</Label> |
|||
<Input |
|||
id="callDuration" |
|||
type="number" |
|||
value={formData.callDuration || 30} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, callDuration: parseInt(e.target.value) }))} |
|||
min="5" |
|||
max="300" |
|||
/> |
|||
</div> |
|||
</div> |
|||
); |
|||
|
|||
const renderFormContent = () => { |
|||
switch (formType) { |
|||
case FormType.DeviceRegistrationForm: |
|||
return renderDeviceRegistrationForm(); |
|||
case FormType.NetworkConnectivityForm: |
|||
return renderNetworkConnectivityForm(); |
|||
case FormType.NetworkPerformanceForm: |
|||
return renderNetworkPerformanceForm(); |
|||
case FormType.VoiceCallForm: |
|||
return renderVoiceCallForm(); |
|||
default: |
|||
return ( |
|||
<div className="text-center py-8 text-gray-500"> |
|||
<FileText className="h-12 w-12 mx-auto mb-4 opacity-50" /> |
|||
<p>此步骤类型无需配置表单</p> |
|||
</div> |
|||
); |
|||
} |
|||
}; |
|||
|
|||
return ( |
|||
<Drawer open={open} onOpenChange={onOpenChange}> |
|||
<DrawerContent> |
|||
<DrawerHeader> |
|||
<DrawerTitle className="flex items-center gap-2"> |
|||
{getFormTypeIcon(formType)} |
|||
{getFormTypeTitle(formType)} |
|||
</DrawerTitle> |
|||
</DrawerHeader> |
|||
<form onSubmit={handleSubmit} className="p-6 space-y-6"> |
|||
{renderFormContent()} |
|||
|
|||
<div className="flex justify-end space-x-2 pt-4 border-t"> |
|||
<Button |
|||
type="button" |
|||
variant="outline" |
|||
onClick={handleCancel} |
|||
disabled={loading} |
|||
> |
|||
取消 |
|||
</Button> |
|||
<Button |
|||
type="submit" |
|||
disabled={loading} |
|||
> |
|||
{loading ? '保存中...' : '保存'} |
|||
</Button> |
|||
</div> |
|||
</form> |
|||
</DrawerContent> |
|||
</Drawer> |
|||
); |
|||
} |
|||
@ -0,0 +1,177 @@ |
|||
import React, { useState, useEffect } from 'react'; |
|||
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import { Label } from '@/components/ui/label'; |
|||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; |
|||
import { Network } from 'lucide-react'; |
|||
|
|||
interface NetworkConnectivityDrawerProps { |
|||
open: boolean; |
|||
onOpenChange: (open: boolean) => void; |
|||
nodeId: string; |
|||
onSave: (data: NetworkConnectivityData) => void; |
|||
loading?: boolean; |
|||
initialData?: NetworkConnectivityData; |
|||
} |
|||
|
|||
export interface NetworkConnectivityData { |
|||
nodeId: string; |
|||
targetHost: string; |
|||
pingCount: number; |
|||
timeout: number; |
|||
packetSize: number; |
|||
interval: number; |
|||
} |
|||
|
|||
export default function NetworkConnectivityDrawer({ |
|||
open, |
|||
onOpenChange, |
|||
nodeId, |
|||
onSave, |
|||
loading = false, |
|||
initialData |
|||
}: NetworkConnectivityDrawerProps) { |
|||
const [formData, setFormData] = useState<NetworkConnectivityData>({ |
|||
nodeId, |
|||
targetHost: '8.8.8.8', |
|||
pingCount: 4, |
|||
timeout: 5000, |
|||
packetSize: 64, |
|||
interval: 1000 |
|||
}); |
|||
|
|||
useEffect(() => { |
|||
if (open) { |
|||
if (initialData) { |
|||
setFormData({ |
|||
...initialData, |
|||
nodeId |
|||
}); |
|||
} else { |
|||
setFormData({ |
|||
nodeId, |
|||
targetHost: '8.8.8.8', |
|||
pingCount: 4, |
|||
timeout: 5000, |
|||
packetSize: 64, |
|||
interval: 1000 |
|||
}); |
|||
} |
|||
} |
|||
}, [open, initialData, nodeId]); |
|||
|
|||
const handleSubmit = (e: React.FormEvent) => { |
|||
e.preventDefault(); |
|||
if (loading) return; |
|||
|
|||
if (!formData.targetHost?.trim()) { |
|||
alert('请输入目标主机地址'); |
|||
return; |
|||
} |
|||
|
|||
onSave(formData); |
|||
}; |
|||
|
|||
const handleCancel = () => { |
|||
onOpenChange(false); |
|||
}; |
|||
|
|||
return ( |
|||
<Drawer open={open} onOpenChange={onOpenChange}> |
|||
<DrawerContent> |
|||
<DrawerHeader> |
|||
<DrawerTitle className="flex items-center gap-2"> |
|||
<Network className="h-4 w-4" /> |
|||
网络连通性测试表单 |
|||
</DrawerTitle> |
|||
</DrawerHeader> |
|||
<form onSubmit={handleSubmit} className="p-6 space-y-6"> |
|||
<Card> |
|||
<CardHeader> |
|||
<CardTitle className="text-sm">Ping 测试配置</CardTitle> |
|||
</CardHeader> |
|||
<CardContent className="space-y-4"> |
|||
<div> |
|||
<Label htmlFor="targetHost">目标主机地址 *</Label> |
|||
<Input |
|||
id="targetHost" |
|||
value={formData.targetHost} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, targetHost: e.target.value }))} |
|||
placeholder="例如: 8.8.8.8" |
|||
/> |
|||
</div> |
|||
|
|||
<div className="grid grid-cols-2 gap-4"> |
|||
<div> |
|||
<Label htmlFor="pingCount">Ping次数</Label> |
|||
<Input |
|||
id="pingCount" |
|||
type="number" |
|||
value={formData.pingCount} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, pingCount: parseInt(e.target.value) }))} |
|||
min="1" |
|||
max="10" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<Label htmlFor="timeout">超时时间(ms)</Label> |
|||
<Input |
|||
id="timeout" |
|||
type="number" |
|||
value={formData.timeout} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, timeout: parseInt(e.target.value) }))} |
|||
min="1000" |
|||
max="30000" |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="grid grid-cols-2 gap-4"> |
|||
<div> |
|||
<Label htmlFor="packetSize">数据包大小(字节)</Label> |
|||
<Input |
|||
id="packetSize" |
|||
type="number" |
|||
value={formData.packetSize} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, packetSize: parseInt(e.target.value) }))} |
|||
min="32" |
|||
max="65507" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<Label htmlFor="interval">间隔时间(ms)</Label> |
|||
<Input |
|||
id="interval" |
|||
type="number" |
|||
value={formData.interval} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, interval: parseInt(e.target.value) }))} |
|||
min="100" |
|||
max="10000" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</CardContent> |
|||
</Card> |
|||
|
|||
<div className="flex justify-end space-x-2 pt-4 border-t"> |
|||
<Button |
|||
type="button" |
|||
variant="outline" |
|||
onClick={handleCancel} |
|||
disabled={loading} |
|||
> |
|||
取消 |
|||
</Button> |
|||
<Button |
|||
type="submit" |
|||
disabled={loading} |
|||
> |
|||
{loading ? '保存中...' : '保存'} |
|||
</Button> |
|||
</div> |
|||
</form> |
|||
</DrawerContent> |
|||
</Drawer> |
|||
); |
|||
} |
|||
@ -0,0 +1,197 @@ |
|||
import React, { useState, useEffect } from 'react'; |
|||
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import { Label } from '@/components/ui/label'; |
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; |
|||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; |
|||
import { Activity } from 'lucide-react'; |
|||
|
|||
interface NetworkPerformanceDrawerProps { |
|||
open: boolean; |
|||
onOpenChange: (open: boolean) => void; |
|||
nodeId: string; |
|||
onSave: (data: NetworkPerformanceData) => void; |
|||
loading?: boolean; |
|||
initialData?: NetworkPerformanceData; |
|||
} |
|||
|
|||
export interface NetworkPerformanceData { |
|||
nodeId: string; |
|||
serverHost: string; |
|||
serverPort: number; |
|||
testDuration: number; |
|||
bandwidth: number; |
|||
protocol: 'tcp' | 'udp'; |
|||
parallel: number; |
|||
} |
|||
|
|||
export default function NetworkPerformanceDrawer({ |
|||
open, |
|||
onOpenChange, |
|||
nodeId, |
|||
onSave, |
|||
loading = false, |
|||
initialData |
|||
}: NetworkPerformanceDrawerProps) { |
|||
const [formData, setFormData] = useState<NetworkPerformanceData>({ |
|||
nodeId, |
|||
serverHost: 'localhost', |
|||
serverPort: 5201, |
|||
testDuration: 10, |
|||
bandwidth: 1000, |
|||
protocol: 'tcp', |
|||
parallel: 1 |
|||
}); |
|||
|
|||
useEffect(() => { |
|||
if (open) { |
|||
if (initialData) { |
|||
setFormData({ |
|||
...initialData, |
|||
nodeId |
|||
}); |
|||
} else { |
|||
setFormData({ |
|||
nodeId, |
|||
serverHost: 'localhost', |
|||
serverPort: 5201, |
|||
testDuration: 10, |
|||
bandwidth: 1000, |
|||
protocol: 'tcp', |
|||
parallel: 1 |
|||
}); |
|||
} |
|||
} |
|||
}, [open, initialData, nodeId]); |
|||
|
|||
const handleSubmit = (e: React.FormEvent) => { |
|||
e.preventDefault(); |
|||
if (loading) return; |
|||
|
|||
if (!formData.serverHost?.trim()) { |
|||
alert('请输入服务器地址'); |
|||
return; |
|||
} |
|||
|
|||
onSave(formData); |
|||
}; |
|||
|
|||
const handleCancel = () => { |
|||
onOpenChange(false); |
|||
}; |
|||
|
|||
return ( |
|||
<Drawer open={open} onOpenChange={onOpenChange}> |
|||
<DrawerContent> |
|||
<DrawerHeader> |
|||
<DrawerTitle className="flex items-center gap-2"> |
|||
<Activity className="h-4 w-4" /> |
|||
网络性能测试表单 |
|||
</DrawerTitle> |
|||
</DrawerHeader> |
|||
<form onSubmit={handleSubmit} className="p-6 space-y-6"> |
|||
<Card> |
|||
<CardHeader> |
|||
<CardTitle className="text-sm">Iperf 测试配置</CardTitle> |
|||
</CardHeader> |
|||
<CardContent className="space-y-4"> |
|||
<div> |
|||
<Label htmlFor="serverHost">服务器地址 *</Label> |
|||
<Input |
|||
id="serverHost" |
|||
value={formData.serverHost} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, serverHost: e.target.value }))} |
|||
placeholder="例如: localhost" |
|||
/> |
|||
</div> |
|||
|
|||
<div className="grid grid-cols-2 gap-4"> |
|||
<div> |
|||
<Label htmlFor="serverPort">服务器端口</Label> |
|||
<Input |
|||
id="serverPort" |
|||
type="number" |
|||
value={formData.serverPort} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, serverPort: parseInt(e.target.value) }))} |
|||
min="1" |
|||
max="65535" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<Label htmlFor="testDuration">测试时长(秒)</Label> |
|||
<Input |
|||
id="testDuration" |
|||
type="number" |
|||
value={formData.testDuration} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, testDuration: parseInt(e.target.value) }))} |
|||
min="1" |
|||
max="3600" |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="grid grid-cols-2 gap-4"> |
|||
<div> |
|||
<Label htmlFor="bandwidth">带宽限制(Mbps)</Label> |
|||
<Input |
|||
id="bandwidth" |
|||
type="number" |
|||
value={formData.bandwidth} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, bandwidth: parseInt(e.target.value) }))} |
|||
min="1" |
|||
max="10000" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<Label htmlFor="parallel">并行连接数</Label> |
|||
<Input |
|||
id="parallel" |
|||
type="number" |
|||
value={formData.parallel} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, parallel: parseInt(e.target.value) }))} |
|||
min="1" |
|||
max="10" |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<div> |
|||
<Label htmlFor="protocol">协议类型</Label> |
|||
<Select |
|||
value={formData.protocol} |
|||
onValueChange={(value) => setFormData(prev => ({ ...prev, protocol: value as 'tcp' | 'udp' }))} |
|||
> |
|||
<SelectTrigger> |
|||
<SelectValue /> |
|||
</SelectTrigger> |
|||
<SelectContent> |
|||
<SelectItem value="tcp">TCP</SelectItem> |
|||
<SelectItem value="udp">UDP</SelectItem> |
|||
</SelectContent> |
|||
</Select> |
|||
</div> |
|||
</CardContent> |
|||
</Card> |
|||
|
|||
<div className="flex justify-end space-x-2 pt-4 border-t"> |
|||
<Button |
|||
type="button" |
|||
variant="outline" |
|||
onClick={handleCancel} |
|||
disabled={loading} |
|||
> |
|||
取消 |
|||
</Button> |
|||
<Button |
|||
type="submit" |
|||
disabled={loading} |
|||
> |
|||
{loading ? '保存中...' : '保存'} |
|||
</Button> |
|||
</div> |
|||
</form> |
|||
</DrawerContent> |
|||
</Drawer> |
|||
); |
|||
} |
|||
@ -0,0 +1,181 @@ |
|||
import React, { useState, useEffect } from 'react'; |
|||
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'; |
|||
import { Button } from '@/components/ui/button'; |
|||
import { Input } from '@/components/ui/input'; |
|||
import { Label } from '@/components/ui/label'; |
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; |
|||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; |
|||
import { PhoneIncoming } from 'lucide-react'; |
|||
|
|||
interface VoiceCallDrawerProps { |
|||
open: boolean; |
|||
onOpenChange: (open: boolean) => void; |
|||
nodeId: string; |
|||
onSave: (data: VoiceCallData) => void; |
|||
loading?: boolean; |
|||
initialData?: VoiceCallData; |
|||
} |
|||
|
|||
export interface VoiceCallData { |
|||
nodeId: string; |
|||
phoneNumber: string; |
|||
callDuration: number; |
|||
callType: 'mo' | 'mt'; |
|||
waitTime: number; |
|||
hangupDelay: number; |
|||
} |
|||
|
|||
export default function VoiceCallDrawer({ |
|||
open, |
|||
onOpenChange, |
|||
nodeId, |
|||
onSave, |
|||
loading = false, |
|||
initialData |
|||
}: VoiceCallDrawerProps) { |
|||
const [formData, setFormData] = useState<VoiceCallData>({ |
|||
nodeId, |
|||
phoneNumber: '', |
|||
callDuration: 30, |
|||
callType: 'mo', |
|||
waitTime: 5000, |
|||
hangupDelay: 2000 |
|||
}); |
|||
|
|||
useEffect(() => { |
|||
if (open) { |
|||
if (initialData) { |
|||
setFormData({ |
|||
...initialData, |
|||
nodeId |
|||
}); |
|||
} else { |
|||
setFormData({ |
|||
nodeId, |
|||
phoneNumber: '', |
|||
callDuration: 30, |
|||
callType: 'mo', |
|||
waitTime: 5000, |
|||
hangupDelay: 2000 |
|||
}); |
|||
} |
|||
} |
|||
}, [open, initialData, nodeId]); |
|||
|
|||
const handleSubmit = (e: React.FormEvent) => { |
|||
e.preventDefault(); |
|||
if (loading) return; |
|||
|
|||
if (!formData.phoneNumber?.trim()) { |
|||
alert('请输入电话号码'); |
|||
return; |
|||
} |
|||
|
|||
onSave(formData); |
|||
}; |
|||
|
|||
const handleCancel = () => { |
|||
onOpenChange(false); |
|||
}; |
|||
|
|||
return ( |
|||
<Drawer open={open} onOpenChange={onOpenChange}> |
|||
<DrawerContent> |
|||
<DrawerHeader> |
|||
<DrawerTitle className="flex items-center gap-2"> |
|||
<PhoneIncoming className="h-4 w-4" /> |
|||
语音通话测试表单 |
|||
</DrawerTitle> |
|||
</DrawerHeader> |
|||
<form onSubmit={handleSubmit} className="p-6 space-y-6"> |
|||
<Card> |
|||
<CardHeader> |
|||
<CardTitle className="text-sm">通话配置</CardTitle> |
|||
</CardHeader> |
|||
<CardContent className="space-y-4"> |
|||
<div> |
|||
<Label htmlFor="phoneNumber">电话号码 *</Label> |
|||
<Input |
|||
id="phoneNumber" |
|||
value={formData.phoneNumber} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, phoneNumber: e.target.value }))} |
|||
placeholder="例如: 13800138000" |
|||
/> |
|||
</div> |
|||
|
|||
<div> |
|||
<Label htmlFor="callType">通话类型</Label> |
|||
<Select |
|||
value={formData.callType} |
|||
onValueChange={(value) => setFormData(prev => ({ ...prev, callType: value as 'mo' | 'mt' }))} |
|||
> |
|||
<SelectTrigger> |
|||
<SelectValue /> |
|||
</SelectTrigger> |
|||
<SelectContent> |
|||
<SelectItem value="mo">主叫(MO)</SelectItem> |
|||
<SelectItem value="mt">被叫(MT)</SelectItem> |
|||
</SelectContent> |
|||
</Select> |
|||
</div> |
|||
|
|||
<div className="grid grid-cols-2 gap-4"> |
|||
<div> |
|||
<Label htmlFor="callDuration">通话时长(秒)</Label> |
|||
<Input |
|||
id="callDuration" |
|||
type="number" |
|||
value={formData.callDuration} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, callDuration: parseInt(e.target.value) }))} |
|||
min="5" |
|||
max="300" |
|||
/> |
|||
</div> |
|||
<div> |
|||
<Label htmlFor="waitTime">等待时间(ms)</Label> |
|||
<Input |
|||
id="waitTime" |
|||
type="number" |
|||
value={formData.waitTime} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, waitTime: parseInt(e.target.value) }))} |
|||
min="1000" |
|||
max="30000" |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
<div> |
|||
<Label htmlFor="hangupDelay">挂断延迟(ms)</Label> |
|||
<Input |
|||
id="hangupDelay" |
|||
type="number" |
|||
value={formData.hangupDelay} |
|||
onChange={(e) => setFormData(prev => ({ ...prev, hangupDelay: parseInt(e.target.value) }))} |
|||
min="0" |
|||
max="10000" |
|||
/> |
|||
</div> |
|||
</CardContent> |
|||
</Card> |
|||
|
|||
<div className="flex justify-end space-x-2 pt-4 border-t"> |
|||
<Button |
|||
type="button" |
|||
variant="outline" |
|||
onClick={handleCancel} |
|||
disabled={loading} |
|||
> |
|||
取消 |
|||
</Button> |
|||
<Button |
|||
type="submit" |
|||
disabled={loading} |
|||
> |
|||
{loading ? '保存中...' : '保存'} |
|||
</Button> |
|||
</div> |
|||
</form> |
|||
</DrawerContent> |
|||
</Drawer> |
|||
); |
|||
} |
|||
@ -0,0 +1,109 @@ |
|||
/* ReactFlow 设计器样式 */ |
|||
|
|||
/* 防止节点字体被缩放影响 */ |
|||
.react-flow__node { |
|||
transform-origin: center center !important; |
|||
} |
|||
|
|||
.react-flow__node .font-medium { |
|||
font-size: 12px !important; |
|||
line-height: 1.2 !important; |
|||
transform: scale(1) !important; |
|||
transform-origin: left center !important; |
|||
} |
|||
|
|||
/* 流程图工具栏样式 */ |
|||
.flow-toolbar { |
|||
display: flex; |
|||
gap: 8px; |
|||
margin-left: 16px; |
|||
} |
|||
|
|||
.toolbar-btn { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 4px; |
|||
padding: 6px 12px; |
|||
border: 1px solid #d9d9d9; |
|||
border-radius: 6px; |
|||
background: #fff; |
|||
color: #595959; |
|||
font-size: 14px; |
|||
cursor: pointer; |
|||
transition: all 0.2s; |
|||
} |
|||
|
|||
.toolbar-btn:hover { |
|||
border-color: #40a9ff; |
|||
color: #40a9ff; |
|||
} |
|||
|
|||
.toolbar-btn svg { |
|||
width: 14px; |
|||
height: 14px; |
|||
} |
|||
|
|||
.export-btn:hover { |
|||
border-color: #52c41a; |
|||
color: #52c41a; |
|||
} |
|||
|
|||
.import-btn:hover { |
|||
border-color: #1890ff; |
|||
color: #1890ff; |
|||
} |
|||
|
|||
/* 导入成功通知样式 */ |
|||
.import-success-notification { |
|||
position: fixed; |
|||
top: 20px; |
|||
right: 20px; |
|||
z-index: 1000; |
|||
background: #f6ffed; |
|||
border: 1px solid #b7eb8f; |
|||
border-radius: 6px; |
|||
padding: 12px 16px; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); |
|||
animation: slideIn 0.3s ease-out; |
|||
} |
|||
|
|||
.notification-content { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.notification-content span { |
|||
color: #52c41a; |
|||
font-size: 14px; |
|||
} |
|||
|
|||
.notification-content button { |
|||
background: none; |
|||
border: none; |
|||
color: #52c41a; |
|||
cursor: pointer; |
|||
font-size: 16px; |
|||
padding: 0; |
|||
width: 20px; |
|||
height: 20px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.notification-content button:hover { |
|||
color: #389e0d; |
|||
} |
|||
|
|||
/* 动画效果 */ |
|||
@keyframes slideIn { |
|||
from { |
|||
transform: translateX(100%); |
|||
opacity: 0; |
|||
} |
|||
to { |
|||
transform: translateX(0); |
|||
opacity: 1; |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
// 表单数据类型定义 - 与表单组件中的类型保持一致
|
|||
export interface DeviceRegistrationData { |
|||
nodeId: string; |
|||
isDualSim: boolean; |
|||
sim1Plmn?: string; |
|||
sim1CellId?: string; |
|||
sim1RegistrationWaitTime?: number; |
|||
sim2Plmn?: string; |
|||
sim2CellId?: string; |
|||
sim2RegistrationWaitTime?: number; |
|||
} |
|||
|
|||
export interface NetworkConnectivityData { |
|||
nodeId: string; |
|||
targetHost: string; |
|||
pingCount: number; |
|||
timeout: number; |
|||
packetSize: number; |
|||
interval: number; |
|||
} |
|||
|
|||
export interface NetworkPerformanceData { |
|||
nodeId: string; |
|||
serverHost: string; |
|||
serverPort: number; |
|||
testDuration: number; |
|||
bandwidth: number; |
|||
protocol: 'tcp' | 'udp'; |
|||
parallel: number; |
|||
} |
|||
|
|||
export interface VoiceCallData { |
|||
nodeId: string; |
|||
phoneNumber: string; |
|||
callDuration: number; |
|||
callType: 'mo' | 'mt'; |
|||
waitTime: number; |
|||
hangupDelay: number; |
|||
} |
|||
|
|||
// 联合类型,根据 FormType 确定具体的数据类型
|
|||
export type FormData = |
|||
| { type: 0; data: null } // NoForm
|
|||
| { type: 1; data: DeviceRegistrationData } // DeviceRegistrationForm
|
|||
| { type: 2; data: NetworkConnectivityData } // NetworkConnectivityForm
|
|||
| { type: 3; data: NetworkPerformanceData } // NetworkPerformanceForm
|
|||
| { type: 4; data: VoiceCallData }; // VoiceCallForm
|
|||
|
|||
// 简化的表单数据类型,直接使用具体的数据类型,因为节点已经有formType
|
|||
export type SimpleFormData = |
|||
| DeviceRegistrationData // DeviceRegistrationForm
|
|||
| NetworkConnectivityData // NetworkConnectivityForm
|
|||
| NetworkPerformanceData // NetworkPerformanceForm
|
|||
| VoiceCallData; // VoiceCallForm
|
|||
|
|||
// FormType 枚举值
|
|||
export enum FormType { |
|||
NoForm = 0, |
|||
DeviceRegistrationForm = 1, |
|||
NetworkConnectivityForm = 2, |
|||
NetworkPerformanceForm = 3, |
|||
VoiceCallForm = 4 |
|||
} |
|||
|
|||
// 表单类型名称映射
|
|||
export const FORM_TYPE_NAMES: Record<FormType, string> = { |
|||
[FormType.NoForm]: '无表单', |
|||
[FormType.DeviceRegistrationForm]: '设备注册表单', |
|||
[FormType.NetworkConnectivityForm]: '网络连通性测试表单', |
|||
[FormType.NetworkPerformanceForm]: '网络性能测试表单', |
|||
[FormType.VoiceCallForm]: '语音通话测试表单' |
|||
}; |
|||
|
|||
// 获取表单类型名称的辅助函数
|
|||
export const getFormTypeName = (formType: number): string => { |
|||
return FORM_TYPE_NAMES[formType as FormType] || '未知表单'; |
|||
}; |
|||
@ -0,0 +1,50 @@ |
|||
import { |
|||
DeviceRegistrationData, |
|||
NetworkConnectivityData, |
|||
NetworkPerformanceData, |
|||
VoiceCallData, |
|||
SimpleFormData |
|||
} from '@/types/formTypes'; |
|||
|
|||
/** |
|||
* 表单数据工具类 |
|||
* 用于创建和管理表单数据,不需要type字段,因为节点已经有formType |
|||
*/ |
|||
|
|||
/** |
|||
* 创建设备注册表单数据 |
|||
*/ |
|||
export const createDeviceRegistrationFormData = (data: DeviceRegistrationData): DeviceRegistrationData => { |
|||
return data; |
|||
}; |
|||
|
|||
/** |
|||
* 创建网络连通性表单数据 |
|||
*/ |
|||
export const createNetworkConnectivityFormData = (data: NetworkConnectivityData): NetworkConnectivityData => { |
|||
return data; |
|||
}; |
|||
|
|||
/** |
|||
* 创建网络性能表单数据 |
|||
*/ |
|||
export const createNetworkPerformanceFormData = (data: NetworkPerformanceData): NetworkPerformanceData => { |
|||
return data; |
|||
}; |
|||
|
|||
/** |
|||
* 创建语音通话表单数据 |
|||
*/ |
|||
export const createVoiceCallFormData = (data: VoiceCallData): VoiceCallData => { |
|||
return data; |
|||
}; |
|||
|
|||
/** |
|||
* 通用表单数据创建函数 |
|||
*/ |
|||
export const createFormData = { |
|||
deviceRegistration: createDeviceRegistrationFormData, |
|||
networkConnectivity: createNetworkConnectivityFormData, |
|||
networkPerformance: createNetworkPerformanceFormData, |
|||
voiceCall: createVoiceCallFormData |
|||
}; |
|||
@ -0,0 +1,305 @@ |
|||
# 场景类型与用例流程类型分析 |
|||
|
|||
## 概述 |
|||
|
|||
本文档详细分析了测试场景类型(ScenarioType)和测试用例流程类型(TestFlowType)的区别、关系以及在实际项目中的应用。 |
|||
|
|||
## 1. 概念层次对比 |
|||
|
|||
### 1.1 场景类型(ScenarioType)- 业务层面 |
|||
|
|||
场景类型关注的是**业务目标和测试目的**,定义了为什么要进行测试以及测试要达到什么目标。 |
|||
|
|||
```csharp |
|||
public enum ScenarioType |
|||
{ |
|||
Functional = 1, // 功能测试 - 验证业务功能 |
|||
Performance = 2, // 性能测试 - 验证系统性能 |
|||
Stress = 3, // 压力测试 - 验证系统极限 |
|||
Regression = 4, // 回归测试 - 验证修改不影响现有功能 |
|||
Integration = 5, // 集成测试 - 验证系统间集成 |
|||
UAT = 6, // 用户验收测试 - 用户验收 |
|||
Other = 99 // 其他类型 |
|||
} |
|||
``` |
|||
|
|||
### 1.2 用例流程类型(TestFlowType)- 技术层面 |
|||
|
|||
用例流程类型关注的是**技术实现和具体操作**,定义了如何进行测试以及具体的测试操作流程。 |
|||
|
|||
```csharp |
|||
public enum TestFlowType |
|||
{ |
|||
Registration = 1, // 注册测试 - 网络注册流程 |
|||
Voice = 2, // 语音测试 - 语音通话流程 |
|||
Data = 3 // 数据测试 - 数据传输流程 |
|||
} |
|||
``` |
|||
|
|||
## 2. 作用范围对比 |
|||
|
|||
### 2.1 场景类型 - 宏观业务目标 |
|||
|
|||
| 场景类型 | 业务目标 | 测试重点 | 适用场景 | |
|||
|----------|----------|----------|----------| |
|||
| **Functional** | 验证业务功能完整性 | 业务流程、用户操作、功能正确性 | 新功能开发、功能验证 | |
|||
| **Performance** | 验证系统性能指标 | 响应时间、吞吐量、资源使用率 | 性能优化、容量规划 | |
|||
| **Stress** | 验证系统极限能力 | 最大并发、极限负载、故障恢复 | 系统稳定性、极限测试 | |
|||
| **Regression** | 验证修改影响范围 | 现有功能、兼容性、稳定性 | 代码修改、版本发布 | |
|||
| **Integration** | 验证系统间协作 | 接口调用、数据流转、系统集成 | 系统集成、接口测试 | |
|||
| **UAT** | 用户验收确认 | 用户体验、业务符合性、验收标准 | 用户验收、上线前验证 | |
|||
|
|||
### 2.2 用例流程类型 - 微观技术实现 |
|||
|
|||
| 流程类型 | 技术实现 | 具体操作 | 技术重点 | |
|||
|----------|----------|----------|----------| |
|||
| **Registration** | 网络注册相关 | IMSI注册、网络连接、认证授权 | 网络协议、认证机制 | |
|||
| **Voice** | 语音通话相关 | 通话建立、通话质量、通话结束 | 语音编解码、通话控制 | |
|||
| **Data** | 数据传输相关 | 数据上传、数据下载、网络性能 | 数据传输、网络性能 | |
|||
|
|||
## 3. 组合关系示例 |
|||
|
|||
### 3.1 场景1:5G网络性能测试(Performance场景) |
|||
|
|||
```csharp |
|||
// 场景定义 - 业务目标:验证5G网络性能 |
|||
var performanceScenario = TestScenario.Create( |
|||
"5G_PERF_001", |
|||
"5G网络性能测试", |
|||
ScenarioType.Performance, // 性能测试场景 |
|||
"admin", |
|||
"测试5G网络在不同负载下的性能表现" |
|||
); |
|||
|
|||
// 该场景包含多个测试用例流程 |
|||
var bindings = new List<TestScenarioTestCase> |
|||
{ |
|||
// 1. 网络注册流程(Registration类型) |
|||
TestScenarioTestCase.Create(scenario.Id, networkRegistrationFlowId, "admin", 1), |
|||
|
|||
// 2. 数据传输性能测试(Data类型) |
|||
TestScenarioTestCase.Create(scenario.Id, dataTransferFlowId, "admin", 2), |
|||
|
|||
// 3. 语音通话质量测试(Voice类型) |
|||
TestScenarioTestCase.Create(scenario.Id, voiceCallFlowId, "admin", 3) |
|||
}; |
|||
``` |
|||
|
|||
**业务逻辑**: |
|||
- 目标:验证5G网络性能 |
|||
- 步骤1:先进行网络注册 |
|||
- 步骤2:测试数据传输性能 |
|||
- 步骤3:测试语音通话质量 |
|||
|
|||
### 3.2 场景2:用户注册功能测试(Functional场景) |
|||
|
|||
```csharp |
|||
// 场景定义 - 业务目标:验证用户注册功能 |
|||
var functionalScenario = TestScenario.Create( |
|||
"USER_REG_001", |
|||
"用户注册功能测试", |
|||
ScenarioType.Functional, // 功能测试场景 |
|||
"admin", |
|||
"验证用户注册的完整业务流程" |
|||
); |
|||
|
|||
// 该场景包含多个测试用例流程 |
|||
var bindings = new List<TestScenarioTestCase> |
|||
{ |
|||
// 1. 用户注册流程(Registration类型) |
|||
TestScenarioTestCase.Create(scenario.Id, userRegistrationFlowId, "admin", 1), |
|||
|
|||
// 2. 邮箱验证流程(Registration类型) |
|||
TestScenarioTestCase.Create(scenario.Id, emailVerificationFlowId, "admin", 2), |
|||
|
|||
// 3. 手机验证流程(Registration类型) |
|||
TestScenarioTestCase.Create(scenario.Id, phoneVerificationFlowId, "admin", 3) |
|||
}; |
|||
``` |
|||
|
|||
**业务逻辑**: |
|||
- 目标:验证用户注册功能 |
|||
- 步骤1:用户填写注册信息 |
|||
- 步骤2:邮箱验证 |
|||
- 步骤3:手机号验证 |
|||
|
|||
## 4. 设计优势分析 |
|||
|
|||
### 4.1 场景类型优势 |
|||
|
|||
1. **业务导向** |
|||
- 以业务目标为中心 |
|||
- 关注用户价值和业务价值 |
|||
- 便于业务人员理解和参与 |
|||
|
|||
2. **灵活组合** |
|||
- 可以组合不同类型的测试用例流程 |
|||
- 支持复杂的业务场景 |
|||
- 适应不同的测试需求 |
|||
|
|||
3. **目标明确** |
|||
- 每个场景都有明确的测试目标 |
|||
- 便于制定测试计划和策略 |
|||
- 便于评估测试结果 |
|||
|
|||
4. **可扩展性** |
|||
- 支持新的业务场景类型 |
|||
- 适应业务发展变化 |
|||
- 便于维护和扩展 |
|||
|
|||
### 4.2 用例流程类型优势 |
|||
|
|||
1. **技术聚焦** |
|||
- 专注于具体的技术实现 |
|||
- 便于技术人员理解和维护 |
|||
- 支持技术层面的优化 |
|||
|
|||
2. **可重用性** |
|||
- 同一个流程可以在不同场景中重用 |
|||
- 减少重复开发工作 |
|||
- 提高开发效率 |
|||
|
|||
3. **标准化** |
|||
- 标准化的技术操作流程 |
|||
- 便于质量控制和保证 |
|||
- 支持自动化测试 |
|||
|
|||
4. **维护性** |
|||
- 技术层面的维护和优化 |
|||
- 便于问题定位和解决 |
|||
- 支持持续改进 |
|||
|
|||
## 5. 实际应用建议 |
|||
|
|||
### 5.1 场景设计原则 |
|||
|
|||
```csharp |
|||
// 好的场景设计示例 |
|||
var goodScenario = TestScenario.Create( |
|||
"E2E_USER_JOURNEY_001", |
|||
"用户端到端旅程测试", |
|||
ScenarioType.Functional, // 明确业务目标 |
|||
"admin", |
|||
"验证用户从注册到完成首次购买的完整流程" |
|||
); |
|||
|
|||
// 包含多种技术流程 |
|||
var bindings = new List<TestScenarioTestCase> |
|||
{ |
|||
// 注册流程 |
|||
TestScenarioTestCase.Create(scenario.Id, registrationFlowId, "admin", 1), |
|||
// 登录流程 |
|||
TestScenarioTestCase.Create(scenario.Id, loginFlowId, "admin", 2), |
|||
// 购物流程 |
|||
TestScenarioTestCase.Create(scenario.Id, shoppingFlowId, "admin", 3), |
|||
// 支付流程 |
|||
TestScenarioTestCase.Create(scenario.Id, paymentFlowId, "admin", 4) |
|||
}; |
|||
``` |
|||
|
|||
**设计要点**: |
|||
- 场景名称要体现业务目标 |
|||
- 场景描述要清晰明确 |
|||
- 包含完整的业务流程 |
|||
- 考虑用户的实际使用场景 |
|||
|
|||
### 5.2 流程设计原则 |
|||
|
|||
```csharp |
|||
// 好的流程设计示例 |
|||
var goodFlow = TestCaseFlow.Create( |
|||
"USER_REGISTRATION_FLOW", |
|||
TestFlowType.Registration, // 明确技术类型 |
|||
"admin", |
|||
viewportX, viewportY, viewportZoom, |
|||
"用户注册的标准流程,包含表单验证、网络注册、数据保存等步骤" |
|||
); |
|||
``` |
|||
|
|||
**设计要点**: |
|||
- 流程名称要体现技术特点 |
|||
- 流程描述要包含技术细节 |
|||
- 专注于单一的技术领域 |
|||
- 便于在不同场景中重用 |
|||
|
|||
### 5.3 组合使用建议 |
|||
|
|||
1. **场景优先** |
|||
- 先确定业务场景和目标 |
|||
- 再选择合适的测试用例流程 |
|||
- 确保场景的完整性和有效性 |
|||
|
|||
2. **流程标准化** |
|||
- 建立标准的技术流程库 |
|||
- 确保流程的可重用性 |
|||
- 持续优化和改进流程 |
|||
|
|||
3. **灵活组合** |
|||
- 根据业务需求灵活组合流程 |
|||
- 支持场景的定制化需求 |
|||
- 保持架构的灵活性 |
|||
|
|||
## 6. 最佳实践 |
|||
|
|||
### 6.1 场景命名规范 |
|||
|
|||
``` |
|||
格式:{业务领域}_{功能模块}_{测试类型}_{序号} |
|||
示例: |
|||
- USER_REGISTRATION_FUNCTIONAL_001 |
|||
- NETWORK_PERFORMANCE_STRESS_001 |
|||
- PAYMENT_INTEGRATION_REGRESSION_001 |
|||
``` |
|||
|
|||
### 6.2 流程命名规范 |
|||
|
|||
``` |
|||
格式:{技术领域}_{具体功能}_{FLOW} |
|||
示例: |
|||
- USER_REGISTRATION_FLOW |
|||
- NETWORK_CONNECTION_FLOW |
|||
- VOICE_CALL_QUALITY_FLOW |
|||
``` |
|||
|
|||
### 6.3 文档化要求 |
|||
|
|||
1. **场景文档** |
|||
- 业务背景和目标 |
|||
- 测试范围和重点 |
|||
- 预期结果和验收标准 |
|||
- 依赖条件和环境要求 |
|||
|
|||
2. **流程文档** |
|||
- 技术实现细节 |
|||
- 操作步骤和参数 |
|||
- 错误处理和异常情况 |
|||
- 性能要求和限制 |
|||
|
|||
## 7. 总结 |
|||
|
|||
### 7.1 核心区别 |
|||
|
|||
| 维度 | 场景类型(ScenarioType) | 用例流程类型(TestFlowType) | |
|||
|------|-------------------------|------------------------------| |
|||
| **关注点** | 业务目标和价值 | 技术实现和操作 | |
|||
| **层次** | 宏观业务层面 | 微观技术层面 | |
|||
| **范围** | 完整的业务场景 | 具体的技术流程 | |
|||
| **目标** | 验证业务功能 | 执行技术操作 | |
|||
| **复用性** | 业务场景复用 | 技术流程复用 | |
|||
|
|||
### 7.2 设计价值 |
|||
|
|||
1. **分层清晰**:业务层和技术层分离,职责明确 |
|||
2. **灵活组合**:支持复杂的业务场景和技术需求 |
|||
3. **可维护性**:便于独立维护和优化 |
|||
4. **可扩展性**:支持业务和技术的发展变化 |
|||
5. **团队协作**:业务人员和技术人员可以并行工作 |
|||
|
|||
### 7.3 应用建议 |
|||
|
|||
1. **项目初期**:重点设计场景类型,明确业务目标 |
|||
2. **开发阶段**:重点设计流程类型,实现技术细节 |
|||
3. **测试阶段**:组合场景和流程,执行完整测试 |
|||
4. **维护阶段**:持续优化场景和流程,提高质量 |
|||
|
|||
这种分层设计使得测试体系既能够满足业务需求,又能够提供技术实现的灵活性,是一个很好的架构设计模式。 |
|||
Loading…
Reference in new issue