You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

655 lines
22 KiB

#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2025 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
#
# 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
import io
from unittest.mock import Mock, MagicMock, patch
sys.path.append(os.path.join(sys.path[0],'../../../libs/pyTermTk'))
import TermTk as ttk
from TermTk.TTkWidgets.TTkModelView.tablewidget import _DragPosType, _ClipboardTable, _SelectionProxy, TTkHeaderView
class TestTTkHeaderView:
"""Test cases for TTkHeaderView class"""
def test_init_default(self):
"""Test default initialization"""
header = TTkHeaderView()
assert header.isVisible() == True
assert hasattr(header, 'visibilityUpdated')
def test_init_with_visibility(self):
"""Test initialization with specific visibility"""
header_visible = TTkHeaderView(visible=True)
header_hidden = TTkHeaderView(visible=False)
assert header_visible.isVisible() == True
assert header_hidden.isVisible() == False
def test_set_visible(self):
"""Test setVisible method"""
header = TTkHeaderView()
header.setVisible(False)
assert header.isVisible() == False
header.setVisible(True)
assert header.isVisible() == True
def test_show_hide(self):
"""Test show and hide methods"""
header = TTkHeaderView(visible=False)
header.show()
assert header.isVisible() == True
header.hide()
assert header.isVisible() == False
def test_visibility_signal(self):
"""Test visibility signal emission"""
header = TTkHeaderView()
signal_calls = []
@ttk.pyTTkSlot(bool)
def mock_slot(visible):
signal_calls.append(visible)
header.visibilityUpdated.connect(mock_slot)
header.hide()
assert len(signal_calls) == 1
assert signal_calls[0] == False
header.show()
assert len(signal_calls) == 2
assert signal_calls[1] == True
class TestClipboardTable:
"""Test cases for _ClipboardTable class"""
def test_init_empty(self):
"""Test initialization with empty data"""
clipboard = _ClipboardTable([])
assert clipboard.data() == []
assert isinstance(clipboard, ttk.TTkString)
def test_init_with_data(self):
"""Test initialization with data"""
data = [
[(0, 0, 'A1'), (0, 1, 'B1')],
[(1, 0, 'A2'), (1, 1, 'B2')]
]
clipboard = _ClipboardTable(data)
assert clipboard.data() == data
assert isinstance(clipboard, ttk.TTkString)
assert len(str(clipboard)) > 0
def test_string_conversion(self):
"""Test TTkString conversion"""
data = [
[(0, 0, 'Cell1'), (0, 1, 'Cell2')],
[(1, 0, 'Cell3'), (1, 1, 'Cell4')]
]
clipboard = _ClipboardTable(data)
string_repr = str(clipboard)
# Should contain the cell data in some formatted way
assert 'Cell1' in string_repr or str(clipboard).find('Cell') >= 0
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']
class MockTableModel(ttk.TTkTableModelList):
def flags(self, row, col):
return super().flags(row, col)
# Create a basic table model for testing
self.table_model = MockTableModel(
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_parameters(self):
"""Test initialization with all parameters"""
widget = ttk.TTkTableWidget(
tableModel=self.table_model,
vSeparator=False,
hSeparator=False,
vHeader=False,
hHeader=False,
sortingEnabled=True,
dataPadding=3
)
assert widget.model() == self.table_model
assert not widget.vSeparatorVisibility()
assert not widget.hSeparatorVisibility()
assert not widget.verticalHeader().isVisible()
assert not widget.horizontalHeader().isVisible()
assert widget.isSortingEnabled()
assert widget._dataPadding == 3
def test_header_views(self):
"""Test header view functionality"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
v_header = widget.verticalHeader()
h_header = widget.horizontalHeader()
assert isinstance(v_header, TTkHeaderView)
assert isinstance(h_header, TTkHeaderView)
assert v_header.isVisible()
assert h_header.isVisible()
# Test header visibility changes trigger layout refresh
with patch.object(widget, '_headerVisibilityChanged') as mock_refresh:
v_header.hide()
# Signal should have been emitted and connected
assert not v_header.isVisible()
def test_model_management(self):
"""Test model getter/setter and related functionality"""
widget = ttk.TTkTableWidget()
original_model = widget.model()
# Test model change
widget.setModel(self.table_model)
assert widget.model() == self.table_model
assert widget.model() != original_model
# Test that model signals are connected
assert widget.model().dataChanged._connected_slots
assert widget.model().modelChanged._connected_slots
# Test model change triggers layout refresh
with patch.object(widget, '_refreshLayout') as mock_refresh:
new_model = ttk.TTkTableModelList(data=[['test']])
widget.setModel(new_model)
# _refreshLayout should be called when model changes
# Note: This depends on the modelChanged signal being emitted
def test_selection_proxy_integration(self):
"""Test selection proxy integration"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
proxy = widget._select_proxy
assert isinstance(proxy, _SelectionProxy)
assert proxy._rows == 3
assert proxy._cols == 3
# Test selection operations update proxy
widget.selectRow(1)
assert proxy.isRowSelected(1)
widget.selectColumn(2)
assert proxy.isColSelected(2)
widget.clearSelection()
assert not proxy.isRowSelected(1)
assert not proxy.isColSelected(2)
def test_current_cell_management(self):
"""Test current cell position management"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Initially no current position
assert widget._currentPos is None
assert widget.currentRow() == 0 # Default
assert widget.currentColumn() == 0 # Default
# Test setting current cell
widget._setCurrentCell(1, 2)
assert widget._currentPos == (1, 2)
assert widget.currentRow() == 1
assert widget.currentColumn() == 2
# # Test movement
# widget._moveCurrentCell(row=1, col=1)
# assert widget._currentPos == (2, 3) # Should be (1+1, 2+1) if movement works
# Test boundary handling
widget._moveCurrentCell(row=5, col=5) # Should clamp to table bounds
assert widget._currentPos[0] < widget.rowCount()
assert widget._currentPos[1] < widget.columnCount()
def test_snapshot_system(self):
"""Test undo/redo snapshot system"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Initially no snapshots
assert len(widget._snapshot) == 0
assert widget._snapshotId == 0
assert not widget.isUndoAvailable()
assert not widget.isRedoAvailable()
# Create a snapshot by modifying data
original_value = widget.model().data(0, 0)
widget._tableModel_setData([(0, 0, 'NewValue')])
# Should have a snapshot now
assert widget.isUndoAvailable()
assert not widget.isRedoAvailable()
assert widget.model().data(0, 0) == 'NewValue'
# Test undo
widget.undo()
assert widget.model().data(0, 0) == original_value
assert not widget.isUndoAvailable()
assert widget.isRedoAvailable()
# Test redo
widget.redo()
assert widget.model().data(0, 0) == 'NewValue'
assert widget.isUndoAvailable()
assert not widget.isRedoAvailable()
def test_clipboard_operations(self):
"""Test clipboard operations"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Select some cells
widget.setSelection((0, 0), (2, 2), ttk.TTkK.TTkItemSelectionModel.Select)
# Test copy
widget.copy()
# Should not raise exception
# Test cut
original_values = [
[widget.model().data(r, c) for c in range(2)]
for r in range(2)
]
widget.cut()
# Values should be cleared after cut
for r in range(2):
for c in range(2):
if widget._select_proxy.isCellSelected(c, r):
assert widget.model().data(r, c) == ''
# Test paste
widget.paste()
# Should restore some values or handle gracefully
def test_sorting_functionality(self):
"""Test sorting functionality"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Initially disabled
assert not widget.isSortingEnabled()
# Enable sorting
widget.setSortingEnabled(True)
assert widget.isSortingEnabled()
# Test sorting by column
original_first_name = widget.model().data(0, 0)
widget.sortByColumn(0, ttk.TTkK.SortOrder.AscendingOrder)
# Can't easily test actual sorting without knowing internal implementation
# but should not raise exception
# Test descending sort
widget.sortByColumn(0, ttk.TTkK.SortOrder.DescendingOrder)
def test_resize_operations(self):
"""Test column and row resize operations"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Test individual operations
widget.setColumnWidth(0, 20)
widget.setRowHeight(0, 3)
# Test resize to contents
widget.resizeColumnToContents(0)
widget.resizeRowToContents(0)
# Test resize all to contents
widget.resizeColumnsToContents()
widget.resizeRowsToContents()
# These should not raise exceptions
def test_view_area_calculation(self):
"""Test view area size calculation"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# With headers
width, height = widget.viewFullAreaSize()
assert width > 0
assert height > 0
# Without headers
widget.verticalHeader().hide()
widget.horizontalHeader().hide()
width_no_headers, height_no_headers = widget.viewFullAreaSize()
assert width_no_headers <= width
assert height_no_headers <= height
def test_find_cell_functionality(self):
"""Test _findCell coordinate conversion"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Test with headers
row, col = widget._findCell(10, 5, headers=True)
assert isinstance(row, int)
assert isinstance(col, int)
assert row >= -1 # -1 for header, >= 0 for cells
assert col >= -1 # -1 for header, >= 0 for cells
# Test without headers
row, col = widget._findCell(10, 5, headers=False)
assert isinstance(row, int)
assert isinstance(col, int)
assert row >= 0
assert col >= 0
def test_mouse_events(self):
"""Test mouse event handling"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Mock mouse events
press_event = Mock()
press_event.x = 15
press_event.y = 2
press_event.mod = ttk.TTkK.NoModifier
move_event = Mock()
move_event.x = 20
move_event.y = 3
move_event.mod = ttk.TTkK.NoModifier
release_event = Mock()
release_event.x = 20
release_event.y = 3
release_event.mod = ttk.TTkK.NoModifier
double_click_event = Mock()
double_click_event.x = 15
double_click_event.y = 2
double_click_event.mod = ttk.TTkK.NoModifier
# Test event handling (should not crash)
with patch.object(widget, '_findCell', return_value=(1, 1)):
assert widget.mousePressEvent(press_event) == True
assert widget.mouseMoveEvent(move_event) == True
assert widget.mouseReleaseEvent(release_event) == True
assert widget.mouseDoubleClickEvent(double_click_event) == True
def test_keyboard_events(self):
"""Test keyboard event handling"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Set initial position
widget._setCurrentCell(1, 1)
# Mock keyboard events
key_events = [
Mock(key=ttk.TTkK.Key_Up, mod=ttk.TTkK.NoModifier),
Mock(key=ttk.TTkK.Key_Down, mod=ttk.TTkK.NoModifier),
Mock(key=ttk.TTkK.Key_Left, mod=ttk.TTkK.NoModifier),
Mock(key=ttk.TTkK.Key_Right, mod=ttk.TTkK.NoModifier),
Mock(key=ttk.TTkK.Key_Tab, mod=ttk.TTkK.NoModifier),
Mock(key=ttk.TTkK.Key_Backtab, mod=ttk.TTkK.NoModifier),
Mock(key=ttk.TTkK.Key_Enter, mod=ttk.TTkK.NoModifier),
Mock(key=ttk.TTkK.Key_Space, mod=ttk.TTkK.NoModifier),
]
for event in key_events:
# Should handle keyboard navigation
result = widget.keyEvent(event)
# Result depends on implementation, but should not crash
def test_signal_emissions(self):
"""Test signal emissions"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Connect signals to mock slots
cell_changed_calls = []
cell_clicked_calls = []
cell_entered_calls = []
current_cell_changed_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))
@ttk.pyTTkSlot(int, int)
def mock_cell_entered(row, col):
cell_entered_calls.append((row, col))
@ttk.pyTTkSlot(int, int, int, int)
def mock_current_cell_changed(curr_row, curr_col, prev_row, prev_col):
current_cell_changed_calls.append((curr_row, curr_col, prev_row, prev_col))
widget.cellChanged.connect(mock_cell_changed)
widget.cellClicked.connect(mock_cell_clicked)
widget.cellEntered.connect(mock_cell_entered)
widget.currentCellChanged.connect(mock_current_cell_changed)
# Trigger events that should emit signals
widget._cellChanged.emit(1, 2)
widget._cellClicked.emit(0, 1)
widget._cellEntered.emit(2, 0)
widget._currentCellChanged.emit(1, 1, 0, 0)
# Verify signals were received
assert len(cell_changed_calls) == 1
assert cell_changed_calls[0] == (1, 2)
assert len(cell_clicked_calls) == 1
assert cell_clicked_calls[0] == (0, 1)
assert len(cell_entered_calls) == 1
assert cell_entered_calls[0] == (2, 0)
assert len(current_cell_changed_calls) == 1
assert current_cell_changed_calls[0] == (1, 1, 0, 0)
def test_edge_cases(self):
"""Test edge cases and boundary conditions"""
# Empty model
empty_model = ttk.TTkTableModelList(data=[], header=[])
widget = ttk.TTkTableWidget(tableModel=empty_model)
assert widget.rowCount() == 0
assert widget.columnCount() == 0
# Operations should handle empty model gracefully
widget.selectAll()
widget.clearSelection()
widget.resizeColumnsToContents()
widget.resizeRowsToContents()
widget.copy()
widget.cut()
widget.paste()
# Single cell model
single_model = ttk.TTkTableModelList(data=[['Cell']], header=['Col'])
widget.setModel(single_model)
assert widget.rowCount() == 1
assert widget.columnCount() == 1
widget.selectAll()
widget._setCurrentCell(0, 0)
# Large model stress test
large_data = [[f'Cell_{i}_{j}' for j in range(50)] for i in range(50)]
large_model = ttk.TTkTableModelList(data=large_data)
widget.setModel(large_model)
assert widget.rowCount() == 50
assert widget.columnCount() == 50
# Should handle large models without issues
widget.selectRow(25)
widget.selectColumn(25)
def test_focus_and_events(self):
"""Test focus handling and event processing"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Test focus policy
focus_policy = widget.focusPolicy()
assert focus_policy & ttk.TTkK.ClickFocus
assert focus_policy & ttk.TTkK.TabFocus
# Test focus events
widget.focusOutEvent() # Should not crash
# Test leave event
leave_event = Mock()
leave_event.x = 100
leave_event.y = 100
result = widget.leaveEvent(leave_event)
# Should handle gracefully
def test_internal_state_consistency(self):
"""Test internal state consistency"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Test that internal positions are consistent
widget._setCurrentCell(1, 2)
assert widget._currentPos == (1, 2)
assert widget.currentRow() == 1
assert widget.currentColumn() == 2
# Test drag position initialization
assert widget._dragPos is None
assert widget._hoverPos is None
assert widget._hSeparatorSelected is None
assert widget._vSeparatorSelected is None
# Test selection proxy consistency
proxy = widget._select_proxy
assert proxy._rows == widget.rowCount()
assert proxy._cols == widget.columnCount()
def test_style_and_appearance(self):
"""Test style properties"""
widget = ttk.TTkTableWidget()
# Test class style exists
assert hasattr(ttk.TTkTableWidget, 'classStyle')
assert isinstance(ttk.TTkTableWidget.classStyle, dict)
assert 'default' in ttk.TTkTableWidget.classStyle
assert 'disabled' in ttk.TTkTableWidget.classStyle
# Test style properties contain expected keys
default_style = ttk.TTkTableWidget.classStyle['default']
expected_keys = ['color', 'lineColor', 'headerColor', 'hoverColor',
'currentColor', 'selectedColor', 'separatorColor']
for key in expected_keys:
assert key in default_style
assert isinstance(default_style[key], ttk.TTkColor)
def test_paste_event_handling(self):
"""Test paste event with different data types"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
widget._setCurrentCell(0, 0)
# Test paste with TTkString
text_data = ttk.TTkString("Hello\tWorld\nFoo\tBar")
result = widget.pasteEvent(text_data)
assert result == True
# Test paste with _ClipboardTable
clipboard_data = [
[(0, 0, 'A1'), (0, 1, 'B1')],
[(1, 0, 'A2'), (1, 1, 'B2')]
]
clipboard_table = _ClipboardTable(clipboard_data)
result = widget.pasteEvent(clipboard_table)
assert result == True
# Test paste with unsupported data
result = widget.pasteEvent("regular string")
assert result == True # Should handle gracefully
def test_model_flags_integration(self):
"""Test integration with model flags"""
widget = ttk.TTkTableWidget(tableModel=self.table_model)
# Mock model flags
def mock_flags(row, col):
if row == 1 and col == 1:
return ttk.TTkK.ItemFlag.NoItemFlags # Not selectable
return ttk.TTkK.ItemFlag.ItemIsSelectable | ttk.TTkK.ItemFlag.ItemIsEnabled
original_flags = widget._tableModel.flags
widget._tableModel.flags = mock_flags
try:
# Test selection respects flags
widget._select_proxy.updateModel(
cols=widget.columnCount(),
rows=widget.rowCount(),
flags=widget._tableModel.flags
)
widget.selectAll()
# Cell (1,1) should not be selected due to flags
assert not widget._select_proxy.isCellSelected(1, 1)
# Other cells should be selected
assert widget._select_proxy.isCellSelected(0, 0)
assert widget._select_proxy.isCellSelected(2, 2)
finally:
widget._tableModel.flags = original_flags