diff --git a/ILSpy.Tests/Options/OptionsPageScrollReachTests.cs b/ILSpy.Tests/Options/OptionsPageScrollReachTests.cs new file mode 100644 index 000000000..94300190c --- /dev/null +++ b/ILSpy.Tests/Options/OptionsPageScrollReachTests.cs @@ -0,0 +1,108 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Linq; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Headless.NUnit; +using Avalonia.Threading; +using Avalonia.VisualTree; + +using AwesomeAssertions; + +using ICSharpCode.ILSpy.Properties; + +using ILSpy.AppEnv; +using ILSpy.Commands; +using ILSpy.Docking; +using ILSpy.Options; +using ILSpy.ViewModels; +using ILSpy.Views; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests; + +[TestFixture] +public class OptionsPageScrollReachTests +{ + [AvaloniaTest] + public void Last_Item_In_Display_Panel_Is_Reachable_At_Max_Scroll() + { + // Regression for the "Reset-to-defaults border obscures last setting" bug: + // before the fix, the outer StackPanel's measured DesiredHeight didn't include + // the trailing HeaderedContentControl's inner content (the "Sort results by + // fitness" checkbox), so ScrollViewer.Extent was short by ~30 pixels and the + // checkbox couldn't be scrolled into view. Padding on the inner Border (rather + // than the ScrollViewer, where vertical padding is collapsed) forces the + // measure pass to include all children plus breathing room. + var window = AppComposition.Current.GetExport(); + window.Width = 900; + window.Height = 600; + window.Show(); + Dispatcher.UIThread.RunJobs(); + + var registry = AppComposition.Current.GetExport(); + var command = registry.Commands + .Single(c => c.Metadata.Header == nameof(Resources._Options)) + .CreateExport().Value; + command.Execute(null); + Dispatcher.UIThread.RunJobs(); + + var view = window.GetVisualDescendants().OfType().Single(); + var model = (OptionsPageModel)((ContentTabPage)((MainWindowViewModel)window.DataContext!) + .DockWorkspace.Documents!.VisibleDockables! + .OfType().Single(t => t.Content is OptionsPageModel)).Content!; + model.SelectedPage = model.Pages.OfType().Single(); + Dispatcher.UIThread.RunJobs(); + + // Each panel now declares its own ScrollViewer (so per-tab scroll offset is + // independent), so the visual tree may contain multiple ScrollViewer instances + // — one per panel that's been materialized at least once. Pick the visible one. + var scrollViewer = view.GetVisualDescendants().OfType() + .First(sv => sv.IsEffectivelyVisible && sv.Bounds.Height > 0); + scrollViewer.Offset = new Vector(0, scrollViewer.Extent.Height); + Dispatcher.UIThread.RunJobs(); + + // The "Sort results by fitness" checkbox sits inside the last HeaderedContentControl + // of the Display panel. After scrolling to max, its rendered bottom edge in + // window-coordinate space must sit at or above the Reset-to-defaults border's top. + // Scope the search inside the visible ScrollViewer in case other panels materialized + // a stale copy of the same checkbox label. + var sortCheckbox = scrollViewer.GetVisualDescendants().OfType() + .FirstOrDefault(cb => cb.Content?.ToString() == Resources.SortResultsFitness); + sortCheckbox.Should().NotBeNull( + "the Display panel must expose a 'Sort results by fitness' checkbox; if this fails the " + + "panel structure changed and the test needs updating, not just the assertion below"); + + var resetBorder = view.GetVisualDescendants().OfType() + .Single(b => b.GetVisualDescendants().OfType