fix: developer view improvements and dependency updates

This commit is contained in:
wakonig_k authored and Klaus Wakonig committed 2025-10-30 17:26:14 +01:00
1 parent f77f5c7741
commit b194ec22b2
16 files changed
+2335 -363

No files matched your search

@@ -0,0 +1,119 @@
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-import
from unittest import mock
import pytest
from qtpy.QtCore import QMimeData, QPoint, Qt
from qtpy.QtWidgets import QLabel
from bec_widgets.widgets.containers.explorer.collapsible_tree_section import CollapsibleSection
@pytest.fixture
def collapsible_section(qtbot):
"""Create a basic CollapsibleSection widget for testing"""
widget = CollapsibleSection(title="Test Section")
qtbot.addWidget(widget)
yield widget
@pytest.fixture
def dummy_content_widget(qtbot):
"""Create a simple widget to be used as content"""
widget = QLabel("Test Content")
qtbot.addWidget(widget)
return widget
def test_basic_initialization(collapsible_section):
"""Test basic initialization"""
assert collapsible_section.title == "Test Section"
assert collapsible_section.expanded is True
assert collapsible_section.content_widget is None
def test_toggle_expanded(collapsible_section):
"""Test toggling expansion state"""
assert collapsible_section.expanded is True
collapsible_section.toggle_expanded()
assert collapsible_section.expanded is False
collapsible_section.toggle_expanded()
assert collapsible_section.expanded is True
def test_set_widget(collapsible_section, dummy_content_widget):
"""Test setting content widget"""
collapsible_section.set_widget(dummy_content_widget)
assert collapsible_section.content_widget == dummy_content_widget
assert dummy_content_widget.parent() == collapsible_section
def test_connect_add_button(qtbot):
"""Test connecting add button"""
widget = CollapsibleSection(title="Test", show_add_button=True)
qtbot.addWidget(widget)
mock_slot = mock.MagicMock()
widget.connect_add_button(mock_slot)
qtbot.mouseClick(widget.header_add_button, Qt.MouseButton.LeftButton)
mock_slot.assert_called_once()
def test_section_reorder_signal(collapsible_section):
"""Test section reorder signal emission"""
signals_received = []
collapsible_section.section_reorder_requested.connect(
lambda source, target: signals_received.append((source, target))
)
# Create mock drop event
mime_data = QMimeData()
mime_data.setText("section:Source Section")
mock_event = mock.MagicMock()
mock_event.mimeData.return_value = mime_data
collapsible_section._header_drop_event(mock_event)
assert len(signals_received) == 1
assert signals_received[0] == ("Source Section", "Test Section")
def test_nested_collapsible_sections(qtbot):
"""Test that collapsible sections can be nested"""
# Create parent section
parent_section = CollapsibleSection(title="Parent Section")
qtbot.addWidget(parent_section)
# Create child section
child_section = CollapsibleSection(title="Child Section")
qtbot.addWidget(child_section)
# Add some content to the child section
child_content = QLabel("Child Content")
qtbot.addWidget(child_content)
child_section.set_widget(child_content)
# Nest the child section inside the parent
parent_section.set_widget(child_section)
# Verify nesting structure
assert parent_section.content_widget == child_section
assert child_section.parent() == parent_section
assert child_section.content_widget == child_content
assert child_content.parent() == child_section
# Test that both sections can expand/collapse independently
assert parent_section.expanded is True
assert child_section.expanded is True
# Collapse child section
child_section.toggle_expanded()
assert child_section.expanded is False
assert parent_section.expanded is True # Parent should remain expanded
# Collapse parent section
parent_section.toggle_expanded()
assert parent_section.expanded is False
assert child_section.expanded is False # Child state unchanged
+378
View File
@@ -0,0 +1,378 @@
"""
Unit tests for the Developer View widget.
This module tests the DeveloperView widget functionality including:
- Widget initialization and setup
- Monaco editor integration
- IDE Explorer integration
- File operations (open, save, format)
- Context menu actions
- Toolbar functionality
"""
import os
import tempfile
from unittest import mock
import pytest
from qtpy.QtWidgets import QDialog
from bec_widgets.examples.developer_view.developer_widget import DeveloperWidget
from bec_widgets.widgets.editors.monaco.monaco_dock import MonacoDock
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
from bec_widgets.widgets.utility.ide_explorer.ide_explorer import IDEExplorer
from .client_mocks import mocked_client
@pytest.fixture
def developer_view(qtbot, mocked_client):
"""Create a DeveloperWidget for testing."""
widget = DeveloperWidget(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
@pytest.fixture
def temp_python_file():
"""Create a temporary Python file for testing."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(
"""# Test Python file
import os
import sys
def test_function():
return "Hello, World!"
if __name__ == "__main__":
print(test_function())
"""
)
temp_file_path = f.name
yield temp_file_path
# Cleanup
if os.path.exists(temp_file_path):
os.unlink(temp_file_path)
@pytest.fixture
def mock_scan_control_dialog():
"""Mock the ScanControlDialog for testing."""
with mock.patch(
"bec_widgets.widgets.editors.monaco.scan_control_dialog.ScanControlDialog"
) as mock_dialog:
# Configure the mock dialog
mock_dialog_instance = mock.MagicMock()
mock_dialog_instance.exec_.return_value = QDialog.DialogCode.Accepted
mock_dialog_instance.get_scan_code.return_value = (
"scans.ascan(dev.samx, 0, 1, 10, exp_time=0.1)"
)
mock_dialog.return_value = mock_dialog_instance
yield mock_dialog_instance
class TestDeveloperViewInitialization:
"""Test developer view initialization and basic functionality."""
def test_developer_view_initialization(self, developer_view):
"""Test that the developer view initializes correctly."""
# Check that main components are created
assert hasattr(developer_view, "monaco")
assert hasattr(developer_view, "explorer")
assert hasattr(developer_view, "console")
assert hasattr(developer_view, "terminal")
assert hasattr(developer_view, "toolbar")
assert hasattr(developer_view, "dock_manager")
assert hasattr(developer_view, "plotting_ads")
assert hasattr(developer_view, "signature_help")
def test_monaco_editor_integration(self, developer_view):
"""Test that Monaco editor is properly integrated."""
assert isinstance(developer_view.monaco, MonacoDock)
assert developer_view.monaco.parent() is not None
def test_ide_explorer_integration(self, developer_view):
"""Test that IDE Explorer is properly integrated."""
assert isinstance(developer_view.explorer, IDEExplorer)
assert developer_view.explorer.parent() is not None
def test_toolbar_components(self, developer_view):
"""Test that toolbar components are properly set up."""
assert developer_view.toolbar is not None
# Check for expected toolbar actions
toolbar_components = developer_view.toolbar.components
expected_actions = ["save", "save_as", "run", "stop", "vim"]
for action_name in expected_actions:
assert toolbar_components.exists(action_name)
def test_dock_manager_setup(self, developer_view):
"""Test that dock manager is properly configured."""
assert developer_view.dock_manager is not None
# Check that docks are added
dock_widgets = developer_view.dock_manager.dockWidgets()
assert len(dock_widgets) >= 4 # Explorer, Monaco, Console, Terminal
class TestFileOperations:
"""Test file operation functionality."""
def test_open_new_file(self, developer_view, temp_python_file, qtbot):
"""Test opening a new file in the Monaco editor."""
# Simulate opening a file through the IDE explorer signal
developer_view._open_new_file(temp_python_file, "scripts/local")
# Wait for the file to be loaded
qtbot.waitUntil(
lambda: temp_python_file in developer_view.monaco._get_open_files(), timeout=2000
)
# Check that the file was opened
assert temp_python_file in developer_view.monaco._get_open_files()
# Check that content was loaded (simplified check)
# Get the editor dock for the file and check its content
dock = developer_view.monaco._get_editor_dock(temp_python_file)
if dock:
editor_widget = dock.widget()
assert "test_function" in editor_widget.get_text()
def test_open_shared_file_readonly(self, developer_view, temp_python_file, qtbot):
"""Test that shared files are opened in read-only mode."""
# Open file with shared scope
developer_view._open_new_file(temp_python_file, "scripts/shared")
qtbot.waitUntil(
lambda: temp_python_file in developer_view.monaco._get_open_files(), timeout=2000
)
# Check that the file is set to read-only
dock = developer_view.monaco._get_editor_dock(temp_python_file)
if dock:
monaco_widget = dock.widget()
# Check that the widget is in read-only mode
# This depends on MonacoWidget having a readonly property or method
assert monaco_widget is not None
def test_file_icon_assignment(self, developer_view, temp_python_file, qtbot):
"""Test that file icons are assigned based on scope."""
# Test script file icon
developer_view._open_new_file(temp_python_file, "scripts/local")
qtbot.waitUntil(
lambda: temp_python_file in developer_view.monaco._get_open_files(), timeout=2000
)
# Check that an icon was set (simplified check)
dock = developer_view.monaco._get_editor_dock(temp_python_file)
if dock:
assert not dock.icon().isNull()
def test_save_functionality(self, developer_view, qtbot):
"""Test the save functionality."""
# Get the currently focused editor widget (if any)
if developer_view.monaco.last_focused_editor:
editor_widget = developer_view.monaco.last_focused_editor.widget()
test_text = "print('Hello from save test')"
editor_widget.set_text(test_text)
qtbot.waitUntil(lambda: editor_widget.get_text() == test_text, timeout=1000)
# Test the save action
with mock.patch.object(developer_view.monaco, "save_file") as mock_save:
developer_view.on_save()
mock_save.assert_called_once()
def test_save_as_functionality(self, developer_view, qtbot):
"""Test the save as functionality."""
# Get the currently focused editor widget (if any)
if developer_view.monaco.last_focused_editor:
editor_widget = developer_view.monaco.last_focused_editor.widget()
test_text = "print('Hello from save as test')"
editor_widget.set_text(test_text)
qtbot.waitUntil(lambda: editor_widget.get_text() == test_text, timeout=1000)
# Test the save as action
with mock.patch.object(developer_view.monaco, "save_file") as mock_save:
developer_view.on_save_as()
mock_save.assert_called_once_with(force_save_as=True)
class TestMonacoEditorIntegration:
"""Test Monaco editor specific functionality."""
def test_vim_mode_toggle(self, developer_view, qtbot):
"""Test vim mode toggle functionality."""
# Test enabling vim mode
with mock.patch.object(developer_view.monaco, "set_vim_mode") as mock_vim:
developer_view.on_vim_triggered()
# The actual call depends on the checkbox state
mock_vim.assert_called_once()
def test_context_menu_insert_scan(self, developer_view, mock_scan_control_dialog, qtbot):
"""Test the Insert Scan context menu action."""
# This functionality is handled by individual MonacoWidget instances
# Test that the dock has editor widgets
dock_widgets = developer_view.monaco.dock_manager.dockWidgets()
assert len(dock_widgets) >= 1
# Test on the first available editor
first_dock = dock_widgets[0]
monaco_widget = first_dock.widget()
assert isinstance(monaco_widget, MonacoWidget)
def test_context_menu_format_code(self, developer_view, qtbot):
"""Test the Format Code context menu action."""
# Get an editor widget from the dock manager
dock_widgets = developer_view.monaco.dock_manager.dockWidgets()
if dock_widgets:
first_dock = dock_widgets[0]
monaco_widget = first_dock.widget()
# Set some unformatted Python code
unformatted_code = "import os,sys\ndef test():\n x=1+2\n return x"
monaco_widget.set_text(unformatted_code)
qtbot.waitUntil(lambda: monaco_widget.get_text() == unformatted_code, timeout=1000)
# Test format action on the individual widget
with mock.patch.object(monaco_widget, "format") as mock_format:
monaco_widget.format()
mock_format.assert_called_once()
def test_save_enabled_signal_handling(self, developer_view, qtbot):
"""Test that save enabled signals are handled correctly."""
# Mock the toolbar update method
with mock.patch.object(developer_view, "_on_save_enabled_update") as mock_update:
# Simulate save enabled signal
developer_view.monaco.save_enabled.emit(True)
mock_update.assert_called_with(True)
developer_view.monaco.save_enabled.emit(False)
mock_update.assert_called_with(False)
class TestIDEExplorerIntegration:
"""Test IDE Explorer integration."""
def test_file_open_signal_connection(self, developer_view):
"""Test that file open signals are properly connected."""
# Test that the signal connection works by mocking the connected method
with mock.patch.object(developer_view, "_open_new_file") as mock_open:
# Emit the signal to test the connection
developer_view.explorer.file_open_requested.emit("test_file.py", "scripts/local")
mock_open.assert_called_once_with("test_file.py", "scripts/local")
def test_file_preview_signal_connection(self, developer_view):
"""Test that file preview signals are properly connected."""
# Test that the signal exists and can be emitted (basic connection test)
try:
developer_view.explorer.file_preview_requested.emit("test_file.py", "scripts/local")
# If no exception is raised, the signal exists and is connectable
assert True
except AttributeError:
assert False, "file_preview_requested signal not found"
def test_sections_configuration(self, developer_view):
"""Test that IDE Explorer sections are properly configured."""
assert "scripts" in developer_view.explorer.sections
assert "macros" in developer_view.explorer.sections
class TestToolbarIntegration:
"""Test toolbar functionality and integration."""
def test_toolbar_save_button_state(self, developer_view):
"""Test toolbar save button state management."""
# Test that save buttons exist and can be controlled
save_action = developer_view.toolbar.components.get_action("save")
save_as_action = developer_view.toolbar.components.get_action("save_as")
# Test that the actions exist and are accessible
assert save_action.action is not None
assert save_as_action.action is not None
# Test that they can be enabled/disabled via the update method
developer_view._on_save_enabled_update(False)
assert not save_action.action.isEnabled()
assert not save_as_action.action.isEnabled()
developer_view._on_save_enabled_update(True)
assert save_action.action.isEnabled()
assert save_as_action.action.isEnabled()
def test_vim_mode_button_toggle(self, developer_view, qtbot):
"""Test vim mode button toggle functionality."""
vim_action = developer_view.toolbar.components.get_action("vim")
if vim_action:
# Test toggling vim mode
initial_state = vim_action.action.isChecked()
# Simulate button click
vim_action.action.trigger()
# Check that state changed
assert vim_action.action.isChecked() != initial_state
class TestErrorHandling:
"""Test error handling in various scenarios."""
def test_invalid_scope_handling(self, developer_view, temp_python_file):
"""Test handling of invalid scope parameters."""
# Test with invalid scope
try:
developer_view._open_new_file(temp_python_file, "invalid/scope")
except Exception as e:
assert False, f"Invalid scope should be handled gracefully: {e}"
def test_monaco_editor_error_handling(self, developer_view):
"""Test error handling in Monaco editor operations."""
# Test with editor widgets from dock manager
dock_widgets = developer_view.monaco.dock_manager.dockWidgets()
if dock_widgets:
first_dock = dock_widgets[0]
monaco_widget = first_dock.widget()
# Test setting invalid text
try:
monaco_widget.set_text(None) # This might cause an error
except Exception:
# Errors should be handled gracefully
pass
class TestSignalIntegration:
"""Test signal connections and data flow."""
def test_file_open_signal_flow(self, developer_view, temp_python_file, qtbot):
"""Test the complete file open signal flow."""
# Mock the _open_new_file method to verify it gets called
with mock.patch.object(developer_view, "_open_new_file") as mock_open:
# Emit the file open signal from explorer
developer_view.explorer.file_open_requested.emit(temp_python_file, "scripts/local")
# Verify the signal was handled
mock_open.assert_called_once_with(temp_python_file, "scripts/local")
def test_save_enabled_signal_flow(self, developer_view, qtbot):
"""Test the save enabled signal flow."""
# Mock the update method (the actual method is _on_save_enabled_update)
with mock.patch.object(developer_view, "_on_save_enabled_update") as mock_update:
# Simulate monaco dock emitting save enabled signal
developer_view.monaco.save_enabled.emit(True)
# Verify the signal was handled
mock_update.assert_called_once_with(True)
if __name__ == "__main__":
pytest.main([__file__])
+422
View File
@@ -1,7 +1,9 @@
import os
from pathlib import Path
from unittest import mock
import pytest
from qtpy.QtWidgets import QMessageBox
from bec_widgets.widgets.utility.ide_explorer.ide_explorer import IDEExplorer
@@ -34,3 +36,423 @@ def test_ide_explorer_add_local_script(ide_explorer, qtbot, tmpdir):
):
ide_explorer._add_local_script()
assert os.path.exists(os.path.join(tmpdir, "test_file.py"))
def test_shared_scripts_section_with_files(ide_explorer, tmpdir):
"""Test that shared scripts section is created when plugin directory has files"""
# Create dummy shared script files
shared_scripts_dir = tmpdir.mkdir("shared_scripts")
shared_scripts_dir.join("shared_script1.py").write("# Shared script 1")
shared_scripts_dir.join("shared_script2.py").write("# Shared script 2")
ide_explorer.clear()
with mock.patch.object(ide_explorer, "_get_plugin_dir") as mock_get_plugin_dir:
mock_get_plugin_dir.return_value = str(shared_scripts_dir)
ide_explorer.add_script_section()
scripts_section = ide_explorer.main_explorer.get_section("SCRIPTS")
assert scripts_section is not None
# Should have both Local and Shared sections
local_section = scripts_section.content_widget.get_section("Local")
shared_section = scripts_section.content_widget.get_section("Shared (Read-only)")
assert local_section is not None
assert shared_section is not None
assert "read-only" in shared_section.toolTip().lower()
def test_shared_macros_section_with_files(ide_explorer, tmpdir):
"""Test that shared macros section is created when plugin directory has files"""
# Create dummy shared macro files
shared_macros_dir = tmpdir.mkdir("shared_macros")
shared_macros_dir.join("shared_macro1.py").write(
"""
def shared_function1():
return "shared1"
def shared_function2():
return "shared2"
"""
)
shared_macros_dir.join("utilities.py").write(
"""
def utility_function():
return "utility"
"""
)
with mock.patch.object(ide_explorer, "_get_plugin_dir") as mock_get_plugin_dir:
mock_get_plugin_dir.return_value = str(shared_macros_dir)
ide_explorer.clear()
ide_explorer.sections = ["macros"]
macros_section = ide_explorer.main_explorer.get_section("MACROS")
assert macros_section is not None
# Should have both Local and Shared sections
local_section = macros_section.content_widget.get_section("Local")
shared_section = macros_section.content_widget.get_section("Shared (Read-only)")
assert local_section is not None
assert shared_section is not None
assert "read-only" in shared_section.toolTip().lower()
def test_shared_sections_not_added_when_plugin_dir_missing(ide_explorer):
"""Test that shared sections are not added when plugin directories don't exist"""
ide_explorer.clear()
with mock.patch.object(ide_explorer, "_get_plugin_dir") as mock_get_plugin_dir:
mock_get_plugin_dir.return_value = None
ide_explorer.add_script_section()
scripts_section = ide_explorer.main_explorer.get_section("SCRIPTS")
assert scripts_section is not None
# Should only have Local section
local_section = scripts_section.content_widget.get_section("Local")
shared_section = scripts_section.content_widget.get_section("Shared (Read-only)")
assert local_section is not None
assert shared_section is None
def test_shared_sections_not_added_when_directory_empty(ide_explorer, tmpdir):
"""Test that shared sections are not added when plugin directory doesn't exist on disk"""
ide_explorer.clear()
# Return a path that doesn't exist
nonexistent_path = str(tmpdir.join("nonexistent"))
with mock.patch.object(ide_explorer, "_get_plugin_dir") as mock_get_plugin_dir:
mock_get_plugin_dir.return_value = nonexistent_path
ide_explorer.add_script_section()
scripts_section = ide_explorer.main_explorer.get_section("SCRIPTS")
assert scripts_section is not None
# Should only have Local section since directory doesn't exist
local_section = scripts_section.content_widget.get_section("Local")
shared_section = scripts_section.content_widget.get_section("Shared (Read-only)")
assert local_section is not None
assert shared_section is None
@pytest.mark.parametrize(
"slot, signal, file_name,scope",
[
(
"_emit_file_open_scripts_local",
"file_open_requested",
"example_script.py",
"scripts/local",
),
(
"_emit_file_preview_scripts_local",
"file_preview_requested",
"example_macro.py",
"scripts/local",
),
(
"_emit_file_open_scripts_shared",
"file_open_requested",
"example_script.py",
"scripts/shared",
),
(
"_emit_file_preview_scripts_shared",
"file_preview_requested",
"example_macro.py",
"scripts/shared",
),
],
)
def test_ide_explorer_file_signals(ide_explorer, qtbot, slot, signal, file_name, scope):
"""Test that the correct signals are emitted when files are opened or previewed"""
recv = []
def recv_file_signal(file_name, scope):
recv.append((file_name, scope))
sig = getattr(ide_explorer, signal)
sig.connect(recv_file_signal)
# Call the appropriate slot
getattr(ide_explorer, slot)(file_name)
qtbot.wait(300)
# Verify the signal was emitted with correct arguments
assert recv == [(file_name, scope)]
@pytest.mark.parametrize(
"slot, signal, func_name, file_path,scope",
[
(
"_emit_file_open_macros_local",
"file_open_requested",
"example_macro_function",
"macros/local/example_macro.py",
"macros/local",
),
(
"_emit_file_preview_macros_local",
"file_preview_requested",
"example_macro_function",
"macros/local/example_macro.py",
"macros/local",
),
(
"_emit_file_open_macros_shared",
"file_open_requested",
"example_macro_function",
"macros/shared/example_macro.py",
"macros/shared",
),
(
"_emit_file_preview_macros_shared",
"file_preview_requested",
"example_macro_function",
"macros/shared/example_macro.py",
"macros/shared",
),
],
)
def test_ide_explorer_file_signals_macros(
ide_explorer, qtbot, slot, signal, func_name, file_path, scope
):
"""Test that the correct signals are emitted when macro files are opened or previewed"""
recv = []
def recv_file_signal(file_name, scope):
recv.append((file_name, scope))
sig = getattr(ide_explorer, signal)
sig.connect(recv_file_signal)
# Call the appropriate slot
getattr(ide_explorer, slot)(func_name, file_path)
qtbot.wait(300)
# Verify the signal was emitted with correct arguments
assert recv == [(file_path, scope)]
def test_ide_explorer_add_local_macro(ide_explorer, qtbot, tmpdir):
"""Test adding a local macro through the UI"""
# Create macros section first
ide_explorer.clear()
ide_explorer.sections = ["macros"]
# Set up the local macro directory
local_macros_section = ide_explorer.main_explorer.get_section(
"MACROS"
).content_widget.get_section("Local")
local_macros_section.content_widget.set_directory(str(tmpdir))
with mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QInputDialog.getText",
return_value=("test_macro_function", True),
):
ide_explorer._add_local_macro()
# Check that the macro file was created
expected_file = os.path.join(tmpdir, "test_macro_function.py")
assert os.path.exists(expected_file)
# Check that the file contains the expected function
with open(expected_file, "r") as f:
content = f.read()
assert "def test_macro_function():" in content
assert "test_macro_function macro" in content
def test_ide_explorer_add_local_macro_invalid_name(ide_explorer, qtbot, tmpdir):
"""Test adding a local macro with invalid function name"""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
local_macros_section = ide_explorer.main_explorer.get_section(
"MACROS"
).content_widget.get_section("Local")
local_macros_section.content_widget.set_directory(str(tmpdir))
# Test with invalid function name (starts with number)
with (
mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QInputDialog.getText",
return_value=("123invalid", True),
),
mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QMessageBox.warning"
) as mock_warning,
):
ide_explorer._add_local_macro()
# Should show warning message
mock_warning.assert_called_once()
# Should not create any file
assert len(os.listdir(tmpdir)) == 0
def test_ide_explorer_add_local_macro_file_exists(ide_explorer, qtbot, tmpdir):
"""Test adding a local macro when file already exists"""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
local_macros_section = ide_explorer.main_explorer.get_section(
"MACROS"
).content_widget.get_section("Local")
local_macros_section.content_widget.set_directory(str(tmpdir))
# Create an existing file
existing_file = Path(tmpdir) / "existing_macro.py"
existing_file.write_text("# Existing macro")
with (
mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QInputDialog.getText",
return_value=("existing_macro", True),
),
mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QMessageBox.question",
return_value=QMessageBox.StandardButton.Yes,
) as mock_question,
):
ide_explorer._add_local_macro()
# Should ask for overwrite confirmation
mock_question.assert_called_once()
# File should be overwritten with new content
with open(existing_file, "r") as f:
content = f.read()
assert "def existing_macro():" in content
def test_ide_explorer_add_local_macro_cancelled(ide_explorer, qtbot, tmpdir):
"""Test cancelling the add local macro dialog"""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
local_macros_section = ide_explorer.main_explorer.get_section(
"MACROS"
).content_widget.get_section("Local")
local_macros_section.content_widget.set_directory(str(tmpdir))
# User cancels the dialog
with mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QInputDialog.getText",
return_value=("", False), # User cancelled
):
ide_explorer._add_local_macro()
# Should not create any file
assert len(os.listdir(tmpdir)) == 0
def test_ide_explorer_reload_macros_success(ide_explorer, qtbot):
"""Test successful macro reloading"""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
# Mock the client and macros
mock_client = mock.MagicMock()
mock_macros = mock.MagicMock()
mock_client.macros = mock_macros
ide_explorer.client = mock_client
with mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QMessageBox.information"
) as mock_info:
ide_explorer._reload_macros()
# Should call load_all_user_macros
mock_macros.load_all_user_macros.assert_called_once()
# Should show success message
mock_info.assert_called_once()
assert "successfully" in mock_info.call_args[0][2]
def test_ide_explorer_reload_macros_error(ide_explorer, qtbot):
"""Test macro reloading when an error occurs"""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
# Mock client with macros that raises an exception
mock_client = mock.MagicMock()
mock_macros = mock.MagicMock()
mock_macros.load_all_user_macros.side_effect = Exception("Test error")
mock_client.macros = mock_macros
ide_explorer.client = mock_client
with mock.patch(
"bec_widgets.widgets.utility.ide_explorer.ide_explorer.QMessageBox.critical"
) as mock_critical:
ide_explorer._reload_macros()
# Should show error message
mock_critical.assert_called_once()
assert "Failed to reload macros" in mock_critical.call_args[0][2]
def test_ide_explorer_refresh_macro_file_local(ide_explorer, qtbot, tmpdir):
"""Test refreshing a local macro file"""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
# Set up the local macro directory
local_macros_section = ide_explorer.main_explorer.get_section(
"MACROS"
).content_widget.get_section("Local")
local_macros_section.content_widget.set_directory(str(tmpdir))
# Create a test macro file
macro_file = Path(tmpdir) / "test_macro.py"
macro_file.write_text("def test_function(): pass")
# Mock the refresh_file_item method
with mock.patch.object(
local_macros_section.content_widget, "refresh_file_item"
) as mock_refresh:
ide_explorer.refresh_macro_file(str(macro_file))
# Should call refresh_file_item with the file path
mock_refresh.assert_called_once_with(str(macro_file))
def test_ide_explorer_refresh_macro_file_no_match(ide_explorer, qtbot, tmpdir):
"""Test refreshing a macro file that doesn't match any directory"""
ide_explorer.clear()
ide_explorer.sections = ["macros"]
# Set up the local macro directory
local_macros_section = ide_explorer.main_explorer.get_section(
"MACROS"
).content_widget.get_section("Local")
local_macros_section.content_widget.set_directory(str(tmpdir))
# Try to refresh a file that's not in any macro directory
unrelated_file = "/some/other/path/unrelated.py"
# Mock the refresh_file_item method
with mock.patch.object(
local_macros_section.content_widget, "refresh_file_item"
) as mock_refresh:
ide_explorer.refresh_macro_file(unrelated_file)
# Should not call refresh_file_item
mock_refresh.assert_not_called()
def test_ide_explorer_refresh_macro_file_no_sections(ide_explorer, qtbot):
"""Test refreshing a macro file when no macro sections exist"""
ide_explorer.clear()
# Don't add macros section
# Should handle gracefully without error
ide_explorer.refresh_macro_file("/some/path/test.py")
# Test passes if no exception is raised
+548
View File
@@ -0,0 +1,548 @@
"""
Unit tests for the MacroTreeWidget.
"""
from pathlib import Path
import pytest
from qtpy.QtCore import QEvent, QModelIndex, Qt
from qtpy.QtGui import QMouseEvent
from bec_widgets.utils.toolbars.actions import MaterialIconAction
from bec_widgets.widgets.containers.explorer.macro_tree_widget import MacroTreeWidget
@pytest.fixture
def temp_macro_files(tmpdir):
"""Create temporary macro files for testing."""
macro_dir = Path(tmpdir) / "macros"
macro_dir.mkdir()
# Create a simple macro file with functions
macro_file1 = macro_dir / "test_macros.py"
macro_file1.write_text(
'''
def test_macro_function():
"""A test macro function."""
return "test"
def another_function(param1, param2):
"""Another function with parameters."""
return param1 + param2
class TestClass:
"""This class should be ignored."""
def method(self):
pass
'''
)
# Create another macro file
macro_file2 = macro_dir / "utils_macros.py"
macro_file2.write_text(
'''
def utility_function():
"""A utility function."""
pass
def deprecated_function():
"""Old function."""
return None
'''
)
# Create a file with no functions (should be ignored)
empty_file = macro_dir / "empty.py"
empty_file.write_text(
"""
# Just a comment
x = 1
y = 2
"""
)
# Create a file starting with underscore (should be ignored)
private_file = macro_dir / "_private.py"
private_file.write_text(
"""
def private_function():
return "private"
"""
)
# Create a file with syntax errors
error_file = macro_dir / "error_file.py"
error_file.write_text(
"""
def broken_function(
# Missing closing parenthesis and colon
pass
"""
)
return macro_dir
@pytest.fixture
def macro_tree(qtbot, temp_macro_files):
"""Create a MacroTreeWidget with test macro files."""
widget = MacroTreeWidget()
widget.set_directory(str(temp_macro_files))
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
class TestMacroTreeWidgetInitialization:
"""Test macro tree widget initialization and basic functionality."""
def test_initialization(self, qtbot):
"""Test that the macro tree widget initializes correctly."""
widget = MacroTreeWidget()
qtbot.addWidget(widget)
# Check basic properties
assert widget.tree is not None
assert widget.model is not None
assert widget.delegate is not None
assert widget.directory is None
# Check that tree is configured properly
assert widget.tree.isHeaderHidden()
assert widget.tree.rootIsDecorated()
assert not widget.tree.editTriggers()
def test_set_directory_with_valid_path(self, macro_tree, temp_macro_files):
"""Test setting a valid directory path."""
assert macro_tree.directory == str(temp_macro_files)
# Check that files were loaded
assert macro_tree.model.rowCount() > 0
# Should have 2 files (test_macros.py and utils_macros.py)
# empty.py and _private.py should be filtered out
expected_files = ["test_macros", "utils_macros"]
actual_files = []
for row in range(macro_tree.model.rowCount()):
item = macro_tree.model.item(row)
if item:
actual_files.append(item.text())
# Sort for consistent comparison
actual_files.sort()
expected_files.sort()
for expected in expected_files:
assert expected in actual_files
def test_set_directory_with_invalid_path(self, qtbot):
"""Test setting an invalid directory path."""
widget = MacroTreeWidget()
qtbot.addWidget(widget)
widget.set_directory("/nonexistent/path")
# Should handle gracefully
assert widget.directory == "/nonexistent/path"
assert widget.model.rowCount() == 0
def test_set_directory_with_none(self, qtbot):
"""Test setting directory to None."""
widget = MacroTreeWidget()
qtbot.addWidget(widget)
widget.set_directory(None)
# Should handle gracefully
assert widget.directory is None
assert widget.model.rowCount() == 0
class TestMacroFunctionParsing:
"""Test macro function parsing and AST functionality."""
def test_extract_functions_from_file(self, macro_tree, temp_macro_files):
"""Test extracting functions from a Python file."""
test_file = temp_macro_files / "test_macros.py"
functions = macro_tree._extract_functions_from_file(test_file)
# Should extract 2 functions, not the class method
assert len(functions) == 2
assert "test_macro_function" in functions
assert "another_function" in functions
assert "method" not in functions # Class methods should be excluded
# Check function details
test_func = functions["test_macro_function"]
assert test_func["line_number"] == 2 # First function starts at line 2
assert "A test macro function" in test_func["docstring"]
def test_extract_functions_from_empty_file(self, macro_tree, temp_macro_files):
"""Test extracting functions from a file with no functions."""
empty_file = temp_macro_files / "empty.py"
functions = macro_tree._extract_functions_from_file(empty_file)
assert len(functions) == 0
def test_extract_functions_from_invalid_file(self, macro_tree):
"""Test extracting functions from a non-existent file."""
nonexistent_file = Path("/nonexistent/file.py")
functions = macro_tree._extract_functions_from_file(nonexistent_file)
assert len(functions) == 0
def test_extract_functions_from_syntax_error_file(self, macro_tree, temp_macro_files):
"""Test extracting functions from a file with syntax errors."""
error_file = temp_macro_files / "error_file.py"
functions = macro_tree._extract_functions_from_file(error_file)
# Should return empty dict on syntax error
assert len(functions) == 0
def test_create_file_item(self, macro_tree, temp_macro_files):
"""Test creating a file item from a Python file."""
test_file = temp_macro_files / "test_macros.py"
file_item = macro_tree._create_file_item(test_file)
assert file_item is not None
assert file_item.text() == "test_macros"
assert file_item.rowCount() == 2 # Should have 2 function children
# Check file data
file_data = file_item.data(Qt.ItemDataRole.UserRole)
assert file_data["type"] == "file"
assert file_data["file_path"] == str(test_file)
# Check function children
func_names = []
for row in range(file_item.rowCount()):
child = file_item.child(row)
func_names.append(child.text())
# Check function data
func_data = child.data(Qt.ItemDataRole.UserRole)
assert func_data["type"] == "function"
assert func_data["file_path"] == str(test_file)
assert "function_name" in func_data
assert "line_number" in func_data
assert "test_macro_function" in func_names
assert "another_function" in func_names
def test_create_file_item_with_private_file(self, macro_tree, temp_macro_files):
"""Test that files starting with underscore are ignored."""
private_file = temp_macro_files / "_private.py"
file_item = macro_tree._create_file_item(private_file)
assert file_item is None
def test_create_file_item_with_no_functions(self, macro_tree, temp_macro_files):
"""Test that files with no functions return None."""
empty_file = temp_macro_files / "empty.py"
file_item = macro_tree._create_file_item(empty_file)
assert file_item is None
class TestMacroTreeInteractions:
"""Test macro tree widget interactions and signals."""
def test_item_click_on_function(self, macro_tree, qtbot):
"""Test clicking on a function item."""
# Set up signal spy
macro_selected_signals = []
def on_macro_selected(function_name, file_path):
macro_selected_signals.append((function_name, file_path))
macro_tree.macro_selected.connect(on_macro_selected)
# Find a function item
file_item = macro_tree.model.item(0) # First file
if file_item and file_item.rowCount() > 0:
func_item = file_item.child(0) # First function
func_index = func_item.index()
# Simulate click
macro_tree._on_item_clicked(func_index)
# Check signal was emitted
assert len(macro_selected_signals) == 1
function_name, file_path = macro_selected_signals[0]
assert function_name is not None
assert file_path is not None
assert file_path.endswith(".py")
def test_item_click_on_file(self, macro_tree, qtbot):
"""Test clicking on a file item (should not emit signal)."""
# Set up signal spy
macro_selected_signals = []
def on_macro_selected(function_name, file_path):
macro_selected_signals.append((function_name, file_path))
macro_tree.macro_selected.connect(on_macro_selected)
# Find a file item
file_item = macro_tree.model.item(0)
if file_item:
file_index = file_item.index()
# Simulate click
macro_tree._on_item_clicked(file_index)
# Should not emit signal for file items
assert len(macro_selected_signals) == 0
def test_item_double_click_on_function(self, macro_tree, qtbot):
"""Test double-clicking on a function item."""
# Set up signal spy
open_requested_signals = []
def on_macro_open_requested(function_name, file_path):
open_requested_signals.append((function_name, file_path))
macro_tree.macro_open_requested.connect(on_macro_open_requested)
# Find a function item
file_item = macro_tree.model.item(0)
if file_item and file_item.rowCount() > 0:
func_item = file_item.child(0)
func_index = func_item.index()
# Simulate double-click
macro_tree._on_item_double_clicked(func_index)
# Check signal was emitted
assert len(open_requested_signals) == 1
function_name, file_path = open_requested_signals[0]
assert function_name is not None
assert file_path is not None
def test_hover_events(self, macro_tree, qtbot):
"""Test mouse hover events and action button visibility."""
# Get the tree view and its viewport
tree_view = macro_tree.tree
viewport = tree_view.viewport()
# Initially, no item should be hovered
assert not macro_tree.delegate.hovered_index.isValid()
# Find a function item to hover over
file_item = macro_tree.model.item(0)
if file_item and file_item.rowCount() > 0:
func_item = file_item.child(0)
func_index = func_item.index()
# Get the position of the function item
rect = tree_view.visualRect(func_index)
pos = rect.center()
# Simulate a mouse move event over the item
mouse_event = QMouseEvent(
QEvent.Type.MouseMove,
pos,
tree_view.mapToGlobal(pos),
Qt.MouseButton.NoButton,
Qt.MouseButton.NoButton,
Qt.KeyboardModifier.NoModifier,
)
# Send the event to the viewport
macro_tree.eventFilter(viewport, mouse_event)
qtbot.wait(100)
# Now, the hover index should be set
assert macro_tree.delegate.hovered_index.isValid()
assert macro_tree.delegate.hovered_index == func_index
# Simulate mouse leaving the viewport
leave_event = QEvent(QEvent.Type.Leave)
macro_tree.eventFilter(viewport, leave_event)
qtbot.wait(100)
# After leaving, no item should be hovered
assert not macro_tree.delegate.hovered_index.isValid()
def test_macro_open_action(self, macro_tree, qtbot):
"""Test the macro open action functionality."""
# Set up signal spy
open_requested_signals = []
def on_macro_open_requested(function_name, file_path):
open_requested_signals.append((function_name, file_path))
macro_tree.macro_open_requested.connect(on_macro_open_requested)
# Find a function item and set it as hovered
file_item = macro_tree.model.item(0)
if file_item and file_item.rowCount() > 0:
func_item = file_item.child(0)
func_index = func_item.index()
# Set the delegate's hovered index and current macro info
macro_tree.delegate.set_hovered_index(func_index)
func_data = func_item.data(Qt.ItemDataRole.UserRole)
macro_tree.delegate.current_macro_info = func_data
# Trigger the open action
macro_tree._on_macro_open_requested()
# Check signal was emitted
assert len(open_requested_signals) == 1
function_name, file_path = open_requested_signals[0]
assert function_name is not None
assert file_path is not None
class TestMacroTreeRefresh:
"""Test macro tree refresh functionality."""
def test_refresh(self, macro_tree, temp_macro_files):
"""Test refreshing the entire tree."""
# Get initial count
initial_count = macro_tree.model.rowCount()
# Add a new macro file
new_file = temp_macro_files / "new_macros.py"
new_file.write_text(
'''
def new_function():
"""A new function."""
return "new"
'''
)
# Refresh the tree
macro_tree.refresh()
# Should have one more file
assert macro_tree.model.rowCount() == initial_count + 1
def test_refresh_file_item(self, macro_tree, temp_macro_files):
"""Test refreshing a single file item."""
# Find the test_macros.py file
test_file_path = str(temp_macro_files / "test_macros.py")
# Get initial function count
initial_functions = []
for row in range(macro_tree.model.rowCount()):
item = macro_tree.model.item(row)
if item:
item_data = item.data(Qt.ItemDataRole.UserRole)
if item_data and item_data.get("file_path") == test_file_path:
for child_row in range(item.rowCount()):
child = item.child(child_row)
initial_functions.append(child.text())
break
# Modify the file to add a new function
with open(test_file_path, "a") as f:
f.write(
'''
def newly_added_function():
"""A newly added function."""
return "added"
'''
)
# Refresh just this file
macro_tree.refresh_file_item(test_file_path)
# Check that the new function was added
updated_functions = []
for row in range(macro_tree.model.rowCount()):
item = macro_tree.model.item(row)
if item:
item_data = item.data(Qt.ItemDataRole.UserRole)
if item_data and item_data.get("file_path") == test_file_path:
for child_row in range(item.rowCount()):
child = item.child(child_row)
updated_functions.append(child.text())
break
# Should have the new function
assert len(updated_functions) == len(initial_functions) + 1
assert "newly_added_function" in updated_functions
def test_refresh_nonexistent_file(self, macro_tree):
"""Test refreshing a non-existent file."""
# Should handle gracefully without crashing
macro_tree.refresh_file_item("/nonexistent/file.py")
# Tree should remain unchanged
assert macro_tree.model.rowCount() >= 0 # Just ensure it doesn't crash
def test_expand_collapse_all(self, macro_tree, qtbot):
"""Test expand/collapse all functionality."""
# Initially should be expanded
for row in range(macro_tree.model.rowCount()):
item = macro_tree.model.item(row)
if item:
# Items with children should be expanded after initial load
if item.rowCount() > 0:
assert macro_tree.tree.isExpanded(item.index())
# Collapse all
macro_tree.collapse_all()
qtbot.wait(50)
for row in range(macro_tree.model.rowCount()):
item = macro_tree.model.item(row)
if item and item.rowCount() > 0:
assert not macro_tree.tree.isExpanded(item.index())
# Expand all
macro_tree.expand_all()
qtbot.wait(50)
for row in range(macro_tree.model.rowCount()):
item = macro_tree.model.item(row)
if item and item.rowCount() > 0:
assert macro_tree.tree.isExpanded(item.index())
class TestMacroItemDelegate:
"""Test the custom macro item delegate functionality."""
def test_delegate_action_management(self, qtbot):
"""Test adding and clearing delegate actions."""
widget = MacroTreeWidget()
qtbot.addWidget(widget)
# Should have at least one default action (open)
assert len(widget.delegate.macro_actions) >= 1
# Add a custom action
custom_action = MaterialIconAction(icon_name="edit", tooltip="Edit", parent=widget)
widget.add_macro_action(custom_action.action)
# Should have the additional action
assert len(widget.delegate.macro_actions) >= 2
# Clear actions
widget.clear_actions()
# Should be empty
assert len(widget.delegate.macro_actions) == 0
def test_delegate_hover_index_management(self, qtbot):
"""Test hover index management in the delegate."""
widget = MacroTreeWidget()
qtbot.addWidget(widget)
# Initially no hover
assert not widget.delegate.hovered_index.isValid()
# Create a fake index
fake_index = widget.model.createIndex(0, 0)
# Set hover
widget.delegate.set_hovered_index(fake_index)
assert widget.delegate.hovered_index == fake_index
# Clear hover
widget.delegate.set_hovered_index(QModelIndex())
assert not widget.delegate.hovered_index.isValid()
+425
View File
@@ -0,0 +1,425 @@
import os
from typing import Generator
from unittest import mock
import pytest
from qtpy.QtWidgets import QFileDialog, QMessageBox
from bec_widgets.widgets.editors.monaco.monaco_dock import MonacoDock
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
from .client_mocks import mocked_client
@pytest.fixture
def monaco_dock(qtbot, mocked_client) -> Generator[MonacoDock, None, None]:
"""Create a MonacoDock for testing."""
# Mock the macros functionality
mocked_client.macros = mock.MagicMock()
mocked_client.macros._update_handler = mock.MagicMock()
mocked_client.macros._update_handler.get_macros_from_file.return_value = {}
mocked_client.macros._update_handler.get_existing_macros.return_value = {}
widget = MonacoDock(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
class TestFocusEditor:
def test_last_focused_editor_initial_none(self, monaco_dock: MonacoDock):
"""Test that last_focused_editor is initially None."""
assert monaco_dock.last_focused_editor is not None
def test_set_last_focused_editor(self, qtbot, monaco_dock: MonacoDock, tmpdir):
"""Test setting last_focused_editor when an editor is focused."""
file_path = tmpdir.join("test.py")
file_path.write("print('Hello, World!')")
monaco_dock.open_file(str(file_path))
qtbot.wait(300) # Wait for the editor to be fully set up
assert monaco_dock.last_focused_editor is not None
def test_last_focused_editor_updates_on_focus_change(
self, qtbot, monaco_dock: MonacoDock, tmpdir
):
"""Test that last_focused_editor updates when focus changes."""
file1 = tmpdir.join("file1.py")
file1.write("print('File 1')")
file2 = tmpdir.join("file2.py")
file2.write("print('File 2')")
monaco_dock.open_file(str(file1))
qtbot.wait(300)
editor1 = monaco_dock.last_focused_editor
monaco_dock.open_file(str(file2))
qtbot.wait(300)
editor2 = monaco_dock.last_focused_editor
assert editor1 != editor2
assert editor2 is not None
def test_opening_existing_file_updates_focus(self, qtbot, monaco_dock: MonacoDock, tmpdir):
"""Test that opening an already open file simply switches focus to it."""
file1 = tmpdir.join("file1.py")
file1.write("print('File 1')")
file2 = tmpdir.join("file2.py")
file2.write("print('File 2')")
monaco_dock.open_file(str(file1))
qtbot.wait(300)
editor1 = monaco_dock.last_focused_editor
monaco_dock.open_file(str(file2))
qtbot.wait(300)
editor2 = monaco_dock.last_focused_editor
# Re-open file1
monaco_dock.open_file(str(file1))
qtbot.wait(300)
editor1_again = monaco_dock.last_focused_editor
assert editor1 == editor1_again
assert editor1 != editor2
assert editor2 is not None
class TestSaveFiles:
def test_save_file_existing_file_no_macros(self, qtbot, monaco_dock: MonacoDock, tmpdir):
"""Test saving an existing file that is not a macro."""
# Create a test file
file_path = tmpdir.join("test.py")
file_path.write("print('Hello, World!')")
# Open file in Monaco dock
monaco_dock.open_file(str(file_path))
qtbot.wait(300)
# Get the editor widget and modify content
editor_widget = monaco_dock.last_focused_editor.widget()
assert isinstance(editor_widget, MonacoWidget)
editor_widget.set_text("print('Modified content')")
qtbot.wait(100)
# Verify the editor is marked as modified
assert editor_widget.modified
# Save the file
with mock.patch(
"bec_widgets.widgets.editors.monaco.monaco_dock.QFileDialog.getSaveFileName"
) as mock_dialog:
mock_dialog.return_value = (str(file_path), "Python files (*.py)")
monaco_dock.save_file()
qtbot.wait(100)
# Verify file was saved
saved_content = file_path.read()
assert saved_content == 'print("Modified content")\n'
# Verify editor is no longer marked as modified
assert not editor_widget.modified
def test_save_file_with_macros_scope(self, qtbot, monaco_dock: MonacoDock, tmpdir):
"""Test saving a file with macros scope updates macro handler."""
# Create a test file
file_path = tmpdir.join("test_macro.py")
file_path.write("def test_function(): pass")
# Open file in Monaco dock with macros scope
monaco_dock.open_file(str(file_path), scope="macros")
qtbot.wait(300)
# Get the editor widget and modify content
editor_widget = monaco_dock.last_focused_editor.widget()
editor_widget.set_text("def modified_function(): pass")
qtbot.wait(100)
# Mock macro validation to return True (valid)
with mock.patch.object(monaco_dock, "_validate_macros", return_value=True):
# Mock file dialog to avoid opening actual dialog (file already exists)
with mock.patch.object(QFileDialog, "getSaveFileName") as mock_dialog:
mock_dialog.return_value = (str(file_path), "") # User cancels
# Save the file (should save to existing file, not open dialog)
monaco_dock.save_file()
qtbot.wait(100)
# Verify macro update methods were called
monaco_dock.client.macros._update_handler.get_macros_from_file.assert_called_with(
str(file_path)
)
monaco_dock.client.macros._update_handler.get_existing_macros.assert_called_with(
str(file_path)
)
def test_save_file_invalid_macro_content(self, qtbot, monaco_dock: MonacoDock, tmpdir):
"""Test saving a macro file with invalid content shows warning."""
# Create a test file
file_path = tmpdir.join("test_macro.py")
file_path.write("def test_function(): pass")
# Open file in Monaco dock with macros scope
monaco_dock.open_file(str(file_path), scope="macros")
qtbot.wait(300)
# Get the editor widget and modify content to invalid macro
editor_widget = monaco_dock.last_focused_editor.widget()
assert isinstance(editor_widget, MonacoWidget)
editor_widget.set_text("exec('print(hello)')") # Invalid macro content
qtbot.wait(100)
# Mock QMessageBox to capture warning
with mock.patch(
"bec_widgets.widgets.editors.monaco.monaco_dock.QMessageBox.warning"
) as mock_warning:
with mock.patch.object(QFileDialog, "getSaveFileName") as mock_dialog:
mock_dialog.return_value = (str(file_path), "")
# Save the file
monaco_dock.save_file()
qtbot.wait(100)
# Verify validation was called and warning was shown
mock_warning.assert_called_once()
# Verify file was not saved (content should remain original)
saved_content = file_path.read()
assert saved_content == "def test_function(): pass"
def test_save_file_as_new_file(self, qtbot, monaco_dock: MonacoDock, tmpdir):
"""Test Save As functionality creates a new file."""
# Create initial content in editor
editor_dock = monaco_dock.add_editor()
editor_widget = editor_dock.widget()
assert isinstance(editor_widget, MonacoWidget)
editor_widget.set_text("print('New file content')")
qtbot.wait(100)
# Mock QFileDialog.getSaveFileName
new_file_path = str(tmpdir.join("new_file.py"))
with mock.patch.object(QFileDialog, "getSaveFileName") as mock_dialog:
mock_dialog.return_value = (new_file_path, "Python files (*.py)")
# Save as new file
monaco_dock.save_file(force_save_as=True)
qtbot.wait(100)
# Verify new file was created
assert os.path.exists(new_file_path)
with open(new_file_path, "r", encoding="utf-8") as f:
content = f.read()
assert content == 'print("New file content")\n'
# Verify editor is no longer marked as modified
assert not editor_widget.modified
# Verify current_file was updated
assert editor_widget.current_file == new_file_path
def test_save_file_as_adds_py_extension(self, qtbot, monaco_dock: MonacoDock, tmpdir):
"""Test Save As automatically adds .py extension if none provided."""
# Create initial content in editor
editor_dock = monaco_dock.add_editor()
editor_widget = editor_dock.widget()
assert isinstance(editor_widget, MonacoWidget)
editor_widget.set_text("print('Test content')")
qtbot.wait(100)
# Mock QFileDialog.getSaveFileName to return path without extension
file_path_no_ext = str(tmpdir.join("test_file"))
expected_path = file_path_no_ext + ".py"
with mock.patch.object(QFileDialog, "getSaveFileName") as mock_dialog:
mock_dialog.return_value = (file_path_no_ext, "All files (*)")
# Save as new file
monaco_dock.save_file(force_save_as=True)
qtbot.wait(100)
# Verify file was created with .py extension
assert os.path.exists(expected_path)
assert editor_widget.current_file == expected_path
def test_save_file_no_focused_editor(self, monaco_dock: MonacoDock):
"""Test save_file handles case when no editor is focused."""
# Set last_focused_editor to None
with mock.patch.object(monaco_dock.last_focused_editor, "widget", return_value=None):
# Attempt to save should not raise exception
monaco_dock.save_file()
def test_save_file_emits_macro_file_updated_signal(self, qtbot, monaco_dock, tmpdir):
"""Test that macro_file_updated signal is emitted when saving macro files."""
# Create a test file
file_path = tmpdir.join("test_macro.py")
file_path.write("def test_function(): pass")
# Open file in Monaco dock with macros scope
monaco_dock.open_file(str(file_path), scope="macros")
qtbot.wait(300)
# Get the editor widget and modify content
editor_widget = monaco_dock.last_focused_editor.widget()
editor_widget.set_text("def modified_function(): pass")
qtbot.wait(100)
# Connect signal to capture emission
signal_emitted = []
monaco_dock.macro_file_updated.connect(lambda path: signal_emitted.append(path))
# Mock file dialog to avoid opening actual dialog (file already exists)
with mock.patch.object(QFileDialog, "getSaveFileName") as mock_dialog:
mock_dialog.return_value = (str(file_path), "")
# Save the file
monaco_dock.save_file()
qtbot.wait(100)
# Verify signal was emitted
assert len(signal_emitted) == 1
assert signal_emitted[0] == str(file_path)
def test_close_dock_asks_to_save_modified_file(self, qtbot, monaco_dock: MonacoDock, tmpdir):
"""Test that closing a modified file dock asks to save changes."""
# Create a test file
file_path = tmpdir.join("test.py")
file_path.write("print('Hello, World!')")
# Open file in Monaco dock
monaco_dock.open_file(str(file_path))
qtbot.wait(300)
# Get the editor widget and modify content
editor_widget = monaco_dock.last_focused_editor.widget()
assert isinstance(editor_widget, MonacoWidget)
editor_widget.set_text("print('Modified content')")
qtbot.wait(100)
# Mock QMessageBox to simulate user clicking 'Save'
with mock.patch(
"bec_widgets.widgets.editors.monaco.monaco_dock.QMessageBox.question"
) as mock_question:
mock_question.return_value = QMessageBox.StandardButton.Yes
# Mock QFileDialog.getSaveFileName
with mock.patch.object(QFileDialog, "getSaveFileName") as mock_dialog:
mock_dialog.return_value = (str(file_path), "Python files (*.py)")
# Close the dock; sadly, calling close() alone does not trigger the closeRequested signal
# It is only triggered if the mouse is on top of the tab close button, so we directly call the handler
monaco_dock._on_editor_close_requested(
monaco_dock.last_focused_editor, editor_widget
)
qtbot.wait(100)
# Verify file was saved
saved_content = file_path.read()
assert saved_content == 'print("Modified content")\n'
class TestSignatureHelp:
def test_signature_help_signal_emission(self, qtbot, monaco_dock: MonacoDock):
"""Test that signature help signal is emitted correctly."""
# Connect signal to capture emission
signature_emitted = []
monaco_dock.signature_help.connect(lambda sig: signature_emitted.append(sig))
# Create mock signature data
signature_data = {
"signatures": [
{
"label": "print(value, sep=' ', end='\\n', file=sys.stdout, flush=False)",
"documentation": {
"value": "Print objects to the text stream file, separated by sep and followed by end."
},
}
],
"activeSignature": 0,
"activeParameter": 0,
}
# Trigger signature change
monaco_dock._on_signature_change(signature_data)
qtbot.wait(100)
# Verify signal was emitted with correct markdown format
assert len(signature_emitted) == 1
emitted_signature = signature_emitted[0]
assert "```python" in emitted_signature
assert "print(value, sep=' ', end='\\n', file=sys.stdout, flush=False)" in emitted_signature
assert "Print objects to the text stream file" in emitted_signature
def test_signature_help_empty_signatures(self, qtbot, monaco_dock: MonacoDock):
"""Test signature help with empty signatures."""
# Connect signal to capture emission
signature_emitted = []
monaco_dock.signature_help.connect(lambda sig: signature_emitted.append(sig))
# Create mock signature data with no signatures
signature_data = {"signatures": []}
# Trigger signature change
monaco_dock._on_signature_change(signature_data)
qtbot.wait(100)
# Verify empty string was emitted
assert len(signature_emitted) == 1
assert signature_emitted[0] == ""
def test_signature_help_no_documentation(self, qtbot, monaco_dock: MonacoDock):
"""Test signature help when documentation is missing."""
# Connect signal to capture emission
signature_emitted = []
monaco_dock.signature_help.connect(lambda sig: signature_emitted.append(sig))
# Create mock signature data without documentation
signature_data = {"signatures": [{"label": "function_name(param)"}], "activeSignature": 0}
# Trigger signature change
monaco_dock._on_signature_change(signature_data)
qtbot.wait(100)
# Verify signal was emitted with just the function signature
assert len(signature_emitted) == 1
emitted_signature = signature_emitted[0]
assert "```python" in emitted_signature
assert "function_name(param)" in emitted_signature
def test_signature_help_string_documentation(self, qtbot, monaco_dock: MonacoDock):
"""Test signature help when documentation is a string instead of dict."""
# Connect signal to capture emission
signature_emitted = []
monaco_dock.signature_help.connect(lambda sig: signature_emitted.append(sig))
# Create mock signature data with string documentation
signature_data = {
"signatures": [
{"label": "function_name(param)", "documentation": "Simple string documentation"}
],
"activeSignature": 0,
}
# Trigger signature change
monaco_dock._on_signature_change(signature_data)
qtbot.wait(100)
# Verify signal was emitted with correct format
assert len(signature_emitted) == 1
emitted_signature = signature_emitted[0]
assert "```python" in emitted_signature
assert "function_name(param)" in emitted_signature
assert "Simple string documentation" in emitted_signature
def test_signature_help_connected_to_editor(self, qtbot, monaco_dock: MonacoDock):
"""Test that signature help is connected when creating new editors."""
# Create a new editor
editor_dock = monaco_dock.add_editor()
editor_widget = editor_dock.widget()
# Verify the signal connection exists by checking connected signals
# We do this by mocking the signal and verifying the connection
with mock.patch.object(monaco_dock, "_on_signature_change") as mock_handler:
# Simulate signature help trigger from the editor
editor_widget.editor.signature_help_triggered.emit({"signatures": []})
qtbot.wait(100)
# Verify the handler was called
mock_handler.assert_called_once()
+84 -3
View File
@@ -1,11 +1,20 @@
import pytest
from unittest import mock
import pytest
from bec_lib.endpoints import MessageEndpoints
from bec_widgets.utils.widget_io import WidgetIO
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
from bec_widgets.widgets.editors.monaco.scan_control_dialog import ScanControlDialog
from .client_mocks import mocked_client
from .test_scan_control import available_scans_message
@pytest.fixture
def monaco_widget(qtbot):
widget = MonacoWidget()
def monaco_widget(qtbot, mocked_client):
widget = MonacoWidget(client=mocked_client)
mocked_client.connector.set(MessageEndpoints.available_scans(), available_scans_message)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
@@ -37,3 +46,75 @@ def test_monaco_widget_readonly(monaco_widget: MonacoWidget, qtbot):
monaco_widget.set_text("Attempting to change text")
qtbot.waitUntil(lambda: monaco_widget.get_text() == "Attempting to change text", timeout=1000)
assert monaco_widget.get_text() == "Attempting to change text"
def test_monaco_widget_show_scan_control_dialog(monaco_widget: MonacoWidget, qtbot):
"""
Test that the MonacoWidget can show the scan control dialog.
"""
with mock.patch.object(monaco_widget, "_run_dialog_and_insert_code") as mock_run_dialog:
monaco_widget._show_scan_control_dialog()
mock_run_dialog.assert_called_once()
def test_monaco_widget_get_scan_control_code(monaco_widget: MonacoWidget, qtbot, mocked_client):
"""
Test that the MonacoWidget can get scan control code from the dialog.
"""
mocked_client.connector.set(MessageEndpoints.available_scans(), available_scans_message)
scan_control_dialog = ScanControlDialog(client=mocked_client)
qtbot.addWidget(scan_control_dialog)
qtbot.waitExposed(scan_control_dialog)
qtbot.wait(300)
scan_control = scan_control_dialog.scan_control
scan_name = "grid_scan"
kwargs = {"exp_time": 0.2, "settling_time": 0.1, "relative": False, "burst_at_each_point": 2}
args_row1 = {"device": "samx", "start": -10, "stop": 10, "steps": 20}
args_row2 = {"device": "samy", "start": -5, "stop": 5, "steps": 10}
mock_slot = mock.MagicMock()
scan_control.scan_args.connect(mock_slot)
scan_control.comboBox_scan_selection.setCurrentText(scan_name)
# Ensure there are two rows in the arg_box
current_rows = scan_control.arg_box.count_arg_rows()
required_rows = 2
while current_rows < required_rows:
scan_control.arg_box.add_widget_bundle()
current_rows += 1
# Set kwargs in the UI
for kwarg_box in scan_control.kwarg_boxes:
for widget in kwarg_box.widgets:
if widget.arg_name in kwargs:
WidgetIO.set_value(widget, kwargs[widget.arg_name])
# Set args in the UI for both rows
arg_widgets = scan_control.arg_box.widgets # This is a flat list of widgets
num_columns = len(scan_control.arg_box.inputs)
num_rows = int(len(arg_widgets) / num_columns)
assert num_rows == required_rows # We expect 2 rows for grid_scan
# Set values for first row
for i in range(num_columns):
widget = arg_widgets[i]
arg_name = widget.arg_name
if arg_name in args_row1:
WidgetIO.set_value(widget, args_row1[arg_name])
# Set values for second row
for i in range(num_columns):
widget = arg_widgets[num_columns + i] # Next row
arg_name = widget.arg_name
if arg_name in args_row2:
WidgetIO.set_value(widget, args_row2[arg_name])
scan_control_dialog.accept()
out = scan_control_dialog.get_scan_code()
expected_code = "scans.grid_scan(dev.samx, -10.0, 10.0, 20, dev.samy, -5.0, 5.0, 10, exp_time=0.2, settling_time=0.1, burst_at_each_point=2, relative=False, optim_trajectory=None)"
assert out == expected_code
-22
View File
@@ -1,22 +0,0 @@
import pytest
from qtpy.QtWidgets import QTreeView, QWidget
from bec_widgets.utils.colors import apply_theme
class DummyTree(QWidget):
def __init__(self):
super().__init__()
tree = QTreeView(self)
@pytest.fixture
def tree_widget(qtbot):
tree = DummyTree()
qtbot.addWidget(tree)
qtbot.waitExposed(tree)
yield tree
def test_tree_widget_init(tree_widget):
assert isinstance(tree_widget, QWidget)