"""Round 25: folder highlight while dragging a playlist. Hovering a folder mid-drag lights it up so the user can see the drop would land inside it (previously there was no indication at all). A folder in the dragged row's own branch is never highlighted — the manager rejects that drop as a cycle, so lighting it up would lie. The autoscroll tick's highlight refresh is now kind-aware so a folder highlight survives edge auto-scrolling. Synthetic QDropEvents report source() == None, so the internal-move branch of dragMoveEvent can't be driven directly; _highlight_drop_target carries the logic and is what gets tested. """ from PyQt6.QtCore import QPoint from lintunes.models import Library, Track from lintunes.library_manager import LibraryManager from lintunes.gui.sidebar import PlaylistTree def _tree(qapp, tmp_path, height=400): library = Library(tracks={1: Track(track_id=1, name="One")}) manager = LibraryManager(library, tmp_path) tree = PlaylistTree(manager) tree.resize(200, height) tree.show() return manager, tree def _pos_over(tree, item) -> QPoint: return tree.visualItemRect(item).center() class TestFolderHighlight: def test_folder_highlighted_while_dragging_playlist(self, qapp, tmp_path): manager, tree = _tree(qapp, tmp_path) folder = manager.create_folder("a folder") playlist = manager.create_playlist("a playlist") folder_item = tree._find_item(folder.persistent_id) tree.setCurrentItem(tree._find_item(playlist.persistent_id)) tree._highlight_drop_target(_pos_over(tree, folder_item), "folder") assert tree._drop_target is folder_item def test_leaving_the_folder_clears_the_highlight(self, qapp, tmp_path): manager, tree = _tree(qapp, tmp_path) folder = manager.create_folder("a folder") playlist = manager.create_playlist("a playlist") folder_item = tree._find_item(folder.persistent_id) tree.setCurrentItem(tree._find_item(playlist.persistent_id)) tree._highlight_drop_target(_pos_over(tree, folder_item), "folder") assert tree._drop_target is not None # Hover empty space below the rows (drop there = top level). tree._highlight_drop_target( QPoint(10, tree.viewport().height() - 2), "folder") assert tree._drop_target is None def test_playlist_row_is_not_a_folder_target(self, qapp, tmp_path): manager, tree = _tree(qapp, tmp_path) playlist = manager.create_playlist("a playlist") other = manager.create_playlist("another playlist") tree.setCurrentItem(tree._find_item(other.persistent_id)) tree._highlight_drop_target( _pos_over(tree, tree._find_item(playlist.persistent_id)), "folder") assert tree._drop_target is None def test_dragged_folder_cannot_drop_into_its_own_branch( self, qapp, tmp_path): manager, tree = _tree(qapp, tmp_path) outer = manager.create_folder("a outer") inner = manager.create_folder("b inner", outer.persistent_id) other = manager.create_folder("c other") outer_item = tree._find_item(outer.persistent_id) inner_item = tree._find_item(inner.persistent_id) other_item = tree._find_item(other.persistent_id) outer_item.setExpanded(True) tree.setCurrentItem(outer_item) # dragging "outer" # Onto itself: no highlight (manager would no-op the drop). tree._highlight_drop_target(_pos_over(tree, outer_item), "folder") assert tree._drop_target is None # Into its own subfolder: no highlight (manager rejects the cycle). tree._highlight_drop_target(_pos_over(tree, inner_item), "folder") assert tree._drop_target is None # Into an unrelated folder: highlighted. tree._highlight_drop_target(_pos_over(tree, other_item), "folder") assert tree._drop_target is other_item def test_track_drag_still_highlights_playlists(self, qapp, tmp_path): manager, tree = _tree(qapp, tmp_path) folder = manager.create_folder("a folder") playlist = manager.create_playlist("a playlist") folder_item = tree._find_item(folder.persistent_id) playlist_item = tree._find_item(playlist.persistent_id) tree._highlight_drop_target(_pos_over(tree, playlist_item), "playlist") assert tree._drop_target is playlist_item # Folders are not track-drop targets. tree._highlight_drop_target(_pos_over(tree, folder_item), "playlist") assert tree._drop_target is None class TestKindAwareTick: def test_tick_clears_folder_highlight_scrolled_out_from_under_cursor( self, qapp, tmp_path): manager, tree = _tree(qapp, tmp_path, height=100) folder = manager.create_folder("aaa folder") # sorts to the top row dragged = None for i in range(60): dragged = manager.create_playlist(f"playlist {i}") tree.setCurrentItem(tree._find_item(dragged.persistent_id)) tree.scrollToTop() # setCurrentItem scrolled the dragged row into view pos = _pos_over(tree, tree._find_item(folder.persistent_id)) tree._highlight_drop_target(pos, "folder") assert tree._drop_target is not None # Cursor held still over that spot while the edge auto-scroll rolls: # the folder scrolls away and a non-target row takes its place. tree._drag_target_kind = "folder" tree._autoscroll_pos = pos tree._autoscroll_dir = 1 assert tree.verticalScrollBar().value() == 0 tree._autoscroll_tick() assert tree.verticalScrollBar().value() == 1 assert tree._drop_target is None def test_stop_autoscroll_resets_drag_target_kind(self, qapp, tmp_path): _manager, tree = _tree(qapp, tmp_path) tree._drag_target_kind = "folder" tree._stop_autoscroll() assert tree._drag_target_kind is None