TableViewTests.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data;
  4. using System.Linq;
  5. using System.Threading.Tasks;
  6. using Terminal.Gui;
  7. using Xunit;
  8. using System.Globalization;
  9. using Xunit.Abstractions;
  10. namespace Terminal.Gui.Views {
  11. public class TableViewTests {
  12. readonly ITestOutputHelper output;
  13. public TableViewTests (ITestOutputHelper output)
  14. {
  15. this.output = output;
  16. }
  17. [Fact]
  18. public void EnsureValidScrollOffsets_WithNoCells ()
  19. {
  20. var tableView = new TableView ();
  21. Assert.Equal (0, tableView.RowOffset);
  22. Assert.Equal (0, tableView.ColumnOffset);
  23. // Set empty table
  24. tableView.Table = new DataTable ();
  25. // Since table has no rows or columns scroll offset should default to 0
  26. tableView.EnsureValidScrollOffsets ();
  27. Assert.Equal (0, tableView.RowOffset);
  28. Assert.Equal (0, tableView.ColumnOffset);
  29. }
  30. [Fact]
  31. public void EnsureValidScrollOffsets_LoadSmallerTable ()
  32. {
  33. var tableView = new TableView ();
  34. tableView.Bounds = new Rect (0, 0, 25, 10);
  35. Assert.Equal (0, tableView.RowOffset);
  36. Assert.Equal (0, tableView.ColumnOffset);
  37. // Set big table
  38. tableView.Table = BuildTable (25, 50);
  39. // Scroll down and along
  40. tableView.RowOffset = 20;
  41. tableView.ColumnOffset = 10;
  42. tableView.EnsureValidScrollOffsets ();
  43. // The scroll should be valid at the moment
  44. Assert.Equal (20, tableView.RowOffset);
  45. Assert.Equal (10, tableView.ColumnOffset);
  46. // Set small table
  47. tableView.Table = BuildTable (2, 2);
  48. // Setting a small table should automatically trigger fixing the scroll offsets to ensure valid cells
  49. Assert.Equal (0, tableView.RowOffset);
  50. Assert.Equal (0, tableView.ColumnOffset);
  51. // Trying to set invalid indexes should not be possible
  52. tableView.RowOffset = 20;
  53. tableView.ColumnOffset = 10;
  54. Assert.Equal (1, tableView.RowOffset);
  55. Assert.Equal (1, tableView.ColumnOffset);
  56. }
  57. [Fact]
  58. public void SelectedCellChanged_NotFiredForSameValue ()
  59. {
  60. var tableView = new TableView () {
  61. Table = BuildTable (25, 50)
  62. };
  63. bool called = false;
  64. tableView.SelectedCellChanged += (e) => { called = true; };
  65. Assert.Equal (0, tableView.SelectedColumn);
  66. Assert.False (called);
  67. // Changing value to same as it already was should not raise an event
  68. tableView.SelectedColumn = 0;
  69. Assert.False (called);
  70. tableView.SelectedColumn = 10;
  71. Assert.True (called);
  72. }
  73. [Fact]
  74. public void SelectedCellChanged_SelectedColumnIndexesCorrect ()
  75. {
  76. var tableView = new TableView () {
  77. Table = BuildTable (25, 50)
  78. };
  79. bool called = false;
  80. tableView.SelectedCellChanged += (e) => {
  81. called = true;
  82. Assert.Equal (0, e.OldCol);
  83. Assert.Equal (10, e.NewCol);
  84. };
  85. tableView.SelectedColumn = 10;
  86. Assert.True (called);
  87. }
  88. [Fact]
  89. public void SelectedCellChanged_SelectedRowIndexesCorrect ()
  90. {
  91. var tableView = new TableView () {
  92. Table = BuildTable (25, 50)
  93. };
  94. bool called = false;
  95. tableView.SelectedCellChanged += (e) => {
  96. called = true;
  97. Assert.Equal (0, e.OldRow);
  98. Assert.Equal (10, e.NewRow);
  99. };
  100. tableView.SelectedRow = 10;
  101. Assert.True (called);
  102. }
  103. [Fact]
  104. public void Test_SumColumnWidth_UnicodeLength ()
  105. {
  106. Assert.Equal (11, "hello there".Sum (c => Rune.ColumnWidth (c)));
  107. // Creates a string with the peculiar (french?) r symbol
  108. String surrogate = "Les Mise" + Char.ConvertFromUtf32 (Int32.Parse ("0301", NumberStyles.HexNumber)) + "rables";
  109. // The unicode width of this string is shorter than the string length!
  110. Assert.Equal (14, surrogate.Sum (c => Rune.ColumnWidth (c)));
  111. Assert.Equal (15, surrogate.Length);
  112. }
  113. [Fact]
  114. public void IsSelected_MultiSelectionOn_Vertical ()
  115. {
  116. var tableView = new TableView () {
  117. Table = BuildTable (25, 50),
  118. MultiSelect = true
  119. };
  120. // 3 cell vertical selection
  121. tableView.SetSelection (1, 1, false);
  122. tableView.SetSelection (1, 3, true);
  123. Assert.False (tableView.IsSelected (0, 0));
  124. Assert.False (tableView.IsSelected (1, 0));
  125. Assert.False (tableView.IsSelected (2, 0));
  126. Assert.False (tableView.IsSelected (0, 1));
  127. Assert.True (tableView.IsSelected (1, 1));
  128. Assert.False (tableView.IsSelected (2, 1));
  129. Assert.False (tableView.IsSelected (0, 2));
  130. Assert.True (tableView.IsSelected (1, 2));
  131. Assert.False (tableView.IsSelected (2, 2));
  132. Assert.False (tableView.IsSelected (0, 3));
  133. Assert.True (tableView.IsSelected (1, 3));
  134. Assert.False (tableView.IsSelected (2, 3));
  135. Assert.False (tableView.IsSelected (0, 4));
  136. Assert.False (tableView.IsSelected (1, 4));
  137. Assert.False (tableView.IsSelected (2, 4));
  138. }
  139. [Fact]
  140. public void IsSelected_MultiSelectionOn_Horizontal ()
  141. {
  142. var tableView = new TableView () {
  143. Table = BuildTable (25, 50),
  144. MultiSelect = true
  145. };
  146. // 2 cell horizontal selection
  147. tableView.SetSelection (1, 0, false);
  148. tableView.SetSelection (2, 0, true);
  149. Assert.False (tableView.IsSelected (0, 0));
  150. Assert.True (tableView.IsSelected (1, 0));
  151. Assert.True (tableView.IsSelected (2, 0));
  152. Assert.False (tableView.IsSelected (3, 0));
  153. Assert.False (tableView.IsSelected (0, 1));
  154. Assert.False (tableView.IsSelected (1, 1));
  155. Assert.False (tableView.IsSelected (2, 1));
  156. Assert.False (tableView.IsSelected (3, 1));
  157. }
  158. [Fact]
  159. public void IsSelected_MultiSelectionOn_BoxSelection ()
  160. {
  161. var tableView = new TableView () {
  162. Table = BuildTable (25, 50),
  163. MultiSelect = true
  164. };
  165. // 4 cell horizontal in box 2x2
  166. tableView.SetSelection (0, 0, false);
  167. tableView.SetSelection (1, 1, true);
  168. Assert.True (tableView.IsSelected (0, 0));
  169. Assert.True (tableView.IsSelected (1, 0));
  170. Assert.False (tableView.IsSelected (2, 0));
  171. Assert.True (tableView.IsSelected (0, 1));
  172. Assert.True (tableView.IsSelected (1, 1));
  173. Assert.False (tableView.IsSelected (2, 1));
  174. Assert.False (tableView.IsSelected (0, 2));
  175. Assert.False (tableView.IsSelected (1, 2));
  176. Assert.False (tableView.IsSelected (2, 2));
  177. }
  178. [AutoInitShutdown]
  179. [Fact]
  180. public void PageDown_ExcludesHeaders ()
  181. {
  182. var tableView = new TableView () {
  183. Table = BuildTable (25, 50),
  184. MultiSelect = true,
  185. Bounds = new Rect (0, 0, 10, 5)
  186. };
  187. // Header should take up 2 lines
  188. tableView.Style.ShowHorizontalHeaderOverline = false;
  189. tableView.Style.ShowHorizontalHeaderUnderline = true;
  190. tableView.Style.AlwaysShowHeaders = false;
  191. // ensure that TableView has the input focus
  192. Application.Top.Add (tableView);
  193. Application.Top.FocusFirst ();
  194. Assert.True (tableView.HasFocus);
  195. Assert.Equal (0, tableView.RowOffset);
  196. tableView.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()));
  197. // window height is 5 rows 2 are header so page down should give 3 new rows
  198. Assert.Equal (3, tableView.RowOffset);
  199. // header is no longer visible so page down should give 5 new rows
  200. tableView.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()));
  201. Assert.Equal (8, tableView.RowOffset);
  202. }
  203. [Fact]
  204. public void DeleteRow_SelectAll_AdjustsSelectionToPreventOverrun ()
  205. {
  206. // create a 4 by 4 table
  207. var tableView = new TableView () {
  208. Table = BuildTable (4, 4),
  209. MultiSelect = true,
  210. Bounds = new Rect (0, 0, 10, 5)
  211. };
  212. tableView.SelectAll ();
  213. Assert.Equal (16, tableView.GetAllSelectedCells ().Count ());
  214. // delete one of the columns
  215. tableView.Table.Columns.RemoveAt (2);
  216. // table should now be 3x4
  217. Assert.Equal (12, tableView.GetAllSelectedCells ().Count ());
  218. // remove a row
  219. tableView.Table.Rows.RemoveAt (1);
  220. // table should now be 3x3
  221. Assert.Equal (9, tableView.GetAllSelectedCells ().Count ());
  222. }
  223. [Fact]
  224. public void DeleteRow_SelectLastRow_AdjustsSelectionToPreventOverrun ()
  225. {
  226. // create a 4 by 4 table
  227. var tableView = new TableView () {
  228. Table = BuildTable (4, 4),
  229. MultiSelect = true,
  230. Bounds = new Rect (0, 0, 10, 5)
  231. };
  232. // select the last row
  233. tableView.MultiSelectedRegions.Clear ();
  234. tableView.MultiSelectedRegions.Push (new TableView.TableSelection (new Point (0, 3), new Rect (0, 3, 4, 1)));
  235. Assert.Equal (4, tableView.GetAllSelectedCells ().Count ());
  236. // remove a row
  237. tableView.Table.Rows.RemoveAt (0);
  238. tableView.EnsureValidSelection ();
  239. // since the selection no longer exists it should be removed
  240. Assert.Empty (tableView.MultiSelectedRegions);
  241. }
  242. [Theory]
  243. [InlineData (true)]
  244. [InlineData (false)]
  245. public void GetAllSelectedCells_SingleCellSelected_ReturnsOne (bool multiSelect)
  246. {
  247. var tableView = new TableView () {
  248. Table = BuildTable (3, 3),
  249. MultiSelect = multiSelect,
  250. Bounds = new Rect (0, 0, 10, 5)
  251. };
  252. tableView.SetSelection (1, 1, false);
  253. Assert.Single (tableView.GetAllSelectedCells ());
  254. Assert.Equal (new Point (1, 1), tableView.GetAllSelectedCells ().Single ());
  255. }
  256. [Fact]
  257. public void GetAllSelectedCells_SquareSelection_ReturnsFour ()
  258. {
  259. var tableView = new TableView () {
  260. Table = BuildTable (3, 3),
  261. MultiSelect = true,
  262. Bounds = new Rect (0, 0, 10, 5)
  263. };
  264. // move cursor to 1,1
  265. tableView.SetSelection (1, 1, false);
  266. // spread selection across to 2,2 (e.g. shift+right then shift+down)
  267. tableView.SetSelection (2, 2, true);
  268. var selected = tableView.GetAllSelectedCells ().ToArray ();
  269. Assert.Equal (4, selected.Length);
  270. Assert.Equal (new Point (1, 1), selected [0]);
  271. Assert.Equal (new Point (2, 1), selected [1]);
  272. Assert.Equal (new Point (1, 2), selected [2]);
  273. Assert.Equal (new Point (2, 2), selected [3]);
  274. }
  275. [Fact]
  276. public void GetAllSelectedCells_SquareSelection_FullRowSelect ()
  277. {
  278. var tableView = new TableView () {
  279. Table = BuildTable (3, 3),
  280. MultiSelect = true,
  281. FullRowSelect = true,
  282. Bounds = new Rect (0, 0, 10, 5)
  283. };
  284. // move cursor to 1,1
  285. tableView.SetSelection (1, 1, false);
  286. // spread selection across to 2,2 (e.g. shift+right then shift+down)
  287. tableView.SetSelection (2, 2, true);
  288. var selected = tableView.GetAllSelectedCells ().ToArray ();
  289. Assert.Equal (6, selected.Length);
  290. Assert.Equal (new Point (0, 1), selected [0]);
  291. Assert.Equal (new Point (1, 1), selected [1]);
  292. Assert.Equal (new Point (2, 1), selected [2]);
  293. Assert.Equal (new Point (0, 2), selected [3]);
  294. Assert.Equal (new Point (1, 2), selected [4]);
  295. Assert.Equal (new Point (2, 2), selected [5]);
  296. }
  297. [Fact]
  298. public void GetAllSelectedCells_TwoIsolatedSelections_ReturnsSix ()
  299. {
  300. var tableView = new TableView () {
  301. Table = BuildTable (20, 20),
  302. MultiSelect = true,
  303. Bounds = new Rect (0, 0, 10, 5)
  304. };
  305. /*
  306. Sets up disconnected selections like:
  307. 00000000000
  308. 01100000000
  309. 01100000000
  310. 00000001100
  311. 00000000000
  312. */
  313. tableView.MultiSelectedRegions.Clear ();
  314. tableView.MultiSelectedRegions.Push (new TableView.TableSelection (new Point (1, 1), new Rect (1, 1, 2, 2)));
  315. tableView.MultiSelectedRegions.Push (new TableView.TableSelection (new Point (7, 3), new Rect (7, 3, 2, 1)));
  316. tableView.SelectedColumn = 8;
  317. tableView.SelectedRow = 3;
  318. var selected = tableView.GetAllSelectedCells ().ToArray ();
  319. Assert.Equal (6, selected.Length);
  320. Assert.Equal (new Point (1, 1), selected [0]);
  321. Assert.Equal (new Point (2, 1), selected [1]);
  322. Assert.Equal (new Point (1, 2), selected [2]);
  323. Assert.Equal (new Point (2, 2), selected [3]);
  324. Assert.Equal (new Point (7, 3), selected [4]);
  325. Assert.Equal (new Point (8, 3), selected [5]);
  326. }
  327. [Fact]
  328. public void TableView_ExpandLastColumn_True ()
  329. {
  330. var tv = SetUpMiniTable ();
  331. // the thing we are testing
  332. tv.Style.ExpandLastColumn = true;
  333. tv.Redraw (tv.Bounds);
  334. string expected = @"
  335. ┌─┬──────┐
  336. │A│B │
  337. ├─┼──────┤
  338. │1│2 │
  339. ";
  340. GraphViewTests.AssertDriverContentsAre (expected, output);
  341. // Shutdown must be called to safely clean up Application if Init has been called
  342. Application.Shutdown ();
  343. }
  344. [Fact]
  345. public void TableView_ExpandLastColumn_False ()
  346. {
  347. var tv = SetUpMiniTable ();
  348. // the thing we are testing
  349. tv.Style.ExpandLastColumn = false;
  350. tv.Redraw (tv.Bounds);
  351. string expected = @"
  352. ┌─┬─┬────┐
  353. │A│B│ │
  354. ├─┼─┼────┤
  355. │1│2│ │
  356. ";
  357. GraphViewTests.AssertDriverContentsAre (expected, output);
  358. // Shutdown must be called to safely clean up Application if Init has been called
  359. Application.Shutdown ();
  360. }
  361. [Fact]
  362. public void TableView_ExpandLastColumn_False_ExactBounds ()
  363. {
  364. var tv = SetUpMiniTable ();
  365. // the thing we are testing
  366. tv.Style.ExpandLastColumn = false;
  367. // width exactly matches the max col widths
  368. tv.Bounds = new Rect (0, 0, 5, 4);
  369. tv.Redraw (tv.Bounds);
  370. string expected = @"
  371. ┌─┬─┐
  372. │A│B│
  373. ├─┼─┤
  374. │1│2│
  375. ";
  376. GraphViewTests.AssertDriverContentsAre (expected, output);
  377. // Shutdown must be called to safely clean up Application if Init has been called
  378. Application.Shutdown ();
  379. }
  380. [Fact]
  381. [AutoInitShutdown]
  382. public void TableView_Activate()
  383. {
  384. string activatedValue = null;
  385. var tv = new TableView (BuildTable(1,1));
  386. tv.CellActivated += (c) => activatedValue = c.Table.Rows[c.Row][c.Col].ToString();
  387. Application.Top.Add (tv);
  388. Application.Begin (Application.Top);
  389. // pressing enter should activate the first cell (selected cell)
  390. tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()));
  391. Assert.Equal ("R0C0",activatedValue);
  392. // reset the test
  393. activatedValue = null;
  394. // clear keybindings and ensure that Enter does not trigger the event anymore
  395. tv.ClearKeybindings ();
  396. tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()));
  397. Assert.Null(activatedValue);
  398. // New method for changing the activation key
  399. tv.AddKeyBinding (Key.z, Command.Accept);
  400. tv.ProcessKey (new KeyEvent (Key.z, new KeyModifiers ()));
  401. Assert.Equal ("R0C0", activatedValue);
  402. // reset the test
  403. activatedValue = null;
  404. tv.ClearKeybindings ();
  405. // Old method for changing the activation key
  406. tv.CellActivationKey = Key.z;
  407. tv.ProcessKey (new KeyEvent (Key.z, new KeyModifiers ()));
  408. Assert.Equal ("R0C0", activatedValue);
  409. }
  410. [Fact]
  411. public void TableView_ColorsTest_ColorGetter ()
  412. {
  413. var tv = SetUpMiniTable ();
  414. tv.Style.ExpandLastColumn = false;
  415. tv.Style.InvertSelectedCellFirstCharacter = true;
  416. // width exactly matches the max col widths
  417. tv.Bounds = new Rect (0, 0, 5, 4);
  418. // Create a style for column B
  419. var bStyle = tv.Style.GetOrCreateColumnStyle (tv.Table.Columns ["B"]);
  420. // when B is 2 use the custom highlight colour
  421. ColorScheme cellHighlight = new ColorScheme () { Normal = Attribute.Make (Color.BrightCyan, Color.DarkGray) };
  422. bStyle.ColorGetter = (a) => Convert.ToInt32(a.CellValue) == 2 ? cellHighlight : null;
  423. tv.Redraw (tv.Bounds);
  424. string expected = @"
  425. ┌─┬─┐
  426. │A│B│
  427. ├─┼─┤
  428. │1│2│
  429. ";
  430. GraphViewTests.AssertDriverContentsAre (expected, output);
  431. string expectedColors = @"
  432. 00000
  433. 00000
  434. 00000
  435. 01020
  436. ";
  437. var invertedNormalColor = Application.Driver.MakeAttribute (tv.ColorScheme.Normal.Background, tv.ColorScheme.Normal.Foreground);
  438. GraphViewTests.AssertDriverColorsAre (expectedColors, new Attribute [] {
  439. // 0
  440. tv.ColorScheme.Normal,
  441. // 1
  442. invertedNormalColor,
  443. // 2
  444. cellHighlight.Normal});
  445. // Shutdown must be called to safely clean up Application if Init has been called
  446. Application.Shutdown ();
  447. }
  448. private TableView SetUpMiniTable ()
  449. {
  450. var tv = new TableView ();
  451. tv.Bounds = new Rect (0, 0, 10, 4);
  452. var dt = new DataTable ();
  453. var colA = dt.Columns.Add ("A");
  454. var colB = dt.Columns.Add ("B");
  455. dt.Rows.Add (1, 2);
  456. tv.Table = dt;
  457. tv.Style.GetOrCreateColumnStyle (colA).MinWidth = 1;
  458. tv.Style.GetOrCreateColumnStyle (colA).MinWidth = 1;
  459. tv.Style.GetOrCreateColumnStyle (colB).MaxWidth = 1;
  460. tv.Style.GetOrCreateColumnStyle (colB).MaxWidth = 1;
  461. GraphViewTests.InitFakeDriver ();
  462. tv.ColorScheme = new ColorScheme () {
  463. Normal = Application.Driver.MakeAttribute (Color.White, Color.Black),
  464. HotFocus = Application.Driver.MakeAttribute (Color.White, Color.Black)
  465. };
  466. return tv;
  467. }
  468. /// <summary>
  469. /// Builds a simple table of string columns with the requested number of columns and rows
  470. /// </summary>
  471. /// <param name="cols"></param>
  472. /// <param name="rows"></param>
  473. /// <returns></returns>
  474. public static DataTable BuildTable (int cols, int rows)
  475. {
  476. var dt = new DataTable ();
  477. for (int c = 0; c < cols; c++) {
  478. dt.Columns.Add ("Col" + c);
  479. }
  480. for (int r = 0; r < rows; r++) {
  481. var newRow = dt.NewRow ();
  482. for (int c = 0; c < cols; c++) {
  483. newRow [c] = $"R{r}C{c}";
  484. }
  485. dt.Rows.Add (newRow);
  486. }
  487. return dt;
  488. }
  489. }
  490. }