Explorar o código

Added low-level Focus tests

Tig hai 1 ano
pai
achega
3f19a6f04a

+ 5 - 58
Terminal.Gui/Application/Application.Navigation.cs

@@ -1,4 +1,6 @@
 #nullable enable
+using System.Diagnostics;
+using System.Reflection.PortableExecutable;
 using System.Security.Cryptography;
 
 namespace Terminal.Gui;
@@ -31,61 +33,6 @@ internal static class ApplicationNavigation
         return view;
     }
 
-    /// <summary>
-    /// Sets the focus to the next view in the specified direction within the provided list of views.
-    /// If the end of the list is reached, the focus wraps around to the first view in the list.
-    /// The method considers the current focused view (`Application.Current`) and attempts to move the focus
-    /// to the next view in the specified direction. If the focus cannot be set to the next view, it wraps around
-    /// to the first view in the list.
-    /// </summary>
-    /// <param name="viewsInTabIndexes"></param>
-    /// <param name="direction"></param>
-    internal static void SetFocusToNextViewWithWrap (IEnumerable<View>? viewsInTabIndexes, NavigationDirection direction)
-    {
-        if (viewsInTabIndexes is null)
-        {
-            return;
-        }
-
-        bool foundCurrentView = false;
-        bool focusSet = false;
-        IEnumerable<View> indexes = viewsInTabIndexes as View [] ?? viewsInTabIndexes.ToArray ();
-        int viewCount = indexes.Count ();
-        int currentIndex = 0;
-
-        foreach (View view in indexes)
-        {
-            if (view == Application.Current)
-            {
-                foundCurrentView = true;
-            }
-            else if (foundCurrentView && !focusSet)
-            {
-                // One of the views is Current, but view is not. Attempt to Advance...
-                Application.Current!.SuperView?.AdvanceFocus (direction);
-                // QUESTION: AdvanceFocus returns false AND sets Focused to null if no view was found to advance to. Should't we only set focusProcessed if it returned true?
-                focusSet = true;
-
-                if (Application.Current.SuperView?.Focused != Application.Current)
-                {
-                    return;
-                }
-
-                // Either AdvanceFocus didn't set focus or the view it set focus to is not current...
-                // continue...
-            }
-
-            currentIndex++;
-
-            if (foundCurrentView && !focusSet && currentIndex == viewCount)
-            {
-                // One of the views is Current AND AdvanceFocus didn't set focus AND we are at the last view in the list...
-                // This means we should wrap around to the first view in the list.
-                indexes.First ().SetFocus ();
-            }
-        }
-    }
-
     /// <summary>
     ///     Moves the focus to the next focusable view.
     ///     Honors <see cref="ViewArrangement.Overlapped"/> and will only move to the next subview
@@ -107,7 +54,7 @@ internal static class ApplicationNavigation
         }
         else
         {
-            SetFocusToNextViewWithWrap (Application.Current.SuperView?.TabIndexes, NavigationDirection.Forward);
+            ApplicationOverlapped.SetFocusToNextViewWithWrap (Application.Current.SuperView?.TabIndexes, NavigationDirection.Forward);
         }
     }
 
@@ -132,7 +79,7 @@ internal static class ApplicationNavigation
             }
             else
             {
-                SetFocusToNextViewWithWrap (Application.Current.SuperView?.TabIndexes, NavigationDirection.Forward);
+                ApplicationOverlapped.SetFocusToNextViewWithWrap (Application.Current.SuperView?.TabIndexes, NavigationDirection.Forward);
             }
 
 
@@ -173,7 +120,7 @@ internal static class ApplicationNavigation
         }
         else
         {
-            SetFocusToNextViewWithWrap (Application.Current.SuperView?.TabIndexes?.Reverse (), NavigationDirection.Backward);
+            ApplicationOverlapped.SetFocusToNextViewWithWrap (Application.Current.SuperView?.TabIndexes?.Reverse (), NavigationDirection.Backward);
         }
     }
 

+ 60 - 0
Terminal.Gui/Application/Application.Overlapped.cs

@@ -1,4 +1,5 @@
 #nullable enable
+using System.Diagnostics;
 using System.Reflection;
 
 namespace Terminal.Gui;
@@ -111,6 +112,65 @@ public static class ApplicationOverlapped
         return null;
     }
 
