diff --git a/src/Filee.App/ViewModels/EngineSetupViewModel.cs b/src/Filee.App/ViewModels/EngineSetupViewModel.cs index 1cd418ff1..9475b91d4 100644 --- a/src/Filee.App/ViewModels/EngineSetupViewModel.cs +++ b/src/Filee.App/ViewModels/EngineSetupViewModel.cs @@ -83,10 +83,15 @@ private void Start(IReadOnlyList packages) [RelayCommand] private void Close() + { + Release(); + CloseRequested?.Invoke(this, EventArgs.Empty); + } + + internal void Release() { foreach (var package in Packages) package.PropertyChanged -= OnPackageChanged; - CloseRequested?.Invoke(this, EventArgs.Empty); } private void OnPackageChanged(object? sender, PropertyChangedEventArgs e) diff --git a/src/Filee.App/Views/EngineSetupWindow.axaml.cs b/src/Filee.App/Views/EngineSetupWindow.axaml.cs index 96ea33d60..2d0351c85 100644 --- a/src/Filee.App/Views/EngineSetupWindow.axaml.cs +++ b/src/Filee.App/Views/EngineSetupWindow.axaml.cs @@ -22,6 +22,12 @@ public EngineSetupWindow() if (DataContext is EngineSetupViewModel vm) vm.CloseRequested += (_, _) => Close(); }; + Closed += (_, _) => + { + // The title bar closes the window without running the command. + if (DataContext is EngineSetupViewModel vm) + vm.Release(); + }; Opened += (_, _) => FitToScreen(); // Progress bars make the cards taller while engines install. SizeChanged += (_, e) => KeepInside(e.NewSize.Height); diff --git a/tests/Filee.App.Tests/EngineSetupReleaseTests.cs b/tests/Filee.App.Tests/EngineSetupReleaseTests.cs new file mode 100644 index 000000000..840414987 --- /dev/null +++ b/tests/Filee.App.Tests/EngineSetupReleaseTests.cs @@ -0,0 +1,278 @@ +using System.ComponentModel; +using System.Reflection; +using System.Runtime.CompilerServices; +using Avalonia.Controls; +using Avalonia.Headless; +using Avalonia.Headless.XUnit; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Threading; +using Avalonia.VisualTree; +using Filee.App.Services; +using Filee.App.ViewModels; +using Filee.App.Views; +using Filee.Core.Localization; + +namespace Filee.App.Tests; + +[Collection("MainWindow")] +public class EngineSetupReleaseTests +{ + private static readonly FieldInfo SetupWindow = typeof(WindowService) + .GetField("_engineSetup", BindingFlags.Instance | BindingFlags.NonPublic)!; + private static readonly FieldInfo PackageChanged = typeof(EnginePackageState).BaseType! + .GetField("PropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic)!; + + [AvaloniaTheory] + [InlineData(false)] + [InlineData(true)] + public void Closing_setup_releases_the_window_and_view_model(bool command) + { + TestServices.EnsureInitialized("en"); + var service = AppHost.Get(); + var states = SavePackages(); + var baseline = HandlerCounts(); + var closed = new List<(WeakReference Window, WeakReference Model)>(); + try + { + for (var i = 0; i < 3; i++) + { + closed.Add(OpenAndClose(service, command ? "command" : "direct", baseline)); + AssertCollected(closed); + } + } + finally + { + (SetupWindow.GetValue(service) as EngineSetupWindow)?.Close(); + RestorePackages(states); + } + } + + [AvaloniaTheory] + [InlineData("direct")] + [InlineData("command")] + [InlineData("escape")] + public void Directly_created_setup_releases_on_close(string path) + { + TestServices.EnsureInitialized("en"); + var states = SavePackages(); + var baseline = HandlerCounts(); + try + { + var closed = OpenAndClose(null, path, baseline); + AssertCollected([closed]); + } + finally + { + RestorePackages(states); + } + } + + [AvaloniaFact] + public void Releasing_setup_twice_preserves_other_subscribers() + { + TestServices.EnsureInitialized("en"); + var states = SavePackages(); + var baseline = HandlerCounts(); + var other = NewModel(); + var target = NewModel(); + try + { + AssertHandlers(baseline, 2, target); + target.Release(); + target.Release(); + AssertHandlers(baseline, 1, other); + Assert.All(target.Packages, p => Assert.DoesNotContain(SetupHandlers(p), d => ReferenceEquals(d.Target, target))); + } + finally + { + target.Release(); + other.Release(); + RestorePackages(states); + } + AssertHandlers(baseline, 0); + } + + [AvaloniaFact] + public void Open_setup_keeps_selection_and_close_text_in_sync() + { + TestServices.EnsureInitialized("en"); + var states = SavePackages(); + var baseline = HandlerCounts(); + var vm = NewModel(); + var window = new EngineSetupWindow { DataContext = vm }; + try + { + window.Show(); + Pump(); + var package = vm.Packages.First(p => p.CanInstall); + var check = window.GetVisualDescendants().OfType() + .Single(c => ReferenceEquals(c.DataContext, package)); + var install = window.FindControl