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..a50fc024 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,40 @@ 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()
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/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..d99d664f 100644
--- a/tests/ui/test_presenter.py
+++ b/tests/ui/test_presenter.py
@@ -49,12 +49,12 @@ def __init__(self):
@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/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_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"
|