mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-06 03:07:04 +02:00
## Summary of the Pull Request Extracts persistence (load/save) logic from `SettingsModel` and `AppStateModel` into dedicated service classes, following the single-responsibility principle. Consumers now interact with `ISettingsService` and `IAppStateService` instead of receiving raw model objects through DI. **New services introduced:** - `IPersistenceService` / `PersistenceService` — generic `Load<T>` / `Save<T>` with AOT-compatible `JsonTypeInfo<T>`, ensures target directory exists before writing - `ISettingsService` / `SettingsService` — loads settings on construction, runs migrations, exposes `Settings` property and `SettingsChanged` event - `IAppStateService` / `AppStateService` — loads state on construction, exposes `State` property and `StateChanged` event **Key changes:** - `SettingsModel` and `AppStateModel` are now pure data models — all file I/O, migration, and directory management removed - Raw `SettingsModel` and `AppStateModel` removed from DI container; consumers receive the appropriate service - `IApplicationInfoService.ConfigDirectory` injected into services for config path resolution (no more hardcoded `Utilities.BaseSettingsPath`) - ~30 consumer files updated across `Microsoft.CmdPal.UI.ViewModels` and `Microsoft.CmdPal.UI` projects - All `#pragma warning disable SA1300` suppressions removed — convenience accessors replaced with direct `_settingsService.Settings` / `_appStateService.State` access - Namespace prefixes (`Services.ISettingsService`) replaced with proper `using` directives ## PR Checklist - [ ] **Communication:** I've discussed this with core contributors already. - [x] **Tests:** Added/updated and all pass - [ ] **Localization:** N/A — no end-user-facing strings changed - [ ] **Dev docs:** N/A — internal refactor, no public API changes - [ ] **New binaries:** N/A — no new binaries introduced ## Detailed Description of the Pull Request / Additional comments ### Architecture Services are registered as singletons in `App.xaml.cs`: ```csharp services.AddSingleton<IPersistenceService, PersistenceService>(); services.AddSingleton<ISettingsService, SettingsService>(); services.AddSingleton<IAppStateService, AppStateService>(); ``` `PersistenceService.Save<T>` writes the serialized model directly to disk, creating the target directory if it doesn't exist. It also does not attempt to merge existing and new settings/state. `SettingsService` runs hotkey migrations on load and raises `SettingsChanged` after saves. `AppStateService` always raises `StateChanged` after saves. ### Files changed (41 files, +1169/−660) | Area | Files | What changed | |------|-------|-------------| | New services | `Services/IPersistenceService.cs`, `PersistenceService.cs`, `ISettingsService.cs`, `SettingsService.cs`, `IAppStateService.cs`, `AppStateService.cs` | New service interfaces and implementations | | Models | `SettingsModel.cs`, `AppStateModel.cs` | Stripped to pure data bags | | DI | `App.xaml.cs` | Service registration, removed raw model DI | | ViewModels | 12 files | Constructor injection of services | | UI | 10 files | Service injection replacing model access | | Settings | `DockSettings.cs` | `Colors.Transparent` replaced with struct literal to avoid WinUI3 COM dependency | | Tests | `PersistenceServiceTests.cs`, `SettingsServiceTests.cs`, `AppStateServiceTests.cs` | 38 unit tests covering all three services | | Config | `.gitignore` | Added `.squad/`, `.github/agents/` exclusions | ## Validation Steps Performed - Built `Microsoft.CmdPal.UI` with MSBuild (x64/Debug) — exit code 0, clean build - Ran 38 unit tests via `vstest.console.exe` — all passing - Verified no remaining `#pragma warning disable SA1300` blocks - Verified no remaining `Services.` namespace prefixes --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
137 lines
4.3 KiB
C#
137 lines
4.3 KiB
C#
// Copyright (c) Microsoft Corporation
|
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using Microsoft.CmdPal.UI.ViewModels.Services;
|
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
|
|
|
namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
|
|
|
|
[TestClass]
|
|
public partial class PersistenceServiceTests
|
|
{
|
|
private PersistenceService _service = null!;
|
|
private string _testDirectory = null!;
|
|
private string _testFilePath = null!;
|
|
|
|
// Simple test model for persistence testing
|
|
private sealed class TestModel
|
|
{
|
|
public string Name { get; set; } = string.Empty;
|
|
|
|
public int Value { get; set; }
|
|
}
|
|
|
|
[JsonSerializable(typeof(TestModel))]
|
|
private sealed partial class TestJsonContext : JsonSerializerContext
|
|
{
|
|
}
|
|
|
|
[TestInitialize]
|
|
public void Setup()
|
|
{
|
|
_service = new PersistenceService();
|
|
_testDirectory = Path.Combine(Path.GetTempPath(), $"PersistenceServiceTests_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(_testDirectory);
|
|
_testFilePath = Path.Combine(_testDirectory, "test.json");
|
|
}
|
|
|
|
[TestCleanup]
|
|
public void Cleanup()
|
|
{
|
|
if (Directory.Exists(_testDirectory))
|
|
{
|
|
Directory.Delete(_testDirectory, recursive: true);
|
|
}
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Load_ReturnsNewInstance_WhenFileDoesNotExist()
|
|
{
|
|
// Arrange
|
|
var nonExistentPath = Path.Combine(_testDirectory, "nonexistent.json");
|
|
|
|
// Act
|
|
var result = _service.Load(nonExistentPath, TestJsonContext.Default.TestModel);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(string.Empty, result.Name);
|
|
Assert.AreEqual(0, result.Value);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Load_ReturnsDeserializedModel_WhenFileContainsValidJson()
|
|
{
|
|
// Arrange
|
|
var expectedModel = new TestModel { Name = "Test", Value = 42 };
|
|
var json = JsonSerializer.Serialize(expectedModel, TestJsonContext.Default.TestModel);
|
|
File.WriteAllText(_testFilePath, json);
|
|
|
|
// Act
|
|
var result = _service.Load(_testFilePath, TestJsonContext.Default.TestModel);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual("Test", result.Name);
|
|
Assert.AreEqual(42, result.Value);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Load_ReturnsNewInstance_WhenFileContainsInvalidJson()
|
|
{
|
|
// Arrange
|
|
File.WriteAllText(_testFilePath, "{ invalid json }");
|
|
|
|
// Act
|
|
var result = _service.Load(_testFilePath, TestJsonContext.Default.TestModel);
|
|
|
|
// Assert
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(string.Empty, result.Name);
|
|
Assert.AreEqual(0, result.Value);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Save_CreatesFile_WhenFileDoesNotExist()
|
|
{
|
|
// Arrange
|
|
var model = new TestModel { Name = "NewFile", Value = 123 };
|
|
|
|
// Act
|
|
_service.Save(model, _testFilePath, TestJsonContext.Default.TestModel);
|
|
|
|
// Assert
|
|
Assert.IsTrue(File.Exists(_testFilePath));
|
|
var content = File.ReadAllText(_testFilePath);
|
|
Assert.IsTrue(content.Contains("NewFile"));
|
|
Assert.IsTrue(content.Contains("123"));
|
|
}
|
|
|
|
[TestMethod]
|
|
public void Save_HandlesExistingDirectory()
|
|
{
|
|
// Arrange
|
|
// Note: PersistenceService.Save() does NOT create missing directories
|
|
// It relies on Directory.CreateDirectory being called by the consumer
|
|
// (e.g., SettingsService calls Directory.CreateDirectory in SettingsJsonPath())
|
|
var nestedDir = Path.Combine(_testDirectory, "nested");
|
|
Directory.CreateDirectory(nestedDir); // Create directory beforehand
|
|
var nestedFilePath = Path.Combine(nestedDir, "test.json");
|
|
var model = new TestModel { Name = "NestedTest", Value = 321 };
|
|
|
|
// Act
|
|
_service.Save(model, nestedFilePath, TestJsonContext.Default.TestModel);
|
|
|
|
// Assert
|
|
Assert.IsTrue(File.Exists(nestedFilePath));
|
|
var result = _service.Load(nestedFilePath, TestJsonContext.Default.TestModel);
|
|
Assert.AreEqual("NestedTest", result.Name);
|
|
Assert.AreEqual(321, result.Value);
|
|
}
|
|
}
|