"""Tree navigation and accessibility regressions, without a server or audio.""" import pytest from PySide6.QtCore import Qt from PySide6.QtGui import QAccessible from PySide6.QtTest import QTest from PySide6.QtWidgets import QApplication, QTreeWidgetItem from src.accessibility.accessible_tree import AccessibleTreeWidget @pytest.fixture def tree(): app = QApplication.instance() or QApplication([]) widget = AccessibleTreeWidget() widget.setHeaderHidden(True) widget.show() app.processEvents() yield widget widget.close() def add(parent, text): item = QTreeWidgetItem([text]) if isinstance(parent, AccessibleTreeWidget): parent.addTopLevelItem(item) else: parent.addChild(item) return item def press(tree, key, expected): QTest.keyClick(tree, key) assert tree.currentItem() is expected def test_collapsed_children_and_late_results_are_not_navigable(tree): root = add(tree, 'Albums') child = add(root, 'Album') last = add(tree, 'Playlists') tree.setCurrentItem(root) press(tree, Qt.Key_Down, last) press(tree, Qt.Key_Up, root) press(tree, Qt.Key_Right, root) press(tree, Qt.Key_Right, child) root.setExpanded(False) assert tree.currentItem() is root late = add(root, 'Loaded after collapse') assert not tree.isItemNavigable(late) press(tree, Qt.Key_Down, last) root.setExpanded(True) assert tree.isItemNavigable(late) def test_hidden_rows_are_skipped_in_both_directions_and_at_edges(tree): hidden_first = add(tree, 'Hidden first') hidden_first.setHidden(True) root = add(tree, 'Root') hidden_child = add(root, 'Hidden child') hidden_child.setHidden(True) child = add(root, 'Visible child') disabled = add(root, 'Disabled') disabled.setDisabled(True) other = add(root, 'Other child') hidden_last = add(tree, 'Hidden last') hidden_last.setHidden(True) root.setExpanded(True) tree.setCurrentItem(root) press(tree, Qt.Key_Right, child) press(tree, Qt.Key_Down, other) press(tree, Qt.Key_Up, child) press(tree, Qt.Key_Home, root) press(tree, Qt.Key_End, other) press(tree, Qt.Key_PageUp, root) press(tree, Qt.Key_PageDown, other) def test_expansion_preserves_names_descriptions_and_flags(tree): root = add(tree, 'Artist') root.setData(0, Qt.AccessibleTextRole, 'Artist, 12 albums') child = add(root, 'Album') child.setData(0, Qt.AccessibleDescriptionRole, 'Released in 2024') child.setFlags(child.flags() & ~Qt.ItemIsSelectable) disabled = add(root, 'Unavailable') disabled.setDisabled(True) original_flags = [child.flags(), disabled.flags()] states = [] tree.itemStateChanged.connect(lambda item, state: states.append(state)) for _ in range(2): root.setExpanded(True) root.setExpanded(False) assert root.data(0, Qt.AccessibleTextRole) == 'Artist, 12 albums' assert child.data(0, Qt.AccessibleDescriptionRole) == 'Released in 2024' assert [child.flags(), disabled.flags()] == original_flags assert states == ['expanded', 'collapsed'] * 2 def test_nested_collapsed_branch_stays_closed_on_parent_reopen(tree): root = add(tree, 'Root') branch = add(root, 'Branch') leaf = add(branch, 'Leaf') root.setExpanded(True) branch.setExpanded(True) tree.setCurrentItem(leaf) branch.setExpanded(False) assert tree.currentItem() is branch root.setExpanded(False) root.setExpanded(True) assert not tree.isItemNavigable(leaf) tree.setCurrentItem(branch) press(tree, Qt.Key_End, branch) def test_accessibility_exposes_only_expanded_rows(tree): root = add(tree, 'Albums') add(root, 'Song') tree.setCurrentItem(root) QApplication.processEvents() interface = QAccessible.queryAccessibleInterface(tree) def spoken_rows(): return [interface.child(i).text(QAccessible.Name) for i in range(interface.childCount()) if interface.child(i).role() == QAccessible.TreeItem] assert spoken_rows() == ['Albums'] root.setExpanded(True) QApplication.processEvents() assert spoken_rows() == ['Albums', 'Song'] root.setExpanded(False) QApplication.processEvents() assert spoken_rows() == ['Albums']