瀏覽代碼

Fixes #4009 - fix tree ordering (#4015)

Thomas Nind 5 月之前
父節點
當前提交
4d87d5f249

+ 2 - 2
Terminal.Gui/Views/TableView/TreeTableSource.cs

@@ -87,8 +87,8 @@ public class TreeTableSource<T> : IEnumerableTableSource<T>, IDisposable where T
         Branch<T> branch = RowToBranch (row);
 
         // Everything on line before the expansion run and branch text
-        Rune [] prefix = branch.GetLinePrefix (Application.Driver).ToArray ();
-        Rune expansion = branch.GetExpandableSymbol (Application.Driver);
+        Rune [] prefix = branch.GetLinePrefix ().ToArray ();
+        Rune expansion = branch.GetExpandableSymbol ();
         string lineBody = _tree.AspectGetter (branch.Model) ?? "";
 
         var sb = new StringBuilder ();

+ 91 - 89
Terminal.Gui/Views/TreeView/Branch.cs

@@ -1,8 +1,10 @@
-namespace Terminal.Gui;
+#nullable enable
+
+namespace Terminal.Gui;
 
 internal class Branch<T> where T : class
 {
-    private readonly TreeView<T> tree;
+    private readonly TreeView<T> _tree;
 
     /// <summary>
     ///     Declares a new branch of <paramref name="tree"/> in which the users object <paramref name="model"/> is
@@ -11,9 +13,9 @@ internal class Branch<T> where T : class
     /// <param name="tree">The UI control in which the branch resides.</param>
     /// <param name="parentBranchIfAny">Pass null for root level branches, otherwise pass the parent.</param>
     /// <param name="model">The user's object that should be displayed.</param>
-    public Branch (TreeView<T> tree, Branch<T> parentBranchIfAny, T model)
+    public Branch (TreeView<T> tree, Branch<T>? parentBranchIfAny, T model)
     {
-        this.tree = tree;
+        _tree = tree;
         Model = model;
 
         if (parentBranchIfAny is { })
@@ -27,7 +29,7 @@ internal class Branch<T> where T : class
     ///     The children of the current branch.  This is null until the first call to <see cref="FetchChildren"/> to avoid
     ///     enumerating the entire underlying hierarchy.
     /// </summary>
-    public Dictionary<T, Branch<T>> ChildBranches { get; set; }
+    public List<Branch<T>>? ChildBranches { get; set; }
 
     /// <summary>The depth of the current branch.  Depth of 0 indicates root level branches.</summary>
     public int Depth { get; }
@@ -39,7 +41,7 @@ internal class Branch<T> where T : class
     public T Model { get; private set; }
 
     /// <summary>The parent <see cref="Branch{T}"/> or null if it is a root.</summary>
-    public Branch<T> Parent { get; }
+    public Branch<T>? Parent { get; }
 
     /// <summary>
     ///     Returns true if the current branch can be expanded according to the <see cref="TreeBuilder{T}"/> or cached
@@ -52,13 +54,13 @@ internal class Branch<T> where T : class
         if (ChildBranches is null)
         {
             //if there is a rapid method for determining whether there are children
-            if (tree.TreeBuilder.SupportsCanExpand)
+            if (_tree.TreeBuilder.SupportsCanExpand)
             {
-                return tree.TreeBuilder.CanExpand (Model);
+                return _tree.TreeBuilder.CanExpand (Model);
             }
 
             //there is no way of knowing whether we can expand without fetching the children
-            FetchChildren ();
+            ChildBranches = FetchChildren ();
         }
 
         //we fetched or already know the children, so return whether we have any
@@ -69,32 +71,30 @@ internal class Branch<T> where T : class
     public void Collapse () { IsExpanded = false; }
 
     /// <summary>Renders the current <see cref="Model"/> on the specified line <paramref name="y"/>.</summary>
-    /// <param name="driver"></param>
-    /// <param name="colorScheme"></param>
     /// <param name="y"></param>
     /// <param name="availableWidth"></param>
-    public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth)
+    public virtual void Draw (int y, int availableWidth)
     {
         List<Cell> cells = new ();
         int? indexOfExpandCollapseSymbol = null;
         int indexOfModelText;
 
         // true if the current line of the tree is the selected one and control has focus
-        bool isSelected = tree.IsSelected (Model);
+        bool isSelected = _tree.IsSelected (Model);
 
         Attribute textColor =
-            isSelected ? tree.HasFocus ? colorScheme.Focus : colorScheme.HotNormal : colorScheme.Normal;
-        Attribute symbolColor = tree.Style.HighlightModelTextOnly ? colorScheme.Normal : textColor;
+            isSelected ? _tree.HasFocus ? _tree.GetFocusColor () : _tree.GetHotNormalColor () : _tree.GetNormalColor ();
+        Attribute symbolColor = _tree.Style.HighlightModelTextOnly ? _tree.GetNormalColor () : textColor;
 
         // Everything on line before the expansion run and branch text
-        Rune [] prefix = GetLinePrefix (driver).ToArray ();
-        Rune expansion = GetExpandableSymbol (driver);
-        string lineBody = tree.AspectGetter (Model) ?? "";
+        Rune [] prefix = GetLinePrefix ().ToArray ();
+        Rune expansion = GetExpandableSymbol ();
+        string lineBody = _tree.AspectGetter (Model) ?? "";
 
-        tree.Move (0, y);
+        _tree.Move (0, y);
 
         // if we have scrolled to the right then bits of the prefix will have disappeared off the screen
-        int toSkip = tree.ScrollOffsetHorizontal;
+        int toSkip = _tree.ScrollOffsetHorizontal;
         Attribute attr = symbolColor;
 
         // Draw the line prefix (all parallel lanes or whitespace and an expand/collapse/leaf symbol)
@@ -112,20 +112,20 @@ internal class Branch<T> where T : class
         }
 
         // pick color for expanded symbol
-        if (tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors)
+        if (_tree.Style.ColorExpandSymbol || _tree.Style.InvertExpandSymbolColors)
         {
-            Attribute color = symbolColor;
+            Attribute color;
 
-            if (tree.Style.ColorExpandSymbol)
+            if (_tree.Style.ColorExpandSymbol)
             {
                 if (isSelected)
                 {
-                    color = tree.Style.HighlightModelTextOnly ? colorScheme.HotNormal :
-                            tree.HasFocus ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal;
+                    color = _tree.Style.HighlightModelTextOnly ? _tree.GetHotNormalColor () :
+                            _tree.HasFocus ? _tree.GetHotFocusColor () : _tree.GetHotNormalColor ();
                 }
                 else
                 {
-                    color = tree.ColorScheme.HotNormal;
+                    color = _tree.GetHotNormalColor ();
                 }
             }
             else
@@ -133,9 +133,9 @@ internal class Branch<T> where T : class
                 color = symbolColor;
             }
 
-            if (tree.Style.InvertExpandSymbolColors)
+            if (_tree.Style.InvertExpandSymbolColors)
             {
-                color = new Attribute (color.Background, color.Foreground);
+                color = new (color.Background, color.Foreground);
             }
 
             attr = color;
@@ -177,10 +177,10 @@ internal class Branch<T> where T : class
         if (lineBody.EnumerateRunes ().Sum (l => l.GetColumns ()) > availableWidth)
         {
             // remaining space is zero and truncate the line
-            lineBody = new string (
-                                   lineBody.TakeWhile (c => (availableWidth -= ((Rune)c).GetColumns ()) >= 0)
-                                           .ToArray ()
-                                  );
+            lineBody = new (
+                            lineBody.TakeWhile (c => (availableWidth -= ((Rune)c).GetColumns ()) >= 0)
+                                    .ToArray ()
+                           );
             availableWidth = 0;
         }
         else
@@ -194,9 +194,9 @@ internal class Branch<T> where T : class
         Attribute modelColor = textColor;
 
         // if custom color delegate invoke it
-        if (tree.ColorGetter is { })
+        if (_tree.ColorGetter is { })
         {
-            ColorScheme modelScheme = tree.ColorGetter (Model);
+            ColorScheme modelScheme = _tree.ColorGetter (Model);
 
             // if custom color scheme is defined for this Model
             if (modelScheme is { })
@@ -206,12 +206,12 @@ internal class Branch<T> where T : class
             }
             else
             {
-                modelColor = new Attribute ();
+                modelColor = new ();
             }
         }
 
         attr = modelColor;
