From c998e3ec486eeec68d7edb879026241539b7a806 Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 19 Feb 2026 13:53:57 +0100 Subject: [PATCH] feat(bec-login): Add login widget in material design style --- bec_widgets/utils/bec_login.py | 91 ++++++++++++++++++++++++ tests/unit_tests/test_utils_bec_login.py | 52 ++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 bec_widgets/utils/bec_login.py create mode 100644 tests/unit_tests/test_utils_bec_login.py diff --git a/bec_widgets/utils/bec_login.py b/bec_widgets/utils/bec_login.py new file mode 100644 index 00000000..6f75b51c --- /dev/null +++ b/bec_widgets/utils/bec_login.py @@ -0,0 +1,91 @@ +""" +Login dialog for user authentication. +The Login Widget is styled in a Material Design style and emits +the entered credentials through a signal for further processing. +""" + +from qtpy.QtCore import Qt, Signal +from qtpy.QtWidgets import QLabel, QLineEdit, QPushButton, QVBoxLayout, QWidget + + +class BECLogin(QWidget): + """Login dialog for user authentication in Material Design style.""" + + credentials_entered = Signal(str, str) + + def __init__(self, parent=None): + super().__init__(parent=parent) + # Only displayed if this widget as standalone widget, and not embedded in another widget + self.setWindowTitle("Login") + + title = QLabel("Sign in", parent=self) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet( + """ + #QLabel + { + font-size: 18px; + font-weight: 600; + } + """ + ) + + self.username = QLineEdit(parent=self) + self.username.setPlaceholderText("Username") + + self.password = QLineEdit(parent=self) + self.password.setPlaceholderText("Password") + self.password.setEchoMode(QLineEdit.EchoMode.Password) + + self.ok_btn = QPushButton("Sign in", parent=self) + self.ok_btn.setDefault(True) + self.ok_btn.clicked.connect(self._emit_credentials) + # If the user presses Enter in the password field, trigger the OK button click + self.password.returnPressed.connect(self.ok_btn.click) + + # Build Layout + layout = QVBoxLayout(self) + layout.setContentsMargins(32, 32, 32, 32) + layout.setSpacing(16) + + layout.addWidget(title) + layout.addSpacing(8) + layout.addWidget(self.username) + layout.addWidget(self.password) + layout.addSpacing(12) + layout.addWidget(self.ok_btn) + + self.username.setFocus() + + self.setStyleSheet( + """ + QLineEdit { + padding: 8px; + } + """ + ) + + def _clear_password(self): + """Clear the password field.""" + self.password.clear() + + def _emit_credentials(self): + """Emit credentials and clear the password field.""" + self.credentials_entered.emit(self.username.text().strip(), self.password.text()) + self._clear_password() + + +if __name__ == "__main__": # pragma: no cover + import sys + + from bec_qthemes import apply_theme + from qtpy.QtWidgets import QApplication + + app = QApplication(sys.argv) + apply_theme("light") + + dialog = BECLogin() + + dialog.credentials_entered.connect(lambda u, p: print(f"Username: {u}, Password: {p}")) + dialog.show() + sys.exit(app.exec_()) diff --git a/tests/unit_tests/test_utils_bec_login.py b/tests/unit_tests/test_utils_bec_login.py new file mode 100644 index 00000000..44fb59de --- /dev/null +++ b/tests/unit_tests/test_utils_bec_login.py @@ -0,0 +1,52 @@ +"""Test the BEC Login widget""" + +import pytest +from qtpy.QtCore import Qt +from qtpy.QtWidgets import QLineEdit + +from bec_widgets.utils.bec_login import BECLogin + + +@pytest.fixture +def login_dialog(qtbot): + """Fixture to create a BECLogin instance.""" + dialog = BECLogin() + qtbot.addWidget(dialog) + qtbot.waitExposed(dialog) # Ensure the dialog is fully shown before running tests + return dialog + + +def test_utils_login_dialog_initialization(login_dialog, qtbot): + """Test that the BECLogin initializes correctly.""" + assert login_dialog.windowTitle() == "Login" + assert login_dialog.username.placeholderText() == "Username" + assert login_dialog.password.placeholderText() == "Password" + assert login_dialog.password.echoMode() == QLineEdit.EchoMode.Password + assert login_dialog.ok_btn.text() == "Sign in" + + # Initially, this should be empty + with qtbot.waitSignal(login_dialog.credentials_entered, timeout=5000) as blocker: + qtbot.mouseClick(login_dialog.ok_btn, Qt.MouseButton.LeftButton) + assert blocker.args == ["", ""] + + +def test_utils_login_dialog_emit_credentials(login_dialog, qtbot): + """Test that the BECLogin emits credentials correctly.""" + test_username = "testuser " + test_password = "testpass" + + login_dialog.username.setText(test_username) + login_dialog.password.setText(test_password) + + with qtbot.waitSignal(login_dialog.credentials_entered, timeout=5000) as blocker: + qtbot.mouseClick(login_dialog.ok_btn, Qt.MouseButton.LeftButton) + + assert blocker.args == [test_username.strip(), test_password] + assert login_dialog.password.text() == "" # Password should be cleared after emitting + + login_dialog.password.setText(test_password) + with qtbot.waitSignal(login_dialog.credentials_entered, timeout=5000) as blocker: + qtbot.keyClick(login_dialog.password, Qt.Key.Key_Return) + + assert blocker.args == [test_username.strip(), test_password] + assert login_dialog.password.text() == "" # Password should be cleared after emitting