mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-07-21 10:42:59 +02:00
347 lines
13 KiB
Python
347 lines
13 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
from qtpy.QtCore import QItemSelectionModel, QModelIndex, QPoint, Qt
|
|
from qtpy.QtWidgets import QAbstractItemView, QInputDialog, QMenu
|
|
|
|
from bec_widgets.widgets.containers.explorer.file_browser_tree_widget import FileBrowserTreeWidget
|
|
|
|
|
|
@pytest.fixture
|
|
def file_browser_tree(qtbot, tmpdir):
|
|
"""Create a FileBrowserTreeWidget rooted at tmpdir."""
|
|
(Path(tmpdir) / "test_file.py").touch()
|
|
(Path(tmpdir) / "__init__.py").touch()
|
|
(Path(tmpdir) / "test_dir").mkdir()
|
|
(Path(tmpdir) / "test_dir" / "nested_file.py").touch()
|
|
(Path(tmpdir) / "__pycache__").mkdir()
|
|
(Path(tmpdir) / "__pycache__" / "ignored.py").touch()
|
|
|
|
widget = FileBrowserTreeWidget(directory=str(tmpdir))
|
|
qtbot.addWidget(widget)
|
|
qtbot.waitExposed(widget)
|
|
yield widget
|
|
|
|
|
|
def test_file_browser_tree_set_directory(file_browser_tree, tmpdir):
|
|
"""Test setting the directory."""
|
|
assert file_browser_tree.directory == str(tmpdir)
|
|
|
|
|
|
def test_file_browser_tree_hides_filtered_entries(file_browser_tree, qtbot):
|
|
"""Test that filtered cache/init entries are hidden from the browser."""
|
|
|
|
def visible_names():
|
|
root_index = file_browser_tree.tree.rootIndex()
|
|
return [
|
|
file_browser_tree.proxy_model.data(
|
|
file_browser_tree.proxy_model.index(i, 0, root_index)
|
|
)
|
|
for i in range(file_browser_tree.proxy_model.rowCount(root_index))
|
|
]
|
|
|
|
qtbot.waitUntil(lambda: "test_file.py" in visible_names(), timeout=5000)
|
|
assert "__pycache__" not in visible_names()
|
|
assert "__init__.py" not in visible_names()
|
|
|
|
|
|
def test_file_browser_tree_native_browser_configuration(file_browser_tree):
|
|
"""Test that the tree is configured like a plain native file browser."""
|
|
assert file_browser_tree.model.isReadOnly() is False
|
|
assert file_browser_tree.model.nameFilters() == ["*.py"]
|
|
assert file_browser_tree.tree.alternatingRowColors() is False
|
|
assert (
|
|
file_browser_tree.tree.selectionMode() == QAbstractItemView.SelectionMode.ExtendedSelection
|
|
)
|
|
assert file_browser_tree.tree.dragDropMode() == QAbstractItemView.DragDropMode.InternalMove
|
|
assert file_browser_tree.tree.defaultDropAction() == Qt.DropAction.MoveAction
|
|
assert (
|
|
file_browser_tree.tree.selectionBehavior() == QAbstractItemView.SelectionBehavior.SelectRows
|
|
)
|
|
|
|
|
|
def test_file_browser_tree_set_readonly(file_browser_tree):
|
|
"""Test toggling read-only mode on the file browser."""
|
|
file_browser_tree.set_readonly(True)
|
|
assert file_browser_tree.model.isReadOnly() is True
|
|
assert file_browser_tree.tree.dragDropMode() == QAbstractItemView.DragDropMode.NoDragDrop
|
|
|
|
file_browser_tree.set_readonly(False)
|
|
assert file_browser_tree.model.isReadOnly() is False
|
|
assert file_browser_tree.tree.dragDropMode() == QAbstractItemView.DragDropMode.InternalMove
|
|
|
|
|
|
@pytest.mark.timeout(10)
|
|
def test_file_browser_tree_on_item_clicked(file_browser_tree, qtbot):
|
|
"""Test that clicks and double-clicks emit the expected file signals."""
|
|
file_selected_signals = []
|
|
file_open_requested_signals = []
|
|
|
|
def on_file_selected(file_path):
|
|
file_selected_signals.append(file_path)
|
|
|
|
def on_file_open_requested(file_path):
|
|
file_open_requested_signals.append(file_path)
|
|
|
|
file_browser_tree.file_selected.connect(on_file_selected)
|
|
file_browser_tree.file_open_requested.connect(on_file_open_requested)
|
|
|
|
def has_py_file():
|
|
nonlocal py_file_index
|
|
root_index = file_browser_tree.tree.rootIndex()
|
|
for i in range(file_browser_tree.proxy_model.rowCount(root_index)):
|
|
index = file_browser_tree.proxy_model.index(i, 0, root_index)
|
|
if file_browser_tree.proxy_model.data(index) == "test_file.py":
|
|
py_file_index = index
|
|
return True
|
|
return False
|
|
|
|
py_file_index = None
|
|
qtbot.waitUntil(has_py_file)
|
|
|
|
file_browser_tree.tree.selectionModel().select(
|
|
py_file_index,
|
|
QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows,
|
|
)
|
|
file_browser_tree._on_item_clicked(py_file_index)
|
|
qtbot.wait(100)
|
|
|
|
py_file_index = None
|
|
qtbot.waitUntil(has_py_file)
|
|
|
|
file_browser_tree._on_item_double_clicked(py_file_index)
|
|
qtbot.wait(100)
|
|
|
|
assert len(file_selected_signals) == 1
|
|
assert Path(file_selected_signals[0]).name == "test_file.py"
|
|
assert len(file_open_requested_signals) == 1
|
|
assert Path(file_open_requested_signals[0]).name == "test_file.py"
|
|
|
|
|
|
@pytest.mark.timeout(10)
|
|
def test_file_browser_tree_accepts_custom_name_filters(qtbot, tmpdir):
|
|
"""Test that the file browser can be configured for non-Python files."""
|
|
(Path(tmpdir) / "config.yaml").touch()
|
|
(Path(tmpdir) / "settings.yml").touch()
|
|
(Path(tmpdir) / "script.py").touch()
|
|
|
|
widget = FileBrowserTreeWidget(directory=str(tmpdir), name_filters=["*.yaml", "*.yml"])
|
|
qtbot.addWidget(widget)
|
|
qtbot.waitExposed(widget)
|
|
|
|
selected = []
|
|
widget.file_selected.connect(selected.append)
|
|
|
|
def visible_names():
|
|
root_index = widget.tree.rootIndex()
|
|
return [
|
|
widget.proxy_model.data(widget.proxy_model.index(i, 0, root_index))
|
|
for i in range(widget.proxy_model.rowCount(root_index))
|
|
]
|
|
|
|
qtbot.waitUntil(lambda: "config.yaml" in visible_names(), timeout=5000)
|
|
assert "settings.yml" in visible_names()
|
|
assert "script.py" not in visible_names()
|
|
|
|
root_index = widget.tree.rootIndex()
|
|
yaml_index = None
|
|
for i in range(widget.proxy_model.rowCount(root_index)):
|
|
index = widget.proxy_model.index(i, 0, root_index)
|
|
if widget.proxy_model.data(index) == "config.yaml":
|
|
yaml_index = index
|
|
break
|
|
|
|
assert yaml_index is not None
|
|
widget.tree.selectionModel().select(
|
|
yaml_index,
|
|
QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows,
|
|
)
|
|
widget._on_item_clicked(yaml_index)
|
|
|
|
assert len(selected) == 1
|
|
assert Path(selected[0]).name == "config.yaml"
|
|
|
|
|
|
def test_file_browser_tree_delete_request_emits_selected_file(file_browser_tree, qtbot):
|
|
"""Test that delete requests emit the currently selected file path."""
|
|
delete_requests = []
|
|
|
|
def on_delete_requested(file_path):
|
|
delete_requests.append(file_path)
|
|
|
|
file_browser_tree.file_delete_requested.connect(on_delete_requested)
|
|
|
|
def get_test_file_index():
|
|
root_index = file_browser_tree.tree.rootIndex()
|
|
for i in range(file_browser_tree.proxy_model.rowCount(root_index)):
|
|
index = file_browser_tree.proxy_model.index(i, 0, root_index)
|
|
if file_browser_tree.proxy_model.data(index) == "test_file.py":
|
|
return index
|
|
return None
|
|
|
|
qtbot.waitUntil(lambda: get_test_file_index() is not None, timeout=5000)
|
|
file_index = get_test_file_index()
|
|
assert file_index is not None
|
|
file_browser_tree.tree.setCurrentIndex(file_index)
|
|
file_browser_tree._on_file_delete_requested()
|
|
qtbot.wait(100)
|
|
|
|
assert len(delete_requests) == 1
|
|
assert Path(delete_requests[0]).name == "test_file.py"
|
|
|
|
|
|
def test_file_browser_tree_clear_selection(file_browser_tree, qtbot):
|
|
"""Test clearing the current highlighted file selection."""
|
|
|
|
def get_test_file_index():
|
|
root_index = file_browser_tree.tree.rootIndex()
|
|
for i in range(file_browser_tree.proxy_model.rowCount(root_index)):
|
|
index = file_browser_tree.proxy_model.index(i, 0, root_index)
|
|
if file_browser_tree.proxy_model.data(index) == "test_file.py":
|
|
return index
|
|
return None
|
|
|
|
qtbot.waitUntil(lambda: get_test_file_index() is not None, timeout=5000)
|
|
file_index = get_test_file_index()
|
|
assert file_index is not None
|
|
file_browser_tree.tree.setCurrentIndex(file_index)
|
|
|
|
assert file_browser_tree.tree.selectionModel().hasSelection()
|
|
|
|
file_browser_tree.clear_selection()
|
|
|
|
assert not file_browser_tree.tree.selectionModel().hasSelection()
|
|
assert not file_browser_tree.tree.currentIndex().isValid()
|
|
|
|
|
|
def test_file_browser_tree_context_menu_rename_action(file_browser_tree, qtbot, monkeypatch):
|
|
"""Test that the context menu exposes a rename action for editable items."""
|
|
|
|
def get_test_file_index():
|
|
root_index = file_browser_tree.tree.rootIndex()
|
|
for i in range(file_browser_tree.proxy_model.rowCount(root_index)):
|
|
index = file_browser_tree.proxy_model.index(i, 0, root_index)
|
|
if file_browser_tree.proxy_model.data(index) == "test_file.py":
|
|
return index
|
|
return None
|
|
|
|
qtbot.waitUntil(lambda: get_test_file_index() is not None, timeout=5000)
|
|
file_index = get_test_file_index()
|
|
assert file_index is not None
|
|
|
|
called = {}
|
|
|
|
def fake_edit(index):
|
|
called["name"] = file_browser_tree.proxy_model.data(index)
|
|
return True
|
|
|
|
monkeypatch.setattr(file_browser_tree.tree, "edit", fake_edit)
|
|
menu = file_browser_tree._build_context_menu(file_index)
|
|
assert menu is not None
|
|
assert [action.text() for action in menu.actions()] == [
|
|
"New Folder",
|
|
"Open",
|
|
"Rename",
|
|
"Delete File",
|
|
]
|
|
menu.actions()[2].trigger()
|
|
|
|
assert called["name"] == "test_file.py"
|
|
|
|
|
|
def test_file_browser_tree_context_menu_open_action(file_browser_tree, qtbot):
|
|
"""Test that the context menu exposes an open action for files."""
|
|
opened = []
|
|
file_browser_tree.file_open_requested.connect(opened.append)
|
|
|
|
def get_test_file_index():
|
|
root_index = file_browser_tree.tree.rootIndex()
|
|
for i in range(file_browser_tree.proxy_model.rowCount(root_index)):
|
|
index = file_browser_tree.proxy_model.index(i, 0, root_index)
|
|
if file_browser_tree.proxy_model.data(index) == "test_file.py":
|
|
return index
|
|
return None
|
|
|
|
qtbot.waitUntil(lambda: get_test_file_index() is not None, timeout=5000)
|
|
file_index = get_test_file_index()
|
|
assert file_index is not None
|
|
|
|
file_browser_tree.tree.setCurrentIndex(file_index)
|
|
menu = file_browser_tree._build_context_menu(file_index)
|
|
assert menu is not None
|
|
menu.actions()[1].trigger()
|
|
|
|
assert len(opened) == 1
|
|
assert Path(opened[0]).name == "test_file.py"
|
|
|
|
|
|
def test_file_browser_tree_context_menu_root_allows_new_folder(file_browser_tree):
|
|
"""Test that right-clicking the root area offers folder creation."""
|
|
menu = file_browser_tree._build_context_menu(QModelIndex())
|
|
assert menu is not None
|
|
assert [action.text() for action in menu.actions()] == ["New Folder"]
|
|
|
|
|
|
def test_file_browser_tree_context_menu_hidden_when_read_only(file_browser_tree, monkeypatch):
|
|
"""Test that no context menu is shown in read-only mode."""
|
|
file_browser_tree.set_readonly(True)
|
|
|
|
called = {"shown": False}
|
|
|
|
def fake_exec(*_args, **_kwargs):
|
|
called["shown"] = True
|
|
return None
|
|
|
|
monkeypatch.setattr(QMenu, "exec", fake_exec)
|
|
file_browser_tree._show_context_menu(QPoint(0, 0))
|
|
|
|
assert called["shown"] is False
|
|
|
|
|
|
def test_file_browser_tree_emits_file_renamed_signal(file_browser_tree):
|
|
"""Test that model rename notifications are emitted as full paths."""
|
|
renamed = []
|
|
file_browser_tree.file_renamed.connect(lambda old, new: renamed.append((old, new)))
|
|
|
|
file_browser_tree._on_model_file_renamed(str(file_browser_tree.directory), "old.py", "new.py")
|
|
|
|
assert renamed == [
|
|
(
|
|
str(Path(file_browser_tree.directory) / "old.py"),
|
|
str(Path(file_browser_tree.directory) / "new.py"),
|
|
)
|
|
]
|
|
|
|
|
|
def test_file_browser_tree_create_subdirectory_at_root(file_browser_tree, monkeypatch):
|
|
"""Test creating a subdirectory at the root level."""
|
|
monkeypatch.setattr(QInputDialog, "getText", lambda *_args, **_kwargs: ("new_root_dir", True))
|
|
|
|
file_browser_tree._create_subdirectory(str(file_browser_tree.directory))
|
|
|
|
assert (Path(file_browser_tree.directory) / "new_root_dir").is_dir()
|
|
|
|
|
|
def test_file_browser_tree_create_subdirectory_in_selected_directory(
|
|
file_browser_tree, qtbot, monkeypatch
|
|
):
|
|
"""Test creating a subdirectory inside the selected directory level."""
|
|
|
|
def get_test_dir_index():
|
|
root_index = file_browser_tree.tree.rootIndex()
|
|
for i in range(file_browser_tree.proxy_model.rowCount(root_index)):
|
|
index = file_browser_tree.proxy_model.index(i, 0, root_index)
|
|
if file_browser_tree.proxy_model.data(index) == "test_dir":
|
|
return index
|
|
return None
|
|
|
|
qtbot.waitUntil(lambda: get_test_dir_index() is not None, timeout=5000)
|
|
dir_index = get_test_dir_index()
|
|
assert dir_index is not None
|
|
assert file_browser_tree._get_target_directory(dir_index).endswith("test_dir")
|
|
|
|
monkeypatch.setattr(QInputDialog, "getText", lambda *_args, **_kwargs: ("child_dir", True))
|
|
file_browser_tree._create_subdirectory(file_browser_tree._get_target_directory(dir_index))
|
|
|
|
assert (Path(file_browser_tree.directory) / "test_dir" / "child_dir").is_dir()
|