.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.
 
 
 

185 lines
9.1 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 AwesomeAssertions;
using ILSpy.Navigation;
using ILSpy.TextView;
using ILSpy.TreeNodes;
using NUnit.Framework;
using FoldingSnapshot = ILSpy.TextView.FoldingsViewState.Snapshot;
namespace ICSharpCode.ILSpy.Tests.Navigation;
/// <summary>
/// End-to-end: select node A, move the caret, select node B, navigate Back. The caret
/// must land where the user left it on A — that's the contract for Back/Forward to
/// feel like a real browser instead of a "reset to top" gesture.
/// </summary>
[TestFixture]
public class ViewStateRoundTripTests
{
[AvaloniaTest]
public async Task Back_Restores_Caret_Position_The_User_Left_On_The_Previous_Node()
{
var (_, vm) = await TestHarness.BootAsync();
var dockWorkspace = vm.DockWorkspace;
// Pick two distinct decompiler targets so the navigation actually moves between
// them. CoreLib's System.Object and System.String are both always present.
var coreLibName = typeof(object).Assembly.GetName().Name!;
var objectNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(coreLibName, "System", "System.Object");
var stringNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(coreLibName, "System", "System.String");
vm.AssemblyTreeModel.SelectNode(objectNode);
await dockWorkspace.WaitForDecompiledTextAsync();
var tab = dockWorkspace.ActiveDecompilerTab!;
// State is pulled from the editor on demand (CaptureViewState) when DockWorkspace records
// a navigation away -- not pushed per caret/scroll event. Headless has no laid-out editor,
// so override the pull delegate to report the position the user "left" on node A.
tab.CaptureViewState = () => new DecompilerTextViewState(500, 120.5, 7.25, null);
// Navigate to a different node — DockWorkspace pulls A's state into the current history
// entry and records B as the new current.
vm.AssemblyTreeModel.SelectNode(stringNode);
await dockWorkspace.WaitForDecompiledTextAsync();
// Verify the capture: the back stack's most recent entry should be A's, with the pulled
// caret + scroll values.
var backEntries = dockWorkspace.BackHistory.OfType<TreeNodeEntry>().ToList();
backEntries.Should().NotBeEmpty("Select(B) must push A onto the back stack");
var captured = backEntries.Last();
ReferenceEquals(captured.Node, objectNode).Should().BeTrue(
"captured back-stack entry must reference node A");
captured.CaretOffset.Should().Be(500);
captured.VerticalOffset.Should().Be(120.5);
captured.HorizontalOffset.Should().Be(7.25);
// Navigate Back. ApplyNavigationTarget synchronously stashes the recorded state on the tab
// as PendingViewState before the async decompile runs; the editor consumes it in
// ApplyDocument once Text lands. Assert the restore intent here, before the await lets the
// view consume it -- applying caret/scroll to the live editor is the view's job (covered by
// the real UI; headless has no rendered viewport).
dockWorkspace.NavigateBackCommand.Execute(null);
tab.PendingViewState.Should().NotBeNull("Back must stash the recorded view state for the editor to apply");
tab.PendingViewState!.Value.CaretOffset.Should().Be(500,
"Back must restore the caret to where the user left it before navigating to B");
tab.PendingViewState!.Value.VerticalOffset.Should().Be(120.5,
"Back must restore the vertical scroll offset the user left on A");
await dockWorkspace.WaitForDecompiledTextAsync();
// The editor consumes and clears PendingViewState once the document lands.
tab.PendingViewState.Should().BeNull("the editor must consume the pending state after applying it");
}
[AvaloniaTest]
public async Task Back_Carries_Expanded_Foldings_Snapshot_From_Capture_Through_To_Pending()
{
// Pins the foldings half of the view-state round trip end-to-end through DockWorkspace.
// The headless editor doesn't build real foldings, so override the pull delegate to report
// a deterministic snapshot, exercise the navigation pipeline, and assert the snapshot
// survives the capture -> entry -> pending hops.
var (_, vm) = await TestHarness.BootAsync();
var dockWorkspace = vm.DockWorkspace;
var coreLibName = typeof(object).Assembly.GetName().Name!;
var objectNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(coreLibName, "System", "System.Object");
var stringNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(coreLibName, "System", "System.String");
vm.AssemblyTreeModel.SelectNode(objectNode);
await dockWorkspace.WaitForDecompiledTextAsync();
var tab = dockWorkspace.ActiveDecompilerTab!;
// Deterministic foldings snapshot — two expanded regions over a four-folding layout.
// Compute via the helper to keep the checksum honest. (The snapshot itself is exercised
// independently in FoldingsViewStateTests.)
var seeded = FoldingsViewState.Capture(new[] {
(Start: 10, End: 50, IsFolded: false),
(Start: 60, End: 100, IsFolded: true),
(Start: 110, End: 200, IsFolded: false),
(Start: 210, End: 250, IsFolded: true),
});
tab.CaptureViewState = () => new DecompilerTextViewState(0, 0, 0, seeded);
// Navigate to a different node — DockWorkspace pulls the state and stamps the foldings
// onto the OUTGOING entry on the back stack.
vm.AssemblyTreeModel.SelectNode(stringNode);
await dockWorkspace.WaitForDecompiledTextAsync();
// Verify the capture: the back-stack entry for node A carries the snapshot.
var captured = dockWorkspace.BackHistory.OfType<TreeNodeEntry>().Last();
ReferenceEquals(captured.Node, objectNode).Should().BeTrue(
"captured back-stack entry must reference node A");
captured.Foldings.Should().NotBeNull("Select(B) must record A's foldings into the back stack");
captured.Foldings!.Value.Checksum.Should().Be(seeded.Checksum);
captured.Foldings.Value.Expanded.Should().Equal(seeded.Expanded);
// Navigate Back. ApplyNavigationTarget stashes the recorded snapshot into PendingViewState
// for the view to consume on the next ApplyDocument. The assignment is synchronous, so we
// can observe it before the await lets the editor consume it.
dockWorkspace.NavigateBackCommand.Execute(null);
tab.PendingViewState.Should().NotBeNull("Back must propagate the captured state to the destination tab");
tab.PendingViewState!.Value.Foldings.Should().NotBeNull();
tab.PendingViewState!.Value.Foldings!.Value.Checksum.Should().Be(seeded.Checksum);
}
[AvaloniaTest]
public async Task Forward_Is_Wired_Through_The_Toolbar_And_Key_Binding()
{
// Pins the existing Forward command wiring: the tracker had listed BrowseForward
// as missing, but it was already implemented. This test makes a regression in any
// of the three wirings (command, toolbar button, Alt+Right) detectable.
var (_, vm) = await TestHarness.BootAsync();
((object?)vm.DockWorkspace.NavigateForwardCommand).Should().NotBeNull();
((object?)vm.DockWorkspace.NavigateBackCommand).Should().NotBeNull();
// Build up history: A → B, then go Back so Forward is enabled.
var coreLibName = typeof(object).Assembly.GetName().Name!;
var objectNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(coreLibName, "System", "System.Object");
var stringNode = vm.AssemblyTreeModel.FindNode<TypeTreeNode>(coreLibName, "System", "System.String");
vm.AssemblyTreeModel.SelectNode(objectNode);
await vm.DockWorkspace.WaitForDecompiledTextAsync();
vm.AssemblyTreeModel.SelectNode(stringNode);
await vm.DockWorkspace.WaitForDecompiledTextAsync();
vm.DockWorkspace.NavigateBackCommand.Execute(null);
await vm.DockWorkspace.WaitForDecompiledTextAsync();
vm.DockWorkspace.NavigateForwardCommand.CanExecute(null).Should().BeTrue(
"Forward must be enabled after Back leaves an entry on the forward stack");
vm.DockWorkspace.NavigateForwardCommand.Execute(null);
await vm.DockWorkspace.WaitForDecompiledTextAsync();
// Back through Forward should land on B again.
ReferenceEquals(vm.AssemblyTreeModel.SelectedItem, stringNode).Should().BeTrue(
"NavigateForward must restore the tree selection to the entry that was just popped from forward");
}
}