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); } [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); } [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); } [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); } 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; } } }