mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
217 lines
8.1 KiB
217 lines
8.1 KiB
// Copyright (c) 2026 Siegfried Pammer |
|
// |
|
// 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. |
|
|
|
#if DEBUG |
|
|
|
using System; |
|
using System.Collections.Generic; |
|
using System.Linq; |
|
using System.Threading.Tasks; |
|
|
|
using Avalonia; |
|
using Avalonia.Controls; |
|
using Avalonia.Headless.NUnit; |
|
using Avalonia.Threading; |
|
using Avalonia.VisualTree; |
|
|
|
using AwesomeAssertions; |
|
|
|
using ICSharpCode.Decompiler.DebugSteps; |
|
|
|
using ICSharpCode.ILSpy.ViewModels; |
|
using ICSharpCode.ILSpy.Views; |
|
|
|
using NUnit.Framework; |
|
|
|
namespace ICSharpCode.ILSpy.Tests.Views; |
|
|
|
/// <summary> |
|
/// The Debug Steps filter box must be a transient view on the step tree: it may expand and |
|
/// hide rows while active, but expansion states the user established before filtering and |
|
/// the selection made while filtering must survive clearing it. Rows are located through the |
|
/// visual tree (header text -> nearest TreeViewItem) and user gestures are simulated with |
|
/// SetCurrentValue, which is what every real expander gesture (toggle-arrow, double-tap, |
|
/// arrow keys) ends in — a plain local SetValue would mask style-driven bindings and test a |
|
/// gesture that cannot occur. |
|
/// </summary> |
|
[TestFixture] |
|
public class DebugStepsFilterStateTests |
|
{ |
|
const string MatchGroupDescription = "CombineQueryExpressions"; |
|
const string OtherGroupDescription = "TransformExpressionTrees"; |
|
const string MatchingLeafDescription = "3: Introduce query continuation"; |
|
const string SiblingLeafDescription = "4: Flatten switch section block"; |
|
const string OtherLeafDescription = "7: Copy annotations"; |
|
|
|
static IList<Stepper.Node> BuildStepTree() |
|
{ |
|
var matchGroup = new Stepper.Node(MatchGroupDescription); |
|
matchGroup.Children.Add(new Stepper.Node(MatchingLeafDescription)); |
|
matchGroup.Children.Add(new Stepper.Node(SiblingLeafDescription)); |
|
var otherGroup = new Stepper.Node(OtherGroupDescription); |
|
otherGroup.Children.Add(new Stepper.Node(OtherLeafDescription)); |
|
return new[] { matchGroup, otherGroup }; |
|
} |
|
|
|
static (Window Window, DebugStepsPaneModel Vm, TreeView Tree) ShowPane() |
|
{ |
|
var vm = new DebugStepsPaneModel(); |
|
SetSteps(vm, BuildStepTree()); |
|
vm.IsAvailable = true; |
|
var window = new Window { Width = 400, Height = 300, Content = new DebugSteps { DataContext = vm } }; |
|
window.Show(); |
|
Dispatcher.UIThread.RunJobs(); |
|
var tree = window.GetVisualDescendants().OfType<TreeView>().First(); |
|
return (window, vm, tree); |
|
} |
|
|
|
static void SetSteps(DebugStepsPaneModel vm, IList<Stepper.Node> steps) |
|
{ |
|
vm.SetStepsSource(steps); |
|
} |
|
|
|
// Rows are looked up by DataContext rather than by header text: a row hidden by the filter |
|
// is never measured, so its header TextBlock may not have been templated yet, but its |
|
// container still exists in the visual tree. |
|
static TreeViewItem RowFor(TreeView tree, string description) |
|
{ |
|
var row = tree.GetVisualDescendants().OfType<TreeViewItem>() |
|
.FirstOrDefault(item => DescriptionOf(item.DataContext) == description); |
|
row.Should().NotBeNull($"a row with the description '{description}' must be materialised"); |
|
return row!; |
|
} |
|
|
|
static string? DescriptionOf(object? dataContext) => dataContext switch { |
|
StepNodeViewModel node => node.Description, |
|
_ => null, |
|
}; |
|
|
|
[AvaloniaTest] |
|
public Task Clearing_The_Filter_Centers_The_Selected_Step() |
|
{ |
|
// A tree tall enough that the selected step sits far outside the initial viewport, so |
|
// centering requires an actual scroll rather than being satisfied trivially. |
|
var group = new Stepper.Node("TransformGroup"); |
|
for (int i = 0; i < 60; i++) |
|
{ |
|
group.Children.Add(new Stepper.Node($"{i}: Step number {i}")); |
|
} |
|
var vm = new DebugStepsPaneModel(); |
|
SetSteps(vm, new[] { group }); |
|
vm.IsAvailable = true; |
|
var window = new Window { Width = 400, Height = 300, Content = new DebugSteps { DataContext = vm } }; |
|
window.Show(); |
|
Dispatcher.UIThread.RunJobs(); |
|
var tree = window.GetVisualDescendants().OfType<TreeView>().First(); |
|
|
|
vm.FilterText = "number 42"; |
|
Dispatcher.UIThread.RunJobs(); |
|
var leaf = RowFor(tree, "42: Step number 42"); |
|
tree.SelectedItem = leaf.DataContext; |
|
Dispatcher.UIThread.RunJobs(); |
|
|
|
vm.FilterText = ""; |
|
Dispatcher.UIThread.RunJobs(); |
|
Dispatcher.UIThread.RunJobs(); |
|
|
|
var scrollViewer = tree.GetVisualDescendants().OfType<ScrollViewer>().First(); |
|
scrollViewer.Offset.Y.Should().BeGreaterThan(0, |
|
"revealing a selected step deep in the tree must scroll the viewport"); |
|
var rowCenter = leaf.TranslatePoint(new Point(0, leaf.Bounds.Height / 2), scrollViewer)!.Value.Y; |
|
var viewportCenter = scrollViewer.Viewport.Height / 2; |
|
Math.Abs(rowCenter - viewportCenter).Should().BeLessThan(leaf.Bounds.Height * 1.5, |
|
"the selected step must end up roughly centered in the viewport"); |
|
|
|
window.Close(); |
|
return Task.CompletedTask; |
|
} |
|
|
|
[AvaloniaTest] |
|
public Task Manual_Expansion_Survives_A_Filter_Round_Trip() |
|
{ |
|
var (window, vm, tree) = ShowPane(); |
|
var matchGroup = RowFor(tree, MatchGroupDescription); |
|
var otherGroup = RowFor(tree, OtherGroupDescription); |
|
|
|
matchGroup.SetCurrentValue(TreeViewItem.IsExpandedProperty, true); |
|
Dispatcher.UIThread.RunJobs(); |
|
|
|
vm.FilterText = "continuation"; |
|
Dispatcher.UIThread.RunJobs(); |
|
vm.FilterText = ""; |
|
Dispatcher.UIThread.RunJobs(); |
|
|
|
matchGroup.IsExpanded.Should().BeTrue( |
|
"a group the user expanded before filtering must still be expanded after the filter is cleared"); |
|
otherGroup.IsExpanded.Should().BeFalse( |
|
"a group the user never expanded must not stay expanded after the filter is cleared"); |
|
|
|
window.Close(); |
|
return Task.CompletedTask; |
|
} |
|
|
|
[AvaloniaTest] |
|
public Task Filtering_Expands_Groups_Leading_To_Matches_And_Hides_The_Rest() |
|
{ |
|
var (window, vm, tree) = ShowPane(); |
|
|
|
vm.FilterText = "continuation"; |
|
Dispatcher.UIThread.RunJobs(); |
|
|
|
var matchGroup = RowFor(tree, MatchGroupDescription); |
|
matchGroup.IsExpanded.Should().BeTrue( |
|
"a group containing a match must expand so the match is revealed"); |
|
matchGroup.IsVisible.Should().BeTrue("the path to a match must stay visible"); |
|
RowFor(tree, MatchingLeafDescription).IsVisible.Should().BeTrue("the match itself must be visible"); |
|
RowFor(tree, SiblingLeafDescription).IsVisible.Should().BeFalse( |
|
"a sibling that does not match must be hidden"); |
|
RowFor(tree, OtherGroupDescription).IsVisible.Should().BeFalse( |
|
"a group without any match must be hidden"); |
|
|
|
window.Close(); |
|
return Task.CompletedTask; |
|
} |
|
|
|
[AvaloniaTest] |
|
public Task Selection_Made_While_Filtering_Stays_Visible_After_Clearing() |
|
{ |
|
var (window, vm, tree) = ShowPane(); |
|
|
|
vm.FilterText = "continuation"; |
|
Dispatcher.UIThread.RunJobs(); |
|
|
|
var leaf = RowFor(tree, MatchingLeafDescription); |
|
tree.SelectedItem = leaf.DataContext; |
|
Dispatcher.UIThread.RunJobs(); |
|
|
|
vm.FilterText = ""; |
|
Dispatcher.UIThread.RunJobs(); |
|
|
|
tree.SelectedItem.Should().BeSameAs(leaf.DataContext, |
|
"clearing the filter must not change the selection"); |
|
RowFor(tree, MatchGroupDescription).IsExpanded.Should().BeTrue( |
|
"the selected step's group must stay expanded so the selection remains visible"); |
|
leaf.IsEffectivelyVisible.Should().BeTrue( |
|
"the step selected while filtering must still be on screen after the filter is cleared"); |
|
|
|
window.Close(); |
|
return Task.CompletedTask; |
|
} |
|
} |
|
|
|
#endif
|
|
|