-        cells.AddRange (lineBody.Select (r => NewCell (attr, new Rune (r))));
+        cells.AddRange (lineBody.Select (r => NewCell (attr, new (r))));
 
         if (availableWidth > 0)
         {
@@ -219,7 +219,7 @@ internal class Branch<T> where T : class
 
             cells.AddRange (
                             Enumerable.Repeat (
-                                               NewCell (attr, new Rune (' ')),
+                                               NewCell (attr, new (' ')),
                                                availableWidth
                                               )
                            );
@@ -230,32 +230,29 @@ internal class Branch<T> where T : class
             Model = Model,
             Y = y,
             Cells = cells,
-            Tree = tree,
+            Tree = _tree,
             IndexOfExpandCollapseSymbol =
                 indexOfExpandCollapseSymbol,
             IndexOfModelText = indexOfModelText
         };
-        tree.OnDrawLine (e);
+        _tree.OnDrawLine (e);
 
-        if (!e.Handled && driver != null)
+        if (!e.Handled)
         {
             foreach (Cell cell in cells)
             {
-                driver.SetAttribute ((Attribute)cell.Attribute!);
-                driver.AddRune (cell.Rune);
+                _tree.SetAttribute ((Attribute)cell.Attribute!);
+                _tree.AddRune (cell.Rune);
             }
         }
 
