diff --git a/src/ScenarioType_TestFlowType_对比分析.md b/src/ScenarioType_TestFlowType_对比分析.md new file mode 100644 index 0000000..407d08d --- /dev/null +++ b/src/ScenarioType_TestFlowType_对比分析.md @@ -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 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 应用建议 +- 在创建测试场景时,首先确定场景类型 +- 在创建用例流程时,根据具体操作确定流程类型 +- 在组合场景和用例时,考虑业务目标和技术实现的匹配 + +这种分层设计很好地体现了"场景是用例集合,用例是具体操作"的核心思想,既保证了业务目标的清晰性,又保证了技术实现的灵活性。 diff --git a/src/X1.Application/DependencyInjection.cs b/src/X1.Application/DependencyInjection.cs index c32b475..6ab03bb 100644 --- a/src/X1.Application/DependencyInjection.cs +++ b/src/X1.Application/DependencyInjection.cs @@ -3,6 +3,7 @@ using MediatR; using FluentValidation; using X1.Application.Behaviours; using Microsoft.Extensions.Configuration; +using X1.Application.Features.TestCaseFlow.Commands.CreateTestCaseFlow; namespace X1.Application; @@ -41,6 +42,9 @@ public static class DependencyInjection // 注册验证器 services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)); + // 注册测试用例流程构建器 + services.AddScoped(); + return services; } } \ No newline at end of file diff --git a/src/X1.Application/Features/TestCaseFlow/Commands/CreateTestCaseFlow/CreateTestCaseFlowCommand.cs b/src/X1.Application/Features/TestCaseFlow/Commands/CreateTestCaseFlow/CreateTestCaseFlowCommand.cs index cc4826d..ea44cd3 100644 --- a/src/X1.Application/Features/TestCaseFlow/Commands/CreateTestCaseFlow/CreateTestCaseFlowCommand.cs +++ b/src/X1.Application/Features/TestCaseFlow/Commands/CreateTestCaseFlow/CreateTestCaseFlowCommand.cs @@ -72,6 +72,21 @@ public class NodeData /// 是否正在拖拽 /// public bool? Dragging { get; set; } + + /// + /// 表单数据 + /// + public object? FormData { get; set; } + + /// + /// 表单类型 + /// + public int? FormType { get; set; } + + /// + /// 是否开启表单 + /// + public bool? IsFormEnabled { get; set; } } /// diff --git a/src/X1.Application/Features/TestCaseFlow/Commands/CreateTestCaseFlow/CreateTestCaseFlowCommandHandler.cs b/src/X1.Application/Features/TestCaseFlow/Commands/CreateTestCaseFlow/CreateTestCaseFlowCommandHandler.cs index 40fe995..749029f 100644 --- a/src/X1.Application/Features/TestCaseFlow/Commands/CreateTestCaseFlow/CreateTestCaseFlowCommandHandler.cs +++ b/src/X1.Application/Features/TestCaseFlow/Commands/CreateTestCaseFlow/CreateTestCaseFlowCommandHandler.cs @@ -3,8 +3,8 @@ using Microsoft.Extensions.Logging; using X1.Domain.Common; using X1.Domain.Entities.TestCase; using X1.Domain.Repositories; -using X1.Domain.Repositories.TestCase; using X1.Domain.Repositories.Base; +using X1.Domain.Repositories.TestCase; using X1.Domain.Services; @@ -16,26 +16,22 @@ namespace X1.Application.Features.TestCaseFlow.Commands.CreateTestCaseFlow; public class CreateTestCaseFlowCommandHandler : IRequestHandler> { private readonly ITestCaseFlowRepository _testCaseFlowRepository; - private readonly ITestCaseNodeRepository _testCaseNodeRepository; - private readonly ITestCaseEdgeRepository _testCaseEdgeRepository; + private readonly TestCaseFlowBuilder _testCaseFlowBuilder; private readonly ILogger _logger; private readonly IUnitOfWork _unitOfWork; private readonly ICurrentUserService _currentUserService; - /// /// 初始化命令处理器 /// public CreateTestCaseFlowCommandHandler( ITestCaseFlowRepository testCaseFlowRepository, - ITestCaseNodeRepository testCaseNodeRepository, - ITestCaseEdgeRepository testCaseEdgeRepository, + TestCaseFlowBuilder testCaseFlowBuilder, ILogger logger, IUnitOfWork unitOfWork, ICurrentUserService currentUserService) { _testCaseFlowRepository = testCaseFlowRepository; - _testCaseNodeRepository = testCaseNodeRepository; - _testCaseEdgeRepository = testCaseEdgeRepository; + _testCaseFlowBuilder = testCaseFlowBuilder; _logger = logger; _unitOfWork = unitOfWork; _currentUserService = currentUserService; @@ -87,17 +83,8 @@ public class CreateTestCaseFlowCommandHandler : IRequestHandler - /// 创建测试用例节点 - /// - private async Task CreateNodesAsync(string testCaseId, List nodes, CancellationToken cancellationToken) - { - var sequenceNumber = 1; - foreach (var nodeData in nodes) - { - 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); - } - - _logger.LogInformation("成功创建 {Count} 个测试用例节点,测试用例ID: {TestCaseId}", nodes.Count, testCaseId); - } - - /// - /// 创建测试用例连线 - /// - private async Task CreateEdgesAsync(string testCaseId, List 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); - } - /// diff --git a/src/X1.Application/Features/TestCaseFlow/Commands/CreateTestCaseFlow/TestCaseFlowBuilder.cs b/src/X1.Application/Features/TestCaseFlow/Commands/CreateTestCaseFlow/TestCaseFlowBuilder.cs new file mode 100644 index 0000000..73d1e51 --- /dev/null +++ b/src/X1.Application/Features/TestCaseFlow/Commands/CreateTestCaseFlow/TestCaseFlowBuilder.cs @@ -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; + +/// +/// 测试用例流程构建器 +/// +public class TestCaseFlowBuilder +{ + private readonly ITestCaseNodeRepository _testCaseNodeRepository; + private readonly ITestCaseEdgeRepository _testCaseEdgeRepository; + private readonly IImsiRegistrationRecordRepository _imsiRegistrationRecordRepository; + private readonly ILogger _logger; + + /// + /// 初始化测试用例流程构建器 + /// + public TestCaseFlowBuilder( + ITestCaseNodeRepository testCaseNodeRepository, + ITestCaseEdgeRepository testCaseEdgeRepository, + IImsiRegistrationRecordRepository imsiRegistrationRecordRepository, + ILogger logger) + { + _testCaseNodeRepository = testCaseNodeRepository; + _testCaseEdgeRepository = testCaseEdgeRepository; + _imsiRegistrationRecordRepository = imsiRegistrationRecordRepository; + _logger = logger; + } + + /// + /// 构建测试用例流程(创建节点和连线) + /// + /// 测试用例ID + /// 节点数据 + /// 连线数据 + /// 取消令牌 + public async Task BuildAsync(string testCaseId, List? nodes, List? 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); + } + + /// + /// 创建测试用例节点 + /// + private async Task CreateNodesAsync(string testCaseId, List nodes, CancellationToken cancellationToken) + { + // 预处理节点数据 + var processedNodes = PreprocessNodes(nodes); + + // 创建节点实体 + await CreateNodeEntitiesAsync(testCaseId, processedNodes, cancellationToken); + } + + /// + /// 预处理节点数据 + /// + /// 原始节点数据 + /// 预处理后的节点数据 + private List PreprocessNodes(List nodes) + { + return nodes.Select(nodeData => new ProcessedNodeData + { + NodeData = nodeData, + FormDataJson = nodeData.FormData?.ToString(), + HasFormData = nodeData.IsFormEnabled == true && nodeData.FormData is not null + }).ToList(); + } + + /// + /// 创建节点实体 + /// + /// 测试用例ID + /// 预处理后的节点数据 + /// 取消令牌 + private async Task CreateNodeEntitiesAsync(string testCaseId, List processedNodes, CancellationToken cancellationToken) + { + var sequenceNumber = 1; + var nodeIdMapping = new Dictionary(); // 前端节点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); + } + + /// + /// 批量处理表单数据 + /// + private async Task ProcessFormDataBatchAsync(string testCaseId, List processedNodes, Dictionary nodeIdMapping, CancellationToken cancellationToken) + { + // 同步处理:组装所有需要保存的实体 + var entities = BuildFormDataEntities(testCaseId, processedNodes, nodeIdMapping); + + // 异步处理:批量保存到数据库 + if (entities.HasAnyEntities()) + { + await SaveFormDataEntitiesAsync(entities, cancellationToken); + _logger.LogInformation("成功处理表单数据,测试用例ID: {TestCaseId}", testCaseId); + } + } + + /// + /// 组装表单数据实体(同步操作) + /// + private FormDataEntities BuildFormDataEntities(string testCaseId, List processedNodes, Dictionary 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( + 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( + // 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; + } + + /// + /// 批量保存表单数据实体(异步操作) + /// + 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)); + // } + // } + } + + /// + /// 组装 IMSI 注册记录 + /// + 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 + ); + } + + /// + /// 创建测试用例连线 + /// + private async Task CreateEdgesAsync(string testCaseId, List 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); + } +} + +/// +/// 预处理后的节点数据 +/// +public class ProcessedNodeData +{ + /// + /// 原始节点数据 + /// + public NodeData NodeData { get; set; } = null!; + + /// + /// 序列化后的表单数据JSON字符串 + /// + public string? FormDataJson { get; set; } + + /// + /// 是否有表单数据需要处理 + /// + public bool HasFormData { get; set; } +} + +/// +/// 表单数据实体集合 +/// +public class FormDataEntities +{ + /// + /// IMSI 注册记录集合 + /// + public List ImsiRegistrationRecords { get; set; } = new(); + + // 可以在这里添加其他实体类型的集合 + // public List NetworkConnectivityEntities { get; set; } = new(); + // public List NetworkPerformanceEntities { get; set; } = new(); + // public List VoiceCallEntities { get; set; } = new(); + + /// + /// 检查是否有任何实体需要保存 + /// + /// 是否有实体 + public bool HasAnyEntities() + { + return ImsiRegistrationRecords.Any(); + // || NetworkConnectivityEntities.Any() + // || NetworkPerformanceEntities.Any() + // || VoiceCallEntities.Any(); + } + + /// + /// 获取所有实体的总数量 + /// + /// 实体总数量 + public int GetTotalEntityCount() + { + return ImsiRegistrationRecords.Count; + // + NetworkConnectivityEntities.Count + // + NetworkPerformanceEntities.Count + // + VoiceCallEntities.Count; + } +} diff --git a/src/X1.Application/Features/TestCaseFlow/Queries/GetTestCaseFlowById/GetTestCaseFlowByIdQueryHandler.cs b/src/X1.Application/Features/TestCaseFlow/Queries/GetTestCaseFlowById/GetTestCaseFlowByIdQueryHandler.cs index 7a4b6a3..e883c82 100644 --- a/src/X1.Application/Features/TestCaseFlow/Queries/GetTestCaseFlowById/GetTestCaseFlowByIdQueryHandler.cs +++ b/src/X1.Application/Features/TestCaseFlow/Queries/GetTestCaseFlowById/GetTestCaseFlowByIdQueryHandler.cs @@ -3,6 +3,7 @@ using X1.Domain.Repositories.TestCase; using Microsoft.Extensions.Logging; using MediatR; using System.Text.Json; +using X1.Domain.Entities.TestCase; namespace X1.Application.Features.TestCaseFlow.Queries.GetTestCaseFlowById; @@ -15,6 +16,7 @@ public class GetTestCaseFlowByIdQueryHandler : IRequestHandler _logger; /// @@ -25,12 +27,14 @@ public class GetTestCaseFlowByIdQueryHandler : IRequestHandler logger) { _testCaseFlowRepository = testCaseFlowRepository; _testCaseNodeRepository = testCaseNodeRepository; _testCaseEdgeRepository = testCaseEdgeRepository; _caseStepConfigRepository = caseStepConfigRepository; + _imsiRegistrationRecordRepository = imsiRegistrationRecordRepository; _logger = logger; } @@ -69,7 +73,7 @@ public class GetTestCaseFlowByIdQueryHandler : IRequestHandler n.Id).ToList(); + + // 批量查询表单数据 + var formDataDict = await GetFormDataForNodesAsync(nodeIds, cancellationToken); + var result = new List(); foreach (var node in nodes) @@ -113,6 +123,10 @@ public class GetTestCaseFlowByIdQueryHandler : IRequestHandler + /// 批量获取节点的表单数据 + /// + private async Task> GetFormDataForNodesAsync(List nodeIds, CancellationToken cancellationToken) + { + var formDataDict = new Dictionary(); + + try + { + // 查询 IMSI 注册记录(设备注册表单数据) + var imsiRecords = await _imsiRegistrationRecordRepository.GetByNodeIdsAsync(nodeIds, cancellationToken); + + foreach (var imsiRecord in imsiRecords) + { + // 将 IMSI 注册记录转换为 JSON 格式 + var formData = new + { + IsDualSim = imsiRecord.IsDualSim, + Sim1Plmn = imsiRecord.Sim1Plmn, + Sim1CellId = imsiRecord.Sim1CellId, + Sim1RegistrationWaitTime = imsiRecord.Sim1RegistrationWaitTime, + Sim2Plmn = imsiRecord.Sim2Plmn, + Sim2CellId = imsiRecord.Sim2CellId, + Sim2RegistrationWaitTime = imsiRecord.Sim2RegistrationWaitTime + }; + + formDataDict[imsiRecord.NodeId] = JsonSerializer.Serialize(formData); + } + + // 可以在这里添加其他表单类型的查询 + // 例如:网络连通性测试表单、网络性能测试表单、语音通话测试表单等 + // var networkConnectivityRecords = await _networkConnectivityRepository.GetByNodeIdsAsync(nodeIds, cancellationToken); + // foreach (var record in networkConnectivityRecords) + // { + // formDataDict[record.NodeId] = JsonSerializer.Serialize(record); + // } + + _logger.LogDebug("成功获取 {Count} 个节点的表单数据", formDataDict.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取节点表单数据时发生错误"); + } + + return formDataDict; + } + /// /// 将连线映射为 ReactFlow 格式 /// - private async Task> MapEdgesToReactFlowFormatAsync(IEnumerable? edges, CancellationToken cancellationToken) + private List MapEdgesToReactFlowFormat(IEnumerable? edges, CancellationToken cancellationToken) { if (edges == null || !edges.Any()) return new List(); diff --git a/src/X1.Application/Features/TestCaseFlow/Queries/GetTestCaseFlowById/GetTestCaseFlowByIdResponse.cs b/src/X1.Application/Features/TestCaseFlow/Queries/GetTestCaseFlowById/GetTestCaseFlowByIdResponse.cs index ec43bed..718c34b 100644 --- a/src/X1.Application/Features/TestCaseFlow/Queries/GetTestCaseFlowById/GetTestCaseFlowByIdResponse.cs +++ b/src/X1.Application/Features/TestCaseFlow/Queries/GetTestCaseFlowById/GetTestCaseFlowByIdResponse.cs @@ -29,6 +29,21 @@ public class TestCaseNodeDataDto /// 图标 /// public string? Icon { get; set; } + + /// + /// 表单数据(JSON格式) + /// + public string? FormData { get; set; } + + /// + /// 表单类型 + /// + public int? FormType { get; set; } + + /// + /// 是否开启表单 + /// + public bool IsFormEnabled { get; set; } } /// diff --git a/src/X1.Domain/Entities/TestCase/ScenarioTestCase.cs b/src/X1.Domain/Entities/TestCase/ScenarioTestCase.cs new file mode 100644 index 0000000..bba46b8 --- /dev/null +++ b/src/X1.Domain/Entities/TestCase/ScenarioTestCase.cs @@ -0,0 +1,101 @@ +using System; +using System.ComponentModel.DataAnnotations; +using X1.Domain.Abstractions; + +namespace X1.Domain.Entities.TestCase +{ + /// + /// 场景测试用例实体 - 表示场景中包含的测试用例 + /// + public class ScenarioTestCase : AuditableEntity + { + /// + /// 场景ID + /// + [Required] + public string ScenarioId { get; set; } = null!; + + /// + /// 测试用例流程ID + /// + [Required] + public string TestCaseFlowId { get; set; } = null!; + + /// + /// 是否启用 + /// + public bool IsEnabled { get; set; } = true; + + /// + /// 执行顺序 - 在场景中的执行顺序 + /// + public int ExecutionOrder { get; set; } = 0; + + /// + /// 执行循环次数 - 测试用例在场景中的执行循环次数 + /// + public int LoopCount { get; set; } = 1; + + // 导航属性 + public virtual TestScenario Scenario { get; set; } = null!; + public virtual TestCaseFlow TestCaseFlow { get; set; } = null!; + + /// + /// 创建场景测试用例 + /// + /// 场景ID + /// 测试用例流程ID + /// 创建人ID + /// 执行顺序 + /// 是否启用 + /// 执行循环次数 + 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 + }; + } + + /// + /// 更新场景测试用例 + /// + /// 更新人ID + /// 执行顺序 + /// 是否启用 + /// 执行循环次数 + 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; + } + } +} diff --git a/src/X1.Domain/Entities/TestCase/ScenarioType.cs b/src/X1.Domain/Entities/TestCase/ScenarioType.cs new file mode 100644 index 0000000..e402994 --- /dev/null +++ b/src/X1.Domain/Entities/TestCase/ScenarioType.cs @@ -0,0 +1,66 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel; + +namespace X1.Domain.Entities.TestCase; + +/// +/// 测试场景类型枚举 +/// +public enum ScenarioType +{ + /// + /// 功能测试 + /// + [Display(Name = "功能测试")] + [Description("验证系统功能是否按预期工作的测试场景")] + Functional = 1, + + /// + /// 性能测试 + /// + [Display(Name = "性能测试")] + [Description("验证系统性能指标是否满足要求的测试场景")] + Performance = 2, + + /// + /// 压力测试 + /// + [Display(Name = "压力测试")] + [Description("验证系统在高负载条件下的稳定性的测试场景")] + Stress = 3, + + /// + /// 兼容性测试 + /// + [Display(Name = "兼容性测试")] + [Description("验证系统与不同设备和环境的兼容性的测试场景")] + Compatibility = 4, + + /// + /// 回归测试 + /// + [Display(Name = "回归测试")] + [Description("验证系统修改后原有功能是否正常的测试场景")] + Regression = 5, + + /// + /// 集成测试 + /// + [Display(Name = "集成测试")] + [Description("验证系统各模块间集成是否正常的测试场景")] + Integration = 6, + + /// + /// 安全测试 + /// + [Display(Name = "安全测试")] + [Description("验证系统安全性和防护能力的测试场景")] + Security = 7, + + /// + /// 用户体验测试 + /// + [Display(Name = "用户体验测试")] + [Description("验证系统用户体验和界面友好性的测试场景")] + UserExperience = 8 +} diff --git a/src/X1.Domain/Entities/TestCase/TestCaseFlow.cs b/src/X1.Domain/Entities/TestCase/TestCaseFlow.cs index eb46339..a6a51f3 100644 --- a/src/X1.Domain/Entities/TestCase/TestCaseFlow.cs +++ b/src/X1.Domain/Entities/TestCase/TestCaseFlow.cs @@ -54,6 +54,7 @@ namespace X1.Domain.Entities.TestCase // 导航属性 public virtual ICollection Nodes { get; set; } = new List(); public virtual ICollection Edges { get; set; } = new List(); + public virtual ICollection TestScenarioTestCases { get; set; } = new List(); /// /// 创建测试用例流程 diff --git a/src/X1.Domain/Entities/TestCase/TestScenario.cs b/src/X1.Domain/Entities/TestCase/TestScenario.cs new file mode 100644 index 0000000..9101fef --- /dev/null +++ b/src/X1.Domain/Entities/TestCase/TestScenario.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using X1.Domain.Abstractions; + +namespace X1.Domain.Entities.TestCase +{ + /// + /// 测试场景实体 - 定义用例集合 + /// + public class TestScenario : AuditableEntity + { + /// + /// 场景编码 + /// + [Required] + [MaxLength(50)] + public string ScenarioCode { get; set; } = null!; + + /// + /// 场景名称 + /// + [Required] + [MaxLength(200)] + public string ScenarioName { get; set; } = null!; + + /// + /// 场景类型 + /// + [Required] + public ScenarioType Type { get; set; } = ScenarioType.Functional; + + /// + /// 场景说明 + /// + [MaxLength(1000)] + public string? Description { get; set; } + + /// + /// 是否启用 + /// + public bool IsEnabled { get; set; } = true; + + // 导航属性 - 场景包含的测试用例 + public virtual ICollection ScenarioTestCases { get; set; } = new List(); + + /// + /// 创建测试场景 + /// + /// 场景编码 + /// 场景名称 + /// 场景类型 + /// 创建人ID + /// 场景说明 + /// 是否启用 + 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 + }; + } + + /// + /// 更新测试场景 + /// + /// 场景编码 + /// 场景名称 + /// 场景类型 + /// 更新人ID + /// 场景说明 + /// 是否启用 + 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; + } + } +} \ No newline at end of file diff --git a/src/X1.Domain/Models/DeviceRegistrationFormData.cs b/src/X1.Domain/Models/DeviceRegistrationFormData.cs new file mode 100644 index 0000000..741b600 --- /dev/null +++ b/src/X1.Domain/Models/DeviceRegistrationFormData.cs @@ -0,0 +1,57 @@ +using System.Text.Json.Serialization; + +namespace X1.Domain.Models; + +/// +/// 设备注册表单数据模型 +/// +public class DeviceRegistrationFormData +{ + /// + /// 节点ID + /// + [JsonPropertyName("nodeId")] + public string NodeId { get; set; } = string.Empty; + + /// + /// 是否双卡 + /// + [JsonPropertyName("isDualSim")] + public bool IsDualSim { get; set; } = false; + + /// + /// 卡1 PLMN + /// + [JsonPropertyName("sim1Plmn")] + public string? Sim1Plmn { get; set; } + + /// + /// 卡1 CellId + /// + [JsonPropertyName("sim1CellId")] + public string? Sim1CellId { get; set; } + + /// + /// 卡1 注册等待时间(毫秒) + /// + [JsonPropertyName("sim1RegistrationWaitTime")] + public int? Sim1RegistrationWaitTime { get; set; } + + /// + /// 卡2 PLMN + /// + [JsonPropertyName("sim2Plmn")] + public string? Sim2Plmn { get; set; } + + /// + /// 卡2 CellId + /// + [JsonPropertyName("sim2CellId")] + public string? Sim2CellId { get; set; } + + /// + /// 卡2 注册等待时间(毫秒) + /// + [JsonPropertyName("sim2RegistrationWaitTime")] + public int? Sim2RegistrationWaitTime { get; set; } +} diff --git a/src/X1.Domain/Repositories/TestCase/IImsiRegistrationRecordRepository.cs b/src/X1.Domain/Repositories/TestCase/IImsiRegistrationRecordRepository.cs index 08e2253..a0d9ee5 100644 --- a/src/X1.Domain/Repositories/TestCase/IImsiRegistrationRecordRepository.cs +++ b/src/X1.Domain/Repositories/TestCase/IImsiRegistrationRecordRepository.cs @@ -28,6 +28,14 @@ namespace X1.Domain.Repositories.TestCase /// IMSI注册记录 Task GetByNodeIdAsync(string nodeId, CancellationToken cancellationToken = default); + /// + /// 根据节点ID列表批量获取IMSI注册记录 + /// + /// 节点ID列表 + /// 取消令牌 + /// IMSI注册记录列表 + Task> GetByNodeIdsAsync(IEnumerable nodeIds, CancellationToken cancellationToken = default); + /// /// 根据测试用例ID和节点ID获取IMSI注册记录 /// diff --git a/src/X1.Infrastructure/Repositories/TestCase/ImsiRegistrationRecordRepository.cs b/src/X1.Infrastructure/Repositories/TestCase/ImsiRegistrationRecordRepository.cs index 20570ec..ea2854a 100644 --- a/src/X1.Infrastructure/Repositories/TestCase/ImsiRegistrationRecordRepository.cs +++ b/src/X1.Infrastructure/Repositories/TestCase/ImsiRegistrationRecordRepository.cs @@ -53,6 +53,18 @@ namespace X1.Infrastructure.Repositories.TestCase return await QueryRepository.FirstOrDefaultAsync(x => x.NodeId == nodeId, cancellationToken: cancellationToken); } + /// + /// 根据节点ID列表批量获取IMSI注册记录 + /// + /// 节点ID列表 + /// 取消令牌 + /// IMSI注册记录列表 + public async Task> GetByNodeIdsAsync(IEnumerable nodeIds, CancellationToken cancellationToken = default) + { + var records = await QueryRepository.FindAsync(x => nodeIds.Contains(x.NodeId), cancellationToken: cancellationToken); + return records.OrderBy(x => x.CreatedAt); + } + /// /// 根据测试用例ID和节点ID获取IMSI注册记录 /// diff --git a/src/X1.WebUI/docs/ReactFlow_Handle_Configuration.md b/src/X1.WebUI/docs/ReactFlow_Handle_Configuration.md new file mode 100644 index 0000000..80eac83 --- /dev/null +++ b/src/X1.WebUI/docs/ReactFlow_Handle_Configuration.md @@ -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 流程图设计器的核心功能,正确的配置确保了: +- **业务逻辑正确性**:符合流程图的业务规则 +- **用户体验友好**:直观的视觉反馈和交互 +- **系统稳定性**:防止无效连线和数据错误 +- **扩展性**:便于后续功能扩展和维护 diff --git a/src/X1.WebUI/src/components/testcases/DeviceRegistrationDrawer.tsx b/src/X1.WebUI/src/components/testcases/DeviceRegistrationDrawer.tsx index 146a4ce..ccf5240 100644 --- a/src/X1.WebUI/src/components/testcases/DeviceRegistrationDrawer.tsx +++ b/src/X1.WebUI/src/components/testcases/DeviceRegistrationDrawer.tsx @@ -5,7 +5,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Smartphone, Wifi, Settings } from 'lucide-react'; +import { UserCheck } from 'lucide-react'; interface DeviceRegistrationDrawerProps { open: boolean; @@ -46,7 +46,6 @@ export default function DeviceRegistrationDrawer({ sim2RegistrationWaitTime: 5000 }); - // 当组件打开或初始数据变化时,更新表单数据 useEffect(() => { if (open) { if (initialData) { @@ -73,7 +72,6 @@ export default function DeviceRegistrationDrawer({ e.preventDefault(); if (loading) return; - // 验证必填字段 if (!formData.sim1Plmn?.trim()) { alert('请输入卡1 PLMN'); return; @@ -91,194 +89,128 @@ export default function DeviceRegistrationDrawer({ onOpenChange(false); }; - const handleDualSimChange = (checked: boolean) => { - setFormData(prev => ({ - ...prev, - isDualSim: checked, - // 如果取消双卡,清空卡2数据 - ...(checked ? {} : { - sim2Plmn: '', - sim2CellId: '', - sim2RegistrationWaitTime: 5000 - }) - })); - }; - return ( - - + + - - 设备注册配置 + + 设备注册表单 - -
-
- {/* 基本信息 */} + +
+ { + setFormData(prev => ({ + ...prev, + isDualSim: checked as boolean, + ...(checked ? {} : { + sim2Plmn: '', + sim2CellId: '', + sim2RegistrationWaitTime: 5000 + }) + })); + }} + /> + +
+ + + + SIM卡1配置 + + +
+ + setFormData(prev => ({ ...prev, sim1Plmn: e.target.value }))} + placeholder="例如: 46001" + /> +
+
+ + setFormData(prev => ({ ...prev, sim1CellId: e.target.value }))} + placeholder="例如: 12345" + /> +
+
+ + setFormData(prev => ({ ...prev, sim1RegistrationWaitTime: parseInt(e.target.value) }))} + min="1000" + max="30000" + /> +
+
+
+ + {formData.isDualSim && ( - - - 基本信息 - + SIM卡2配置 - -
- + +
+ setFormData(prev => ({ ...prev, sim2Plmn: e.target.value }))} + placeholder="例如: 46002" /> -

节点标识,不可修改

- -
- + + setFormData(prev => ({ ...prev, sim2CellId: e.target.value }))} + placeholder="例如: 12346" /> - -
-
- - - {/* 卡1配置 */} - - - - - 卡1配置 - - - -
-
- - setFormData(prev => ({ ...prev, sim1Plmn: e.target.value }))} - placeholder="例如: 46001" - required - disabled={loading} - /> -

