using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Threading.Tasks; using Terminal.Gui; using Xunit; using System.Globalization; using Xunit.Abstractions; namespace Terminal.Gui.Views { public class TableViewTests { readonly ITestOutputHelper output; public TableViewTests (ITestOutputHelper output) { this.output = output; } [Fact] public void EnsureValidScrollOffsets_WithNoCells () { var tableView = new TableView (); Assert.Equal (0, tableView.RowOffset); Assert.Equal (0, tableView.ColumnOffset); // Set empty table tableView.Table = new DataTable (); // Since table has no rows or columns scroll offset should default to 0 tableView.EnsureValidScrollOffsets (); Assert.Equal (0, tableView.RowOffset); Assert.Equal (0, tableView.ColumnOffset); } [Fact] public void EnsureValidScrollOffsets_LoadSmallerTable () { var tableView = new TableView (); tableView.Bounds = new Rect (0, 0, 25, 10); Assert.Equal (0, tableView.RowOffset); Assert.Equal (0, tableView.ColumnOffset); // Set big table tableView.Table = BuildTable (25, 50); // Scroll down and along tableView.RowOffset = 20; tableView.ColumnOffset = 10; tableView.EnsureValidScrollOffsets (); // The scroll should be valid at the moment Assert.Equal (20, tableView.RowOffset); Assert.Equal (10, tableView.ColumnOffset); // Set small table tableView.Table = BuildTable (2, 2); // Setting a small table should automatically trigger fixing the scroll offsets to ensure valid cells Assert.Equal (0, tableView.RowOffset); Assert.Equal (0, tableView.ColumnOffset); // Trying to set invalid indexes should not be possible tableView.RowOffset = 20; tableView.ColumnOffset = 10; Assert.Equal (1, tableView.RowOffset); Assert.Equal (1, tableView.ColumnOffset); } [Fact] public void SelectedCellChanged_NotFiredForSameValue () { var tableView = new TableView () { Table = BuildTable (25, 50) }; bool called = false; tableView.SelectedCellChanged += (e) => { called = true; }; Assert.Equal (0, tableView.SelectedColumn); Assert.False (called); // Changing value to same as it already was should not raise an event tableView.SelectedColumn = 0; Assert.False (called); tableView.SelectedColumn = 10; Assert.True (called); } [Fact] public void SelectedCellChanged_SelectedColumnIndexesCorrect () { var tableView = new TableView () { Table = BuildTable (25, 50) }; bool called = false; tableView.SelectedCellChanged += (e) => { called = true; Assert.Equal (0, e.OldCol); Assert.Equal (10, e.NewCol); }; tableView.SelectedColumn = 10; Assert.True (called); } [Fact] public void SelectedCellChanged_SelectedRowIndexesCorrect () { var tableView = new TableView () { Table = BuildTable (25, 50) }; bool called = false; tableView.SelectedCellChanged += (e) => { called = true; Assert.Equal (0, e.OldRow); Assert.Equal (10, e.NewRow); }; tableView.SelectedRow = 10; Assert.True (called); } [Fact] public void Test_SumColumnWidth_UnicodeLength () { Assert.Equal (11, "hello there".Sum (c => Rune.ColumnWidth (c))); // Creates a string with the peculiar (french?) r symbol String surrogate = "Les Mise" + Char.ConvertFromUtf32 (Int32.Parse ("0301", NumberStyles.HexNumber)) + "rables"; // The unicode width of this string is shorter than the string length! Assert.Equal (14, surrogate.Sum (c => Rune.ColumnWidth (c))); Assert.Equal (15, surrogate.Length); } [Fact] public void IsSelected_MultiSelectionOn_Vertical () { var tableView = new TableView () { Table = BuildTable (25, 50), MultiSelect = true }; // 3 cell vertical selection tableView.SetSelection (1, 1, false); tableView.SetSelection (1, 3, true); Assert.False (tableView.IsSelected (0, 0)); Assert.False (tableView.IsSelected (1, 0)); Assert.False (tableView.IsSelected (2, 0)); Assert.False (tableView.IsSelected (0, 1)); Assert.True (tableView.IsSelected (1, 1)); Assert.False (tableView.IsSelected (2, 1)); Assert.False (tableView.IsSelected (0, 2)); Assert.True (tableView.IsSelected (1, 2)); Assert.False (tableView.IsSelected (2, 2)); Assert.False (tableView.IsSelected (0, 3)); Assert.True (tableView.IsSelected (1, 3)); Assert.False (tableView.IsSelected (2, 3)); Assert.False (tableView.IsSelected (0, 4)); Assert.False (tableView.IsSelected (1, 4)); Assert.False (tableView.IsSelected (2, 4)); } [Fact] public void IsSelected_MultiSelectionOn_Horizontal () { var tableView = new TableView () { Table = BuildTable (25, 50), MultiSelect = true }; // 2 cell horizontal selection tableView.SetSelection (1, 0, false); tableView.SetSelection (2, 0, true); Assert.False (tableView.IsSelected (0, 0)); Assert.True (tableView.IsSelected (1, 0)); Assert.True (tableView.IsSelected (2, 0)); Assert.False (tableView.IsSelected (3, 0)); Assert.False (tableView.IsSelected (0, 1)); Assert.False (tableView.IsSelected (1, 1)); Assert.False (tableView.IsSelected (2, 1)); Assert.False (tableView.IsSelected (3, 1)); } [Fact] public void IsSelected_MultiSelectionOn_BoxSelection () { var tableView = new TableView () { Table = BuildTable (25, 50), MultiSelect = true }; // 4 cell horizontal in box 2x2 tableView.SetSelection (0, 0, false); tableView.SetSelection (1, 1, true); Assert.True (tableView.IsSelected (0, 0)); Assert.True (tableView.IsSelected (1, 0)); Assert.False (tableView.IsSelected (2, 0)); Assert.True (tableView.IsSelected (0, 1)); Assert.True (tableView.IsSelected (1, 1)); Assert.False (tableView.IsSelected (2, 1)); Assert.False (tableView.IsSelected (0, 2)); Assert.False (tableView.IsSelected (1, 2)); Assert.False (tableView.IsSelected (2, 2)); } [Fact] public void PageDown_ExcludesHeaders () { var driver = new FakeDriver (); Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true))); driver.Init (() => { }); var tableView = new TableView () { Table = BuildTable (25, 50), MultiSelect = true, Bounds = new Rect (0, 0, 10, 5) }; // Header should take up 2 lines tableView.Style.ShowHorizontalHeaderOverline = false; tableView.Style.ShowHorizontalHeaderUnderline = true; tableView.Style.AlwaysShowHeaders = false; Assert.Equal (0, tableView.RowOffset); tableView.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ())); // window height is 5 rows 2 are header so page down should give 3 new rows Assert.Equal (3, tableView.RowOffset); // header is no longer visible so page down should give 5 new rows tableView.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ())); Assert.Equal (8, tableView.RowOffset); // Shutdown must be called to safely clean up Application if Init has been called Application.Shutdown (); } [Fact] public void DeleteRow_SelectAll_AdjustsSelectionToPreventOverrun () { // create a 4 by 4 table var tableView = new TableView () { Table = BuildTable (4, 4), MultiSelect = true, Bounds = new Rect (0, 0, 10, 5) }; tableView.SelectAll (); Assert.Equal (16, tableView.GetAllSelectedCells ().Count ()); // delete one of the columns tableView.Table.Columns.RemoveAt (2); // table should now be 3x4 Assert.Equal (12, tableView.GetAllSelectedCells ().Count ()); // remove a row tableView.Table.Rows.RemoveAt (1); // table should now be 3x3 Assert.Equal (9, tableView.GetAllSelectedCells ().Count ()); } [Fact] public void DeleteRow_SelectLastRow_AdjustsSelectionToPreventOverrun () { // create a 4 by 4 table var tableView = new TableView () { Table = BuildTable (4, 4), MultiSelect = true, Bounds = new Rect (0, 0, 10, 5) }; // select the last row tableView.MultiSelectedRegions.Clear (); tableView.MultiSelectedRegions.Push (new TableView.TableSelection (new Point (0, 3), new Rect (0, 3, 4, 1))); Assert.Equal (4, tableView.GetAllSelectedCells ().Count ()); // remove a row tableView.Table.Rows.RemoveAt (0); tableView.EnsureValidSelection (); // since the selection no longer exists it should be removed Assert.Empty (tableView.MultiSelectedRegions); } [Theory] [InlineData (true)] [InlineData (false)] public void GetAllSelectedCells_SingleCellSelected_ReturnsOne (bool multiSelect) { var tableView = new TableView () { Table = BuildTable (3, 3), MultiSelect = multiSelect, Bounds = new Rect (0, 0, 10, 5) }; tableView.SetSelection (1, 1, false); Assert.Single (tableView.GetAllSelectedCells ()); Assert.Equal (new Point (1, 1), tableView.GetAllSelectedCells ().Single ()); } [Fact] public void GetAllSelectedCells_SquareSelection_ReturnsFour () { var tableView = new TableView () { Table = BuildTable (3, 3), MultiSelect = true, Bounds = new Rect (0, 0, 10, 5) }; // move cursor to 1,1 tableView.SetSelection (1, 1, false); // spread selection across to 2,2 (e.g. shift+right then shift+down) tableView.SetSelection (2, 2, true); var selected = tableView.GetAllSelectedCells ().ToArray (); Assert.Equal (4, selected.Length); Assert.Equal (new Point (1, 1), selected [0]); Assert.Equal (new Point (2, 1), selected [1]); Assert.Equal (new Point (1, 2), selected [2]); Assert.Equal (new Point (2, 2), selected [3]); } [Fact] public void GetAllSelectedCells_SquareSelection_FullRowSelect () { var tableView = new TableView () { Table = BuildTable (3, 3), MultiSelect = true, FullRowSelect = true, Bounds = new Rect (0, 0, 10, 5) }; // move cursor to 1,1 tableView.SetSelection (1, 1, false); // spread selection across to 2,2 (e.g. shift+right then shift+down) tableView.SetSelection (2, 2, true); var selected = tableView.GetAllSelectedCells ().ToArray (); Assert.Equal (6, selected.Length); Assert.Equal (new Point (0, 1), selected [0]); Assert.Equal (new Point (1, 1), selected [1]); Assert.Equal (new Point (2, 1), selected [2]); Assert.Equal (new Point (0, 2), selected [3]); Assert.Equal (new Point (1, 2), selected [4]); Assert.Equal (new Point (2, 2), selected [5]); } [Fact] public void GetAllSelectedCells_TwoIsolatedSelections_ReturnsSix () { var tableView = new TableView () { Table = BuildTable (20, 20), MultiSelect = true, Bounds = new Rect (0, 0, 10, 5) }; /* Sets up disconnected selections like: 00000000000 01100000000 01100000000 00000001100 00000000000 */ tableView.MultiSelectedRegions.Clear (); tableView.MultiSelectedRegions.Push (new TableView.TableSelection (new Point (1, 1), new Rect (1, 1, 2, 2))); tableView.MultiSelectedRegions.Push (new TableView.TableSelection (new Point (7, 3), new Rect (7, 3, 2, 1))); tableView.SelectedColumn = 8; tableView.SelectedRow = 3; var selected = tableView.GetAllSelectedCells ().ToArray (); Assert.Equal (6, selected.Length); Assert.Equal (new Point (1, 1), selected [0]); Assert.Equal (new Point (2, 1), selected [1]); Assert.Equal (new Point (1, 2), selected [2]); Assert.Equal (new Point (2, 2), selected [3]); Assert.Equal (new Point (7, 3), selected [4]); Assert.Equal (new Point (8, 3), selected [5]); } [Fact] public void TableView_ExpandLastColumn_True () { var tv = SetUpMiniTable (); // the thing we are testing tv.Style.ExpandLastColumn = true; tv.Redraw (tv.Bounds); string expected = @" ┌─┬──────┐ │A│B │ ├─┼──────┤ │1│2 │ "; GraphViewTests.AssertDriverContentsAre (expected, output); // Shutdown must be called to safely clean up Application if Init has been called Application.Shutdown (); } [Fact] public void TableView_ExpandLastColumn_False () { var tv = SetUpMiniTable (); // the thing we are testing tv.Style.ExpandLastColumn = false; tv.Redraw (tv.Bounds); string expected = @" ┌─┬─┬────┐ │A│B│ │ ├─┼─┼────┤ │1│2│ │ "; GraphViewTests.AssertDriverContentsAre (expected, output); // Shutdown must be called to safely clean up Application if Init has been called Application.Shutdown (); } [Fact] public void TableView_ExpandLastColumn_False_ExactBounds () { var tv = SetUpMiniTable (); // the thing we are testing tv.Style.ExpandLastColumn = false; // width exactly matches the max col widths tv.Bounds = new Rect (0, 0, 5, 4); tv.Redraw (tv.Bounds); string expected = @" ┌─┬─┐ │A│B│ ├─┼─┤ │1│2│ "; GraphViewTests.AssertDriverContentsAre (expected, output); // Shutdown must be called to safely clean up Application if Init has been called Application.Shutdown (); } private TableView SetUpMiniTable () { var tv = new TableView (); tv.Bounds = new Rect (0, 0, 10, 4); var dt = new DataTable (); var colA = dt.Columns.Add ("A"); var colB = dt.Columns.Add ("B"); dt.Rows.Add (1, 2); tv.Table = dt; tv.Style.GetOrCreateColumnStyle (colA).MinWidth = 1; tv.Style.GetOrCreateColumnStyle (colA).MinWidth = 1; tv.Style.GetOrCreateColumnStyle (colB).MaxWidth = 1; tv.Style.GetOrCreateColumnStyle (colB).MaxWidth = 1; GraphViewTests.InitFakeDriver (); tv.ColorScheme = new ColorScheme () { Normal = Application.Driver.MakeAttribute (Color.White, Color.Black), HotFocus = Application.Driver.MakeAttribute (Color.White, Color.Black) }; return tv; } /// /// Builds a simple table of string columns with the requested number of columns and rows /// /// /// /// public static DataTable BuildTable (int cols, int rows) { var dt = new DataTable (); for (int c = 0; c < cols; c++) { dt.Columns.Add ("Col" + c); } for (int r = 0; r < rows; r++) { var newRow = dt.NewRow (); for (int c = 0; c < cols; c++) { newRow [c] = $"R{r}C{c}"; } dt.Rows.Add (newRow); } return dt; } } }