|
|
|
|
#!/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
|