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.
531 lines
19 KiB
531 lines
19 KiB
from unittest.mock import MagicMock |
|
from TermTk.TTkCore.constant import TTkK |
|
from TermTk.TTkCore.string import TTkString |
|
from TermTk.TTkWidgets.TTkModelView.treewidget import TTkTreeWidget |
|
from TermTk.TTkWidgets.TTkModelView.treewidgetitem import TTkTreeWidgetItem |
|
|
|
|
|
class TestTTkTreeWidgetEmpty: |
|
"""Test TTkTreeWidget with empty tree to catch crashes""" |
|
|
|
def test_empty_tree_creation(self): |
|
"""Test creating an empty tree widget""" |
|
tree = TTkTreeWidget() |
|
assert tree is not None |
|
assert tree._rootItem is not None |
|
|
|
def test_empty_tree_view_area_size(self): |
|
"""Test viewFullAreaSize with empty tree""" |
|
tree = TTkTreeWidget() |
|
w, h = tree.viewFullAreaSize() |
|
assert w == 0 |
|
assert h == 1 # Header row |
|
|
|
def test_empty_tree_clear(self): |
|
"""Test clear on empty tree""" |
|
tree = TTkTreeWidget() |
|
tree.clear() # Should not crash |
|
assert tree._rootItem.size() == 0 |
|
|
|
def test_empty_tree_paint_event(self): |
|
"""Test paintEvent with empty tree""" |
|
tree = TTkTreeWidget() |
|
canvas = MagicMock() |
|
tree.paintEvent(canvas) # Should not crash |
|
|
|
def test_empty_tree_selected_items(self): |
|
"""Test selectedItems with empty tree""" |
|
tree = TTkTreeWidget() |
|
selected = tree.selectedItems() |
|
assert selected == [] |
|
|
|
def test_empty_tree_mouse_press(self): |
|
"""Test mouse press on empty tree""" |
|
tree = TTkTreeWidget() |
|
evt = MagicMock() |
|
evt.x = 5 |
|
evt.y = 5 |
|
evt.mod = 0 |
|
tree.mousePressEvent(evt) # Should not crash |
|
|
|
def test_empty_tree_resize_column(self): |
|
"""Test resizeColumnToContents with empty tree""" |
|
tree = TTkTreeWidget(header=[TTkString("Col1")]) |
|
tree.resizeColumnToContents(0) # Should not crash |
|
|
|
|
|
class TestTTkTreeWidgetBasic: |
|
"""Test basic TTkTreeWidget functionality""" |
|
|
|
def test_tree_with_header(self): |
|
"""Test tree creation with headers""" |
|
tree = TTkTreeWidget(header=[TTkString("A"), TTkString("B")]) |
|
assert len(tree._header) == 2 |
|
assert len(tree._columnsPos) == 2 |
|
|
|
def test_add_top_level_item(self): |
|
"""Test adding a top-level item""" |
|
tree = TTkTreeWidget() |
|
item = TTkTreeWidgetItem(["Item 1"]) |
|
tree.addTopLevelItem(item) |
|
assert tree._rootItem.size() == 1 |
|
assert tree.topLevelItem(0) == item |
|
|
|
def test_add_multiple_top_level_items(self): |
|
"""Test adding multiple top-level items""" |
|
tree = TTkTreeWidget() |
|
items = [TTkTreeWidgetItem([f"Item {i}"]) for i in range(3)] |
|
tree.addTopLevelItems(items) |
|
assert tree._rootItem.size() == 3 |
|
|
|
def test_take_top_level_item(self): |
|
"""Test removing top-level item""" |
|
tree = TTkTreeWidget() |
|
item = TTkTreeWidgetItem(["Item 1"]) |
|
tree.addTopLevelItem(item) |
|
taken = tree.takeTopLevelItem(0) |
|
assert taken == item |
|
assert tree._rootItem.size() == 0 |
|
|
|
def test_parent_link_set_and_cleared_for_top_level_item(self): |
|
"""Test top-level item parent link is set on add and cleared on remove""" |
|
tree = TTkTreeWidget() |
|
item = TTkTreeWidgetItem(["Item 1"]) |
|
|
|
tree.addTopLevelItem(item) |
|
assert item._parent == tree._rootItem |
|
|
|
taken = tree.takeTopLevelItem(0) |
|
assert taken == item |
|
assert item._parent is None |
|
|
|
def test_index_of_top_level_item(self): |
|
"""Test finding index of top-level item""" |
|
tree = TTkTreeWidget() |
|
items = [TTkTreeWidgetItem([f"Item {i}"]) for i in range(3)] |
|
tree.addTopLevelItems(items) |
|
assert tree.indexOfTopLevelItem(items[1]) == 1 |
|
|
|
def test_invisible_root_item(self): |
|
"""Test getting invisible root item""" |
|
tree = TTkTreeWidget() |
|
root = tree.invisibleRootItem() |
|
assert root == tree._rootItem |
|
|
|
|
|
class TestTTkTreeWidgetSelection: |
|
"""Test selection modes""" |
|
|
|
def test_single_selection_mode(self): |
|
"""Test single selection mode""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.SingleSelection) |
|
assert tree.selectionMode() == TTkK.SelectionMode.SingleSelection |
|
|
|
def test_multi_selection_mode(self): |
|
"""Test multi selection mode""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
assert tree.selectionMode() == TTkK.SelectionMode.MultiSelection |
|
|
|
def test_set_selection_mode(self): |
|
"""Test changing selection mode""" |
|
tree = TTkTreeWidget() |
|
tree.setSelectionMode(TTkK.SelectionMode.MultiSelection) |
|
assert tree.selectionMode() == TTkK.SelectionMode.MultiSelection |
|
|
|
def test_set_selection_mode_to_no_selection_clears(self): |
|
"""Test switching to NoSelection clears all selected items""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
item1 = TTkTreeWidgetItem(["Item 1"]) |
|
item2 = TTkTreeWidgetItem(["Item 2"]) |
|
tree.addTopLevelItems([item1, item2]) |
|
|
|
tree.selectItem(item1) |
|
tree.selectItem(item2) |
|
assert len(tree.selectedItems()) == 2 |
|
|
|
tree.setSelectionMode(TTkK.SelectionMode.NoSelection) |
|
assert tree.selectedItems() == [] |
|
|
|
def test_set_selection_mode_single_to_multi_preserves(self): |
|
"""Test switching from Single to Multi preserves the selected item""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.SingleSelection) |
|
item = TTkTreeWidgetItem(["Item"]) |
|
tree.addTopLevelItem(item) |
|
|
|
tree.selectItem(item) |
|
assert tree.selectedItems() == [item] |
|
|
|
tree.setSelectionMode(TTkK.SelectionMode.MultiSelection) |
|
assert tree.selectedItems() == [item] |
|
|
|
def test_set_selection_mode_multi_to_single_one_item(self): |
|
"""Test switching Multi→Single with exactly one item keeps it""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
item = TTkTreeWidgetItem(["Item"]) |
|
tree.addTopLevelItem(item) |
|
|
|
tree.selectItem(item) |
|
tree.setSelectionMode(TTkK.SelectionMode.SingleSelection) |
|
assert tree.selectedItems() == [item] |
|
|
|
def test_set_selection_mode_multi_to_single_empty(self): |
|
"""Test switching Multi→Single with no selection stays empty""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
tree.addTopLevelItem(TTkTreeWidgetItem(["Item"])) |
|
|
|
tree.setSelectionMode(TTkK.SelectionMode.SingleSelection) |
|
assert tree.selectedItems() == [] |
|
|
|
def test_set_selection_mode_no_to_single(self): |
|
"""Test switching from NoSelection to Single allows selection again""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.NoSelection) |
|
item = TTkTreeWidgetItem(["Item"]) |
|
tree.addTopLevelItem(item) |
|
|
|
tree.selectItem(item) |
|
assert tree.selectedItems() == [] |
|
|
|
tree.setSelectionMode(TTkK.SelectionMode.SingleSelection) |
|
tree.selectItem(item) |
|
assert tree.selectedItems() == [item] |
|
|
|
def test_set_selection_mode_trims_keeps_first(self): |
|
"""Test switching Multi→Single keeps the first selected item""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
items = [TTkTreeWidgetItem([f"Item {i}"]) for i in range(5)] |
|
tree.addTopLevelItems(items) |
|
|
|
for item in items: |
|
tree.selectItem(item) |
|
assert len(tree.selectedItems()) == 5 |
|
|
|
tree.setSelectionMode(TTkK.SelectionMode.SingleSelection) |
|
assert tree.selectedItems() == [items[0]] |
|
|
|
def test_programmatic_single_selection(self): |
|
"""Test select/deselect API in single selection mode""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.SingleSelection) |
|
item1 = TTkTreeWidgetItem(["Item 1"]) |
|
item2 = TTkTreeWidgetItem(["Item 2"]) |
|
tree.addTopLevelItems([item1, item2]) |
|
|
|
tree.selectItem(item1) |
|
assert tree.selectedItems() == [item1] |
|
|
|
tree.selectItem(item2) |
|
assert tree.selectedItems() == [item2] |
|
|
|
tree.deselectItem(item2) |
|
assert tree.selectedItems() == [] |
|
|
|
def test_programmatic_multi_selection(self): |
|
"""Test select/deselect API in multi selection mode""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
item1 = TTkTreeWidgetItem(["Item 1"]) |
|
item2 = TTkTreeWidgetItem(["Item 2"]) |
|
tree.addTopLevelItems([item1, item2]) |
|
|
|
tree.selectItem(item1) |
|
tree.selectItem(item2) |
|
assert tree.selectedItems() == [item1, item2] |
|
|
|
tree.deselectItem(item1) |
|
assert tree.selectedItems() == [item2] |
|
|
|
tree.clearSelection() |
|
assert tree.selectedItems() == [] |
|
|
|
def test_programmatic_no_selection_mode(self): |
|
"""Test selection API is ignored in no selection mode""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.NoSelection) |
|
item = TTkTreeWidgetItem(["Item 1"]) |
|
|
|
tree.selectItem(item) |
|
tree.setCurrentItem(item) |
|
assert tree.selectedItems() == [] |
|
|
|
def test_set_selection_mode_trims_selected_items(self): |
|
"""Test selection gets trimmed when switching to single mode""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
item1 = TTkTreeWidgetItem(["Item 1"]) |
|
item2 = TTkTreeWidgetItem(["Item 2"]) |
|
tree.addTopLevelItems([item1, item2]) |
|
|
|
tree.selectItem(item1) |
|
tree.selectItem(item2) |
|
tree.setSelectionMode(TTkK.SelectionMode.SingleSelection) |
|
|
|
assert tree.selectedItems() == [item1] |
|
|
|
def test_programmatic_selection_ignores_detached_items(self): |
|
"""Test selection API ignores items not belonging to this tree""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
in_tree = TTkTreeWidgetItem(["In tree"]) |
|
detached = TTkTreeWidgetItem(["Detached"]) |
|
tree.addTopLevelItem(in_tree) |
|
|
|
tree.selectItem(detached) |
|
tree.setCurrentItem(detached) |
|
tree.deselectItem(detached) |
|
|
|
assert tree.selectedItems() == [] |
|
|
|
def test_multilevel_single_selection(self): |
|
"""Test single selection mode with nested items""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.SingleSelection) |
|
parent = TTkTreeWidgetItem(["Parent"]) |
|
child1 = TTkTreeWidgetItem(["Child 1"]) |
|
child2 = TTkTreeWidgetItem(["Child 2"]) |
|
grandchild = TTkTreeWidgetItem(["Grandchild"]) |
|
|
|
parent.addChildren([child1, child2]) |
|
child1.addChild(grandchild) |
|
tree.addTopLevelItem(parent) |
|
|
|
# Select parent |
|
tree.selectItem(parent) |
|
assert tree.selectedItems() == [parent] |
|
|
|
# Select child (replaces parent) |
|
tree.selectItem(child1) |
|
assert tree.selectedItems() == [child1] |
|
|
|
# Select grandchild (replaces child) |
|
tree.selectItem(grandchild) |
|
assert tree.selectedItems() == [grandchild] |
|
|
|
def test_multilevel_multi_selection(self): |
|
"""Test multi selection mode with nested items""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
parent = TTkTreeWidgetItem(["Parent"]) |
|
child1 = TTkTreeWidgetItem(["Child 1"]) |
|
child2 = TTkTreeWidgetItem(["Child 2"]) |
|
grandchild = TTkTreeWidgetItem(["Grandchild"]) |
|
|
|
parent.addChildren([child1, child2]) |
|
child1.addChild(grandchild) |
|
tree.addTopLevelItem(parent) |
|
|
|
# Select multiple items at different levels |
|
tree.selectItem(parent) |
|
tree.selectItem(child1) |
|
tree.selectItem(grandchild) |
|
assert tree.selectedItems() == [parent, child1, grandchild] |
|
|
|
# Deselect middle item |
|
tree.deselectItem(child1) |
|
assert tree.selectedItems() == [parent, grandchild] |
|
|
|
# Clear selection |
|
tree.clearSelection() |
|
assert tree.selectedItems() == [] |
|
|
|
def test_multilevel_deselect_nested_item(self): |
|
"""Test deselecting deeply nested items""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
root = TTkTreeWidgetItem(["Root"]) |
|
level1 = TTkTreeWidgetItem(["Level 1"]) |
|
level2 = TTkTreeWidgetItem(["Level 2"]) |
|
level3 = TTkTreeWidgetItem(["Level 3"]) |
|
|
|
root.addChild(level1) |
|
level1.addChild(level2) |
|
level2.addChild(level3) |
|
tree.addTopLevelItem(root) |
|
|
|
# Select all levels |
|
tree.selectItem(root) |
|
tree.selectItem(level1) |
|
tree.selectItem(level2) |
|
tree.selectItem(level3) |
|
assert len(tree.selectedItems()) == 4 |
|
|
|
# Deselect items at different levels |
|
tree.deselectItem(level3) |
|
assert tree.selectedItems() == [root, level1, level2] |
|
|
|
tree.deselectItem(level1) |
|
assert tree.selectedItems() == [root, level2] |
|
|
|
def test_multilevel_selection_after_tree_modification(self): |
|
"""Test selection is pruned after removing items from tree""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
parent = TTkTreeWidgetItem(["Parent"]) |
|
child = TTkTreeWidgetItem(["Child"]) |
|
parent.addChild(child) |
|
tree.addTopLevelItem(parent) |
|
|
|
# Select both items |
|
tree.selectItem(parent) |
|
tree.selectItem(child) |
|
assert len(tree.selectedItems()) == 2 |
|
|
|
# Remove child from parent |
|
parent.removeChild(child) |
|
# Selection should be pruned to only include items still in tree |
|
tree._pruneSelection() |
|
assert tree.selectedItems() == [parent] |
|
|
|
def test_detached_item_from_another_tree_multilevel(self): |
|
"""Test selection ignores detached items from another tree (multilevel)""" |
|
tree1 = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
tree2 = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
|
|
# Create structured trees |
|
parent1 = TTkTreeWidgetItem(["Parent 1"]) |
|
child1 = TTkTreeWidgetItem(["Child 1"]) |
|
parent1.addChild(child1) |
|
tree1.addTopLevelItem(parent1) |
|
|
|
parent2 = TTkTreeWidgetItem(["Parent 2"]) |
|
child2 = TTkTreeWidgetItem(["Child 2"]) |
|
parent2.addChild(child2) |
|
tree2.addTopLevelItem(parent2) |
|
|
|
# Try to select items from tree2 in tree1 |
|
tree1.selectItem(parent2) |
|
tree1.selectItem(child2) |
|
assert tree1.selectedItems() == [] |
|
|
|
# Select valid item from tree1 |
|
tree1.selectItem(parent1) |
|
assert tree1.selectedItems() == [parent1] |
|
|
|
# Deselect items from tree2 in tree1 (should do nothing) |
|
tree1.deselectItem(child2) |
|
assert tree1.selectedItems() == [parent1] |
|
|
|
def test_orphaned_item_after_reparenting(self): |
|
"""Test selection handles orphaned items that were reparented""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
parent1 = TTkTreeWidgetItem(["Parent 1"]) |
|
parent2 = TTkTreeWidgetItem(["Parent 2"]) |
|
child = TTkTreeWidgetItem(["Child"]) |
|
|
|
parent1.addChild(child) |
|
tree.addTopLevelItems([parent1, parent2]) |
|
|
|
# Select all items |
|
tree.selectItem(parent1) |
|
tree.selectItem(parent2) |
|
tree.selectItem(child) |
|
assert len(tree.selectedItems()) == 3 |
|
|
|
# Reparent child to parent2 |
|
parent1.removeChild(child) |
|
parent2.addChild(child) |
|
|
|
# Selection should still be valid (item is still in tree) |
|
tree._pruneSelection() |
|
assert len(tree.selectedItems()) == 3 |
|
|
|
def test_selection_after_take_child(self): |
|
"""Test selection after using takeChild API""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
parent = TTkTreeWidgetItem(["Parent"]) |
|
child1 = TTkTreeWidgetItem(["Child 1"]) |
|
child2 = TTkTreeWidgetItem(["Child 2"]) |
|
|
|
parent.addChildren([child1, child2]) |
|
tree.addTopLevelItem(parent) |
|
|
|
# Select both children |
|
tree.selectItem(child1) |
|
tree.selectItem(child2) |
|
assert len(tree.selectedItems()) == 2 |
|
|
|
# Take child1 from tree |
|
taken = parent.takeChild(0) |
|
assert taken == child1 |
|
|
|
# Prune selection |
|
tree._pruneSelection() |
|
# Only child2 should remain in selection |
|
assert tree.selectedItems() == [child2] |
|
|
|
def test_selection_after_take_children(self): |
|
"""Test selection pruning after using takeChildren API""" |
|
tree = TTkTreeWidget(selectionMode=TTkK.SelectionMode.MultiSelection) |
|
parent = TTkTreeWidgetItem(["Parent"]) |
|
child1 = TTkTreeWidgetItem(["Child 1"]) |
|
child2 = TTkTreeWidgetItem(["Child 2"]) |
|
|
|
parent.addChildren([child1, child2]) |
|
tree.addTopLevelItem(parent) |
|
|
|
# Select parent and children |
|
tree.selectItem(parent) |
|
tree.selectItem(child1) |
|
tree.selectItem(child2) |
|
assert len(tree.selectedItems()) == 3 |
|
|
|
# Detach all children from parent |
|
taken = parent.takeChildren() |
|
assert taken == [child1, child2] |
|
|
|
# Selection should keep only items still in the tree |
|
tree._pruneSelection() |
|
assert tree.selectedItems() == [parent] |
|
|
|
def test_parent_link_set_and_cleared_for_nested_manipulation(self): |
|
"""Test nested item parent link across remove and reparent operations""" |
|
tree = TTkTreeWidget() |
|
parent1 = TTkTreeWidgetItem(["Parent 1"]) |
|
parent2 = TTkTreeWidgetItem(["Parent 2"]) |
|
child = TTkTreeWidgetItem(["Child"]) |
|
tree.addTopLevelItems([parent1, parent2]) |
|
|
|
parent1.addChild(child) |
|
assert child._parent == parent1 |
|
|
|
parent1.removeChild(child) |
|
assert child._parent is None |
|
|
|
parent2.addChild(child) |
|
assert child._parent == parent2 |
|
|
|
parent2.removeChild(child) |
|
assert child._parent is None |
|
|
|
|
|
class TestTTkTreeWidgetSorting: |
|
"""Test sorting functionality""" |
|
|
|
def test_sorting_enabled_default(self): |
|
"""Test sorting is enabled by default""" |
|
tree = TTkTreeWidget() |
|
assert tree.isSortingEnabled() is True |
|
|
|
def test_set_sorting_enabled(self): |
|
"""Test enabling/disabling sorting""" |
|
tree = TTkTreeWidget(sortingEnabled=False) |
|
assert tree.isSortingEnabled() is False |
|
tree.setSortingEnabled(True) |
|
assert tree.isSortingEnabled() is True |
|
|
|
def test_sort_column(self): |
|
"""Test sort column getter""" |
|
tree = TTkTreeWidget() |
|
assert tree.sortColumn() == -1 |
|
|
|
def test_sort_items_when_disabled(self): |
|
"""Test sorting when disabled does nothing""" |
|
tree = TTkTreeWidget(sortingEnabled=False) |
|
tree.sortItems(0, TTkK.AscendingOrder) |
|
assert tree._sortColumn == -1 |
|
|
|
|
|
class TestTTkTreeWidgetColumns: |
|
"""Test column operations""" |
|
|
|
def test_column_width(self): |
|
"""Test getting column width""" |
|
tree = TTkTreeWidget(header=[TTkString("A"), TTkString("B")]) |
|
width = tree.columnWidth(0) |
|
assert width == 20 # Default width |
|
|
|
def test_set_column_width(self): |
|
"""Test setting column width""" |
|
tree = TTkTreeWidget(header=[TTkString("A"), TTkString("B")]) |
|
tree.setColumnWidth(0, 30) |
|
assert tree.columnWidth(0) == 30 + 1 # separator |