-        driver?.SetAttribute (colorScheme.Normal);
+        _tree.SetAttribute (_tree.GetNormalColor());
     }
 
     /// <summary>Expands the current branch if possible.</summary>
     public void Expand ()
     {
-        if (ChildBranches is null)
-        {
-            FetchChildren ();
-        }
+        ChildBranches ??= FetchChildren ();
 
         if (ChildBranches.Any ())
         {
@@ -264,45 +261,44 @@ internal class Branch<T> where T : class
     }
 
     /// <summary>Fetch the children of this branch. This method populates <see cref="ChildBranches"/>.</summary>
-    public virtual void FetchChildren ()
+    private List<Branch<T>> FetchChildren ()
     {
-        if (tree.TreeBuilder is null)
+        if (_tree.TreeBuilder is null)
         {
-            return;
+            return [];
         }
 
         IEnumerable<T> children;
 
-        if (Depth >= tree.MaxDepth)
+        if (Depth >= _tree.MaxDepth)
         {
-            children = Enumerable.Empty<T> ();
+            children = [];
         }
         else
         {
-            children = tree.TreeBuilder.GetChildren (Model) ?? Enumerable.Empty<T> ();
+            children = _tree.TreeBuilder.GetChildren (Model) ?? [];
         }
 
-        ChildBranches = children.ToDictionary (k => k, val => new Branch<T> (tree, this, val));
+        return children.Select (o => new Branch<T> (_tree, this, o)).ToList ();
     }
 
     /// <summary>
     ///     Returns an appropriate symbol for displaying next to the string representation of the <see cref="Model"/>
     ///     object to indicate whether it <see cref="IsExpanded"/> or not (or it is a leaf).
     /// </summary>
-    /// <param name="driver"></param>
     /// <returns></returns>
-    public Rune GetExpandableSymbol (IConsoleDriver driver)
+    public Rune GetExpandableSymbol ()
     {
-        Rune leafSymbol = tree.Style.ShowBranchLines ? Glyphs.HLine : (Rune)' ';
+        Rune leafSymbol = _tree.Style.ShowBranchLines ? Glyphs.HLine : (Rune)' ';
 
         if (IsExpanded)
         {
-            return tree.Style.CollapseableSymbol ?? leafSymbol;
+            return _tree.Style.CollapseableSymbol ?? leafSymbol;
         }
 
         if (CanExpand ())
         {
-            return tree.Style.ExpandableSymbol ?? leafSymbol;
+            return _tree.Style.ExpandableSymbol ?? leafSymbol;
         }
 
         return leafSymbol;
@@ -313,10 +309,10 @@ internal class Branch<T> where T : class
     ///     line body).
     /// </summary>
     /// <returns></returns>
-    public virtual int GetWidth (IConsoleDriver driver)
+    public virtual int GetWidth ()
     {
         return
-            GetLinePrefix (driver).Sum (r => r.GetColumns ()) + GetExpandableSymbol (driver).GetColumns () + (tree.AspectGetter (Model) ?? "").Length;
+            GetLinePrefix ().Sum (r => r.GetColumns ()) + GetExpandableSymbol ().GetColumns () + (_tree.AspectGetter (Model) ?? "").Length;
     }
 
     /// <summary>Refreshes cached knowledge in this branch e.g. what children an object has.</summary>
@@ -333,41 +329,46 @@ internal class Branch<T> where T : class
         //if we don't know about any children yet just use the normal method
         if (ChildBranches is null)
         {
-            FetchChildren ();
+            ChildBranches = FetchChildren ();
         }
         else
         {
             // we already knew about some children so preserve the state of the old children
 
             // first gather the new Children
-            IEnumerable<T> newChildren = tree.TreeBuilder?.GetChildren (Model) ?? Enumerable.Empty<T> ();
+            T [] newChildren = _tree.TreeBuilder?.GetChildren (Model).ToArray () ?? [];
 
             // Children who no longer appear need to go
-            foreach (T toRemove in ChildBranches.Keys.Except (newChildren).ToArray ())
+            foreach (Branch<T> toRemove in ChildBranches.Where (b => !newChildren.Contains (b.Model)).ToArray ())
             {
                 ChildBranches.Remove (toRemove);
 
                 //also if the user has this node selected (its disappearing) so lets change selection to us (the parent object) to be helpful
-                if (Equals (tree.SelectedObject, toRemove))
+                if (Equals (_tree.SelectedObject, toRemove.Model))
                 {
-                    tree.SelectedObject = Model;
+                    _tree.SelectedObject = Model;
                 }
             }
 
             // New children need to be added
             foreach (T newChild in newChildren)
             {
+                Branch<T>? existingBranch = ChildBranches.FirstOrDefault (b => b.Model.Equals (newChild));
+
                 // If we don't know about the child, yet we need a new branch
-                if (!ChildBranches.ContainsKey (newChild))
+                if (existingBranch == null)
                 {
-                    ChildBranches.Add (newChild, new Branch<T> (tree, this, newChild));
+                    ChildBranches.Add (new (_tree, this, newChild));
                 }
                 else
                 {
                     //we already have this object but update the reference anyway in case Equality match but the references are new
-                    ChildBranches [newChild].Model = newChild;
+                    existingBranch.Model = newChild;
                 }
             }
+
+            // Order the list
+            ChildBranches = ChildBranches.OrderBy (b => newChildren.IndexOf (b.Model)).ToList ();
         }
     }
 
@@ -381,9 +382,9 @@ internal class Branch<T> where T : class
 
         if (ChildBranches is { })
         {
-            foreach (KeyValuePair<T, Branch<T>> child in ChildBranches)
+            foreach (Branch<T> child in ChildBranches)
             {
-                child.Value.CollapseAll ();
+                child.CollapseAll ();
             }
         }
     }
