diff --git a/rascal2/widgets/__init__.py b/rascal2/widgets/__init__.py
index 92ec968e..dab74dc0 100644
--- a/rascal2/widgets/__init__.py
+++ b/rascal2/widgets/__init__.py
@@ -1,5 +1,12 @@
from rascal2.widgets.controls import ControlsWidget
-from rascal2.widgets.inputs import AdaptiveDoubleSpinBox, MultiSelectComboBox, MultiSelectList, get_validated_input
+from rascal2.widgets.inputs import (
+ AdaptiveDoubleSpinBox,
+ MultiSelectComboBox,
+ MultiSelectList,
+ PathWidget,
+ ProgressButton,
+ get_validated_input,
+)
from rascal2.widgets.plot import PlotWidget
from rascal2.widgets.project.slider_view import SliderViewWidget
from rascal2.widgets.terminal import TerminalWidget
@@ -10,7 +17,9 @@
"get_validated_input",
"MultiSelectComboBox",
"MultiSelectList",
+ "PathWidget",
"PlotWidget",
+ "ProgressButton",
"TerminalWidget",
"SliderViewWidget",
]
diff --git a/tests/conftest.py b/tests/conftest.py
index ff31b498..97160a16 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,7 +1,9 @@
+import os
import tempfile
from pathlib import Path
-from unittest.mock import patch
+from unittest.mock import MagicMock, patch
+os.environ["DELAY_MATLAB_START"] = "1"
import pytest
from PyQt6 import QtCore, QtWidgets
@@ -19,6 +21,11 @@ def global_setting():
return GLOBAL_SETTING
+@pytest.fixture
+def mock_window_view():
+ return MockWindowView()
+
+
@pytest.fixture(scope="session", autouse=True)
def mock_setting(request):
global GLOBAL_SETTING
@@ -38,3 +45,41 @@ def teardown_mock_setting():
target.stop()
request.addfinalizer(teardown_mock_setting)
+
+
+class MockUndoStack:
+ """A mock Undo stack."""
+
+ def __init__(self):
+ self.stack = []
+ self.clean = True
+
+ def push(self, command):
+ self.clean = False
+ command.redo()
+
+ def setClean(self):
+ self.clean = True
+
+ def isClean(self):
+ return self.clean
+
+
+class MockWindowView(QtWidgets.QMainWindow):
+ """A mock MainWindowView class."""
+
+ def __init__(self):
+ super().__init__()
+ self.undo_stack = MockUndoStack()
+ self.presenter = MagicMock()
+ self.controls_widget = MagicMock()
+ self.project_widget = MagicMock()
+ self.terminal_widget = MagicMock()
+ self.plot_widget = MagicMock()
+ self.handle_results = MagicMock()
+ self.settings = MagicMock()
+ self.get_project_folder = lambda: "new path/"
+ self.windowTitle = lambda: "RasCAL2"
+ self.show_message = MagicMock()
+ self.toggle_sliders = MagicMock()
+ self.set_editing_enabled = MagicMock()
diff --git a/tests/core/test_commands.py b/tests/core/test_commands.py
new file mode 100644
index 00000000..7421e937
--- /dev/null
+++ b/tests/core/test_commands.py
@@ -0,0 +1,90 @@
+"""Tests for the undo Command classes."""
+
+from unittest.mock import MagicMock, patch
+
+import pytest
+from ratapi import Controls, Project
+from ratapi.rat_core import ProblemDefinition
+
+from rascal2.core.commands import CommandID, EditControls, EditProject, SaveCalculationOutputs
+from rascal2.ui.presenter import MainWindowPresenter
+
+
+@pytest.fixture
+def presenter(mock_window_view):
+ with (
+ patch("rascal2.ui.presenter.LOGGER", autospec=True),
+ patch("rascal2.ui.model.os.chdir", autospec=True),
+ ):
+ pr = MainWindowPresenter(mock_window_view)
+ results = MagicMock()
+ results.calculationResults.sumChi = 45
+ pr.quick_run = MagicMock(return_value=results)
+ pr.model.controls = Controls()
+ pr.model.project = Project()
+ pr.model.results = None
+ pr.model.result_log = ""
+
+ yield pr
+
+
+def test_edit_controls(presenter):
+ command = EditControls({"procedure": "de", "targetValue": 3}, presenter)
+ assert command.id() == CommandID.EditControls
+ assert presenter.model.controls.procedure == "calculate"
+ assert presenter.model.controls.targetValue == 1
+ command.redo()
+ assert presenter.model.controls.procedure == "de"
+ assert presenter.model.controls.targetValue == 3
+ command.undo()
+ assert presenter.model.controls.procedure == "calculate"
+ assert presenter.model.controls.targetValue == 1
+
+
+def test_edit_project(presenter):
+ command = EditProject({"model": "custom layers"}, presenter)
+ assert command.id() == CommandID.EditProject
+ assert presenter.model.project.model == "standard layers"
+ command.redo()
+ assert presenter.model.project.model == "custom layers"
+ command.undo()
+ assert presenter.model.project.model == "standard layers"
+
+
+def test_edit_project_preview(presenter):
+ command = EditProject({"model": "custom layers"}, presenter, preview=True)
+ command.redo()
+ presenter.quick_run.assert_called_once()
+ assert presenter.model.results.calculationResults.sumChi == 45
+ command.undo()
+ assert presenter.model.results is None
+ command.redo()
+ # confirm quick_run is always done once
+ presenter.quick_run.assert_called_once()
+ assert presenter.model.results.calculationResults.sumChi == 45
+
+ presenter.quick_run.side_effect = ValueError("calculate error")
+ command = EditProject({"model": "custom layers"}, presenter, preview=True)
+ command.redo()
+ # run failed so result is None
+ assert command.new_result is None
+
+
+def test_save_calculation_outputs(presenter):
+ project = ProblemDefinition()
+ project.params = [4.5]
+ results = MagicMock()
+ results.calculationResults.sumChi = 45
+ log = "Stuff happened during calculation"
+ command = SaveCalculationOutputs(project, results, log, presenter)
+ assert presenter.model.project.parameters[0].value == 3
+ assert presenter.model.results is None
+ assert presenter.model.result_log == ""
+ command.redo()
+ assert presenter.model.project.parameters[0].value == 4.5
+ assert presenter.model.results.calculationResults.sumChi == 45
+ assert presenter.model.result_log == log
+ command.undo()
+ assert presenter.model.project.parameters[0].value == 3
+ assert presenter.model.results is None
+ assert presenter.model.result_log == ""
diff --git a/tests/dialogs/test_about_dialog.py b/tests/dialogs/test_about_dialog.py
index f621fb84..1f83b3c3 100644
--- a/tests/dialogs/test_about_dialog.py
+++ b/tests/dialogs/test_about_dialog.py
@@ -8,7 +8,9 @@
@patch("rascal2.dialogs.about_dialog.MatlabHelper", autospec=True)
def test_update_info_works(mock_matlab):
"""Check if `update_rascal_info` adds all necessary information to the dialog."""
- mock_matlab.return_value = MagicMock()
+ helper = MagicMock()
+ mock_matlab.return_value = helper
+ helper.get_matlab_path.return_value = "Test_Path"
parent = QtWidgets.QMainWindow()
about = AboutDialog(parent)
assert about._rascal_label.text() == "information about RASCAL-2"
@@ -17,5 +19,9 @@ def test_update_info_works(mock_matlab):
rascal_info = about._rascal_label.text()
assert "Version" in rascal_info
assert "RasCAL 2" in rascal_info
- assert "Matlab Path:" in rascal_info
+ assert "Matlab Path:
Test_Path" in rascal_info
assert "Log File:" in rascal_info
+
+ helper.get_matlab_path.return_value = ""
+ about.update_rascal_info()
+ assert "Matlab Path: | None" in about._rascal_label.text()
diff --git a/tests/dialogs/test_custom_file_editor.py b/tests/dialogs/test_custom_file_editor.py
index e2189814..e73a7c3f 100644
--- a/tests/dialogs/test_custom_file_editor.py
+++ b/tests/dialogs/test_custom_file_editor.py
@@ -1,6 +1,7 @@
"""Tests for the custom file editor."""
import logging
+import os
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch
@@ -9,15 +10,22 @@
from PyQt6 import Qsci, QtWidgets
from ratapi.utils.enums import Languages
-from rascal2.dialogs.custom_file_editor import CustomFileEditorDialog, edit_file_local, edit_file_matlab
-
-parent = QtWidgets.QMainWindow()
+from rascal2.dialogs.custom_file_editor import (
+ CustomFileEditorDialog,
+ create_new_file,
+ edit_file,
+ edit_file_local,
+ edit_file_matlab,
+)
+from tests.utils import assert_error_logged
@pytest.fixture
def custom_file_dialog():
"""Fixture for a custom file dialog."""
+ parent = QtWidgets.QMainWindow()
dlg = CustomFileEditorDialog(parent)
+ dlg.show = MagicMock()
yield dlg
dlg.reject()
@@ -31,28 +39,24 @@ def temp_file():
f.close()
-@patch("rascal2.dialogs.custom_file_editor.CustomFileEditorDialog.show")
-def test_edit_file_local(exec_mock):
+def test_edit_file_local(custom_file_dialog, mock_window_view):
"""Test that the dialog is executed when edit_file_local() is called on a valid file."""
with tempfile.TemporaryDirectory() as tmp:
file = Path(tmp, "testfile.py")
file.touch()
- edit_file_local(file, Languages.Python, parent)
+ edit_file_local(file, Languages.Python, mock_window_view)
- exec_mock.assert_called_once()
+ custom_file_dialog.show.assert_called_once()
@pytest.mark.parametrize("filepath", ["dir/", "not_there.m"])
-@patch("rascal2.dialogs.custom_file_editor.CustomFileEditorDialog")
-def test_edit_incorrect_file(dialog_mock, filepath, caplog):
+def test_edit_incorrect_file(filepath, caplog, mock_window_view):
"""A logger error should be emitted if a directory or nonexistent file is given to the editor."""
with tempfile.TemporaryDirectory() as tmp:
file = Path(tmp, filepath)
- edit_file_local(file, Languages.Python, parent)
+ edit_file_local(file, Languages.Python, mock_window_view)
- errors = [record for record in caplog.get_records("call") if record.levelno == logging.ERROR]
- assert len(errors) == 1
- assert "Attempted to edit a custom file which does not exist!" in caplog.text
+ assert_error_logged(caplog, "Attempted to edit a custom file which does not exist!")
@patch("rascal2.dialogs.custom_file_editor.MatlabHelper", autospec=True)
@@ -101,9 +105,8 @@ def test_dialog_init(custom_file_dialog, temp_file, language, expected_lexer):
assert custom_file_dialog.editor.text() == "Test text for a test dialog!"
-@patch("rascal2.dialogs.custom_file_editor.LOGGER")
@patch("rascal2.dialogs.custom_file_editor.QtWidgets.QMessageBox")
-def test_dialog_save(mock_msg_box, mock_logger, custom_file_dialog):
+def test_dialog_save(mock_msg_box, caplog, custom_file_dialog):
"""Text changes to the editor are saved to the file when save_file is called."""
temp_file = MagicMock()
temp_file.read_text = MagicMock(return_value="This is a test")
@@ -132,11 +135,11 @@ def test_dialog_save(mock_msg_box, mock_logger, custom_file_dialog):
custom_file_dialog.save_file()
temp_file.write_text.assert_called_once()
assert not custom_file_dialog.is_modified
- custom_file_dialog.unchanged_text = temp_file.write_text.call_args[0]
+ custom_file_dialog.editor.setText("User changed text")
temp_file.write_text = MagicMock(side_effect=OSError)
custom_file_dialog.save_file()
- mock_logger.error.assert_called_once()
+ assert_error_logged(caplog, f"Failed to save custom file to {custom_file_dialog.file}")
mock_msg_box.critical.assert_called_once()
@@ -146,7 +149,6 @@ def test_save_changes_when_opening_file(mock_msg_box, custom_file_dialog, temp_f
custom_file_dialog.open_file(temp_file, Languages.Python)
custom_file_dialog.editor.setText("New test text...")
-
# Opening the same file should not trigger a save warning
custom_file_dialog.open_file(temp_file, Languages.Python)
@@ -167,3 +169,70 @@ def test_save_changes_when_opening_file(mock_msg_box, custom_file_dialog, temp_f
# Changes should be discarded as user selected discard in msg box
custom_file_dialog.open_file(temp_file, Languages.Python)
assert new_file.read_text() == "This is a new file"
+
+
+@pytest.mark.parametrize(
+ "language, file_type, domain",
+ (
+ ["python", "Background", False],
+ ["python", "Model", True],
+ ["python", "Model", False],
+ ["matlab", "Background", False],
+ ["matlab", "Model", True],
+ ["matlab", "Model", False],
+ ),
+)
+@patch("rascal2.dialogs.custom_file_editor.edit_file", autospec=True)
+def test_create_file(mock_edit_file, mock_window_view, caplog, language, file_type, domain):
+ with tempfile.TemporaryDirectory() as tmp:
+ cur_dir = os.getcwd()
+ try:
+ os.chdir(tmp)
+ create_new_file("hello world", language, domain, file_type, mock_window_view)
+ mock_edit_file.assert_called()
+ file = Path("hello_world.py" if language == "python" else "hello_world.m")
+ assert file.is_file()
+ assert file.read_text().find("hello_world(") != -1
+
+ create_new_file("hello world", language, domain, file_type, mock_window_view)
+ # non-unique name so file already exist
+ assert_error_logged(
+ caplog, f"The file ({file.name}) already exists, change custom file name to create a different file."
+ )
+ finally:
+ os.chdir(cur_dir)
+
+
+@pytest.mark.parametrize(
+ "language, file_type, domain",
+ (
+ ["c++", "Background", False],
+ ["c++", "Model", True],
+ ),
+)
+@patch("rascal2.dialogs.custom_file_editor.edit_file", autospec=True)
+def test_create_bad_file_type(mock_edit_file, mock_window_view, caplog, language, file_type, domain):
+ create_new_file("hello world", language, domain, file_type, mock_window_view)
+ mock_edit_file.assert_not_called()
+
+ assert_error_logged(caplog, f"Creating a new file for {language} is not supported.")
+
+
+@patch("rascal2.dialogs.custom_file_editor.SETTINGS", autospec=True)
+@patch("rascal2.dialogs.custom_file_editor.edit_file_matlab", autospec=True)
+@patch("rascal2.dialogs.custom_file_editor.edit_file_local", autospec=True)
+def test_edit_file(mock_edit_file_local, mock_edit_file_matlab, mock_setting, mock_window_view):
+ mock_setting.matlab_as_default_editor = False
+ edit_file("hello", "python", mock_window_view)
+ mock_edit_file_local.assert_called_once()
+
+ mock_edit_file_matlab.return_value = True
+ edit_file("hello", "matlab", mock_window_view)
+ mock_edit_file_matlab.assert_called_once()
+ assert mock_edit_file_local.call_count == 1
+
+ mock_edit_file_matlab.return_value = False # matlab editor failed so fallback on local
+ mock_setting.matlab_as_default_editor = True
+ edit_file("hello", "python", mock_window_view)
+ assert mock_edit_file_matlab.call_count == 2
+ assert mock_edit_file_local.call_count == 2
diff --git a/tests/dialogs/test_settings_dialog.py b/tests/dialogs/test_settings_dialog.py
new file mode 100644
index 00000000..38f9d148
--- /dev/null
+++ b/tests/dialogs/test_settings_dialog.py
@@ -0,0 +1,92 @@
+import platform
+import tempfile
+from pathlib import Path
+from unittest.mock import MagicMock, patch
+
+from rascal2.dialogs.settings_dialog import SettingsDialog
+from rascal2.settings import Settings
+from tests.utils import edit_line_edit_text
+
+
+class FakeSetting:
+ def __init__(self):
+ self.settings = Settings()
+ self.reset_global_settings = MagicMock()
+ self.set_global_settings = MagicMock()
+
+ def __getattribute__(self, name):
+ if name in ["settings", "reset_global_settings", "set_global_settings"]:
+ return object.__getattribute__(self, name)
+ else:
+ return getattr(self.settings, name)
+
+ def __setattr__(self, name, value):
+ if name in ["settings", "reset_global_settings", "set_global_settings"]:
+ object.__setattr__(self, name, value)
+ else:
+ setattr(self.settings, name, value)
+
+
+@patch("rascal2.dialogs.settings_dialog.SETTINGS", new_callable=FakeSetting)
+@patch("rascal2.dialogs.settings_dialog.MatlabHelper")
+def test_setting_dialog(mock_matlab_helper, fake_setting, mock_window_view):
+ mock_matlab_helper.return_value = MagicMock()
+ mock_matlab_helper.return_value.matlab_dir = "matlab_2023a"
+
+ dialog = SettingsDialog(mock_window_view)
+
+ general_tab = dialog.tab_widget.widget(0)
+ old_fontsize = general_tab.widgets["editor_fontsize"].editor.value()
+ new_fontsize = old_fontsize + 5
+ edit_line_edit_text(general_tab.widgets["editor_fontsize"].editor, str(new_fontsize))
+
+ old_live_recalculate = general_tab.widgets["live_recalculate"].editor.isChecked()
+ general_tab.widgets["live_recalculate"].editor.setChecked(not old_live_recalculate)
+
+ dialog.accept_button.click()
+ assert fake_setting.editor_fontsize == new_fontsize
+ assert fake_setting.live_recalculate != old_live_recalculate
+ fake_setting.set_global_settings.assert_called_once()
+
+ dialog.reset_button.click()
+ fake_setting.reset_global_settings.assert_called_once()
+
+
+@patch("rascal2.dialogs.settings_dialog.sys", autospec=True)
+@patch("rascal2.dialogs.settings_dialog.QtWidgets.QFileDialog.getOpenFileName")
+@patch("rascal2.dialogs.settings_dialog.QtWidgets.QFileDialog.getExistingDirectory")
+@patch("rascal2.dialogs.settings_dialog.SETTINGS", new_callable=FakeSetting)
+@patch("rascal2.dialogs.settings_dialog.MatlabHelper")
+def test_matlab_setup(mock_matlab_helper, _fake_setting, mock_get_dir, mock_get_file, mock_sys, mock_window_view):
+ mock_matlab_helper.return_value = MagicMock()
+ matlab_dir = "matlab_2023a"
+ mock_matlab_helper.return_value.matlab_dir = matlab_dir
+
+ dialog = SettingsDialog(mock_window_view)
+ assert dialog.matlab_tab.matlab_path.text() == matlab_dir
+
+ matlab_dir = "matlab_2024b.app" if platform.system() == "Darwin" else "matlab_2024b"
+ mock_get_file.return_value = (matlab_dir,)
+ mock_get_dir.return_value = matlab_dir
+ assert not dialog.matlab_tab.changed
+ dialog.matlab_tab.open_folder_selector()
+ assert dialog.matlab_tab.changed
+ assert dialog.matlab_tab.matlab_path.text() == matlab_dir
+
+ with patch("rascal2.dialogs.settings_dialog.MATLAB_ARCH_FILE", new=""):
+ dialog.matlab_tab.set_matlab_paths()
+ mock_matlab_helper.return_value.async_start.assert_called_once()
+
+ with tempfile.TemporaryDirectory() as tmp:
+ file = Path(tmp, "_arch.txt")
+ file.write_text("arch file")
+ with patch("rascal2.dialogs.settings_dialog.MATLAB_ARCH_FILE", new=file.as_posix()):
+ dialog.matlab_tab.set_matlab_paths()
+ assert mock_matlab_helper.return_value.async_start.call_count == 1
+
+ mock_sys.frozen = True
+ dialog.matlab_tab.set_matlab_paths()
+ arch_content = file.read_text().split("\n")
+ assert len(arch_content) == 5
+ assert arch_content[-1] == ""
+ assert mock_matlab_helper.return_value.async_start.call_count == 2
diff --git a/tests/core/test_settings.py b/tests/test_settings.py
similarity index 100%
rename from tests/core/test_settings.py
rename to tests/test_settings.py
diff --git a/tests/ui/test_presenter.py b/tests/ui/test_presenter.py
index 0d85e053..da571b37 100644
--- a/tests/ui/test_presenter.py
+++ b/tests/ui/test_presenter.py
@@ -4,7 +4,6 @@
import pytest
from pydantic import ValidationError
-from PyQt6 import QtWidgets
from ratapi import Controls
from ratapi.events import ProgressEventData
from ratapi.inputs import ProblemDefinition
@@ -13,48 +12,13 @@
from rascal2.ui.presenter import MainWindowPresenter
-class MockUndoStack:
- """A mock Undo stack."""
-
- def __init__(self):
- self.stack = []
- self.clean = True
-
- def push(self, command):
- self.clean = False
- command.redo()
-
- def setClean(self):
- self.clean = True
-
- def isClean(self):
- return self.clean
-
-
-class MockWindowView(QtWidgets.QMainWindow):
- """A mock MainWindowView class."""
-
- def __init__(self):
- super().__init__()
- self.undo_stack = MockUndoStack()
- self.controls_widget = MagicMock()
- self.project_widget = MagicMock()
- self.terminal_widget = MagicMock()
- self.plot_widget = MagicMock()
- self.handle_results = MagicMock()
- self.settings = MagicMock()
- self.get_project_folder = lambda: "new path/"
- self.windowTitle = lambda: "RasCAL2"
- self.show_message = MagicMock()
-
-
@pytest.fixture
-def presenter():
+def presenter(mock_window_view):
with (
patch("rascal2.ui.presenter.LOGGER", autospec=True) as mock_log,
patch("rascal2.ui.model.os.chdir", autospec=True),
):
- pr = MainWindowPresenter(MockWindowView())
+ pr = MainWindowPresenter(mock_window_view)
pr.runner = MagicMock()
pr.model.controls = Controls()
pr.model.project = MagicMock()
diff --git a/tests/utils.py b/tests/utils.py
index 0a5fd8a9..979ebb46 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,5 +1,8 @@
+import logging
+
import numpy as np
import ratapi.outputs
+from PyQt6 import QtCore, QtTest
def check_results_equal(actual_results, expected_results) -> None:
@@ -90,3 +93,32 @@ def check_bayes_fields_equal(actual_results, expected_results) -> None:
)
assert (actual_results.chain == expected_results.chain).all()
+
+
+def edit_line_edit_text(line_edit, text):
+ """Clear and edit of a line edit.
+
+ line_edit: QtWidgets.QLineEdit
+ widget to edit
+ text: str
+ The new text
+ """
+ QtTest.QTest.keyClick(line_edit, QtCore.Qt.Key.Key_A, QtCore.Qt.KeyboardModifier.ControlModifier)
+ QtTest.QTest.keyClicks(line_edit, text)
+ QtTest.QTest.keyClick(line_edit, QtCore.Qt.Key.Key_Enter)
+ QtTest.QTest.qWait(100)
+
+
+def assert_error_logged(caplog, last_error_message, expected_error_count=1):
+ """Assert an exception was logged.
+
+ caplog: caplog
+ pytest caplog
+ last_error_message: str
+ The partial or full error message
+ expected_error_count: int
+ number of expected errors
+ """
+ errors = [record for record in caplog.get_records("call") if record.levelno == logging.ERROR]
+ assert len(errors) == expected_error_count
+ assert last_error_message in caplog.text
diff --git a/tests/widgets/project/test_project.py b/tests/widgets/project/test_project.py
index 493e4b43..075ae747 100644
--- a/tests/widgets/project/test_project.py
+++ b/tests/widgets/project/test_project.py
@@ -7,12 +7,7 @@
from ratapi.utils.enums import Calculations, Geometries, LayerModels
from rascal2.widgets.project.project import ProjectTabWidget, ProjectWidget, create_draft_project
-from rascal2.widgets.project.tables import (
- ClassListTableModel,
- ParameterFieldWidget,
- ParametersModel,
- ProjectFieldWidget,
-)
+from rascal2.widgets.project.tables import ParameterFieldWidget, ProjectFieldWidget
class MockModel(QtCore.QObject):
@@ -31,15 +26,6 @@ def __init__(self):
self.edit_project = MagicMock()
-class MockMainWindow(QtWidgets.QMainWindow):
- def __init__(self):
- super().__init__()
- self.presenter = MockPresenter()
- self.controls_widget = MagicMock()
- self.project_widget = None
- self.toggle_sliders = MagicMock()
-
-
class DataModel(pydantic.BaseModel, validate_assignment=True):
"""Test Pydantic model."""
@@ -47,9 +33,6 @@ class DataModel(pydantic.BaseModel, validate_assignment=True):
value: int = 15
-parent = MockMainWindow()
-
-
@pytest.fixture
def classlist():
"""Test ClassList."""
@@ -57,23 +40,17 @@ def classlist():
@pytest.fixture
-def table_model(classlist):
- """Test ClassListTableModel."""
- return ClassListTableModel(classlist, parent)
-
-
-@pytest.fixture
-def setup_project_widget():
- parent = MockMainWindow()
- project_widget = ProjectWidget(parent)
+def project_widget(mock_window_view):
+ mock_window_view.presenter = MockPresenter()
+ project_widget = ProjectWidget(mock_window_view)
project_widget.update_project_view()
return project_widget
@pytest.fixture
-def project_with_draft():
+def project_with_draft(mock_window_view):
draft = create_draft_project(ratapi.Project())
- project = ProjectWidget(parent)
+ project = ProjectWidget(mock_window_view)
project.draft_project = draft
return project
@@ -91,19 +68,8 @@ def _classlist(protected_indices):
return _classlist
-@pytest.fixture
-def param_model(param_classlist):
- def _param_model(protected_indices):
- model = ParametersModel(param_classlist(protected_indices), parent)
- return model
-
- return _param_model
-
-
-def test_project_widget_initial_state(setup_project_widget):
- """Tests the inital state of the ProjectWidget class."""
- project_widget = setup_project_widget
-
+def test_project_widget_initial_state(project_widget):
+ """Tests the initial state of the ProjectWidget class."""
# Check the layout of the project view
assert project_widget.stacked_widget.currentIndex() == 0
@@ -146,10 +112,8 @@ def test_project_widget_initial_state(setup_project_widget):
assert project_widget.edit_project_tab.currentIndex() == 0
-def test_edit_cancel_button_toggle(setup_project_widget):
+def test_edit_cancel_button_toggle(project_widget):
"""Tests clicking the edit button causes the stacked widget to change state."""
- project_widget = setup_project_widget
-
assert project_widget.stacked_widget.currentIndex() == 0
project_widget.edit_project_button.click()
assert project_widget.stacked_widget.currentIndex() == 1
@@ -166,10 +130,24 @@ def test_edit_cancel_button_toggle(setup_project_widget):
assert project_widget.calculation_type.text() == Calculations.Normal
-def test_save_changes_to_model_project(setup_project_widget):
- """Tests that making changes to the project settings."""
- project_widget = setup_project_widget
+def test_show_slider_view(project_widget):
+ assert project_widget.stacked_widget.currentIndex() == 0
+ project_widget.show_slider_view()
+ assert project_widget.stacked_widget.currentIndex() == 2
+ slider_view = project_widget.stacked_widget.currentWidget()
+ assert len(slider_view.parameters) == 1
+ project_widget.parent_model.project.parameters.append(name="test", fit=True)
+ project_widget.update_slider_view()
+
+ # show slider creates a new slider
+ project_widget.show_slider_view()
+ slider_view_2 = project_widget.stacked_widget.currentWidget()
+ assert len(slider_view_2.parameters) == 2
+
+
+def test_save_changes_to_model_project(project_widget):
+ """Tests that making changes to the project settings."""
project_widget.edit_project_button.click()
project_widget.calculation_combobox.setCurrentText(Calculations.Domains)
@@ -184,10 +162,8 @@ def test_save_changes_to_model_project(setup_project_widget):
assert project_widget.parent.presenter.edit_project.call_count == 1
-def test_cancel_changes_to_model_project(setup_project_widget):
+def test_cancel_changes_to_model_project(project_widget):
"""Tests that making changes to the project settings and not saving them reverts the changes."""
- project_widget = setup_project_widget
-
project_widget.edit_project_button.click()
project_widget.calculation_combobox.setCurrentText(Calculations.Domains)
@@ -209,9 +185,8 @@ def test_cancel_changes_to_model_project(setup_project_widget):
assert project_widget.geometry_type.text() == Geometries.AirSubstrate
-def test_domains_tab(setup_project_widget):
+def test_domains_tab(project_widget):
"""Tests that domain tab is visible."""
- project_widget = setup_project_widget
project_widget.edit_project_button.click()
project_widget.calculation_combobox.setCurrentText(Calculations.Domains)
assert project_widget.draft_project["calculation"] == Calculations.Domains
@@ -222,11 +197,11 @@ def test_domains_tab(setup_project_widget):
assert project_widget.edit_project_tab.isTabVisible(domains_tab_index)
-def test_project_tab_init():
+def test_project_tab_init(mock_window_view):
"""Test that the project tab correctly creates field widgets."""
fields = ["my_field", "parameters", "bulk_in"]
- tab = ProjectTabWidget(fields, parent)
+ tab = ProjectTabWidget(fields, mock_window_view)
for field in fields:
if field in ratapi.project.parameter_class_lists:
@@ -236,11 +211,11 @@ def test_project_tab_init():
@pytest.mark.parametrize("edit_mode", [True, False])
-def test_project_tab_update_model(classlist, param_classlist, edit_mode):
+def test_project_tab_update_model(classlist, param_classlist, edit_mode, mock_window_view):
"""Test that updating a ProjectTabEditWidget produces the desired models."""
new_model = {"my_field": classlist, "parameters": param_classlist([])}
- tab = ProjectTabWidget(list(new_model), parent, edit_mode=edit_mode)
+ tab = ProjectTabWidget(list(new_model), mock_window_view, edit_mode=edit_mode)
# change the parent to a mock to avoid spec issues
for table in tab.tables.values():
table.parent = MagicMock()
@@ -260,7 +235,7 @@ def test_project_tab_update_model(classlist, param_classlist, edit_mode):
],
)
@pytest.mark.parametrize("absorption", [True, False])
-def test_project_tab_validate_layers(input_params, absorption):
+def test_project_tab_validate_layers(input_params, absorption, mock_window_view):
"""Test that the project tab produces the correct result for validating the layers tab."""
params = ["Param 1", "Param 2", "Invalid Param", ""]
if absorption:
@@ -300,7 +275,7 @@ def test_project_tab_validate_layers(input_params, absorption):
]
)
- project = ProjectWidget(parent)
+ project = ProjectWidget(mock_window_view)
project.draft_project = draft
assert list(project.validate_layers()) == expected_err
diff --git a/tests/widgets/project/test_slider_view.py b/tests/widgets/project/test_slider_view.py
index cb7270c3..2aea0ba9 100644
--- a/tests/widgets/project/test_slider_view.py
+++ b/tests/widgets/project/test_slider_view.py
@@ -4,7 +4,6 @@
import ratapi
from PyQt6 import QtWidgets
-from rascal2.ui.view import MainWindowView
from rascal2.widgets.project.project import create_draft_project
from rascal2.widgets.project.slider_view import LabeledSlider, SliderViewWidget
@@ -52,22 +51,20 @@ def draft_project():
return draft
-def test_no_sliders_creation():
+def test_no_sliders_creation(mock_window_view):
"""Slider view should show warning when there is no fitted parameter."""
- mw = MainWindowView()
draft = create_draft_project(ratapi.Project())
draft["parameters"][0].fit = False
- slider_view = SliderViewWidget(draft, mw)
+ slider_view = SliderViewWidget(draft, mock_window_view)
assert len(slider_view.parameters) == 0
assert len(slider_view._sliders) == 0
label = slider_view.slider_content_layout.takeAt(0).widget()
assert label.text().startswith("There are no fitted parameters")
-def test_sliders_creation(draft_project):
+def test_sliders_creation(draft_project, mock_window_view):
"""Sliders should be created for fitted parameter only."""
- mw = MainWindowView()
- slider_view = SliderViewWidget(draft_project, mw)
+ slider_view = SliderViewWidget(draft_project, mock_window_view)
assert len(slider_view.parameters) == 8
assert len(slider_view._sliders) == 8
@@ -76,30 +73,30 @@ def test_sliders_creation(draft_project):
assert param_name == slider_name
draft_project["parameters"][0].fit = False
- slider_view = SliderViewWidget(draft_project, mw)
+ slider_view = SliderViewWidget(draft_project, mock_window_view)
assert len(slider_view.parameters) == 7
assert draft_project["parameters"][0].name not in slider_view._sliders
-def test_accept_and_cancel_slider_buttons():
- mw = MainWindowView()
+def test_accept_and_cancel_slider_buttons(mock_window_view):
+ mock_window_view.presenter = MagicMock()
draft = create_draft_project(ratapi.Project())
- mw.toggle_sliders = MagicMock()
- mw.plot_widget.update_plots = MagicMock()
- mw.presenter.edit_project = MagicMock()
+ mock_window_view.toggle_sliders = MagicMock()
+ mock_window_view.plot_widget.update_plots = MagicMock()
+ mock_window_view.presenter.edit_project = MagicMock()
- slider_view = SliderViewWidget(draft, mw)
+ slider_view = SliderViewWidget(draft, mock_window_view)
buttons = slider_view.findChildren(QtWidgets.QPushButton)
accept_button = buttons[0]
accept_button.click()
- mw.toggle_sliders.assert_called_once()
- mw.presenter.edit_project.assert_called_once_with(draft)
+ mock_window_view.toggle_sliders.assert_called_once()
+ mock_window_view.presenter.edit_project.assert_called_once_with(draft)
- mw.toggle_sliders.reset_mock()
+ mock_window_view.toggle_sliders.reset_mock()
cancel_button = buttons[1]
cancel_button.click()
- mw.toggle_sliders.assert_called_once()
- mw.plot_widget.update_plots.assert_called_once()
+ mock_window_view.toggle_sliders.assert_called_once()
+ mock_window_view.plot_widget.update_plots.assert_called_once()
@pytest.mark.parametrize(
@@ -110,9 +107,12 @@ def test_accept_and_cancel_slider_buttons():
ratapi.models.Parameter(name="Param 3", min=3, max=3, value=3, fit=True),
],
)
-@patch("rascal2.widgets.project.slider_view.SliderViewWidget", autospec=True)
-def test_labelled_slider_value(slider_view, param):
- slider_view.update_result_and_plots = MagicMock()
+@patch("rascal2.widgets.project.slider_view.LOGGER")
+def test_labelled_slider_value(mock_logger, param, mock_window_view):
+ draft = create_draft_project(ratapi.Project())
+ mock_window_view.presenter = MagicMock()
+ slider_view = SliderViewWidget(draft, mock_window_view)
+
slider = LabeledSlider(param, slider_view)
# actual range of the slider should never change but
# value would be scaled to parameter range.
@@ -122,4 +122,9 @@ def test_labelled_slider_value(slider_view, param):
slider._slider.setValue(79)
assert param.value == slider._slider_value_to_param_value(slider._slider.value())
- slider_view.update_result_and_plots.assert_called_once()
+ mock_window_view.presenter.quick_run.assert_called_once()
+ mock_window_view.plot_widget.reflectivity_plot.plot.assert_called_once()
+
+ mock_window_view.presenter.quick_run.side_effect = ValueError("calculate error")
+ slider._slider.setValue(90)
+ mock_logger.error.assert_called_once()
diff --git a/tests/widgets/test_controls.py b/tests/widgets/test_controls.py
index a17b7e4a..2203f306 100644
--- a/tests/widgets/test_controls.py
+++ b/tests/widgets/test_controls.py
@@ -10,25 +10,11 @@
from rascal2.widgets.controls import ControlsWidget, FitSettingsWidget
-class MockWindowView(QtWidgets.QMainWindow):
- """A mock MainWindowView class."""
-
- def __init__(self):
- super().__init__()
- self.presenter = MagicMock()
- self.presenter.model = MagicMock()
- self.presenter.model.controls = Controls()
- self.project_widget = MagicMock()
- self.set_editing_enabled = MagicMock()
-
-
-view = MockWindowView()
-
-
@pytest.fixture
-def controls_widget() -> ControlsWidget:
+def controls_widget(mock_window_view) -> ControlsWidget:
def _widget():
- widget = ControlsWidget(view)
+ mock_window_view.presenter.model.controls = Controls()
+ widget = ControlsWidget(mock_window_view)
widget.setup_controls()
return widget
diff --git a/tests/widgets/test_inputs.py b/tests/widgets/test_inputs.py
index 6fa7b78d..8143aa5b 100644
--- a/tests/widgets/test_inputs.py
+++ b/tests/widgets/test_inputs.py
@@ -11,8 +11,14 @@
from pydantic.fields import FieldInfo
from PyQt6 import QtWidgets
-from rascal2.widgets import AdaptiveDoubleSpinBox, MultiSelectComboBox, MultiSelectList, get_validated_input
-from rascal2.widgets.inputs import PathWidget
+from rascal2.widgets import (
+ AdaptiveDoubleSpinBox,
+ MultiSelectComboBox,
+ MultiSelectList,
+ PathWidget,
+ ProgressButton,
+ get_validated_input,
+)
class MyEnum(StrEnum):
@@ -86,3 +92,23 @@ def test_path_widget():
widget.setText(path)
assert widget.path == path.parent.as_posix()
assert widget.text() == path.name
+
+
+def test_progress_button():
+ widget = ProgressButton("Progress", "Testing button")
+
+ assert widget.text() == "Progress"
+ widget.default_text = "Start"
+ assert widget.text() == "Start"
+
+ widget.click()
+ assert not widget.isEnabled()
+ assert widget.text() == "Testing button ..."
+
+ widget.update_progress(1, 2)
+ assert not widget.isEnabled()
+ assert widget.text() == "Testing button - 1 of 2"
+
+ widget.hide_progress()
+ assert widget.isEnabled()
+ assert widget.text() == "Start"
diff --git a/tests/widgets/test_plot.py b/tests/widgets/test_plot.py
index 49ff3e95..0df35168 100644
--- a/tests/widgets/test_plot.py
+++ b/tests/widgets/test_plot.py
@@ -12,29 +12,17 @@
)
-class MockWindowView(QtWidgets.QMainWindow):
- """A mock MainWindowView class."""
-
- def __init__(self):
- super().__init__()
- self.presenter = MagicMock()
- self.presenter.model = MagicMock()
-
-
-view = MockWindowView()
-
-
@pytest.fixture
-def plot_widget():
- plot_widget = PlotWidget(view)
+def plot_widget(mock_window_view):
+ plot_widget = PlotWidget(mock_window_view)
plot_widget.reflectivity_plot = MagicMock()
return plot_widget
@pytest.fixture
-def sld_widget():
- sld_widget = RefSLDWidget(view)
+def sld_widget(mock_window_view):
+ sld_widget = RefSLDWidget(mock_window_view)
sld_widget.canvas = MagicMock()
sld_widget.update_figure_size = MagicMock()
@@ -42,8 +30,8 @@ def sld_widget():
@pytest.fixture
-def shaded_plot_widget():
- shaded_plot_widget = ShadedPlotWidget(view)
+def shaded_plot_widget(mock_window_view):
+ shaded_plot_widget = ShadedPlotWidget(mock_window_view)
shaded_plot_widget.canvas = MagicMock()
return shaded_plot_widget
@@ -171,11 +159,11 @@ def test_ref_sld_plot(mock_inputs, sld_widget):
sld_widget.canvas.draw.assert_called_once()
-def test_param_combobox_items(mock_bayes_results):
+def test_param_combobox_items(mock_bayes_results, mock_window_view):
"""Test that the parameter multi-select combobox items are the full set of fit parameters."""
bayes_results = mock_bayes_results(["A", "B", "C"])
- widget = MockPanelPlot(view)
+ widget = MockPanelPlot(mock_window_view)
widget.plot(None, bayes_results)
assert widget.all_params == ["A", "B", "C"]
@@ -188,11 +176,11 @@ def test_param_combobox_items(mock_bayes_results):
@pytest.mark.parametrize("init_select", ([], ["A", "C"], ["B"], ["A", "B", "C"]))
-def test_param_combobox_select(mock_bayes_results, init_select):
+def test_param_combobox_select(mock_bayes_results, init_select, mock_window_view):
"""Test that the select button correctly selects all parameters."""
bayes_results = mock_bayes_results(["A", "B", "C"])
- widget = MockPanelPlot(view)
+ widget = MockPanelPlot(mock_window_view)
widget.plot(None, bayes_results)
widget.param_combobox.select_items(init_select)
@@ -211,11 +199,11 @@ def test_param_combobox_select(mock_bayes_results, init_select):
@pytest.mark.parametrize("init_select", ([], ["A", "C"], ["B"], ["A", "B", "C"]))
-def test_param_combobox_deselect(mock_bayes_results, init_select):
+def test_param_combobox_deselect(mock_bayes_results, init_select, mock_window_view):
"""Test that the select button correctly selects all parameters."""
bayes_results = mock_bayes_results(["A", "B", "C"])
- widget = MockPanelPlot(view)
+ widget = MockPanelPlot(mock_window_view)
widget.plot(None, bayes_results)
widget.param_combobox.select_items(init_select)
|