+
+    /// <summary>
+    /// Sets the focus to the next view in the specified direction within the provided list of views.
+    /// If the end of the list is reached, the focus wraps around to the first view in the list.
+    /// The method considers the current focused view (`Application.Current`) and attempts to move the focus
+    /// to the next view in the specified direction. If the focus cannot be set to the next view, it wraps around
+    /// to the first view in the list.
+    /// </summary>
+    /// <param name="viewsInTabIndexes"></param>
+    /// <param name="direction"></param>
+    internal static void SetFocusToNextViewWithWrap (IEnumerable<View>? viewsInTabIndexes, NavigationDirection direction)
+    {
+        if (viewsInTabIndexes is null)
+        {
+            return;
+        }
+
+        // This code-path only executes in obtuse IsOverlappedContainer scenarios.
+        Debug.Assert (Application.Current!.IsOverlappedContainer);
+
+        bool foundCurrentView = false;
+        bool focusSet = false;
+        IEnumerable<View> indexes = viewsInTabIndexes as View [] ?? viewsInTabIndexes.ToArray ();
+        int viewCount = indexes.Count ();
+        int currentIndex = 0;
+
+        foreach (View view in indexes)
+        {
+            if (view == Application.Current)
+            {
+                foundCurrentView = true;
+            }
+            else if (foundCurrentView && !focusSet)
+            {
+                // One of the views is Current, but view is not. Attempt to Advance...
+                Application.Current!.SuperView?.AdvanceFocus (direction);
+                // QUESTION: AdvanceFocus returns false AND sets Focused to null if no view was found to advance to. Should't we only set focusProcessed if it returned true?
+                focusSet = true;
+
+                if (Application.Current.SuperView?.Focused != Application.Current)
+                {
+                    return;
+                }
+
+                // Either AdvanceFocus didn't set focus or the view it set focus to is not current...
+                // continue...
+            }
+
+            currentIndex++;
+
+            if (foundCurrentView && !focusSet && currentIndex == viewCount)
+            {
+                // One of the views is Current AND AdvanceFocus didn't set focus AND we are at the last view in the list...
+                // This means we should wrap around to the first view in the list.
+                indexes.First ().SetFocus ();
+            }
+        }
+    }
+
     /// <summary>
     ///     Move to the next Overlapped child from the <see cref="OverlappedTop"/> and set it as the <see cref="Application.Top"/> if
     ///     it is not already.

+ 86 - 0
UnitTests/Application/Application.NavigationTests.cs

@@ -0,0 +1,86 @@
+using Moq;
+using Xunit.Abstractions;
+
+namespace Terminal.Gui.ApplicationTests;
+
+public class ApplicationNavigationTests (ITestOutputHelper output)
+{
+    private readonly ITestOutputHelper _output = output;
+
+    [Fact]
+    public void GetDeepestFocusedSubview_ShouldReturnNull_WhenViewIsNull ()
+    {
+        // Act
+        var result = ApplicationNavigation.GetDeepestFocusedSubview (null);
+
+        // Assert
+        Assert.Null (result);
+    }
+
+    [Fact]
+    public void GetDeepestFocusedSubview_ShouldReturnSameView_WhenNoSubviewsHaveFocus ()
+    {
+        // Arrange
+        var view = new View () { Id = "view", CanFocus = true };;
+
+        // Act
+        var result = ApplicationNavigation.GetDeepestFocusedSubview (view);
+
+        // Assert
+        Assert.Equal (view, result);
+    }
+
+    [Fact]
+    public void GetDeepestFocusedSubview_ShouldReturnFocusedSubview ()
+    {
+        // Arrange
+        var parentView = new View () { Id = "parentView", CanFocus = true };;
+        var childView1 = new View () { Id = "childView1", CanFocus = true };;
+        var childView2 = new View () { Id = "childView2", CanFocus = true };;
+        var grandChildView = new View () { Id = "grandChildView", CanFocus = true };;
+
+        parentView.Add (childView1, childView2);
+        childView2.Add (grandChildView);
+
+        grandChildView.SetFocus ();
+
+        // Act
+        var result = ApplicationNavigation.GetDeepestFocusedSubview (parentView);
+
+        // Assert
+        Assert.Equal (grandChildView, result);
+    }
+
+    [Fact]
+    public void GetDeepestFocusedSubview_ShouldReturnDeepestFocusedSubview ()
+    {
+        // Arrange
+        var parentView = new View () { Id = "parentView", CanFocus = true };;
+        var childView1 = new View () { Id = "childView1", CanFocus = true };;
+        var childView2 = new View () { Id = "childView2", CanFocus = true };;
+        var grandChildView = new View () { Id = "grandChildView", CanFocus = true };;
+        var greatGrandChildView = new View () { Id = "greatGrandChildView", CanFocus = true };;
+
+        parentView.Add (childView1, childView2);
+        childView2.Add (grandChildView);
+        grandChildView.Add (greatGrandChildView);
+
+        grandChildView.SetFocus();
+
+        // Act
+        var result = ApplicationNavigation.GetDeepestFocusedSubview (parentView);
+
+        // Assert
+        Assert.Equal (greatGrandChildView, result);
+
+        // Arrange
+        greatGrandChildView.CanFocus = false;
+        grandChildView.SetFocus ();
+
+        // Act
+        result = ApplicationNavigation.GetDeepestFocusedSubview (parentView);
+
+        // Assert
+        Assert.Equal (grandChildView, result);
+    }
+}

