1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-12-29 18:31:17 +01:00
Files
bec_widgets/tests/unit_tests/test_monaco_dock.py

426 lines
17 KiB
Python

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()