diff --git a/.antigravitycli/5ff4f24c-d4a3-4a12-ba23-13f71a7b0b49.json b/.antigravitycli/5ff4f24c-d4a3-4a12-ba23-13f71a7b0b49.json
new file mode 120000
index 0000000..5894821
--- /dev/null
+++ b/.antigravitycli/5ff4f24c-d4a3-4a12-ba23-13f71a7b0b49.json
@@ -0,0 +1 @@
+C:/Users/jasonross/.gemini/config/projects/5ff4f24c-d4a3-4a12-ba23-13f71a7b0b49.json
\ No newline at end of file
diff --git a/LibreHardwareMonitor.Windows.WinUI.Tests/LibreHardwareMonitor.Windows.WinUI.Tests.csproj b/LibreHardwareMonitor.Windows.WinUI.Tests/LibreHardwareMonitor.Windows.WinUI.Tests.csproj
new file mode 100644
index 0000000..e43b6f1
--- /dev/null
+++ b/LibreHardwareMonitor.Windows.WinUI.Tests/LibreHardwareMonitor.Windows.WinUI.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net10.0-windows10.0.19041.0
+ 10.0.19041.0
+ x64
+ win-x64
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LibreHardwareMonitor.Windows.WinUI.Tests/Services/AppSettingsTests.cs b/LibreHardwareMonitor.Windows.WinUI.Tests/Services/AppSettingsTests.cs
new file mode 100644
index 0000000..e77489f
--- /dev/null
+++ b/LibreHardwareMonitor.Windows.WinUI.Tests/Services/AppSettingsTests.cs
@@ -0,0 +1,118 @@
+using System;
+using System.IO;
+using System.Reflection;
+using LibreHardwareMonitor.Windows.WinUI.Services;
+using Windows.UI;
+using Xunit;
+
+namespace LibreHardwareMonitor.Windows.WinUI.Tests.Services;
+
+public class AppSettingsTests : IDisposable
+{
+ private readonly AppSettings _settings;
+ private readonly string _tempFile;
+
+ public AppSettingsTests()
+ {
+ _tempFile = Path.GetTempFileName();
+
+ // Use reflection to instantiate AppSettings with custom filename
+ var ctor = typeof(AppSettings).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(string) }, null);
+ _settings = (AppSettings)ctor.Invoke(new object[] { _tempFile });
+ }
+
+ public void Dispose()
+ {
+ if (File.Exists(_tempFile))
+ {
+ File.Delete(_tempFile);
+ }
+ }
+
+ [Fact]
+ public void FileName_ReturnsCorrectPath()
+ {
+ Assert.Equal(_tempFile, _settings.FileName);
+ }
+
+ [Fact]
+ public void SetAndGetString()
+ {
+ _settings.SetValue("testStr", "hello");
+ Assert.True(_settings.Contains("testStr"));
+ Assert.Equal("hello", _settings.GetValue("testStr", "default"));
+ Assert.Equal("default", _settings.GetValue("missing", "default"));
+ }
+
+ [Fact]
+ public void SetAndGetInt()
+ {
+ _settings.SetValue("testInt", 42);
+ Assert.Equal(42, _settings.GetValue("testInt", 0));
+ Assert.Equal(10, _settings.GetValue("missing", 10));
+ }
+
+ [Fact]
+ public void SetAndGetFloat()
+ {
+ _settings.SetValue("testFloat", 3.14f);
+ Assert.Equal(3.14f, _settings.GetValue("testFloat", 0f));
+ Assert.Equal(1.5f, _settings.GetValue("missing", 1.5f));
+ }
+
+ [Fact]
+ public void SetAndGetDouble()
+ {
+ _settings.SetValue("testDouble", 3.14159);
+ Assert.Equal(3.14159, _settings.GetValue("testDouble", 0d));
+ Assert.Equal(1.5d, _settings.GetValue("missing", 1.5d));
+ }
+
+ [Fact]
+ public void SetAndGetBool()
+ {
+ _settings.SetValue("testBool", true);
+ Assert.True(_settings.GetValue("testBool", false));
+ Assert.False(_settings.GetValue("missing", false));
+ }
+
+ [Fact]
+ public void SetAndGetColor()
+ {
+ var color = Color.FromArgb(255, 100, 150, 200);
+ _settings.SetValue("testColor", color);
+ var retrievedColor = _settings.GetValue("testColor", Color.FromArgb(0, 0, 0, 0));
+
+ Assert.Equal(color.A, retrievedColor.A);
+ Assert.Equal(color.R, retrievedColor.R);
+ Assert.Equal(color.G, retrievedColor.G);
+ Assert.Equal(color.B, retrievedColor.B);
+
+ var defaultColor = Color.FromArgb(10, 20, 30, 40);
+ Assert.Equal(defaultColor, _settings.GetValue("missing", defaultColor));
+ }
+
+ [Fact]
+ public void Remove_DeletesKey()
+ {
+ _settings.SetValue("toRemove", "value");
+ Assert.True(_settings.Contains("toRemove"));
+ _settings.Remove("toRemove");
+ Assert.False(_settings.Contains("toRemove"));
+ }
+
+ [Fact]
+ public void SaveAndLoad_PersistsData()
+ {
+ _settings.SetValue("persistKey", "persistValue");
+ _settings.Save();
+
+ // Create new instance pointing to same file
+ var ctor = typeof(AppSettings).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(string) }, null);
+ var loadedSettings = (AppSettings)ctor.Invoke(new object[] { _tempFile });
+
+ loadedSettings.Load();
+
+ Assert.Equal("persistValue", loadedSettings.GetValue("persistKey", ""));
+ }
+}
diff --git a/LibreHardwareMonitor.Windows.WinUI.Tests/Services/LoggerTests.cs b/LibreHardwareMonitor.Windows.WinUI.Tests/Services/LoggerTests.cs
new file mode 100644
index 0000000..bf2ae14
--- /dev/null
+++ b/LibreHardwareMonitor.Windows.WinUI.Tests/Services/LoggerTests.cs
@@ -0,0 +1,54 @@
+using System;
+using LibreHardwareMonitor.Hardware;
+using LibreHardwareMonitor.Windows.WinUI.Services;
+using Moq;
+using Xunit;
+
+namespace LibreHardwareMonitor.Windows.WinUI.Tests.Services;
+
+public class LoggerTests
+{
+ [Fact]
+ public void Constructor_SubscribesToComputerEvents()
+ {
+ var mockComputer = new Mock();
+
+ var logger = new Logger(mockComputer.Object);
+
+ // We can't directly test if the event was subscribed, but we can verify default properties
+ Assert.Equal(LoggerFileRotation.PerSession, logger.FileRotationMethod);
+ Assert.Equal(TimeSpan.FromSeconds(1), logger.LoggingInterval);
+ }
+
+ [Fact]
+ public void Properties_CanBeSet()
+ {
+ var mockComputer = new Mock();
+ var logger = new Logger(mockComputer.Object)
+ {
+ FileRotationMethod = LoggerFileRotation.Daily,
+ LoggingInterval = TimeSpan.FromMinutes(5)
+ };
+
+ Assert.Equal(LoggerFileRotation.Daily, logger.FileRotationMethod);
+ Assert.Equal(TimeSpan.FromMinutes(5), logger.LoggingInterval);
+ }
+
+ [Fact]
+ public void Log_DoesNotThrow_WhenNoSensors()
+ {
+ var mockComputer = new Mock();
+ var logger = new Logger(mockComputer.Object);
+
+ // We can't fully mock the file system easily here without abstraction,
+ // but if there are no sensors it will just create a basic CSV.
+ // Actually, it tries to create a file in the app directory, which might throw in some environments.
+ // Let's just assure it doesn't crash when interval hasn't passed.
+
+ logger.LoggingInterval = TimeSpan.FromHours(1);
+ logger.Log(); // Should execute
+
+ // Calling again immediately should return early without doing anything
+ logger.Log();
+ }
+}
diff --git a/LibreHardwareMonitor.Windows.WinUI.Tests/Services/UpdateVisitorTests.cs b/LibreHardwareMonitor.Windows.WinUI.Tests/Services/UpdateVisitorTests.cs
new file mode 100644
index 0000000..7e15bc2
--- /dev/null
+++ b/LibreHardwareMonitor.Windows.WinUI.Tests/Services/UpdateVisitorTests.cs
@@ -0,0 +1,59 @@
+using System;
+using LibreHardwareMonitor.Hardware;
+using LibreHardwareMonitor.Windows.WinUI.Services;
+using Moq;
+using Xunit;
+
+namespace LibreHardwareMonitor.Windows.WinUI.Tests.Services;
+
+public class UpdateVisitorTests
+{
+ [Fact]
+ public void VisitComputer_CallsTraverse()
+ {
+ var mockComputer = new Mock();
+ var visitor = new UpdateVisitor();
+
+ visitor.VisitComputer(mockComputer.Object);
+
+ mockComputer.Verify(c => c.Traverse(visitor), Times.Once);
+ }
+
+ [Fact]
+ public void VisitHardware_CallsUpdateAndAcceptsOnSubHardware()
+ {
+ var visitor = new UpdateVisitor();
+
+ var mockHardware = new Mock();
+ var mockSubHardware1 = new Mock();
+ var mockSubHardware2 = new Mock();
+
+ mockHardware.Setup(h => h.SubHardware).Returns(new[] { mockSubHardware1.Object, mockSubHardware2.Object });
+
+ visitor.VisitHardware(mockHardware.Object);
+
+ mockHardware.Verify(h => h.Update(), Times.Once);
+ mockSubHardware1.Verify(h => h.Accept(visitor), Times.Once);
+ mockSubHardware2.Verify(h => h.Accept(visitor), Times.Once);
+ }
+
+ [Fact]
+ public void VisitSensor_DoesNothing()
+ {
+ var visitor = new UpdateVisitor();
+ var mockSensor = new Mock();
+
+ visitor.VisitSensor(mockSensor.Object);
+ // Assert it does not throw
+ }
+
+ [Fact]
+ public void VisitParameter_DoesNothing()
+ {
+ var visitor = new UpdateVisitor();
+ var mockParameter = new Mock();
+
+ visitor.VisitParameter(mockParameter.Object);
+ // Assert it does not throw
+ }
+}
diff --git a/LibreHardwareMonitor.Windows.WinUI.Tests/UnitTest1.cs b/LibreHardwareMonitor.Windows.WinUI.Tests/UnitTest1.cs
new file mode 100644
index 0000000..e0a5243
--- /dev/null
+++ b/LibreHardwareMonitor.Windows.WinUI.Tests/UnitTest1.cs
@@ -0,0 +1,10 @@
+namespace LibreHardwareMonitor.Windows.WinUI.Tests;
+
+public class UnitTest1
+{
+ [Fact]
+ public void Test1()
+ {
+
+ }
+}
diff --git a/LibreHardwareMonitor.Windows.WinUI.Tests/Utilities/SensorFormatterTests.cs b/LibreHardwareMonitor.Windows.WinUI.Tests/Utilities/SensorFormatterTests.cs
new file mode 100644
index 0000000..ea78a72
--- /dev/null
+++ b/LibreHardwareMonitor.Windows.WinUI.Tests/Utilities/SensorFormatterTests.cs
@@ -0,0 +1,193 @@
+using System;
+using LibreHardwareMonitor.Hardware;
+using LibreHardwareMonitor.Windows.WinUI.Utilities;
+using LibreHardwareMonitor.Windows.WinUI.ViewModels;
+using Moq;
+using Xunit;
+
+namespace LibreHardwareMonitor.Windows.WinUI.Tests.Utilities;
+
+public class SensorFormatterTests
+{
+ [Fact]
+ public void GetFormatString_ReturnsCorrectFormat_ForAllSensorTypes()
+ {
+ var mockSensor = new Mock();
+
+ // Test a few specific types
+ mockSensor.Setup(s => s.SensorType).Returns(SensorType.Voltage);
+ Assert.Equal("{0:F3} V", SensorFormatter.GetFormatString(mockSensor.Object));
+
+ mockSensor.Setup(s => s.SensorType).Returns(SensorType.Load);
+ Assert.Equal("{0:F1} %", SensorFormatter.GetFormatString(mockSensor.Object));
+
+ mockSensor.Setup(s => s.SensorType).Returns(SensorType.Temperature);
+ Assert.Equal("{0:F1} \u00B0C", SensorFormatter.GetFormatString(mockSensor.Object));
+
+ mockSensor.Setup(s => s.SensorType).Returns(SensorType.Fan);
+ Assert.Equal("{0:F0} RPM", SensorFormatter.GetFormatString(mockSensor.Object));
+
+ // Test default case
+ mockSensor.Setup(s => s.SensorType).Returns((SensorType)999);
+ Assert.Equal("{0:F1}", SensorFormatter.GetFormatString(mockSensor.Object));
+ }
+
+ [Fact]
+ public void FormatValue_NullValue_ReturnsDash()
+ {
+ var mockSensor = new Mock();
+ var result = SensorFormatter.FormatValue(mockSensor.Object, null, TemperatureUnit.Celsius);
+ Assert.Equal("-", result);
+ }
+
+ [Fact]
+ public void FormatValue_Celsius_ReturnsCelsiusString()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.SensorType).Returns(SensorType.Temperature);
+
+ var result = SensorFormatter.FormatValue(mockSensor.Object, 25.5f, TemperatureUnit.Celsius);
+ Assert.Equal("25.5 \u00B0C", result);
+ }
+
+ [Fact]
+ public void FormatValue_Fahrenheit_ReturnsFahrenheitString()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.SensorType).Returns(SensorType.Temperature);
+
+ var result = SensorFormatter.FormatValue(mockSensor.Object, 25.5f, TemperatureUnit.Fahrenheit);
+ // 25.5 * 1.8 + 32 = 77.9
+ Assert.Equal("77.9 \u00B0F", result);
+ }
+
+ [Fact]
+ public void FormatValue_Throughput_ReturnsCorrectString()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.SensorType).Returns(SensorType.Throughput);
+ mockSensor.Setup(s => s.Name).Returns("Other");
+
+ // Below 1MB
+ var result1 = SensorFormatter.FormatValue(mockSensor.Object, 512f, TemperatureUnit.Celsius);
+ Assert.Equal("0.5 KB/s", result1);
+
+ // Above 1MB
+ var result2 = SensorFormatter.FormatValue(mockSensor.Object, 2097152f, TemperatureUnit.Celsius);
+ Assert.Equal("2.0 MB/s", result2);
+ }
+
+ [Fact]
+ public void FormatValue_Throughput_ConnectionSpeed_ReturnsCorrectString()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.SensorType).Returns(SensorType.Throughput);
+ mockSensor.Setup(s => s.Name).Returns("Connection Speed");
+
+ Assert.Equal("100Mbps", SensorFormatter.FormatValue(mockSensor.Object, 100000000f, TemperatureUnit.Celsius));
+ Assert.Equal("1Gbps", SensorFormatter.FormatValue(mockSensor.Object, 1000000000f, TemperatureUnit.Celsius));
+ Assert.Equal("500 bps", SensorFormatter.FormatValue(mockSensor.Object, 500f, TemperatureUnit.Celsius));
+ Assert.Equal("2.0 Kbps", SensorFormatter.FormatValue(mockSensor.Object, 2048f, TemperatureUnit.Celsius));
+ Assert.Equal("2.0 Mbps", SensorFormatter.FormatValue(mockSensor.Object, 2097152f, TemperatureUnit.Celsius));
+ Assert.Equal("2.0 Gbps", SensorFormatter.FormatValue(mockSensor.Object, 2147483648f, TemperatureUnit.Celsius));
+ }
+
+ [Fact]
+ public void FormatValue_TimeSpan_ReturnsCorrectString()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.SensorType).Returns(SensorType.TimeSpan);
+
+ var result = SensorFormatter.FormatValue(mockSensor.Object, 3600f, TemperatureUnit.Celsius);
+ Assert.Equal("1:00:00", result); // g format for 1 hour
+ }
+
+ [Fact]
+ public void FormatValue_Default_ReturnsFormattedFloat()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.SensorType).Returns((SensorType)999);
+
+ var result = SensorFormatter.FormatValue(mockSensor.Object, 12.34f, TemperatureUnit.Celsius);
+ Assert.Equal("12.3", result);
+ }
+
+ [Fact]
+ public void GetPlotValue_NullValue_ReturnsNull()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.Value).Returns((float?)null);
+
+ var result = SensorFormatter.GetPlotValue(mockSensor.Object, TemperatureUnit.Celsius);
+ Assert.Null(result);
+ }
+
+ [Fact]
+ public void GetPlotValue_TemperatureFahrenheit_ReturnsConvertedValue()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.Value).Returns(25.5f);
+ mockSensor.Setup(s => s.SensorType).Returns(SensorType.Temperature);
+
+ var result = SensorFormatter.GetPlotValue(mockSensor.Object, TemperatureUnit.Fahrenheit);
+ Assert.Equal(77.9, result.Value, 1);
+ }
+
+ [Fact]
+ public void GetPlotValue_OtherSensor_ReturnsRawValue()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.Value).Returns(25.5f);
+ mockSensor.Setup(s => s.SensorType).Returns(SensorType.Voltage);
+
+ var result = SensorFormatter.GetPlotValue(mockSensor.Object, TemperatureUnit.Celsius);
+ Assert.Equal(25.5f, result);
+ }
+
+ // A mock interface combining ISensor and ICriticalSensorLimits for testing
+ public interface ICriticalSensorMock : ISensor, ICriticalSensorLimits, ISensorLimits
+ {
+ }
+
+ [Fact]
+ public void GetToolTip_WithLimits_ReturnsFormattedToolTip()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.SensorType).Returns(SensorType.Temperature);
+ mockSensor.Setup(s => s.CriticalLowLimit).Returns(10f);
+ mockSensor.Setup(s => s.CriticalHighLimit).Returns(90f);
+ mockSensor.Setup(s => s.LowLimit).Returns(20f);
+ mockSensor.Setup(s => s.HighLimit).Returns(80f);
+
+ var result = SensorFormatter.GetToolTip(mockSensor.Object, TemperatureUnit.Celsius);
+
+ Assert.Contains("Critical range: 10.0 \u00B0C to 90.0 \u00B0C.", result);
+ Assert.Contains("Normal range: 20.0 \u00B0C to 80.0 \u00B0C.", result);
+ }
+
+ [Fact]
+ public void GetToolTip_WithOnlyMinLimit_ReturnsFormattedToolTip()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.SensorType).Returns(SensorType.Voltage);
+ mockSensor.Setup(s => s.CriticalLowLimit).Returns(1.0f);
+ mockSensor.Setup(s => s.CriticalHighLimit).Returns((float?)null);
+
+ var result = SensorFormatter.GetToolTip(mockSensor.Object, TemperatureUnit.Celsius);
+
+ Assert.Contains("Minimal critical value: 1.000 V.", result);
+ }
+
+ [Fact]
+ public void GetToolTip_WithOnlyMaxLimit_ReturnsFormattedToolTip()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.SensorType).Returns(SensorType.Voltage);
+ mockSensor.Setup(s => s.CriticalLowLimit).Returns((float?)null);
+ mockSensor.Setup(s => s.CriticalHighLimit).Returns(2.0f);
+
+ var result = SensorFormatter.GetToolTip(mockSensor.Object, TemperatureUnit.Celsius);
+
+ Assert.Contains("Maximal critical value: 2.000 V.", result);
+ }
+}
diff --git a/LibreHardwareMonitor.Windows.WinUI.Tests/Utilities/SensorTypeDisplayTests.cs b/LibreHardwareMonitor.Windows.WinUI.Tests/Utilities/SensorTypeDisplayTests.cs
new file mode 100644
index 0000000..63f9676
--- /dev/null
+++ b/LibreHardwareMonitor.Windows.WinUI.Tests/Utilities/SensorTypeDisplayTests.cs
@@ -0,0 +1,100 @@
+using System;
+using LibreHardwareMonitor.Hardware;
+using LibreHardwareMonitor.Windows.WinUI.Utilities;
+using Xunit;
+
+namespace LibreHardwareMonitor.Windows.WinUI.Tests.Utilities;
+
+public class SensorTypeDisplayTests
+{
+ [Fact]
+ public void GetText_ReturnsCorrectString_ForAllKnownTypes()
+ {
+ Assert.Equal("Voltages", SensorTypeDisplay.GetText(SensorType.Voltage));
+ Assert.Equal("Currents", SensorTypeDisplay.GetText(SensorType.Current));
+ Assert.Equal("Clocks", SensorTypeDisplay.GetText(SensorType.Clock));
+ Assert.Equal("Load", SensorTypeDisplay.GetText(SensorType.Load));
+ Assert.Equal("Temperatures", SensorTypeDisplay.GetText(SensorType.Temperature));
+ Assert.Equal("Fans", SensorTypeDisplay.GetText(SensorType.Fan));
+ Assert.Equal("Flows", SensorTypeDisplay.GetText(SensorType.Flow));
+ Assert.Equal("Controls", SensorTypeDisplay.GetText(SensorType.Control));
+ Assert.Equal("Levels", SensorTypeDisplay.GetText(SensorType.Level));
+ Assert.Equal("Powers", SensorTypeDisplay.GetText(SensorType.Power));
+ Assert.Equal("Data", SensorTypeDisplay.GetText(SensorType.Data));
+ Assert.Equal("Data", SensorTypeDisplay.GetText(SensorType.SmallData));
+ Assert.Equal("Factors", SensorTypeDisplay.GetText(SensorType.Factor));
+ Assert.Equal("Frequencies", SensorTypeDisplay.GetText(SensorType.Frequency));
+ Assert.Equal("Throughput", SensorTypeDisplay.GetText(SensorType.Throughput));
+ Assert.Equal("Times", SensorTypeDisplay.GetText(SensorType.TimeSpan));
+ Assert.Equal("Timings", SensorTypeDisplay.GetText(SensorType.Timing));
+ Assert.Equal("Capacities", SensorTypeDisplay.GetText(SensorType.Energy));
+ Assert.Equal("Noise Levels", SensorTypeDisplay.GetText(SensorType.Noise));
+ Assert.Equal("Conductivities", SensorTypeDisplay.GetText(SensorType.Conductivity));
+ Assert.Equal("Humidity Levels", SensorTypeDisplay.GetText(SensorType.Humidity));
+ }
+
+ [Fact]
+ public void GetText_ReturnsToString_ForUnknownType()
+ {
+ SensorType unknownType = (SensorType)999;
+ Assert.Equal("999", SensorTypeDisplay.GetText(unknownType));
+ }
+
+ [Fact]
+ public void GetGlyph_ReturnsCorrectGlyph_ForAllKnownTypes()
+ {
+ Assert.Equal("\uE945", SensorTypeDisplay.GetGlyph(SensorType.Voltage));
+ Assert.Equal("\uE945", SensorTypeDisplay.GetGlyph(SensorType.Current));
+ Assert.Equal("\uE916", SensorTypeDisplay.GetGlyph(SensorType.Clock));
+ Assert.Equal("\uE9D9", SensorTypeDisplay.GetGlyph(SensorType.Load));
+ Assert.Equal("\uE9CA", SensorTypeDisplay.GetGlyph(SensorType.Temperature));
+ Assert.Equal("\uE9F3", SensorTypeDisplay.GetGlyph(SensorType.Fan));
+ Assert.Equal("\uE9D5", SensorTypeDisplay.GetGlyph(SensorType.Flow));
+ Assert.Equal("\uE713", SensorTypeDisplay.GetGlyph(SensorType.Control));
+ Assert.Equal("\uE9D2", SensorTypeDisplay.GetGlyph(SensorType.Level));
+ Assert.Equal("\uE7E8", SensorTypeDisplay.GetGlyph(SensorType.Power));
+ Assert.Equal("\uE8AB", SensorTypeDisplay.GetGlyph(SensorType.Data));
+ Assert.Equal("\uE8AB", SensorTypeDisplay.GetGlyph(SensorType.SmallData));
+ Assert.Equal("\uE9D2", SensorTypeDisplay.GetGlyph(SensorType.Factor));
+ Assert.Equal("\uE916", SensorTypeDisplay.GetGlyph(SensorType.Frequency));
+ Assert.Equal("\uE9D5", SensorTypeDisplay.GetGlyph(SensorType.Throughput));
+ Assert.Equal("\uE121", SensorTypeDisplay.GetGlyph(SensorType.TimeSpan));
+ Assert.Equal("\uE121", SensorTypeDisplay.GetGlyph(SensorType.Timing));
+ Assert.Equal("\uEBAA", SensorTypeDisplay.GetGlyph(SensorType.Energy));
+ Assert.Equal("\uE767", SensorTypeDisplay.GetGlyph(SensorType.Noise));
+ Assert.Equal("\uE945", SensorTypeDisplay.GetGlyph(SensorType.Conductivity));
+ Assert.Equal("\uE9CA", SensorTypeDisplay.GetGlyph(SensorType.Humidity));
+ }
+
+ [Fact]
+ public void GetGlyph_ReturnsDefault_ForUnknownType()
+ {
+ SensorType unknownType = (SensorType)999;
+ Assert.Equal("\uE9D9", SensorTypeDisplay.GetGlyph(unknownType));
+ }
+
+ [Fact]
+ public void GetHardwareGlyph_ReturnsCorrectGlyph_ForAllKnownTypes()
+ {
+ Assert.Equal("\uE950", SensorTypeDisplay.GetHardwareGlyph(HardwareType.Motherboard));
+ Assert.Equal("\uE950", SensorTypeDisplay.GetHardwareGlyph(HardwareType.SuperIO));
+ Assert.Equal("\uE950", SensorTypeDisplay.GetHardwareGlyph(HardwareType.Cpu));
+ Assert.Equal("\uE8AB", SensorTypeDisplay.GetHardwareGlyph(HardwareType.Memory));
+ Assert.Equal("\uE7F4", SensorTypeDisplay.GetHardwareGlyph(HardwareType.GpuNvidia));
+ Assert.Equal("\uE7F4", SensorTypeDisplay.GetHardwareGlyph(HardwareType.GpuAmd));
+ Assert.Equal("\uE7F4", SensorTypeDisplay.GetHardwareGlyph(HardwareType.GpuIntel));
+ Assert.Equal("\uEDA2", SensorTypeDisplay.GetHardwareGlyph(HardwareType.Storage));
+ Assert.Equal("\uE968", SensorTypeDisplay.GetHardwareGlyph(HardwareType.Network));
+ Assert.Equal("\uE9F3", SensorTypeDisplay.GetHardwareGlyph(HardwareType.Cooler));
+ Assert.Equal("\uE950", SensorTypeDisplay.GetHardwareGlyph(HardwareType.EmbeddedController));
+ Assert.Equal("\uE7E8", SensorTypeDisplay.GetHardwareGlyph(HardwareType.Psu));
+ Assert.Equal("\uEBAA", SensorTypeDisplay.GetHardwareGlyph(HardwareType.Battery));
+ }
+
+ [Fact]
+ public void GetHardwareGlyph_ReturnsDefault_ForUnknownType()
+ {
+ HardwareType unknownType = (HardwareType)999;
+ Assert.Equal("\uE950", SensorTypeDisplay.GetHardwareGlyph(unknownType));
+ }
+}
diff --git a/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/SensorTreeItemViewModelTests.cs b/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/SensorTreeItemViewModelTests.cs
new file mode 100644
index 0000000..c4cc012
--- /dev/null
+++ b/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/SensorTreeItemViewModelTests.cs
@@ -0,0 +1,185 @@
+using System;
+using System.Linq;
+using LibreHardwareMonitor.Hardware;
+using LibreHardwareMonitor.Windows.WinUI.Services;
+using LibreHardwareMonitor.Windows.WinUI.ViewModels;
+using Moq;
+using Xunit;
+using Microsoft.UI.Xaml;
+using Windows.UI;
+
+namespace LibreHardwareMonitor.Windows.WinUI.Tests.ViewModels;
+
+public class SensorTreeItemViewModelTests
+{
+ private AppSettings CreateMockSettings()
+ {
+ return AppSettings.LoadDefault();
+ }
+
+ [Fact]
+ public void CreateRoot_ReturnsRootKind()
+ {
+ var vm = SensorTreeItemViewModel.CreateRoot("Root Node");
+ Assert.Equal(SensorTreeItemKind.Root, vm.Kind);
+ Assert.Equal("Root Node", vm.Text);
+ Assert.Null(vm.Sensor);
+ Assert.Null(vm.Hardware);
+ Assert.Equal("\uE7F4", vm.IconGlyph);
+ Assert.True(vm.IsExpanded);
+ Assert.Empty(vm.Children);
+ }
+
+ [Fact]
+ public void Text_CanBeSetForRoot()
+ {
+ var vm = SensorTreeItemViewModel.CreateRoot("Root");
+ vm.Text = "New Root";
+ Assert.Equal("New Root", vm.Text);
+ }
+
+ [Fact]
+ public void FromHardware_CreatesHierarchy()
+ {
+ var mockHardware = new Mock();
+ mockHardware.Setup(h => h.Name).Returns("My Hardware");
+ mockHardware.Setup(h => h.HardwareType).Returns(HardwareType.Cpu);
+ mockHardware.Setup(h => h.Identifier).Returns(new Identifier("cpu", "0"));
+
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.Name).Returns("Core 0");
+ mockSensor.Setup(s => s.SensorType).Returns(SensorType.Temperature);
+ mockSensor.Setup(s => s.Index).Returns(0);
+ mockSensor.Setup(s => s.Identifier).Returns(new Identifier("cpu", "0", "temperature", "0"));
+
+ mockHardware.Setup(h => h.Sensors).Returns(new[] { mockSensor.Object });
+ mockHardware.Setup(h => h.SubHardware).Returns(Array.Empty());
+
+ var settings = CreateMockSettings();
+ var vm = SensorTreeItemViewModel.FromHardware(mockHardware.Object, settings);
+
+ Assert.Equal(SensorTreeItemKind.Hardware, vm.Kind);
+ Assert.Equal("My Hardware", vm.Text);
+ Assert.Same(mockHardware.Object, vm.Hardware);
+ Assert.Single(vm.Children); // One SensorType group
+
+ var typeGroup = vm.Children[0];
+ Assert.Equal(SensorTreeItemKind.SensorType, typeGroup.Kind);
+ Assert.Equal("Temperatures", typeGroup.Text);
+ Assert.Single(typeGroup.Children); // One sensor
+
+ var sensorVm = typeGroup.Children[0];
+ Assert.Equal(SensorTreeItemKind.Sensor, sensorVm.Kind);
+ Assert.Equal("Core 0", sensorVm.Text);
+ Assert.Same(mockSensor.Object, sensorVm.Sensor);
+ }
+
+ [Fact]
+ public void Plot_SetsPlotPropertyAndSettings()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.Identifier).Returns(new Identifier("cpu", "0"));
+
+ var mockHardware = new Mock();
+ mockHardware.Setup(h => h.Identifier).Returns(new Identifier("hw"));
+ mockHardware.Setup(h => h.Sensors).Returns(new[] { mockSensor.Object });
+ mockHardware.Setup(h => h.SubHardware).Returns(Array.Empty());
+
+ var settings = CreateMockSettings();
+ var vm = SensorTreeItemViewModel.FromHardware(mockHardware.Object, settings);
+
+ // Root has no sensor, so Plot won't do anything
+ vm.Plot = true;
+ Assert.False(vm.Plot);
+
+ // Get SensorVM
+ var sensorVm = vm.Children.First().Children.First();
+ sensorVm.Plot = true;
+ Assert.True(sensorVm.Plot);
+ }
+
+ [Fact]
+ public void PenColor_SetsPropertyAndSettings()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.Identifier).Returns(new Identifier("cpu", "0"));
+
+ var mockHardware = new Mock();
+ mockHardware.Setup(h => h.Sensors).Returns(new[] { mockSensor.Object });
+ mockHardware.Setup(h => h.Identifier).Returns(new Identifier("hw"));
+ mockHardware.Setup(h => h.SubHardware).Returns(Array.Empty());
+
+ var settings = CreateMockSettings();
+ var vm = SensorTreeItemViewModel.FromHardware(mockHardware.Object, settings);
+ var sensorVm = vm.Children.First().Children.First();
+
+ Assert.Null(sensorVm.PenColor);
+
+ var color = Color.FromArgb(255, 100, 100, 100);
+ sensorVm.PenColor = color;
+ Assert.Equal(color, sensorVm.PenColor);
+
+ sensorVm.PenColor = null;
+ Assert.Null(sensorVm.PenColor);
+ }
+
+ [Fact]
+ public void SetShowHiddenSensors_UpdatesVisibility()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.Identifier).Returns(new Identifier("cpu", "0"));
+
+ var mockHardware = new Mock();
+ mockHardware.Setup(h => h.Sensors).Returns(new[] { mockSensor.Object });
+ mockHardware.Setup(h => h.Identifier).Returns(new Identifier("hw"));
+ mockHardware.Setup(h => h.SubHardware).Returns(Array.Empty());
+
+ var settings = CreateMockSettings();
+ var vm = SensorTreeItemViewModel.FromHardware(mockHardware.Object, settings);
+ var typeGroup = vm.Children.First();
+ var sensorVm = typeGroup.Children.First();
+
+ sensorVm.IsVisible = false;
+ Assert.Equal(Visibility.Collapsed, sensorVm.RowVisibility);
+
+ // typeGroup doesn't update automatically in ViewModel when child changes, so skip assert
+
+ vm.SetShowHiddenSensors(true);
+
+ Assert.Equal(Visibility.Visible, sensorVm.RowVisibility);
+ Assert.Equal(Visibility.Visible, typeGroup.RowVisibility);
+ }
+
+ [Fact]
+ public void SetColumnVisibility_UpdatesAllChildren()
+ {
+ var root = SensorTreeItemViewModel.CreateRoot("Root");
+
+ root.SetColumnVisibility(false, true, true);
+
+ Assert.Equal(Visibility.Collapsed, root.ValueColumnVisibility);
+ Assert.Equal(Visibility.Visible, root.MinColumnVisibility);
+ Assert.Equal(Visibility.Visible, root.MaxColumnVisibility);
+ }
+
+ [Fact]
+ public void RefreshValues_UpdatesProperties()
+ {
+ var mockSensor = new Mock();
+ mockSensor.Setup(s => s.Identifier).Returns(new Identifier("cpu", "0"));
+ mockSensor.Setup(s => s.Value).Returns(50f);
+
+ var mockHardware = new Mock();
+ mockHardware.Setup(h => h.Sensors).Returns(new[] { mockSensor.Object });
+ mockHardware.Setup(h => h.Identifier).Returns(new Identifier("hw"));
+ mockHardware.Setup(h => h.SubHardware).Returns(Array.Empty());
+
+ var vm = SensorTreeItemViewModel.FromHardware(mockHardware.Object, CreateMockSettings());
+ var sensorVm = vm.Children.First().Children.First();
+
+ mockSensor.Setup(s => s.Value).Returns(60f); // change value
+ sensorVm.RefreshValues(); // This triggers PropertyChanged
+
+ Assert.Contains("60.0", sensorVm.Value);
+ }
+}
diff --git a/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/SimpleViewModelsTests.cs b/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/SimpleViewModelsTests.cs
new file mode 100644
index 0000000..acf9098
--- /dev/null
+++ b/LibreHardwareMonitor.Windows.WinUI.Tests/ViewModels/SimpleViewModelsTests.cs
@@ -0,0 +1,101 @@
+using System;
+using System.ComponentModel;
+using Windows.UI;
+using LibreHardwareMonitor.Windows.WinUI.ViewModels;
+using Xunit;
+
+namespace LibreHardwareMonitor.Windows.WinUI.Tests.ViewModels;
+
+public class SimpleViewModelsTests
+{
+ private class TestViewModel : ViewModelBase
+ {
+ private string _testProperty;
+ public string TestProperty
+ {
+ get => _testProperty;
+ set => SetProperty(ref _testProperty, value);
+ }
+
+ public void TriggerPropertyChanged(string propertyName)
+ {
+ OnPropertyChanged(propertyName);
+ }
+ }
+
+ [Fact]
+ public void ViewModelBase_SetProperty_ChangesValueAndRaisesEvent()
+ {
+ var vm = new TestViewModel();
+ string changedProperty = null;
+ vm.PropertyChanged += (sender, args) => changedProperty = args.PropertyName;
+
+ // Change value
+ vm.TestProperty = "NewValue";
+ vm.TestProperty = "NewValue";
+
+ Assert.Equal("NewValue", vm.TestProperty);
+ Assert.Equal(nameof(TestViewModel.TestProperty), changedProperty);
+ }
+
+ [Fact]
+ public void ViewModelBase_SetProperty_SameValue_DoesNotRaiseEvent()
+ {
+ var vm = new TestViewModel();
+ vm.TestProperty = "Value";
+
+ bool eventRaised = false;
+ vm.PropertyChanged += (sender, args) => eventRaised = true;
+
+ vm.TestProperty = "Value"; // Set same value
+
+ Assert.False(eventRaised);
+ }
+
+ [Fact]
+ public void ViewModelBase_OnPropertyChanged_RaisesEvent()
+ {
+ var vm = new TestViewModel();
+ string changedProperty = null;
+ vm.PropertyChanged += (sender, args) => changedProperty = args.PropertyName;
+
+ vm.TriggerPropertyChanged("CustomProperty");
+
+ Assert.Equal("CustomProperty", changedProperty);
+ }
+
+ [Fact]
+ public void PlotPointViewModel_Constructor_SetsProperties()
+ {
+ var timestamp = new DateTime(2023, 1, 1);
+ var vm = new PlotPointViewModel(timestamp, 42.5);
+
+ Assert.Equal(timestamp, vm.Timestamp);
+ Assert.Equal(42.5, vm.Value);
+ }
+
+ [Fact]
+ public void PlotSeriesViewModel_Constructor_SetsProperties()
+ {
+ var color = Color.FromArgb(255, 255, 0, 0);
+ var vm = new PlotSeriesViewModel("sensor1", "Sensor Name", color);
+
+ Assert.Equal("sensor1", vm.SensorIdentifier);
+ Assert.Equal("Sensor Name", vm.Name);
+ Assert.Equal(color, vm.Color);
+ Assert.NotNull(vm.Points);
+ Assert.Empty(vm.Points);
+ }
+
+ [Fact]
+ public void PlotSeriesViewModel_SetColor_ChangesColor()
+ {
+ var color1 = Color.FromArgb(255, 255, 0, 0);
+ var color2 = Color.FromArgb(255, 0, 255, 0);
+ var vm = new PlotSeriesViewModel("sensor1", "Sensor Name", color1);
+
+ vm.Color = color2;
+
+ Assert.Equal(color2, vm.Color);
+ }
+}
diff --git a/LibreHardwareMonitor.sln b/LibreHardwareMonitor.sln
index 86aaccc..3d7aacc 100644
--- a/LibreHardwareMonitor.sln
+++ b/LibreHardwareMonitor.sln
@@ -3,12 +3,12 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.11018.127
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibreHardwareMonitor.Windows.WinUI", "LibreHardwareMonitor.Windows.WinUI\LibreHardwareMonitor.Windows.WinUI.csproj", "{49D41E03-7028-4B28-9F5E-4558D0E26EA2}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibreHardwareMonitorLib", "LibreHardwareMonitorLib\LibreHardwareMonitorLib.csproj", "{B0397530-545A-471D-BB74-027AE456DF1A}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibreHardwareMonitor.Windows.Forms", "LibreHardwareMonitor.Windows.Forms\LibreHardwareMonitor.Windows.Forms.csproj", "{F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}"
-EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibreHardwareMonitor.Windows.WinUI", "LibreHardwareMonitor.Windows.WinUI\LibreHardwareMonitor.Windows.WinUI.csproj", "{49D41E03-7028-4B28-9F5E-4558D0E26EA2}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibreHardwareMonitorLib", "LibreHardwareMonitorLib\LibreHardwareMonitorLib.csproj", "{B0397530-545A-471D-BB74-027AE456DF1A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LibreHardwareMonitor.Windows.Forms", "LibreHardwareMonitor.Windows.Forms\LibreHardwareMonitor.Windows.Forms.csproj", "{F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aga.Controls", "Aga.Controls\Aga.Controls.csproj", "{E73BB233-D88B-44A7-A98F-D71EE158381D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Controls", "Controls", "{23F54DBD-8998-4E22-A4E4-60F4F77F9B65}"
@@ -19,64 +19,100 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibreHardwareMonitorLib.Tests", "LibreHardwareMonitorLib.Tests\LibreHardwareMonitorLib.Tests.csproj", "{AF286D81-C1D0-47B2-AD60-9F60CAE602F9}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
+ Debug|Any CPU = Debug|Any CPU
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
+ Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {B0397530-545A-471D-BB74-027AE456DF1A}.Debug|ARM64.ActiveCfg = Debug|ARM64
- {B0397530-545A-471D-BB74-027AE456DF1A}.Debug|ARM64.Build.0 = Debug|ARM64
- {B0397530-545A-471D-BB74-027AE456DF1A}.Debug|x64.ActiveCfg = Debug|x64
- {B0397530-545A-471D-BB74-027AE456DF1A}.Debug|x64.Build.0 = Debug|x64
- {B0397530-545A-471D-BB74-027AE456DF1A}.Debug|x86.ActiveCfg = Debug|x86
- {B0397530-545A-471D-BB74-027AE456DF1A}.Debug|x86.Build.0 = Debug|x86
- {B0397530-545A-471D-BB74-027AE456DF1A}.Release|ARM64.ActiveCfg = Release|ARM64
- {B0397530-545A-471D-BB74-027AE456DF1A}.Release|ARM64.Build.0 = Release|ARM64
- {B0397530-545A-471D-BB74-027AE456DF1A}.Release|x64.ActiveCfg = Release|x64
- {B0397530-545A-471D-BB74-027AE456DF1A}.Release|x64.Build.0 = Release|x64
- {B0397530-545A-471D-BB74-027AE456DF1A}.Release|x86.ActiveCfg = Release|x86
- {B0397530-545A-471D-BB74-027AE456DF1A}.Release|x86.Build.0 = Release|x86
- {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Debug|ARM64.ActiveCfg = Debug|ARM64
- {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Debug|ARM64.Build.0 = Debug|ARM64
- {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Debug|x64.ActiveCfg = Debug|x64
- {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Debug|x64.Build.0 = Debug|x64
- {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Debug|x86.ActiveCfg = Debug|x86
- {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Debug|x86.Build.0 = Debug|x86
- {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|ARM64.ActiveCfg = Release|ARM64
- {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|ARM64.Build.0 = Release|ARM64
- {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|x64.ActiveCfg = Release|x64
- {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|x64.Build.0 = Release|x64
- {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|x86.ActiveCfg = Release|x86
- {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|x86.Build.0 = Release|x86
{49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Debug|ARM64.ActiveCfg = Debug|ARM64
{49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Debug|ARM64.Build.0 = Debug|ARM64
{49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Debug|x64.ActiveCfg = Debug|x64
{49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Debug|x64.Build.0 = Debug|x64
{49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Debug|x86.ActiveCfg = Debug|x86
{49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Debug|x86.Build.0 = Debug|x86
+ {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Release|ARM64.ActiveCfg = Release|ARM64
{49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Release|ARM64.Build.0 = Release|ARM64
{49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Release|x64.ActiveCfg = Release|x64
{49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Release|x64.Build.0 = Release|x64
{49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Release|x86.ActiveCfg = Release|x86
{49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Release|x86.Build.0 = Release|x86
+ {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {49D41E03-7028-4B28-9F5E-4558D0E26EA2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Debug|ARM64.Build.0 = Debug|ARM64
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Debug|x64.ActiveCfg = Debug|x64
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Debug|x64.Build.0 = Debug|x64
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Debug|x86.ActiveCfg = Debug|x86
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Debug|x86.Build.0 = Debug|x86
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Release|ARM64.ActiveCfg = Release|ARM64
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Release|ARM64.Build.0 = Release|ARM64
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Release|x64.ActiveCfg = Release|x64
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Release|x64.Build.0 = Release|x64
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Release|x86.ActiveCfg = Release|x86
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Release|x86.Build.0 = Release|x86
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B0397530-545A-471D-BB74-027AE456DF1A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Debug|ARM64.Build.0 = Debug|ARM64
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Debug|x64.ActiveCfg = Debug|x64
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Debug|x64.Build.0 = Debug|x64
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Debug|x86.ActiveCfg = Debug|x86
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Debug|x86.Build.0 = Debug|x86
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|ARM64.ActiveCfg = Release|ARM64
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|ARM64.Build.0 = Release|ARM64
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|x64.ActiveCfg = Release|x64
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|x64.Build.0 = Release|x64
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|x86.ActiveCfg = Release|x86
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|x86.Build.0 = Release|x86
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F5E0C1F7-9E9B-46F2-AC88-8C9C1C923880}.Release|Any CPU.Build.0 = Release|Any CPU
{E73BB233-D88B-44A7-A98F-D71EE158381D}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E73BB233-D88B-44A7-A98F-D71EE158381D}.Debug|ARM64.Build.0 = Debug|ARM64
{E73BB233-D88B-44A7-A98F-D71EE158381D}.Debug|x64.ActiveCfg = Debug|x64
{E73BB233-D88B-44A7-A98F-D71EE158381D}.Debug|x64.Build.0 = Debug|x64
{E73BB233-D88B-44A7-A98F-D71EE158381D}.Debug|x86.ActiveCfg = Debug|x86
{E73BB233-D88B-44A7-A98F-D71EE158381D}.Debug|x86.Build.0 = Debug|x86
+ {E73BB233-D88B-44A7-A98F-D71EE158381D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E73BB233-D88B-44A7-A98F-D71EE158381D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E73BB233-D88B-44A7-A98F-D71EE158381D}.Release|ARM64.ActiveCfg = Release|ARM64
{E73BB233-D88B-44A7-A98F-D71EE158381D}.Release|ARM64.Build.0 = Release|ARM64
{E73BB233-D88B-44A7-A98F-D71EE158381D}.Release|x64.ActiveCfg = Release|x64
{E73BB233-D88B-44A7-A98F-D71EE158381D}.Release|x64.Build.0 = Release|x64
{E73BB233-D88B-44A7-A98F-D71EE158381D}.Release|x86.ActiveCfg = Release|x86
{E73BB233-D88B-44A7-A98F-D71EE158381D}.Release|x86.Build.0 = Release|x86
+ {E73BB233-D88B-44A7-A98F-D71EE158381D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E73BB233-D88B-44A7-A98F-D71EE158381D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Debug|x64.Build.0 = Debug|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Debug|x86.Build.0 = Debug|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Release|ARM64.Build.0 = Release|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Release|x64.ActiveCfg = Release|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Release|x64.Build.0 = Release|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Release|x86.ActiveCfg = Release|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Release|x86.Build.0 = Release|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AF286D81-C1D0-47B2-AD60-9F60CAE602F9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/LibreHardwareMonitorLib.Tests/Hardware/ComputerTests.cs b/LibreHardwareMonitorLib.Tests/Hardware/ComputerTests.cs
new file mode 100644
index 0000000..7db675d
--- /dev/null
+++ b/LibreHardwareMonitorLib.Tests/Hardware/ComputerTests.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Threading.Tasks;
+using LibreHardwareMonitor.Hardware;
+using Xunit;
+
+namespace LibreHardwareMonitorLib.Tests.Hardware;
+
+public class ComputerTests
+{
+ [Fact]
+ public void OpenAndClose_WorksCorrectly_AndCompletesHardwareDiscoveryTask()
+ {
+ var computer = new Computer
+ {
+ IsCpuEnabled = false,
+ IsGpuEnabled = false,
+ IsMemoryEnabled = false,
+ IsMotherboardEnabled = false,
+ IsControllerEnabled = false,
+ IsNetworkEnabled = false,
+ IsStorageEnabled = false,
+ IsBatteryEnabled = false,
+ IsPsuEnabled = false,
+ IsPowerMonitorEnabled = false
+ };
+
+ // HardwareDiscoveryTask should be available
+ Assert.NotNull(computer.HardwareDiscoveryTask);
+
+ // Open the computer
+ computer.Open();
+
+ // HardwareDiscoveryTask should complete because all are disabled, so no long-running task
+ Assert.True(computer.HardwareDiscoveryTask.IsCompleted);
+
+ // Close the computer
+ computer.Close();
+
+ // Ensure it doesn't throw and remains in a good state
+ Assert.True(computer.HardwareDiscoveryTask.IsCompleted);
+ }
+
+ [Fact]
+ public async Task OpenAsync_WorksCorrectly_AndCompletesHardwareDiscoveryTask()
+ {
+ var computer = new Computer
+ {
+ IsCpuEnabled = false,
+ IsGpuEnabled = false,
+ IsMemoryEnabled = false,
+ IsMotherboardEnabled = false,
+ IsControllerEnabled = false,
+ IsNetworkEnabled = false,
+ IsStorageEnabled = false,
+ IsBatteryEnabled = false,
+ IsPsuEnabled = false,
+ IsPowerMonitorEnabled = false
+ };
+
+ await computer.OpenAsync();
+
+ // The HardwareDiscoveryTask should complete successfully.
+ Assert.True(computer.HardwareDiscoveryTask.IsCompletedSuccessfully);
+
+ computer.Close();
+ }
+
+ [Fact]
+ public void Reset_ClearsHardwareAndCompletesDiscovery()
+ {
+ var computer = new Computer
+ {
+ IsCpuEnabled = false
+ };
+
+ computer.Open();
+ Assert.True(computer.HardwareDiscoveryTask.IsCompleted);
+
+ computer.Reset();
+
+ Assert.Empty(computer.Hardware);
+ Assert.True(computer.HardwareDiscoveryTask.IsCompleted);
+ }
+}
diff --git a/LibreHardwareMonitorLib.Tests/LibreHardwareMonitorLib.Tests.csproj b/LibreHardwareMonitorLib.Tests/LibreHardwareMonitorLib.Tests.csproj
new file mode 100644
index 0000000..a4eb82d
--- /dev/null
+++ b/LibreHardwareMonitorLib.Tests/LibreHardwareMonitorLib.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LibreHardwareMonitorLib/Hardware/Computer.cs b/LibreHardwareMonitorLib/Hardware/Computer.cs
index ff6eb40..6d0425b 100644
--- a/LibreHardwareMonitorLib/Hardware/Computer.cs
+++ b/LibreHardwareMonitorLib/Hardware/Computer.cs
@@ -1,4 +1,4 @@
-// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
+// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
// Partial Copyright (C) Michael Möller and Contributors.
@@ -68,7 +68,7 @@ public class Computer : IComputer
private SMBios _smbios;
private bool _storageEnabled;
private CancellationTokenSource _deferredGroupCancellationTokenSource;
- private TaskCompletionSource _deferredGroupCompletionSource = CreateCompletedTaskCompletionSource();
+ private TaskCompletionSource