Files
Terminal.Gui/UnitTests/TreeViewTests.cs
2021-03-02 19:14:38 +00:00

612 lines
17 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Terminal.Gui;
using Xunit;
namespace UnitTests {
public class TreeViewTests
{
#region Test Setup Methods
class Factory
{
public Car[] Cars {get;set;}
public override string ToString ()
{
return "Factory";
}
};
class Car {
public string Name{get;set;}
public override string ToString ()
{
return Name;
}
};
private TreeView<object> CreateTree()
{
return CreateTree(out _, out _, out _);
}
private TreeView<object> CreateTree(out Factory factory1, out Car car1, out Car car2)
{
car1 = new Car();
car2 = new Car();
factory1 = new Factory()
{
Cars = new []{car1 ,car2}
};
var tree = new TreeView<object>(new DelegateTreeBuilder<object>((s)=> s is Factory f ? f.Cars: null));
tree.AddObject(factory1);
return tree;
}
#endregion
/// <summary>
/// Tests that <see cref="TreeView.Expand(object)"/> and <see cref="TreeView.IsExpanded(object)"/> are consistent
/// </summary>
[Fact]
public void IsExpanded_TrueAfterExpand()
{
var tree = CreateTree(out Factory f, out _, out _);
Assert.False(tree.IsExpanded(f));
tree.Expand(f);
Assert.True(tree.IsExpanded(f));
tree.Collapse(f);
Assert.False(tree.IsExpanded(f));
}
[Fact]
public void EmptyTreeView_ContentSizes()
{
var emptyTree = new TreeView();
Assert.Equal(0,emptyTree.ContentHeight);
Assert.Equal(0,emptyTree.GetContentWidth(true));
Assert.Equal(0,emptyTree.GetContentWidth(false));
}
[Fact]
public void EmptyTreeViewGeneric_ContentSizes()
{
var emptyTree = new TreeView<string>();
Assert.Equal(0,emptyTree.ContentHeight);
Assert.Equal(0,emptyTree.GetContentWidth(true));
Assert.Equal(0,emptyTree.GetContentWidth(false));
}
/// <summary>
/// Tests that <see cref="TreeView.Expand(object)"/> results in a correct content height
/// </summary>
[Fact]
public void ContentHeight_BiggerAfterExpand()
{
var tree = CreateTree(out Factory f, out _, out _);
Assert.Equal(1,tree.ContentHeight);
tree.Expand(f);
Assert.Equal(3,tree.ContentHeight);
tree.Collapse(f);
Assert.Equal(1,tree.ContentHeight);
}
[Fact]
public void ContentWidth_BiggerAfterExpand()
{
var tree = CreateTree(out Factory f, out Car car1, out _);
tree.Bounds = new Rect(0,0,10,10);
InitFakeDriver();
//-+Factory
Assert.Equal(9,tree.GetContentWidth(true));
car1.Name = "123456789";
tree.Expand(f);
//..├-123456789
Assert.Equal(13,tree.GetContentWidth(true));
tree.Collapse(f);
//-+Factory
Assert.Equal(9,tree.GetContentWidth(true));
}
[Fact]
public void ContentWidth_VisibleVsAll()
{
var tree = CreateTree(out Factory f, out Car car1, out Car car2);
// control only allows 1 row to be viewed at once
tree.Bounds = new Rect(0,0,20,1);
InitFakeDriver();
//-+Factory
Assert.Equal(9,tree.GetContentWidth(true));
Assert.Equal(9,tree.GetContentWidth(false));
car1.Name = "123456789";
car2.Name = "12345678";
tree.Expand(f);
// Although expanded the bigger (longer) child node is not in the rendered area of the control
Assert.Equal(9,tree.GetContentWidth(true));
Assert.Equal(13,tree.GetContentWidth(false)); // If you ask for the global max width it includes the longer child
// Now that we have scrolled down 1 row we should see the big child
tree.ScrollOffsetVertical = 1;
Assert.Equal(13,tree.GetContentWidth(true));
Assert.Equal(13,tree.GetContentWidth(false));
// Scroll down so only car2 is visible
tree.ScrollOffsetVertical = 2;
Assert.Equal(12,tree.GetContentWidth(true));
Assert.Equal(13,tree.GetContentWidth(false));
// Scroll way down (off bottom of control even)
tree.ScrollOffsetVertical = 5;
Assert.Equal(0,tree.GetContentWidth(true));
Assert.Equal(13,tree.GetContentWidth(false));
}
/// <summary>
/// Tests that <see cref="TreeView.IsExpanded(object)"/> and <see cref="TreeView.Expand(object)"/> behaves correctly when an object cannot be expanded (because it has no children)
/// </summary>
[Fact]
public void IsExpanded_FalseIfCannotExpand()
{
var tree = CreateTree(out Factory f, out Car c, out _);
// expose the car by expanding the factory
tree.Expand(f);
// car is not expanded
Assert.False(tree.IsExpanded(c));
//try to expand the car (should have no effect because cars have no children)
tree.Expand(c);
Assert.False(tree.IsExpanded(c));
// should also be ignored
tree.Collapse(c);
Assert.False(tree.IsExpanded(c));
}
/// <summary>
/// Tests illegal ranges for <see cref="TreeView.ScrollOffset"/>
/// </summary>
[Fact]
public void ScrollOffset_CannotBeNegative()
{
var tree = CreateTree();
Assert.Equal(0,tree.ScrollOffsetVertical);
tree.ScrollOffsetVertical = -100;
Assert.Equal(0,tree.ScrollOffsetVertical);
tree.ScrollOffsetVertical = 10;
Assert.Equal(10,tree.ScrollOffsetVertical);
}
/// <summary>
/// Tests <see cref="TreeView.GetScrollOffsetOf(object)"/> for objects that are as yet undiscovered by the tree
/// </summary>
[Fact]
public void GetScrollOffsetOf_MinusOneForUnRevealed()
{
var tree = CreateTree(out Factory f, out Car c1, out Car c2);
// to start with the tree is collapsed and only knows about the root object
Assert.Equal(0,tree.GetScrollOffsetOf(f));
Assert.Equal(-1,tree.GetScrollOffsetOf(c1));
Assert.Equal(-1,tree.GetScrollOffsetOf(c2));
// reveal it by expanding the root object
tree.Expand(f);
// tree now knows about children
Assert.Equal(0,tree.GetScrollOffsetOf(f));
Assert.Equal(1,tree.GetScrollOffsetOf(c1));
Assert.Equal(2,tree.GetScrollOffsetOf(c2));
// after collapsing the root node again
tree.Collapse(f);
// tree no longer knows about the locations of these objects
Assert.Equal(0,tree.GetScrollOffsetOf(f));
Assert.Equal(-1,tree.GetScrollOffsetOf(c1));
Assert.Equal(-1,tree.GetScrollOffsetOf(c2));
}
/// <summary>
/// Simulates behind the scenes changes to an object (which children it has) and how to sync that into the tree using <see cref="TreeView.RefreshObject(object, bool)"/>
/// </summary>
[Fact]
public void RefreshObject_ChildRemoved()
{
var tree = CreateTree(out Factory f, out Car c1, out Car c2);
//reveal it by expanding the root object
tree.Expand(f);
Assert.Equal(0,tree.GetScrollOffsetOf(f));
Assert.Equal(1,tree.GetScrollOffsetOf(c1));
Assert.Equal(2,tree.GetScrollOffsetOf(c2));
// Factory now no longer makes Car c1 (only c2)
f.Cars = new Car[]{c2};
// Tree does not know this yet
Assert.Equal(0,tree.GetScrollOffsetOf(f));
Assert.Equal(1,tree.GetScrollOffsetOf(c1));
Assert.Equal(2,tree.GetScrollOffsetOf(c2));
// If the user has selected the node c1
tree.SelectedObject = c1;
// When we refresh the tree
tree.RefreshObject(f);
// Now tree knows that factory has only one child node c2
Assert.Equal(0,tree.GetScrollOffsetOf(f));
Assert.Equal(-1,tree.GetScrollOffsetOf(c1));
Assert.Equal(1,tree.GetScrollOffsetOf(c2));
// The old selection was c1 which is now gone so selection should default to the parent of that branch (the factory)
Assert.Equal(f,tree.SelectedObject);
}
/// <summary>
/// Tests that <see cref="TreeView.GetParent(object)"/> returns the parent object for
/// Cars (Factories). Note that the method only works once the parent branch (Factory)
/// is expanded to expose the child (Car)
/// </summary>
[Fact]
public void GetParent_ReturnsParentOnlyWhenExpanded()
{
var tree = CreateTree(out Factory f, out Car c1, out Car c2);
Assert.Null(tree.GetParent(f));
Assert.Null(tree.GetParent(c1));
Assert.Null(tree.GetParent(c2));
// now when we expand the factory we discover the cars
tree.Expand(f);
Assert.Null(tree.GetParent(f));
Assert.Equal(f,tree.GetParent(c1));
Assert.Equal(f,tree.GetParent(c2));
tree.Collapse(f);
Assert.Null(tree.GetParent(f));
Assert.Null(tree.GetParent(c1));
Assert.Null(tree.GetParent(c2));
}
/// <summary>
/// Tests how the tree adapts to changes in the ChildrenGetter delegate during runtime
/// when some branches are expanded and the new delegate returns children for a node that
/// previously didn't have any children
/// </summary>
[Fact]
public void RefreshObject_AfterChangingChildrenGetterDuringRuntime()
{
var tree = CreateTree(out Factory f, out Car c1, out Car c2);
string wheel = "Shiny Wheel";
// Expand the Factory
tree.Expand(f);
// c1 cannot have children
Assert.Equal(f,tree.GetParent(c1));
// expanding it does nothing
tree.Expand(c1);
Assert.False(tree.IsExpanded(c1));
// change the children getter so that now cars can have wheels
tree.TreeBuilder = new DelegateTreeBuilder<object>((o)=>
// factories have cars
o is Factory ? new object[]{c1,c2}
// cars have wheels
: new object[]{wheel });
// still cannot expand
tree.Expand(c1);
Assert.False(tree.IsExpanded(c1));
tree.RefreshObject(c1);
tree.Expand(c1);
Assert.True(tree.IsExpanded(c1));
Assert.Equal(wheel,tree.GetChildren(c1).FirstOrDefault());
}
/// <summary>
/// Same as <see cref="RefreshObject_AfterChangingChildrenGetterDuringRuntime"/> but
/// uses <see cref="TreeView.RebuildTree()"/> instead of <see cref="TreeView.RefreshObject(object, bool)"/>
/// </summary>
[Fact]
public void RebuildTree_AfterChangingChildrenGetterDuringRuntime()
{
var tree = CreateTree(out Factory f, out Car c1, out Car c2);
string wheel = "Shiny Wheel";
// Expand the Factory
tree.Expand(f);
// c1 cannot have children
Assert.Equal(f,tree.GetParent(c1));
// expanding it does nothing
tree.Expand(c1);
Assert.False(tree.IsExpanded(c1));
// change the children getter so that now cars can have wheels
tree.TreeBuilder = new DelegateTreeBuilder<object>((o)=>
// factories have cars
o is Factory ? new object[]{c1,c2}
// cars have wheels
: new object[]{wheel });
// still cannot expand
tree.Expand(c1);
Assert.False(tree.IsExpanded(c1));
// Rebuild the tree
tree.RebuildTree();
// Rebuild should not have collapsed any branches or done anything wierd
Assert.True(tree.IsExpanded(f));
tree.Expand(c1);
Assert.True(tree.IsExpanded(c1));
Assert.Equal(wheel,tree.GetChildren(c1).FirstOrDefault());
}
/// <summary>
/// Tests that <see cref="TreeView.GetChildren(object)"/> returns the child objects for
/// the factory. Note that the method only works once the parent branch (Factory)
/// is expanded to expose the child (Car)
/// </summary>
[Fact]
public void GetChildren_ReturnsChildrenOnlyWhenExpanded()
{
var tree = CreateTree(out Factory f, out Car c1, out Car c2);
Assert.Empty(tree.GetChildren(f));
Assert.Empty(tree.GetChildren(c1));
Assert.Empty(tree.GetChildren(c2));
// now when we expand the factory we discover the cars
tree.Expand(f);
Assert.Contains(c1,tree.GetChildren(f));
Assert.Contains(c2,tree.GetChildren(f));
Assert.Empty(tree.GetChildren(c1));
Assert.Empty(tree.GetChildren(c2));
tree.Collapse(f);
Assert.Empty(tree.GetChildren(f));
Assert.Empty(tree.GetChildren(c1));
Assert.Empty(tree.GetChildren(c2));
}
[Fact]
public void TreeNode_WorksWithoutDelegate()
{
var tree = new TreeView();
var root = new TreeNode("Root");
root.Children.Add(new TreeNode("Leaf1"));
root.Children.Add(new TreeNode("Leaf2"));
tree.AddObject(root);
tree.Expand(root);
Assert.Equal(2,tree.GetChildren(root).Count());
}
[Fact]
public void MultiSelect_GetAllSelectedObjects()
{
var tree = new TreeView();
TreeNode l1;
TreeNode l2;
TreeNode l3;
TreeNode l4;
var root = new TreeNode("Root");
root.Children.Add(l1 = new TreeNode("Leaf1"));
root.Children.Add(l2 = new TreeNode("Leaf2"));
root.Children.Add(l3 = new TreeNode("Leaf3"));
root.Children.Add(l4 = new TreeNode("Leaf4"));
tree.AddObject(root);
tree.MultiSelect = true;
tree.Expand(root);
Assert.Empty(tree.GetAllSelectedObjects());
tree.SelectedObject = root;
Assert.Single(tree.GetAllSelectedObjects(),root);
// move selection down 1
tree.AdjustSelection(1,false);
Assert.Single(tree.GetAllSelectedObjects(),l1);
// expand selection down 2 (e.g. shift down twice)
tree.AdjustSelection(1,true);
tree.AdjustSelection(1,true);
Assert.Equal(3,tree.GetAllSelectedObjects().Count());
Assert.Contains(l1,tree.GetAllSelectedObjects());
Assert.Contains(l2,tree.GetAllSelectedObjects());
Assert.Contains(l3,tree.GetAllSelectedObjects());
tree.Collapse(root);
// No selected objects since the root was collapsed
Assert.Empty(tree.GetAllSelectedObjects());
}
[Fact]
public void ObjectActivated_Called()
{
var tree = CreateTree(out Factory f, out Car car1, out _);
InitFakeDriver();
object activated = null;
bool called = false;
// register for the event
tree.ObjectActivated += (s)=>
{
activated = s.ActivatedObject;
called = true;
};
Assert.False(called);
// no object is selected yet so no event should happen
tree.ProcessKey(new KeyEvent(Key.Enter,new KeyModifiers()));
Assert.Null(activated);
Assert.False(called);
// down to select factory
tree.ProcessKey(new KeyEvent(Key.CursorDown,new KeyModifiers()));
tree.ProcessKey(new KeyEvent(Key.Enter,new KeyModifiers()));
Assert.True(called);
Assert.Same(f,activated);
}
[Fact]
public void ObjectActivated_CustomKey()
{
var tree = CreateTree(out Factory f, out Car car1, out _);
InitFakeDriver();
tree.ObjectActivationKey = Key.Delete;
object activated = null;
bool called = false;
// register for the event
tree.ObjectActivated += (s)=>
{
activated = s.ActivatedObject;
called = true;
};
Assert.False(called);
// no object is selected yet so no event should happen
tree.ProcessKey(new KeyEvent(Key.Enter,new KeyModifiers()));
Assert.Null(activated);
Assert.False(called);
// down to select factory
tree.ProcessKey(new KeyEvent(Key.CursorDown,new KeyModifiers()));
tree.ProcessKey(new KeyEvent(Key.Enter,new KeyModifiers()));
// Enter is not the activation key in this unit test
Assert.Null(activated);
Assert.False(called);
// Delete is the activation key in this test so should result in activation occurring
tree.ProcessKey(new KeyEvent(Key.Delete,new KeyModifiers()));
Assert.True(called);
Assert.Same(f,activated);
}
/// <summary>
/// Simulates behind the scenes changes to an object (which children it has) and how to sync that into the tree using <see cref="TreeView.RefreshObject(object, bool)"/>
/// </summary>
[Fact]
public void RefreshObject_EqualityTest()
{
var obj1 = new EqualityTestObject(){Name="Bob",Age=1 };
var obj2 = new EqualityTestObject(){Name="Bob",Age=2 };;
string root = "root";
var tree = new TreeView<object>();
tree.TreeBuilder = new DelegateTreeBuilder<object>((s)=> ReferenceEquals(s , root) ? new object[]{obj1 } : null);
tree.AddObject(root);
// Tree is not expanded so the root has no children yet
Assert.Empty(tree.GetChildren(root));
tree.Expand(root);
// now that the tree is expanded we should get our child returned
Assert.Equal(1,tree.GetChildren(root).Count(child=>ReferenceEquals(obj1,child)));
// change the getter to return an Equal object (but not the same reference - obj2)
tree.TreeBuilder = new DelegateTreeBuilder<object>((s)=> ReferenceEquals(s , root) ? new object[]{obj2 } : null);
// tree has cached the knowledge of what children the root has so won't know about the change (we still get obj1)
Assert.Equal(1,tree.GetChildren(root).Count(child=>ReferenceEquals(obj1,child)));
// now that we refresh the root we should get the new child reference (obj2)
tree.RefreshObject(root);
Assert.Equal(1,tree.GetChildren(root).Count(child=>ReferenceEquals(obj2,child)));
}
/// <summary>
/// Test object which considers for equality only <see cref="Name"/>
/// </summary>
private class EqualityTestObject
{
public string Name { get;set;}
public int Age { get;set;}
public override int GetHashCode ()
{
return Name?.GetHashCode()??base.GetHashCode ();
}
public override bool Equals (object obj)
{
return obj is EqualityTestObject eto && Equals(Name, eto.Name);
}
}
private void InitFakeDriver()
{
var driver = new FakeDriver ();
Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
driver.Init (() => { });
}
}
}