diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeExpanderHitboxTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeExpanderHitboxTests.cs index 85094c0b24..90df93d699 100644 --- a/ILSpy.Tests/AssemblyList/AssemblyTreeExpanderHitboxTests.cs +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeExpanderHitboxTests.cs @@ -46,11 +46,10 @@ public class AssemblyTreeExpanderHitboxTests [AvaloniaTest] public async Task Expander_Toggle_Offers_At_Least_16x16_Clickable_Target() { - // The +/- expander in the assembly tree must give a click target that fills the full 13px - // expander column and the 16px row-tall toggle, while its visible glyph stays the classic - // 9x9 box. The column is kept at 13px so the +/- box centres on the tree connector lines; - // the target must be genuinely hittable across that whole area — not merely occupy it in - // layout while only the 9x9 glyph receives input. + // The +/- expander's click target fills the 13px expander column and the 16px row height, + // while the drawn glyph stays the classic 9x9 box centred on the tree connector lines. The + // grown area has to be genuinely hittable rather than merely occupied in layout, so the + // test clicks below the glyph instead of measuring the boxes. // Arrange — boot, wait for assemblies, expand a node so an expandable row is realised. var (window, vm) = await TestHarness.BootAsync(3); @@ -63,39 +62,21 @@ public async Task Expander_Toggle_Offers_At_Least_16x16_Clickable_Target() var pane = await window.WaitForComponent(); var grid = await pane.WaitForComponent(); - // Let rows realise and layout settle. - for (int i = 0; i < 8; i++) - { - Dispatcher.UIThread.RunJobs(); - grid.UpdateLayout(); - await Task.Delay(25); - } - - // Act — locate the expander toggle of the (expandable) assembly row. - var row = grid.GetVisualDescendants().OfType() - .FirstOrDefault(r => RowMatches(r, assemblyNode)); - row.Should().NotBeNull("the expanded assembly row must be realised"); - var expander = row!.GetVisualDescendants().OfType() - .FirstOrDefault(b => b.Name == "PART_Expander"); - expander.Should().NotBeNull("an expandable row must realise a PART_Expander toggle"); + // Act — locate the expander toggle of the (expandable) assembly row, once the row and its + // template have been realised. + ToggleButton? expander = null; + await Waiters.WaitForAsync( + () => { + grid.UpdateLayout(); + expander = grid.GetVisualDescendants().OfType() + .FirstOrDefault(r => RowMatches(r, assemblyNode)) + ?.GetVisualDescendants().OfType() + .FirstOrDefault(b => b.Name == "PART_Expander"); + return expander is { Bounds.Height: >= 16 }; + }, + description: "the expanded assembly row must realise a PART_Expander toggle filling the row height"); expander!.IsEnabled.Should().BeTrue("the assembly row is expandable"); - // Assert — the click target fills the 13px expander column and is 16px tall. - expander.Bounds.Width.Should().BeGreaterThanOrEqualTo(13, - "the expander click target must fill the 13px expander column for reliable tapping"); - expander.Bounds.Height.Should().BeGreaterThanOrEqualTo(16, - "the expander click target must be at least 16px tall for reliable tapping"); - - // Assert — the visible glyph box is unchanged at 9x9 (the nearest Border around ExpandPath, - // i.e. the drawn box, not the transparent hit-target wrapper). - var glyphPath = expander.GetVisualDescendants().OfType() - .FirstOrDefault(p => p.Name == "ExpandPath"); - glyphPath.Should().NotBeNull("the expander must render its ExpandPath glyph"); - var glyph = glyphPath!.GetVisualAncestors().OfType().FirstOrDefault(); - glyph.Should().NotBeNull("the expander must still render its glyph box"); - glyph!.Bounds.Width.Should().BeApproximately(9, 0.5, "the visible glyph box must stay 9px wide"); - glyph.Bounds.Height.Should().BeApproximately(9, 0.5, "the visible glyph box must stay 9px tall"); - // Assert — a real click well below the 9x9 glyph (y=14, inside the 16-tall target but // outside the centred glyph at ~y=3.5..12.5) collapses the node. This proves the grown // area is genuinely hittable, not just larger in layout. diff --git a/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesGridVerification.cs b/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesGridVerification.cs deleted file mode 100644 index 9ae23e9cb4..0000000000 --- a/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesGridVerification.cs +++ /dev/null @@ -1,95 +0,0 @@ -// 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.Collections; -using System.Linq; -using System.Threading.Tasks; - -using Avalonia.Headless.NUnit; - -using AwesomeAssertions; - -using ICSharpCode.ILSpyX.TreeView; - -using ICSharpCode.ILSpy; -using ICSharpCode.ILSpy.AppEnv; -using ICSharpCode.ILSpy.AssemblyTree; -using ICSharpCode.ILSpy.TreeNodes; - -using NUnit.Framework; - -namespace ICSharpCode.ILSpy.Tests; - -[TestFixture] -public class UseNestedNamespaceNodesGridVerification -{ - [AvaloniaTest] - public async Task Toggling_UseNestedNamespaceNodes_Reshapes_The_Visible_Tree_In_Place() - { - // Toggling UseNestedNamespaceNodes rebuilds each loaded assembly's namespace subtree. - // SharpTreeView's flattener observes node.Children mutations live, so the rebuilt shape - // surfaces in the visible row list without any model re-bind (the ProDataGrid snapshot - // problem that used to need a HierarchicalModel rebind is gone). This pins that the - // flat-mode -> nested-mode switch is reflected in the SharpTreeView's ItemsSource. - - var settings = AppComposition.Current.GetExport().DisplaySettings; - settings.UseNestedNamespaceNodes = false; - - var (window, vm) = await TestHarness.BootAsync(3); - var pane = await window.WaitForComponent(); - var grid = await pane.WaitForComponent(); - - // Expand an assembly so its namespace children become visible rows. - var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.IsExpanded = true; - await Waiters.WaitForAsync(() => assemblyNode.Children.OfType().Any()); - TestCapture.Step("system-linq-expanded-flat"); - - var flat = (IList)grid.ItemsSource!; - bool VisibleNamespace(string name) => flat.Cast() - .OfType() - .Any(n => string.Equals(n.Text?.ToString(), name, System.StringComparison.Ordinal)); - - // In flat mode the full dotted namespace is a single visible row. - await Waiters.WaitForAsync(() => VisibleNamespace("System.Collections.Generic"), - description: "flat mode shows the dotted namespace as one row"); - - try - { - settings.UseNestedNamespaceNodes = true; - assemblyNode.IsExpanded = true; - TestCapture.Step("nested-namespaces-rebuilt"); - - // In nested mode the leaf segment ("Generic") becomes its own row nested under - // "System" -> "Collections"; the live flattener must surface that without a re-bind. - await Waiters.WaitForAsync( - () => assemblyNode.Children.OfType() - .Any(n => string.Equals(n.Text?.ToString(), "System", System.StringComparison.Ordinal)), - System.TimeSpan.FromSeconds(5), - "toggling UseNestedNamespaceNodes must rebuild the namespace subtree into nested nodes"); - - VisibleNamespace("System").Should().BeTrue( - "the rebuilt nested 'System' namespace node must appear in the SharpTreeView's visible rows " - + "without any model re-bind (the live flattener observes the child mutations directly)"); - } - finally - { - settings.UseNestedNamespaceNodes = false; - } - } -} diff --git a/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesLiveTests.cs b/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesLiveTests.cs deleted file mode 100644 index c34d0be77c..0000000000 --- a/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesLiveTests.cs +++ /dev/null @@ -1,78 +0,0 @@ -// 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 System.Threading.Tasks; - -using Avalonia.Headless.NUnit; - -using AwesomeAssertions; - -using ICSharpCode.ILSpy; -using ICSharpCode.ILSpy.AppEnv; -using ICSharpCode.ILSpy.TreeNodes; - -using NUnit.Framework; - -namespace ICSharpCode.ILSpy.Tests; - -[TestFixture] -public class UseNestedNamespaceNodesLiveTests -{ - [AvaloniaTest] - public async Task Toggling_UseNestedNamespaceNodes_Live_Refreshes_The_Tree_Shape() - { - // Drives the live-reactivity path: toggle the Display-Settings flag and observe - // that the AssemblyTreeNode's namespace children switch between flat and nested - // layouts without a manual rebuild. - var settings = AppComposition.Current.GetExport().DisplaySettings; - settings.UseNestedNamespaceNodes = false; - - var (_, vm) = await TestHarness.BootAsync(3); - - try - { - var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.EnsureLazyChildren(); - - var flatNamespaces = assemblyNode.Children.OfType() - .Select(n => n.Name).ToList(); - flatNamespaces.Should().Contain("System.Linq", - "baseline: flat mode lists 'System.Linq' as a top-level sibling"); - - // Toggle the live setting — should fan out through MessageBus - // to AssemblyTreeModel.OnSettingsChanged and rebuild the namespace subtrees. - settings.UseNestedNamespaceNodes = true; - - await Waiters.WaitForAsync(() => - assemblyNode.Children.OfType().Any(n => n.Name == "System"), - System.TimeSpan.FromSeconds(5)); - - var nestedNames = assemblyNode.Children.OfType() - .Select(n => n.Name).ToList(); - nestedNames.Should().Contain("System", - "after live toggle: nested mode must surface 'System' as top-level"); - nestedNames.Should().NotContain("System.Linq", - "after live toggle: the flat 'System.Linq' sibling must disappear"); - } - finally - { - settings.UseNestedNamespaceNodes = false; - } - } -} diff --git a/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesTests.cs b/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesTests.cs index b9bbdc3133..149681faf8 100644 --- a/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesTests.cs +++ b/ILSpy.Tests/AssemblyList/UseNestedNamespaceNodesTests.cs @@ -16,6 +16,8 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System; +using System.Collections; using System.Linq; using System.Threading.Tasks; @@ -23,77 +25,83 @@ using AwesomeAssertions; -using ICSharpCode.ILSpy; using ICSharpCode.ILSpy.AppEnv; +using ICSharpCode.ILSpy.AssemblyTree; using ICSharpCode.ILSpy.TreeNodes; +using ICSharpCode.ILSpyX.TreeView; + using NUnit.Framework; +using SharpTreeView = ICSharpCode.ILSpy.Controls.TreeView.SharpTreeView; + namespace ICSharpCode.ILSpy.Tests; +/// +/// The "Use nested namespace structure" display setting, from the setting through to the +/// rows on screen. Flat mode keeps every dotted namespace as its own sibling of the assembly +/// node; nested mode splits them, so "System.Linq" becomes "Linq" under "System". The switch +/// happens live: toggling rebuilds each loaded assembly's namespace subtree, and because +/// SharpTreeView's flattener observes node.Children directly, the rebuilt shape reaches the +/// visible rows with no model re-bind. +/// [TestFixture] public class UseNestedNamespaceNodesTests { [AvaloniaTest] - public async Task When_UseNestedNamespaceNodes_True_Namespaces_Are_Hierarchical() + public async Task Toggling_The_Setting_Reshapes_The_Tree_Live_Down_To_The_Visible_Rows() { - // With the setting on, "System" becomes a single root node holding "Collections", - // "IO", "Linq", … as descendants — not the flat "System", "System.Collections", - // "System.IO" siblings the default flat mode produces. - var settings = AppComposition.Current.GetExport().DisplaySettings; - var (_, vm) = await TestHarness.BootAsync(3); + settings.UseNestedNamespaceNodes = false; + + var (window, vm) = await TestHarness.BootAsync(3); + var pane = await window.WaitForComponent(); + var grid = await pane.WaitForComponent(); try { - settings.UseNestedNamespaceNodes = true; - - // Use System.Linq's assembly — it has System and System.Linq as namespaces. + // System.Linq's assembly carries both "System" and "System.Linq", so one assembly + // shows the difference between the two layouts. var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.Children.Clear(); - assemblyNode.LazyLoading = true; - assemblyNode.EnsureLazyChildren(); - - var systemNode = assemblyNode.Children.OfType() - .SingleOrDefault(ns => ns.Name == "System"); - ((object?)systemNode).Should().NotBeNull( - "in nested mode the top-level node for the System namespace must exist as 'System' (last segment), not 'System.Linq'"); - - var nestedLinq = systemNode!.Children.OfType() - .SingleOrDefault(ns => ns.Name == "Linq"); - ((object?)nestedLinq).Should().NotBeNull( - "the System.Linq namespace must nest under the System node in nested mode"); - - // Sanity: there is NO sibling "System.Linq" at the assembly-node level. - assemblyNode.Children.OfType() - .Select(n => n.Name).Should().NotContain("System.Linq", - "flat-style 'System.Linq' sibling must not appear when nesting is on"); + assemblyNode.IsExpanded = true; + await Waiters.WaitForAsync(() => assemblyNode.Children.OfType().Any()); + + var visibleRows = (IList)grid.ItemsSource!; + bool VisibleNamespace(string name) => visibleRows.Cast() + .OfType() + .Any(n => string.Equals(n.Text?.ToString(), name, StringComparison.Ordinal)); + + NamespaceNames().Should().Contain("System.Linq", + "flat mode lists the whole dotted namespace as one sibling of the assembly node"); + await Waiters.WaitForAsync(() => VisibleNamespace("System.Collections.Generic"), + description: "flat mode shows the dotted namespace as a single visible row"); + TestCapture.Step("flat-mode"); + + // The setting fans out through MessageBus to + // AssemblyTreeModel.OnSettingsChanged, which rebuilds the namespace subtrees. + settings.UseNestedNamespaceNodes = true; + assemblyNode.IsExpanded = true; + + await Waiters.WaitForAsync(() => NamespaceNames().Contains("System"), + TimeSpan.FromSeconds(5), + "toggling the setting must rebuild the namespace subtree into nested nodes"); + TestCapture.Step("nested-mode"); + + NamespaceNames().Should().NotContain("System.Linq", + "the flat dotted sibling must be gone once its segments are nested"); + assemblyNode.Children.OfType().Single(n => n.Name == "System") + .Children.OfType().Select(n => n.Name).Should().Contain("Linq", + "the trailing segment must hang under the node for the leading one"); + VisibleNamespace("System").Should().BeTrue( + "the rebuilt node must reach the visible rows without a model re-bind - the live " + + "flattener observes the child mutations directly"); + + string[] NamespaceNames() => assemblyNode.Children.OfType() + .Select(n => n.Name).ToArray(); } finally { settings.UseNestedNamespaceNodes = false; } } - - [AvaloniaTest] - public async Task When_UseNestedNamespaceNodes_False_Namespaces_Are_Flat() - { - // Baseline: the default flat layout keeps every distinct namespace string as a - // sibling under the AssemblyTreeNode. - var settings = AppComposition.Current.GetExport().DisplaySettings; - settings.UseNestedNamespaceNodes = false; - - var (_, vm) = await TestHarness.BootAsync(3); - - var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.Children.Clear(); - assemblyNode.LazyLoading = true; - assemblyNode.EnsureLazyChildren(); - - var namespaceNames = assemblyNode.Children.OfType() - .Select(n => n.Name).ToList(); - - namespaceNames.Should().Contain("System.Linq", - "in flat mode 'System.Linq' must appear as a top-level sibling"); - } } diff --git a/ILSpy.Tests/Bookmarks/BookmarksPaneStructureTests.cs b/ILSpy.Tests/Bookmarks/BookmarksPaneStructureTests.cs index 39dae06f5b..f67523b242 100644 --- a/ILSpy.Tests/Bookmarks/BookmarksPaneStructureTests.cs +++ b/ILSpy.Tests/Bookmarks/BookmarksPaneStructureTests.cs @@ -26,7 +26,6 @@ using ICSharpCode.ILSpy.Bookmarks; using ICSharpCode.ILSpy.Properties; -using ICSharpCode.ILSpy.Views.Controls; using NUnit.Framework; @@ -35,22 +34,6 @@ namespace ICSharpCode.ILSpy.Tests.Bookmarks; [TestFixture] public class BookmarksPaneStructureTests { - [AvaloniaTest] - public void Toolbar_uses_main_toolbar_chrome_and_button_content() - { - var pane = new BookmarksPane(); - var toolbarBorder = pane.FindControl("ToolbarBorder")!; - var toolbarRoot = pane.FindControl("ToolbarRoot")!; - - toolbarBorder.BorderThickness.Should().Be(new Avalonia.Thickness(0, 0, 0, 1)); - toolbarBorder.MinHeight.Should().Be(29); - toolbarBorder.Padding.Should().Be(new Avalonia.Thickness(3)); - toolbarRoot.Children.OfType().Should().HaveCount(2); - toolbarRoot.Children.OfType