From ed3802a3975e0964fcd618ce82ed0a21cefc973b Mon Sep 17 00:00:00 2001 From: raven <248401188+r4venrudy@users.noreply.github.com> Date: Mon, 12 Oct 2026 12:09:24 +0200 Subject: [PATCH] Fix restart prompt when loading text settings --- .../Views/Controls/Settings/TextboxCard.cs | 4 + src/UniGetUI.Tests/TextboxCardTests.cs | 104 ++++++++++++++++++ src/UniGetUI.Tests/UniGetUI.Tests.csproj | 1 + 3 files changed, 109 insertions(+) create mode 100644 src/UniGetUI.Tests/TextboxCardTests.cs diff --git a/src/UniGetUI.Avalonia/Views/Controls/Settings/TextboxCard.cs b/src/UniGetUI.Avalonia/Views/Controls/Settings/TextboxCard.cs index 2d4d7cc5f3..7f97d4c3dc 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/Settings/TextboxCard.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/Settings/TextboxCard.cs @@ -110,6 +110,10 @@ public void SaveValue() if (CoreSettings.ResolveKey(setting_name).Contains("File")) sanitizedText = CoreTools.MakeValidFileName(sanitizedText); + // TextChanged is queued, so loading a setting can reach this handler too. + if (sanitizedText == CoreSettings.GetValue(setting_name)) + return; + if (sanitizedText != "") CoreSettings.SetValue(setting_name, sanitizedText); else diff --git a/src/UniGetUI.Tests/TextboxCardTests.cs b/src/UniGetUI.Tests/TextboxCardTests.cs new file mode 100644 index 0000000000..d6aa2d6e85 --- /dev/null +++ b/src/UniGetUI.Tests/TextboxCardTests.cs @@ -0,0 +1,104 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Headless; +using Avalonia.Threading; +using CommunityToolkit.Mvvm.Input; +using UniGetUI.Avalonia.Views.Controls.Settings; +using UniGetUI.Core.Data; +using CoreSettings = UniGetUI.Core.SettingsEngine.Settings; + +namespace UniGetUI.Tests; + +public sealed class TextboxCardTests : IDisposable +{ + private readonly HeadlessUnitTestSession _session = HeadlessUnitTestSession.StartNew(typeof(Application)); + private readonly string _testRoot = Path.Combine(Path.GetTempPath(), nameof(TextboxCardTests), Guid.NewGuid().ToString("N")); + + public TextboxCardTests() + { + CoreData.TEST_DataDirectoryOverride = Path.Combine(_testRoot, "Data"); + Directory.CreateDirectory(CoreData.UniGetUIUserConfigurationDirectory); + CoreSettings.ResetSettings(); + } + + public void Dispose() + { + _session.Dispose(); + CoreSettings.ResetSettings(); + CoreData.TEST_DataDirectoryOverride = null; + Directory.Delete(_testRoot, recursive: true); + } + + [Theory] + [InlineData("")] + [InlineData("https://example.com/icons.json")] + public Task LoadingSavedValueDoesNotNotify(string savedValue) => _session.Dispatch(() => + { + CoreSettings.SetValue(CoreSettings.K.IconDataBaseURL, savedValue); + int events = 0; + int commands = 0; + var card = new TextboxCard + { + ValueChangedCommand = new RelayCommand(() => commands++), + }; + card.ValueChanged += (_, _) => events++; + card.SettingName = CoreSettings.K.IconDataBaseURL; + + // Avalonia posts TextChanged, so loading the value can reach handlers attached afterwards. + Dispatcher.UIThread.RunJobs(); + + Assert.Equal(0, events); + Assert.Equal(0, commands); + Assert.Equal(savedValue, CoreSettings.GetValue(CoreSettings.K.IconDataBaseURL)); + }, CancellationToken.None); + + [Fact] + public Task EditingAndClearingValueSavesAndNotifiesOncePerChange() => _session.Dispatch(() => + { + var card = new TextboxCard { SettingName = CoreSettings.K.IconDataBaseURL }; + Dispatcher.UIThread.RunJobs(); + int events = 0; + int commands = 0; + card.ValueChanged += (_, _) => events++; + card.ValueChangedCommand = new RelayCommand(() => commands++); + TextBox textBox = Assert.Single(Assert.IsType(card.Content).Children.OfType()); + + textBox.Text = "https://example.com/icons.json"; + Dispatcher.UIThread.RunJobs(); + card.SaveValue(); + + Assert.Equal(textBox.Text, CoreSettings.GetValue(CoreSettings.K.IconDataBaseURL)); + Assert.Equal(1, events); + Assert.Equal(1, commands); + + textBox.Text = ""; + Dispatcher.UIThread.RunJobs(); + card.SaveValue(); + + Assert.Equal("", CoreSettings.GetValue(CoreSettings.K.IconDataBaseURL)); + Assert.False(CoreSettings.Get(CoreSettings.K.IconDataBaseURL)); + Assert.Equal(2, events); + Assert.Equal(2, commands); + }, CancellationToken.None); + + [Fact] + public Task NumericFilteringNotifiesOnlyWhenSanitizedValueChanges() => _session.Dispatch(() => + { + CoreSettings.SetValue(CoreSettings.K.IconDataBaseURL, "12"); + var card = new TextboxCard { SettingName = CoreSettings.K.IconDataBaseURL, IsNumericOnly = true }; + Dispatcher.UIThread.RunJobs(); + int changes = 0; + card.ValueChanged += (_, _) => changes++; + TextBox textBox = Assert.Single(Assert.IsType(card.Content).Children.OfType()); + + textBox.Text = "1a2"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal("12", textBox.Text); + Assert.Equal(0, changes); + + textBox.Text = "3a4"; + Dispatcher.UIThread.RunJobs(); + Assert.Equal("34", CoreSettings.GetValue(CoreSettings.K.IconDataBaseURL)); + Assert.Equal(1, changes); + }, CancellationToken.None); +} diff --git a/src/UniGetUI.Tests/UniGetUI.Tests.csproj b/src/UniGetUI.Tests/UniGetUI.Tests.csproj index 9085f908c3..011e8efef1 100644 --- a/src/UniGetUI.Tests/UniGetUI.Tests.csproj +++ b/src/UniGetUI.Tests/UniGetUI.Tests.csproj @@ -13,6 +13,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive