@ -19,9 +19,12 @@
@@ -19,9 +19,12 @@
using System.Linq ;
using System.Threading.Tasks ;
using Avalonia ;
using Avalonia.Controls ;
using Avalonia.Headless ;
using Avalonia.Headless.NUnit ;
using Avalonia.Input ;
using Avalonia.Interactivity ;
using Avalonia.VisualTree ;
using AwesomeAssertions ;
@ -29,8 +32,10 @@ using AwesomeAssertions;
@@ -29,8 +32,10 @@ using AwesomeAssertions;
using ICSharpCode.ILSpy.Properties ;
using ICSharpCode.ILSpy.AppEnv ;
using ICSharpCode.ILSpy.AssemblyTree ;
using ICSharpCode.ILSpy.Commands ;
using ICSharpCode.ILSpy.Docking ;
using ICSharpCode.ILSpy.TextView ;
using ICSharpCode.ILSpy.TreeNodes ;
using ICSharpCode.ILSpy.ViewModels ;
using ICSharpCode.ILSpy.Views ;
@ -121,6 +126,118 @@ public class BrowseBackForwardCommandTests
@@ -121,6 +126,118 @@ public class BrowseBackForwardCommandTests
"after one back-step the forward stack should be non-empty" ) ;
}
[AvaloniaTest]
public async Task Mouse_Back_And_Forward_Buttons_Navigate_The_History ( )
{
// The extra mouse buttons (XButton1 = back, XButton2 = forward) drive the same history
// as Alt+Left / Alt+Right, matching browsers and the WPF version (where WPF itself
// translated the buttons into BrowseBack/BrowseForward commands). Avalonia has no such
// translation, so MainWindow routes the pointer events to the navigation commands.
// Arrange — build a two-entry history exactly like the menu-driven test above.
var ( window , vm ) = await TestHarness . BootAsync ( 3 ) ;
var ( firstMethod , secondMethod ) = await BuildTwoEntryHistoryAsync ( vm ) ;
// Act — click mouse-back anywhere in the window.
var point = new Point ( 1 0 0 , 1 0 0 ) ;
window . MouseDown ( point , MouseButton . XButton1 ) ;
window . MouseUp ( point , MouseButton . XButton1 ) ;
// Assert — selection rewinds, then mouse-forward replays the step.
await Waiters . WaitForAsync ( ( ) = > ReferenceEquals ( vm . AssemblyTreeModel . SelectedItem , firstMethod ) ,
description : "XButton1 must navigate back one history entry" ) ;
await Waiters . WaitForAsync ( ( ) = > vm . DockWorkspace . NavigateForwardCommand . CanExecute ( null ) ,
description : "after one back-step the forward stack should be non-empty" ) ;
window . MouseDown ( point , MouseButton . XButton2 ) ;
window . MouseUp ( point , MouseButton . XButton2 ) ;
await Waiters . WaitForAsync ( ( ) = > ReferenceEquals ( vm . AssemblyTreeModel . SelectedItem , secondMethod ) ,
description : "XButton2 must navigate forward one history entry" ) ;
}
[AvaloniaTest]
public async Task Mouse_Back_Button_Press_Does_Not_Reach_The_Control_Under_The_Pointer ( )
{
// The X buttons are navigation gestures, not clicks (WPF never delivered them to the
// control under the pointer). The press must not activate the pane under the pointer,
// move keyboard focus, or toggle a folding marker; only the release navigates, and the
// active pane stays where it was across the navigation.
// Arrange — two-entry history, assembly pane active, pointer over the editor.
var ( window , vm ) = await TestHarness . BootAsync ( 3 ) ;
var ( firstMethod , _ ) = await BuildTwoEntryHistoryAsync ( vm ) ;
var view = await window . WaitForComponent < DecompilerTextView > ( ) ;
vm . DockWorkspace . ShowToolPane ( AssemblyTreeModel . PaneContentId ) ;
var activePane = vm . DockWorkspace . Layout . FocusedDockable ;
activePane . Should ( ) . NotBeNull ( "showing the assembly pane must make it the focused dockable" ) ;
var focusedElement = window . FocusManager ? . GetFocusedElement ( ) ;
int pressedInEditor = 0 ;
view . AddHandler ( InputElement . PointerPressedEvent , ( _ , _ ) = > pressedInEditor + + ,
RoutingStrategies . Tunnel | RoutingStrategies . Bubble ) ;
var point = view . TranslatePoint ( new Point ( view . Bounds . Width / 2 , view . Bounds . Height / 2 ) , window ) ;
point . Should ( ) . NotBeNull ( "the editor centre must map into the test window" ) ;
// Act / Assert — the press is swallowed at the window ...
window . MouseDown ( point ! . Value , MouseButton . XButton1 ) ;
pressedInEditor . Should ( ) . Be ( 0 , "an X-button press must not reach the control under the pointer" ) ;
vm . DockWorkspace . Layout . FocusedDockable . Should ( ) . BeSameAs ( activePane ,
"pressing a mouse navigation button must not activate the pane under the pointer" ) ;
ReferenceEquals ( window . FocusManager ? . GetFocusedElement ( ) , focusedElement ) . Should ( ) . BeTrue (
"pressing a mouse navigation button must not move keyboard focus" ) ;
// ... and the release navigates without moving the active pane to the editor.
window . MouseUp ( point . Value , MouseButton . XButton1 ) ;
await Waiters . WaitForAsync ( ( ) = > ReferenceEquals ( vm . AssemblyTreeModel . SelectedItem , firstMethod ) ,
description : "XButton1 must navigate back one history entry" ) ;
vm . DockWorkspace . Layout . FocusedDockable . Should ( ) . BeSameAs ( activePane ,
"navigating back must not move the active pane to the editor" ) ;
}
[AvaloniaTest]
public async Task Browse_Back_Keeps_The_Active_Pane ( )
{
// Back/Forward re-select a tree node and restore the tab's view state; the tab being
// navigated is already the active document, so the navigation must not move the active
// pane to it (WPF kept the current view focused). Exercises the command directly, which
// is what the Alt+Left key binding and the View menu invoke.
var ( _ , vm ) = await TestHarness . BootAsync ( 3 ) ;
var ( firstMethod , _ ) = await BuildTwoEntryHistoryAsync ( vm ) ;
vm . DockWorkspace . ShowToolPane ( AssemblyTreeModel . PaneContentId ) ;
var activePane = vm . DockWorkspace . Layout . FocusedDockable ;
activePane . Should ( ) . NotBeNull ( "showing the assembly pane must make it the focused dockable" ) ;
vm . DockWorkspace . NavigateBackCommand . Execute ( null ) ;
await Waiters . WaitForAsync ( ( ) = > ReferenceEquals ( vm . AssemblyTreeModel . SelectedItem , firstMethod ) ,
description : "BrowseBack must navigate back one history entry" ) ;
await vm . DockWorkspace . WaitForDecompiledTextAsync ( ) ;
vm . DockWorkspace . Layout . FocusedDockable . Should ( ) . BeSameAs ( activePane ,
"navigating back must not move the active pane to the editor" ) ;
}
// Selects two methods of System.Linq.Enumerable with a pause in between so the history records
// them as two separate entries; returns them in selection order.
static async Task < ( MethodTreeNode First , MethodTreeNode Second ) > BuildTwoEntryHistoryAsync ( MainWindowViewModel vm )
{
var typeNode = vm . AssemblyTreeModel . FindNode < TypeTreeNode > (
"System.Linq" , "System.Linq" , "System.Linq.Enumerable" ) ;
typeNode . IsExpanded = true ;
var firstMethod = typeNode . Children . OfType < MethodTreeNode > ( )
. Single ( m = > m . MethodDefinition . Name = = "AsEnumerable" ) ;
var secondMethod = typeNode . Children . OfType < MethodTreeNode > ( )
. First ( m = > m . MethodDefinition . Name = = "Empty" ) ;
vm . AssemblyTreeModel . SelectNode ( firstMethod ) ;
await vm . DockWorkspace . WaitForDecompiledTextAsync ( ) ;
await Task . Delay ( 6 0 0 ) ;
vm . AssemblyTreeModel . SelectNode ( secondMethod ) ;
await vm . DockWorkspace . WaitForDecompiledTextAsync ( ) ;
return ( firstMethod , secondMethod ) ;
}
[AvaloniaTest]
public void BrowseBack_MenuItem_Carries_The_Alt_Left_Gesture ( )
{