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:
194
PersonalToolBox.Tests/Services/JsonDataServiceTests.cs
Normal file
194
PersonalToolBox.Tests/Services/JsonDataServiceTests.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
using System.IO;
|
||||
using Moq;
|
||||
using PersonalToolBox.Models;
|
||||
using PersonalToolBox.Services;
|
||||
|
||||
namespace PersonalToolBox.Tests.Services;
|
||||
|
||||
public class JsonDataServiceTests
|
||||
{
|
||||
private readonly Mock<ILogService> _logServiceMock = new();
|
||||
private readonly string _testDir;
|
||||
|
||||
public JsonDataServiceTests()
|
||||
{
|
||||
_testDir = Path.Combine(Path.GetTempPath(), $"toolbox_test_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(_testDir);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_FileNotExists_CreatesDefaultConfig()
|
||||
{
|
||||
// Use a path that doesn't exist
|
||||
var configPath = Path.Combine(_testDir, "nonexistent.json");
|
||||
var service = CreateService(configPath);
|
||||
|
||||
service.Load();
|
||||
|
||||
Assert.NotNull(service.Config);
|
||||
Assert.Equal("Dark", service.Config.Theme);
|
||||
Assert.Empty(service.Config.Tools);
|
||||
|
||||
// Verify warning was logged
|
||||
VerifyLog(LogLevel.Info, "默认配置");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save_ThenLoad_PreservesData()
|
||||
{
|
||||
var configPath = Path.Combine(_testDir, "config.json");
|
||||
var service = CreateService(configPath);
|
||||
|
||||
service.Config.Theme = "Light";
|
||||
service.Config.Categories.Add(new Category { Id = "1", Name = "dev" });
|
||||
service.Config.Tools.Add(new ToolItem
|
||||
{
|
||||
Name = "TestTool",
|
||||
ExecutablePath = Path.Combine(_testDir, "test.exe")
|
||||
});
|
||||
|
||||
service.Save();
|
||||
|
||||
// Load into a new service
|
||||
var service2 = CreateService(configPath);
|
||||
service2.Load();
|
||||
|
||||
Assert.Equal("Light", service2.Config.Theme);
|
||||
Assert.Single(service2.Config.Categories);
|
||||
Assert.Equal("dev", service2.Config.Categories[0].Name);
|
||||
Assert.Single(service2.Config.Tools);
|
||||
Assert.Equal("TestTool", service2.Config.Tools[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_InvalidPath_ToolSetToInvalid()
|
||||
{
|
||||
var configPath = Path.Combine(_testDir, "config.json");
|
||||
var service = CreateService(configPath);
|
||||
|
||||
service.Config.Tools.Add(new ToolItem
|
||||
{
|
||||
Name = "MissingTool",
|
||||
ExecutablePath = @"C:\definitely\does\not\exist.exe"
|
||||
});
|
||||
|
||||
service.Save();
|
||||
|
||||
var service2 = CreateService(configPath);
|
||||
service2.Load();
|
||||
|
||||
var tool = service2.Config.Tools[0];
|
||||
Assert.False(tool.IsValid);
|
||||
VerifyLog(LogLevel.Warning, "路径失效");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_ValidPath_ToolRemainsValid()
|
||||
{
|
||||
var configPath = Path.Combine(_testDir, "config.json");
|
||||
var service = CreateService(configPath);
|
||||
|
||||
// Create a real temp file
|
||||
var tempFile = Path.Combine(_testDir, "real_tool.exe");
|
||||
File.WriteAllText(tempFile, "dummy");
|
||||
|
||||
service.Config.Tools.Add(new ToolItem
|
||||
{
|
||||
Name = "RealTool",
|
||||
ExecutablePath = tempFile
|
||||
});
|
||||
|
||||
service.Save();
|
||||
|
||||
var service2 = CreateService(configPath);
|
||||
service2.Load();
|
||||
|
||||
var tool = service2.Config.Tools[0];
|
||||
Assert.True(tool.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_UrlPath_AlwaysValid()
|
||||
{
|
||||
var configPath = Path.Combine(_testDir, "config.json");
|
||||
var service = CreateService(configPath);
|
||||
|
||||
service.Config.Tools.Add(new ToolItem
|
||||
{
|
||||
Name = "WebTool",
|
||||
ExecutablePath = "https://example.com/tool"
|
||||
});
|
||||
|
||||
service.Save();
|
||||
|
||||
var service2 = CreateService(configPath);
|
||||
service2.Load();
|
||||
|
||||
var tool = service2.Config.Tools[0];
|
||||
Assert.True(tool.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_EmptyExecutablePath_SkipsValidation()
|
||||
{
|
||||
var configPath = Path.Combine(_testDir, "config.json");
|
||||
var service = CreateService(configPath);
|
||||
|
||||
service.Config.Tools.Add(new ToolItem
|
||||
{
|
||||
Name = "NoPathTool",
|
||||
ExecutablePath = ""
|
||||
});
|
||||
|
||||
service.Save();
|
||||
|
||||
var service2 = CreateService(configPath);
|
||||
service2.Load();
|
||||
|
||||
var tool = service2.Config.Tools[0];
|
||||
Assert.True(tool.IsValid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_CorruptedJson_LogsErrorAndUsesDefaults()
|
||||
{
|
||||
var configPath = Path.Combine(_testDir, "config.json");
|
||||
File.WriteAllText(configPath, "this is not json {{{");
|
||||
|
||||
var service = CreateService(configPath);
|
||||
service.Load();
|
||||
|
||||
Assert.NotNull(service.Config);
|
||||
Assert.Equal("Dark", service.Config.Theme);
|
||||
VerifyLog(LogLevel.Error, "解析失败");
|
||||
}
|
||||
|
||||
// ───────────────────────────── helpers ─────────────────────────────
|
||||
|
||||
private JsonDataService CreateService(string filePath)
|
||||
{
|
||||
var service = new JsonDataService(_logServiceMock.Object);
|
||||
// Replace the file path via reflection
|
||||
typeof(JsonDataService)
|
||||
.GetField("_filePath", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?
|
||||
.SetValue(service, filePath);
|
||||
return service;
|
||||
}
|
||||
|
||||
private void VerifyLog(LogLevel level, string fragment)
|
||||
{
|
||||
// 根据日志级别验证对应方法被调用
|
||||
switch (level)
|
||||
{
|
||||
case LogLevel.Info:
|
||||
_logServiceMock.Verify(x => x.Info(It.Is<string>(s => s.Contains(fragment))), Times.AtLeastOnce);
|
||||
break;
|
||||
case LogLevel.Warning:
|
||||
_logServiceMock.Verify(x => x.Warning(It.Is<string>(s => s.Contains(fragment))), Times.AtLeastOnce);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
_logServiceMock.Verify(x => x.Error(It.Is<string>(s => s.Contains(fragment))), Times.AtLeastOnce);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user