@@ -395,9 +396,9 @@ internal class Branch<T> where T : class
 
         if (ChildBranches is { })
         {
-            foreach (KeyValuePair<T, Branch<T>> child in ChildBranches)
+            foreach (Branch<T> child in ChildBranches)
             {
-                child.Value.ExpandAll ();
+                child.ExpandAll ();
             }
         }
     }
@@ -406,16 +407,15 @@ internal class Branch<T> where T : class
     ///     Gets all characters to render prior to the current branches line.  This includes indentation whitespace and
     ///     any tree branches (if enabled).
     /// </summary>
-    /// <param name="driver"></param>
     /// <returns></returns>
-    internal IEnumerable<Rune> GetLinePrefix (IConsoleDriver driver)
+    internal IEnumerable<Rune> GetLinePrefix ()
     {
         // If not showing line branches or this is a root object.
-        if (!tree.Style.ShowBranchLines)
+        if (!_tree.Style.ShowBranchLines)
         {
             for (var i = 0; i < Depth; i++)
             {
-                yield return new Rune (' ');
+                yield return new (' ');
             }
 
             yield break;
@@ -426,14 +426,14 @@ internal class Branch<T> where T : class
         {
             if (cur.IsLast ())
             {
-                yield return new Rune (' ');
+                yield return new (' ');
             }
             else
             {
                 yield return Glyphs.VLine;
             }
 
-            yield return new Rune (' ');
+            yield return new (' ');
         }
 
         if (IsLast ())
@@ -462,15 +462,15 @@ internal class Branch<T> where T : class
         }
 
         // if we could theoretically expand
-        if (!IsExpanded && tree.Style.ExpandableSymbol != default (Rune?))
+        if (!IsExpanded && _tree.Style.ExpandableSymbol != default (Rune?))
         {
-            return x == GetLinePrefix (driver).Count ();
+            return x == GetLinePrefix ().Count ();
         }
 
         // if we could theoretically collapse
-        if (IsExpanded && tree.Style.CollapseableSymbol != default (Rune?))
+        if (IsExpanded && _tree.Style.CollapseableSymbol != default (Rune?))
         {
-            return x == GetLinePrefix (driver).Count ();
+            return x == GetLinePrefix ().Count ();
         }
 
         return false;
@@ -487,9 +487,9 @@ internal class Branch<T> where T : class
             if (IsExpanded)
             {
                 // if we are expanded we need to update the visible children
-                foreach (KeyValuePair<T, Branch<T>> child in ChildBranches)
+                foreach (Branch<T> child in ChildBranches)
                 {
-                    child.Value.Rebuild ();
+                    child.Rebuild ();
                 }
             }
             else
@@ -504,7 +504,7 @@ internal class Branch<T> where T : class
     /// <returns></returns>
     private IEnumerable<Branch<T>> GetParentBranches ()
     {
-        Branch<T> cur = Parent;
+        Branch<T>? cur = Parent;
 
         while (cur is { })
         {
@@ -523,11 +523,13 @@ internal class Branch<T> where T : class
     {
         if (Parent is null)
         {
-            return this == tree.roots.Values.LastOrDefault ();
+            return this == _tree.roots.Values.LastOrDefault ();
         }
 
-        return Parent.ChildBranches.Values.LastOrDefault () == this;
+        Parent.ChildBranches ??= Parent.FetchChildren ();
+
+        return Parent.ChildBranches.LastOrDefault () == this;
     }
 
-    private static Cell NewCell (Attribute attr, Rune r) { return new Cell { Rune = r, Attribute = new (attr) }; }
+    private static Cell NewCell (Attribute attr, Rune r) { return new() { Rune = r, Attribute = new (attr) }; }
 }

+ 5 - 5
Terminal.Gui/Views/TreeView/TreeView.cs

@@ -847,7 +847,7 @@ public class TreeView<T> : View, ITreeView where T : class
             return new T [0];
         }
 
-        return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0];
+        return branch.ChildBranches?.Select (b => b.Model)?.ToArray () ?? new T [0];
     }
 
     /// <summary>Returns the maximum width line in the tree including prefix and expansion symbols.</summary>