公共陆地移动网络代码

-
-
- - setFormData(prev => ({ ...prev, sim1CellId: e.target.value }))} - placeholder="例如: 12345" - disabled={loading} - /> -

小区标识符(可选)

-
- -
- +
+ setFormData(prev => ({ - ...prev, - sim1RegistrationWaitTime: parseInt(e.target.value) || 5000 - }))} + value={formData.sim2RegistrationWaitTime} + onChange={(e) => setFormData(prev => ({ ...prev, sim2RegistrationWaitTime: parseInt(e.target.value) }))} min="1000" max="30000" - disabled={loading} /> -

等待注册完成的最大时间,默认5000ms

- - {/* 卡2配置 - 仅在双卡模式下显示 */} - {formData.isDualSim && ( - - - - - 卡2配置 - - - -
-
- - setFormData(prev => ({ ...prev, sim2Plmn: e.target.value }))} - placeholder="例如: 46002" - required - disabled={loading} - /> -

公共陆地移动网络代码

-
-
- - setFormData(prev => ({ ...prev, sim2CellId: e.target.value }))} - placeholder="例如: 67890" - disabled={loading} - /> -

小区标识符(可选)

-
-
- -
- - setFormData(prev => ({ - ...prev, - sim2RegistrationWaitTime: parseInt(e.target.value) || 5000 - }))} - min="1000" - max="30000" - disabled={loading} - /> -

