diff --git a/poetry.lock b/poetry.lock index cd45155..88c5936 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1228,6 +1228,24 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +[[package]] +name = "pytest-qt" +version = "4.2.0" +description = "pytest support for PyQt and PySide applications" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-qt-4.2.0.tar.gz", hash = "sha256:00a17b586dd530b6d7a9399923a40489ca4a9a309719011175f55dc6b5dc8f41"}, + {file = "pytest_qt-4.2.0-py2.py3-none-any.whl", hash = "sha256:a7659960a1ab2af8fc944655a157ff45d714b80ed7a6af96a4b5bb99ecf40a22"}, +] + +[package.dependencies] +pytest = ">=3.0.0" + +[package.extras] +dev = ["pre-commit", "tox"] +doc = ["sphinx", "sphinx-rtd-theme"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -1750,4 +1768,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "b6ba5546bab7608b097d4b6da502d28057a9605632765a6b193bebb3b6315391" +content-hash = "b24f663425f63a9fe378065f74f310ed7ec59d8f48c25011e1c40297ca53b14e" diff --git a/pyproject.toml b/pyproject.toml index d9081d5..cf98b7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ pyqt6-webengine = "^6.5.0" pytest = "^7.1.3" pytest-cov = "^4.0.0" invoke = "^2.0.0" +pytest-qt = "^4.2.0" [tool.poetry.group.dev.dependencies] @@ -72,7 +73,10 @@ testpaths = [ markers = [ "crud: marks CRUD integration tests using an in-memory database (deselect with '-m \"not crud\"')", "api", + "frontend", ] +qt_qapp_name = "Dfacto-tests" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/src/dfacto/__init__.py b/src/dfacto/__init__.py index eb81939..427c9f8 100644 --- a/src/dfacto/__init__.py +++ b/src/dfacto/__init__.py @@ -37,6 +37,7 @@ def _(_text: str) -> str: IS_FROZEN: Final[bool] = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS") DEV_MODE: Final[bool] = os.environ.get("DFACTO_DEV", "0") != "0" +TEST_MODE: Final[bool] = os.environ.get("DFACTO_TEST", "0") != "0" def except_hook(exc_type, exc_value, _exc_traceback): # type: ignore[no-untyped-def] diff --git a/src/dfacto/frontend/globals_editor.py b/src/dfacto/frontend/globals_editor.py index 89e517a..1074810 100644 --- a/src/dfacto/frontend/globals_editor.py +++ b/src/dfacto/frontend/globals_editor.py @@ -175,14 +175,12 @@ def is_valid(self) -> bool: return due_delta >= 0 and penalty >= 0.0 and discount >= 0.0 - @QtCore.pyqtSlot(str) def check_spn_text(self, spn: QtWidgets.QSpinBox, text: str) -> None: if text == spn.suffix(): spn.setValue(0) self._enable_buttons(self.has_changed, self.is_valid) - @QtCore.pyqtSlot(str) def check_spn_value(self, _value: Union[int, float]) -> None: self._enable_buttons(self.has_changed, self.is_valid) diff --git a/src/dfacto/frontend/settingsview.py b/src/dfacto/frontend/settingsview.py index 8420141..e7b1a41 100644 --- a/src/dfacto/frontend/settingsview.py +++ b/src/dfacto/frontend/settingsview.py @@ -113,21 +113,23 @@ def __init__(self, *args, parent=None, **kwargs): self.scale_spn.valueChanged.connect(self.select_qt_scale_factor_from_float) self.scale_sld.valueChanged.connect(self.select_qt_scale_factor_from_int) - reset_btn = QtWidgets.QPushButton(_("Reset to defaults")) - reset_btn.clicked.connect(self.reset_to_defaults) + self.reset_btn = QtWidgets.QPushButton(_("Reset to defaults")) + self.reset_btn.clicked.connect(self.reset_to_defaults) - button_box = QtWidgets.QDialogButtonBox( + self.button_box = QtWidgets.QDialogButtonBox( QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel, QtCore.Qt.Orientation.Horizontal, self, ) - button_box.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setText(_("OK")) - button_box.button(QtWidgets.QDialogButtonBox.StandardButton.Cancel).setText( - _("Cancel") + self.button_box.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setText( + _("OK") ) - button_box.accepted.connect(self.accept) - button_box.rejected.connect(self.reject) + self.button_box.button( + QtWidgets.QDialogButtonBox.StandardButton.Cancel + ).setText(_("Cancel")) + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.reject) edit_layout = QtWidgets.QGridLayout() edit_layout.addWidget(self.default_dir_selector, 0, 0, 1, 3) @@ -153,8 +155,8 @@ def __init__(self, *args, parent=None, **kwargs): QtWidgets.QSizePolicy.Policy.Expanding, ) edit_layout.addItem(vertical_spacer, 7, 0, 1, 1) - edit_layout.addWidget(reset_btn, 8, 0, 1, 1) - edit_layout.addWidget(button_box, 9, 1, 1, 2) + edit_layout.addWidget(self.reset_btn, 8, 0, 1, 1) + edit_layout.addWidget(self.button_box, 9, 1, 1, 2) self.setLayout(edit_layout) diff --git a/src/dfacto/settings.py b/src/dfacto/settings.py index e6a70bb..662183d 100644 --- a/src/dfacto/settings.py +++ b/src/dfacto/settings.py @@ -14,7 +14,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from dfacto import DEV_MODE, IS_FROZEN +from dfacto import DEV_MODE, IS_FROZEN, TEST_MODE from dfacto.backend import naming from dfacto.util.settings import Setting, Settings, get_app_dirs @@ -60,7 +60,11 @@ class DfactoSettings(Settings): resources: Path templates: Path - _DEFAULT_LOGLEVEL = "INFO" + DEFAULT_COMPANY_FOLDER = "C:/Users/T0018179/MyApp/Git/home/portable/DFacto" + # DEFAULT_COMPANY_FOLDER = "F:/Users/Documents/Dfacto" + DEFAULT_LOG_LEVEL = "INFO" + DEFAULT_QT_SCALE_FACTOR = "1.0" + DEFAULT_FONT_SIZE = 2 last_profile: Setting = Setting(default_value=None) profiles: Setting = Setting(default_value=None) @@ -70,15 +74,12 @@ class DfactoSettings(Settings): lastDestinationNamingTemplate: Setting = Setting( default_value=naming.NamingTemplates.default_destination_naming_template ) - # default_company_folder = Setting( - # default_value="C:/Users/T0018179/MyApp/Git/home/portable/DFacto" - # ) - default_company_folder = Setting(default_value="F:/Users/Documents/Dfacto") - log_level: Setting = Setting(default_value=_DEFAULT_LOGLEVEL) + default_company_folder = Setting(default_value=DEFAULT_COMPANY_FOLDER) + log_level: Setting = Setting(default_value=DEFAULT_LOG_LEVEL) window_position: Setting = Setting(default_value=(0, 0)) window_size: Setting = Setting(default_value=(1600, 800)) - qt_scale_factor: Setting = Setting(default_value="1.0") - font_size: Setting = Setting(default_value=2) + qt_scale_factor: Setting = Setting(default_value=DEFAULT_QT_SCALE_FACTOR) + font_size: Setting = Setting(default_value=DEFAULT_FONT_SIZE) # locale: Setting = Setting(default_value="en_US") # locale: Setting = Setting(default_value="fr_FR") locale: Setting = Setting(default_value=None) @@ -116,7 +117,9 @@ def reset_to_defaults(self) -> None: setattr(self, setting, default_value) -if DEV_MODE: +if TEST_MODE: + dfacto_settings = DfactoSettings("dfacto_test") +elif DEV_MODE: dfacto_settings = DfactoSettings("dfacto_dev") else: dfacto_settings = DfactoSettings("dfacto") diff --git a/tests/frontend/__init__.py b/tests/frontend/__init__.py new file mode 100644 index 0000000..45edec1 --- /dev/null +++ b/tests/frontend/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) 2023 Eric Lemoine +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. diff --git a/tests/frontend/test_globals_editor.py b/tests/frontend/test_globals_editor.py new file mode 100644 index 0000000..71ac055 --- /dev/null +++ b/tests/frontend/test_globals_editor.py @@ -0,0 +1,116 @@ +# Copyright (c) 2022, Eric Lemoine +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from decimal import Decimal + +import PyQt6.QtCore as QtCore +import PyQt6.QtWidgets as QtWidgets +import pytest + +from dfacto.backend import schemas +from dfacto.frontend.globals_editor import GlobalsEditor + +pytestmark = pytest.mark.frontend + + +@pytest.fixture(scope="session", autouse=True) +def install_tr(): + import gettext + from pathlib import Path + + from dfacto.settings import dfacto_settings + + locales_dir = Path(__file__).resolve().parent.parent.parent / "locales" + translations = gettext.translation( + "dfacto", + locales_dir, + languages=[dfacto_settings.locale], + fallback=True, + ) + translations.install() + + +DUE_DELTA = 15 +PENALTY_RATE_D = Decimal("12.0") +PENALTY_RATE_F = 12.0 +DISCOUNT_RATE_D = Decimal("2.5") +DISCOUNT_RATE_F = 2.5 +FAKE_GLOBALS = schemas.Globals( + id=1, + due_delta=DUE_DELTA, + penalty_rate=PENALTY_RATE_D, + discount_rate=DISCOUNT_RATE_D, + is_current=True, +) + + +def test_init(qtbot): + editor = GlobalsEditor(FAKE_GLOBALS) + # editor.show() + # qtbot.stop() + qtbot.addWidget(editor) + + assert editor.due_spn.value() == DUE_DELTA + assert editor.penalty_spn.value() == PENALTY_RATE_F + assert editor.discount_spn.value() == DISCOUNT_RATE_F + assert not editor.button_box.button( + QtWidgets.QDialogButtonBox.StandardButton.Ok + ).isEnabled() + assert not editor.reset_btn.isEnabled() + + +@pytest.mark.parametrize( + "spn, old, new, attr, res", + ( + ("due_spn", DUE_DELTA, 45, "due_delta", 45), + ("penalty_spn", PENALTY_RATE_F, 20.0, "penalty_rate", Decimal("20.0")), + ("discount_spn", DISCOUNT_RATE_F, 3.0, "discount_rate", Decimal("3.0")), + ), +) +def test_change(spn, old, new, attr, res, qtbot): + editor = GlobalsEditor(FAKE_GLOBALS) + qtbot.addWidget(editor) + + spn_box = getattr(editor, spn) + spn_box.setValue(new) + + result = getattr(editor.globals, attr) + assert result == res + assert editor.button_box.button( + QtWidgets.QDialogButtonBox.StandardButton.Ok + ).isEnabled() + assert editor.reset_btn.isEnabled() + + spn_box.setValue(old) + + assert not editor.button_box.button( + QtWidgets.QDialogButtonBox.StandardButton.Ok + ).isEnabled() + assert not editor.reset_btn.isEnabled() + + +@pytest.mark.parametrize("spn", ("due_spn", "penalty_spn", "discount_spn")) +def test_clear(spn, qtbot): + editor = GlobalsEditor(FAKE_GLOBALS) + qtbot.addWidget(editor) + + spn_box = getattr(editor, spn) + suffix = spn_box.suffix() + spn_box.selectAll() + + def check_suffix(txt: str) -> bool: + return txt == suffix + + with qtbot.wait_signal( + spn_box.lineEdit().textEdited, timeout=1000, check_params_cb=check_suffix + ): + qtbot.keyClick(spn_box, QtCore.Qt.Key.Key_Delete) + + assert spn_box.value() == 0 + assert editor.button_box.button( + QtWidgets.QDialogButtonBox.StandardButton.Ok + ).isEnabled() + assert editor.reset_btn.isEnabled() diff --git a/tests/frontend/test_settings_editor.py b/tests/frontend/test_settings_editor.py new file mode 100644 index 0000000..d1d3e65 --- /dev/null +++ b/tests/frontend/test_settings_editor.py @@ -0,0 +1,140 @@ +# Copyright (c) 2022, Eric Lemoine +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +from pathlib import Path + +import PyQt6.QtWidgets as QtWidgets +import pytest + +from dfacto.frontend.settingsview import SettingsView + +pytestmark = pytest.mark.frontend + + +@pytest.fixture(scope="session", autouse=True) +def install_tr(): + import gettext + + from dfacto.settings import dfacto_settings + + locales_dir = Path(__file__).resolve().parent.parent.parent / "locales" + translations = gettext.translation( + "dfacto", + locales_dir, + languages=[dfacto_settings.locale], + fallback=True, + ) + translations.install() + + +COMPANY_FOLDER = "DFacto-Test" +FONT_SIZE = 0 +LOG_LEVEL = "DEBUG" +QT_SCALE_FACTOR_S = "1.1" +QT_SCALE_FACTOR_F = 1.1 +QT_SCALE_DELTA = 1 + + +@pytest.fixture +def init_settings(tmp_path): + from dfacto.settings import dfacto_settings + + default_company_folder = tmp_path.joinpath(COMPANY_FOLDER).as_posix() + dfacto_settings.default_company_folder = default_company_folder + dfacto_settings.font_size = FONT_SIZE + dfacto_settings.log_level = LOG_LEVEL + dfacto_settings.qt_scale_factor = QT_SCALE_FACTOR_S + dfacto_settings.save() + + return dfacto_settings + + +def test_init(qtbot, init_settings): + settings = init_settings + + editor = SettingsView() + # editor.show() + # qtbot.stop() + qtbot.addWidget(editor) + + assert editor.default_dir_selector.text() == settings.default_company_folder + assert editor.log_level_cmb.currentText() == LOG_LEVEL + assert editor.font_spn.value() == FONT_SIZE + assert editor.scale_spn.value() == QT_SCALE_FACTOR_F + assert editor.scale_sld.value() == QT_SCALE_DELTA + + +def test_change_company_folder(qtbot, init_settings): + settings = init_settings + new = "path/to/test" + + editor = SettingsView() + qtbot.addWidget(editor) + + editor.default_dir_selector.setText(new) + editor.button_box.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).click() + assert settings.default_company_folder == new + + +def test_change_log_level(qtbot, init_settings): + settings = init_settings + new = "ERROR" + + editor = SettingsView() + qtbot.addWidget(editor) + + editor.log_level_cmb.setCurrentText(new) + editor.button_box.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).click() + assert settings.log_level == new + + +def test_change_scale_factor(qtbot, init_settings): + settings = init_settings + + new = 1.5 + new_delta = 5 + editor = SettingsView() + qtbot.addWidget(editor) + + editor.scale_spn.setValue(new) + assert editor.scale_sld.value() == new_delta + editor.button_box.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).click() + assert settings.qt_scale_factor == str(new) + + new = 1.3 + new_delta = 3 + editor.scale_sld.setValue(new_delta) + assert editor.scale_spn.value() == new + editor.button_box.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).click() + assert settings.qt_scale_factor == str(new) + + +def test_change_font_size(qtbot, init_settings): + settings = init_settings + new = 2 + + editor = SettingsView() + qtbot.addWidget(editor) + + editor.font_spn.setValue(new) + editor.button_box.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).click() + assert settings.font_size == new + + +def test_reset(qtbot, init_settings): + settings = init_settings + + editor = SettingsView() + qtbot.addWidget(editor) + + editor.reset_btn.click() + assert editor.default_dir_selector.text() == settings.DEFAULT_COMPANY_FOLDER + assert editor.log_level_cmb.currentText() == settings.DEFAULT_LOG_LEVEL + assert editor.font_spn.value() == settings.DEFAULT_FONT_SIZE + assert editor.scale_spn.value() == float(settings.DEFAULT_QT_SCALE_FACTOR) + assert editor.scale_sld.value() == int( + 10 * (float(settings.DEFAULT_QT_SCALE_FACTOR) - 1) + )