@@ -879,10 +879,10 @@ public class TreeView<T> : View, ITreeView where T : class
                 return 0;
             }
 
-            return map.Skip (ScrollOffsetVertical).Take (Viewport.Height).Max (b => b.GetWidth (Driver));
+            return map.Skip (ScrollOffsetVertical).Take (Viewport.Height).Max (b => b.GetWidth ());
         }
 
-        return map.Max (b => b.GetWidth (Driver));
+        return map.Max (b => b.GetWidth ());
     }
 
     /// <summary>
@@ -1171,7 +1171,7 @@ public class TreeView<T> : View, ITreeView where T : class
             if (idxToRender < map.Count)
             {
                 // Render the line
-                map.ElementAt (idxToRender).Draw (Driver, ColorScheme, line, Viewport.Width);
+                map.ElementAt (idxToRender).Draw (line, Viewport.Width);
             }
             else
             {
@@ -1488,7 +1488,7 @@ public class TreeView<T> : View, ITreeView where T : class
 
         if (currentBranch.IsExpanded)
         {
-            foreach (Branch<T> subBranch in currentBranch.ChildBranches.Values)
+            foreach (Branch<T> subBranch in currentBranch.ChildBranches)
             {
                 foreach (Branch<T> sub in AddToLineMap (subBranch, weMatch, out bool childMatch))
                 {

+ 6 - 0
Terminal.sln

@@ -65,6 +65,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable",
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting", "TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj", "{2DBA7BDC-17AE-474B-A507-00807D087607}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting.Xunit", "TerminalGuiFluentTesting.Xunit\TerminalGuiFluentTesting.Xunit.csproj", "{231B9723-10F3-46DB-8EAE-50C0C0375AD3}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -123,6 +125,10 @@ Global
 		{2DBA7BDC-17AE-474B-A507-00807D087607}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.Build.0 = Release|Any CPU
+		{231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 14 - 0
TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj" />
+    <PackageReference Include="xunit" />
+  </ItemGroup>
+
+</Project>

+ 25 - 0
TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs

@@ -0,0 +1,25 @@
+using Xunit;
+
+namespace TerminalGuiFluentTesting;
+
+public static class XunitContextExtensions
+{
+    public static GuiTestContext AssertTrue (this GuiTestContext context, bool? condition)
+    {
+        context.Then (
+                      () =>
+                      {
+                          Assert.True (condition);
+                      });
+        return context;
+    }
+    public static GuiTestContext AssertEqual (this GuiTestContext context, object? expected, object? actual)
+    {
+        context.Then (
+                      () =>
+                      {
+                          Assert.Equal (expected,actual);
+                      });
+        return context;
+    }
+}

+ 34 - 1
TerminalGuiFluentTesting/GuiTestContext.cs

@@ -243,7 +243,18 @@ public class GuiTestContext : IDisposable
     /// <returns></returns>
     public GuiTestContext Then (Action doAction)
     {
-        doAction ();
+        try
+        {
+            doAction ();
+        }
+        catch(Exception)
+        {
+            Stop ();
+            _hardStop.Cancel();
+
+            throw;
+
+        }
 
         return this;
     }
@@ -360,6 +371,7 @@ public class GuiTestContext : IDisposable
                 {
                     SendNetKey (k);
                 }
+                WaitIteration ();
                 break;
             default:
                 throw new ArgumentOutOfRangeException ();
@@ -550,4 +562,25 @@ public class GuiTestContext : IDisposable
 
         WaitIteration ();
     }
+
+    /// <summary>
+    /// Sets the input focus to the given <see cref="View"/>.
+    /// Throws <see cref="ArgumentException"/> if focus did not change due to system
+    /// constraints e.g. <paramref name="toFocus"/>
+    /// <see cref="View.CanFocus"/> is <see langword="false"/>
+    /// </summary>
+    /// <param name="toFocus"></param>
+    /// <returns></returns>
+    /// <exception cref="ArgumentException"></exception>
+    public GuiTestContext Focus (View toFocus)
+    {
+        toFocus.FocusDeepest (NavigationDirection.Forward, TabBehavior.TabStop);
+
+        if (!toFocus.HasFocus)
+        {
+            throw new ArgumentException ("Failed to set focus, FocusDeepest did not result in HasFocus becoming true. Ensure view is added and focusable");
+        }
+
+        return WaitIteration ();
+    }
 }

+ 1 - 13
Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs

@@ -1,5 +1,4 @@
-using System.Text;
-using Terminal.Gui;
+using Terminal.Gui;
 using TerminalGuiFluentTesting;
 using Xunit.Abstractions;
 
@@ -9,17 +8,6 @@ public class BasicFluentAssertionTests
 {
     private readonly TextWriter _out;
 
-    public class TestOutputWriter : TextWriter
-    {
-        private readonly ITestOutputHelper _output;
-
-        public TestOutputWriter (ITestOutputHelper output) { _output = output; }
-
-        public override void WriteLine (string? value) { _output.WriteLine (value ?? string.Empty); }
-
-        public override Encoding Encoding => Encoding.UTF8;
-    }
-
     public BasicFluentAssertionTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); }
 
     [Theory]

+ 15 - 0
Tests/IntegrationTests/FluentTests/TestOutputWriter.cs

@@ -0,0 +1,15 @@
+using System.Text;
+using Xunit.Abstractions;
+
+namespace IntegrationTests.FluentTests;
+
+public class TestOutputWriter : TextWriter
+{
+    private readonly ITestOutputHelper _output;
+
+    public TestOutputWriter (ITestOutputHelper output) { _output = output; }
+
+    public override void WriteLine (string? value) { _output.WriteLine (value ?? string.Empty); }
+
+    public override Encoding Encoding => Encoding.UTF8;
+}

+ 162 - 0
Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs

@@ -0,0 +1,162 @@
+using Terminal.Gui;
+using TerminalGuiFluentTesting;
+using Xunit.Abstractions;
+
+namespace IntegrationTests.FluentTests;
+
+public class TreeViewFluentTests
+{
+    private readonly TextWriter _out;
+
+    public TreeViewFluentTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); }
+
+    [Theory]
+    [ClassData (typeof (V2TestDrivers))]
+    public void TreeView_AllowReOrdering (V2TestDriver d)
+    {
+        var tv = new TreeView
+        {
+            Width = Dim.Fill (),
+            Height = Dim.Fill ()
+        };
+
+        TreeNode car;
+        TreeNode lorry;
+        TreeNode bike;
+
+        var root = new TreeNode ("Root")
+        {
+            Children =
+            [
+                car = new ("Car"),
+                lorry = new ("Lorry"),
+                bike = new ("Bike")
+            ]
+        };
+
+        tv.AddObject (root);
+
+        using GuiTestContext context =
+            With.A<Window> (40, 10, d)
+                .Add (tv)
+                .Focus (tv)
+                .WaitIteration ()
+                .ScreenShot ("Before expanding", _out)
+                .AssertEqual (root, tv.GetObjectOnRow (0))
+                .Then (() => Assert.Null (tv.GetObjectOnRow (1)))
+                .Right ()
+                .ScreenShot ("After expanding", _out)
+                .AssertEqual (root, tv.GetObjectOnRow (0))
+                .AssertEqual (car, tv.GetObjectOnRow (1))
+                .AssertEqual (lorry, tv.GetObjectOnRow (2))
+                .AssertEqual (bike, tv.GetObjectOnRow (3))
+                .Then (
+                       () =>
+                       {
+                           // Re order
+                           root.Children = [bike, car, lorry];
+                           tv.RefreshObject (root);
+                       })
+                .WaitIteration ()
+                .ScreenShot ("After re-order", _out)
+                .AssertEqual (root, tv.GetObjectOnRow (0))
+                .AssertEqual (bike, tv.GetObjectOnRow (1))
+                .AssertEqual (car, tv.GetObjectOnRow (2))
+                .AssertEqual (lorry, tv.GetObjectOnRow (3))
+                .WriteOutLogs (_out);
+
+        context.Stop ();
+    }
+
+    [Theory]
+    [ClassData (typeof (V2TestDrivers))]
+    public void TreeViewReOrder_PreservesExpansion (V2TestDriver d)
+    {
+        var tv = new TreeView
+        {
+            Width = Dim.Fill (),
+            Height = Dim.Fill ()
+        };
+
+        TreeNode car;
+        TreeNode lorry;
+        TreeNode bike;
+
+        TreeNode mrA;
+        TreeNode mrB;
+
+        TreeNode mrC;
+
+        TreeNode mrD;
+        TreeNode mrE;
+
+        var root = new TreeNode ("Root")
+        {
+            Children =
+            [
+                car = new ("Car")
+                {
+                    Children =
+                    [
+                        mrA = new ("Mr A"),
+                        mrB = new ("Mr B")
+                    ]
+                },
+                lorry = new ("Lorry")
+                {
+                    Children =
+                    [
+                        mrC = new ("Mr C")
+                    ]
+                },
+                bike = new ("Bike")
+                {
+                    Children =
+                    [
+                        mrD = new ("Mr D"),
+                        mrE = new ("Mr E")
+                    ]
+                }
+            ]
+        };
+
+        tv.AddObject (root);
+        tv.ExpandAll ();
+
+        using GuiTestContext context =
+            With.A<Window> (40, 13, d)
+                .Add (tv)
+                .WaitIteration ()
+                .ScreenShot ("Initial State", _out)
+                .AssertEqual (root, tv.GetObjectOnRow (0))
+                .AssertEqual (car, tv.GetObjectOnRow (1))
+                .AssertEqual (mrA, tv.GetObjectOnRow (2))
+                .AssertEqual (mrB, tv.GetObjectOnRow (3))
+                .AssertEqual (lorry, tv.GetObjectOnRow (4))
+                .AssertEqual (mrC, tv.GetObjectOnRow (5))
+                .AssertEqual (bike, tv.GetObjectOnRow (6))
+                .AssertEqual (mrD, tv.GetObjectOnRow (7))
+                .AssertEqual (mrE, tv.GetObjectOnRow (8))
+                .Then (
+                       () =>
+                       {
+                           // Re order
+                           root.Children = [bike, car, lorry];
+                           tv.RefreshObject (root);
+                       })
+                .WaitIteration ()
+                .ScreenShot ("After re-order", _out)
+                .AssertEqual (root, tv.GetObjectOnRow (0))
+                .AssertEqual (bike, tv.GetObjectOnRow (1))
+                .AssertEqual (mrD, tv.GetObjectOnRow (2))
+                .AssertEqual (mrE, tv.GetObjectOnRow (3))
+                .AssertEqual (car, tv.GetObjectOnRow (4))
+                .AssertEqual (mrA, tv.GetObjectOnRow (5))
+                .AssertEqual (mrB, tv.GetObjectOnRow (6))
+                .AssertEqual (lorry, tv.GetObjectOnRow (7))
+                .AssertEqual (mrC, tv.GetObjectOnRow (8))
+                .WriteOutLogs (_out);
+
+        context.Stop ();
+    }
+}

+ 1 - 0
Tests/IntegrationTests/IntegrationTests.csproj

@@ -26,6 +26,7 @@
     </ItemGroup>
     <ItemGroup>
         <ProjectReference Include="..\..\Terminal.Gui\Terminal.Gui.csproj" />
+        <ProjectReference Include="..\..\TerminalGuiFluentTesting.Xunit\TerminalGuiFluentTesting.Xunit.csproj" />
         <ProjectReference Include="..\..\TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj" />
         <ProjectReference Include="..\..\UICatalog\UICatalog.csproj" />
         <ProjectReference Include="..\UnitTests\UnitTests.csproj" />