TreeViewTests.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using Terminal.Gui;
  7. using Xunit;
  8. namespace UnitTests {
  9. public class TreeViewTests
  10. {
  11. #region Test Setup Methods
  12. class Factory
  13. {
  14. public Car[] Cars {get;set;}
  15. public override string ToString ()
  16. {
  17. return "Factory";
  18. }
  19. };
  20. class Car {
  21. public string Name{get;set;}
  22. public override string ToString ()
  23. {
  24. return Name;
  25. }
  26. };
  27. private TreeView<object> CreateTree()
  28. {
  29. return CreateTree(out _, out _, out _);
  30. }
  31. private TreeView<object> CreateTree(out Factory factory1, out Car car1, out Car car2)
  32. {
  33. car1 = new Car();
  34. car2 = new Car();
  35. factory1 = new Factory()
  36. {
  37. Cars = new []{car1 ,car2}
  38. };
  39. var tree = new TreeView<object>(new DelegateTreeBuilder<object>((s)=> s is Factory f ? f.Cars: null));
  40. tree.AddObject(factory1);
  41. return tree;
  42. }
  43. #endregion
  44. /// <summary>
  45. /// Tests that <see cref="TreeView.Expand(object)"/> and <see cref="TreeView.IsExpanded(object)"/> are consistent
  46. /// </summary>
  47. [Fact]
  48. public void IsExpanded_TrueAfterExpand()
  49. {
  50. var tree = CreateTree(out Factory f, out _, out _);
  51. Assert.False(tree.IsExpanded(f));
  52. tree.Expand(f);
  53. Assert.True(tree.IsExpanded(f));
  54. tree.Collapse(f);
  55. Assert.False(tree.IsExpanded(f));
  56. }
  57. [Fact]
  58. public void EmptyTreeView_ContentSizes()
  59. {
  60. var emptyTree = new TreeView();
  61. Assert.Equal(0,emptyTree.ContentHeight);
  62. Assert.Equal(0,emptyTree.GetContentWidth(true));
  63. Assert.Equal(0,emptyTree.GetContentWidth(false));
  64. }
  65. [Fact]
  66. public void EmptyTreeViewGeneric_ContentSizes()
  67. {
  68. var emptyTree = new TreeView<string>();
  69. Assert.Equal(0,emptyTree.ContentHeight);
  70. Assert.Equal(0,emptyTree.GetContentWidth(true));
  71. Assert.Equal(0,emptyTree.GetContentWidth(false));
  72. }
  73. /// <summary>
  74. /// Tests that <see cref="TreeView.Expand(object)"/> results in a correct content height
  75. /// </summary>
  76. [Fact]
  77. public void ContentHeight_BiggerAfterExpand()
  78. {
  79. var tree = CreateTree(out Factory f, out _, out _);
  80. Assert.Equal(1,tree.ContentHeight);
  81. tree.Expand(f);
  82. Assert.Equal(3,tree.ContentHeight);
  83. tree.Collapse(f);
  84. Assert.Equal(1,tree.ContentHeight);
  85. }
  86. [Fact]
  87. public void ContentWidth_BiggerAfterExpand()
  88. {
  89. var tree = CreateTree(out Factory f, out Car car1, out _);
  90. tree.Bounds = new Rect(0,0,10,10);
  91. InitFakeDriver();
  92. //-+Factory
  93. Assert.Equal(9,tree.GetContentWidth(true));
  94. car1.Name = "123456789";
  95. tree.Expand(f);
  96. //..├-123456789
  97. Assert.Equal(13,tree.GetContentWidth(true));
  98. tree.Collapse(f);
  99. //-+Factory
  100. Assert.Equal(9,tree.GetContentWidth(true));
  101. }
  102. [Fact]
  103. public void ContentWidth_VisibleVsAll()
  104. {
  105. var tree = CreateTree(out Factory f, out Car car1, out Car car2);
  106. // control only allows 1 row to be viewed at once
  107. tree.Bounds = new Rect(0,0,20,1);
  108. InitFakeDriver();
  109. //-+Factory
  110. Assert.Equal(9,tree.GetContentWidth(true));
  111. Assert.Equal(9,tree.GetContentWidth(false));
  112. car1.Name = "123456789";
  113. car2.Name = "12345678";
  114. tree.Expand(f);
  115. // Although expanded the bigger (longer) child node is not in the rendered area of the control
  116. Assert.Equal(9,tree.GetContentWidth(true));
  117. Assert.Equal(13,tree.GetContentWidth(false)); // If you ask for the global max width it includes the longer child
  118. // Now that we have scrolled down 1 row we should see the big child
  119. tree.ScrollOffsetVertical = 1;
  120. Assert.Equal(13,tree.GetContentWidth(true));
  121. Assert.Equal(13,tree.GetContentWidth(false));
  122. // Scroll down so only car2 is visible
  123. tree.ScrollOffsetVertical = 2;
  124. Assert.Equal(12,tree.GetContentWidth(true));
  125. Assert.Equal(13,tree.GetContentWidth(false));
  126. // Scroll way down (off bottom of control even)
  127. tree.ScrollOffsetVertical = 5;
  128. Assert.Equal(0,tree.GetContentWidth(true));
  129. Assert.Equal(13,tree.GetContentWidth(false));
  130. }
  131. /// <summary>
  132. /// 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)
  133. /// </summary>
  134. [Fact]
  135. public void IsExpanded_FalseIfCannotExpand()
  136. {
  137. var tree = CreateTree(out Factory f, out Car c, out _);
  138. // expose the car by expanding the factory
  139. tree.Expand(f);
  140. // car is not expanded
  141. Assert.False(tree.IsExpanded(c));
  142. //try to expand the car (should have no effect because cars have no children)
  143. tree.Expand(c);
  144. Assert.False(tree.IsExpanded(c));
  145. // should also be ignored
  146. tree.Collapse(c);
  147. Assert.False(tree.IsExpanded(c));
  148. }
  149. /// <summary>
  150. /// Tests illegal ranges for <see cref="TreeView.ScrollOffset"/>
  151. /// </summary>
  152. [Fact]
  153. public void ScrollOffset_CannotBeNegative()
  154. {
  155. var tree = CreateTree();
  156. Assert.Equal(0,tree.ScrollOffsetVertical);
  157. tree.ScrollOffsetVertical = -100;
  158. Assert.Equal(0,tree.ScrollOffsetVertical);
  159. tree.ScrollOffsetVertical = 10;
  160. Assert.Equal(10,tree.ScrollOffsetVertical);
  161. }
  162. /// <summary>
  163. /// Tests <see cref="TreeView.GetScrollOffsetOf(object)"/> for objects that are as yet undiscovered by the tree
  164. /// </summary>
  165. [Fact]
  166. public void GetScrollOffsetOf_MinusOneForUnRevealed()
  167. {
  168. var tree = CreateTree(out Factory f, out Car c1, out Car c2);
  169. // to start with the tree is collapsed and only knows about the root object
  170. Assert.Equal(0,tree.GetScrollOffsetOf(f));
  171. Assert.Equal(-1,tree.GetScrollOffsetOf(c1));
  172. Assert.Equal(-1,tree.GetScrollOffsetOf(c2));
  173. // reveal it by expanding the root object
  174. tree.Expand(f);
  175. // tree now knows about children
  176. Assert.Equal(0,tree.GetScrollOffsetOf(f));
  177. Assert.Equal(1,tree.GetScrollOffsetOf(c1));
  178. Assert.Equal(2,tree.GetScrollOffsetOf(c2));
  179. // after collapsing the root node again
  180. tree.Collapse(f);
  181. // tree no longer knows about the locations of these objects
  182. Assert.Equal(0,tree.GetScrollOffsetOf(f));
  183. Assert.Equal(-1,tree.GetScrollOffsetOf(c1));
  184. Assert.Equal(-1,tree.GetScrollOffsetOf(c2));
  185. }
  186. /// <summary>
  187. /// 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)"/>
  188. /// </summary>
  189. [Fact]
  190. public void RefreshObject_ChildRemoved()
  191. {
  192. var tree = CreateTree(out Factory f, out Car c1, out Car c2);
  193. //reveal it by expanding the root object
  194. tree.Expand(f);
  195. Assert.Equal(0,tree.GetScrollOffsetOf(f));
  196. Assert.Equal(1,tree.GetScrollOffsetOf(c1));
  197. Assert.Equal(2,tree.GetScrollOffsetOf(c2));
  198. // Factory now no longer makes Car c1 (only c2)
  199. f.Cars = new Car[]{c2};
  200. // Tree does not know this yet
  201. Assert.Equal(0,tree.GetScrollOffsetOf(f));
  202. Assert.Equal(1,tree.GetScrollOffsetOf(c1));
  203. Assert.Equal(2,tree.GetScrollOffsetOf(c2));
  204. // If the user has selected the node c1
  205. tree.SelectedObject = c1;
  206. // When we refresh the tree
  207. tree.RefreshObject(f);
  208. // Now tree knows that factory has only one child node c2
  209. Assert.Equal(0,tree.GetScrollOffsetOf(f));
  210. Assert.Equal(-1,tree.GetScrollOffsetOf(c1));
  211. Assert.Equal(1,tree.GetScrollOffsetOf(c2));
  212. // The old selection was c1 which is now gone so selection should default to the parent of that branch (the factory)
  213. Assert.Equal(f,tree.SelectedObject);
  214. }
  215. /// <summary>
  216. /// Tests that <see cref="TreeView.GetParent(object)"/> returns the parent object for
  217. /// Cars (Factories). Note that the method only works once the parent branch (Factory)
  218. /// is expanded to expose the child (Car)
  219. /// </summary>
  220. [Fact]
  221. public void GetParent_ReturnsParentOnlyWhenExpanded()
  222. {
  223. var tree = CreateTree(out Factory f, out Car c1, out Car c2);
  224. Assert.Null(tree.GetParent(f));
  225. Assert.Null(tree.GetParent(c1));
  226. Assert.Null(tree.GetParent(c2));
  227. // now when we expand the factory we discover the cars
  228. tree.Expand(f);
  229. Assert.Null(tree.GetParent(f));
  230. Assert.Equal(f,tree.GetParent(c1));
  231. Assert.Equal(f,tree.GetParent(c2));
  232. tree.Collapse(f);
  233. Assert.Null(tree.GetParent(f));
  234. Assert.Null(tree.GetParent(c1));
  235. Assert.Null(tree.GetParent(c2));
  236. }
  237. /// <summary>
  238. /// Tests how the tree adapts to changes in the ChildrenGetter delegate during runtime
  239. /// when some branches are expanded and the new delegate returns children for a node that
  240. /// previously didn't have any children
  241. /// </summary>
  242. [Fact]
  243. public void RefreshObject_AfterChangingChildrenGetterDuringRuntime()
  244. {
  245. var tree = CreateTree(out Factory f, out Car c1, out Car c2);
  246. string wheel = "Shiny Wheel";
  247. // Expand the Factory
  248. tree.Expand(f);
  249. // c1 cannot have children
  250. Assert.Equal(f,tree.GetParent(c1));
  251. // expanding it does nothing
  252. tree.Expand(c1);
  253. Assert.False(tree.IsExpanded(c1));
  254. // change the children getter so that now cars can have wheels
  255. tree.TreeBuilder = new DelegateTreeBuilder<object>((o)=>
  256. // factories have cars
  257. o is Factory ? new object[]{c1,c2}
  258. // cars have wheels
  259. : new object[]{wheel });
  260. // still cannot expand
  261. tree.Expand(c1);
  262. Assert.False(tree.IsExpanded(c1));
  263. tree.RefreshObject(c1);
  264. tree.Expand(c1);
  265. Assert.True(tree.IsExpanded(c1));
  266. Assert.Equal(wheel,tree.GetChildren(c1).FirstOrDefault());
  267. }
  268. /// <summary>
  269. /// Same as <see cref="RefreshObject_AfterChangingChildrenGetterDuringRuntime"/> but
  270. /// uses <see cref="TreeView.RebuildTree()"/> instead of <see cref="TreeView.RefreshObject(object, bool)"/>
  271. /// </summary>
  272. [Fact]
  273. public void RebuildTree_AfterChangingChildrenGetterDuringRuntime()
  274. {
  275. var tree = CreateTree(out Factory f, out Car c1, out Car c2);
  276. string wheel = "Shiny Wheel";
  277. // Expand the Factory
  278. tree.Expand(f);
  279. // c1 cannot have children
  280. Assert.Equal(f,tree.GetParent(c1));
  281. // expanding it does nothing
  282. tree.Expand(c1);
  283. Assert.False(tree.IsExpanded(c1));
  284. // change the children getter so that now cars can have wheels
  285. tree.TreeBuilder = new DelegateTreeBuilder<object>((o)=>
  286. // factories have cars
  287. o is Factory ? new object[]{c1,c2}
  288. // cars have wheels
  289. : new object[]{wheel });
  290. // still cannot expand
  291. tree.Expand(c1);
  292. Assert.False(tree.IsExpanded(c1));
  293. // Rebuild the tree
  294. tree.RebuildTree();
  295. // Rebuild should not have collapsed any branches or done anything wierd
  296. Assert.True(tree.IsExpanded(f));
  297. tree.Expand(c1);
  298. Assert.True(tree.IsExpanded(c1));
  299. Assert.Equal(wheel,tree.GetChildren(c1).FirstOrDefault());
  300. }
  301. /// <summary>
  302. /// Tests that <see cref="TreeView.GetChildren(object)"/> returns the child objects for
  303. /// the factory. Note that the method only works once the parent branch (Factory)
  304. /// is expanded to expose the child (Car)
  305. /// </summary>
  306. [Fact]
  307. public void GetChildren_ReturnsChildrenOnlyWhenExpanded()
  308. {
  309. var tree = CreateTree(out Factory f, out Car c1, out Car c2);
  310. Assert.Empty(tree.GetChildren(f));
  311. Assert.Empty(tree.GetChildren(c1));
  312. Assert.Empty(tree.GetChildren(c2));
  313. // now when we expand the factory we discover the cars
  314. tree.Expand(f);
  315. Assert.Contains(c1,tree.GetChildren(f));
  316. Assert.Contains(c2,tree.GetChildren(f));
  317. Assert.Empty(tree.GetChildren(c1));
  318. Assert.Empty(tree.GetChildren(c2));
  319. tree.Collapse(f);
  320. Assert.Empty(tree.GetChildren(f));
  321. Assert.Empty(tree.GetChildren(c1));
  322. Assert.Empty(tree.GetChildren(c2));
  323. }
  324. [Fact]
  325. public void TreeNode_WorksWithoutDelegate()
  326. {
  327. var tree = new TreeView();
  328. var root = new TreeNode("Root");
  329. root.Children.Add(new TreeNode("Leaf1"));
  330. root.Children.Add(new TreeNode("Leaf2"));
  331. tree.AddObject(root);
  332. tree.Expand(root);
  333. Assert.Equal(2,tree.GetChildren(root).Count());
  334. }
  335. [Fact]
  336. public void MultiSelect_GetAllSelectedObjects()
  337. {
  338. var tree = new TreeView();
  339. TreeNode l1;
  340. TreeNode l2;
  341. TreeNode l3;
  342. TreeNode l4;
  343. var root = new TreeNode("Root");
  344. root.Children.Add(l1 = new TreeNode("Leaf1"));
  345. root.Children.Add(l2 = new TreeNode("Leaf2"));
  346. root.Children.Add(l3 = new TreeNode("Leaf3"));
  347. root.Children.Add(l4 = new TreeNode("Leaf4"));
  348. tree.AddObject(root);
  349. tree.MultiSelect = true;
  350. tree.Expand(root);
  351. Assert.Empty(tree.GetAllSelectedObjects());
  352. tree.SelectedObject = root;
  353. Assert.Single(tree.GetAllSelectedObjects(),root);
  354. // move selection down 1
  355. tree.AdjustSelection(1,false);
  356. Assert.Single(tree.GetAllSelectedObjects(),l1);
  357. // expand selection down 2 (e.g. shift down twice)
  358. tree.AdjustSelection(1,true);
  359. tree.AdjustSelection(1,true);
  360. Assert.Equal(3,tree.GetAllSelectedObjects().Count());
  361. Assert.Contains(l1,tree.GetAllSelectedObjects());
  362. Assert.Contains(l2,tree.GetAllSelectedObjects());
  363. Assert.Contains(l3,tree.GetAllSelectedObjects());
  364. tree.Collapse(root);
  365. // No selected objects since the root was collapsed
  366. Assert.Empty(tree.GetAllSelectedObjects());
  367. }
  368. [Fact]
  369. public void ObjectActivated_Called()
  370. {
  371. var tree = CreateTree(out Factory f, out Car car1, out _);
  372. InitFakeDriver();
  373. object activated = null;
  374. bool called = false;
  375. // register for the event
  376. tree.ObjectActivated += (s)=>
  377. {
  378. activated = s.ActivatedObject;
  379. called = true;
  380. };
  381. Assert.False(called);
  382. // no object is selected yet so no event should happen
  383. tree.ProcessKey(new KeyEvent(Key.Enter,new KeyModifiers()));
  384. Assert.Null(activated);
  385. Assert.False(called);
  386. // down to select factory
  387. tree.ProcessKey(new KeyEvent(Key.CursorDown,new KeyModifiers()));
  388. tree.ProcessKey(new KeyEvent(Key.Enter,new KeyModifiers()));
  389. Assert.True(called);
  390. Assert.Same(f,activated);
  391. }
  392. [Fact]
  393. public void ObjectActivated_CustomKey()
  394. {
  395. var tree = CreateTree(out Factory f, out Car car1, out _);
  396. InitFakeDriver();
  397. tree.ObjectActivationKey = Key.Delete;
  398. object activated = null;
  399. bool called = false;
  400. // register for the event
  401. tree.ObjectActivated += (s)=>
  402. {
  403. activated = s.ActivatedObject;
  404. called = true;
  405. };
  406. Assert.False(called);
  407. // no object is selected yet so no event should happen
  408. tree.ProcessKey(new KeyEvent(Key.Enter,new KeyModifiers()));
  409. Assert.Null(activated);
  410. Assert.False(called);
  411. // down to select factory
  412. tree.ProcessKey(new KeyEvent(Key.CursorDown,new KeyModifiers()));
  413. tree.ProcessKey(new KeyEvent(Key.Enter,new KeyModifiers()));
  414. // Enter is not the activation key in this unit test
  415. Assert.Null(activated);
  416. Assert.False(called);
  417. // Delete is the activation key in this test so should result in activation occurring
  418. tree.ProcessKey(new KeyEvent(Key.Delete,new KeyModifiers()));
  419. Assert.True(called);
  420. Assert.Same(f,activated);
  421. }
  422. /// <summary>
  423. /// 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)"/>
  424. /// </summary>
  425. [Fact]
  426. public void RefreshObject_EqualityTest()
  427. {
  428. var obj1 = new EqualityTestObject(){Name="Bob",Age=1 };
  429. var obj2 = new EqualityTestObject(){Name="Bob",Age=2 };;
  430. string root = "root";
  431. var tree = new TreeView<object>();
  432. tree.TreeBuilder = new DelegateTreeBuilder<object>((s)=> ReferenceEquals(s , root) ? new object[]{obj1 } : null);
  433. tree.AddObject(root);
  434. // Tree is not expanded so the root has no children yet
  435. Assert.Empty(tree.GetChildren(root));
  436. tree.Expand(root);
  437. // now that the tree is expanded we should get our child returned
  438. Assert.Equal(1,tree.GetChildren(root).Count(child=>ReferenceEquals(obj1,child)));
  439. // change the getter to return an Equal object (but not the same reference - obj2)
  440. tree.TreeBuilder = new DelegateTreeBuilder<object>((s)=> ReferenceEquals(s , root) ? new object[]{obj2 } : null);
  441. // tree has cached the knowledge of what children the root has so won't know about the change (we still get obj1)
  442. Assert.Equal(1,tree.GetChildren(root).Count(child=>ReferenceEquals(obj1,child)));
  443. // now that we refresh the root we should get the new child reference (obj2)
  444. tree.RefreshObject(root);
  445. Assert.Equal(1,tree.GetChildren(root).Count(child=>ReferenceEquals(obj2,child)));
  446. }
  447. /// <summary>
  448. /// Test object which considers for equality only <see cref="Name"/>
  449. /// </summary>
  450. private class EqualityTestObject
  451. {
  452. public string Name { get;set;}
  453. public int Age { get;set;}
  454. public override int GetHashCode ()
  455. {
  456. return Name?.GetHashCode()??base.GetHashCode ();
  457. }
  458. public override bool Equals (object obj)
  459. {
  460. return obj is EqualityTestObject eto && Equals(Name, eto.Name);
  461. }
  462. }
  463. private void InitFakeDriver()
  464. {
  465. var driver = new FakeDriver ();
  466. Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true)));
  467. driver.Init (() => { });
  468. }
  469. }
  470. }