34 changed files with 15209 additions and 4614 deletions
@ -0,0 +1,175 @@ |
|||
using X1.Domain.Common; |
|||
using X1.Domain.Entities.TestCase; |
|||
using MediatR; |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace X1.Application.Features.TestCaseFlow.Commands.CreateTestCaseFlow; |
|||
|
|||
/// <summary>
|
|||
/// 节点数据DTO
|
|||
/// </summary>
|
|||
public class NodeData |
|||
{ |
|||
/// <summary>
|
|||
/// 节点ID
|
|||
/// </summary>
|
|||
[Required] |
|||
public string Id { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 节点类型
|
|||
/// </summary>
|
|||
[Required] |
|||
public string Type { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 步骤ID
|
|||
/// </summary>
|
|||
[Required(ErrorMessage = "步骤ID不能为空")] |
|||
public string StepId { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 节点位置X坐标
|
|||
/// </summary>
|
|||
public double PositionX { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 节点位置Y坐标
|
|||
/// </summary>
|
|||
public double PositionY { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 节点数据
|
|||
/// </summary>
|
|||
public object? Data { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 节点宽度
|
|||
/// </summary>
|
|||
public double? Width { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 节点高度
|
|||
/// </summary>
|
|||
public double? Height { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 是否被选中
|
|||
/// </summary>
|
|||
public bool? Selected { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 绝对位置X坐标
|
|||
/// </summary>
|
|||
public double? PositionAbsoluteX { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 绝对位置Y坐标
|
|||
/// </summary>
|
|||
public double? PositionAbsoluteY { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 是否正在拖拽
|
|||
/// </summary>
|
|||
public bool? Dragging { get; set; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 连线数据DTO
|
|||
/// </summary>
|
|||
public class EdgeData |
|||
{ |
|||
/// <summary>
|
|||
/// 连线ID
|
|||
/// </summary>
|
|||
[Required] |
|||
public string Id { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 源节点ID
|
|||
/// </summary>
|
|||
[Required] |
|||
public string Source { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 目标节点ID
|
|||
/// </summary>
|
|||
[Required] |
|||
public string Target { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 连线类型
|
|||
/// </summary>
|
|||
[Required(ErrorMessage = "连线类型不能为空")] |
|||
public string Type { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 是否动画
|
|||
/// </summary>
|
|||
public bool? Animated { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 连线条件(JSON字符串格式)
|
|||
/// </summary>
|
|||
public string? Condition { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 连线样式(JSON字符串格式)
|
|||
/// </summary>
|
|||
public string? Style { get; set; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 创建测试用例流程命令
|
|||
/// </summary>
|
|||
public class CreateTestCaseFlowCommand : IRequest<OperationResult<CreateTestCaseFlowResponse>> |
|||
{ |
|||
/// <summary>
|
|||
/// 流程名称
|
|||
/// </summary>
|
|||
[Required(ErrorMessage = "流程名称不能为空")] |
|||
[MaxLength(200, ErrorMessage = "流程名称不能超过200个字符")] |
|||
public string Name { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 流程描述
|
|||
/// </summary>
|
|||
[MaxLength(1000, ErrorMessage = "流程描述不能超过1000个字符")] |
|||
public string? Description { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 测试流程类型
|
|||
/// </summary>
|
|||
[Required(ErrorMessage = "测试流程类型不能为空")] |
|||
public TestFlowType Type { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 是否启用
|
|||
/// </summary>
|
|||
public bool IsEnabled { get; set; } = true; |
|||
|
|||
/// <summary>
|
|||
/// 视口X坐标
|
|||
/// </summary>
|
|||
public double ViewportX { get; set; } = 40.54057017483274; |
|||
|
|||
/// <summary>
|
|||
/// 视口Y坐标
|
|||
/// </summary>
|
|||
public double ViewportY { get; set; } = 21.183463943747256; |
|||
|
|||
/// <summary>
|
|||
/// 视口缩放级别
|
|||
/// </summary>
|
|||
public double ViewportZoom { get; set; } = 1.1367874248827994; |
|||
|
|||
/// <summary>
|
|||
/// 节点数据列表
|
|||
/// </summary>
|
|||
public List<NodeData>? Nodes { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 连线数据列表
|
|||
/// </summary>
|
|||
public List<EdgeData>? Edges { get; set; } |
|||
} |
|||
@ -0,0 +1,198 @@ |
|||
using MediatR; |
|||
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.Services; |
|||
|
|||
|
|||
namespace X1.Application.Features.TestCaseFlow.Commands.CreateTestCaseFlow; |
|||
|
|||
/// <summary>
|
|||
/// 创建测试用例流程命令处理器
|
|||
/// </summary>
|
|||
public class CreateTestCaseFlowCommandHandler : IRequestHandler<CreateTestCaseFlowCommand, OperationResult<CreateTestCaseFlowResponse>> |
|||
{ |
|||
private readonly ITestCaseFlowRepository _testCaseFlowRepository; |
|||
private readonly ITestCaseNodeRepository _testCaseNodeRepository; |
|||
private readonly ITestCaseEdgeRepository _testCaseEdgeRepository; |
|||
private readonly ILogger<CreateTestCaseFlowCommandHandler> _logger; |
|||
private readonly IUnitOfWork _unitOfWork; |
|||
private readonly ICurrentUserService _currentUserService; |
|||
|
|||
/// <summary>
|
|||
/// 初始化命令处理器
|
|||
/// </summary>
|
|||
public CreateTestCaseFlowCommandHandler( |
|||
ITestCaseFlowRepository testCaseFlowRepository, |
|||
ITestCaseNodeRepository testCaseNodeRepository, |
|||
ITestCaseEdgeRepository testCaseEdgeRepository, |
|||
ILogger<CreateTestCaseFlowCommandHandler> logger, |
|||
IUnitOfWork unitOfWork, |
|||
ICurrentUserService currentUserService) |
|||
{ |
|||
_testCaseFlowRepository = testCaseFlowRepository; |
|||
_testCaseNodeRepository = testCaseNodeRepository; |
|||
_testCaseEdgeRepository = testCaseEdgeRepository; |
|||
_logger = logger; |
|||
_unitOfWork = unitOfWork; |
|||
_currentUserService = currentUserService; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 处理创建测试用例流程命令
|
|||
/// </summary>
|
|||
public async Task<OperationResult<CreateTestCaseFlowResponse>> Handle(CreateTestCaseFlowCommand request, CancellationToken cancellationToken) |
|||
{ |
|||
// 验证请求参数
|
|||
var validationResult = CreateTestCaseFlowCommandValidator.Validate(request); |
|||
if (!validationResult.IsSuccess) |
|||
{ |
|||
return OperationResult<CreateTestCaseFlowResponse>.CreateFailure(validationResult.ErrorMessages ?? new List<string>()); |
|||
} |
|||
|
|||
_logger.LogInformation("开始创建测试用例流程,流程名称: {Name}, 类型: {Type}", |
|||
request.Name, request.Type); |
|||
|
|||
// 验证用户认证
|
|||
var currentUserId = ValidateUserAuthentication(); |
|||
if (!currentUserId.IsSuccess) |
|||
{ |
|||
return OperationResult<CreateTestCaseFlowResponse>.CreateFailure(currentUserId.ErrorMessages ?? new List<string>()); |
|||
} |
|||
|
|||
try |
|||
{ |
|||
// 检查流程名称是否已存在
|
|||
var nameExists = await _testCaseFlowRepository.NameExistsAsync(request.Name, cancellationToken); |
|||
if (nameExists) |
|||
{ |
|||
_logger.LogWarning("测试用例流程名称已存在: {Name}", request.Name); |
|||
return OperationResult<CreateTestCaseFlowResponse>.CreateFailure(new List<string> { $"流程名称 '{request.Name}' 已存在" }); |
|||
} |
|||
|
|||
// 创建测试用例流程
|
|||
var testCaseFlow = X1.Domain.Entities.TestCase.TestCaseFlow.Create( |
|||
name: request.Name, |
|||
type: request.Type, |
|||
createdBy: currentUserId.Data!, |
|||
viewportX: request.ViewportX, |
|||
viewportY: request.ViewportY, |
|||
viewportZoom: request.ViewportZoom, |
|||
description: request.Description, |
|||
isEnabled: request.IsEnabled); |
|||
|
|||
// 保存测试用例流程到数据库
|
|||
var createdFlow = await _testCaseFlowRepository.AddTestCaseFlowAsync(testCaseFlow, cancellationToken); |
|||
|
|||
// 创建节点
|
|||
if (request.Nodes != null && request.Nodes.Any()) |
|||
{ |
|||
await CreateNodesAsync(createdFlow.Id, request.Nodes, cancellationToken); |
|||
} |
|||
|
|||
// 创建连线
|
|||
if (request.Edges != null && request.Edges.Any()) |
|||
{ |
|||
await CreateEdgesAsync(createdFlow.Id, request.Edges, cancellationToken); |
|||
} |
|||
|
|||
await _unitOfWork.SaveChangesAsync(cancellationToken); |
|||
|
|||
_logger.LogInformation("测试用例流程创建成功,ID: {Id}, 名称: {Name}, 节点数量: {NodeCount}, 连线数量: {EdgeCount}", |
|||
createdFlow.Id, createdFlow.Name, request.Nodes?.Count ?? 0, request.Edges?.Count ?? 0); |
|||
|
|||
// 构建响应
|
|||
var response = new CreateTestCaseFlowResponse |
|||
{ |
|||
Id = createdFlow.Id, |
|||
Name = createdFlow.Name, |
|||
Description = createdFlow.Description, |
|||
Type = createdFlow.Type.ToString(), |
|||
IsEnabled = createdFlow.IsEnabled, |
|||
ViewportX = createdFlow.ViewportX, |
|||
ViewportY = createdFlow.ViewportY, |
|||
ViewportZoom = createdFlow.ViewportZoom, |
|||
CreatedAt = createdFlow.CreatedAt, |
|||
CreatedBy = createdFlow.CreatedBy |
|||
}; |
|||
|
|||
return OperationResult<CreateTestCaseFlowResponse>.CreateSuccess(response); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "创建测试用例流程时发生错误,流程名称: {Name}", request.Name); |
|||
return OperationResult<CreateTestCaseFlowResponse>.CreateFailure(new List<string> { "创建测试用例流程失败,请稍后重试" }); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 创建测试用例节点
|
|||
/// </summary>
|
|||
private async Task CreateNodesAsync(string testCaseId, List<NodeData> 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); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 创建测试用例连线
|
|||
/// </summary>
|
|||
private async Task CreateEdgesAsync(string testCaseId, List<EdgeData> edges, CancellationToken cancellationToken) |
|||
{ |
|||
foreach (var edgeData in edges) |
|||
{ |
|||
var testCaseEdge = TestCaseEdge.Create( |
|||
testCaseId: testCaseId, |
|||
edgeId: edgeData.Id, |
|||
sourceNodeId: edgeData.Source, |
|||
targetNodeId: edgeData.Target, |
|||
edgeType: edgeData.Type, |
|||
condition: edgeData.Condition, |
|||
isAnimated: edgeData.Animated ?? false, |
|||
style: edgeData.Style); |
|||
|
|||
await _testCaseEdgeRepository.AddTestCaseEdgeAsync(testCaseEdge, cancellationToken); |
|||
} |
|||
|
|||
_logger.LogInformation("成功创建 {Count} 个测试用例连线,测试用例ID: {TestCaseId}", edges.Count, testCaseId); |
|||
} |
|||
|
|||
|
|||
|
|||
/// <summary>
|
|||
/// 验证用户认证
|
|||
/// </summary>
|
|||
private OperationResult<string> ValidateUserAuthentication() |
|||
{ |
|||
var currentUserId = _currentUserService.GetCurrentUserId(); |
|||
if (string.IsNullOrEmpty(currentUserId)) |
|||
{ |
|||
_logger.LogWarning("用户未认证,无法创建测试用例流程"); |
|||
return OperationResult<string>.CreateFailure(new List<string> { "用户未认证,请先登录" }); |
|||
} |
|||
|
|||
return OperationResult<string>.CreateSuccess(currentUserId); |
|||
} |
|||
} |
|||
@ -0,0 +1,84 @@ |
|||
using X1.Domain.Common; |
|||
using X1.Domain.Entities.TestCase; |
|||
|
|||
namespace X1.Application.Features.TestCaseFlow.Commands.CreateTestCaseFlow; |
|||
|
|||
/// <summary>
|
|||
/// 创建测试用例流程命令验证器
|
|||
/// </summary>
|
|||
public class CreateTestCaseFlowCommandValidator |
|||
{ |
|||
/// <summary>
|
|||
/// 验证创建测试用例流程命令
|
|||
/// </summary>
|
|||
/// <param name="request">创建测试用例流程命令</param>
|
|||
/// <returns>验证结果</returns>
|
|||
public static OperationResult<string> Validate(CreateTestCaseFlowCommand request) |
|||
{ |
|||
var errors = new List<string>(); |
|||
|
|||
if (string.IsNullOrWhiteSpace(request.Name)) |
|||
{ |
|||
errors.Add("流程名称不能为空"); |
|||
} |
|||
|
|||
if (request.Name?.Length > 200) |
|||
{ |
|||
errors.Add("流程名称不能超过200个字符"); |
|||
} |
|||
|
|||
if (request.Description?.Length > 1000) |
|||
{ |
|||
errors.Add("流程描述不能超过1000个字符"); |
|||
} |
|||
|
|||
if (!Enum.IsDefined(typeof(TestFlowType), request.Type)) |
|||
{ |
|||
errors.Add("无效的测试流程类型"); |
|||
} |
|||
|
|||
// 验证节点数据
|
|||
if (request.Nodes != null) |
|||
{ |
|||
foreach (var node in request.Nodes) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(node.Id)) |
|||
{ |
|||
errors.Add("节点ID不能为空"); |
|||
} |
|||
if (string.IsNullOrWhiteSpace(node.Type)) |
|||
{ |
|||
errors.Add("节点类型不能为空"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 验证连线数据
|
|||
if (request.Edges != null) |
|||
{ |
|||
foreach (var edge in request.Edges) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(edge.Id)) |
|||
{ |
|||
errors.Add("连线ID不能为空"); |
|||
} |
|||
if (string.IsNullOrWhiteSpace(edge.Source)) |
|||
{ |
|||
errors.Add("连线源节点ID不能为空"); |
|||
} |
|||
if (string.IsNullOrWhiteSpace(edge.Target)) |
|||
{ |
|||
errors.Add("连线目标节点ID不能为空"); |
|||
} |
|||
if (string.IsNullOrWhiteSpace(edge.Type)) |
|||
{ |
|||
errors.Add("连线类型不能为空"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return errors.Any() |
|||
? OperationResult<string>.CreateFailure(errors) |
|||
: OperationResult<string>.CreateSuccess(string.Empty); |
|||
} |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
namespace X1.Application.Features.TestCaseFlow.Commands.CreateTestCaseFlow; |
|||
|
|||
/// <summary>
|
|||
/// 创建测试用例流程响应
|
|||
/// </summary>
|
|||
public class CreateTestCaseFlowResponse |
|||
{ |
|||
/// <summary>
|
|||
/// 流程ID
|
|||
/// </summary>
|
|||
public string Id { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 流程名称
|
|||
/// </summary>
|
|||
public string Name { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 流程描述
|
|||
/// </summary>
|
|||
public string? Description { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 测试流程类型
|
|||
/// </summary>
|
|||
public string Type { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 是否启用
|
|||
/// </summary>
|
|||
public bool IsEnabled { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 视口X坐标
|
|||
/// </summary>
|
|||
public double ViewportX { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 视口Y坐标
|
|||
/// </summary>
|
|||
public double ViewportY { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 视口缩放级别
|
|||
/// </summary>
|
|||
public double ViewportZoom { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 创建时间
|
|||
/// </summary>
|
|||
public DateTime CreatedAt { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 创建人
|
|||
/// </summary>
|
|||
public string CreatedBy { get; set; } |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using MediatR; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using X1.Domain.Common; |
|||
|
|||
namespace X1.Application.Features.TestCaseFlow.Commands.DeleteTestCaseFlow; |
|||
|
|||
/// <summary>
|
|||
/// 删除测试用例流程命令
|
|||
/// </summary>
|
|||
public class DeleteTestCaseFlowCommand : IRequest<OperationResult<bool>> |
|||
{ |
|||
/// <summary>
|
|||
/// 测试用例流程ID
|
|||
/// </summary>
|
|||
[Required(ErrorMessage = "流程ID不能为空")] |
|||
public string Id { get; set; } = string.Empty; |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
using MediatR; |
|||
using Microsoft.Extensions.Logging; |
|||
using X1.Domain.Common; |
|||
using X1.Domain.Repositories.TestCase; |
|||
using X1.Domain.Repositories.Base; |
|||
using X1.Domain.Services; |
|||
|
|||
namespace X1.Application.Features.TestCaseFlow.Commands.DeleteTestCaseFlow; |
|||
|
|||
/// <summary>
|
|||
/// 删除测试用例流程命令处理器
|
|||
/// </summary>
|
|||
public class DeleteTestCaseFlowCommandHandler : IRequestHandler<DeleteTestCaseFlowCommand, OperationResult<bool>> |
|||
{ |
|||
private readonly ITestCaseFlowRepository _testCaseFlowRepository; |
|||
private readonly ILogger<DeleteTestCaseFlowCommandHandler> _logger; |
|||
private readonly IUnitOfWork _unitOfWork; |
|||
private readonly ICurrentUserService _currentUserService; |
|||
|
|||
/// <summary>
|
|||
/// 初始化命令处理器
|
|||
/// </summary>
|
|||
public DeleteTestCaseFlowCommandHandler( |
|||
ITestCaseFlowRepository testCaseFlowRepository, |
|||
ILogger<DeleteTestCaseFlowCommandHandler> logger, |
|||
IUnitOfWork unitOfWork, |
|||
ICurrentUserService currentUserService) |
|||
{ |
|||
_testCaseFlowRepository = testCaseFlowRepository; |
|||
_logger = logger; |
|||
_unitOfWork = unitOfWork; |
|||
_currentUserService = currentUserService; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 处理删除测试用例流程命令
|
|||
/// </summary>
|
|||
public async Task<OperationResult<bool>> Handle(DeleteTestCaseFlowCommand request, CancellationToken cancellationToken) |
|||
{ |
|||
try |
|||
{ |
|||
_logger.LogInformation("开始删除测试用例流程,流程ID: {Id}", request.Id); |
|||
|
|||
// 验证用户认证
|
|||
var currentUserId = _currentUserService.GetCurrentUserId(); |
|||
if (string.IsNullOrEmpty(currentUserId)) |
|||
{ |
|||
_logger.LogError("无法获取当前用户ID,用户可能未认证"); |
|||
return OperationResult<bool>.CreateFailure("用户未认证,无法删除测试用例流程"); |
|||
} |
|||
|
|||
// 检查测试用例流程是否存在
|
|||
var existingFlow = await _testCaseFlowRepository.GetTestCaseFlowByIdAsync(request.Id, cancellationToken); |
|||
if (existingFlow == null) |
|||
{ |
|||
_logger.LogWarning("测试用例流程不存在: {Id}", request.Id); |
|||
return OperationResult<bool>.CreateFailure($"测试用例流程 {request.Id} 不存在"); |
|||
} |
|||
|
|||
// 删除测试用例流程
|
|||
await _testCaseFlowRepository.DeleteTestCaseFlowAsync(request.Id, cancellationToken); |
|||
await _unitOfWork.SaveChangesAsync(cancellationToken); |
|||
|
|||
_logger.LogInformation("测试用例流程删除成功,流程ID: {Id}, 流程名称: {Name}", |
|||
request.Id, existingFlow.Name); |
|||
return OperationResult<bool>.CreateSuccess(true); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "删除测试用例流程时发生错误,流程ID: {Id}", request.Id); |
|||
return OperationResult<bool>.CreateFailure($"删除测试用例流程时发生错误: {ex.Message}"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using X1.Domain.Common; |
|||
using MediatR; |
|||
|
|||
namespace X1.Application.Features.TestCaseFlow.Queries.GetTestCaseFlowById; |
|||
|
|||
/// <summary>
|
|||
/// 根据ID获取测试用例流程详情查询
|
|||
/// </summary>
|
|||
public class GetTestCaseFlowByIdQuery : IRequest<OperationResult<GetTestCaseFlowByIdResponse>> |
|||
{ |
|||
/// <summary>
|
|||
/// 测试用例流程ID
|
|||
/// </summary>
|
|||
public string Id { get; set; } = null!; |
|||
} |
|||
@ -0,0 +1,217 @@ |
|||
using X1.Domain.Common; |
|||
using X1.Domain.Repositories.TestCase; |
|||
using Microsoft.Extensions.Logging; |
|||
using MediatR; |
|||
using System.Text.Json; |
|||
|
|||
namespace X1.Application.Features.TestCaseFlow.Queries.GetTestCaseFlowById; |
|||
|
|||
/// <summary>
|
|||
/// 根据ID获取测试用例流程详情查询处理器
|
|||
/// </summary>
|
|||
public class GetTestCaseFlowByIdQueryHandler : IRequestHandler<GetTestCaseFlowByIdQuery, OperationResult<GetTestCaseFlowByIdResponse>> |
|||
{ |
|||
private readonly ITestCaseFlowRepository _testCaseFlowRepository; |
|||
private readonly ITestCaseNodeRepository _testCaseNodeRepository; |
|||
private readonly ITestCaseEdgeRepository _testCaseEdgeRepository; |
|||
private readonly ICaseStepConfigRepository _caseStepConfigRepository; |
|||
private readonly ILogger<GetTestCaseFlowByIdQueryHandler> _logger; |
|||
|
|||
/// <summary>
|
|||
/// 初始化查询处理器
|
|||
/// </summary>
|
|||
public GetTestCaseFlowByIdQueryHandler( |
|||
ITestCaseFlowRepository testCaseFlowRepository, |
|||
ITestCaseNodeRepository testCaseNodeRepository, |
|||
ITestCaseEdgeRepository testCaseEdgeRepository, |
|||
ICaseStepConfigRepository caseStepConfigRepository, |
|||
ILogger<GetTestCaseFlowByIdQueryHandler> logger) |
|||
{ |
|||
_testCaseFlowRepository = testCaseFlowRepository; |
|||
_testCaseNodeRepository = testCaseNodeRepository; |
|||
_testCaseEdgeRepository = testCaseEdgeRepository; |
|||
_caseStepConfigRepository = caseStepConfigRepository; |
|||
_logger = logger; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 处理根据ID获取测试用例流程详情查询
|
|||
/// </summary>
|
|||
public async Task<OperationResult<GetTestCaseFlowByIdResponse>> Handle(GetTestCaseFlowByIdQuery request, CancellationToken cancellationToken) |
|||
{ |
|||
try |
|||
{ |
|||
_logger.LogInformation("开始获取测试用例流程详情,ID: {Id}", request.Id); |
|||
|
|||
// 获取测试用例流程详情(包含节点和连线)
|
|||
var testCaseFlow = await _testCaseFlowRepository.GetTestCaseFlowWithDetailsAsync(request.Id, cancellationToken); |
|||
if (testCaseFlow == null) |
|||
{ |
|||
_logger.LogWarning("测试用例流程不存在,ID: {Id}", request.Id); |
|||
return OperationResult<GetTestCaseFlowByIdResponse>.CreateFailure("测试用例流程不存在"); |
|||
} |
|||
|
|||
// 构建响应
|
|||
var response = new GetTestCaseFlowByIdResponse |
|||
{ |
|||
TestCaseFlow = new TestCaseFlowDetailDto |
|||
{ |
|||
Id = testCaseFlow.Id, |
|||
Name = testCaseFlow.Name, |
|||
Description = testCaseFlow.Description, |
|||
Type = testCaseFlow.Type.ToString(), |
|||
IsEnabled = testCaseFlow.IsEnabled, |
|||
ViewportX = testCaseFlow.ViewportX, |
|||
ViewportY = testCaseFlow.ViewportY, |
|||
ViewportZoom = testCaseFlow.ViewportZoom, |
|||
CreatedAt = testCaseFlow.CreatedAt, |
|||
CreatedBy = testCaseFlow.CreatedBy, |
|||
UpdatedAt = testCaseFlow.UpdatedAt, |
|||
UpdatedBy = testCaseFlow.UpdatedBy, |
|||
Nodes = await MapNodesToReactFlowFormatAsync(testCaseFlow.Nodes, cancellationToken), |
|||
Edges = await MapEdgesToReactFlowFormatAsync(testCaseFlow.Edges, cancellationToken) |
|||
} |
|||
}; |
|||
|
|||
_logger.LogInformation("成功获取测试用例流程详情,ID: {Id}, 名称: {Name}, 节点数量: {NodeCount}, 连线数量: {EdgeCount}", |
|||
testCaseFlow.Id, testCaseFlow.Name, response.TestCaseFlow.Nodes.Count, response.TestCaseFlow.Edges.Count); |
|||
|
|||
return OperationResult<GetTestCaseFlowByIdResponse>.CreateSuccess(response); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取测试用例流程详情时发生错误,ID: {Id}", request.Id); |
|||
return OperationResult<GetTestCaseFlowByIdResponse>.CreateFailure("获取测试用例流程详情失败,请稍后重试"); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 将节点映射为 ReactFlow 格式
|
|||
/// </summary>
|
|||
private async Task<List<TestCaseNodeDto>> MapNodesToReactFlowFormatAsync(IEnumerable<X1.Domain.Entities.TestCase.TestCaseNode>? nodes, CancellationToken cancellationToken) |
|||
{ |
|||
if (nodes == null || !nodes.Any()) |
|||
return new List<TestCaseNodeDto>(); |
|||
|
|||
var result = new List<TestCaseNodeDto>(); |
|||
|
|||
foreach (var node in nodes) |
|||
{ |
|||
// 获取步骤配置信息
|
|||
X1.Domain.Entities.TestCase.CaseStepConfig? stepConfig = null; |
|||
if (!string.IsNullOrEmpty(node.StepId)) |
|||
{ |
|||
stepConfig = await _caseStepConfigRepository.GetStepConfigByIdAsync(node.StepId, cancellationToken); |
|||
} |
|||
|
|||
var nodeDto = new TestCaseNodeDto |
|||
{ |
|||
// ReactFlow 格式字段 - 前端需要的格式
|
|||
Id = node.NodeId, |
|||
Type = "testStep", |
|||
Position = new TestCaseNodePositionDto |
|||
{ |
|||
X = node.PositionX, |
|||
Y = node.PositionY |
|||
}, |
|||
Data = new TestCaseNodeDataDto |
|||
{ |
|||
StepId = node.StepId ?? "", |
|||
StepName = stepConfig?.StepName ?? "Unknown", |
|||
StepType = (int)(stepConfig?.StepType ?? X1.Domain.Entities.TestCase.CaseStepType.Process), |
|||
StepTypeName = GetStepTypeName(stepConfig?.StepType ?? X1.Domain.Entities.TestCase.CaseStepType.Process), |
|||
Description = stepConfig?.Description ?? "", |
|||
Icon = stepConfig?.Icon ?? "settings" |
|||
}, |
|||
Width = node.Width, |
|||
Height = node.Height, |
|||
Selected = node.IsSelected, |
|||
PositionAbsolute = node.PositionAbsoluteX.HasValue && node.PositionAbsoluteY.HasValue |
|||
? new TestCaseNodePositionDto |
|||
{ |
|||
X = node.PositionAbsoluteX.Value, |
|||
Y = node.PositionAbsoluteY.Value |
|||
} |
|||
: null, |
|||
Dragging = node.IsDragging |
|||
}; |
|||
|
|||
result.Add(nodeDto); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 将连线映射为 ReactFlow 格式
|
|||
/// </summary>
|
|||
private async Task<List<TestCaseEdgeDto>> MapEdgesToReactFlowFormatAsync(IEnumerable<X1.Domain.Entities.TestCase.TestCaseEdge>? edges, CancellationToken cancellationToken) |
|||
{ |
|||
if (edges == null || !edges.Any()) |
|||
return new List<TestCaseEdgeDto>(); |
|||
|
|||
var result = new List<TestCaseEdgeDto>(); |
|||
|
|||
foreach (var edge in edges) |
|||
{ |
|||
// 解析样式 JSON
|
|||
TestCaseEdgeStyleDto? styleDto = null; |
|||
if (!string.IsNullOrEmpty(edge.Style)) |
|||
{ |
|||
try |
|||
{ |
|||
styleDto = JsonSerializer.Deserialize<TestCaseEdgeStyleDto>(edge.Style); |
|||
} |
|||
catch (JsonException) |
|||
{ |
|||
// 如果解析失败,使用默认样式
|
|||
styleDto = new TestCaseEdgeStyleDto |
|||
{ |
|||
Stroke = "#3b82f6", |
|||
StrokeWidth = 2 |
|||
}; |
|||
} |
|||
} |
|||
|
|||
var edgeDto = new TestCaseEdgeDto |
|||
{ |
|||
// ReactFlow 格式字段 - 前端需要的格式
|
|||
Id = edge.EdgeId, |
|||
Source = edge.SourceNodeId, |
|||
SourceHandle = "bottom", |
|||
Target = edge.TargetNodeId, |
|||
TargetHandle = "top", |
|||
Type = edge.EdgeType ?? "smoothstep", |
|||
Animated = edge.IsAnimated, |
|||
Style = styleDto ?? new TestCaseEdgeStyleDto |
|||
{ |
|||
Stroke = "#3b82f6", |
|||
StrokeWidth = 2 |
|||
}, |
|||
Data = new TestCaseEdgeDataDto |
|||
{ |
|||
Condition = edge.Condition ?? "default" |
|||
} |
|||
}; |
|||
|
|||
result.Add(edgeDto); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取步骤类型名称
|
|||
/// </summary>
|
|||
private string GetStepTypeName(X1.Domain.Entities.TestCase.CaseStepType stepType) |
|||
{ |
|||
return stepType switch |
|||
{ |
|||
X1.Domain.Entities.TestCase.CaseStepType.Start => "Start", |
|||
X1.Domain.Entities.TestCase.CaseStepType.End => "End", |
|||
X1.Domain.Entities.TestCase.CaseStepType.Process => "Process", |
|||
X1.Domain.Entities.TestCase.CaseStepType.Decision => "Decision", |
|||
_ => "Unknown" |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,356 @@ |
|||
namespace X1.Application.Features.TestCaseFlow.Queries.GetTestCaseFlowById; |
|||
|
|||
/// <summary>
|
|||
/// 测试用例节点数据DTO
|
|||
/// </summary>
|
|||
public class TestCaseNodeDataDto |
|||
{ |
|||
/// <summary>
|
|||
/// 步骤ID
|
|||
/// </summary>
|
|||
public string StepId { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 步骤名称
|
|||
/// </summary>
|
|||
public string StepName { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 步骤类型
|
|||
/// </summary>
|
|||
public int StepType { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 步骤类型名称
|
|||
/// </summary>
|
|||
public string StepTypeName { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 描述
|
|||
/// </summary>
|
|||
public string? Description { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 图标
|
|||
/// </summary>
|
|||
public string? Icon { get; set; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 测试用例节点位置DTO
|
|||
/// </summary>
|
|||
public class TestCaseNodePositionDto |
|||
{ |
|||
/// <summary>
|
|||
/// X坐标
|
|||
/// </summary>
|
|||
public double X { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Y坐标
|
|||
/// </summary>
|
|||
public double Y { get; set; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 测试用例节点DTO
|
|||
/// </summary>
|
|||
public class TestCaseNodeDto |
|||
{ |
|||
/// <summary>
|
|||
/// 节点ID
|
|||
/// </summary>
|
|||
public string Id { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 节点类型
|
|||
/// </summary>
|
|||
public string Type { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 节点位置
|
|||
/// </summary>
|
|||
public TestCaseNodePositionDto Position { get; set; } = new(); |
|||
|
|||
/// <summary>
|
|||
/// 节点数据
|
|||
/// </summary>
|
|||
public TestCaseNodeDataDto Data { get; set; } = new(); |
|||
|
|||
/// <summary>
|
|||
/// 节点宽度
|
|||
/// </summary>
|
|||
public double Width { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 节点高度
|
|||
/// </summary>
|
|||
public double Height { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 是否被选中
|
|||
/// </summary>
|
|||
public bool Selected { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 绝对位置
|
|||
/// </summary>
|
|||
public TestCaseNodePositionDto? PositionAbsolute { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 是否正在拖拽
|
|||
/// </summary>
|
|||
public bool Dragging { get; set; } |
|||
|
|||
// 兼容性字段 - 保留原有字段
|
|||
/// <summary>
|
|||
/// 测试用例ID
|
|||
/// </summary>
|
|||
public string TestCaseId { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 节点ID (ReactFlow中的节点ID)
|
|||
/// </summary>
|
|||
public string NodeId { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 执行序号
|
|||
/// </summary>
|
|||
public int SequenceNumber { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 位置X坐标
|
|||
/// </summary>
|
|||
public double PositionX { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 位置Y坐标
|
|||
/// </summary>
|
|||
public double PositionY { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 是否被选中
|
|||
/// </summary>
|
|||
public bool IsSelected { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 绝对位置X坐标
|
|||
/// </summary>
|
|||
public double? PositionAbsoluteX { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 绝对位置Y坐标
|
|||
/// </summary>
|
|||
public double? PositionAbsoluteY { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 是否正在拖拽
|
|||
/// </summary>
|
|||
public bool IsDragging { get; set; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 测试用例连线样式DTO
|
|||
/// </summary>
|
|||
public class TestCaseEdgeStyleDto |
|||
{ |
|||
/// <summary>
|
|||
/// 描边颜色
|
|||
/// </summary>
|
|||
public string? Stroke { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 描边宽度
|
|||
/// </summary>
|
|||
public int StrokeWidth { get; set; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 测试用例连线数据DTO
|
|||
/// </summary>
|
|||
public class TestCaseEdgeDataDto |
|||
{ |
|||
/// <summary>
|
|||
/// 条件
|
|||
/// </summary>
|
|||
public string? Condition { get; set; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 测试用例连线DTO
|
|||
/// </summary>
|
|||
public class TestCaseEdgeDto |
|||
{ |
|||
/// <summary>
|
|||
/// 源节点ID
|
|||
/// </summary>
|
|||
public string Source { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 源连接点
|
|||
/// </summary>
|
|||
public string? SourceHandle { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 目标节点ID
|
|||
/// </summary>
|
|||
public string Target { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 目标连接点
|
|||
/// </summary>
|
|||
public string? TargetHandle { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 连线ID
|
|||
/// </summary>
|
|||
public string Id { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 连线类型
|
|||
/// </summary>
|
|||
public string Type { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 是否动画
|
|||
/// </summary>
|
|||
public bool Animated { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 连线样式
|
|||
/// </summary>
|
|||
public TestCaseEdgeStyleDto Style { get; set; } = new(); |
|||
|
|||
/// <summary>
|
|||
/// 连线数据
|
|||
/// </summary>
|
|||
public TestCaseEdgeDataDto Data { get; set; } = new(); |
|||
|
|||
// 兼容性字段 - 保留原有字段
|
|||
/// <summary>
|
|||
/// 测试用例ID
|
|||
/// </summary>
|
|||
public string TestCaseId { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 连线ID (ReactFlow中的连线ID)
|
|||
/// </summary>
|
|||
public string EdgeId { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 源节点ID
|
|||
/// </summary>
|
|||
public string SourceNodeId { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 目标节点ID
|
|||
/// </summary>
|
|||
public string TargetNodeId { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 连线类型
|
|||
/// </summary>
|
|||
public string? EdgeType { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 连线条件(JSON字符串格式)
|
|||
/// </summary>
|
|||
public string? Condition { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 是否动画
|
|||
/// </summary>
|
|||
public bool IsAnimated { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 连线样式(JSON字符串格式)
|
|||
/// </summary>
|
|||
public string? StyleJson { get; set; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 测试用例流程详情DTO
|
|||
/// </summary>
|
|||
public class TestCaseFlowDetailDto |
|||
{ |
|||
/// <summary>
|
|||
/// 流程ID
|
|||
/// </summary>
|
|||
public string Id { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 流程名称
|
|||
/// </summary>
|
|||
public string Name { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 流程描述
|
|||
/// </summary>
|
|||
public string? Description { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 流程类型
|
|||
/// </summary>
|
|||
public string Type { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 是否启用
|
|||
/// </summary>
|
|||
public bool IsEnabled { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 视口X坐标
|
|||
/// </summary>
|
|||
public double ViewportX { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 视口Y坐标
|
|||
/// </summary>
|
|||
public double ViewportY { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 视口缩放级别
|
|||
/// </summary>
|
|||
public double ViewportZoom { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 创建时间
|
|||
/// </summary>
|
|||
public DateTime CreatedAt { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 创建人
|
|||
/// </summary>
|
|||
public string CreatedBy { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 更新时间
|
|||
/// </summary>
|
|||
public DateTime UpdatedAt { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 更新人
|
|||
/// </summary>
|
|||
public string UpdatedBy { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 节点列表
|
|||
/// </summary>
|
|||
public List<TestCaseNodeDto> Nodes { get; set; } = new(); |
|||
|
|||
/// <summary>
|
|||
/// 连线列表
|
|||
/// </summary>
|
|||
public List<TestCaseEdgeDto> Edges { get; set; } = new(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 根据ID获取测试用例流程详情响应
|
|||
/// </summary>
|
|||
public class GetTestCaseFlowByIdResponse |
|||
{ |
|||
/// <summary>
|
|||
/// 测试用例流程详情
|
|||
/// </summary>
|
|||
public TestCaseFlowDetailDto TestCaseFlow { get; set; } = null!; |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using X1.Domain.Common; |
|||
using MediatR; |
|||
|
|||
namespace X1.Application.Features.TestCaseFlow.Queries.GetTestCaseFlows; |
|||
|
|||
/// <summary>
|
|||
/// 获取测试用例流程列表查询
|
|||
/// </summary>
|
|||
public class GetTestCaseFlowsQuery : IRequest<OperationResult<GetTestCaseFlowsResponse>> |
|||
{ |
|||
/// <summary>
|
|||
/// 搜索关键词(流程名称或描述)
|
|||
/// </summary>
|
|||
public string? SearchTerm { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 流程类型过滤
|
|||
/// </summary>
|
|||
public string? Type { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 启用状态过滤
|
|||
/// </summary>
|
|||
public bool? IsEnabled { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 页码
|
|||
/// </summary>
|
|||
public int PageNumber { get; set; } = 1; |
|||
|
|||
/// <summary>
|
|||
/// 每页大小
|
|||
/// </summary>
|
|||
public int PageSize { get; set; } = 10; |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
using X1.Domain.Common; |
|||
using X1.Domain.Repositories.TestCase; |
|||
using Microsoft.Extensions.Logging; |
|||
using MediatR; |
|||
|
|||
namespace X1.Application.Features.TestCaseFlow.Queries.GetTestCaseFlows; |
|||
|
|||
/// <summary>
|
|||
/// 获取测试用例流程列表查询处理器
|
|||
/// </summary>
|
|||
public class GetTestCaseFlowsQueryHandler : IRequestHandler<GetTestCaseFlowsQuery, OperationResult<GetTestCaseFlowsResponse>> |
|||
{ |
|||
private readonly ITestCaseFlowRepository _testCaseFlowRepository; |
|||
private readonly ILogger<GetTestCaseFlowsQueryHandler> _logger; |
|||
|
|||
/// <summary>
|
|||
/// 初始化查询处理器
|
|||
/// </summary>
|
|||
public GetTestCaseFlowsQueryHandler( |
|||
ITestCaseFlowRepository testCaseFlowRepository, |
|||
ILogger<GetTestCaseFlowsQueryHandler> logger) |
|||
{ |
|||
_testCaseFlowRepository = testCaseFlowRepository; |
|||
_logger = logger; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 处理获取测试用例流程列表查询
|
|||
/// </summary>
|
|||
public async Task<OperationResult<GetTestCaseFlowsResponse>> Handle(GetTestCaseFlowsQuery request, CancellationToken cancellationToken) |
|||
{ |
|||
try |
|||
{ |
|||
_logger.LogInformation("开始获取测试用例流程列表,搜索条件: {SearchTerm}, 类型: {Type}, 启用状态: {IsEnabled}, 页码: {PageNumber}, 每页大小: {PageSize}", |
|||
request.SearchTerm, request.Type, request.IsEnabled, request.PageNumber, request.PageSize); |
|||
|
|||
// 解析流程类型
|
|||
X1.Domain.Entities.TestCase.TestFlowType? flowType = null; |
|||
if (!string.IsNullOrEmpty(request.Type) && Enum.TryParse<X1.Domain.Entities.TestCase.TestFlowType>(request.Type, out var parsedType)) |
|||
{ |
|||
flowType = parsedType; |
|||
} |
|||
|
|||
// 获取分页数据
|
|||
var pagedResult = await _testCaseFlowRepository.GetPagedFlowsAsync( |
|||
name: request.SearchTerm, |
|||
type: flowType, |
|||
isEnabled: request.IsEnabled, |
|||
pageNumber: request.PageNumber, |
|||
pageSize: request.PageSize, |
|||
cancellationToken); |
|||
|
|||
// 构建响应
|
|||
var response = new GetTestCaseFlowsResponse |
|||
{ |
|||
TestCaseFlows = pagedResult.Items.Select(flow => new TestCaseFlowDto |
|||
{ |
|||
Id = flow.Id, |
|||
Name = flow.Name, |
|||
Description = flow.Description, |
|||
Type = flow.Type.ToString(), |
|||
IsEnabled = flow.IsEnabled, |
|||
ViewportX = flow.ViewportX, |
|||
ViewportY = flow.ViewportY, |
|||
ViewportZoom = flow.ViewportZoom, |
|||
CreatedAt = flow.CreatedAt, |
|||
CreatedBy = flow.CreatedBy, |
|||
UpdatedAt = flow.UpdatedAt, |
|||
UpdatedBy = flow.UpdatedBy |
|||
}).ToList(), |
|||
TotalCount = pagedResult.TotalCount, |
|||
PageNumber = request.PageNumber, |
|||
PageSize = request.PageSize, |
|||
TotalPages = (int)Math.Ceiling((double)pagedResult.TotalCount / request.PageSize) |
|||
}; |
|||
|
|||
_logger.LogInformation("成功获取测试用例流程列表,总数: {TotalCount}, 当前页: {PageNumber}, 每页大小: {PageSize}", |
|||
response.TotalCount, response.PageNumber, response.PageSize); |
|||
|
|||
return OperationResult<GetTestCaseFlowsResponse>.CreateSuccess(response); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
_logger.LogError(ex, "获取测试用例流程列表时发生错误"); |
|||
return OperationResult<GetTestCaseFlowsResponse>.CreateFailure("获取测试用例流程列表失败,请稍后重试"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
namespace X1.Application.Features.TestCaseFlow.Queries.GetTestCaseFlows; |
|||
|
|||
/// <summary>
|
|||
/// 测试用例流程DTO
|
|||
/// </summary>
|
|||
public class TestCaseFlowDto |
|||
{ |
|||
/// <summary>
|
|||
/// 流程ID
|
|||
/// </summary>
|
|||
public string Id { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 流程名称
|
|||
/// </summary>
|
|||
public string Name { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 流程描述
|
|||
/// </summary>
|
|||
public string? Description { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 流程类型
|
|||
/// </summary>
|
|||
public string Type { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 是否启用
|
|||
/// </summary>
|
|||
public bool IsEnabled { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 视口X坐标
|
|||
/// </summary>
|
|||
public double ViewportX { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 视口Y坐标
|
|||
/// </summary>
|
|||
public double ViewportY { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 视口缩放级别
|
|||
/// </summary>
|
|||
public double ViewportZoom { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 创建时间
|
|||
/// </summary>
|
|||
public DateTime CreatedAt { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 创建人
|
|||
/// </summary>
|
|||
public string CreatedBy { get; set; } = null!; |
|||
|
|||
/// <summary>
|
|||
/// 更新时间
|
|||
/// </summary>
|
|||
public DateTime UpdatedAt { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 更新人
|
|||
/// </summary>
|
|||
public string UpdatedBy { get; set; } = null!; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取测试用例流程列表响应
|
|||
/// </summary>
|
|||
public class GetTestCaseFlowsResponse |
|||
{ |
|||
/// <summary>
|
|||
/// 测试用例流程列表
|
|||
/// </summary>
|
|||
public List<TestCaseFlowDto> TestCaseFlows { get; set; } = new(); |
|||
|
|||
/// <summary>
|
|||
/// 总记录数
|
|||
/// </summary>
|
|||
public int TotalCount { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 当前页码
|
|||
/// </summary>
|
|||
public int PageNumber { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 每页大小
|
|||
/// </summary>
|
|||
public int PageSize { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 总页数
|
|||
/// </summary>
|
|||
public int TotalPages { get; set; } |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,165 @@ |
|||
using System; |
|||
using Microsoft.EntityFrameworkCore.Migrations; |
|||
|
|||
#nullable disable |
|||
|
|||
namespace X1.Infrastructure.Migrations |
|||
{ |
|||
/// <inheritdoc />
|
|||
public partial class AddTestCaseFlowTables : Migration |
|||
{ |
|||
/// <inheritdoc />
|
|||
protected override void Up(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.CreateTable( |
|||
name: "tb_testcaseflow", |
|||
columns: table => new |
|||
{ |
|||
id = table.Column<string>(type: "character varying(450)", maxLength: 450, nullable: false), |
|||
name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false), |
|||
description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true), |
|||
type = table.Column<int>(type: "integer", nullable: false), |
|||
isenabled = table.Column<bool>(type: "boolean", nullable: false), |
|||
viewport_x = table.Column<double>(type: "double precision", nullable: false), |
|||
viewport_y = table.Column<double>(type: "double precision", nullable: false), |
|||
viewport_zoom = table.Column<double>(type: "double precision", nullable: false), |
|||
createdat = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), |
|||
updatedat = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), |
|||
createdby = table.Column<string>(type: "character varying(450)", maxLength: 450, nullable: false), |
|||
updatedby = table.Column<string>(type: "character varying(450)", maxLength: 450, nullable: true) |
|||
}, |
|||
constraints: table => |
|||
{ |
|||
table.PrimaryKey("PK_tb_testcaseflow", x => x.id); |
|||
}); |
|||
|
|||
migrationBuilder.CreateTable( |
|||
name: "tb_testcaseedge", |
|||
columns: table => new |
|||
{ |
|||
id = table.Column<string>(type: "character varying(450)", maxLength: 450, nullable: false), |
|||
testcaseid = table.Column<string>(type: "character varying(450)", maxLength: 450, nullable: false), |
|||
edgeid = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false), |
|||
sourcenodeid = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false), |
|||
targetnodeid = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false), |
|||
edgetype = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true), |
|||
condition = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true), |
|||
isanimated = table.Column<bool>(type: "boolean", nullable: false), |
|||
style = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true) |
|||
}, |
|||
constraints: table => |
|||
{ |
|||
table.PrimaryKey("PK_tb_testcaseedge", x => x.id); |
|||
table.ForeignKey( |
|||
name: "FK_tb_testcaseedge_tb_testcaseflow_testcaseid", |
|||
column: x => x.testcaseid, |
|||
principalTable: "tb_testcaseflow", |
|||
principalColumn: "id", |
|||
onDelete: ReferentialAction.Cascade); |
|||
}); |
|||
|
|||
migrationBuilder.CreateTable( |
|||
name: "tb_testcasenode", |
|||
columns: table => new |
|||
{ |
|||
id = table.Column<string>(type: "character varying(450)", maxLength: 450, nullable: false), |
|||
testcaseid = table.Column<string>(type: "character varying(450)", maxLength: 450, nullable: false), |
|||
nodeid = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false), |
|||
sequencenumber = table.Column<int>(type: "integer", nullable: false), |
|||
stepid = table.Column<string>(type: "character varying(450)", maxLength: 450, nullable: true), |
|||
positionx = table.Column<double>(type: "double precision", nullable: false), |
|||
positiony = table.Column<double>(type: "double precision", nullable: false), |
|||
width = table.Column<double>(type: "double precision", nullable: false), |
|||
height = table.Column<double>(type: "double precision", nullable: false), |
|||
isselected = table.Column<bool>(type: "boolean", nullable: false), |
|||
positionabsolutex = table.Column<double>(type: "double precision", nullable: true), |
|||
positionabsolutey = table.Column<double>(type: "double precision", nullable: true), |
|||
isdragging = table.Column<bool>(type: "boolean", nullable: false) |
|||
}, |
|||
constraints: table => |
|||
{ |
|||
table.PrimaryKey("PK_tb_testcasenode", x => x.id); |
|||
table.ForeignKey( |
|||
name: "FK_tb_testcasenode_tb_casestepconfig_stepid", |
|||
column: x => x.stepid, |
|||
principalTable: "tb_casestepconfig", |
|||
principalColumn: "id", |
|||
onDelete: ReferentialAction.SetNull); |
|||
table.ForeignKey( |
|||
name: "FK_tb_testcasenode_tb_testcaseflow_testcaseid", |
|||
column: x => x.testcaseid, |
|||
principalTable: "tb_testcaseflow", |
|||
principalColumn: "id", |
|||
onDelete: ReferentialAction.Cascade); |
|||
}); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_testcaseedge_edgeid", |
|||
table: "tb_testcaseedge", |
|||
column: "edgeid"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_testcaseedge_sourcenodeid", |
|||
table: "tb_testcaseedge", |
|||
column: "sourcenodeid"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_testcaseedge_targetnodeid", |
|||
table: "tb_testcaseedge", |
|||
column: "targetnodeid"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_testcaseedge_testcaseid", |
|||
table: "tb_testcaseedge", |
|||
column: "testcaseid"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_testcaseflow_isenabled", |
|||
table: "tb_testcaseflow", |
|||
column: "isenabled"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_testcaseflow_name", |
|||
table: "tb_testcaseflow", |
|||
column: "name"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_testcaseflow_type", |
|||
table: "tb_testcaseflow", |
|||
column: "type"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_tb_testcasenode_stepid", |
|||
table: "tb_testcasenode", |
|||
column: "stepid"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_testcasenode_nodeid", |
|||
table: "tb_testcasenode", |
|||
column: "nodeid"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_testcasenode_sequencenumber", |
|||
table: "tb_testcasenode", |
|||
column: "sequencenumber"); |
|||
|
|||
migrationBuilder.CreateIndex( |
|||
name: "IX_testcasenode_testcaseid", |
|||
table: "tb_testcasenode", |
|||
column: "testcaseid"); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
protected override void Down(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropTable( |
|||
name: "tb_testcaseedge"); |
|||
|
|||
migrationBuilder.DropTable( |
|||
name: "tb_testcasenode"); |
|||
|
|||
migrationBuilder.DropTable( |
|||
name: "tb_testcaseflow"); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,144 @@ |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using X1.Application.Features.TestCaseFlow.Queries.GetTestCaseFlows; |
|||
using X1.Application.Features.TestCaseFlow.Queries.GetTestCaseFlowById; |
|||
using X1.Application.Features.TestCaseFlow.Commands.CreateTestCaseFlow; |
|||
using X1.Application.Features.TestCaseFlow.Commands.DeleteTestCaseFlow; |
|||
using X1.Presentation.Abstractions; |
|||
using X1.Domain.Common; |
|||
using MediatR; |
|||
using Microsoft.Extensions.Logging; |
|||
|
|||
namespace X1.Presentation.Controllers; |
|||
|
|||
/// <summary>
|
|||
/// 测试用例流程控制器
|
|||
/// </summary>
|
|||
[ApiController] |
|||
[Route("api/testcaseflow")] |
|||
[Authorize] |
|||
public class TestCaseFlowController : ApiController |
|||
{ |
|||
private readonly ILogger<TerminalServicesController> _logger; |
|||
|
|||
/// <summary>
|
|||
/// 初始化终端服务控制器
|
|||
/// </summary>
|
|||
public TestCaseFlowController(IMediator mediator, ILogger<TerminalServicesController> logger) |
|||
: base(mediator) |
|||
{ |
|||
_logger = logger; |
|||
} |
|||
|
|||
|
|||
/// <summary>
|
|||
/// 获取测试用例流程列表
|
|||
/// </summary>
|
|||
/// <param name="searchTerm">搜索关键词</param>
|
|||
/// <param name="type">流程类型</param>
|
|||
/// <param name="isEnabled">启用状态</param>
|
|||
/// <param name="pageNumber">页码</param>
|
|||
/// <param name="pageSize">每页大小</param>
|
|||
/// <returns>测试用例流程列表</returns>
|
|||
[HttpGet] |
|||
public async Task<OperationResult<GetTestCaseFlowsResponse>> GetTestCaseFlows( |
|||
[FromQuery] string? searchTerm = null, |
|||
[FromQuery] string? type = null, |
|||
[FromQuery] bool? isEnabled = null, |
|||
[FromQuery] int pageNumber = 1, |
|||
[FromQuery] int pageSize = 10) |
|||
{ |
|||
_logger.LogInformation("获取测试用例流程列表,搜索条件: {SearchTerm}, 类型: {Type}, 启用状态: {IsEnabled}, 页码: {PageNumber}, 每页大小: {PageSize}", |
|||
searchTerm, type, isEnabled, pageNumber, pageSize); |
|||
|
|||
var query = new GetTestCaseFlowsQuery |
|||
{ |
|||
SearchTerm = searchTerm, |
|||
Type = type, |
|||
IsEnabled = isEnabled, |
|||
PageNumber = pageNumber, |
|||
PageSize = pageSize |
|||
}; |
|||
|
|||
var result = await mediator.Send(query); |
|||
if (!result.IsSuccess) |
|||
{ |
|||
_logger.LogWarning("获取测试用例流程列表失败: {ErrorMessages}", string.Join(", ", result.ErrorMessages ?? new List<string>())); |
|||
return result; |
|||
} |
|||
|
|||
_logger.LogInformation("成功获取测试用例流程列表,总数: {TotalCount}", result.Data?.TotalCount); |
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 根据ID获取测试用例流程详情
|
|||
/// </summary>
|
|||
/// <param name="id">测试用例流程ID</param>
|
|||
/// <returns>测试用例流程详情(包含节点和连线)</returns>
|
|||
[HttpGet("{id}")] |
|||
public async Task<OperationResult<GetTestCaseFlowByIdResponse>> GetTestCaseFlowById(string id) |
|||
{ |
|||
_logger.LogInformation("获取测试用例流程详情,ID: {Id}", id); |
|||
|
|||
var query = new GetTestCaseFlowByIdQuery { Id = id }; |
|||
var result = await mediator.Send(query); |
|||
if (!result.IsSuccess) |
|||
{ |
|||
_logger.LogWarning("获取测试用例流程详情失败,ID: {Id}, 错误: {ErrorMessages}", |
|||
id, string.Join(", ", result.ErrorMessages ?? new List<string>())); |
|||
return result; |
|||
} |
|||
|
|||
_logger.LogInformation("成功获取测试用例流程详情,ID: {Id}, 名称: {Name}, 节点数量: {NodeCount}, 连线数量: {EdgeCount}", |
|||
id, result.Data?.TestCaseFlow.Name, result.Data?.TestCaseFlow.Nodes.Count, result.Data?.TestCaseFlow.Edges.Count); |
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 创建测试用例流程
|
|||
/// </summary>
|
|||
/// <param name="command">创建测试用例流程命令</param>
|
|||
/// <returns>创建结果</returns>
|
|||
[HttpPost] |
|||
public async Task<OperationResult<CreateTestCaseFlowResponse>> CreateTestCaseFlow([FromBody] CreateTestCaseFlowCommand command) |
|||
{ |
|||
_logger.LogInformation("开始创建测试用例流程,流程名称: {Name}, 类型: {Type}", |
|||
command.Name, command.Type); |
|||
|
|||
var result = await mediator.Send(command); |
|||
if (!result.IsSuccess) |
|||
{ |
|||
_logger.LogWarning("创建测试用例流程失败,流程名称: {Name}, 错误: {ErrorMessages}", |
|||
command.Name, string.Join(", ", result.ErrorMessages ?? new List<string>())); |
|||
return result; |
|||
} |
|||
|
|||
_logger.LogInformation("成功创建测试用例流程,ID: {Id}, 名称: {Name}, 节点数量: {NodeCount}, 连线数量: {EdgeCount}", |
|||
result.Data?.Id, result.Data?.Name, command.Nodes?.Count ?? 0, command.Edges?.Count ?? 0); |
|||
return result; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 删除测试用例流程
|
|||
/// </summary>
|
|||
/// <param name="id">测试用例流程ID</param>
|
|||
/// <returns>删除结果</returns>
|
|||
[HttpDelete("{id}")] |
|||
public async Task<OperationResult<bool>> DeleteTestCaseFlow(string id) |
|||
{ |
|||
_logger.LogInformation("开始删除测试用例流程,流程ID: {Id}", id); |
|||
|
|||
var command = new DeleteTestCaseFlowCommand { Id = id }; |
|||
var result = await mediator.Send(command); |
|||
if (!result.IsSuccess) |
|||
{ |
|||
_logger.LogWarning("删除测试用例流程失败,流程ID: {Id}, 错误: {ErrorMessages}", |
|||
id, string.Join(", ", result.ErrorMessages ?? new List<string>())); |
|||
return result; |
|||
} |
|||
|
|||
_logger.LogInformation("成功删除测试用例流程,流程ID: {Id}", id); |
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,487 @@ |
|||
import { useState, useEffect, useCallback } from 'react'; |
|||
import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'; |
|||
import ReactFlow, { |
|||
Node, |
|||
Edge, |
|||
Controls, |
|||
Background, |
|||
MiniMap, |
|||
Handle, |
|||
Position, |
|||
ReactFlowProvider, |
|||
useReactFlow |
|||
} from 'reactflow'; |
|||
import { testcaseService, TestCaseFlowDetail } from '@/services/testcaseService'; |
|||
import 'reactflow/dist/style.css'; |
|||
import { |
|||
Play, |
|||
Square, |
|||
GitBranch, |
|||
Smartphone, |
|||
Wifi, |
|||
WifiOff, |
|||
Phone, |
|||
PhoneCall, |
|||
PhoneOff, |
|||
Network, |
|||
Activity, |
|||
Signal, |
|||
SignalHigh, |
|||
SignalLow, |
|||
Settings |
|||
} from 'lucide-react'; |
|||
|
|||
interface TestCaseDetailDrawerProps { |
|||
testCaseId: string | null; |
|||
open: boolean; |
|||
onOpenChange: (open: boolean) => void; |
|||
} |
|||
|
|||
// 自定义节点组件 - 与 ReactFlowDesigner 完全一致
|
|||
const TestStepNode = ({ data, selected }: { data: any; selected?: boolean }) => { |
|||
console.log('TestStepNode 接收到的数据:', data); |
|||
console.log('TestStepNode selected:', selected); |
|||
|
|||
const getIconComponent = (iconName: string) => { |
|||
// 根据图标名称返回对应的图标组件
|
|||
switch (iconName) { |
|||
case 'play-circle': return <Play className="h-4 w-4" />; |
|||
case 'stop-circle': return <Square className="h-4 w-4" />; |
|||
case 'git-branch': return <GitBranch className="h-4 w-4" />; |
|||
case 'settings': return <Settings className="h-4 w-4" />; |
|||
case 'smartphone': return <Smartphone className="h-4 w-4" />; |
|||
case 'wifi': return <Wifi className="h-4 w-4" />; |
|||
case 'wifi-off': return <WifiOff className="h-4 w-4" />; |
|||
case 'phone': return <Phone className="h-4 w-4" />; |
|||
case 'phone-call': return <PhoneCall className="h-4 w-4" />; |
|||
case 'phone-off': return <PhoneOff className="h-4 w-4" />; |
|||
case 'network': return <Network className="h-4 w-4" />; |
|||
case 'activity': return <Activity className="h-4 w-4" />; |
|||
case 'signal': return <Signal className="h-4 w-4" />; |
|||
case 'signal-high': return <SignalHigh className="h-4 w-4" />; |
|||
case 'signal-low': return <SignalLow className="h-4 w-4" />; |
|||
default: return <Settings className="h-4 w-4" />; |
|||
} |
|||
}; |
|||
|
|||
const getNodeStyle = (stepType: number) => { |
|||
switch (stepType) { |
|||
case 1: // 开始步骤 - 圆形
|
|||
return { |
|||
shape: 'rounded-full', |
|||
bgColor: 'bg-blue-50 dark:bg-blue-900/30', |
|||
textColor: 'text-blue-600 dark:text-blue-400', |
|||
borderColor: 'border-blue-200 dark:border-blue-600/50', |
|||
hoverBorderColor: 'hover:border-blue-300 dark:hover:border-blue-500' |
|||
}; |
|||
case 2: // 结束步骤 - 圆形
|
|||
return { |
|||
shape: 'rounded-full', |
|||
bgColor: 'bg-red-50 dark:bg-red-900/30', |
|||
textColor: 'text-red-600 dark:text-red-400', |
|||
borderColor: 'border-red-200 dark:border-red-600/50', |
|||
hoverBorderColor: 'hover:border-red-300 dark:hover:border-red-500' |
|||
}; |
|||
case 3: // 处理步骤 - 矩形
|
|||
return { |
|||
shape: 'rounded-md', |
|||
bgColor: 'bg-green-50 dark:bg-green-900/30', |
|||
textColor: 'text-green-600 dark:text-green-400', |
|||
borderColor: 'border-green-200 dark:border-green-600/50', |
|||
hoverBorderColor: 'hover:border-green-300 dark:hover:border-green-500' |
|||
}; |
|||
case 4: // 判断步骤 - 菱形
|
|||
return { |
|||
shape: 'transform rotate-45', |
|||
bgColor: 'bg-purple-50 dark:bg-purple-900/30', |
|||
textColor: 'text-purple-600 dark:text-purple-400', |
|||
borderColor: 'border-purple-200 dark:border-purple-600/50', |
|||
hoverBorderColor: 'hover:border-purple-300 dark:hover:border-purple-500' |
|||
}; |
|||
default: |
|||
return { |
|||
shape: 'rounded-md', |
|||
bgColor: 'bg-gray-50 dark:bg-gray-800', |
|||
textColor: 'text-gray-600 dark:text-gray-400', |
|||
borderColor: 'border-gray-200 dark:border-gray-700', |
|||
hoverBorderColor: 'hover:border-gray-300 dark:hover:border-gray-600' |
|||
}; |
|||
} |
|||
}; |
|||
|
|||
const getIconBgColor = (iconName: string) => { |
|||
// 设备相关图标 - 蓝色
|
|||
const deviceIcons = ['smartphone', 'phone', 'phone-call', 'phone-off', 'wifi', 'wifi-off', 'signal', 'signal-high', 'signal-low']; |
|||
// 网络相关图标 - 绿色
|
|||
const networkIcons = ['network', 'activity']; |
|||
// 控制相关图标 - 橙色
|
|||
const controlIcons = ['play-circle', 'stop-circle']; |
|||
// 配置相关图标 - 紫色
|
|||
const configIcons = ['settings', 'git-branch']; |
|||
|
|||
if (deviceIcons.includes(iconName)) { |
|||
return 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400'; |
|||
} else if (networkIcons.includes(iconName)) { |
|||
return 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400'; |
|||
} else if (controlIcons.includes(iconName)) { |
|||
return 'bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400'; |
|||
} else if (configIcons.includes(iconName)) { |
|||
return 'bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400'; |
|||
} |
|||
|
|||
return 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400'; |
|||
}; |
|||
|
|||
const nodeStyle = getNodeStyle(data.stepType); |
|||
|
|||
return ( |
|||
<div className={`group relative transition-all duration-200`}> |
|||
{/* 开始和结束步骤使用圆形 */} |
|||
{(data.stepType === 1 || data.stepType === 2) && ( |
|||
<div |
|||
className={`px-2 py-1 ${nodeStyle.shape} ${nodeStyle.bgColor} border ${nodeStyle.borderColor} ${ |
|||
selected ? 'ring-2 ring-blue-500' : '' |
|||
}`}
|
|||
> |
|||
<div className="flex items-center space-x-1"> |
|||
<div className={`flex-shrink-0 w-3 h-3 rounded-lg ${getIconBgColor(data.icon || 'settings')} flex items-center justify-center`}> |
|||
{getIconComponent(data.icon || 'settings')} |
|||
</div> |
|||
<div className="flex-1 min-w-0"> |
|||
<div className="font-medium text-sm text-gray-900 dark:text-gray-100 break-words"> |
|||
{data.stepName} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
)} |
|||
|
|||
{/* 处理步骤使用矩形 */} |
|||
{data.stepType === 3 && ( |
|||
<div |
|||
className={`px-2 py-1 ${nodeStyle.shape} ${nodeStyle.bgColor} border ${nodeStyle.borderColor} ${ |
|||
selected ? 'ring-2 ring-blue-500' : '' |
|||
}`}
|
|||
> |
|||
<div className="flex items-center space-x-1"> |
|||
<div className={`flex-shrink-0 w-3 h-3 rounded-lg ${getIconBgColor(data.icon || 'settings')} flex items-center justify-center`}> |
|||
{getIconComponent(data.icon || 'settings')} |
|||
</div> |
|||
<div className="flex-1 min-w-0"> |
|||
<div className="font-medium text-sm text-gray-900 dark:text-gray-100 break-words"> |
|||
{data.stepName} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
)} |
|||
|
|||
{/* 判断步骤使用菱形 */} |
|||
{data.stepType === 4 && ( |
|||
<div |
|||
className={`px-2 py-1 ${nodeStyle.shape} ${nodeStyle.bgColor} border ${nodeStyle.borderColor} ${ |
|||
selected ? 'ring-2 ring-blue-500' : '' |
|||
}`}
|
|||
> |
|||
<div className="flex items-center space-x-1"> |
|||
<div className={`flex-shrink-0 w-3 h-3 rounded-lg ${getIconBgColor(data.icon || 'settings')} flex items-center justify-center`}> |
|||
{getIconComponent(data.icon || 'settings')} |
|||
</div> |
|||
<div className="flex-1 min-w-0"> |
|||
<div className="font-medium text-sm text-gray-900 dark:text-gray-100 break-words"> |
|||
{data.stepName} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
)} |
|||
|
|||
{/* 连接点 - 根据节点类型显示不同的连接点 */} |
|||
{/* 开始节点 (type=1) - 只有输出连接点 */} |
|||
{data.stepType === 1 && ( |
|||
<> |
|||
<Handle |
|||
type="source" |
|||
position={Position.Right} |
|||
id="right" |
|||
className="w-3 h-3 bg-green-500 border-2 border-white rounded-full hover:bg-green-600" |
|||
style={{ right: -6 }} |
|||
/> |
|||
<Handle |
|||
type="source" |
|||
position={Position.Bottom} |
|||
id="bottom" |
|||
className="w-3 h-3 bg-green-500 border-2 border-white rounded-full hover:bg-green-600" |
|||
style={{ bottom: -6 }} |
|||
/> |
|||
</> |
|||
)} |
|||
|
|||
{/* 结束节点 (type=2) - 只有输入连接点 */} |
|||
{data.stepType === 2 && ( |
|||
<> |
|||
<Handle |
|||
type="target" |
|||
position={Position.Top} |
|||
id="top" |
|||
className="w-3 h-3 bg-blue-500 border-2 border-white rounded-full hover:bg-blue-600" |
|||
style={{ top: -6 }} |
|||
/> |
|||
<Handle |
|||
type="target" |
|||
position={Position.Left} |
|||
id="left" |
|||
className="w-3 h-3 bg-blue-500 border-2 border-white rounded-full hover:bg-blue-600" |
|||
style={{ left: -6 }} |
|||
/> |
|||
</> |
|||
)} |
|||
|
|||
{/* 处理节点 (type=3) - 有输入和输出连接点 */} |
|||
{data.stepType === 3 && ( |
|||
<> |
|||
<Handle |
|||
type="target" |
|||
position={Position.Top} |
|||
id="top" |
|||
className="w-3 h-3 bg-blue-500 border-2 border-white rounded-full hover:bg-blue-600" |
|||
style={{ top: -6 }} |
|||
/> |
|||
<Handle |
|||
type="source" |
|||
position={Position.Right} |
|||
id="right" |
|||
className="w-3 h-3 bg-green-500 border-2 border-white rounded-full hover:bg-green-600" |
|||
style={{ right: -6 }} |
|||
/> |
|||
<Handle |
|||
type="source" |
|||
position={Position.Bottom} |
|||
id="bottom" |
|||
className="w-3 h-3 bg-green-500 border-2 border-white rounded-full hover:bg-green-600" |
|||
style={{ bottom: -6 }} |
|||
/> |
|||
<Handle |
|||
type="target" |
|||
position={Position.Left} |
|||
id="left" |
|||
className="w-3 h-3 bg-blue-500 border-2 border-white rounded-full hover:bg-blue-600" |
|||
style={{ left: -6 }} |
|||
/> |
|||
</> |
|||
)} |
|||
|
|||
{/* 判断节点 (type=4) - 有输入和输出连接点 */} |
|||
{data.stepType === 4 && ( |
|||
<> |
|||
<Handle |
|||
type="target" |
|||
position={Position.Top} |
|||
id="top" |
|||
className="w-3 h-3 bg-blue-500 border-2 border-white rounded-full hover:bg-blue-600" |
|||
style={{ top: -6 }} |
|||
/> |
|||
<Handle |
|||
type="source" |
|||
position={Position.Right} |
|||
id="right" |
|||
className="w-3 h-3 bg-green-500 border-2 border-white rounded-full hover:bg-green-600" |
|||
style={{ right: -6 }} |
|||
/> |
|||
<Handle |
|||
type="source" |
|||
position={Position.Bottom} |
|||
id="bottom" |
|||
className="w-3 h-3 bg-green-500 border-2 border-white rounded-full hover:bg-green-600" |
|||
style={{ bottom: -6 }} |
|||
/> |
|||
<Handle |
|||
type="target" |
|||
position={Position.Left} |
|||
id="left" |
|||
className="w-3 h-3 bg-blue-500 border-2 border-white rounded-full hover:bg-blue-600" |
|||
style={{ left: -6 }} |
|||
/> |
|||
</> |
|||
)} |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
// 节点类型定义
|
|||
const nodeTypes = { |
|||
testStep: TestStepNode, |
|||
}; |
|||
|
|||
function TestCaseDetailDrawerInner({ testCaseId, open, onOpenChange }: TestCaseDetailDrawerProps) { |
|||
const [selectedTestCase, setSelectedTestCase] = useState<TestCaseFlowDetail | null>(null); |
|||
const [flowLoading, setFlowLoading] = useState(false); |
|||
const [error, setError] = useState<string | null>(null); |
|||
const [nodes, setNodes] = useState<Node[]>([]); |
|||
const [edges, setEdges] = useState<Edge[]>([]); |
|||
const { fitView } = useReactFlow(); |
|||
|
|||
useEffect(() => { |
|||
if (open && testCaseId) { |
|||
loadTestCaseDetail(testCaseId); |
|||
} else { |
|||
// 关闭抽屉时清理状态
|
|||
setSelectedTestCase(null); |
|||
setError(null); |
|||
setNodes([]); |
|||
setEdges([]); |
|||
} |
|||
}, [open, testCaseId]); |
|||
|
|||
const loadTestCaseDetail = async (id: string) => { |
|||
try { |
|||
setFlowLoading(true); |
|||
setError(null); |
|||
const result = await testcaseService.getTestCaseFlowById(id); |
|||
if (result.isSuccess && result.data) { |
|||
setSelectedTestCase(result.data.testCaseFlow); |
|||
// 转换节点和连线为ReactFlow格式
|
|||
const flowData = getReactFlowData(result.data.testCaseFlow); |
|||
setNodes(flowData.nodes); |
|||
setEdges(flowData.edges); |
|||
// 延迟执行 fitView,确保节点已渲染
|
|||
setTimeout(() => { |
|||
fitView({ padding: 0.1 }); |
|||
}, 100); |
|||
} else { |
|||
setError('加载测试用例详情失败'); |
|||
console.error('加载测试用例详情失败:', result.errorMessages); |
|||
} |
|||
} catch (error) { |
|||
setError('加载测试用例详情出错'); |
|||
console.error('加载测试用例详情出错:', error); |
|||
} finally { |
|||
setFlowLoading(false); |
|||
} |
|||
}; |
|||
|
|||
// 转换节点和连线为ReactFlow格式
|
|||
const getReactFlowData = (testCase: TestCaseFlowDetail) => { |
|||
console.log('原始节点数据:', testCase.nodes); |
|||
console.log('原始连线数据:', testCase.edges); |
|||
|
|||
const flowNodes: Node[] = testCase.nodes.map(node => { |
|||
console.log('处理节点:', node); |
|||
console.log('节点数据:', node.data); |
|||
console.log('节点位置:', node.position); |
|||
|
|||
// 检查数据字段是否存在,如果不存在则使用默认值
|
|||
const nodeData = { |
|||
stepId: node.data?.stepId || node.id, |
|||
stepName: node.data?.stepName || 'Unknown', |
|||
stepType: node.data?.stepType || 3, |
|||
stepTypeName: node.data?.stepTypeName || '处理步骤', |
|||
description: node.data?.description || '', |
|||
icon: node.data?.icon || 'settings' |
|||
}; |
|||
|
|||
console.log('处理后的节点数据:', nodeData); |
|||
|
|||
return { |
|||
id: node.id, |
|||
type: 'testStep', // 使用自定义节点类型
|
|||
position: node.position, |
|||
data: nodeData, |
|||
width: node.width || 150, |
|||
height: node.height || 50, |
|||
selected: node.selected || false, |
|||
positionAbsolute: node.positionAbsolute, |
|||
dragging: node.dragging || false |
|||
}; |
|||
}); |
|||
|
|||
const flowEdges: Edge[] = testCase.edges.map(edge => { |
|||
console.log('处理连线:', edge); |
|||
|
|||
return { |
|||
id: edge.id, |
|||
source: edge.source, |
|||
sourceHandle: edge.sourceHandle || null, |
|||
target: edge.target, |
|||
targetHandle: edge.targetHandle || null, |
|||
type: edge.type || 'smoothstep', |
|||
animated: edge.animated || false, |
|||
style: edge.style || { stroke: '#333', strokeWidth: 2 }, |
|||
data: edge.data || {} |
|||
}; |
|||
}); |
|||
|
|||
return { nodes: flowNodes, edges: flowEdges }; |
|||
}; |
|||
|
|||
return ( |
|||
<Drawer open={open} onOpenChange={onOpenChange}> |
|||
<DrawerContent className="h-[90vh]"> |
|||
<DrawerHeader> |
|||
<DrawerTitle>{selectedTestCase?.name || '测试用例详情'}</DrawerTitle> |
|||
<DrawerDescription> |
|||
{selectedTestCase?.description || '无描述'} |
|||
</DrawerDescription> |
|||
</DrawerHeader> |
|||
<div className="flex-1 p-4 overflow-hidden"> |
|||
{flowLoading ? ( |
|||
<div className="flex items-center justify-center h-full"> |
|||
<div className="text-center"> |
|||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div> |
|||
<p className="text-gray-600">加载流程中...</p> |
|||
</div> |
|||
</div> |
|||
) : error ? ( |
|||
<div className="flex items-center justify-center h-full"> |
|||
<div className="text-center"> |
|||
<div className="text-red-500 mb-2"> |
|||
<svg className="w-8 h-8 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" /> |
|||
</svg> |
|||
</div> |
|||
<p className="text-gray-600">{error}</p> |
|||
<button |
|||
onClick={() => testCaseId && loadTestCaseDetail(testCaseId)} |
|||
className="mt-2 text-blue-600 hover:text-blue-800 text-sm" |
|||
> |
|||
重试 |
|||
</button> |
|||
</div> |
|||
</div> |
|||
) : selectedTestCase ? ( |
|||
<div className="h-full border rounded-lg"> |
|||
<ReactFlow |
|||
nodes={nodes} |
|||
edges={edges} |
|||
nodeTypes={nodeTypes} |
|||
fitView |
|||
className="bg-gray-50" |
|||
defaultViewport={{ x: 0, y: 0, zoom: 1.5 }} |
|||
minZoom={1} |
|||
maxZoom={2} |
|||
style={{ width: '100%', height: '100%' }} |
|||
snapToGrid={true} |
|||
snapGrid={[15, 15]} |
|||
> |
|||
<Controls /> |
|||
<Background /> |
|||
</ReactFlow> |
|||
</div> |
|||
) : ( |
|||
<div className="flex items-center justify-center h-full"> |
|||
<p className="text-gray-500">未找到流程数据</p> |
|||
</div> |
|||
)} |
|||
</div> |
|||
</DrawerContent> |
|||
</Drawer> |
|||
); |
|||
} |
|||
|
|||
export default function TestCaseDetailDrawer(props: TestCaseDetailDrawerProps) { |
|||
return ( |
|||
<ReactFlowProvider> |
|||
<TestCaseDetailDrawerInner {...props} /> |
|||
</ReactFlowProvider> |
|||
); |
|||
} |
|||
@ -1,203 +1,229 @@ |
|||
import { httpClient } from '@/lib/http-client'; |
|||
import { OperationResult } from '@/types/auth'; |
|||
import { API_PATHS } from '@/constants/api'; |
|||
|
|||
// 用例状态类型
|
|||
export type TestCaseStatus = 'active' | 'inactive' | 'draft' | 'completed'; |
|||
// 测试流程类型 - 与后端 TestFlowType 枚举对应
|
|||
export type TestFlowType = 1 | 2 | 3; |
|||
|
|||
// 用例优先级类型
|
|||
export type TestCasePriority = 'low' | 'medium' | 'high' | 'critical'; |
|||
|
|||
// 用例类型
|
|||
export type TestCaseType = 'functional' | 'performance' | 'security' | 'ui' | 'api'; |
|||
|
|||
// 用例接口定义
|
|||
export interface TestCase { |
|||
// 测试用例流程接口定义
|
|||
export interface TestCaseFlow { |
|||
id: string; |
|||
name: string; |
|||
description: string; |
|||
status: TestCaseStatus; |
|||
priority: TestCasePriority; |
|||
type: TestCaseType; |
|||
tags: string[]; |
|||
requirements: string; |
|||
expectedResult: string; |
|||
preconditions: string; |
|||
testSteps: TestStep[]; |
|||
createdBy: string; |
|||
description?: string; |
|||
type: string; |
|||
isEnabled: boolean; |
|||
viewportX: number; |
|||
viewportY: number; |
|||
viewportZoom: number; |
|||
createdAt: string; |
|||
createdBy: string; |
|||
updatedAt: string; |
|||
updatedBy: string; |
|||
} |
|||
|
|||
// 测试用例节点数据接口
|
|||
export interface TestCaseNodeData { |
|||
stepId: string; |
|||
stepName: string; |
|||
stepType: number; |
|||
stepTypeName: string; |
|||
description?: string; |
|||
icon?: string; |
|||
} |
|||
|
|||
// 测试用例节点位置接口
|
|||
export interface TestCaseNodePosition { |
|||
x: number; |
|||
y: number; |
|||
} |
|||
|
|||
// 测试步骤接口定义
|
|||
export interface TestStep { |
|||
// 测试用例节点接口
|
|||
export interface TestCaseNode { |
|||
id: string; |
|||
type: string; |
|||
position: TestCaseNodePosition; |
|||
data: TestCaseNodeData; |
|||
width: number; |
|||
height: number; |
|||
selected: boolean; |
|||
positionAbsolute?: TestCaseNodePosition; |
|||
dragging: boolean; |
|||
// 兼容性字段
|
|||
testCaseId: string; |
|||
stepNumber: number; |
|||
action: string; |
|||
expectedResult: string; |
|||
actualResult?: string; |
|||
status: 'pending' | 'passed' | 'failed' | 'blocked'; |
|||
notes?: string; |
|||
createdBy: string; |
|||
createdAt: string; |
|||
updatedAt: string; |
|||
nodeId: string; |
|||
sequenceNumber: number; |
|||
positionX: number; |
|||
positionY: number; |
|||
isSelected: boolean; |
|||
positionAbsoluteX?: number; |
|||
positionAbsoluteY?: number; |
|||
isDragging: boolean; |
|||
} |
|||
|
|||
// 创建用例请求接口
|
|||
export interface CreateTestCaseRequest { |
|||
name: string; |
|||
description: string; |
|||
status: TestCaseStatus; |
|||
priority: TestCasePriority; |
|||
type: TestCaseType; |
|||
tags: string[]; |
|||
requirements: string; |
|||
expectedResult: string; |
|||
preconditions: string; |
|||
} |
|||
|
|||
// 更新用例请求接口
|
|||
export interface UpdateTestCaseRequest { |
|||
name: string; |
|||
description: string; |
|||
status: TestCaseStatus; |
|||
priority: TestCasePriority; |
|||
type: TestCaseType; |
|||
tags: string[]; |
|||
requirements: string; |
|||
expectedResult: string; |
|||
preconditions: string; |
|||
} |
|||
|
|||
// 获取用例列表请求接口
|
|||
export interface GetAllTestCasesRequest { |
|||
name?: string; |
|||
status?: TestCaseStatus; |
|||
priority?: TestCasePriority; |
|||
type?: TestCaseType; |
|||
page?: number; |
|||
pageSize?: number; |
|||
// 测试用例连线样式接口
|
|||
export interface TestCaseEdgeStyle { |
|||
stroke?: string; |
|||
strokeWidth: number; |
|||
} |
|||
|
|||
// 获取用例列表响应接口
|
|||
export interface GetAllTestCasesResponse { |
|||
testCases: TestCase[]; |
|||
totalCount: number; |
|||
// 测试用例连线数据接口
|
|||
export interface TestCaseEdgeData { |
|||
condition?: string; |
|||
} |
|||
|
|||
// 创建测试步骤请求接口
|
|||
export interface CreateTestStepRequest { |
|||
// 测试用例连线接口
|
|||
export interface TestCaseEdge { |
|||
source: string; |
|||
sourceHandle?: string; |
|||
target: string; |
|||
targetHandle?: string; |
|||
id: string; |
|||
type: string; |
|||
animated: boolean; |
|||
style: TestCaseEdgeStyle; |
|||
data: TestCaseEdgeData; |
|||
// 兼容性字段
|
|||
testCaseId: string; |
|||
stepNumber: number; |
|||
action: string; |
|||
expectedResult: string; |
|||
notes?: string; |
|||
} |
|||
|
|||
// 更新测试步骤请求接口
|
|||
export interface UpdateTestStepRequest { |
|||
stepNumber: number; |
|||
action: string; |
|||
expectedResult: string; |
|||
actualResult?: string; |
|||
status: 'pending' | 'passed' | 'failed' | 'blocked'; |
|||
notes?: string; |
|||
} |
|||
|
|||
// 获取测试步骤列表请求接口
|
|||
export interface GetAllTestStepsRequest { |
|||
testCaseId?: string; |
|||
status?: 'pending' | 'passed' | 'failed' | 'blocked'; |
|||
page?: number; |
|||
pageSize?: number; |
|||
edgeId: string; |
|||
sourceNodeId: string; |
|||
targetNodeId: string; |
|||
edgeType?: string; |
|||
condition?: string; |
|||
isAnimated: boolean; |
|||
styleJson?: string; |
|||
} |
|||
|
|||
// 获取测试步骤列表响应接口
|
|||
export interface GetAllTestStepsResponse { |
|||
testSteps: TestStep[]; |
|||
totalCount: number; |
|||
// 测试用例流程详情接口
|
|||
export interface TestCaseFlowDetail { |
|||
id: string; |
|||
name: string; |
|||
description?: string; |
|||
type: string; |
|||
isEnabled: boolean; |
|||
viewportX: number; |
|||
viewportY: number; |
|||
viewportZoom: number; |
|||
createdAt: string; |
|||
createdBy: string; |
|||
updatedAt: string; |
|||
updatedBy: string; |
|||
nodes: TestCaseNode[]; |
|||
edges: TestCaseEdge[]; |
|||
} |
|||
|
|||
class TestCaseService { |
|||
private readonly baseUrl = '/api/testcases'; |
|||
// 获取测试用例流程列表请求接口
|
|||
export interface GetTestCaseFlowsRequest { |
|||
searchTerm?: string; |
|||
type?: string; |
|||
isEnabled?: boolean; |
|||
pageNumber?: number; |
|||
pageSize?: number; |
|||
} |
|||
|
|||
// 获取所有用例
|
|||
async getAllTestCases(params: GetAllTestCasesRequest = {}): Promise<OperationResult<GetAllTestCasesResponse>> { |
|||
const queryParams = new URLSearchParams(); |
|||
|
|||
if (params.name) queryParams.append('name', params.name); |
|||
if (params.status) queryParams.append('status', params.status); |
|||
if (params.priority) queryParams.append('priority', params.priority); |
|||
if (params.type) queryParams.append('type', params.type); |
|||
if (params.page) queryParams.append('page', params.page.toString()); |
|||
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); |
|||
// 获取测试用例流程列表响应接口
|
|||
export interface GetTestCaseFlowsResponse { |
|||
testCaseFlows: TestCaseFlow[]; |
|||
totalCount: number; |
|||
pageNumber: number; |
|||
pageSize: number; |
|||
totalPages: number; |
|||
hasPreviousPage: boolean; |
|||
hasNextPage: boolean; |
|||
} |
|||
|
|||
const url = `${this.baseUrl}?${queryParams.toString()}`; |
|||
return httpClient.get<GetAllTestCasesResponse>(url); |
|||
} |
|||
// 获取测试用例流程详情响应接口
|
|||
export interface GetTestCaseFlowByIdResponse { |
|||
testCaseFlow: TestCaseFlowDetail; |
|||
} |
|||
|
|||
// 根据ID获取用例
|
|||
async getTestCaseById(id: string): Promise<OperationResult<TestCase>> { |
|||
return httpClient.get<TestCase>(`${this.baseUrl}/${id}`); |
|||
} |
|||
// 创建测试用例流程节点数据接口
|
|||
export interface CreateNodeData { |
|||
id: string; |
|||
type: string; |
|||
stepId: string; // 步骤配置ID(必填)
|
|||
positionX: number; |
|||
positionY: number; |
|||
data?: any; |
|||
width?: number; |
|||
height?: number; |
|||
selected?: boolean; |
|||
positionAbsoluteX?: number; |
|||
positionAbsoluteY?: number; |
|||
dragging?: boolean; |
|||
} |
|||
|
|||
// 创建用例
|
|||
async createTestCase(data: CreateTestCaseRequest): Promise<OperationResult<TestCase>> { |
|||
return httpClient.post<TestCase>(this.baseUrl, data); |
|||
} |
|||
// 创建测试用例流程连线数据接口
|
|||
export interface CreateEdgeData { |
|||
id: string; |
|||
source: string; |
|||
target: string; |
|||
type: string; |
|||
animated?: boolean; |
|||
condition?: string; |
|||
style?: string; |
|||
} |
|||
|
|||
// 更新用例
|
|||
async updateTestCase(id: string, data: UpdateTestCaseRequest): Promise<OperationResult<TestCase>> { |
|||
return httpClient.put<TestCase>(`${this.baseUrl}/${id}`, data); |
|||
} |
|||
// 创建测试用例流程请求接口
|
|||
export interface CreateTestCaseFlowRequest { |
|||
name: string; |
|||
description?: string; |
|||
type: TestFlowType; |
|||
isEnabled?: boolean; |
|||
viewportX?: number; |
|||
viewportY?: number; |
|||
viewportZoom?: number; |
|||
nodes?: CreateNodeData[]; |
|||
edges?: CreateEdgeData[]; |
|||
} |
|||
|
|||
// 删除用例
|
|||
async deleteTestCase(id: string): Promise<OperationResult<void>> { |
|||
return httpClient.delete<void>(`${this.baseUrl}/${id}`); |
|||
} |
|||
// 创建测试用例流程响应接口
|
|||
export interface CreateTestCaseFlowResponse { |
|||
id: string; |
|||
name: string; |
|||
description?: string; |
|||
type: string; |
|||
isEnabled: boolean; |
|||
viewportX: number; |
|||
viewportY: number; |
|||
viewportZoom: number; |
|||
createdAt: string; |
|||
createdBy: string; |
|||
} |
|||
|
|||
// 获取用例的测试步骤
|
|||
async getTestStepsByTestCaseId(testCaseId: string): Promise<OperationResult<TestStep[]>> { |
|||
return httpClient.get<TestStep[]>(`${this.baseUrl}/${testCaseId}/steps`); |
|||
} |
|||
class TestCaseFlowService { |
|||
private readonly baseUrl = API_PATHS.TEST_CASE_FLOW; |
|||
|
|||
// 获取所有测试步骤
|
|||
async getAllTestSteps(params: GetAllTestStepsRequest = {}): Promise<OperationResult<GetAllTestStepsResponse>> { |
|||
// 获取测试用例流程列表
|
|||
async getTestCaseFlows(params: GetTestCaseFlowsRequest = {}): Promise<OperationResult<GetTestCaseFlowsResponse>> { |
|||
const queryParams = new URLSearchParams(); |
|||
|
|||
if (params.testCaseId) queryParams.append('testCaseId', params.testCaseId); |
|||
if (params.status) queryParams.append('status', params.status); |
|||
if (params.page) queryParams.append('page', params.page.toString()); |
|||
if (params.searchTerm) queryParams.append('searchTerm', params.searchTerm); |
|||
if (params.type) queryParams.append('type', params.type); |
|||
if (params.isEnabled !== undefined) queryParams.append('isEnabled', params.isEnabled.toString()); |
|||
if (params.pageNumber) queryParams.append('pageNumber', params.pageNumber.toString()); |
|||
if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); |
|||
|
|||
const url = `/api/teststeps?${queryParams.toString()}`; |
|||
return httpClient.get<GetAllTestStepsResponse>(url); |
|||
} |
|||
|
|||
// 根据ID获取测试步骤
|
|||
async getTestStepById(id: string): Promise<OperationResult<TestStep>> { |
|||
return httpClient.get<TestStep>(`/api/teststeps/${id}`); |
|||
} |
|||
|
|||
// 创建测试步骤
|
|||
async createTestStep(data: CreateTestStepRequest): Promise<OperationResult<TestStep>> { |
|||
return httpClient.post<TestStep>('/api/teststeps', data); |
|||
const url = `${this.baseUrl}?${queryParams.toString()}`; |
|||
return httpClient.get<GetTestCaseFlowsResponse>(url); |
|||
} |
|||
|
|||
// 更新测试步骤
|
|||
async updateTestStep(id: string, data: UpdateTestStepRequest): Promise<OperationResult<TestStep>> { |
|||
return httpClient.put<TestStep>(`/api/teststeps/${id}`, data); |
|||
// 根据ID获取测试用例流程详情
|
|||
async getTestCaseFlowById(id: string): Promise<OperationResult<GetTestCaseFlowByIdResponse>> { |
|||
return httpClient.get<GetTestCaseFlowByIdResponse>(`${this.baseUrl}/${id}`); |
|||
} |
|||
|
|||
// 删除测试步骤
|
|||
async deleteTestStep(id: string): Promise<OperationResult<void>> { |
|||
return httpClient.delete<void>(`/api/teststeps/${id}`); |
|||
// 创建测试用例流程
|
|||
async createTestCaseFlow(data: CreateTestCaseFlowRequest): Promise<OperationResult<CreateTestCaseFlowResponse>> { |
|||
return httpClient.post<CreateTestCaseFlowResponse>(this.baseUrl, data); |
|||
} |
|||
|
|||
// 批量更新测试步骤状态
|
|||
async updateTestStepsStatus(testCaseId: string, stepIds: string[], status: 'pending' | 'passed' | 'failed' | 'blocked'): Promise<OperationResult<void>> { |
|||
return httpClient.put<void>(`${this.baseUrl}/${testCaseId}/steps/status`, { stepIds, status }); |
|||
// 删除测试用例流程
|
|||
async deleteTestCaseFlow(id: string): Promise<OperationResult<boolean>> { |
|||
return httpClient.delete<boolean>(`${this.baseUrl}/${id}`); |
|||
} |
|||
} |
|||
|
|||
export const testcaseService = new TestCaseService(); |
|||
export const testcaseFlowService = new TestCaseFlowService(); |
|||
|
|||
// 为了保持向后兼容,保留原有的 testcaseService 导出
|
|||
export const testcaseService = testcaseFlowService; |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue