Fixes #4009 - fix tree ordering (#4015)

This commit is contained in:
Thomas Nind
2025-04-05 13:47:39 +01:00
committed by GitHub
parent 7ba9e539ed
commit e02d57675f
11 changed files with 356 additions and 110 deletions

View File

@@ -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 ();

View File

@@ -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) }; }
}

View File

@@ -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))
{

View File

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

View File

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

View File

@@ -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;
}
}

View File

@@ -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 ();
}
}

View File

@@ -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]

View File

@@ -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;
}

View File

@@ -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 ();
}
}

View File

@@ -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" />