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 CreateTree() { return CreateTree(out _, out _, out _); } private TreeView 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(new DelegateTreeBuilder((s)=> s is Factory f ? f.Cars: null)); tree.AddObject(factory1); return tree; } #endregion /// /// Tests that and are consistent /// [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(); Assert.Equal(0,emptyTree.ContentHeight); Assert.Equal(0,emptyTree.GetContentWidth(true)); Assert.Equal(0,emptyTree.GetContentWidth(false)); } /// /// Tests that results in a correct content height /// [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)); } /// /// Tests that and behaves correctly when an object cannot be expanded (because it has no children) /// [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)); } /// /// Tests illegal ranges for /// [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); } /// /// Tests for objects that are as yet undiscovered by the tree /// [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)); } /// /// Simulates behind the scenes changes to an object (which children it has) and how to sync that into the tree using /// [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); } /// /// Tests that 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) /// [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)); } /// /// 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 /// [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((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()); } /// /// Same as but /// uses instead of /// [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((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()); } /// /// Tests that 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) /// [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); } /// /// Simulates behind the scenes changes to an object (which children it has) and how to sync that into the tree using /// [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(); tree.TreeBuilder = new DelegateTreeBuilder((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((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))); } /// /// Test object which considers for equality only /// 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 (() => { }); } } }