+ 4 - 3
UnitTests/UnitTests.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <!-- Version numbers are automatically updated by gitversion when a release is released -->
     <!-- In the source tree the version will always be 2.0 for all projects. -->
@@ -31,8 +31,9 @@
   <ItemGroup>
     <PackageReference Include="JetBrains.Annotations" Version="[2024.2.0,)" PrivateAssets="all" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="[17.10,18)" />
-    <PackageReference Include="ReportGenerator" Version="[5.3.7,6)" />
-    <PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="[21.0.22,22)" />
+    <PackageReference Include="Moq" Version="4.20.70" />
+    <PackageReference Include="ReportGenerator" Version="5.3.8" />
+    <PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="21.0.29" />
     <PackageReference Include="xunit" Version="[2.9.0,3)" />
     <PackageReference Include="Xunit.Combinatorial" Version="[1.6.24,2)" />
     <PackageReference Include="xunit.runner.visualstudio" Version="[2.8.2,3)">

+ 105 - 0
UnitTests/Views/OverlappedTests.cs

@@ -1199,4 +1199,109 @@ public class OverlappedTests
         win1.Dispose ();
         top.Dispose ();
     }
+
+
+    [Fact]
+    public void SetFocusToNextViewWithWrap_ShouldFocusNextView ()
+    {
+        // Arrange
+        var superView = new TestToplevel () { Id = "superView", IsOverlappedContainer = true };
+
+        var view1 = new TestView () { Id = "view1" };
+        var view2 = new TestView () { Id = "view2" };
+        var view3 = new TestView () { Id = "view3" }; ;
+        superView.Add (view1, view2, view3);
+
+        var current = new TestToplevel () { Id = "current", IsOverlappedContainer = true };
+
+        superView.Add (current);
+        superView.BeginInit ();
+        superView.EndInit ();
+        current.SetFocus ();
+
+        Application.Current = current;
+        Assert.True (current.HasFocus);
+        Assert.Equal (superView.Focused, current);
+        Assert.Equal (superView.MostFocused, current);
+
+        // Act
+        ApplicationOverlapped.SetFocusToNextViewWithWrap (Application.Current.SuperView.TabIndexes, NavigationDirection.Forward);
+
+        // Assert
+        Assert.True (view1.HasFocus);
+    }
+
+    [Fact]
+    public void SetFocusToNextViewWithWrap_ShouldNotChangeFocusIfViewsIsNull ()
+    {
+        // Arrange
+        var currentView = new TestToplevel ();
+        Application.Current = currentView;
+
+        // Act
+        ApplicationOverlapped.SetFocusToNextViewWithWrap (null, NavigationDirection.Forward);
+
+        // Assert
+        Assert.Equal (currentView, Application.Current);
+    }
+
+    [Fact]
+    public void SetFocusToNextViewWithWrap_ShouldNotChangeFocusIfCurrentViewNotFound ()
+    {
+        // Arrange
+        var view1 = new TestToplevel ();
+        var view2 = new TestToplevel ();
+        var view3 = new TestToplevel ();
+
+        var views = new List<View> { view1, view2, view3 };
+
+        var currentView = new TestToplevel () { IsOverlappedContainer = true }; // Current view is not in the list
+        Application.Current = currentView;
+
+        // Act
+        ApplicationOverlapped.SetFocusToNextViewWithWrap (views, NavigationDirection.Forward);
+
+        // Assert
+        Assert.False (view1.IsFocused);
+        Assert.False (view2.IsFocused);
+        Assert.False (view3.IsFocused);
+    }
+
+    private class TestToplevel : Toplevel
+    {
+        public bool IsFocused { get; private set; }
+
+        public override bool OnEnter (View view)
+        {
+            IsFocused = true;
+            return base.OnEnter (view);
+        }
+
+        public override bool OnLeave (View view)
+        {
+            IsFocused = false;
+            return base.OnLeave (view);
+        }
+    }
+
+    private class TestView : View
+    {
+        public TestView ()
+        {
+            CanFocus = true;
+        }
+        public bool IsFocused { get; private set; }
+
+        public override bool OnEnter (View view)
+        {
+            IsFocused = true;
+            return base.OnEnter (view);
+        }
+
+        public override bool OnLeave (View view)
+        {
+            IsFocused = false;
+            return base.OnLeave (view);
+        }
+    }
 }