diff --git a/ILSpy.Tests/Compare/CompareViewRenderTests.cs b/ILSpy.Tests/Compare/CompareViewRenderTests.cs new file mode 100644 index 000000000..0fd3e06ea --- /dev/null +++ b/ILSpy.Tests/Compare/CompareViewRenderTests.cs @@ -0,0 +1,109 @@ +// 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.Controls; +using Avalonia.Headless.NUnit; +using Avalonia.VisualTree; + +using AwesomeAssertions; + +using ICSharpCode.ILSpyX.TreeView; + +using ILSpy; +using ILSpy.AppEnv; +using ILSpy.Commands; +using ILSpy.Compare; +using ILSpy.TreeNodes; +using ILSpy.ViewModels; +using ILSpy.Views; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests.Compare; + +/// +/// End-to-end render checks for the assembly-comparison view. Pure context-menu and engine +/// tests don't catch the case where the new tab opens but renders empty because the +/// hierarchical DataGrid was never wired with a model — these tests mount the view, walk +/// the visual tree, and assert rows materialise. +/// +[TestFixture] +public class CompareViewRenderTests +{ + [AvaloniaTest] + public async Task CompareView_Binds_A_HierarchicalModel_When_Opened() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 2); + + var entry = AppComposition.Current.GetExport() + .Entries.Single(e => e.Metadata.Header == "Compare...").Value; + var assemblies = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() + .Where(a => a.IsLoadedAsValidAssembly).Take(2).ToList(); + var nodes = assemblies.Select(a => + (SharpTreeNode)vm.AssemblyTreeModel.FindNode(a.ShortName)).ToArray(); + + entry.Execute(new TextViewContext { SelectedTreeNodes = nodes }); + + var view = await window.WaitForComponent(); + var grid = await view.WaitForComponent(); + + await Waiters.WaitForAsync(() => grid.HierarchicalModel != null, + description: "CompareView must populate the DataGrid's HierarchicalModel — without it " + + "the hierarchical column is rendered over an empty source and the tab shows nothing"); + grid.HierarchicalModel.Should().NotBeNull(); + } + + [AvaloniaTest] + public async Task CompareView_Renders_Rows_For_The_Merged_Tree() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 2); + + var entry = AppComposition.Current.GetExport() + .Entries.Single(e => e.Metadata.Header == "Compare...").Value; + var assemblies = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() + .Where(a => a.IsLoadedAsValidAssembly).Take(2).ToList(); + var nodes = assemblies.Select(a => + (SharpTreeNode)vm.AssemblyTreeModel.FindNode(a.ShortName)).ToArray(); + + entry.Execute(new TextViewContext { SelectedTreeNodes = nodes }); + + var view = await window.WaitForComponent(); + var grid = await view.WaitForComponent(); + + // Default state: ShowIdentical=false, so we expect at least one diff row to be present + // when comparing two non-empty assemblies against themselves *or* against a sibling. + // (Comparing an assembly with itself yields zero diffs; if the two assemblies are the + // same instance we toggle ShowIdentical to force the tree to surface rows.) + var page = (CompareTabPageModel)view.DataContext!; + if (ReferenceEquals(page.LeftAssembly, page.RightAssembly)) + page.ShowIdentical = true; + + await Waiters.WaitForAsync( + () => grid.GetVisualDescendants().OfType().Any(), + description: "the diff grid must render at least one row once the merged tree is bound"); + } +} diff --git a/ILSpy/Views/CompareView.axaml b/ILSpy/Views/CompareView.axaml index c67d927f4..7c48d6a77 100644 --- a/ILSpy/Views/CompareView.axaml +++ b/ILSpy/Views/CompareView.axaml @@ -22,8 +22,7 @@ HierarchicalRowsEnabled="True" GridLinesVisibility="None" CanUserResizeColumns="False" - SelectionMode="Single" - ItemsSource="{Binding RootEntry.Children}"> + SelectionMode="Single"> { + ChildrenSelector = node => { + if (node is TreeNodes.ILSpyTreeNode ilspy) + ilspy.EnsureChildrenFiltered(); + else + node.EnsureLazyChildren(); + return node.Children; + }, + IsLeafSelector = node => !node.ShowExpander, + VirtualizeChildren = false, + IsExpandedPropertyPath = nameof(SharpTreeNode.IsExpanded), + }; + var model = new HierarchicalModel(options); + model.SetRoots(root.Children); + DiffGrid.HierarchicalModel = model; + } } } diff --git a/ILSpy/Views/ContentTabPageView.axaml b/ILSpy/Views/ContentTabPageView.axaml index 323f7075e..ce1b06dda 100644 --- a/ILSpy/Views/ContentTabPageView.axaml +++ b/ILSpy/Views/ContentTabPageView.axaml @@ -6,6 +6,7 @@ xmlns:textView="using:ILSpy.TextView" xmlns:metaViews="using:ILSpy.Views" xmlns:options="using:ILSpy.Options" + xmlns:compare="using:ILSpy.Compare" mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="400" x:Class="ILSpy.Views.ContentTabPageView" x:DataType="vm:ContentTabPage"> @@ -19,6 +20,7 @@ + diff --git a/ILSpy/Views/ContentTabPageView.axaml.cs b/ILSpy/Views/ContentTabPageView.axaml.cs index e5a544cde..8a010ad74 100644 --- a/ILSpy/Views/ContentTabPageView.axaml.cs +++ b/ILSpy/Views/ContentTabPageView.axaml.cs @@ -21,6 +21,7 @@ using System.ComponentModel; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using ILSpy.Compare; using ILSpy.Options; using ILSpy.TextView; using ILSpy.ViewModels; @@ -39,6 +40,7 @@ namespace ILSpy.Views DecompilerTextView decompilerView = null!; MetadataTablePage metadataView = null!; OptionsPageView optionsView = null!; + CompareView compareView = null!; public ContentTabPageView() { @@ -46,6 +48,7 @@ namespace ILSpy.Views decompilerView = this.FindControl("DecompilerView")!; metadataView = this.FindControl("MetadataView")!; optionsView = this.FindControl("OptionsView")!; + compareView = this.FindControl("CompareViewSlot")!; DataContextChanged += (_, _) => RebindPage(); } @@ -103,6 +106,17 @@ namespace ILSpy.Views optionsView.IsVisible = false; optionsView.DataContext = null; } + + if (content is CompareTabPageModel compare) + { + compareView.DataContext = compare; + compareView.IsVisible = true; + } + else + { + compareView.IsVisible = false; + compareView.DataContext = null; + } } } }