.NET Decompiler with support for PDB generation, ReadyToRun, Metadata (&more) - cross-platform!
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.
 
 
 

189 lines
7.5 KiB

// 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 Avalonia.Input.Platform;
using AvaloniaEdit;
using AvaloniaEdit.Search;
using AwesomeAssertions;
using ICSharpCode.ILSpy.Properties;
using ILSpy;
using ILSpy.AppEnv;
using ILSpy.TextView;
using ILSpy.TreeNodes;
using ILSpy.ViewModels;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests;
[TestFixture]
public class EditorCommandsTests
{
[AvaloniaTest]
public async Task Copy_Entry_Is_Registered_Under_The_Editor_Category()
{
// Mirrors WPF's CopyContextMenuEntry — exported with Header=Copy / Category=Editor so
// it lands under the editor's right-click menu next to other text-view actions.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var registry = AppComposition.Current.GetExport<ContextMenuEntryRegistry>();
registry.Entries.Should().Contain(
e => e.Metadata.Header == nameof(Resources.Copy)
&& e.Metadata.Category == nameof(Resources.Editor));
}
[AvaloniaTest]
public async Task SelectAll_Entry_Is_Registered_Under_The_Editor_Category()
{
// Mirrors WPF's SelectAllContextMenuEntry — Header=Select / Category=Editor.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var registry = AppComposition.Current.GetExport<ContextMenuEntryRegistry>();
registry.Entries.Should().Contain(
e => e.Metadata.Header == nameof(Resources.Select)
&& e.Metadata.Category == nameof(Resources.Editor));
}
[AvaloniaTest]
public async Task Copy_Entry_Reflects_Editor_Selection_State()
{
// Visible whenever a text view is the source; enabled only when the editor has a
// non-empty selection. Mirrors WPF: SelectionLength > 0 gates the "Copy" item.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
// Single-method target — decompiling the full Enumerable class can exceed
// Waiters.WaitForAsync's 15s timeout (300+ LINQ methods); a single method body
// is sub-second. Uses the strong completion signal (IsDecompiling: false +
// non-empty Text) instead of polling the editor's TextLength, which races the
// model→view binding and can latch on stale state from previous tests.
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
typeNode.IsExpanded = true;
var methodNode = typeNode.Children.OfType<MethodTreeNode>()
.First(m => m.MethodDefinition.Name == "AsEnumerable");
vm.AssemblyTreeModel.SelectNode(methodNode);
await vm.DockWorkspace.WaitForDecompiledTextAsync();
var view = await window.WaitForComponent<DecompilerTextView>();
await view.WaitForComponent<TextEditor>();
var copy = LocateEntry(nameof(Resources.Copy));
var ctx = new TextViewContext { TextView = view };
copy.IsVisible(ctx).Should().BeTrue();
view.Editor.Select(0, 0);
copy.IsEnabled(ctx).Should().BeFalse();
view.Editor.Select(0, 5);
copy.IsEnabled(ctx).Should().BeTrue();
// Without a text view, the entry must hide entirely.
var blank = new TextViewContext();
copy.IsVisible(blank).Should().BeFalse();
}
[AvaloniaTest]
public async Task Copy_Execute_Writes_The_Selection_To_The_Clipboard()
{
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
typeNode.IsExpanded = true;
var methodNode = typeNode.Children.OfType<MethodTreeNode>()
.First(m => m.MethodDefinition.Name == "AsEnumerable");
vm.AssemblyTreeModel.SelectNode(methodNode);
await vm.DockWorkspace.WaitForDecompiledTextAsync();
var view = await window.WaitForComponent<DecompilerTextView>();
await view.WaitForComponent<TextEditor>();
var copy = LocateEntry(nameof(Resources.Copy));
view.Editor.Select(0, 6);
var expected = view.Editor.Document.GetText(0, 6);
copy.Execute(new TextViewContext { TextView = view });
var clipboard = global::Avalonia.Controls.TopLevel.GetTopLevel(view)?.Clipboard;
var actual = await clipboard!.TryGetTextAsync();
actual.Should().Be(expected);
}
[AvaloniaTest]
public async Task SelectAll_Execute_Highlights_The_Entire_Document()
{
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
typeNode.IsExpanded = true;
var methodNode = typeNode.Children.OfType<MethodTreeNode>()
.First(m => m.MethodDefinition.Name == "AsEnumerable");
vm.AssemblyTreeModel.SelectNode(methodNode);
await vm.DockWorkspace.WaitForDecompiledTextAsync();
var view = await window.WaitForComponent<DecompilerTextView>();
await view.WaitForComponent<TextEditor>();
var selectAll = LocateEntry(nameof(Resources.Select));
view.Editor.Select(0, 0);
selectAll.Execute(new TextViewContext { TextView = view });
view.Editor.SelectionLength.Should().Be(view.Editor.Document.TextLength);
}
[AvaloniaTest]
public async Task DecompilerTextView_Installs_AvaloniaEdits_SearchPanel()
{
// Ctrl+F opens AvaloniaEdit's built-in search overlay; the panel must be installed on
// the editor's TextArea. SearchPanel is an Avalonia control; SearchInputHandler is
// what wires up Ctrl+F + nested handler registration.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var typeNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
vm.AssemblyTreeModel.SelectNode(typeNode);
var view = await window.WaitForComponent<DecompilerTextView>();
await view.WaitForComponent<TextEditor>();
view.SearchPanel.Should().NotBeNull(
"DecompilerTextView must install AvaloniaEdit's SearchPanel so Ctrl+F brings up the find bar");
}
static IContextMenuEntry LocateEntry(string header)
{
var registry = AppComposition.Current.GetExport<ContextMenuEntryRegistry>();
return registry.Entries.Single(e => e.Metadata.Header == header).Value;
}
}