#!/usr/bin/env python3 # MIT License # # Copyright (c) 2025 Eugenio Parodi # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import os import sys import pytest from unittest.mock import Mock, MagicMock, patch sys.path.append(os.path.join(sys.path[0],'../../../libs/pyTermTk')) import TermTk as ttk class TestTTkTableWidget: """Test cases for TTkTableWidget class""" def setup_method(self): """Set up test fixtures before each test method.""" self.test_data = [ ['Alice', 25, 'Engineer'], ['Bob', 30, 'Designer'], ['Charlie', 35, 'Manager'] ] self.header = ['Name', 'Age', 'Role'] self.indexes = ['Row1', 'Row2', 'Row3'] # Create a basic table model for testing self.table_model = ttk.TTkTableModelList( data=self.test_data, header=self.header, indexes=self.indexes ) def test_init_default(self): """Test default initialization""" widget = ttk.TTkTableWidget() assert widget is not None assert widget.model() is not None assert isinstance(widget.model(), ttk.TTkAbstractTableModel) # Default model should have 10x10 grid with empty strings assert widget.rowCount() == 10 assert widget.columnCount() == 10 def test_init_with_model(self): """Test initialization with a table model""" widget = ttk.TTkTableWidget(tableModel=self.table_model) assert widget.model() == self.table_model assert widget.rowCount() == 3 assert widget.columnCount() == 3 def test_init_with_separator_options(self): """Test initialization with separator visibility options""" widget = ttk.TTkTableWidget( vSeparator=False, hSeparator=False ) assert not widget.vSeparatorVisibility() assert not widget.hSeparatorVisibility() def test_init_with_header_options(self): """Test initialization with header visibility options""" widget = ttk.TTkTableWidget( vHeader=False, hHeader=False ) assert not widget.verticalHeader().isVisible() assert not widget.horizontalHeader().isVisible() def test_init_with_sorting_enabled(self): """Test initialization with sorting enabled""" widget = ttk.TTkTableWidget(sortingEnabled=True) assert widget.isSortingEnabled() def test_init_with_data_padding(self): """Test initialization with custom data padding""" widget = ttk.TTkTableWidget(dataPadding=3) # Data padding is internal, but we can verify the widget was created assert widget is not None def test_model_getter_setter(self): """Test model getter and setter""" widget = ttk.TTkTableWidget() original_model = widget.model() # Set new model widget.setModel(self.table_model) assert widget.model() == self.table_model assert widget.model() != original_model # Verify model data is accessible assert widget.rowCount() == 3 assert widget.columnCount() == 3 def test_row_column_count(self): """Test row and column count methods""" widget = ttk.TTkTableWidget(tableModel=self.table_model) assert widget.rowCount() == 3 assert widget.columnCount() == 3 def test_current_row_column(self): """Test current row and column methods""" widget = ttk.TTkTableWidget(tableModel=self.table_model) # Initially should be at (0, 0) or (-1, -1) if no selection current_row = widget.currentRow() current_col = widget.currentColumn() assert isinstance(current_row, int) assert isinstance(current_col, int) assert current_row >= -1 assert current_col >= -1 def test_header_views(self): """Test vertical and horizontal header views""" widget = ttk.TTkTableWidget(tableModel=self.table_model) v_header = widget.verticalHeader() h_header = widget.horizontalHeader() assert isinstance(v_header, ttk.TTkHeaderView) assert isinstance(h_header, ttk.TTkHeaderView) assert v_header.isVisible() assert h_header.isVisible() def test_separator_visibility(self): """Test separator visibility methods""" widget = ttk.TTkTableWidget() # Default should be visible assert widget.hSeparatorVisibility() assert widget.vSeparatorVisibility() # Test setters widget.setHSeparatorVisibility(False) widget.setVSeparatorVisibility(False) assert not widget.hSeparatorVisibility() assert not widget.vSeparatorVisibility() # Test setting back to visible widget.setHSeparatorVisibility(True) widget.setVSeparatorVisibility(True) assert widget.hSeparatorVisibility() assert widget.vSeparatorVisibility() def test_sorting_functionality(self): """Test sorting enabled/disabled and sort by column""" widget = ttk.TTkTableWidget(tableModel=self.table_model) # Initially sorting should be disabled assert not widget.isSortingEnabled() # Enable sorting widget.setSortingEnabled(True) assert widget.isSortingEnabled() # Test sorting by column widget.sortByColumn(0, ttk.TTkK.SortOrder.AscendingOrder) # Note: We can't easily verify the actual sorting without checking the model state # Disable sorting widget.setSortingEnabled(False) assert not widget.isSortingEnabled() def test_selection_methods(self): """Test selection-related methods""" widget = ttk.TTkTableWidget(tableModel=self.table_model) # Test clear selection widget.clearSelection() # Test select all widget.selectAll() # Test select row widget.selectRow(0) # Test select column widget.selectColumn(1) # Test set selection with position and size widget.setSelection((0, 0), (2, 2), ttk.TTkK.TTkItemSelectionModel.Select) # These methods should not raise exceptions # Actual selection state testing would require more complex setup def test_column_width_operations(self): """Test column width setting and resizing""" widget = ttk.TTkTableWidget(tableModel=self.table_model) # Test setting column width widget.setColumnWidth(0, 20) # Test resize column to contents widget.resizeColumnToContents(0) # Test resize all columns to contents widget.resizeColumnsToContents() # These methods should not raise exceptions def test_row_height_operations(self): """Test row height setting and resizing""" widget = ttk.TTkTableWidget(tableModel=self.table_model) # Test setting row height widget.setRowHeight(0, 3) # Test resize row to contents widget.resizeRowToContents(0) # Test resize all rows to contents widget.resizeRowsToContents() # These methods should not raise exceptions def test_clipboard_operations(self): """Test copy, cut, and paste operations""" widget = ttk.TTkTableWidget(tableModel=self.table_model) # These operations should not raise exceptions widget.copy() widget.cut() widget.paste() def test_undo_redo_operations(self): """Test undo and redo operations""" widget = ttk.TTkTableWidget(tableModel=self.table_model) # Initially, there should be no undo/redo available # But the methods should not raise exceptions widget.undo() widget.redo() def test_signals_exist(self): """Test that all expected signals exist""" widget = ttk.TTkTableWidget(tableModel=self.table_model) # Test that signals are accessible assert hasattr(widget, 'cellChanged') assert hasattr(widget, 'cellClicked') assert hasattr(widget, 'cellDoubleClicked') assert hasattr(widget, 'cellEntered') assert hasattr(widget, 'currentCellChanged') # Test that they are signal objects assert isinstance(widget.cellChanged, ttk.pyTTkSignal) assert isinstance(widget.cellClicked, ttk.pyTTkSignal) assert isinstance(widget.cellDoubleClicked, ttk.pyTTkSignal) assert isinstance(widget.cellEntered, ttk.pyTTkSignal) assert isinstance(widget.currentCellChanged, ttk.pyTTkSignal) def test_signal_connections(self): """Test signal connections""" widget = ttk.TTkTableWidget(tableModel=self.table_model) # Create mock slots cell_changed_slot = Mock() cell_clicked_slot = Mock() current_cell_changed_slot = Mock() @ttk.pyTTkSlot(int,int) def _mock_cell_changed_slot(row:int,col:int): cell_changed_slot(row, col) @ttk.pyTTkSlot(int,int) def _mock_cell_clicked_slot(row:int,col:int): cell_clicked_slot(row, col) @ttk.pyTTkSlot(int,int,int,int) def _mock_cell_cchanged_slot(a:int,b:int,c:int,d:int): current_cell_changed_slot(a,b,c,d) # Connect signals widget.cellChanged.connect(_mock_cell_changed_slot) widget.cellClicked.connect(_mock_cell_clicked_slot) widget.currentCellChanged.connect(_mock_cell_cchanged_slot) # Verify connections were made (signals should have connections) assert len(widget.cellChanged._connected_slots) > 0 assert len(widget.cellClicked._connected_slots) > 0 assert len(widget.currentCellChanged._connected_slots) > 0 def test_model_change_propagation(self): """Test that model changes are propagated to the widget""" widget = ttk.TTkTableWidget(tableModel=self.table_model) # Modify the model self.table_model.setData(0, 0, 'Modified') # The widget should be notified (through signal connections) # We can't easily test the actual update without a more complex setup assert widget.model().data(0, 0) == 'Modified' def test_empty_model_handling(self): """Test widget behavior with empty model""" empty_model = ttk.TTkTableModelList(data=[[]]) widget = ttk.TTkTableWidget(tableModel=empty_model) assert widget.rowCount() == 1 assert widget.columnCount() == 0 def test_large_model_handling(self): """Test widget behavior with large model""" large_data = [[f'Cell_{i}_{j}' for j in range(100)] for i in range(100)] large_model = ttk.TTkTableModelList(data=large_data) widget = ttk.TTkTableWidget(tableModel=large_model) assert widget.rowCount() == 100 assert widget.columnCount() == 100 def test_model_replacement(self): """Test replacing the model after widget creation""" widget = ttk.TTkTableWidget() original_row_count = widget.rowCount() original_col_count = widget.columnCount() # Replace with our test model widget.setModel(self.table_model) # Verify the change assert widget.rowCount() != original_row_count assert widget.columnCount() != original_col_count assert widget.rowCount() == 3 assert widget.columnCount() == 3 def test_widget_with_different_model_types(self): """Test widget with different types of table models""" # Test with TTkTableModelList list_model = ttk.TTkTableModelList(data=self.test_data) widget1 = ttk.TTkTableWidget(tableModel=list_model) assert widget1.rowCount() == 3 # Test with TTkTableModelCSV (using in-memory data) import io csv_data = io.StringIO("Name,Age\nAlice,25\nBob,30") csv_model = ttk.TTkTableModelCSV(fd=csv_data) widget2 = ttk.TTkTableWidget(tableModel=csv_model) assert widget2.rowCount() == 2 def test_header_visibility_changes(self): """Test changing header visibility after initialization""" widget = ttk.TTkTableWidget() # Initially headers should be visible assert widget.verticalHeader().isVisible() assert widget.horizontalHeader().isVisible() # Hide headers widget.verticalHeader().hide() widget.horizontalHeader().hide() assert not widget.verticalHeader().isVisible() assert not widget.horizontalHeader().isVisible() # Show headers again widget.verticalHeader().show() widget.horizontalHeader().show() assert widget.verticalHeader().isVisible() assert widget.horizontalHeader().isVisible() def test_focus_policy(self): """Test that the widget has proper focus policy""" widget = ttk.TTkTableWidget() # Should accept click and tab focus focus_policy = widget.focusPolicy() assert focus_policy & ttk.TTkK.ClickFocus assert focus_policy & ttk.TTkK.TabFocus def test_minimum_size(self): """Test minimum size constraints""" widget = ttk.TTkTableWidget() # Should have minimum height of 1 min_size = widget.minimumSize() assert min_size[1] >= 1 # height should be at least 1 def test_style_properties(self): """Test that the widget has proper style properties""" widget = ttk.TTkTableWidget() # Should have class style defined assert hasattr(ttk.TTkTableWidget, 'classStyle') assert isinstance(ttk.TTkTableWidget.classStyle, dict) assert 'default' in ttk.TTkTableWidget.classStyle def test_boundary_conditions(self): """Test boundary conditions and edge cases""" widget = ttk.TTkTableWidget(tableModel=self.table_model) # Test with invalid row/column indices (should not crash) try: widget.selectRow(-1) widget.selectRow(1000) widget.selectColumn(-1) widget.selectColumn(1000) widget.setColumnWidth(-1, 10) widget.setColumnWidth(1000, 10) widget.setRowHeight(-1, 2) widget.setRowHeight(1000, 2) except Exception as e: # Some operations might raise exceptions, but they shouldn't crash pass def test_integration_with_scroll_area(self): """Test that the widget works as expected within a scroll area (via TTkTable)""" table = ttk.TTkTable(tableModel=self.table_model) # TTkTable should forward methods to the internal TTkTableWidget assert table.rowCount() == 3 assert table.columnCount() == 3 assert table.model() == self.table_model def test_zero_rows_model(self): """Test widget behavior with model that has zero rows""" # Create model with zero rows but some columns empty_rows_model = ttk.TTkTableModelList( data=[], header=['Col1', 'Col2', 'Col3'] ) widget = ttk.TTkTableWidget(tableModel=empty_rows_model) assert widget.rowCount() == 0 assert widget.columnCount() == 0 assert widget.currentRow() == 0 # No valid current row assert widget.currentColumn() == 0 # No valid current column # Selection operations should handle empty rows gracefully widget.clearSelection() widget.selectAll() # Should not crash with no rows # Column operations should still work widget.setColumnWidth(0, 50) widget.resizeColumnsToContents() # Row operations should handle empty case widget.resizeRowsToContents() # Should not crash with no rows def test_zero_columns_model(self): """Test widget behavior with model that has zero columns""" # Create model with zero columns but some rows empty_cols_model = ttk.TTkTableModelList( data=[[], [], []], # 3 empty rows header=[] ) widget = ttk.TTkTableWidget(tableModel=empty_cols_model) assert widget.rowCount() == 3 assert widget.columnCount() == 0 assert widget.currentRow() == 0 # No valid current row assert widget.currentColumn() == 0 # No valid current column # Selection operations should handle empty columns gracefully widget.clearSelection() widget.selectAll() # Should not crash with no columns # Row operations should still work widget.setRowHeight(0, 2) widget.resizeRowsToContents() # Column operations should handle empty case widget.resizeColumnsToContents() # Should not crash with no columns def test_completely_empty_model(self): """Test widget behavior with model that has zero rows and zero columns""" # Create completely empty model empty_model = ttk.TTkTableModelList( data=[], header=[] ) widget = ttk.TTkTableWidget(tableModel=empty_model) assert widget.rowCount() == 0 assert widget.columnCount() == 0 assert widget.currentRow() == 0 assert widget.currentColumn() == 0 # All operations should handle completely empty case gracefully widget.clearSelection() widget.selectAll() widget.resizeColumnsToContents() widget.resizeRowsToContents() # Clipboard operations with empty model widget.copy() # Should not crash widget.cut() # Should not crash widget.paste() # Should not crash # Undo/redo with empty model widget.undo() widget.redo() def test_single_cell_model(self): """Test widget behavior with model that has exactly one cell""" single_cell_model = ttk.TTkTableModelList( data=[['SingleCell']], header=['OnlyColumn'] ) widget = ttk.TTkTableWidget(tableModel=single_cell_model) assert widget.rowCount() == 1 assert widget.columnCount() == 1 # Selection operations widget.selectAll() widget.selectRow(0) widget.selectColumn(0) widget.setSelection((0, 0), (1, 1), ttk.TTkK.TTkItemSelectionModel.Select) # Resize operations widget.setColumnWidth(0, 20) widget.setRowHeight(0, 3) widget.resizeColumnToContents(0) widget.resizeRowToContents(0) def test_model_transitions(self): """Test transitioning between different model sizes""" widget = ttk.TTkTableWidget(tableModel=self.table_model) # Start with normal model assert widget.rowCount() == 3 assert widget.columnCount() == 3 # Transition to empty rows empty_rows_model = ttk.TTkTableModelList(data=[], header=['A', 'B']) widget.setModel(empty_rows_model) assert widget.rowCount() == 0 assert widget.columnCount() == 0 # Transition to empty columns empty_cols_model = ttk.TTkTableModelList(data=[[], []], header=[]) widget.setModel(empty_cols_model) assert widget.rowCount() == 2 assert widget.columnCount() == 0 # Transition to completely empty empty_model = ttk.TTkTableModelList(data=[], header=[]) widget.setModel(empty_model) assert widget.rowCount() == 0 assert widget.columnCount() == 0 # Transition back to normal model widget.setModel(self.table_model) assert widget.rowCount() == 3 assert widget.columnCount() == 3 def test_boundary_operations_on_empty_models(self): """Test boundary operations on various empty model configurations""" # Test with zero rows model zero_rows_model = ttk.TTkTableModelList(data=[], header=['A', 'B']) widget = ttk.TTkTableWidget(tableModel=zero_rows_model) # These should not crash even with no rows try: widget.selectRow(0) # Invalid row widget.unselectRow(0) # Invalid row widget.setRowHeight(0, 2) # Invalid row widget.resizeRowToContents(0) # Invalid row except (IndexError, ValueError): pass # Expected for invalid indices # Test with zero columns model zero_cols_model = ttk.TTkTableModelList(data=[[], []], header=[]) widget.setModel(zero_cols_model) # These should not crash even with no columns try: widget.selectColumn(0) # Invalid column widget.unselectColumn(0) # Invalid column widget.setColumnWidth(0, 20) # Invalid column widget.resizeColumnToContents(0) # Invalid column except (IndexError, ValueError): pass # Expected for invalid indices def test_empty_model_sorting(self): """Test sorting operations on empty models""" # Test sorting with zero rows zero_rows_model = ttk.TTkTableModelList(data=[], header=['Name', 'Age']) widget = ttk.TTkTableWidget(tableModel=zero_rows_model, sortingEnabled=True) assert widget.isSortingEnabled() # Sorting empty data should not crash widget.sortByColumn(0, ttk.TTkK.SortOrder.AscendingOrder) widget.sortByColumn(1, ttk.TTkK.SortOrder.DescendingOrder) # Test sorting with zero columns zero_cols_model = ttk.TTkTableModelList(data=[[], []], header=[]) widget.setModel(zero_cols_model) # Sorting with no columns should handle gracefully try: widget.sortByColumn(0, ttk.TTkK.SortOrder.AscendingOrder) # Invalid column except (IndexError, ValueError): pass # Expected for invalid column index def test_empty_model_signals(self): """Test that signals work correctly with empty models""" empty_model = ttk.TTkTableModelList(data=[], header=[]) widget = ttk.TTkTableWidget(tableModel=empty_model) # Connect signals to mock slots cell_changed_calls = [] cell_clicked_calls = [] @ttk.pyTTkSlot(int, int) def mock_cell_changed(row, col): cell_changed_calls.append((row, col)) @ttk.pyTTkSlot(int, int) def mock_cell_clicked(row, col): cell_clicked_calls.append((row, col)) widget.cellChanged.connect(mock_cell_changed) widget.cellClicked.connect(mock_cell_clicked) # Signals should be connected even with empty model assert len(widget.cellChanged._connected_slots) > 0 assert len(widget.cellClicked._connected_slots) > 0 # No signals should be emitted for empty model operations widget.clearSelection() widget.selectAll() # Should have no signal calls since there are no cells assert len(cell_changed_calls) == 0 assert len(cell_clicked_calls) == 0 def test_integration_with_full_application(): """Integration test - verify TTkTableWidget works in a full application context""" # Create a more complex scenario data = [ ['Product A', 100, 29.99], ['Product B', 50, 19.99], ['Product C', 75, 39.99] ] headers = ['Product', 'Stock', 'Price'] model = ttk.TTkTableModelList(data=data, header=headers) widget = ttk.TTkTableWidget( tableModel=model, sortingEnabled=True, vSeparator=True, hSeparator=True ) # Test various operations in sequence widget.resizeColumnsToContents() widget.setSortingEnabled(True) widget.sortByColumn(2, ttk.TTkK.SortOrder.DescendingOrder) # Sort by price descending widget.selectRow(0) widget.copy() # Verify the widget still functions correctly assert widget.rowCount() == 3 assert widget.columnCount() == 3 assert widget.isSortingEnabled() if __name__ == '__main__': pytest.main([__file__])