diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6883588..0391563 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,11 +33,11 @@ To install pytest use Run the tests and generate a coverage report with - pytest tests --cov=RAT + pytest tests --cov=rascal2 The coverage report can be saved to the directory htmlcov by running the tests with - pytest tests --cov-report html --cov=RAT + pytest tests --cov-report html --cov=rascal2 For information on other coverage report formats, see https://pytest-cov.readthedocs.io/en/latest/reporting.html diff --git a/rascal2/dialogs/project_dialog.py b/rascal2/dialogs/project_dialog.py index e20d627..d6373c2 100644 --- a/rascal2/dialogs/project_dialog.py +++ b/rascal2/dialogs/project_dialog.py @@ -23,9 +23,14 @@ class ProjectDialog(QtWidgets.QDialog): _error_style = "color: #E34234" _line_edit_error_style = "border: 1px solid #E34234" - def __init__(self, parent=None): + def __init__(self, parent): """ Initializes the dialog + + Parameters + ---------- + parent: MainWindowView + An instance of the MainWindowView """ super().__init__(parent) @@ -86,7 +91,7 @@ def create_project_dialog_labels(self) -> None: self.project_name = QtWidgets.QLineEdit(self) self.project_name.setPlaceholderText("Enter project name") - self.project_name_error = QtWidgets.QLabel("", self) + self.project_name_error = QtWidgets.QLabel("Project name needs to be specified.", self) self.project_name_error.setStyleSheet(self._error_style) self.project_name_error.hide() @@ -99,7 +104,7 @@ def create_project_dialog_labels(self) -> None: self.project_folder.setReadOnly(True) self.project_folder.setPlaceholderText("Select project folder") - self.project_folder_error = QtWidgets.QLabel("", self) + self.project_folder_error = QtWidgets.QLabel("An empty project folder needs to be selected.", self) self.project_folder_error.setStyleSheet(self._error_style) self.project_folder_error.hide() @@ -126,10 +131,7 @@ def cancel_project_creation(self) -> None: """ Cancel project creation. """ - if self.parent().new_project_action_called: - self.close() - else: - self.parent().toggleView() + self.close() if self.parent().new_project_action_called else self.parent().toggleView() self.reset_variables() def reset_variables(self) -> None: @@ -149,12 +151,12 @@ def open_folder_selector(self) -> None: if folder_path: files_in_folder = list(filter(lambda x: x[0] != ".", os.listdir(folder_path))) if files_in_folder: - self.handle_error("folder", "Selected folder contains files. Select an empty folder.") + self.handle_error("folder") else: self.handle_non_error("folder") self.project_folder.setText(folder_path) - def handle_error(self, tag: Literal["name", "folder"], msg: str) -> None: + def handle_error(self, tag: Literal["name", "folder"]) -> None: """ Displays the line edit error msgs. @@ -163,16 +165,11 @@ def handle_error(self, tag: Literal["name", "folder"], msg: str) -> None: tag: Literal["name", "folder"] Specifies whether the input verification is for the project name or folder. - msg: str - Specifies the msg to display. """ input = getattr(self, "project_" + tag) input.setStyleSheet(self._line_edit_error_style) - error_label = getattr(self, "project_" + tag + "_error") - error_label.setText(msg) error_label.show() - setattr(self, tag + "_error", True) def handle_non_error(self, tag: Literal["name", "folder"]) -> None: @@ -187,13 +184,11 @@ def handle_non_error(self, tag: Literal["name", "folder"]) -> None: """ input = getattr(self, "project_" + tag) input.setStyleSheet("") - error_label = getattr(self, "project_" + tag + "_error") error_label.hide() - setattr(self, tag + "_error", False) - def verify_inputs(self, tag: Literal["name", "folder"], msg: str) -> None: + def verify_inputs(self, tag: Literal["name", "folder"]) -> None: """ Verifies the name and folder inputs of the project dialog. @@ -202,12 +197,9 @@ def verify_inputs(self, tag: Literal["name", "folder"], msg: str) -> None: tag: Literal["name", "folder"] Specifies whether the input verification is for the project name or folder. - msg: str - Specifies the msg to display if there is an error - in verification. """ if getattr(self, "project_" + tag).text() == "": - self.handle_error(tag, msg) + self.handle_error(tag) else: self.handle_non_error(tag) @@ -215,14 +207,7 @@ def create_project(self) -> None: """ Verifies the inputs specified. """ - self.verify_inputs("name", "Project name needs to be specified.") - self.verify_inputs("folder", "An empty project folder needs to be selected.") + self.verify_inputs("name") + self.verify_inputs("folder") if not self.name_error and not self.folder_error: self.parent().createNewProject() - - def resizeEvent(self, _) -> None: - """ - Recenters the dialog when project folder is selected. - """ - if self.parent(): - self.move(self.parent().geometry().center() - self.rect().center()) diff --git a/rascal2/widgets/startup_widget.py b/rascal2/widgets/startup_widget.py index 4738ec3..a55d6d2 100644 --- a/rascal2/widgets/startup_widget.py +++ b/rascal2/widgets/startup_widget.py @@ -17,9 +17,14 @@ class StartUpWidget(QtWidgets.QWidget): _label_style = "font-weight: bold; font-size: 1em;" - def __init__(self, parent=None): + def __init__(self, parent): """ Initializes the widget + + Parameters + ---------- + parent: MainWindowView + An instance of the MainWindowView """ super().__init__(parent) @@ -29,7 +34,7 @@ def __init__(self, parent=None): self.add_widgets_to_startup_page_layout() - def add_widgets_to_startup_page_layout(self): + def add_widgets_to_startup_page_layout(self) -> None: """ Adds the widgets to the startup layout. """ @@ -55,7 +60,7 @@ def add_widgets_to_startup_page_layout(self): self.setLayout(startup_layout) - def create_banner_and_footer(self): + def create_banner_and_footer(self) -> None: """ Creates the banner and footer for the startup page. """ @@ -69,13 +74,19 @@ def create_banner_and_footer(self): self.footer_label.setPixmap(QtGui.QPixmap(path_for("footer.png"))) self.footer_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - def create_buttons_for_startup_page(self): + def toggle_view(self) -> None: + """ + Shows the project dialog + """ + self.parent().toggleView() + + def create_buttons_for_startup_page(self) -> None: """ Creates the buttons for the startup page. """ self.new_project_button = QtWidgets.QPushButton(self) self.new_project_button.setIcon(QtGui.QIcon(path_for("plus.png"))) - self.new_project_button.clicked.connect(self.parent().toggleView) + self.new_project_button.clicked.connect(self.toggle_view) self.new_project_button.setStyleSheet(self._button_style) self.import_project_button = QtWidgets.QPushButton(self) @@ -86,7 +97,7 @@ def create_buttons_for_startup_page(self): self.import_r1_button.setIcon(QtGui.QIcon(path_for("import-r1.png"))) self.import_r1_button.setStyleSheet(self._button_style) - def create_labels_for_startup_page(self): + def create_labels_for_startup_page(self) -> None: """ Creates the labels for the startup page. """ diff --git a/tests/test_dialogs.py b/tests/test_dialogs.py index e79412f..e43b2a6 100644 --- a/tests/test_dialogs.py +++ b/tests/test_dialogs.py @@ -1,17 +1,38 @@ -from PyQt6 import QtCore +from unittest.mock import patch + +import pytest +from PyQt6 import QtCore, QtTest, QtWidgets from rascal2.dialogs.project_dialog import ProjectDialog -def test_project_dialog_initial_state(): +class MockParentWindow(QtWidgets.QMainWindow): + new_project_action_called = False + + def toggleView(self): + pass + + def createNewProject(self): + pass + + +@pytest.fixture +def setup_project_dialog_widget(): + parent = MockParentWindow() + project_dialog = ProjectDialog(parent) + return project_dialog, parent + + +def test_project_dialog_initial_state(setup_project_dialog_widget): """ Tests the inital state of the project dialog. """ - project_dialog = ProjectDialog() + project_dialog, _ = setup_project_dialog_widget assert project_dialog.windowFlags() == (QtCore.Qt.WindowType.FramelessWindowHint | QtCore.Qt.WindowType.Dialog) assert project_dialog.isModal() + assert project_dialog.minimumWidth() == 700 assert project_dialog.create_button.isEnabled() assert project_dialog.cancel_button.isEnabled() @@ -21,10 +42,113 @@ def test_project_dialog_initial_state(): assert project_dialog.cancel_button.text() == " Cancel" assert project_dialog.browse_button.text() == " Browse" + assert project_dialog.project_name.placeholderText() == "Enter project name" assert project_dialog.project_name_label.text() == "Project Name:" + assert project_dialog.project_name_error.text() == "Project name needs to be specified." + assert project_dialog.project_name_error.isHidden() + + assert project_dialog.project_folder.placeholderText() == "Select project folder" assert project_dialog.project_folder_label.text() == "Project Folder:" - assert project_dialog.project_folder.text() == "No folder selected" + assert project_dialog.project_folder_error.text() == "An empty project folder needs to be selected." + assert project_dialog.project_folder_error.isHidden() + assert project_dialog.project_folder.isReadOnly() + + assert not project_dialog.name_error + assert not project_dialog.folder_error + + +def test_lineedit_errors(setup_project_dialog_widget): + """ + Tests the project name and folder line edit errors displayed. + """ + project_dialog, _ = setup_project_dialog_widget + + # tests the project name and folder line edit errors displayed. + QtTest.QTest.mouseClick(project_dialog.create_button, QtCore.Qt.MouseButton.LeftButton) + + assert not project_dialog.project_name_error.isHidden() + assert project_dialog.name_error + assert not project_dialog.project_folder_error.isHidden() + assert project_dialog.folder_error + + # tests the project name line edit error displayed. + project_dialog.project_folder.setText("test-folder") + QtTest.QTest.mouseClick(project_dialog.create_button, QtCore.Qt.MouseButton.LeftButton) + + assert not project_dialog.project_name_error.isHidden() + assert project_dialog.name_error + assert project_dialog.project_folder_error.isHidden() + assert not project_dialog.folder_error + + # tests the project folder line edit error displayed. + project_dialog.project_name.setText("test-name") + project_dialog.project_folder.setText("") + QtTest.QTest.mouseClick(project_dialog.create_button, QtCore.Qt.MouseButton.LeftButton) + + assert project_dialog.project_name_error.isHidden() + assert not project_dialog.name_error + assert not project_dialog.project_folder_error.isHidden() + assert project_dialog.folder_error + + +@patch.object(MockParentWindow, "createNewProject") +def test_create_button(mock_create_new_project, setup_project_dialog_widget): + """ + Tests create button on the ProjectDialog class. + """ + project_dialog, _ = setup_project_dialog_widget + + project_dialog.project_name.setText("test-name") + project_dialog.project_folder.setText("test-folder") + QtTest.QTest.mouseClick(project_dialog.create_button, QtCore.Qt.MouseButton.LeftButton) + mock_create_new_project.assert_called_once() + + +@patch.object(MockParentWindow, "toggleView") +def test_cancel_button(mock_toggle_view, setup_project_dialog_widget): + """ + Tests cancel button on the ProjectDialog class. + """ + project_dialog, _ = setup_project_dialog_widget + + project_dialog.project_name.setText("test-name") + project_dialog.name_error = True + project_dialog.project_folder.setText("test-folder") + project_dialog.folder_error = True + + QtTest.QTest.mouseClick(project_dialog.cancel_button, QtCore.Qt.MouseButton.LeftButton) + + mock_toggle_view.assert_called_once() assert project_dialog.project_name.text() == "" - assert project_dialog.project_name.placeholderText() == "Enter project name" - assert not project_dialog.folder_selected + assert not project_dialog.name_error + assert project_dialog.project_folder.text() == "" + assert not project_dialog.folder_error + + +@patch("os.listdir") +@patch("PyQt6.QtWidgets.QFileDialog.getExistingDirectory") +def test_browse_button(mock_get_existing_directory, mock_listdir, setup_project_dialog_widget): + """ + Tests the browse button on the ProjectDialog class. + """ + project_dialog, _ = setup_project_dialog_widget + + # When empty folder is selected. + mock_get_existing_directory.return_value = "/test/folder/path" + mock_listdir.return_value = [".hiddenfile"] + + QtTest.QTest.mouseClick(project_dialog.browse_button, QtCore.Qt.MouseButton.LeftButton) + + assert project_dialog.project_folder.text() == "/test/folder/path" + assert not project_dialog.folder_error + + project_dialog.reset_variables() + + # When a non empty folder is selected. + mock_listdir.return_value = [".hiddenfile", "testfile"] + + QtTest.QTest.mouseClick(project_dialog.browse_button, QtCore.Qt.MouseButton.LeftButton) + + assert project_dialog.project_folder.text() == "" + assert project_dialog.folder_error diff --git a/tests/test_widgets.py b/tests/test_widgets.py index d51e8d4..ddf4dd0 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -1,24 +1,38 @@ -from rascal2.ui.view import MainWindowView +from unittest.mock import patch + +import pytest +from PyQt6 import QtCore, QtTest + from rascal2.widgets.startup_widget import StartUpWidget +from tests.test_dialogs import MockParentWindow + +@pytest.fixture +def setup_startup_widget(): + parent = MockParentWindow() + startup_widget = StartUpWidget(parent) + return startup_widget, parent -def test_startup_widget_initial_state(): + +def test_startup_widget_initial_state(setup_startup_widget): """ Tests the initial state of the start up widget. """ - view = MainWindowView() - startup_widget = StartUpWidget(view) - + startup_widget, _ = setup_startup_widget assert startup_widget.new_project_button.isEnabled() assert startup_widget.import_project_button.isEnabled() - assert startup_widget.load_example_button.isEnabled() assert startup_widget.import_r1_button.isEnabled() - assert startup_widget.cancel_button.isEnabled() - assert startup_widget.import_rascal_button.isEnabled() assert startup_widget.new_project_label.text() == "New\nProject" assert startup_widget.import_project_label.text() == "Import Existing\nProject" - assert startup_widget.load_example_label.text() == "Open Example\nProject" assert startup_widget.import_r1_label.text() == "Import R1\nProject" - assert startup_widget.cancel_label.text() == "Cancel" - assert startup_widget.import_rascal_label.text() == "Import RasCAL\nProject" + + +@patch.object(MockParentWindow, "toggleView") +def test_toggle_view_called(mock, setup_startup_widget): + """ + Tests the toggleView method is called once. + """ + startup_widget, _ = setup_startup_widget + QtTest.QTest.mouseClick(startup_widget.new_project_button, QtCore.Qt.MouseButton.LeftButton) + mock.assert_called_once()