等待注册完成的最大时间,默认5000ms

-
-
-
- )} - - {/* 操作按钮 */} -
- - -
- -
+ )} + +
+ + +
+ ); diff --git a/src/X1.WebUI/src/components/testcases/FormTypeDrawer.tsx b/src/X1.WebUI/src/components/testcases/FormTypeDrawer.tsx new file mode 100644 index 0000000..765b135 --- /dev/null +++ b/src/X1.WebUI/src/components/testcases/FormTypeDrawer.tsx @@ -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({}); + + 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 ; + case FormType.NetworkConnectivityForm: + return ; + case FormType.NetworkPerformanceForm: + return ; + case FormType.VoiceCallForm: + return ; + default: + return ; + } + }; + + 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 = () => ( +
+
+ { + setFormData(prev => ({ + ...prev, + isDualSim: checked, + ...(checked ? {} : { + sim2Plmn: '', + sim2CellId: '', + sim2RegistrationWaitTime: 5000 + }) + })); + }} + /> + +
+ + + + SIM卡1配置 + + +
+ + setFormData(prev => ({ ...prev, sim1Plmn: e.target.value }))} + placeholder="例如: 46001" + /> +
+
+ + setFormData(prev => ({ ...prev, sim1CellId: e.target.value }))} + placeholder="例如: 12345" + /> +
+
+ + setFormData(prev => ({ ...prev, sim1RegistrationWaitTime: parseInt(e.target.value) }))} + min="1000" + max="30000" + /> +
+
+
+ + {formData.isDualSim && ( + + + SIM卡2配置 + + +
+ + setFormData(prev => ({ ...prev, sim2Plmn: e.target.value }))} + placeholder="例如: 46002" + /> +
+
+ + setFormData(prev => ({ ...prev, sim2CellId: e.target.value }))} + placeholder="例如: 12346" + /> +
+
+ + setFormData(prev => ({ ...prev, sim2RegistrationWaitTime: parseInt(e.target.value) }))} + min="1000" + max="30000" + /> +
+
+
+ )} +
+ ); + + const renderNetworkConnectivityForm = () => ( +
+
+ + setFormData(prev => ({ ...prev, targetHost: e.target.value }))} + placeholder="例如: 8.8.8.8" + /> +
+
+
+ + setFormData(prev => ({ ...prev, pingCount: parseInt(e.target.value) }))} + min="1" + max="10" + /> +
+
+ + setFormData(prev => ({ ...prev, timeout: parseInt(e.target.value) }))} + min="1000" + max="30000" + /> +
+
+
+ ); + + const renderNetworkPerformanceForm = () => ( +
+
+ + setFormData(prev => ({ ...prev, serverHost: e.target.value }))} + placeholder="例如: localhost" + /> +
+
+
+ + setFormData(prev => ({ ...prev, serverPort: parseInt(e.target.value) }))} + min="1" + max="65535" + /> +
+
+ + setFormData(prev => ({ ...prev, testDuration: parseInt(e.target.value) }))} + min="1" + max="3600" + /> +
+
+
+ ); + + const renderVoiceCallForm = () => ( +
+
+ + setFormData(prev => ({ ...prev, phoneNumber: e.target.value }))} + placeholder="例如: 13800138000" + /> +
+
+ + +
+
+ + setFormData(prev => ({ ...prev, callDuration: parseInt(e.target.value) }))} + min="5" + max="300" + /> +
+
+ ); + + 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 ( +
+ +

此步骤类型无需配置表单

+
+ ); + } + }; + + return ( + + + + + {getFormTypeIcon(formType)} + {getFormTypeTitle(formType)} + + +
+ {renderFormContent()} + +
+ + +
+
+
+
+ ); +} diff --git a/src/X1.WebUI/src/components/testcases/NetworkConnectivityDrawer.tsx b/src/X1.WebUI/src/components/testcases/NetworkConnectivityDrawer.tsx new file mode 100644 index 0000000..27bb92b --- /dev/null +++ b/src/X1.WebUI/src/components/testcases/NetworkConnectivityDrawer.tsx @@ -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({ + 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 ( + + + + + + 网络连通性测试表单 + + +
+ + + Ping 测试配置 + + +
+ + setFormData(prev => ({ ...prev, targetHost: e.target.value }))} + placeholder="例如: 8.8.8.8" + /> +
+ +
+
+ + setFormData(prev => ({ ...prev, pingCount: parseInt(e.target.value) }))} + min="1" + max="10" + /> +
+
+ + setFormData(prev => ({ ...prev, timeout: parseInt(e.target.value) }))} + min="1000" + max="30000" + /> +
+
+ +
+
+ + setFormData(prev => ({ ...prev, packetSize: parseInt(e.target.value) }))} + min="32" + max="65507" + /> +
+
+ + setFormData(prev => ({ ...prev, interval: parseInt(e.target.value) }))} + min="100" + max="10000" + /> +
+
+
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/X1.WebUI/src/components/testcases/NetworkPerformanceDrawer.tsx b/src/X1.WebUI/src/components/testcases/NetworkPerformanceDrawer.tsx new file mode 100644 index 0000000..339bf8d --- /dev/null +++ b/src/X1.WebUI/src/components/testcases/NetworkPerformanceDrawer.tsx @@ -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({ + 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 ( + + + + + + 网络性能测试表单 + + +
+ + + Iperf 测试配置 + + +
+ + setFormData(prev => ({ ...prev, serverHost: e.target.value }))} + placeholder="例如: localhost" + /> +
+ +
+
+ + setFormData(prev => ({ ...prev, serverPort: parseInt(e.target.value) }))} + min="1" + max="65535" + /> +
+
+ + setFormData(prev => ({ ...prev, testDuration: parseInt(e.target.value) }))} + min="1" + max="3600" + /> +
+
+ +
+
+ + setFormData(prev => ({ ...prev, bandwidth: parseInt(e.target.value) }))} + min="1" + max="10000" + /> +
+
+ + setFormData(prev => ({ ...prev, parallel: parseInt(e.target.value) }))} + min="1" + max="10" + /> +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/X1.WebUI/src/components/testcases/TestCaseDetailDrawer.tsx b/src/X1.WebUI/src/components/testcases/TestCaseDetailDrawer.tsx index c3c76be..74a00bf 100644 --- a/src/X1.WebUI/src/components/testcases/TestCaseDetailDrawer.tsx +++ b/src/X1.WebUI/src/components/testcases/TestCaseDetailDrawer.tsx @@ -1,5 +1,7 @@ import { useState, useEffect } from 'react'; import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; import ReactFlow, { Node, Edge, @@ -8,9 +10,11 @@ import ReactFlow, { Handle, Position, ReactFlowProvider, - useReactFlow + useReactFlow, + EdgeMarker } from 'reactflow'; import { testcaseService, TestCaseFlowDetail } from '@/services/testcaseService'; +import { FormType, getFormTypeName } from '@/types/formTypes'; import 'reactflow/dist/style.css'; import styles from './TestCaseDetailDrawer.module.css'; import { @@ -28,7 +32,10 @@ import { Activity, Signal, SignalHigh, - SignalLow + SignalLow, + FileText, + CheckCircle, + XCircle } from 'lucide-react'; interface TestCaseDetailDrawerProps { @@ -38,7 +45,35 @@ interface TestCaseDetailDrawerProps { } // 简化的自定义节点组件 -const TestStepNode = ({ data, selected }: { data: any; selected?: boolean }) => { +const TestStepNode = ({ data, selected, id }: { data: any; selected?: boolean; id: string }) => { + const [isFormEnabled, setIsFormEnabled] = useState(data.isFormEnabled); + + // 从父组件获取全局状态 + const { activeFormNodeId, setActiveFormNodeId } = data; + + const showFormInfo = activeFormNodeId === id; + + const handleNodeClick = () => { + if (setActiveFormNodeId) { + console.log('点击节点:', id, '当前激活节点:', activeFormNodeId, '有表单数据:', isFormEnabled && data.formData); + + // 如果当前节点已经显示表单信息,则关闭 + if (activeFormNodeId === id) { + console.log('关闭当前节点的表单信息'); + setActiveFormNodeId(null); + } else { + // 否则显示当前节点的表单信息(只有有表单数据的节点才显示) + if (isFormEnabled && data.formData) { + console.log('显示节点的表单信息'); + setActiveFormNodeId(id); + } else { + // 如果点击的是无表单节点,则关闭当前显示的表单信息 + console.log('点击无表单节点,关闭当前表单信息'); + setActiveFormNodeId(null); + } + } + } + }; const getIconComponent = (iconName: string) => { // 根据图标名称返回对应的图标组件 switch (iconName) { @@ -133,95 +168,48 @@ const TestStepNode = ({ data, selected }: { data: any; selected?: boolean }) => return (
- {/* 开始和结束步骤使用圆形 */} - {(data.stepType === 1 || data.stepType === 2) && ( -
-
-
- {getIconComponent(data.icon || 'settings')} -
-
-
- {data.stepName} -
-
-
-
- )} - - {/* 处理步骤使用矩形 */} - {data.stepType === 3 && ( -
-
-
- {getIconComponent(data.icon || 'settings')} -
-
-
- {data.stepName} -
-
-
-
- )} + {/* 节点主体 */} +
+
+
+ {getIconComponent(data.icon || 'settings')} +
+
+
+ {data.stepName} +
+
+
+
- {/* 判断步骤使用菱形 */} - {data.stepType === 4 && ( -
-
-
- {getIconComponent(data.icon || 'settings')} -
-
-
- {data.stepName} -
-
-
-
- )} + {/* 表单信息显示区域 - 只有启用表单且有数据时才显示 */} + {showFormInfo && isFormEnabled && data.formData && ( +
+
+ 表单配置信息 +
+ +
+ )} {/* 连接点 - 根据节点数据动态生成 */} {data.handles && data.handles.map((handle: any, index: number) => ( @@ -246,6 +234,49 @@ const TestStepNode = ({ data, selected }: { data: any; selected?: boolean }) => ); }; +// 表单信息显示组件 +const FormInfoDisplay = ({ formData, formType, isFormEnabled }: { + formData?: string; + formType?: number; + isFormEnabled?: boolean; +}) => { + if (!isFormEnabled || !formType || formType === FormType.NoForm) { + return ( +
+ + 无表单配置 +
+ ); + } + + let parsedFormData = null; + try { + if (formData) { + parsedFormData = JSON.parse(formData); + } + } catch (error) { + console.error('解析表单数据失败:', error); + } + + return ( +
+
+ + + {getFormTypeName(formType)} + +
+ {parsedFormData && ( +
+
+            {JSON.stringify(parsedFormData, null, 2)}
+          
+
+ )} +
+ ); +}; + const nodeTypes = { testStep: TestStepNode, startStep: TestStepNode, @@ -260,6 +291,7 @@ function TestCaseDetailDrawerInner({ testCaseId, open, onOpenChange }: TestCaseD const [error, setError] = useState(null); const [nodes, setNodes] = useState([]); const [edges, setEdges] = useState([]); + const [activeFormNodeId, setActiveFormNodeId] = useState(null); const { fitView } = useReactFlow(); useEffect(() => { @@ -270,9 +302,20 @@ function TestCaseDetailDrawerInner({ testCaseId, open, onOpenChange }: TestCaseD setError(null); setNodes([]); setEdges([]); + setActiveFormNodeId(null); } }, [open, testCaseId]); + // 当 activeFormNodeId 变化时,更新节点数据 + useEffect(() => { + if (selectedTestCase) { + console.log('更新节点数据,当前激活节点:', activeFormNodeId); + const flowData = getReactFlowData(selectedTestCase); + setNodes(flowData.nodes); + setEdges(flowData.edges); + } + }, [activeFormNodeId, selectedTestCase]); + const loadTestCaseDetail = async (id: string) => { try { setFlowLoading(true); @@ -348,11 +391,16 @@ function TestCaseDetailDrawerInner({ testCaseId, open, onOpenChange }: TestCaseD stepName: node.data?.stepName || 'Unknown', stepType: node.data?.stepType || 3, icon: node.data?.icon || 'settings', - handles: nodeHandles[node.id] || [] + handles: nodeHandles[node.id] || [], + formData: node.data?.formData, + formType: node.data?.formType, + isFormEnabled: node.data?.isFormEnabled, + activeFormNodeId, + setActiveFormNodeId } })); - const flowEdges: Edge[] = testCase.edges.map(edge => ({ + const flowEdges = testCase.edges.map(edge => ({ id: edge.id, source: edge.source, target: edge.target, @@ -360,8 +408,14 @@ function TestCaseDetailDrawerInner({ testCaseId, open, onOpenChange }: TestCaseD targetHandle: edge.targetHandle, type: 'smoothstep', style: { stroke: '#3b82f6', strokeWidth: 2 }, + markerEnd: { + type: 'arrowclosed' as const, + width: 8, + height: 8, + color: '#3b82f6', + }, data: edge.data - })); + })) as Edge[]; return { nodes: flowNodes, edges: flowEdges }; }; @@ -422,6 +476,7 @@ function TestCaseDetailDrawerInner({ testCaseId, open, onOpenChange }: TestCaseD style={{ width: '100%', height: '100%' }} snapToGrid={true} snapGrid={[15, 15]} + onPaneClick={() => setActiveFormNodeId(null)} > 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({ + 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 ( + + + + + + 语音通话测试表单 + + +
+ + + 通话配置 + + +
+ + setFormData(prev => ({ ...prev, phoneNumber: e.target.value }))} + placeholder="例如: 13800138000" + /> +
+ +
+ + +
+ +
+
+ + setFormData(prev => ({ ...prev, callDuration: parseInt(e.target.value) }))} + min="5" + max="300" + /> +
+
+ + setFormData(prev => ({ ...prev, waitTime: parseInt(e.target.value) }))} + min="1000" + max="30000" + /> +
+
+ +
+ + setFormData(prev => ({ ...prev, hangupDelay: parseInt(e.target.value) }))} + min="0" + max="10000" + /> +
+
+
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/X1.WebUI/src/config/core/env.config.ts b/src/X1.WebUI/src/config/core/env.config.ts index 57fedf0..d656a75 100644 --- a/src/X1.WebUI/src/config/core/env.config.ts +++ b/src/X1.WebUI/src/config/core/env.config.ts @@ -6,7 +6,7 @@ const DEFAULT_CONFIG = { VITE_API_BASE_URL: 'https://localhost:7268/api', VITE_API_TIMEOUT: '30000', VITE_API_VERSION: 'v1', - VITE_API_MAX_RETRIES: '1', + VITE_API_MAX_RETRIES: '0', VITE_API_RETRY_DELAY: '1000', // 应用配置 diff --git a/src/X1.WebUI/src/pages/testcases/ReactFlowDesigner.tsx b/src/X1.WebUI/src/pages/testcases/ReactFlowDesigner.tsx index 49a3e4f..4574d31 100644 --- a/src/X1.WebUI/src/pages/testcases/ReactFlowDesigner.tsx +++ b/src/X1.WebUI/src/pages/testcases/ReactFlowDesigner.tsx @@ -15,6 +15,7 @@ import ReactFlow, { Position, } from 'reactflow'; import 'reactflow/dist/style.css'; +import '@/styles/reactflow.css'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; @@ -40,6 +41,21 @@ import { SignalLow } from 'lucide-react'; import { TestStep } from '@/services/teststepsService'; +import DeviceRegistrationDrawer from '@/components/testcases/DeviceRegistrationDrawer'; +import NetworkConnectivityDrawer from '@/components/testcases/NetworkConnectivityDrawer'; +import NetworkPerformanceDrawer from '@/components/testcases/NetworkPerformanceDrawer'; +import VoiceCallDrawer from '@/components/testcases/VoiceCallDrawer'; +import { + FormData, + FormType, + SimpleFormData, + DeviceRegistrationData, + NetworkConnectivityData, + NetworkPerformanceData, + VoiceCallData, + getFormTypeName +} from '@/types/formTypes'; +import { createFormData } from '@/utils/formDataUtils'; // 右键菜单组件 interface ContextMenuProps { @@ -47,20 +63,55 @@ interface ContextMenuProps { y: number; onClose: () => void; onDelete: () => void; + onShowForm?: () => void; type: 'node' | 'edge'; + formType?: number; + isFormEnabled?: boolean; } -const ContextMenu: React.FC = ({ x, y, onClose, onDelete, type }) => { +const ContextMenu: React.FC = ({ x, y, onClose, onDelete, onShowForm, type, formType, isFormEnabled }) => { const handleDelete = () => { onDelete(); onClose(); }; + const handleShowForm = () => { + if (onShowForm) { + onShowForm(); + } + onClose(); + }; + + const getFormTypeName = (formType: number) => { + switch (formType) { + case 0: return '无表单'; + case 1: return '设备注册表单'; + case 2: return '网络连通性测试表单'; + case 3: return '网络性能测试表单'; + case 4: return '语音通话测试表单'; + default: return '表单'; + } + }; + return (
+ {type === 'node' && formType && formType > 0 && ( + + )} -
-
- )} -
-
-
- ); -} + {/* 添加导入成功提示 */} + {importedFlow && ( +
+
+ ✓ Flow数据导入成功 + +
+
+ )} + + {/* 表单组件 */} + {formDrawerOpen && currentFormType === 1 && ( + + )} + + {formDrawerOpen && currentFormType === 2 && ( + + )} + + {formDrawerOpen && currentFormType === 3 && ( + + )} + + {formDrawerOpen && currentFormType === 4 && ( + + )} +
+ + + ); + } export default function ReactFlowDesigner(props: ReactFlowDesignerProps) { return ( diff --git a/src/X1.WebUI/src/pages/testcases/TestCasesView.tsx b/src/X1.WebUI/src/pages/testcases/TestCasesView.tsx index 0bdb656..a6257a3 100644 --- a/src/X1.WebUI/src/pages/testcases/TestCasesView.tsx +++ b/src/X1.WebUI/src/pages/testcases/TestCasesView.tsx @@ -66,7 +66,10 @@ export default function TestCasesView() { selected: node.selected, positionAbsoluteX: node.positionAbsolute?.x, positionAbsoluteY: node.positionAbsolute?.y, - dragging: node.dragging + dragging: node.dragging, + formData: node.data?.isFormEnabled && node.data?.formData ? JSON.stringify(node.data.formData) : undefined, // 根据是否开启表单来决定是否序列化表单数据 + formType: node.data?.formType, // 添加表单类型字段 + isFormEnabled: node.data?.isFormEnabled // 添加是否开启表单字段 })); // 转换连线数据格式 diff --git a/src/X1.WebUI/src/pages/teststeps/TestStepsTable.tsx b/src/X1.WebUI/src/pages/teststeps/TestStepsTable.tsx index e65e4c7..600a6e8 100644 --- a/src/X1.WebUI/src/pages/teststeps/TestStepsTable.tsx +++ b/src/X1.WebUI/src/pages/teststeps/TestStepsTable.tsx @@ -187,56 +187,47 @@ export default function TestStepsTable({ return (
- {/* 固定的表头 */} -
- - - - {columns.filter(col => col.visible !== false).map((column) => ( - - {column.title || column.label} - - ))} +
+ + + {columns.filter(col => col.visible !== false).map((column) => ( + + {column.title || column.label} + + ))} + + + + {loading ? ( + + col.visible !== false).length} className="text-center py-8"> +
+
+ 加载中... +
+
- -
-
- - {/* 可滚动的表体 */} -
- - - {loading ? ( - - col.visible !== false).length} className="text-center py-8"> -
-
- 加载中... -
-
-
- ) : testSteps.length === 0 ? ( - - col.visible !== false).length} className="text-center py-8"> -
- 暂无数据 -
-
+ ) : testSteps.length === 0 ? ( + + col.visible !== false).length} className="text-center py-8"> +
+ 暂无数据 +
+
+
+ ) : ( + testSteps.map((step) => ( + + {columns.filter(col => col.visible !== false).map((column) => ( + + {renderCell(step, column.key)} + + ))} - ) : ( - testSteps.map((step) => ( - - {columns.filter(col => col.visible !== false).map((column) => ( - - {renderCell(step, column.key)} - - ))} - - )) - )} -
-
-
+ )) + )} + +
); diff --git a/src/X1.WebUI/src/services/testcaseService.ts b/src/X1.WebUI/src/services/testcaseService.ts index 9f4d195..26dac67 100644 --- a/src/X1.WebUI/src/services/testcaseService.ts +++ b/src/X1.WebUI/src/services/testcaseService.ts @@ -28,6 +28,9 @@ export interface TestCaseNodeData { stepType: number; description?: string; icon?: string; + formData?: string; // 表单数据(JSON格式) + formType?: number; // 表单类型 + isFormEnabled?: boolean; // 是否开启表单 } // 测试用例节点位置接口 @@ -149,6 +152,9 @@ export interface CreateNodeData { positionAbsoluteX?: number; positionAbsoluteY?: number; dragging?: boolean; + formData?: string; // 表单数据(JSON格式) + formType?: number; // 表单类型 + isFormEnabled?: boolean; // 是否开启表单 } // 创建测试用例流程连线数据接口 diff --git a/src/X1.WebUI/src/styles/reactflow.css b/src/X1.WebUI/src/styles/reactflow.css new file mode 100644 index 0000000..8de4115 --- /dev/null +++ b/src/X1.WebUI/src/styles/reactflow.css @@ -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; + } +} diff --git a/src/X1.WebUI/src/types/formTypes.ts b/src/X1.WebUI/src/types/formTypes.ts new file mode 100644 index 0000000..f250c26 --- /dev/null +++ b/src/X1.WebUI/src/types/formTypes.ts @@ -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.NoForm]: '无表单', + [FormType.DeviceRegistrationForm]: '设备注册表单', + [FormType.NetworkConnectivityForm]: '网络连通性测试表单', + [FormType.NetworkPerformanceForm]: '网络性能测试表单', + [FormType.VoiceCallForm]: '语音通话测试表单' +}; + +// 获取表单类型名称的辅助函数 +export const getFormTypeName = (formType: number): string => { + return FORM_TYPE_NAMES[formType as FormType] || '未知表单'; +}; diff --git a/src/X1.WebUI/src/utils/formDataUtils.ts b/src/X1.WebUI/src/utils/formDataUtils.ts new file mode 100644 index 0000000..70b60f2 --- /dev/null +++ b/src/X1.WebUI/src/utils/formDataUtils.ts @@ -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 +}; diff --git a/src/modify.md b/src/modify.md index c2ef235..1071b33 100644 --- a/src/modify.md +++ b/src/modify.md @@ -1,5 +1,666 @@ # 修改记录 +## 2024-12-19 - 修复 ScenarioType 类型找不到的问题 + +### 问题描述 +在 `X1.Domain/Entities/TestCase/TestScenario.cs` 文件中,`ScenarioType` 类型无法找到,导致编译错误: +- 未能找到类型或命名空间名"ScenarioType"(是否缺少 using 指令或程序集引用?) +- 当前上下文中不存在名称"ScenarioType" + +### 解决方案 +1. **创建 ScenarioType 枚举文件**:在 `X1.Domain/Entities/TestCase/ScenarioType.cs` 中定义了 `ScenarioType` 枚举 +2. **枚举定义包含以下类型**: + - Functional (功能测试) = 1 + - Performance (性能测试) = 2 + - Stress (压力测试) = 3 + - Compatibility (兼容性测试) = 4 + - Regression (回归测试) = 5 + - Integration (集成测试) = 6 + - Security (安全测试) = 7 + - UserExperience (用户体验测试) = 8 + +### 修改的文件 +- `X1.Domain/Entities/TestCase/ScenarioType.cs` (新增) +- `modify.md` (更新,用于记录修改) + +### 技术细节 +- 枚举遵循项目现有的命名规范,使用 `Display` 和 `Description` 特性 +- 与 `TestScenario.cs` 在同一命名空间 `X1.Domain.Entities.TestCase` 中,无需额外 using 语句 +- 默认值设置为 `ScenarioType.Functional`,符合测试场景的常见用途 + +### 验证 +- 编译错误已解决 +- `TestScenario.cs` 中的 `ScenarioType` 引用现在可以正确识别 +- 枚举值 `ScenarioType.Functional` 在默认值设置中正常工作 + +--- + +## 2024-12-19 - 添加测试场景相关实体 + +### 新增文件 + +#### 1. X1.Domain/Entities/TestCase/TestScenario.cs +- 创建测试场景实体 +- 包含字段: + - 场景编码 (ScenarioCode) + - 场景名称 (ScenarioName) + - 场景类型 (Type) - 枚举类型 + - 场景说明 (Description) + - 是否启用 (IsEnabled) + - 优先级 (Priority) +- 提供 Create() 和 Update() 方法 +- 包含场景类型枚举 (ScenarioType) + +#### 2. X1.Domain/Entities/TestCase/ScenarioTestCase.cs +- 创建场景测试用例实体(替代糟糕的 TestScenarioTestCase 命名) +- 表示场景中包含的测试用例 +- 包含字段: + - 场景ID (ScenarioId) + - 测试用例流程ID (TestCaseFlowId) + - 是否启用 (IsEnabled) + - 执行顺序 (ExecutionOrder) +- 提供 Create() 和 Update() 方法 + +### 修改文件 + +#### 1. X1.Domain/Entities/TestCase/TestCaseFlow.cs +- 添加导航属性:`TestScenarioTestCases` +- 支持与场景的一对多关系 + +### 关系设计 +- TestScenario (1) ←→ (N) ScenarioTestCase (N) ←→ (1) TestCaseFlow +- 一个场景可以包含多个测试用例 +- 一个测试用例可以属于多个场景 +- 通过中间表 ScenarioTestCase 实现多对多关系 + +--- + +## 2024-12-19 - 重构 TestScenarioTestCase 实体命名和结构 + +### 问题描述 +- `TestScenarioTestCase` 命名不规范,重复了 "Test" 前缀 +- 实体功能过于简单,缺乏测试用例执行所需的配置信息 +- 命名不符合领域驱动设计的最佳实践 + +### 解决方案 +1. **重命名为 `ScenarioTestCase`**: + - 更简洁明了的命名 + - 符合领域语言 + - 避免重复的 "Test" 前缀 + +2. **简化实体功能**: + - 保留核心字段:场景ID、测试用例流程ID、启用状态、执行顺序 + - 移除复杂的执行配置,保持实体简洁 + - 专注于场景与测试用例的关联关系 + +3. **更新导航属性**: + - `TestScenario.TestScenarioTestCases` → `TestScenario.ScenarioTestCases` + - 更符合领域语言 + +### 修改的文件 +- `X1.Domain/Entities/TestCase/ScenarioTestCase.cs` (新增,替代旧文件) +- `X1.Domain/Entities/TestCase/TestScenario.cs` (更新导航属性) +- `X1.Domain/Entities/TestCase/TestScenarioTestCase.cs` (删除) + +### 技术特性 +- **领域驱动设计**:使用更符合业务语言的命名 +- **简洁设计**:专注于核心关联关系,避免过度设计 +- **向后兼容**:保持原有的核心功能 +- **易于维护**:简化的结构便于理解和维护 + +--- + +## 2025-01-22 - 为 TestCaseDetailDrawer 添加连线箭头功能 + +### 修改内容 + +1. **问题分析**: + - `TestCaseDetailDrawer.tsx` 中的 ReactFlow 组件缺少连线箭头功能 + - 需要与 `ReactFlowDesigner.tsx` 保持一致的视觉效果 + - 连线缺少方向指示,影响用户体验 + +2. **解决方案**: + - 在 `getReactFlowData` 方法中为 edges 添加 `markerEnd` 属性 + - 使用与 `ReactFlowDesigner.tsx` 相同的箭头配置 + - 添加类型断言解决 TypeScript 类型兼容性问题 + +3. **技术实现**: + ```typescript + const flowEdges = testCase.edges.map(edge => ({ + id: edge.id, + source: edge.source, + target: edge.target, + sourceHandle: edge.sourceHandle, + targetHandle: edge.targetHandle, + type: 'smoothstep', + style: { stroke: '#3b82f6', strokeWidth: 2 }, + markerEnd: { + type: 'arrowclosed' as const, + width: 8, + height: 8, + color: '#3b82f6', + }, + data: edge.data + })) as Edge[]; + ``` + +4. **优化内容**: + - 添加 `EdgeMarker` 类型导入 + - 使用类型断言确保类型安全 + - 保持与设计器组件一致的箭头样式 + +### 修改时间 +2025-01-22 + +### 修改原因 +用户要求在 `FormTypeDrawer.tsx` 中保存之前 `ReactFlowDesigner.tsx` 的连线箭头功能,实际需要在 `TestCaseDetailDrawer.tsx` 中添加箭头功能。 + +### 验证方法 +1. 打开测试用例详情抽屉,查看连线是否显示箭头 +2. 确认箭头样式与设计器中的一致 +3. 验证箭头方向正确指示流程方向 + +--- + +## 2025-01-22 - 修复 ReactFlow 缩放显示问题 + +### 修改内容 + +1. **问题分析**: + - ReactFlow 缩放功能显示 NaN 或缩放值不更新 + - `onMove` 回调接收的是 `WheelEvent` 而不是 `viewport` 对象 + - 需要正确监听视口变化来更新缩放值 + +2. **解决方案**: + - 使用 `useEffect` 和 `setInterval` 定期检查视口变化 + - 通过 `reactFlowInstance.getViewport()` 获取当前视口状态 + - 添加数值验证,确保只有有效的缩放值才会更新状态 + - 优化显示逻辑,防止 NaN 显示 + +3. **技术实现**: + ```typescript + // 使用 useEffect 监听视口变化 + useEffect(() => { + const interval = setInterval(() => { + const viewport = reactFlowInstance.getViewport(); + const { zoom } = viewport; + if (typeof zoom === 'number' && !isNaN(zoom) && zoom !== currentZoom) { + setCurrentZoom(zoom); + } + }, 100); // 每100ms检查一次 + + return () => clearInterval(interval); + }, [reactFlowInstance, currentZoom]); + ``` + +4. **优化内容**: + - 移除调试日志,提高性能 + - 添加缩放值变化检测,避免不必要的状态更新 + - 确保缩放显示始终为有效数值 + +### 修改时间 +2025-01-22 + +### 修改原因 +用户报告缩放功能显示 NaN 或缩放值不更新,影响用户体验。 + +### 验证方法 +1. 测试缩放功能,确保缩放值实时更新 +2. 验证缩放显示不会出现 NaN +3. 确认缩放重置功能正常工作 + +--- + +## 2025-01-22 - 修复 ImsiRegistrationRecord 外键约束错误 + +### 修改内容 + +1. **问题分析**: + - **TestCaseNode** 有两个ID字段: + - `Id`:数据库主键(通过 `Guid.NewGuid().ToString()` 生成) + - `NodeId`:前端ReactFlow的节点ID(前端传入) + - **ImsiRegistrationRecord.NodeId** 应该引用 `TestCaseNode.Id`(数据库主键),而不是 `TestCaseNode.NodeId`(前端节点ID) + - 在 `BuildImsiRegistrationRecord` 方法中,传递的是 `nodeData.Id`(前端节点ID),导致外键约束失败 + +2. **解决方案**: + - 在 `CreateNodeEntitiesAsync` 方法中创建 `nodeIdMapping` 字典,保存前端节点ID到数据库节点ID的映射 + - 修改 `ProcessFormDataBatchAsync` 方法签名,添加 `nodeIdMapping` 参数 + - 修改 `BuildFormDataEntities` 方法,使用 `nodeIdMapping` 获取正确的数据库节点ID + - 在创建 `ImsiRegistrationRecord` 时使用数据库节点ID而不是前端节点ID + +3. **技术实现**: + - 使用 `Dictionary` 存储前端节点ID到数据库节点ID的映射 + - 在创建 `TestCaseNode` 后立即保存映射关系 + - 在 `BuildFormDataEntities` 中使用 `TryGetValue` 安全获取数据库节点ID + - 添加详细的日志记录,区分前端节点ID和数据库节点ID + +4. **修改的方法**: + - `CreateNodeEntitiesAsync`:添加节点ID映射逻辑 + - `ProcessFormDataBatchAsync`:添加 `nodeIdMapping` 参数 + - `BuildFormDataEntities`:使用正确的数据库节点ID创建实体 + +### 修改时间 +2025-01-22 + +### 修改原因 +用户报告外键约束错误:`insert or update on table "tb_imsi_registration_records" violates foreign key constraint "FK_tb_imsi_registration_records_tb_testcasenode_NodeId"`。问题在于 `ImsiRegistrationRecord.NodeId` 引用了错误的前端节点ID而不是数据库节点ID。 + +### 验证方法 +1. 创建测试用例流程时,确保 `ImsiRegistrationRecord` 的 `NodeId` 字段正确引用 `TestCaseNode.Id` +2. 检查数据库中的外键约束是否满足 +3. 验证日志中显示的前端节点ID和数据库节点ID映射是否正确 + +--- + +## 2025-01-22 - 修复 CreateNodesAsync 方法中的设备注册表单处理逻辑 + +### 修改内容 + +1. **完善 CreateNodesAsync 方法中的表单处理逻辑**: + - 修复了 `if (nodeData.IsFormEnabled==true && nodeData.FormData is not null)` 条件判断 + - 使用 `Select` 预处理节点数据,分离表单处理逻辑,避免嵌套循环 + - 提取了表单处理逻辑到独立的方法中,提高代码可维护性 + - 创建了 `DeviceRegistrationFormData` 类来映射前端表单数据 + +2. **添加设备注册表单数据处理**: + - 当 `FormType.DeviceRegistrationForm` 时,解析表单数据并创建 `ImsiRegistrationRecord` 实体 + - 支持双卡配置:`isDualSim`、`sim1Plmn`、`sim1CellId`、`sim1RegistrationWaitTime` 等字段 + - 自动保存 IMSI 注册记录到数据库 + - 使用策略模式处理不同类型的表单数据,便于扩展 + - 实现并行处理表单数据,提高性能 + - 分离实体组装和数据库保存操作,实现更好的解耦 + +3. **依赖注入和仓储模式**: + - 添加了 `IImsiRegistrationRecordRepository` 依赖注入 + - 使用仓储模式保存 IMSI 注册记录 + - 确保数据一致性和事务完整性 + +4. **数据模型定义**: + - 在 `X1.Domain/Models/DeviceRegistrationFormData.cs` 中创建了 `DeviceRegistrationFormData` 类,包含以下字段: + - `NodeId`: 节点ID + - `IsDualSim`: 是否双卡 + - `Sim1Plmn`: 卡1 PLMN + - `Sim1CellId`: 卡1 CellId + - `Sim1RegistrationWaitTime`: 卡1 注册等待时间 + - `Sim2Plmn`: 卡2 PLMN + - `Sim2CellId`: 卡2 CellId + - `Sim2RegistrationWaitTime`: 卡2 注册等待时间 + - 创建了 `ProcessedNodeData` 类用于预处理节点数据,包含: + - `NodeData`: 原始节点数据 + - `FormDataJson`: 序列化后的表单数据JSON字符串 + - `HasFormData`: 是否有表单数据需要处理 + - 创建了 `FormDataEntities` 类用于强类型管理表单数据实体,包含: + - `ImsiRegistrationRecords`: IMSI 注册记录集合 + - `HasAnyEntities()`: 检查是否有任何实体需要保存 + - `GetTotalEntityCount()`: 获取所有实体的总数量 + - 删除了冗余的 `ProcessDeviceRegistrationFormAsync` 方法,其功能已被新的批量处理方法替代 + - 创建了 `TestCaseFlowBuilder` 类,将节点和连线创建逻辑从命令处理器中提取出来 + - 在 `TestCaseFlowBuilder` 中进一步提取了 `PreprocessNodes` 和 `CreateNodeEntitiesAsync` 方法,使代码结构更清晰 + - 在 `X1.Application/DependencyInjection.cs` 中注册了 `TestCaseFlowBuilder` 服务 + - 修复了 `DeviceRegistrationFormData` 类的 JSON 反序列化问题,添加了 `JsonPropertyName` 属性映射 + - 在 `TestCaseFlowBuilder` 中添加了详细的 JSON 反序列化错误处理和调试日志 + - 修复了 JSON 双重序列化问题,添加了 `GetFormDataJson` 方法来正确处理表单数据 + - 使用 `Newtonsoft.Json` 和 `System.Text.Json.Nodes.JsonObject.Parse` 来解决转义 JSON 字符串的反序列化问题 + - 移除了 `System.Text.Json` 引用,统一使用 `Newtonsoft.Json` 进行 JSON 序列化和反序列化 + - 移除了冗余的 `GetFormDataJson` 方法,简化了预处理逻辑 + - 删除了冗余的 `BuildFormDataEntity` 方法,将表单处理逻辑直接整合到 `BuildFormDataEntities` 方法中 + +5. **技术特性**: + - **类型安全**:使用强类型的数据模型 + - **JSON 序列化**:正确解析前端传递的 JSON 数据 + - **错误处理**:包含空值检查和异常处理 + - **日志记录**:添加详细的日志信息 + - **架构规范**:将数据模型放在 Domain 层,遵循 DDD 架构原则 + - **代码重构**:提取表单处理逻辑到独立方法,提高代码可读性和可维护性 + - **扩展性**:使用 switch 语句处理不同表单类型,便于后续添加新的表单类型 + - **性能优化**:使用 `Select` 预处理数据,实现并行处理表单数据 + - **分离关注点**:将节点创建和表单处理分离,提高代码清晰度 + - **解耦设计**:分离实体组装(同步)和数据库保存(异步)操作 + - **合理使用异步**:只在真正需要异步的地方使用异步操作,避免为异步而异步 + - **类型安全**:使用强类型替代 `object`,避免运行时类型错误 + - **代码清理**:删除冗余方法,保持代码简洁 + - **职责分离**:将流程构建逻辑提取到专门的构建器类中 + - **方法细化**:将复杂方法拆分为更小、更专注的方法 + - **依赖注入**:正确注册和配置服务依赖 + - **数据映射**:确保前后端数据格式的一致性 + +### 修改时间 +2025-01-22 + +### 修改原因 +用户要求修复 `CreateNodesAsync` 方法中的设备注册表单处理逻辑,确保能够正确解析前端传递的表单数据并创建对应的数据库记录。 + +--- + +## 2025-01-22 - 添加 FormType 字段到 ReactFlowDesigner 并创建对应的表单组件 + +### 修改内容 + +1. **更新 ReactFlowDesigner 中的节点数据结构**: + - 在 `FlowNode` 类型中添加 `formType?: number` 字段 + - 在创建新节点时包含 `formType: step.formType` 字段 + +2. **创建表单类型对应的 Drawer 组件**: + - `DeviceRegistrationDrawer.tsx` - 设备注册表单 (FormType.DeviceRegistrationForm = 1) + - `NetworkConnectivityDrawer.tsx` - 网络连通性测试表单 (FormType.NetworkConnectivityForm = 2) + - `NetworkPerformanceDrawer.tsx` - 网络性能测试表单 (FormType.NetworkPerformanceForm = 3) + - `VoiceCallDrawer.tsx` - 语音通话测试表单 (FormType.VoiceCallForm = 4) + +3. **表单组件特性**: + - 每个组件都有完整的表单验证 + - 支持初始数据加载和编辑 + - 统一的 UI 设计和交互模式 + - 类型安全的数据接口 + +4. **添加右键菜单表单显示功能**: + - 在右键菜单中添加了"显示表单"选项 + - 根据节点的 `formType` 显示对应的表单组件 + - 添加了表单状态管理和处理函数 + - 支持表单数据的保存和是否开启状态管理 + +5. **样式优化**: + - 将 `ReactFlowDesigner.tsx` 中的内联样式提取到单独的 `reactflow.css` 文件中 + - 便于后续维护和样式管理 + +6. **提取表单类型定义到单独文件**: + - 创建 `X1.WebUI/src/types/formTypes.ts` 文件 + - 包含所有表单数据类型的定义 + - 添加 FormType 枚举和辅助函数 + - 便于后期维护和类型管理 + +### 表单类型映射 +- **FormType.NoForm (0)** - 无表单,无需特殊处理 +- **FormType.DeviceRegistrationForm (1)** - 设备注册表单,支持双卡配置 +- **FormType.NetworkConnectivityForm (2)** - 网络连通性测试表单,Ping 测试配置 +- **FormType.NetworkPerformanceForm (3)** - 网络性能测试表单,Iperf 测试配置 +- **FormType.VoiceCallForm (4)** - 语音通话测试表单,通话配置 + +### 修改时间 +2025-01-22 + +### 修改原因 +用户要求在 ReactFlowDesigner 中检查并添加 FormType 字段,根据不同的表单类型显示不同的表单组件。 + +--- + +## 2025-01-22 - 节点数据保存优化 + +### 修改内容 + +1. **TestCasesView.tsx 节点数据转换优化**: + - 更新了 `handleSaveFormSubmit` 函数中的节点数据转换逻辑 + - 在 `convertedNodes` 映射中添加了以下字段: + - `formData: node.data?.formData` - 表单数据字段 + - `formType: node.data?.formType` - 表单类型字段 + - `isFormEnabled: node.data?.isFormEnabled` - 是否开启表单字段 + +2. **数据完整性保障**: + - 确保表单配置数据能够完整保存到后端 + - 支持后续的流程加载和表单状态恢复 + - 保持前端表单状态与后端数据的一致性 + +3. **技术特性**: + - **类型安全**:使用可选链操作符确保安全访问 + - **数据完整性**:保存所有必要的表单相关字段 + - **向后兼容**:不影响现有的节点数据结构 + - **用户体验**:确保表单配置在流程保存后能够正确恢复 + +### 修改时间 +2025-01-22 + +### 修改原因 +用户要求在保存测试用例流程时,确保 `formData`、`formType` 和 `isFormEnabled` 字段也被包含在节点数据中,以便后续加载流程时能够正确恢复表单状态。 + +--- + +## 2025-01-22 - 后端 NodeData 添加表单相关字段 + +### 修改内容 + +1. **CreateTestCaseFlowCommand.cs 中的 NodeData 类增强**: + - 在 `NodeData` 类中添加了以下表单相关字段: + - `FormData: object?` - 表单数据字段,支持任意类型的表单数据 + - `FormType: int?` - 表单类型字段,对应 FormType 枚举值 + - `IsFormEnabled: bool?` - 是否开启表单字段,表示表单是否已配置 + +2. **前后端数据一致性**: + - 确保后端 `NodeData` 类与前端传递的数据结构完全匹配 + - 支持表单配置数据的完整保存和恢复 + - 保持类型安全和数据完整性 + +3. **技术特性**: + - **类型安全**:使用可空类型确保向后兼容 + - **数据完整性**:支持完整的表单配置信息 + - **扩展性**:为未来表单功能扩展预留空间 + - **一致性**:前后端数据结构完全对应 + +### 修改时间 +2025-01-22 + +### 修改原因 +用户要求在后端的 `NodeData` 类中添加对应的表单相关字段,以匹配前端传递的 `formData`、`formType` 和 `isFormEnabled` 数据,确保前后端数据结构一致。 + +--- + +## 2025-01-22 - TestCaseNode 实体和处理器添加表单支持 + +### 修改内容 + +1. **TestCaseNode 实体增强**: + - 在 `TestCaseNode` 实体中添加了以下表单相关字段: + - `FormData: string?` - 表单数据字段,以JSON格式存储 + - `FormType: int?` - 表单类型字段,对应 FormType 枚举值 + - `IsFormEnabled: bool` - 是否开启表单字段,默认值为 false + - 更新了 `Create` 方法,添加了表单相关参数支持 + - 保持了向后兼容性,所有新参数都有默认值 + +2. **CreateTestCaseFlowCommandHandler 处理器更新**: + - 在 `CreateNodesAsync` 方法中添加了表单数据处理逻辑 + - 将前端传递的 `FormData` 对象序列化为JSON字符串存储 + - 传递 `FormType` 和 `IsFormEnabled` 字段到 `TestCaseNode.Create` 方法 + - 添加了完整的表单数据保存支持 + +3. **技术特性**: + - **JSON序列化**:使用 `System.Text.Json.JsonSerializer` 将表单数据序列化为JSON字符串 + - **类型安全**:使用可空类型确保向后兼容 + - **数据完整性**:支持完整的表单配置信息保存 + - **扩展性**:为未来表单功能扩展预留空间 + +### 修改时间 +2025-01-22 + +### 修改原因 +用户要求在 `CreateTestCaseFlowCommandHandler` 中也处理新增的表单相关字段,确保表单配置数据能够正确保存到数据库中。 + +--- + +## 2025-01-22 - 前端表单数据序列化优化 + +### 修改内容 + +1. **TestCasesView.tsx 数据序列化**: + - 修改了 `convertedNodes` 中的 `formData` 字段处理逻辑 + - 将前端传递的 `formData` 对象使用 `JSON.stringify()` 序列化为JSON字符串 + - 确保与后台 `FormData` 字段的 JSON 格式存储方式保持一致 + - 添加了空值检查,避免序列化 `null` 或 `undefined` 值 + +2. **数据格式统一**: + - **前端**:`formData` 对象 → `JSON.stringify()` → JSON字符串 + - **后台**:`FormData` 字段接收 JSON字符串格式的数据 + - **数据库**:以 JSON字符串格式存储表单配置数据 + +3. **技术特性**: + - **类型安全**:确保数据格式的一致性 + - **向后兼容**:保持现有功能不受影响 + - **数据完整性**:完整的表单配置信息保存 + +### 修改时间 +2025-01-22 + +### 修改原因 +用户指出后台 `FormData` 字段是 JSON 格式字符串,需要将前端传递的 `formData` 对象序列化为 JSON 字符串以匹配后台存储格式。 + +--- + +## 2025-01-22 - 表单数据条件序列化和服务接口更新 + +### 修改内容 + +1. **TestCasesView.tsx 条件序列化优化**: + - 修改了 `formData` 字段的处理逻辑,现在根据 `isFormEnabled` 来判断是否序列化表单数据 + - 只有当 `isFormEnabled` 为 `true` 且 `formData` 存在时才进行 JSON 序列化 + - 修复了类型错误,将 `null` 改为 `undefined` 以匹配接口定义 + +2. **testcaseService.ts 接口更新**: + - 在 `CreateNodeData` 接口中添加了表单相关字段: + - `formData?: string` - 表单数据(JSON格式) + - `formType?: number` - 表单类型 + - `isFormEnabled?: boolean` - 是否开启表单 + - 确保前端接口定义与后端 `NodeData` 类保持一致 + +3. **数据逻辑优化**: + - **条件序列化**:只有在表单开启且有数据时才序列化,避免存储空数据 + - **类型安全**:修复了 TypeScript 类型错误 + - **接口一致性**:前后端接口定义完全匹配 + +### 修改时间 +2025-01-22 + +### 修改原因 +用户要求根据 `isFormEnabled` 字段来判断是否序列化 `formData`,并且需要检查 `testcaseService.ts` 是否已经更新以匹配新的 `CreateTestCaseFlowCommand` 结构。 + +--- + +## 2025-01-22 - 表单数据工具类提取和类型优化 + +### 修改内容 + +1. **创建表单数据工具类**: + - 新建 `X1.WebUI/src/utils/formDataUtils.ts` 文件 + - 提取了表单数据创建逻辑,不再需要 `type` 字段 + - 提供了统一的表单数据创建接口: + - `createDeviceRegistrationFormData` + - `createNetworkConnectivityFormData` + - `createNetworkPerformanceFormData` + - `createVoiceCallFormData` + +2. **类型系统优化**: + - 在 `formTypes.ts` 中添加了 `SimpleFormData` 类型 + - 移除了 `type` 字段,因为节点已经有 `formType` + - 简化了表单数据结构,避免冗余信息 + +3. **ReactFlowDesigner.tsx 重构**: + - 更新了所有表单保存处理函数,使用新的工具类 + - 修改了 `updateNodeFormData` 函数参数类型 + - 更新了 `formData` 状态变量类型 + - 修复了 Drawer 组件的 `initialData` 类型转换 + +4. **代码优化**: + - **类型安全**:使用类型断言确保正确的数据类型传递 + - **代码复用**:表单数据创建逻辑集中管理 + - **维护性**:表单数据创建逻辑独立维护 + - **一致性**:统一的数据创建模式 + +### 修改时间 +2025-01-22 + +### 修改原因 +用户要求将 `typedFormData` 提取到单独的文件中维护,不需要 `type` 字段,因为节点已经有了 `formType`。 + +--- + +## 2025-01-22 - 修复右键菜单中 formType = 0 的处理 + +### 修改内容 + +1. **完善 getFormTypeName 函数**: + - 在 `getFormTypeName` 函数中添加了 `case 0: return '无表单';` 的处理 + - 确保所有表单类型都有对应的名称显示 + +2. **右键菜单逻辑验证**: + - 确认右键菜单的条件判断 `formType > 0` 是正确的 + - 确认 `handleShowForm` 函数中的检查 `node.data.formType === 0` 是正确的 + - 当 `formType` 为 0 时,不会显示"显示表单"选项 + +3. **逻辑完整性**: + - **右键菜单显示**:只有当 `formType > 0` 时才显示表单配置选项 + - **表单打开检查**:在 `handleShowForm` 中再次检查 `formType === 0` 的情况 + - **双重保护**:确保无表单的节点不会意外打开表单 + +### 修改时间 +2025-01-22 + +### 修改原因 +用户指出右键菜单在 `formType` 为 0 时显示不正确,需要完善表单类型名称的处理逻辑。 + +--- + +## 2025-01-22 - 修复表单数据结构,移除多余的 data 包装 + +### 修改内容 + +1. **简化表单数据结构**: + - 移除了 `formData` 中外层的 `data` 包装 + - 现在 `formData` 直接是具体的数据对象,而不是 `{"data": {...}}` 格式 + - 例如:`{"nodeId":"node-1","isDualSim":false,...}` 而不是 `{"data":{"nodeId":"node-1","isDualSim":false,...}}` + +2. **更新 formDataUtils.ts**: + - 修改所有表单数据创建函数,直接返回数据对象 + - 更新返回类型,不再使用 `SimpleFormData` 包装 + - 简化了数据创建逻辑 + +3. **更新 formTypes.ts**: + - 修改 `SimpleFormData` 类型定义,直接使用具体的数据类型 + - 移除了 `{ data: ... }` 的包装结构 + +4. **更新 ReactFlowDesigner.tsx**: + - 修改 Drawer 组件的 `initialData` 类型转换 + - 直接使用具体的数据类型进行类型断言 + - 简化了数据传递逻辑 + +5. **数据结构对比**: + - **之前**:`{"data":{"nodeId":"node-1","isDualSim":false,...}}` + - **现在**:`{"nodeId":"node-1","isDualSim":false,...}` + +### 修改时间 +2025-01-22 + +### 修改原因 +用户指出 `formData` 中不需要 `data` 包装,数据结构应该是直接的数据对象而不是 `{"data": {...}}` 格式。 + +--- + +## 2025-01-27 - TestStepsTable.tsx 表格对齐问题修复 + +### 修改内容 +1. **问题描述**: + - 表格的表头和表体单元格没有对齐,导致显示效果不佳 + - 原代码将表头和表体分为两个独立的 `Table` 组件,导致列宽计算不一致 + +2. **解决方案**: + - 将表头和表体合并到一个 `Table` 组件中 + - 移除了分离的表头和表体容器 + - 确保所有列使用相同的宽度计算逻辑 + +3. **具体修改**: + - 删除了分离的表头容器 `
` + - 删除了分离的表体容器 `
` + - 将 `TableHeader` 和 `TableBody` 合并到同一个 `Table` 组件中 + - 保持了原有的样式和功能 + +4. **影响**: + - 修复了表格对齐问题 + - 提高了表格的视觉效果 + - 保持了原有的响应式设计和交互功能 + +### 修改时间 +2025-01-27 + +### 修改原因 +用户反映 TestStepsTable.tsx 表格的单元格和表头没有对齐,需要修复表格布局问题。 + +--- + ## 2025-01-22 - TestStepForm 改为 Drawer 方式 ### 修改内容 diff --git a/src/场景与用例类型分析.md b/src/场景与用例类型分析.md new file mode 100644 index 0000000..21a79f0 --- /dev/null +++ b/src/场景与用例类型分析.md @@ -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 +{ + // 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 +{ + // 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.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. **维护阶段**:持续优化场景和流程,提高质量 + +这种分层设计使得测试体系既能够满足业务需求,又能够提供技术实现的灵活性,是一个很好的架构设计模式。