diff --git a/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs new file mode 100644 index 000000000..f8e1ba139 --- /dev/null +++ b/ILSpy.Tests/AssemblyList/AssemblyTreeTests.cs @@ -0,0 +1,230 @@ +// 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 System.Xml.Linq; + +using Avalonia.Headless.NUnit; + +using AwesomeAssertions; + +using ICSharpCode.ILSpy.Properties; + +using ILSpy; +using ILSpy.AppEnv; +using ILSpy.Commands; +using ILSpy.TreeNodes; +using ILSpy.ViewModels; +using ILSpy.Views; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests; + +[TestFixture] +public class AssemblyTreeTests +{ + [AvaloniaTest] + public async Task Assembly_Node_Has_References_Folder_Child() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); + assemblyNode.EnsureLazyChildren(); + + var refFolder = assemblyNode.Children.OfType().Single(); + refFolder.Text.Should().Be(Resources.References); + + refFolder.EnsureLazyChildren(); + refFolder.Children.Should().NotBeEmpty(); + refFolder.Children.Should().AllBeAssignableTo(); + } + + [AvaloniaTest] + public async Task Activating_Assembly_Reference_Selects_Resolved_Assembly_Node() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); + assemblyNode.EnsureLazyChildren(); + var refFolder = assemblyNode.Children.OfType().Single(); + refFolder.EnsureLazyChildren(); + + var refNode = refFolder.Children.OfType() + .Single(n => n.AssemblyReference.Name == "System.Runtime"); + + var args = new StubRoutedEventArgs(); + refNode.ActivateItem(args); + + args.Handled.Should().BeTrue(); + await Waiters.WaitForAsync(() => + vm.AssemblyTreeModel.SelectedItem is AssemblyTreeNode selected + && selected.LoadedAssembly.ShortName == "System.Runtime"); + } + + sealed class StubRoutedEventArgs : ICSharpCode.ILSpyX.TreeView.PlatformAbstractions.IPlatformRoutedEventArgs + { + public bool Handled { get; set; } + } + + [AvaloniaTest] + public async Task Selecting_Method_Auto_Loads_Referenced_Assemblies() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var newAsmPath = typeof(System.Net.Http.HttpClient).Assembly.Location; + var registry = AppComposition.Current.GetExport(); + var openCommand = registry.Commands + .Single(c => c.Metadata.Header == nameof(Resources._Open)) + .CreateExport().Value; + openCommand.Execute(newAsmPath); + + await Waiters.WaitForAsync(() => + vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => + string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase))); + + var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() + .First(a => string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase)); + await loaded.GetLoadResultAsync(); + + var initialFiles = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() + .Select(a => a.FileName) + .ToHashSet(System.StringComparer.OrdinalIgnoreCase); + + var typeNode = vm.AssemblyTreeModel.FindNode( + loaded.ShortName, "System.Net.Http", "System.Net.Http.HttpClient"); + typeNode.EnsureLazyChildren(); + var methodNode = typeNode.Children.OfType() + .First(m => m.MethodDefinition.Name == "CancelPendingRequests"); + + vm.AssemblyTreeModel.SelectedItem = methodNode; + await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + await Waiters.WaitForAsync(() => + vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => !initialFiles.Contains(a.FileName))); + + var addedAssemblies = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() + .Where(a => !initialFiles.Contains(a.FileName)) + .ToList(); + addedAssemblies.Should().NotBeEmpty(); + addedAssemblies.Should().OnlyContain(a => a.IsAutoLoaded); + + foreach (var auto in addedAssemblies) + { + var node = vm.AssemblyTreeModel.FindNode(auto.ShortName); + node.IsAutoLoaded.Should().BeTrue(); + } + } + + [AvaloniaTest] + public async Task Auto_Loaded_Assemblies_Are_Not_Persisted() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var newAsmPath = typeof(System.Net.Http.HttpClient).Assembly.Location; + var registry = AppComposition.Current.GetExport(); + var openCommand = registry.Commands + .Single(c => c.Metadata.Header == nameof(Resources._Open)) + .CreateExport().Value; + openCommand.Execute(newAsmPath); + + await Waiters.WaitForAsync(() => + vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => + string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase))); + + var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() + .First(a => string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase)); + await loaded.GetLoadResultAsync(); + + var typeNode = vm.AssemblyTreeModel.FindNode( + loaded.ShortName, "System.Net.Http", "System.Net.Http.HttpClient"); + typeNode.EnsureLazyChildren(); + var methodNode = typeNode.Children.OfType() + .First(m => m.MethodDefinition.Name == "CancelPendingRequests"); + + vm.AssemblyTreeModel.SelectedItem = methodNode; + await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + await Waiters.WaitForAsync(() => + vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => a.IsAutoLoaded)); + + var settingsService = AppComposition.Current.GetExport(); + var manager = settingsService.AssemblyListManager; + manager.SaveList(vm.AssemblyTreeModel.AssemblyList!); + + var settingsPath = ICSharpCode.ILSpyX.Settings.ILSpySettings.SettingsFilePathProvider!(); + var doc = XDocument.Load(settingsPath); + var savedFiles = doc.Descendants("Assembly") + .Select(e => e.Value) + .ToHashSet(System.StringComparer.OrdinalIgnoreCase); + + var assemblies = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies(); + var autoLoaded = assemblies.Where(a => a.IsAutoLoaded).ToList(); + autoLoaded.Should().NotBeEmpty(); + foreach (var asm in autoLoaded) + savedFiles.Should().NotContain(asm.FileName, "auto-loaded assembly {0} must not be persisted", asm.FileName); + + var manualLoaded = assemblies.Where(a => !a.IsAutoLoaded).ToList(); + foreach (var asm in manualLoaded) + savedFiles.Should().Contain(asm.FileName, "manually loaded assembly {0} must be persisted", asm.FileName); + } + + [AvaloniaTest] + public async Task Opening_Assembly_Adds_It_To_The_Tree() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var newAsmPath = typeof(System.Net.Http.HttpClient).Assembly.Location; + var initialCount = vm.AssemblyTreeModel.AssemblyList!.Count; + + var registry = AppComposition.Current.GetExport(); + var openCommand = registry.Commands + .Single(c => c.Metadata.Header == nameof(Resources._Open)) + .CreateExport().Value; + openCommand.Execute(newAsmPath); + + await Waiters.WaitForAsync(() => + vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => + string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase))); + + var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() + .First(a => string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase)); + await loaded.GetLoadResultAsync(); + + vm.AssemblyTreeModel.AssemblyList!.Count.Should().Be(initialCount + 1); + var node = vm.AssemblyTreeModel.FindNode(loaded.ShortName); + node.LoadedAssembly.Should().BeSameAs(loaded); + node.IsSelected.Should().BeTrue(); + } +} diff --git a/ILSpy.Tests/Editor/DecompilerViewTests.cs b/ILSpy.Tests/Editor/DecompilerViewTests.cs new file mode 100644 index 000000000..a41254c11 --- /dev/null +++ b/ILSpy.Tests/Editor/DecompilerViewTests.cs @@ -0,0 +1,207 @@ +// 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 ILSpy.AppEnv; +using ILSpy.TreeNodes; +using ILSpy.ViewModels; +using ILSpy.Views; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests; + +[TestFixture] +public class DecompilerViewTests +{ + [AvaloniaTest] + public async Task Selecting_AsEnumerable_Method_Decompiles_Into_Document_View() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var typeNode = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable"); + typeNode.EnsureLazyChildren(); + var methodNode = typeNode.Children.OfType() + .Single(m => m.MethodDefinition.Name == "AsEnumerable"); + methodNode.MethodDefinition.IsExtensionMethod.Should().BeTrue(); + + vm.AssemblyTreeModel.SelectedItem = methodNode; + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + tab.Text.Should().Contain("AsEnumerable"); + tab.Text.Should().Contain("IEnumerable"); + tab.Text.Should().Contain("return source"); + + methodNode.Should().Be().CenteredInView(); + } + + [AvaloniaTest] + public async Task Selecting_References_Folder_Decompiles_References_Listing() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); + assemblyNode.EnsureLazyChildren(); + var refFolder = assemblyNode.Children.OfType().Single(); + + vm.AssemblyTreeModel.SelectedItem = refFolder; + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + tab.Text.Should().Contain("Detected TargetFramework-Id:"); + tab.Text.Should().Contain("Detected RuntimePack:"); + tab.Text.Should().Contain("Referenced assemblies (in metadata order):"); + tab.Text.Should().Contain("System.Runtime"); + tab.Text.Should().Contain("Assembly load log including transitive references:"); + } + + [AvaloniaTest] + public async Task Selecting_Type_Node_Decompiles_Type_Definition() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var coreLib = typeof(object).Assembly.GetName().Name!; + var typeNode = vm.AssemblyTreeModel.FindNode( + coreLib, "System", "System.Version"); + + vm.AssemblyTreeModel.SelectedItem = typeNode; + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + tab.Text.Should().Contain("class Version"); + tab.Text.Should().Contain("Major"); + tab.Text.Should().Contain("Minor"); + } + + [AvaloniaTest] + public async Task Selecting_Property_Node_Decompiles_Property() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var coreLib = typeof(object).Assembly.GetName().Name!; + var typeNode = vm.AssemblyTreeModel.FindNode( + coreLib, "System", "System.Version"); + typeNode.EnsureLazyChildren(); + var propertyNode = typeNode.Children.OfType() + .Single(p => p.PropertyDefinition.Name == "Major"); + + vm.AssemblyTreeModel.SelectedItem = propertyNode; + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + tab.Text.Should().Contain("public int Major"); + } + + [AvaloniaTest] + public async Task Selecting_Field_Node_Decompiles_Field() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var coreLib = typeof(object).Assembly.GetName().Name!; + var typeNode = vm.AssemblyTreeModel.FindNode( + coreLib, "System", "System.Math"); + typeNode.EnsureLazyChildren(); + var fieldNode = typeNode.Children.OfType() + .Single(f => f.FieldDefinition.Name == "PI"); + + vm.AssemblyTreeModel.SelectedItem = fieldNode; + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + tab.Text.Should().Contain("public const double PI"); + tab.Text.Should().Contain("3.14159"); + } + + [AvaloniaTest] + public async Task Selecting_Event_Node_Decompiles_Event() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var coreLib = typeof(object).Assembly.GetName().Name!; + var typeNode = vm.AssemblyTreeModel.FindNode( + coreLib, "System", "System.AppDomain"); + typeNode.EnsureLazyChildren(); + var eventNode = typeNode.Children.OfType() + .Single(e => e.EventDefinition.Name == "ProcessExit"); + + vm.AssemblyTreeModel.SelectedItem = eventNode; + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + tab.Text.Should().Contain("event EventHandler"); + tab.Text.Should().Contain("ProcessExit"); + } + + [AvaloniaTest] + public async Task Selecting_Namespace_Node_Decompiles_Namespace() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var coreLib = typeof(object).Assembly.GetName().Name!; + var namespaceNode = vm.AssemblyTreeModel.FindNode( + coreLib, "System.Runtime.Versioning"); + + vm.AssemblyTreeModel.SelectedItem = namespaceNode; + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + tab.Text.Should().Contain("TargetFrameworkAttribute"); + tab.Text.Should().Contain("SupportedOSPlatformAttribute"); + } + + [AvaloniaTest] + public async Task Selecting_Assembly_Node_Emits_Header_And_Assembly_Attributes() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); + vm.AssemblyTreeModel.SelectedItem = assemblyNode; + + var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + tab.Text.Should().Contain("// " + assemblyNode.LoadedAssembly.FileName); + tab.Text.Should().Contain("// System.Linq, Version="); + tab.Text.Should().Contain("// Global type: "); + tab.Text.Should().Contain("// Architecture: "); + tab.Text.Should().Contain("// Runtime: "); + tab.Text.Should().Contain("[assembly: AssemblyVersion("); + } +} diff --git a/ILSpy.Tests/MainMenuTests.cs b/ILSpy.Tests/MainWindow/MainMenuTests.cs similarity index 64% rename from ILSpy.Tests/MainMenuTests.cs rename to ILSpy.Tests/MainWindow/MainMenuTests.cs index 260246c3e..389612aee 100644 --- a/ILSpy.Tests/MainMenuTests.cs +++ b/ILSpy.Tests/MainWindow/MainMenuTests.cs @@ -17,13 +17,19 @@ // DEALINGS IN THE SOFTWARE. using System.Linq; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Headless.NUnit; +using Avalonia.Input; +using Avalonia.VisualTree; using AwesomeAssertions; -using ILSpy; +using ICSharpCode.ILSpy.Properties; + +using ILSpy.AppEnv; +using ILSpy.Views; using NUnit.Framework; @@ -39,11 +45,32 @@ public class MainMenuTests [AvaloniaTest] public void MainMenu_top_level_items_are_File_View_Window_in_order() { - var mainMenu = new MainMenu(); + var mainMenu = new global::ILSpy.MainMenu(); var menu = mainMenu.FindControl("MainMenuRoot"); menu.Should().NotBeNull(); var headers = menu!.Items.OfType().Select(m => m.Header as string).ToList(); headers.Should().Equal("_File", "_View", "_Window"); } + + [AvaloniaTest] + public async Task Main_Menu_Items_Display_Input_Gestures() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + + var menu = window.GetVisualDescendants().OfType().First(); + await Waiters.WaitForAsync(() => + menu.Items.OfType().Any(m => (string?)m.Tag == nameof(Resources._File)) + && menu.Items.OfType().Single(m => (string?)m.Tag == nameof(Resources._File)) + .Items.OfType().Any()); + + var fileMenu = menu.Items.OfType().Single(m => (string?)m.Tag == nameof(Resources._File)); + var openItem = fileMenu.Items.OfType() + .Single(m => string.Equals(m.Header as string, Resources._Open, System.StringComparison.Ordinal)); + + openItem.InputGesture.Should().NotBeNull(); + openItem.InputGesture!.Should().Be(KeyGesture.Parse("Ctrl+O")); + openItem.HotKey.Should().Be(KeyGesture.Parse("Ctrl+O")); + } } diff --git a/ILSpy.Tests/MainWindow/MainWindowTests.cs b/ILSpy.Tests/MainWindow/MainWindowTests.cs index ca37e3443..01d04fdd4 100644 --- a/ILSpy.Tests/MainWindow/MainWindowTests.cs +++ b/ILSpy.Tests/MainWindow/MainWindowTests.cs @@ -16,23 +16,11 @@ // 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 System.Xml.Linq; - -using Avalonia.Controls; using Avalonia.Headless.NUnit; -using Avalonia.Input; -using Avalonia.VisualTree; using AwesomeAssertions; -using ICSharpCode.ILSpy.Properties; - -using ILSpy; using ILSpy.AppEnv; -using ILSpy.Commands; -using ILSpy.TreeNodes; using ILSpy.ViewModels; using ILSpy.Views; @@ -53,423 +41,4 @@ public class MainWindowTests window.Title.Should().Be("ILSpy"); window.DataContext.Should().BeOfType(); } - - [AvaloniaTest] - public async Task Selecting_AsEnumerable_Method_Decompiles_Into_Document_View() - { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); - - var typeNode = vm.AssemblyTreeModel.FindNode( - "System.Linq", "System.Linq", "System.Linq.Enumerable"); - typeNode.EnsureLazyChildren(); - var methodNode = typeNode.Children.OfType() - .Single(m => m.MethodDefinition.Name == "AsEnumerable"); - methodNode.MethodDefinition.IsExtensionMethod.Should().BeTrue(); - - vm.AssemblyTreeModel.SelectedItem = methodNode; - var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); - tab.Text.Should().Contain("AsEnumerable"); - tab.Text.Should().Contain("IEnumerable"); - tab.Text.Should().Contain("return source"); - - methodNode.Should().Be().CenteredInView(); - } - - [AvaloniaTest] - public async Task Back_Navigation_Restores_Previously_Selected_Node() - { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); - - var typeNode = vm.AssemblyTreeModel.FindNode( - "System.Linq", "System.Linq", "System.Linq.Enumerable"); - typeNode.EnsureLazyChildren(); - var firstMethod = typeNode.Children.OfType() - .Single(m => m.MethodDefinition.Name == "AsEnumerable"); - var secondMethod = typeNode.Children.OfType() - .First(m => m.MethodDefinition.Name == "Empty"); - - vm.AssemblyTreeModel.SelectedItem = firstMethod; - var firstTab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); - firstTab.Text.Should().Contain("AsEnumerable"); - - // NavigationHistory collapses selections that happen within 0.5s into one entry. Wait - // past that window so the second selection records a real back-history entry. - await Task.Delay(600); - - vm.AssemblyTreeModel.SelectedItem = secondMethod; - await Waiters.WaitForAsync(() => ReferenceEquals(vm.AssemblyTreeModel.SelectedItem, secondMethod)); - var secondTab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); - secondTab.Text.Should().Contain("Empty"); - - vm.DockWorkspace.NavigateBackCommand.CanExecute(null).Should().BeTrue(); - vm.DockWorkspace.NavigateBackCommand.Execute(null); - - await Waiters.WaitForAsync(() => ReferenceEquals(vm.AssemblyTreeModel.SelectedItem, firstMethod)); - var restoredTab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); - restoredTab.Text.Should().Contain("AsEnumerable"); - restoredTab.Text.Should().Contain("return source"); - - firstMethod.Should().Be().CenteredInView(); - } - - [AvaloniaTest] - public async Task Assembly_Node_Has_References_Folder_Child() - { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); - - var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.EnsureLazyChildren(); - - var refFolder = assemblyNode.Children.OfType().Single(); - refFolder.Text.Should().Be(Resources.References); - - refFolder.EnsureLazyChildren(); - refFolder.Children.Should().NotBeEmpty(); - refFolder.Children.Should().AllBeAssignableTo(); - } - - [AvaloniaTest] - public async Task Activating_Assembly_Reference_Selects_Resolved_Assembly_Node() - { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); - - var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.EnsureLazyChildren(); - var refFolder = assemblyNode.Children.OfType().Single(); - refFolder.EnsureLazyChildren(); - - var refNode = refFolder.Children.OfType() - .Single(n => n.AssemblyReference.Name == "System.Runtime"); - - var args = new StubRoutedEventArgs(); - refNode.ActivateItem(args); - - args.Handled.Should().BeTrue(); - await Waiters.WaitForAsync(() => - vm.AssemblyTreeModel.SelectedItem is AssemblyTreeNode selected - && selected.LoadedAssembly.ShortName == "System.Runtime"); - } - - sealed class StubRoutedEventArgs : ICSharpCode.ILSpyX.TreeView.PlatformAbstractions.IPlatformRoutedEventArgs - { - public bool Handled { get; set; } - } - - [AvaloniaTest] - public async Task Selecting_References_Folder_Decompiles_References_Listing() - { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); - - var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - assemblyNode.EnsureLazyChildren(); - var refFolder = assemblyNode.Children.OfType().Single(); - - vm.AssemblyTreeModel.SelectedItem = refFolder; - var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); - - tab.Text.Should().Contain("Detected TargetFramework-Id:"); - tab.Text.Should().Contain("Detected RuntimePack:"); - tab.Text.Should().Contain("Referenced assemblies (in metadata order):"); - tab.Text.Should().Contain("System.Runtime"); - tab.Text.Should().Contain("Assembly load log including transitive references:"); - } - - [AvaloniaTest] - public async Task Selecting_Type_Node_Decompiles_Type_Definition() - { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); - - var coreLib = typeof(object).Assembly.GetName().Name!; - var typeNode = vm.AssemblyTreeModel.FindNode( - coreLib, "System", "System.Version"); - - vm.AssemblyTreeModel.SelectedItem = typeNode; - var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); - - tab.Text.Should().Contain("class Version"); - tab.Text.Should().Contain("Major"); - tab.Text.Should().Contain("Minor"); - } - - [AvaloniaTest] - public async Task Selecting_Property_Node_Decompiles_Property() - { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); - - var coreLib = typeof(object).Assembly.GetName().Name!; - var typeNode = vm.AssemblyTreeModel.FindNode( - coreLib, "System", "System.Version"); - typeNode.EnsureLazyChildren(); - var propertyNode = typeNode.Children.OfType() - .Single(p => p.PropertyDefinition.Name == "Major"); - - vm.AssemblyTreeModel.SelectedItem = propertyNode; - var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); - - tab.Text.Should().Contain("public int Major"); - } - - [AvaloniaTest] - public async Task Selecting_Field_Node_Decompiles_Field() - { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); - - var coreLib = typeof(object).Assembly.GetName().Name!; - var typeNode = vm.AssemblyTreeModel.FindNode( - coreLib, "System", "System.Math"); - typeNode.EnsureLazyChildren(); - var fieldNode = typeNode.Children.OfType() - .Single(f => f.FieldDefinition.Name == "PI"); - - vm.AssemblyTreeModel.SelectedItem = fieldNode; - var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); - - tab.Text.Should().Contain("public const double PI"); - tab.Text.Should().Contain("3.14159"); - } - - [AvaloniaTest] - public async Task Selecting_Event_Node_Decompiles_Event() - { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); - - var coreLib = typeof(object).Assembly.GetName().Name!; - var typeNode = vm.AssemblyTreeModel.FindNode( - coreLib, "System", "System.AppDomain"); - typeNode.EnsureLazyChildren(); - var eventNode = typeNode.Children.OfType() - .Single(e => e.EventDefinition.Name == "ProcessExit"); - - vm.AssemblyTreeModel.SelectedItem = eventNode; - var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); - - tab.Text.Should().Contain("event EventHandler"); - tab.Text.Should().Contain("ProcessExit"); - } - - [AvaloniaTest] - public async Task Selecting_Namespace_Node_Decompiles_Namespace() - { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); - - var coreLib = typeof(object).Assembly.GetName().Name!; - var namespaceNode = vm.AssemblyTreeModel.FindNode( - coreLib, "System.Runtime.Versioning"); - - vm.AssemblyTreeModel.SelectedItem = namespaceNode; - var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); - - tab.Text.Should().Contain("TargetFrameworkAttribute"); - tab.Text.Should().Contain("SupportedOSPlatformAttribute"); - } - - [AvaloniaTest] - public async Task Selecting_Assembly_Node_Emits_Header_And_Assembly_Attributes() - { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); - - var assemblyNode = vm.AssemblyTreeModel.FindNode("System.Linq"); - vm.AssemblyTreeModel.SelectedItem = assemblyNode; - - var tab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); - tab.Text.Should().Contain("// " + assemblyNode.LoadedAssembly.FileName); - tab.Text.Should().Contain("// System.Linq, Version="); - tab.Text.Should().Contain("// Global type: "); - tab.Text.Should().Contain("// Architecture: "); - tab.Text.Should().Contain("// Runtime: "); - tab.Text.Should().Contain("[assembly: AssemblyVersion("); - } - - [AvaloniaTest] - public async Task Selecting_Method_Auto_Loads_Referenced_Assemblies() - { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); - - var newAsmPath = typeof(System.Net.Http.HttpClient).Assembly.Location; - var registry = AppComposition.Current.GetExport(); - var openCommand = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Open)) - .CreateExport().Value; - openCommand.Execute(newAsmPath); - - await Waiters.WaitForAsync(() => - vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => - string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase))); - - var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .First(a => string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase)); - await loaded.GetLoadResultAsync(); - - var initialFiles = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .Select(a => a.FileName) - .ToHashSet(System.StringComparer.OrdinalIgnoreCase); - - var typeNode = vm.AssemblyTreeModel.FindNode( - loaded.ShortName, "System.Net.Http", "System.Net.Http.HttpClient"); - typeNode.EnsureLazyChildren(); - var methodNode = typeNode.Children.OfType() - .First(m => m.MethodDefinition.Name == "CancelPendingRequests"); - - vm.AssemblyTreeModel.SelectedItem = methodNode; - await vm.DockWorkspace.WaitForDecompiledTextAsync(); - - await Waiters.WaitForAsync(() => - vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => !initialFiles.Contains(a.FileName))); - - var addedAssemblies = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .Where(a => !initialFiles.Contains(a.FileName)) - .ToList(); - addedAssemblies.Should().NotBeEmpty(); - addedAssemblies.Should().OnlyContain(a => a.IsAutoLoaded); - - foreach (var auto in addedAssemblies) - { - var node = vm.AssemblyTreeModel.FindNode(auto.ShortName); - node.IsAutoLoaded.Should().BeTrue(); - } - } - - [AvaloniaTest] - public async Task Auto_Loaded_Assemblies_Are_Not_Persisted() - { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); - - var newAsmPath = typeof(System.Net.Http.HttpClient).Assembly.Location; - var registry = AppComposition.Current.GetExport(); - var openCommand = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Open)) - .CreateExport().Value; - openCommand.Execute(newAsmPath); - - await Waiters.WaitForAsync(() => - vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => - string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase))); - - var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .First(a => string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase)); - await loaded.GetLoadResultAsync(); - - var typeNode = vm.AssemblyTreeModel.FindNode( - loaded.ShortName, "System.Net.Http", "System.Net.Http.HttpClient"); - typeNode.EnsureLazyChildren(); - var methodNode = typeNode.Children.OfType() - .First(m => m.MethodDefinition.Name == "CancelPendingRequests"); - - vm.AssemblyTreeModel.SelectedItem = methodNode; - await vm.DockWorkspace.WaitForDecompiledTextAsync(); - - await Waiters.WaitForAsync(() => - vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => a.IsAutoLoaded)); - - var settingsService = AppComposition.Current.GetExport(); - var manager = settingsService.AssemblyListManager; - manager.SaveList(vm.AssemblyTreeModel.AssemblyList!); - - var settingsPath = ICSharpCode.ILSpyX.Settings.ILSpySettings.SettingsFilePathProvider!(); - var doc = XDocument.Load(settingsPath); - var savedFiles = doc.Descendants("Assembly") - .Select(e => e.Value) - .ToHashSet(System.StringComparer.OrdinalIgnoreCase); - - var assemblies = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies(); - var autoLoaded = assemblies.Where(a => a.IsAutoLoaded).ToList(); - autoLoaded.Should().NotBeEmpty(); - foreach (var asm in autoLoaded) - savedFiles.Should().NotContain(asm.FileName, "auto-loaded assembly {0} must not be persisted", asm.FileName); - - var manualLoaded = assemblies.Where(a => !a.IsAutoLoaded).ToList(); - foreach (var asm in manualLoaded) - savedFiles.Should().Contain(asm.FileName, "manually loaded assembly {0} must be persisted", asm.FileName); - } - - [AvaloniaTest] - public async Task Main_Menu_Items_Display_Input_Gestures() - { - var window = AppComposition.Current.GetExport(); - window.Show(); - - var menu = window.GetVisualDescendants().OfType().First(); - await Waiters.WaitForAsync(() => - menu.Items.OfType().Any(m => (string?)m.Tag == nameof(Resources._File)) - && menu.Items.OfType().Single(m => (string?)m.Tag == nameof(Resources._File)) - .Items.OfType().Any()); - - var fileMenu = menu.Items.OfType().Single(m => (string?)m.Tag == nameof(Resources._File)); - var openItem = fileMenu.Items.OfType() - .Single(m => string.Equals(m.Header as string, Resources._Open, System.StringComparison.Ordinal)); - - openItem.InputGesture.Should().NotBeNull(); - openItem.InputGesture!.Should().Be(KeyGesture.Parse("Ctrl+O")); - openItem.HotKey.Should().Be(KeyGesture.Parse("Ctrl+O")); - } - - [AvaloniaTest] - public async Task Opening_Assembly_Adds_It_To_The_Tree() - { - var window = AppComposition.Current.GetExport(); - window.Show(); - var vm = (MainWindowViewModel)window.DataContext!; - await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); - - var newAsmPath = typeof(System.Net.Http.HttpClient).Assembly.Location; - var initialCount = vm.AssemblyTreeModel.AssemblyList!.Count; - - var registry = AppComposition.Current.GetExport(); - var openCommand = registry.Commands - .Single(c => c.Metadata.Header == nameof(Resources._Open)) - .CreateExport().Value; - openCommand.Execute(newAsmPath); - - await Waiters.WaitForAsync(() => - vm.AssemblyTreeModel.AssemblyList!.GetAssemblies().Any(a => - string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase))); - - var loaded = vm.AssemblyTreeModel.AssemblyList!.GetAssemblies() - .First(a => string.Equals(a.FileName, newAsmPath, System.StringComparison.OrdinalIgnoreCase)); - await loaded.GetLoadResultAsync(); - - vm.AssemblyTreeModel.AssemblyList!.Count.Should().Be(initialCount + 1); - var node = vm.AssemblyTreeModel.FindNode(loaded.ShortName); - node.LoadedAssembly.Should().BeSameAs(loaded); - node.IsSelected.Should().BeTrue(); - } } diff --git a/ILSpy.Tests/Navigation/NavigationTests.cs b/ILSpy.Tests/Navigation/NavigationTests.cs new file mode 100644 index 000000000..586b9996a --- /dev/null +++ b/ILSpy.Tests/Navigation/NavigationTests.cs @@ -0,0 +1,77 @@ +// 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 ILSpy.AppEnv; +using ILSpy.TreeNodes; +using ILSpy.ViewModels; +using ILSpy.Views; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests; + +[TestFixture] +public class NavigationTests +{ + [AvaloniaTest] + public async Task Back_Navigation_Restores_Previously_Selected_Node() + { + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + var typeNode = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable"); + typeNode.EnsureLazyChildren(); + var firstMethod = typeNode.Children.OfType() + .Single(m => m.MethodDefinition.Name == "AsEnumerable"); + var secondMethod = typeNode.Children.OfType() + .First(m => m.MethodDefinition.Name == "Empty"); + + vm.AssemblyTreeModel.SelectedItem = firstMethod; + var firstTab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + firstTab.Text.Should().Contain("AsEnumerable"); + + // NavigationHistory collapses selections that happen within 0.5s into one entry. Wait + // past that window so the second selection records a real back-history entry. + await Task.Delay(600); + + vm.AssemblyTreeModel.SelectedItem = secondMethod; + await Waiters.WaitForAsync(() => ReferenceEquals(vm.AssemblyTreeModel.SelectedItem, secondMethod)); + var secondTab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + secondTab.Text.Should().Contain("Empty"); + + vm.DockWorkspace.NavigateBackCommand.CanExecute(null).Should().BeTrue(); + vm.DockWorkspace.NavigateBackCommand.Execute(null); + + await Waiters.WaitForAsync(() => ReferenceEquals(vm.AssemblyTreeModel.SelectedItem, firstMethod)); + var restoredTab = await vm.DockWorkspace.WaitForDecompiledTextAsync(); + restoredTab.Text.Should().Contain("AsEnumerable"); + restoredTab.Text.Should().Contain("return source"); + + firstMethod.Should().Be().CenteredInView(); + } +}