Phase 6: 一键多开 (工具组合) 功能

- 数据模型: ToolItem 新增 IsGroup(bool) + SubToolIds(List<string>) 字段

- 执行逻辑: ProcessExecutionService 改为 ExecuteAsync, 组合卡片遍历子工具逐一启动(500ms延迟), 孤儿ID跳过并打印警告

- 组合编辑: GroupEditViewModel + GroupEditWindow, 复选框列表勾选非组合工具

- 主界面: 标题栏新增 '+添加组合' 按钮(蓝色), 组合卡片右下角显示 📦 角标

- 右键菜单: 区分 '编辑工具' (普通) 和 '编辑组合' (IsGroup=true)

- 快捷键: HotKeyManager 适配 ExecuteAsync 异步调用

- 测试: 82 tests total (ProcessExecution 4->6, GroupEdit 5 new)
This commit is contained in:
2026-05-10 00:15:39 +08:00
parent 599964f078
commit 2c985e8d63
14 changed files with 668 additions and 31 deletions

View File

@@ -7,19 +7,26 @@ namespace PersonalToolBox.Tests.Services;
public class ProcessExecutionServiceTests
{
private readonly Mock<ILogService> _logServiceMock = new();
private readonly Mock<IDataService> _dataServiceMock = new();
private readonly AppConfig _config = new();
public ProcessExecutionServiceTests()
{
_dataServiceMock.Setup(d => d.Config).Returns(_config);
}
[Fact]
public void Execute_NullTool_LogsError()
public async Task Execute_NullTool_LogsError()
{
var service = new ProcessExecutionService(_logServiceMock.Object);
var service = new ProcessExecutionService(_logServiceMock.Object, _dataServiceMock.Object);
service.Execute(null!);
await service.ExecuteAsync(null!);
_logServiceMock.Verify(x => x.Error(It.Is<string>(s => s.Contains("空工具项"))), Times.Once);
}
[Fact]
public void Execute_InvalidTool_LogsWarning()
public async Task Execute_InvalidTool_LogsWarning()
{
var tool = new ToolItem
{
@@ -27,15 +34,15 @@ public class ProcessExecutionServiceTests
ExecutablePath = @"C:\nonexistent.exe",
IsValid = false
};
var service = new ProcessExecutionService(_logServiceMock.Object);
var service = new ProcessExecutionService(_logServiceMock.Object, _dataServiceMock.Object);
service.Execute(tool);
await service.ExecuteAsync(tool);
_logServiceMock.Verify(x => x.Warning(It.Is<string>(s => s.Contains("无法运行") && s.Contains("BadTool"))), Times.Once);
}
[Fact]
public void Execute_ProcessFails_LogsError()
public async Task Execute_ProcessFails_LogsError()
{
var tool = new ToolItem
{
@@ -43,28 +50,71 @@ public class ProcessExecutionServiceTests
ExecutablePath = @"Z:\impossible_path\not_really.exe",
IsValid = true
};
var service = new ProcessExecutionService(_logServiceMock.Object);
var service = new ProcessExecutionService(_logServiceMock.Object, _dataServiceMock.Object);
service.Execute(tool);
await service.ExecuteAsync(tool);
// Opening a non-existent file should throw and be caught
_logServiceMock.Verify(x => x.Error(It.Is<string>(s => s.Contains("FailTool") && s.Contains("失败"))), Times.Once);
}
[Fact]
public void Execute_Success_LogsInfo()
public async Task Execute_Success_LogsInfo()
{
// Use a command that definitely exists on Windows
var tool = new ToolItem
{
Name = "记事本",
ExecutablePath = "notepad.exe",
IsValid = true
};
var service = new ProcessExecutionService(_logServiceMock.Object);
var service = new ProcessExecutionService(_logServiceMock.Object, _dataServiceMock.Object);
service.Execute(tool);
await service.ExecuteAsync(tool);
_logServiceMock.Verify(x => x.Info(It.Is<string>(s => s.Contains("成功启动") && s.Contains("记事本"))), Times.Once);
}
[Fact]
public async Task Execute_Group_BatchExecutesSubTools()
{
var sub1 = new ToolItem { Id = "s1", Name = "Sub1", ExecutablePath = "cmd.exe", IsValid = true };
var sub2 = new ToolItem { Id = "s2", Name = "Sub2", ExecutablePath = "cmd.exe", IsValid = true };
_config.Tools.Add(sub1);
_config.Tools.Add(sub2);
var group = new ToolItem
{
Name = "MyGroup",
IsGroup = true,
SubToolIds = new List<string> { "s1", "s2" },
IsValid = true
};
var service = new ProcessExecutionService(_logServiceMock.Object, _dataServiceMock.Object);
await service.ExecuteAsync(group);
_logServiceMock.Verify(x => x.Info(It.Is<string>(s => s.Contains("开始启动组合"))), Times.Once);
_logServiceMock.Verify(x => x.Info(It.Is<string>(s => s.Contains("组合") && s.Contains("启动完成"))), Times.Once);
_logServiceMock.Verify(x => x.Info(It.Is<string>(s => s.Contains("成功启动") && s.Contains("Sub1"))), Times.Once);
}
[Fact]
public async Task Execute_Group_OrphanSubIds_Skipped()
{
var sub1 = new ToolItem { Id = "s1", Name = "Sub1", ExecutablePath = "cmd.exe", IsValid = true };
_config.Tools.Add(sub1);
var group = new ToolItem
{
Name = "Group",
IsGroup = true,
SubToolIds = new List<string> { "s1", "missing_id" },
IsValid = true
};
var service = new ProcessExecutionService(_logServiceMock.Object, _dataServiceMock.Object);
await service.ExecuteAsync(group);
_logServiceMock.Verify(x => x.Warning(It.Is<string>(s => s.Contains("找不到 ID") && s.Contains("missing_id"))), Times.Once);
_logServiceMock.Verify(x => x.Info(It.Is<string>(s => s.Contains("成功启动") && s.Contains("Sub1"))), Times.Once);
}
}