Phase 2-3: UI layout, theme switching, CRUD tools, process execution
- Phase 2: MainWindow 3-section layout (sidebar/content/log bar), Dark/Light theme with ThemeHelper, MainViewModel with ObservableProperty/RelayCommand, tool card filtering by search + category - Phase 3: ToolEditWindow for add/edit tools, ProcessExecutionService (Process.Start + error handling), double-click + right-click context menu (run/edit), path browse dialog - Bugfix: ContextMenu commands now use PlacementTarget.Tag binding (ContextMenu in separate visual tree) - Bugfix: StaticResource converters moved to XAML before DataTemplate to fix XamlParseException on tool card render - Bugfix: Pure filenames (no path separators) treated as PATH commands, not marked invalid - Bugfix: RefreshData preserves SelectedCategory; Load() catches all exceptions; Save() wrapped in try-catch; auto-scroll log to newest entry - Tests: xUnit project with 55 tests covering models, services, converters, and view models
This commit is contained in:
151
PersonalToolBox.Tests/ViewModels/ToolEditViewModelTests.cs
Normal file
151
PersonalToolBox.Tests/ViewModels/ToolEditViewModelTests.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using Moq;
|
||||
using PersonalToolBox.Models;
|
||||
using PersonalToolBox.Services;
|
||||
using PersonalToolBox.ViewModels;
|
||||
|
||||
namespace PersonalToolBox.Tests.ViewModels;
|
||||
|
||||
public class ToolEditViewModelTests
|
||||
{
|
||||
private readonly Mock<IDataService> _dataServiceMock = new();
|
||||
private readonly Mock<ILogService> _logServiceMock = new();
|
||||
private readonly AppConfig _config;
|
||||
|
||||
public ToolEditViewModelTests()
|
||||
{
|
||||
_config = new AppConfig
|
||||
{
|
||||
Categories =
|
||||
[
|
||||
new() { Id = "1", Name = "开发工具" }
|
||||
]
|
||||
};
|
||||
_dataServiceMock.Setup(d => d.Config).Returns(_config);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_AddMode_SetsTitle()
|
||||
{
|
||||
var vm = new ToolEditViewModel(_dataServiceMock.Object, _logServiceMock.Object);
|
||||
|
||||
Assert.Equal("添加工具", vm.WindowTitle);
|
||||
Assert.Empty(vm.Name);
|
||||
Assert.Empty(vm.ExecutablePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_EditMode_SetsTitleAndFields()
|
||||
{
|
||||
var tool = new ToolItem
|
||||
{
|
||||
Name = "MyTool",
|
||||
ExecutablePath = @"C:\test.exe",
|
||||
Arguments = "/arg",
|
||||
HotKey = "Ctrl+T",
|
||||
CategoryId = "1"
|
||||
};
|
||||
|
||||
var vm = new ToolEditViewModel(_dataServiceMock.Object, _logServiceMock.Object, tool);
|
||||
|
||||
Assert.Equal("编辑工具", vm.WindowTitle);
|
||||
Assert.Equal("MyTool", vm.Name);
|
||||
Assert.Equal(@"C:\test.exe", vm.ExecutablePath);
|
||||
Assert.Equal("/arg", vm.Arguments);
|
||||
Assert.Equal("Ctrl+T", vm.HotKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_EmptyName_LogsWarning()
|
||||
{
|
||||
var vm = new ToolEditViewModel(_dataServiceMock.Object, _logServiceMock.Object)
|
||||
{
|
||||
ExecutablePath = @"C:\test.exe"
|
||||
};
|
||||
|
||||
vm.SaveCommand.Execute(null);
|
||||
|
||||
_logServiceMock.Verify(x => x.Warning(It.Is<string>(s => s.Contains("名称不能为空"))), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_EmptyPath_LogsWarning()
|
||||
{
|
||||
var vm = new ToolEditViewModel(_dataServiceMock.Object, _logServiceMock.Object)
|
||||
{
|
||||
Name = "Test"
|
||||
};
|
||||
|
||||
vm.SaveCommand.Execute(null);
|
||||
|
||||
_logServiceMock.Verify(x => x.Warning(It.Is<string>(s => s.Contains("路径不能为空"))), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_AddMode_AddsToolAndCloses()
|
||||
{
|
||||
var vm = new ToolEditViewModel(_dataServiceMock.Object, _logServiceMock.Object)
|
||||
{
|
||||
Name = "NewTool",
|
||||
ExecutablePath = @"C:\test.exe"
|
||||
};
|
||||
bool? closeResult = null;
|
||||
vm.CloseAction = (r) => closeResult = r;
|
||||
|
||||
vm.SaveCommand.Execute(null);
|
||||
|
||||
Assert.True(vm.Saved);
|
||||
Assert.True(closeResult);
|
||||
Assert.Single(_config.Tools);
|
||||
Assert.Equal("NewTool", _config.Tools[0].Name);
|
||||
_dataServiceMock.Verify(x => x.Save(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_EditMode_UpdatesToolAndCloses()
|
||||
{
|
||||
var tool = new ToolItem
|
||||
{
|
||||
Name = "OldName",
|
||||
ExecutablePath = @"C:\old.exe"
|
||||
};
|
||||
|
||||
var vm = new ToolEditViewModel(_dataServiceMock.Object, _logServiceMock.Object, tool)
|
||||
{
|
||||
Name = "NewName",
|
||||
ExecutablePath = @"C:\new.exe"
|
||||
};
|
||||
bool? closeResult = null;
|
||||
vm.CloseAction = (r) => closeResult = r;
|
||||
|
||||
vm.SaveCommand.Execute(null);
|
||||
|
||||
Assert.True(vm.Saved);
|
||||
Assert.True(closeResult);
|
||||
Assert.Equal("NewName", tool.Name);
|
||||
Assert.Equal(@"C:\new.exe", tool.ExecutablePath);
|
||||
_dataServiceMock.Verify(x => x.Save(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Cancel_ClosesWithoutSaving()
|
||||
{
|
||||
var vm = new ToolEditViewModel(_dataServiceMock.Object, _logServiceMock.Object);
|
||||
bool? closeResult = null;
|
||||
vm.CloseAction = (r) => closeResult = r;
|
||||
|
||||
vm.CancelCommand.Execute(null);
|
||||
|
||||
Assert.False(vm.Saved);
|
||||
Assert.False(closeResult);
|
||||
_dataServiceMock.Verify(x => x.Save(), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_LoadsCategories()
|
||||
{
|
||||
var vm = new ToolEditViewModel(_dataServiceMock.Object, _logServiceMock.Object);
|
||||
|
||||
Assert.Single(vm.Categories);
|
||||
Assert.Equal("开发工具", vm.Categories[0].Name);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user