From c6e888a61dbd27a0d40d2f801f4c0523ea03667e Mon Sep 17 00:00:00 2001 From: rahilmansuri1 Date: Fri, 21 Feb 2025 19:06:23 +0530 Subject: [PATCH] Fix: UI bugs and macOS crash issue --- .bumpversion.cfg | 2 +- .gitignore | 1 + binary/native_auth_windows.exe | Bin 206336 -> 0 bytes pylint_logs.txt | 3 - pyproject.toml | 2 +- src/main.py | 4 +- src/model/help_card_content_model.py | 52 +++- src/translations/en_IN.qm | Bin 130592 -> 130832 bytes src/translations/en_IN.ts | 16 +- src/utils/common_utils.py | 2 +- src/version.py | 2 +- src/viewmodels/enter_password_view_model.py | 2 +- src/viewmodels/header_frame_view_model.py | 49 ++-- src/viewmodels/main_view_model.py | 3 + .../set_wallet_password_view_model.py | 2 +- src/viewmodels/setting_view_model.py | 8 +- src/viewmodels/splash_view_model.py | 4 +- ...wallet_and_transfer_selection_viewmodel.py | 2 +- src/views/components/keyring_error_dialog.py | 2 +- .../components/on_close_progress_dialog.py | 14 +- src/views/qss/channel_management_style.qss | 41 +++ src/views/ui_channel_management.py | 274 ++++++++++-------- src/views/ui_fungible_asset.py | 31 +- src/views/ui_help.py | 56 ++-- src/views/ui_settings.py | 4 +- .../components/keyring_error_diaog_test.py | 4 +- .../on_close_progress_dialog_test.py | 20 +- .../ui_tests/ui_channel_management_test.py | 57 +++- .../tests/ui_tests/ui_fungible_asset_test.py | 64 +--- unit_tests/tests/ui_tests/ui_help_test.py | 79 ++++- .../tests/ui_tests/ui_send_bitcoin_test.py | 28 +- .../tests/ui_tests/ui_send_rgb_asset_test.py | 13 + .../tests/utils_test/common_utils_test.py | 2 +- .../header_frame_view_model_test.py | 251 +++++++--------- .../viewmodel_tests/splash_view_model_test.py | 4 +- ...llet_transfer_selection_view_model_test.py | 2 +- 36 files changed, 645 insertions(+), 455 deletions(-) delete mode 100644 binary/native_auth_windows.exe delete mode 100644 pylint_logs.txt diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c850cdb..5abf99e 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.1.1 +current_version = 0.1.2 commit = False tag = False allow_dirty = True diff --git a/.gitignore b/.gitignore index bc69401..cf114ce 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ build_info.json settings.json /appimages/ temp_build_constant.py +.DS_Store diff --git a/binary/native_auth_windows.exe b/binary/native_auth_windows.exe deleted file mode 100644 index c20a154a774090557e03b57136bbab38349c0bb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 206336 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 N=b3fDGmE(&|6d!uw}t=! delta 3148 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 diff --git a/src/translations/en_IN.ts b/src/translations/en_IN.ts index 1437701..04b0f81 100644 --- a/src/translations/en_IN.ts +++ b/src/translations/en_IN.ts @@ -1665,8 +1665,20 @@ If you understand the above remarks and wish to proceed, press the button below RGB invoice - copied - Copied! + copied + Copied! + + + local_balance + Local Balance + + + remote_balance + Remote Balance + + + asset + Asset data_directory_path_label diff --git a/src/utils/common_utils.py b/src/utils/common_utils.py index 15793f1..b44eb8a 100644 --- a/src/utils/common_utils.py +++ b/src/utils/common_utils.py @@ -509,7 +509,7 @@ def sigterm_handler(_sig, _frame): # Stop the LN node server and quit the application ln_node_manager = LnNodeServerManager.get_instance() ln_node_manager.stop_server_from_close_button() - QApplication.instance().quit() + QApplication.instance().exit() def set_number_validator(input_widget: QLineEdit) -> None: diff --git a/src/version.py b/src/version.py index dbdd9e0..e366a01 100644 --- a/src/version.py +++ b/src/version.py @@ -6,4 +6,4 @@ """ from __future__ import annotations -__version__ = '0.1.1' +__version__ = '0.1.2' diff --git a/src/viewmodels/enter_password_view_model.py b/src/viewmodels/enter_password_view_model.py index 2b3c203..e9b509a 100644 --- a/src/viewmodels/enter_password_view_model.py +++ b/src/viewmodels/enter_password_view_model.py @@ -121,7 +121,7 @@ def on_error(self, error: CommonException): if error.message == ERROR_NETWORK_MISMATCH: local_store.clear_settings() MessageBox('critical', error.message) - QApplication.instance().quit() + QApplication.instance().exit() self.message.emit( ToastPreset.ERROR, error.message or ERROR_SOMETHING_WENT_WRONG, diff --git a/src/viewmodels/header_frame_view_model.py b/src/viewmodels/header_frame_view_model.py index 73c6b0f..fd74c39 100644 --- a/src/viewmodels/header_frame_view_model.py +++ b/src/viewmodels/header_frame_view_model.py @@ -5,6 +5,7 @@ from PySide6.QtCore import QObject from PySide6.QtCore import QThread +from PySide6.QtCore import QTimer from PySide6.QtCore import Signal from src.utils.constant import PING_DNS_ADDRESS_FOR_NETWORK_CHECK @@ -12,26 +13,17 @@ class NetworkCheckerThread(QThread): - """View model to handle network connectivity""" + """Thread to handle network connectivity checking.""" network_status_signal = Signal(bool) - _instance = None - - def __init__(self): - super().__init__() - self.running = True def run(self): - """Run the network checking loop.""" - while self.running: - is_connected = self.check_internet_conn() - self.network_status_signal.emit(is_connected) - # Wait 5 seconds before the next check - self.msleep(PING_DNS_SERVER_CALL_INTERVAL) + """Run the network check once.""" + is_connected = self.check_internet_conn() + self.network_status_signal.emit(is_connected) def check_internet_conn(self): - """Check internet connection by making a request to the specified URL.""" + """Check internet connection and return status.""" try: - # Attempt to resolve the hostname of Google to test internet socket.create_connection( (PING_DNS_ADDRESS_FOR_NETWORK_CHECK, 53), timeout=3, ) @@ -39,19 +31,26 @@ def check_internet_conn(self): except OSError: return False - def stop(self): - """Stop the thread.""" - self.running = False - self.quit() - self.wait() - class HeaderFrameViewModel(QObject): - """Handle network connectivity""" + """Handles network connectivity in the UI.""" network_status_signal = Signal(bool) def __init__(self): - super().__init__() # Call the parent constructor + super().__init__() + + self.network_checker = None + + # Use QTimer in the main thread + self.timer = QTimer(self) + self.timer.setInterval(PING_DNS_SERVER_CALL_INTERVAL) + self.timer.timeout.connect(self.start_network_check) + + # Start checking + self.timer.start() + + def start_network_check(self): + """Start a new network check using a separate thread.""" self.network_checker = NetworkCheckerThread() self.network_checker.network_status_signal.connect( self.handle_network_status, @@ -59,9 +58,9 @@ def __init__(self): self.network_checker.start() def handle_network_status(self, is_connected): - """Handle the network status change.""" + """Emit network status signal.""" self.network_status_signal.emit(is_connected) def stop_network_checker(self): - """Stop the network checker when no longer needed.""" - self.network_checker.stop() + """Stop network checking when it's no longer needed.""" + self.timer.stop() diff --git a/src/viewmodels/main_view_model.py b/src/viewmodels/main_view_model.py index c7d9683..d0f710a 100644 --- a/src/viewmodels/main_view_model.py +++ b/src/viewmodels/main_view_model.py @@ -12,6 +12,7 @@ from src.viewmodels.enter_password_view_model import EnterWalletPasswordViewModel from src.viewmodels.faucets_view_model import FaucetsViewModel from src.viewmodels.fee_rate_view_model import EstimateFeeViewModel +from src.viewmodels.header_frame_view_model import HeaderFrameViewModel from src.viewmodels.issue_rgb20_view_model import IssueRGB20ViewModel from src.viewmodels.issue_rgb25_view_model import IssueRGB25ViewModel from src.viewmodels.ln_endpoint_view_model import LnEndpointViewModel @@ -108,3 +109,5 @@ def __init__(self, page_navigation): ) self.estimate_fee_view_model = EstimateFeeViewModel() + + self.header_frame_view_model = HeaderFrameViewModel() diff --git a/src/viewmodels/set_wallet_password_view_model.py b/src/viewmodels/set_wallet_password_view_model.py index bfdd6fa..3e32ced 100644 --- a/src/viewmodels/set_wallet_password_view_model.py +++ b/src/viewmodels/set_wallet_password_view_model.py @@ -181,7 +181,7 @@ def on_error(self, exc: CommonException): if exc.message == ERROR_NETWORK_MISMATCH: local_store.clear_settings() MessageBox('critical', exc.message) - QApplication.instance().quit() + QApplication.instance().exit() self.message.emit( ToastPreset.ERROR, exc.message or 'Something went wrong', diff --git a/src/viewmodels/setting_view_model.py b/src/viewmodels/setting_view_model.py index 532f270..95909aa 100644 --- a/src/viewmodels/setting_view_model.py +++ b/src/viewmodels/setting_view_model.py @@ -544,7 +544,7 @@ def set_bitcoind_host(self, bitcoind_host: str, password: str): ) def set_bitcoind_port(self, bitcoind_port: int, password: str): - """Sets the Default bitcoind host.""" + """Sets the Default bitcoind port.""" self.is_loading.emit(True) try: self.password = password @@ -562,7 +562,7 @@ def set_bitcoind_port(self, bitcoind_port: int, password: str): ) def set_announce_address(self, announce_address: str, password: str): - """Sets the Default bitcoind host.""" + """Sets the Default announce address.""" self.is_loading.emit(True) try: @@ -581,7 +581,7 @@ def set_announce_address(self, announce_address: str, password: str): ) def set_announce_alias(self, announce_alias: str, password: str): - """Sets the Default bitcoind host.""" + """Sets the Default announce alias.""" self.is_loading.emit(True) try: self.password = password @@ -599,7 +599,7 @@ def set_announce_alias(self, announce_alias: str, password: str): ) def set_min_confirmation(self, min_confirmation: int): - """Sets the default fee rate.""" + """Sets the default min confirmation.""" try: success: IsDefaultMinConfirmationSet = SettingCardRepository.set_default_min_confirmation( min_confirmation, diff --git a/src/viewmodels/splash_view_model.py b/src/viewmodels/splash_view_model.py index 8ccaa6f..3ee4681 100644 --- a/src/viewmodels/splash_view_model.py +++ b/src/viewmodels/splash_view_model.py @@ -68,7 +68,7 @@ def on_error(self, error: Exception): error, CommonException, ) else ERROR_SOMETHING_WENT_WRONG ToastManager.error(description=description) - QApplication.instance().quit() + QApplication.instance().exit() def is_login_authentication_enabled(self, view_model: WalletTransferSelectionViewModel): """Check login authentication enabled""" @@ -121,7 +121,7 @@ def on_error_of_unlock_api(self, error: Exception): if error_message in [ERROR_CONNECTION_FAILED_WITH_LN, ERROR_REQUEST_TIMEOUT]: MessageBox('critical', message_text=ERROR_CONNECTION_FAILED_WITH_LN) - QApplication.instance().quit() + QApplication.instance().exit() # Log the error and display a toast message logger.error( diff --git a/src/viewmodels/wallet_and_transfer_selection_viewmodel.py b/src/viewmodels/wallet_and_transfer_selection_viewmodel.py index cb6a994..2d3c20f 100644 --- a/src/viewmodels/wallet_and_transfer_selection_viewmodel.py +++ b/src/viewmodels/wallet_and_transfer_selection_viewmodel.py @@ -131,7 +131,7 @@ def on_ln_node_error(self, code: int, error: str): str(error), str(code), ) MessageBox('critical', message_text=ERROR_UNABLE_TO_START_NODE) - QApplication.instance().quit() + QApplication.instance().exit() def on_ln_node_already_running(self): """Log and toast when node already running""" diff --git a/src/views/components/keyring_error_dialog.py b/src/views/components/keyring_error_dialog.py index 878f2b7..0995abb 100644 --- a/src/views/components/keyring_error_dialog.py +++ b/src/views/components/keyring_error_dialog.py @@ -285,7 +285,7 @@ def handle_when_origin_page_set_wallet(self): else: local_store.clear_settings() self.close() - QApplication.instance().quit() + QApplication.instance().exit() except CommonException as exc: self.error.emit(exc.message) ToastManager.error( diff --git a/src/views/components/on_close_progress_dialog.py b/src/views/components/on_close_progress_dialog.py index 1193d0b..09e30ab 100644 --- a/src/views/components/on_close_progress_dialog.py +++ b/src/views/components/on_close_progress_dialog.py @@ -1,7 +1,7 @@ """ Module for handling the OnCloseDialogBox, which manages the closing process of application """ -# pylint: disable=E1121 +# pylint: disable=E1121,too-many-instance-attributes from __future__ import annotations from PySide6.QtCore import QProcess @@ -27,6 +27,7 @@ from src.utils.ln_node_manage import LnNodeServerManager from src.utils.logging import logger from src.utils.worker import ThreadManager +from src.viewmodels.header_frame_view_model import HeaderFrameViewModel from src.views.ui_restore_mnemonic import RestoreMnemonicWidget @@ -59,6 +60,7 @@ def __init__(self, parent=None): self.ln_node_manage.process_finished_on_request_app_close_error.connect( self._on_error_of_closing_node, ) + self.header_frame_view_model = HeaderFrameViewModel() # Set minimum size for flexibility but still prevent excessive resizing self.setMinimumSize(200, 200) @@ -190,7 +192,7 @@ def _on_error_of_closing_node(self): self._update_status(ERROR_UNABLE_TO_STOP_NODE) self.qmessage_info = ERROR_UNABLE_TO_STOP_NODE QMessageBox.critical(self, 'Failed', self.qmessage_info) - QApplication.instance().quit() + QApplication.instance().exit() def _on_success_close_node(self): """ @@ -198,7 +200,8 @@ def _on_success_close_node(self): """ self.is_node_closing_onprogress = False self._update_status('The node closed successfully!') - QApplication.instance().quit() + self.header_frame_view_model.stop_network_checker() + QApplication.instance().exit() def _close_node_app(self): """ @@ -216,7 +219,8 @@ def _close_node_app(self): self.dialog_title = 'Node closing in progress' self.ln_node_manage.stop_server_from_close_button() else: - QApplication.instance().quit() + self.header_frame_view_model.stop_network_checker() + QApplication.instance().exit() # pylint disable(invalid-name) because of closeEvent is internal function of QWidget def closeEvent(self, event): # pylint:disable=invalid-name @@ -246,4 +250,4 @@ def closeEvent(self, event): # pylint:disable=invalid-name ) else: event.accept() - QApplication.instance().quit() + QApplication.instance().exit() diff --git a/src/views/qss/channel_management_style.qss b/src/views/qss/channel_management_style.qss index 2a5f5b2..935e75e 100644 --- a/src/views/qss/channel_management_style.qss +++ b/src/views/qss/channel_management_style.qss @@ -103,3 +103,44 @@ QWidget#btc_scroll_area_widget_contents{ background:transparent; border: none; } + +#asset_id_label, +#asset_name_label, +#local_balance_label, +#status_label, +#remote_balance_label{ + border: none; + background: transparent; + font: 14px "Inter"; + color: white; + font-weight: 600; +} + +#remote_balance_value, +#local_balance_value, +#asset_id, +#asset_name{ + border: none; + background: transparent; + font: 14px "Inter"; + color: white; +} + +#list_frame{ + background:transparent; + background-color: rgba(21, 28, 52, 1); + color:white; + font:14px Inter; + border-radius:8px; + border:none; +} + +#channel_header{ + border-radius: 8px; + background: transparent; + background-color: rgb(27, 35, 59); +} +QToolTip{ + background-color: rgba(21, 28, 52, 1); + color:white; +} diff --git a/src/views/ui_channel_management.py b/src/views/ui_channel_management.py index c58d7e6..163636d 100644 --- a/src/views/ui_channel_management.py +++ b/src/views/ui_channel_management.py @@ -30,6 +30,7 @@ from src.utils.clickable_frame import ClickableFrame from src.utils.common_utils import generate_identicon from src.utils.common_utils import get_bitcoin_info_by_network +from src.utils.common_utils import translate_value from src.utils.helpers import create_circular_pixmap from src.utils.helpers import load_stylesheet from src.utils.render_timer import RenderTimer @@ -66,11 +67,6 @@ def __init__(self, view_model): self.list_h_box_layout = None self.counter_party = None self.pub_key = None - self.opened_date = None - self.asset_remote_balance = None - self.asset = None - self.channel_status = None - self.status = None self.scroll_v_spacer = None self.asset_local_balance = None self.channel_management_loading_screen = None @@ -80,11 +76,12 @@ def __init__(self, view_model): self.asset_name = None self.local_balance = None self.remote_balance = None - self.asset_name_logo = None self.nia_asset_lookup = {} - self.grid_layout_2 = None - self.asset_logo_container = None - self.horizontal_layout_3 = None + self.status_pixmap = None + self.local_balance_value = None + self.remote_balance_value = None + self.channel_frame_horizontal_layout = None + self.asset_id_value = None self.setObjectName('channel_management_page') self.vertical_layout_channel = QVBoxLayout(self) @@ -132,15 +129,6 @@ def __init__(self, view_model): self.main_list_v_layout.setObjectName('main_list_v_layout') self.main_list_v_layout.setContentsMargins(0, 0, 0, 0) - self.frame = QFrame(self) - self.frame.setObjectName('header') - self.frame.setMinimumSize(QSize(0, 70)) - - self.frame.setFrameShape(QFrame.StyledPanel) - self.frame.setFrameShadow(QFrame.Raised) - self.grid_layout = QGridLayout(self.frame) - self.grid_layout.setContentsMargins(20, 0, 22, 0) - self.scroll_area = QScrollArea(self) self.scroll_area.setObjectName('scroll_area') self.scroll_area.setAutoFillBackground(False) @@ -171,8 +159,71 @@ def __init__(self, view_model): self.vertical_layout_channel.addWidget(self.widget_channel) self.vertical_layout_2_channel.addLayout(self.horizontal_layout_2) + self.channel_header = QFrame(self) + self.channel_header.setObjectName('channel_header') + self.channel_header.setMinimumSize(QSize(900, 70)) + self.channel_header.setMaximumSize(QSize(16777215, 70)) + self.channel_header.setFrameShape(QFrame.Shape.StyledPanel) + self.channel_header.setFrameShadow(QFrame.Shadow.Raised) + + self.channel_header_layout = QHBoxLayout(self.channel_header) + self.channel_header_layout.setSpacing(6) + self.channel_header_layout.setObjectName('channel_header_layout') + + self.asset_logo_label = QLabel(self.channel_header) + self.asset_logo_label.setObjectName('asset_logo_label') + self.asset_logo_label.setMinimumSize(QSize(40, 40)) + self.asset_logo_label.setMaximumSize(QSize(40, 40)) + + self.channel_header_layout.addWidget(self.asset_logo_label) + + self.asset_name_label = QLabel(self.channel_header) + self.asset_name_label.setObjectName('asset_name_label') + self.asset_name_label.setMinimumSize(QSize(136, 40)) + self.channel_header_layout.addWidget(self.asset_name_label) + + self.asset_id_label = QLabel(self.channel_header) + self.asset_id_label.setObjectName('asset_id_label') + self.asset_id_label.setMinimumSize(QSize(450, 0)) + self.asset_id_label.setMaximumSize(QSize(16777215, 16777215)) + self.asset_id_label.setAlignment( + Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, + ) + + self.channel_header_layout.addWidget(self.asset_id_label) + + self.local_balance_label = QLabel(self.channel_header) + self.local_balance_label.setObjectName('local_balance_label') + self.local_balance_label.setMinimumSize(QSize(136, 0)) + self.local_balance_label.setAlignment( + Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter, + ) + + self.channel_header_layout.addWidget(self.local_balance_label) + + self.remote_balance_label = QLabel(self.channel_header) + self.remote_balance_label.setObjectName('remote_balance_label') + self.remote_balance_label.setMinimumSize(QSize(136, 0)) + self.remote_balance_label.setAlignment( + Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter, + ) + + self.channel_header_layout.addWidget(self.remote_balance_label) + + self.status_label = QLabel(self.channel_header) + self.status_label.setObjectName('status_label') + self.status_label.setMinimumSize(QSize(60, 0)) + self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.channel_header_layout.addWidget(self.status_label) + + self.vertical_spacer = QSpacerItem( + 20, 78, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding, + ) + + self.vertical_layout_2_channel.addWidget(self.channel_header) + self.scroll_area.setWidget(self.scroll_area_widget_contents) - self.setup_headers() self.main_list_v_layout.addWidget(self.scroll_area) self.vertical_layout_2_channel.addWidget(self.channel_list_widget) self.retranslate_ui() @@ -180,44 +231,6 @@ def __init__(self, view_model): self._view_model.channel_view_model.available_channels() self._view_model.channel_view_model.get_asset_list() - def create_header_label(self, text, min_width, alignment): - """ - Helper function to create header labels. - """ - label = QLabel(self.frame) - label.setObjectName('header_label') - label.setMinimumWidth(min_width) - label.setText(QCoreApplication.translate('iris_wallet', text, None)) - label.setWordWrap(True) - label.setStyleSheet( - 'color: white;font: 14px \"Inter\";\n' - 'background: transparent;\n' - 'border: none;\n' - 'font-weight: 600;\n', - ) - self.grid_layout.addWidget( - label, 0, alignment, Qt.AlignLeft if alignment != 1 else Qt.AlignCenter, - ) - return label - - def setup_headers(self): - """ - Creates and adds header labels to the grid layout. - """ - column_counter = 0 - for header in self.header_col: - if header == 'Asset ID': - self.header_label_asset_id = self.create_header_label( - header, 450, 1, - ) - else: - self.header_labels = self.create_header_label( - header, 136, column_counter, - ) - column_counter += 1 - - self.main_list_v_layout.addWidget(self.frame) - def show_available_channels(self): """This method shows the available channels.""" for i in reversed(range(self.list_v_box_layout.count())): @@ -239,27 +252,33 @@ def show_available_channels(self): self.list_frame.setObjectName('list_frame') self.list_frame.setMinimumSize(QSize(0, 70)) self.list_frame.setMaximumSize(QSize(16777215, 70)) - self.list_frame.setStyleSheet( - 'background:transparent;' - 'background-color: rgba(21, 28, 52, 1);\n' - 'color:white;\n' - 'font:14px Inter;\n' - 'border-radius:8px;\n' - 'border:none\n', + self.list_frame.setFrameShape(QFrame.Shape.StyledPanel) + self.list_frame.setFrameShadow(QFrame.Shadow.Raised) + + self.channel_frame_horizontal_layout = QHBoxLayout(self.list_frame) + self.channel_frame_horizontal_layout.setSpacing(6) + self.channel_frame_horizontal_layout.setContentsMargins( + 20, 0, 20, 0, ) - self.list_frame.setFrameShape(QFrame.StyledPanel) - self.list_frame.setFrameShadow(QFrame.Raised) - self.grid_layout_2 = QGridLayout(self.list_frame) - self.grid_layout_2.setContentsMargins(20, 0, 20, 0) - self.asset_logo_container = QWidget() - self.horizontal_layout_3 = QHBoxLayout(self.asset_logo_container) - self.horizontal_layout_3.setContentsMargins(0, 0, 0, 0) - - self.asset_logo = QLabel(self.asset_logo_container) + + self.asset_logo = QLabel(self.list_frame) self.asset_logo.setObjectName('asset_logo') - self.asset_logo.setMaximumWidth(40) + self.asset_logo.setMinimumSize(QSize(40, 0)) + self.asset_logo.setMaximumSize(QSize(40, 16777215)) + + self.channel_frame_horizontal_layout.addWidget( + self.asset_logo, Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, + ) + + self.asset_name = QLabel(self.list_frame) + self.asset_name.setObjectName('asset_name') + self.asset_name.setMinimumSize(QSize(136, 40)) if channel.asset_id: + for key, value in self._view_model.channel_view_model.total_asset_lookup_list.items(): + if channel.asset_id == key: + self.asset_name.setText(value) + img_str = generate_identicon(channel.asset_id) image = QImage.fromData( QByteArray.fromBase64(img_str.encode()), @@ -267,58 +286,58 @@ def show_available_channels(self): pixmap = QPixmap.fromImage(image) self.asset_logo.setPixmap(pixmap) - self.horizontal_layout_3.addWidget(self.asset_logo) + self.channel_frame_horizontal_layout.addWidget(self.asset_name) - self.asset_name = QLabel(self.asset_logo) - self.asset_name.setObjectName('asset_name') + self.asset_id_value = QLabel(self.list_frame) + self.asset_id_value.setObjectName('asset_id') + self.asset_id_value.setMinimumSize(QSize(450, 0)) + self.asset_id_value.setText(channel.asset_id) - if channel.asset_id: - for key, value in self._view_model.channel_view_model.total_asset_lookup_list.items(): - if channel.asset_id == key: - self.asset_name.setText(value) - self.horizontal_layout_3.addWidget(self.asset_name) - self.asset_logo_container.setMinimumWidth(136) - self.asset_logo_container.setMinimumHeight(40) - self.grid_layout_2.addWidget(self.asset_logo_container, 0, 0) - - self.asset = QLabel(self.list_frame) - self.asset.setObjectName('asset_id') - self.asset.setMinimumWidth(450) - self.asset.setText(channel.asset_id) - - self.grid_layout_2.addWidget(self.asset, 0, 1, Qt.AlignLeft) - - self.local_balance = QLabel(self.list_frame) - self.local_balance.setObjectName('local_balance') - self.local_balance.setMinimumWidth(136) - self.local_balance.setText( + self.channel_frame_horizontal_layout.addWidget(self.asset_id_value) + + self.local_balance_value = QLabel(self.list_frame) + self.local_balance_value.setObjectName('local_balance_value') + self.local_balance_value.setMinimumSize(QSize(136, 0)) + self.local_balance_value.setText( str( channel.asset_local_amount if channel.asset_id else int( channel.outbound_balance_msat/1000, ), ), ) - self.grid_layout_2.addWidget( - self.local_balance, 0, 2, Qt.AlignLeft, + self.local_balance_value.setAlignment( + Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTrailing | Qt.AlignmentFlag.AlignVCenter, + ) + + self.channel_frame_horizontal_layout.addWidget( + self.local_balance_value, ) - self.remote_balance = QLabel(self.list_frame) - self.remote_balance.setObjectName('remote_balance') - self.remote_balance.setMinimumWidth(136) - self.remote_balance.setText( + self.remote_balance_value = QLabel(self.list_frame) + self.remote_balance_value.setObjectName('remote_balance_value') + self.remote_balance_value.setMinimumSize(QSize(136, 0)) + self.remote_balance_value.setText( str( channel.asset_remote_amount if channel.asset_id else int( channel.inbound_balance_msat/1000, ), ), ) + self.remote_balance_value.setAlignment( + Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTrailing | Qt.AlignmentFlag.AlignVCenter, + ) + + self.channel_frame_horizontal_layout.addWidget( + self.remote_balance_value, + ) - self.grid_layout_2.addWidget( - self.remote_balance, 0, 3, Qt.AlignLeft, + self.status_pixmap = QLabel(self.list_frame) + self.status_pixmap.setObjectName('status_pixmap') + self.status_pixmap.setMinimumSize(QSize(60, 0)) + self.status_pixmap.setStyleSheet( + 'padding-left:25px;', ) - self.status = QLabel(self.list_frame) - self.status.setObjectName('status') - self.status.setMaximumSize(QSize(40, 40)) + color = ( QColor(235, 90, 90) if channel.status == 'Closing' else QColor(0, 201, 145) if channel.is_usable else @@ -326,7 +345,7 @@ def show_available_channels(self): QColor(255, 255, 0) ) - self.status.setToolTip( + self.status_pixmap.setToolTip( QCoreApplication.translate('iris_wallet_desktop', 'closing', None) if color == QColor(235, 90, 90) else QCoreApplication.translate('iris_wallet_desktop', 'opening', None) if color == QColor(0, 201, 145) else QCoreApplication.translate('iris_wallet_desktop', 'offline', None) if color == QColor(169, 169, 169) else @@ -334,17 +353,16 @@ def show_available_channels(self): 'iris_wallet_desktop', 'pending', None, ), ) + self.status_pixmap.setAlignment(Qt.AlignmentFlag.AlignCenter) pixmap = create_circular_pixmap(16, color) - self.status.setPixmap(pixmap) - self.status.setStyleSheet( - 'padding-left: 20px;', - ) + self.status_pixmap.setPixmap(pixmap) + + self.channel_frame_horizontal_layout.addWidget(self.status_pixmap) - self.grid_layout_2.addWidget(self.status, 0, 4, Qt.AlignLeft) self.list_v_box_layout.addWidget(self.list_frame) if channel.asset_id is None: bitcoin_asset = get_bitcoin_info_by_network() - self.asset.setText(bitcoin_asset[0]) + self.asset_id_value.setText(bitcoin_asset[0]) self.asset_name.setText(bitcoin_asset[1]) self.asset_logo.setPixmap(QPixmap(bitcoin_asset[2])) @@ -410,6 +428,32 @@ def retranslate_ui(self): 'iris_wallet_desktop', 'create_channel', None, ), ) + self.status_label.setText( + QCoreApplication.translate( + 'iris_wallet_desktop', 'status', None, + ), + ) + + self.asset_id_label.setText( + QCoreApplication.translate( + 'iris_wallet_desktop', 'asset_id', None, + ), + ) + self.asset_name_label.setText( + QCoreApplication.translate( + 'iris_wallet_desktop', 'asset', None, + ), + ) + self.local_balance_label.setText( + QCoreApplication.translate( + 'iris_wallet_desktop', 'local_balance', None, + ), + ) + self.remote_balance_label.setText( + QCoreApplication.translate( + 'iris_wallet_desktop', 'remote_balance', None, + ), + ) def trigger_render_and_refresh(self): """This method start the render timer and perform the channel list refresh""" diff --git a/src/views/ui_fungible_asset.py b/src/views/ui_fungible_asset.py index ffc117a..08960fd 100644 --- a/src/views/ui_fungible_asset.py +++ b/src/views/ui_fungible_asset.py @@ -102,7 +102,6 @@ def __init__(self, view_model): self.outbound_amount_header = None self.symbol_header = None self.outbound_balance = None - self.signal_connected = False self.vertical_layout_fungible_2.addWidget(self.title_frame) @@ -169,11 +168,6 @@ def __init__(self, view_model): def show_assets(self): """This method creates all the fungible assets elements of the main asset page.""" - self._view_model.receive_bitcoin_view_model.get_bitcoin_address() - if self.signal_connected: - self._view_model.receive_bitcoin_view_model.address.disconnect() - self.signal_connected = False - for i in reversed(range(self.vertical_layout_3.count())): widget = self.vertical_layout_3.itemAt(i).widget() if widget is not None: @@ -202,6 +196,9 @@ def show_assets(self): self.address_header.setMinimumSize(QSize(600, 0)) self.address_header.setMaximumSize(QSize(16777215, 16777215)) self.header_layout.addWidget(self.address_header, 0, 2, Qt.AlignLeft) + self.address_header.setStyleSheet( + 'padding-left: 10px;', + ) self.amount_header = QLabel(self.header_frame) self.amount_header.setObjectName('amount_header') @@ -250,7 +247,7 @@ def show_assets(self): ) self.address_header.setText( QCoreApplication.translate( - 'iris_wallet_desktop', 'address', None, + 'iris_wallet_desktop', 'asset_id', None, ), ) self.amount_header.setText( @@ -325,16 +322,16 @@ def create_fungible_card(self, asset, img_path=None): self.address.setObjectName('address') self.address.setMinimumSize(QSize(600, 0)) self.address.setMaximumSize(QSize(16777215, 16777215)) + self.address.setStyleSheet( + 'padding-left:10px;', + ) if asset.asset_iface == AssetType.BITCOIN: - # Connect signal to update label when the address is updated - self.asset_name.setMinimumSize(QSize(130, 40)) - self._view_model.receive_bitcoin_view_model.address.connect( - lambda addr, label=self.address: self.set_bitcoin_address( - label, addr, - ), - ) - self.signal_connected = True + network = SettingRepository.get_wallet_network() + if network == NetworkEnumModel.REGTEST: + self.address.setText(TokenSymbol.REGTEST_BITCOIN) + elif network == NetworkEnumModel.TESTNET: + self.address.setText(TokenSymbol.TESTNET_BITCOIN) else: self.asset_name.setMinimumSize(QSize(135, 40)) self.address.setText(asset.asset_id) @@ -521,7 +518,3 @@ def show_faucet_unavailability_message(self): ToastManager.info( description=INFO_FAUCET_NOT_AVAILABLE, ) - - def set_bitcoin_address(self, label: QLabel, address: str): - """Set the Bitcoin address in the provided QLabel.""" - label.setText(address) diff --git a/src/views/ui_help.py b/src/views/ui_help.py index 61cad61..8bdbb71 100644 --- a/src/views/ui_help.py +++ b/src/views/ui_help.py @@ -11,6 +11,7 @@ from PySide6.QtGui import QCursor from PySide6.QtWidgets import QFrame from PySide6.QtWidgets import QGridLayout +from PySide6.QtWidgets import QHBoxLayout from PySide6.QtWidgets import QLabel from PySide6.QtWidgets import QScrollArea from PySide6.QtWidgets import QSizePolicy @@ -111,12 +112,34 @@ def retranslate_ui(self): ) def create_help_frames(self): - """This method creates the help frames according to the faucet list""" - for card in self._model.card_content: - faucet_frame = self.create_help_card( + """Creates the help frames and distributes them into two columns.""" + help_card_horizontal_layout = QHBoxLayout() + + help_card_left_vertical_layout = QVBoxLayout() + help_card_right_vertical_layout = QVBoxLayout() + + card_list = self._model.card_content + for i, card in enumerate(card_list): + help_card = self.create_help_card( card.title, card.detail, card.links, ) - self.vertical_layout_4.addWidget(faucet_frame) + + if i % 2 == 0: + help_card_left_vertical_layout.addWidget(help_card) + else: + help_card_right_vertical_layout.addWidget(help_card) + + help_card_left_vertical_layout.addStretch() + help_card_right_vertical_layout.addStretch() + self.main_horizontal_spacer = QSpacerItem( + 40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum, + ) + help_card_horizontal_layout.addLayout(help_card_left_vertical_layout) + help_card_horizontal_layout.addLayout(help_card_right_vertical_layout) + help_card_horizontal_layout.addItem(self.main_horizontal_spacer) + + self.vertical_layout_4.addLayout(help_card_horizontal_layout) + self.main_vertical_spacer = QSpacerItem( 20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding, ) @@ -152,20 +175,19 @@ def create_help_card(self, title, detail, links): self.url_vertical_layout = QVBoxLayout() self.url_vertical_layout.setObjectName('url_vertical_layout') - for link in links: - self.url = QLabel(self.help_card_frame) - self.url.setObjectName(str(link)) - self.url.setText( - f"{link}", - ) - self.url.setMinimumSize(QSize(0, 15)) - self.url.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) - self.url.setTextInteractionFlags(Qt.TextBrowserInteraction) - self.url.setOpenExternalLinks(True) - self.url_vertical_layout.addWidget(self.url) + if links: + for link in links: + self.url = QLabel(self.help_card_frame) + self.url.setObjectName(str(link)) + self.url.setText( + f"{link}", + ) + self.url.setMinimumSize(QSize(0, 15)) + self.url.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) + self.url.setTextInteractionFlags(Qt.TextBrowserInteraction) + self.url.setOpenExternalLinks(True) + self.url_vertical_layout.addWidget(self.url) self.vertical_layout_3.addLayout(self.url_vertical_layout) - # self.verticalLayout_4.addWidget(self.help_card_frame) - return self.help_card_frame diff --git a/src/views/ui_settings.py b/src/views/ui_settings.py index 2fc981b..025ce31 100644 --- a/src/views/ui_settings.py +++ b/src/views/ui_settings.py @@ -639,7 +639,7 @@ def _set_proxy_endpoint(self): ) def _set_bitcoind_host(self): - """ Set the default announce address based on user input.""" + """ Set the default bitcoind host based on user input.""" password = self._check_keyring_state() if password: self._view_model.setting_view_model.set_bitcoind_host( @@ -647,7 +647,7 @@ def _set_bitcoind_host(self): ) def _set_bitcoind_port(self): - """Set the default announce address based on user input.""" + """Set the default bitcoind port based on user input.""" password = self._check_keyring_state() if password: self._view_model.setting_view_model.set_bitcoind_port( diff --git a/unit_tests/tests/ui_tests/components/keyring_error_diaog_test.py b/unit_tests/tests/ui_tests/components/keyring_error_diaog_test.py index 4cc4122..71c218b 100644 --- a/unit_tests/tests/ui_tests/components/keyring_error_diaog_test.py +++ b/unit_tests/tests/ui_tests/components/keyring_error_diaog_test.py @@ -208,7 +208,7 @@ def test_handle_when_origin_page_set_wallet_unchecked(keyring_error_dialog_widge 'src.utils.local_store.local_store.clear_settings', ) mock_close = mocker.patch.object(keyring_error_dialog_widget, 'close') - mock_quit = mocker.patch('PySide6.QtWidgets.QApplication.instance') + mock_exit = mocker.patch('PySide6.QtWidgets.QApplication.instance') # Set checkbox to unchecked keyring_error_dialog_widget.check_box.setChecked(False) @@ -219,7 +219,7 @@ def test_handle_when_origin_page_set_wallet_unchecked(keyring_error_dialog_widge # Verify the unchecked path mock_clear_settings.assert_called_once() mock_close.assert_called_once() - mock_quit.return_value.quit.assert_called_once() + mock_exit.return_value.exit.assert_called_once() def test_handle_when_origin_page_set_wallet_common_exception(keyring_error_dialog_widget, mocker): diff --git a/unit_tests/tests/ui_tests/components/on_close_progress_dialog_test.py b/unit_tests/tests/ui_tests/components/on_close_progress_dialog_test.py index 67ebd62..e9d02c0 100644 --- a/unit_tests/tests/ui_tests/components/on_close_progress_dialog_test.py +++ b/unit_tests/tests/ui_tests/components/on_close_progress_dialog_test.py @@ -106,18 +106,18 @@ def test_on_error_of_backup(mock_close_node, mock_critical, on_close_progress_di @patch.object(QMessageBox, 'critical') -@patch.object(QApplication, 'quit') -def test_on_error_of_closing_node(mock_quit, mock_critical, on_close_progress_dialog_widget): +@patch.object(QApplication, 'exit') +def test_on_error_of_closing_node(mock_exit, mock_critical, on_close_progress_dialog_widget): """Test _on_error_of_closing_node method to ensure error handling during node closing.""" on_close_progress_dialog_widget._on_error_of_closing_node() assert on_close_progress_dialog_widget.is_node_closing_onprogress is False mock_critical.assert_called_once_with( on_close_progress_dialog_widget, 'Failed', ERROR_UNABLE_TO_STOP_NODE, ) - mock_quit.assert_called_once() + mock_exit.assert_called_once() -@patch.object(QApplication, 'quit') +@patch.object(QApplication, 'exit') def test_on_success_close_node(mock_quit, on_close_progress_dialog_widget): """Test _on_success_close_node method to ensure application quits after node closes.""" on_close_progress_dialog_widget._on_success_close_node() @@ -126,8 +126,8 @@ def test_on_success_close_node(mock_quit, on_close_progress_dialog_widget): @patch.object(LnNodeServerManager, 'stop_server_from_close_button') -@patch.object(QApplication, 'quit') -def test_close_node_app_when_node_running(mock_quit, mock_stop_server, on_close_progress_dialog_widget): +@patch.object(QApplication, 'exit') +def test_close_node_app_when_node_running(mock_exit, mock_stop_server, on_close_progress_dialog_widget): """Test _close_node_app method when node is still running.""" mock_state = MagicMock(return_value=QProcess.Running) on_close_progress_dialog_widget.ln_node_manage.process.state = mock_state @@ -135,16 +135,16 @@ def test_close_node_app_when_node_running(mock_quit, mock_stop_server, on_close_ assert on_close_progress_dialog_widget.is_backup_onprogress is False assert on_close_progress_dialog_widget.is_node_closing_onprogress is True mock_stop_server.assert_called_once() - mock_quit.assert_not_called() + mock_exit.assert_not_called() -@patch.object(QApplication, 'quit') -def test_close_node_app_when_node_not_running(mock_quit, on_close_progress_dialog_widget): +@patch.object(QApplication, 'exit') +def test_close_node_app_when_node_not_running(mock_exit, on_close_progress_dialog_widget): """Test _close_node_app method when node is not running.""" mock_state = MagicMock(return_value=QProcess.NotRunning) on_close_progress_dialog_widget.ln_node_manage.process.state = mock_state on_close_progress_dialog_widget._close_node_app() - mock_quit.assert_called_once() + mock_exit.assert_called_once() def test_close_event_backup_in_progress(on_close_progress_dialog_widget): diff --git a/unit_tests/tests/ui_tests/ui_channel_management_test.py b/unit_tests/tests/ui_tests/ui_channel_management_test.py index 09a3adf..67b9775 100644 --- a/unit_tests/tests/ui_tests/ui_channel_management_test.py +++ b/unit_tests/tests/ui_tests/ui_channel_management_test.py @@ -77,10 +77,10 @@ def test_show_available_channels_positive(channel_management_widget, qtbot): assert channel_management_widget.list_v_box_layout.count() > 1 assert isinstance(channel_management_widget.list_frame, QFrame) - assert isinstance(channel_management_widget.local_balance, QLabel) - assert channel_management_widget.local_balance.text() == '1000' - assert isinstance(channel_management_widget.remote_balance, QLabel) - assert channel_management_widget.remote_balance.text() == '500' + assert isinstance(channel_management_widget.local_balance_value, QLabel) + assert isinstance(channel_management_widget.remote_balance_value, QLabel) + assert channel_management_widget.local_balance_value.text() == '1000' + assert channel_management_widget.remote_balance_value.text() == '500' def test_show_available_channels_no_channels(channel_management_widget): @@ -177,10 +177,10 @@ def test_show_available_channels_with_none_asset_id(mock_create_pixmap, channel_ # 1 for the channel, 1 for the spacer assert channel_management_widget.list_v_box_layout.count() == 2 assert channel_management_widget.list_frame.findChild( - QLabel, 'local_balance', + QLabel, 'local_balance_value', ).text() == '1000' assert channel_management_widget.list_frame.findChild( - QLabel, 'remote_balance', + QLabel, 'remote_balance_value', ).text() == '500' @@ -231,13 +231,13 @@ def test_show_available_channels_with_asset_id(mock_create_pixmap, channel_manag # 1 for the channel, 1 for spacer assert channel_management_widget.list_v_box_layout.count() == 2 assert channel_management_widget.list_frame.findChild( - QLabel, 'local_balance', + QLabel, 'local_balance_value', ).text() == '777' assert channel_management_widget.list_frame.findChild( - QLabel, 'remote_balance', + QLabel, 'remote_balance_value', ).text() == '0' tooltip = channel_management_widget.list_frame.findChild( - QLabel, 'status', + QLabel, 'status_pixmap', ).toolTip() assert tooltip == 'opening' asset_label = channel_management_widget.list_frame.findChild( @@ -354,7 +354,7 @@ def test_show_available_channels_status_change(channel_management_widget, qtbot) # Assert status color and tooltip for "Closing" assert channel_management_widget.list_frame.findChild( - QLabel, 'status', + QLabel, 'status_pixmap', ).toolTip() == 'closing' # Change channel status @@ -363,7 +363,7 @@ def test_show_available_channels_status_change(channel_management_widget, qtbot) # Assert status color and tooltip for "Opening" assert channel_management_widget.list_frame.findChild( - QLabel, 'status', + QLabel, 'status_pixmap', ).toolTip() == 'opening' @@ -424,3 +424,38 @@ def test_channel_detail_event(channel_management_widget): # Assert that the exec method was called on the dialog box mock_channel_detail_dialog_box.return_value.exec.assert_called_once() + + +@patch('src.utils.helpers.create_circular_pixmap') +def test_show_available_channels_with_asset_id_lookup(mock_create_pixmap, channel_management_widget): + """Test show_available_channels with asset lookup functionality.""" + # Mock the return value of create_circular_pixmap to be a QPixmap instance + mock_create_pixmap.return_value = QPixmap(16, 16) + + # Mock valid channel data with asset_id + mock_channel = MagicMock() + mock_channel.peer_pubkey = 'mock_pubkey' + mock_channel.asset_local_amount = 777 + mock_channel.asset_remote_amount = 0 + mock_channel.asset_id = 'test_asset_123' + mock_channel.ready = True + + # Set up the asset lookup dictionary + channel_management_widget._view_model.channel_view_model.total_asset_lookup_list = { + 'test_asset_123': 'Test Asset Name', + } + + channel_management_widget._view_model.channel_view_model.channels = [ + mock_channel, + ] + + channel_management_widget.show_available_channels() + + # Assertions to verify the correct display of channel information + assert channel_management_widget.list_v_box_layout.count() == 2 + assert channel_management_widget.list_frame.findChild( + QLabel, 'asset_name', + ).text() == 'Test Asset Name' + assert channel_management_widget.list_frame.findChild( + QLabel, 'asset_id', + ).text() == 'test_asset_123' diff --git a/unit_tests/tests/ui_tests/ui_fungible_asset_test.py b/unit_tests/tests/ui_tests/ui_fungible_asset_test.py index 6c2e499..a917ddc 100644 --- a/unit_tests/tests/ui_tests/ui_fungible_asset_test.py +++ b/unit_tests/tests/ui_tests/ui_fungible_asset_test.py @@ -44,15 +44,6 @@ def mock_fungible_asset_view_model(): return mock_view_model -# @pytest.fixture -# def fungible_asset_nodeinfo_mock(): -# """Provides a mock for fungible asset nodeinfo.""" -# mock = MagicMock() -# mock.is_ready = MagicMock(return_value=True) -# mock.get_network_name = MagicMock(return_value='Testnet') -# return mock - - @pytest.fixture def create_fungible_asset_widget(qtbot, mock_fungible_asset_view_model): """Fixture to create the FungibleAssetWidget.""" @@ -93,7 +84,7 @@ def test_fungible_asset_widget_show_assets(create_fungible_asset_widget: Fungibl bitcoin_mock.name = 'bitcoin' bitcoin_mock.balance.future = '0.5' bitcoin_mock.ticker = 'BTC' - bitcoin_mock.asset_id = 'rgb123rgb' + bitcoin_mock.asset_id = 'rBTC' # Set the mock assets in the view model widget._view_model.main_asset_view_model.assets.vanilla = bitcoin_mock @@ -104,7 +95,7 @@ def test_fungible_asset_widget_show_assets(create_fungible_asset_widget: Fungibl # Check that the asset name was set correctly assert widget.asset_name.text() == 'bitcoin' - assert widget.address.text() == 'rgb123rgb' + assert widget.address.text() == 'rBTC' assert widget.amount.text() == '0.5' assert widget.asset_logo.pixmap() is not None @@ -193,7 +184,7 @@ def test_show_assets_with_various_assets(create_fungible_asset_widget, qtbot): 'iris_wallet_desktop', 'asset_name', None, ) assert widget.address_header.text() == QCoreApplication.translate( - 'iris_wallet_desktop', 'address', None, + 'iris_wallet_desktop', 'asset_id', None, ) assert widget.amount_header.text() == QCoreApplication.translate( 'iris_wallet_desktop', 'on_chain_balance', None, @@ -235,14 +226,9 @@ def test_update_faucet_availability_when_unavailable(create_fungible_asset_widge # Check if the stylesheet was set correctly faucet_mock.setStyleSheet.assert_called_once_with( - 'Text-align:left;' - 'font: 15px "Inter";' - 'color: rgb(120, 120, 120);' - 'padding: 17.5px 16px;' - 'background-image: url(:/assets/right_small.png);' - 'background-repeat: no-repeat;' - 'background-position: right center;' - 'background-origin: content;', + 'Text-align:left;font: 15px "Inter";color: rgb(120, 120, 120);padding: 17.5px 16px;' + 'background-image: url(:/assets/right_small.png);background-repeat: no-repeat;' + 'background-position: right center;background-origin: content;', ) # Check if the click event handler was disconnected @@ -304,24 +290,6 @@ def test_show_faucet_unavailability_message(mocker, mock_fungible_asset_view_mod ) -def test_set_bitcoin_address(mock_fungible_asset_view_model): - """Test that the set_bitcoin_address method sets the text of the provided QLabel.""" - # Create a mock QLabel - label_mock = MagicMock(spec=QLabel) - - # Example Bitcoin address - bitcoin_address = '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa' - - # Create an instance of the widget - widget = FungibleAssetWidget(mock_fungible_asset_view_model) - - # Call the method - widget.set_bitcoin_address(label=label_mock, address=bitcoin_address) - - # Verify that QLabel.setText is called with the correct address - label_mock.setText.assert_called_once_with(bitcoin_address) - - def test_stop_fungible_loading_screen(create_fungible_asset_widget): """Test that the stop_fungible_loading_screen method stops the loading screen and enables refresh button.""" widget = create_fungible_asset_widget @@ -571,9 +539,6 @@ def test_create_fungible_card(create_fungible_asset_widget, qtbot): AssetType.BITCOIN.value.lower() }' - # Test signal connection for Bitcoin address updates - assert widget.signal_connected is True - def test_show_assets(create_fungible_asset_widget, qtbot): """Test the show_assets method to ensure assets are cleared and the Bitcoin address signal is managed correctly.""" @@ -613,20 +578,3 @@ def test_show_assets(create_fungible_asset_widget, qtbot): # Call show_assets widget.show_assets() - - # Ensure get_bitcoin_address is called - widget._view_model.receive_bitcoin_view_model.get_bitcoin_address.assert_called_once() - - # Check that address.disconnect() was called if signal_connected is True - widget._view_model.receive_bitcoin_view_model.address.disconnect.assert_called_once() - - # Check if signal_connected is set to False after disconnection - assert widget.signal_connected is False - - # Test the case when signal_connected is False and no disconnection should happen - widget.signal_connected = False - widget.show_assets() # Should not attempt to disconnect again - - # Ensure address.disconnect is not called in this case - # Should still be called once from previous check - widget._view_model.receive_bitcoin_view_model.address.disconnect.assert_called_once() diff --git a/unit_tests/tests/ui_tests/ui_help_test.py b/unit_tests/tests/ui_tests/ui_help_test.py index 238478e..4a80fcd 100644 --- a/unit_tests/tests/ui_tests/ui_help_test.py +++ b/unit_tests/tests/ui_tests/ui_help_test.py @@ -1,14 +1,16 @@ """Unit test for Enter Help UI.""" # Disable the redefined-outer-name warning as # it's normal to pass mocked objects in test functions -# pylint: disable=redefined-outer-name,unused-argument +# pylint: disable=redefined-outer-name,unused-argument,protected-access from __future__ import annotations from unittest.mock import MagicMock import pytest +from PySide6.QtCore import Qt from PySide6.QtWidgets import QFrame from PySide6.QtWidgets import QLabel +from PySide6.QtWidgets import QVBoxLayout from src.viewmodels.main_view_model import MainViewModel from src.views.ui_help import HelpWidget @@ -51,13 +53,21 @@ def test_create_help_card(help_widget): assert isinstance(help_card_frame, QFrame) assert help_card_frame.objectName() == 'help_card_frame' + assert help_card_frame.minimumSize().width() == 492 + assert help_card_frame.minimumSize().height() == 70 + assert help_card_frame.maximumSize().width() == 335 + assert help_card_frame.maximumSize().height() == 16777215 + assert help_card_frame.frameShape() == QFrame.StyledPanel + assert help_card_frame.frameShadow() == QFrame.Raised # Check that the title and detail are set correctly title_label = help_card_frame.findChild(QLabel, 'help_card_title_label') assert title_label.text() == title + assert title_label.wordWrap() is True detail_label = help_card_frame.findChild(QLabel, 'help_card_detail_label') assert detail_label.text() == detail + assert detail_label.wordWrap() is True # Check that the links are set correctly for link in links: @@ -65,3 +75,70 @@ def test_create_help_card(help_widget): assert link_label.text() == f"{link}" + assert link_label.minimumSize().height() == 15 + assert link_label.cursor().shape() == Qt.CursorShape.PointingHandCursor + assert link_label.textInteractionFlags() == Qt.TextBrowserInteraction + assert link_label.openExternalLinks() is True + + # Test with no links + help_card_frame = help_widget.create_help_card(title, detail, None) + assert help_card_frame.findChild(QLabel, 'http://example.com') is None + + # Test vertical layout properties + vertical_layout = help_card_frame.findChild( + QVBoxLayout, 'verticalLayout_3', + ) + assert vertical_layout.spacing() == 15 + margins = vertical_layout.contentsMargins() + assert ( + margins.left(), margins.top(), margins.right(), + margins.bottom(), + ) == (15, 20, 15, 20) + + +def test_create_help_frames(help_widget, qtbot): + """Test that help frames are created and distributed correctly into two columns.""" + # Call the method + help_widget.create_help_frames() + + # Get the card list from the model + card_list = help_widget._model.card_content + + # Verify horizontal layout exists and has correct spacer + assert help_widget.main_horizontal_spacer is not None + assert help_widget.main_horizontal_spacer.sizeHint().width() == 40 + assert help_widget.main_horizontal_spacer.sizeHint().height() == 20 + + # Verify vertical spacer exists and has correct properties + assert help_widget.main_vertical_spacer is not None + assert help_widget.main_vertical_spacer.sizeHint().width() == 20 + assert help_widget.main_vertical_spacer.sizeHint().height() == 40 + + # Find all help card frames + help_cards = help_widget.findChildren(QFrame, 'help_card_frame') + + # Clear any existing cards before checking + for card in help_cards[len(card_list):]: + card.deleteLater() + help_cards = help_cards[:len(card_list)] + + # Verify number of cards matches model content + assert len(help_cards) == len(card_list) + + # Verify each card's content + for i, card in enumerate(card_list): + help_card = help_cards[i] + + title_label = help_card.findChild(QLabel, 'help_card_title_label') + detail_label = help_card.findChild(QLabel, 'help_card_detail_label') + + assert title_label.text() == card.title + assert detail_label.text() == card.detail + + if card.links: + for link in card.links: + link_label = help_card.findChild(QLabel, str(link)) + assert link_label is not None + assert link_label.text() == f"{link}" diff --git a/unit_tests/tests/ui_tests/ui_send_bitcoin_test.py b/unit_tests/tests/ui_tests/ui_send_bitcoin_test.py index ca70dd0..a9a7ee4 100644 --- a/unit_tests/tests/ui_tests/ui_send_bitcoin_test.py +++ b/unit_tests/tests/ui_tests/ui_send_bitcoin_test.py @@ -97,20 +97,44 @@ def test_send_bitcoin_button(send_bitcoin_widget: SendBitcoinWidget, qtbot): def test_handle_button_enabled(send_bitcoin_widget: SendBitcoinWidget): """Test the handle_button_enabled method.""" + # Test valid address, amount, fee and payment send_bitcoin_widget.send_bitcoin_page.asset_address_value.setText( '1BitcoinAddress', ) send_bitcoin_widget.send_bitcoin_page.asset_amount_value.setText('0.001') send_bitcoin_widget.send_bitcoin_page.fee_rate_value.setText('0.0001') + send_bitcoin_widget.send_bitcoin_page.pay_amount = 1000 + send_bitcoin_widget.send_bitcoin_page.spendable_amount = 2000 send_bitcoin_widget.handle_button_enabled() - assert send_bitcoin_widget.send_bitcoin_page.send_btn.isEnabled() - # Clear one of the inputs to disable the button again + # Test invalid address (empty) send_bitcoin_widget.send_bitcoin_page.asset_address_value.clear() send_bitcoin_widget.handle_button_enabled() + assert not send_bitcoin_widget.send_bitcoin_page.send_btn.isEnabled() + + # Test invalid amount (zero) + send_bitcoin_widget.send_bitcoin_page.asset_amount_value.setText('0') + send_bitcoin_widget.handle_button_enabled() + assert not send_bitcoin_widget.send_bitcoin_page.send_btn.isEnabled() + + # Test invalid fee (empty) + send_bitcoin_widget.send_bitcoin_page.asset_amount_value.setText('0.001') + send_bitcoin_widget.send_bitcoin_page.fee_rate_value.clear() + send_bitcoin_widget.handle_button_enabled() + assert not send_bitcoin_widget.send_bitcoin_page.send_btn.isEnabled() + # Test invalid fee (zero) + send_bitcoin_widget.send_bitcoin_page.fee_rate_value.setText('0') + send_bitcoin_widget.handle_button_enabled() + assert not send_bitcoin_widget.send_bitcoin_page.send_btn.isEnabled() + + # Test invalid payment (pay_amount > spendable_amount) + send_bitcoin_widget.send_bitcoin_page.fee_rate_value.setText('0.0001') + send_bitcoin_widget.send_bitcoin_page.pay_amount = 3000 + send_bitcoin_widget.send_bitcoin_page.spendable_amount = 2000 + send_bitcoin_widget.handle_button_enabled() assert not send_bitcoin_widget.send_bitcoin_page.send_btn.isEnabled() diff --git a/unit_tests/tests/ui_tests/ui_send_rgb_asset_test.py b/unit_tests/tests/ui_tests/ui_send_rgb_asset_test.py index e3d3ee3..3216ff5 100644 --- a/unit_tests/tests/ui_tests/ui_send_rgb_asset_test.py +++ b/unit_tests/tests/ui_tests/ui_send_rgb_asset_test.py @@ -75,9 +75,22 @@ def test_handle_button_enabled(send_rgb_asset_widget: SendRGBAssetWidget, qtbot) 'blind_utxo_123', ) send_rgb_asset_widget.send_rgb_asset_page.asset_amount_value.setText('10') + + # Mocking the spendable balance to be greater than 0 + send_rgb_asset_widget.asset_spendable_balance = 50 send_rgb_asset_widget.handle_button_enabled() assert send_rgb_asset_widget.send_rgb_asset_page.send_btn.isEnabled() is True + # Test with invalid amount (zero) + send_rgb_asset_widget.send_rgb_asset_page.asset_amount_value.setText('0') + send_rgb_asset_widget.handle_button_enabled() + assert send_rgb_asset_widget.send_rgb_asset_page.send_btn.isEnabled() is False + + # Test with valid address and invalid spendable balance + send_rgb_asset_widget.asset_spendable_balance = 0 + send_rgb_asset_widget.handle_button_enabled() + assert send_rgb_asset_widget.send_rgb_asset_page.send_btn.isEnabled() is False + def test_set_asset_balance(send_rgb_asset_widget: SendRGBAssetWidget, qtbot): """Test the set_asset_balance method.""" diff --git a/unit_tests/tests/utils_test/common_utils_test.py b/unit_tests/tests/utils_test/common_utils_test.py index 04405f3..972c141 100644 --- a/unit_tests/tests/utils_test/common_utils_test.py +++ b/unit_tests/tests/utils_test/common_utils_test.py @@ -968,7 +968,7 @@ def test_sigterm_handler(mock_qapp_instance, mock_ln_node_manager_get_instance, QMessageBox.Ok | QMessageBox.Cancel, ) mock_ln_node_manager.stop_server_from_close_button.assert_called_once() - mock_qapp.quit.assert_called_once() + mock_qapp.exit.assert_called_once() # Reset mocks for next case mock_qmessagebox_warning.reset_mock() diff --git a/unit_tests/tests/viewmodel_tests/header_frame_view_model_test.py b/unit_tests/tests/viewmodel_tests/header_frame_view_model_test.py index 3f36684..f1105ca 100644 --- a/unit_tests/tests/viewmodel_tests/header_frame_view_model_test.py +++ b/unit_tests/tests/viewmodel_tests/header_frame_view_model_test.py @@ -2,172 +2,127 @@ # pylint: disable=redefined-outer-name,unused-argument from __future__ import annotations -from unittest.mock import MagicMock -from unittest.mock import Mock -from unittest.mock import patch - -import pytest - +from src.utils.constant import PING_DNS_SERVER_CALL_INTERVAL from src.viewmodels.header_frame_view_model import HeaderFrameViewModel from src.viewmodels.header_frame_view_model import NetworkCheckerThread -@pytest.fixture -def mock_network_checker(): - """Mock NetworkCheckerThread""" - with patch('src.viewmodels.header_frame_view_model.NetworkCheckerThread') as mock: - mock_thread = MagicMock() - mock.return_value = mock_thread - yield mock_thread +def test_network_checker_thread_success(mocker, qtbot): + """Test that NetworkCheckerThread emits True when network is available.""" + network_checker = NetworkCheckerThread() + + mocker.patch.object( + network_checker, 'check_internet_conn', return_value=True, + ) + + def signal_received(value): + assert value is True + + network_checker.network_status_signal.connect(signal_received) + + network_checker.start() + qtbot.waitSignal(network_checker.finished, timeout=1000) + + +def test_network_checker_thread_failure(mocker, qtbot): + """Test that NetworkCheckerThread emits False when network is unavailable.""" + network_checker = NetworkCheckerThread() + + mocker.patch.object( + network_checker, 'check_internet_conn', return_value=False, + ) + + def signal_received(value): + assert value is False + + network_checker.network_status_signal.connect(signal_received) + + network_checker.start() + qtbot.waitSignal(network_checker.finished, timeout=1000) + + +def test_check_internet_conn_success(mocker): + """Test check_internet_conn returns True when socket connection succeeds.""" + network_checker = NetworkCheckerThread() + + mocker.patch('socket.create_connection', return_value=True) + assert network_checker.check_internet_conn() is True + + +def test_check_internet_conn_failure(mocker): + """Test check_internet_conn returns False when socket connection fails.""" + network_checker = NetworkCheckerThread() + + mocker.patch('socket.create_connection', side_effect=OSError) + + assert network_checker.check_internet_conn() is False + + +def test_header_frame_view_model_init(mocker): + """Test that HeaderFrameViewModel initializes correctly with a running timer.""" + mock_timer = mocker.patch( + 'src.viewmodels.header_frame_view_model.QTimer', + ) -@pytest.fixture -def header_frame_view_model(mock_network_checker): - """Fixture for creating a HeaderFrameViewModel instance.""" view_model = HeaderFrameViewModel() - return view_model + # Ensure QTimer was instantiated + mock_timer.assert_called_once() + + # Retrieve the mocked QTimer instance + mock_timer_instance = mock_timer.return_value -def test_header_frame_view_model_init(mock_network_checker): - """Test HeaderFrameViewModel initialization.""" + # Ensure timer settings and start behavior are correct + mock_timer_instance.setInterval.assert_called_once_with( + PING_DNS_SERVER_CALL_INTERVAL, + ) + mock_timer_instance.timeout.connect.assert_called_once_with( + view_model.start_network_check, + ) + mock_timer_instance.start.assert_called_once() + + +def test_header_frame_view_model_network_check(mocker): + """Test that start_network_check creates a NetworkCheckerThread and starts it.""" view_model = HeaderFrameViewModel() - assert hasattr(view_model, 'network_checker') - assert view_model.network_checker == mock_network_checker - mock_network_checker.start.assert_called_once() + mock_thread = mocker.patch( + 'src.viewmodels.header_frame_view_model.NetworkCheckerThread', + ) + mock_instance = mock_thread.return_value + view_model.start_network_check() -def test_handle_network_status(header_frame_view_model): - """Test handle_network_status method.""" - mock_signal = Mock() - header_frame_view_model.network_status_signal.connect(mock_signal) + mock_thread.assert_called_once() + mock_instance.network_status_signal.connect.assert_called_once_with( + view_model.handle_network_status, + ) + mock_instance.start.assert_called_once() - header_frame_view_model.handle_network_status(True) - mock_signal.assert_called_once_with(True) +def test_header_frame_view_model_handle_network_status(): + """Test that handle_network_status emits the correct signal.""" + view_model = HeaderFrameViewModel() + received_signals = [] + + def signal_received(value): + received_signals.append(value) + view_model.network_status_signal.connect(signal_received) -def test_stop_network_checker(header_frame_view_model, mock_network_checker): - """Test stop_network_checker method.""" - header_frame_view_model.stop_network_checker() + view_model.handle_network_status(True) + view_model.handle_network_status(False) - mock_network_checker.stop.assert_called_once() + assert received_signals == [True, False] + + +def test_header_frame_view_model_stop_network_checker(mocker): + """Test that stop_network_checker stops the timer.""" + view_model = HeaderFrameViewModel() + mock_timer = mocker.patch.object(view_model.timer, 'stop') -@patch('src.viewmodels.header_frame_view_model.NetworkCheckerThread') -def test_network_checker_thread_init(mock_thread_class): - """Test NetworkCheckerThread initialization.""" - mock_thread = mock_thread_class.return_value - mock_thread.running = True - - thread = mock_thread_class() - assert thread.running is True - + view_model.stop_network_checker() -@patch('socket.create_connection') -@patch('src.viewmodels.header_frame_view_model.NetworkCheckerThread') -def test_check_internet_conn_success(mock_thread_class, mock_socket): - """Test check_internet_conn method when connection succeeds.""" - mock_thread = mock_thread_class.return_value - mock_socket.return_value = True - - # Don't mock check_internet_conn itself since we want to test the actual implementation - result = NetworkCheckerThread.check_internet_conn(mock_thread) - mock_socket.assert_called_once_with(('8.8.8.8', 53), timeout=3) - assert result is True - - -@patch('socket.create_connection') -@patch('src.viewmodels.header_frame_view_model.NetworkCheckerThread') -def test_check_internet_conn_failure(mock_thread_class, mock_socket): - """Test check_internet_conn method when connection fails.""" - mock_thread = mock_thread_class.return_value - mock_socket.side_effect = OSError() - - # Don't mock check_internet_conn itself since we want to test the actual implementation - result = NetworkCheckerThread.check_internet_conn(mock_thread) - mock_socket.assert_called_once_with(('8.8.8.8', 53), timeout=3) - assert result is False - - -@patch('src.viewmodels.header_frame_view_model.NetworkCheckerThread') -def test_network_checker_stop(mock_thread_class): - """Test NetworkCheckerThread stop method.""" - mock_thread = mock_thread_class.return_value - mock_thread.running = True - - mock_thread.stop() - mock_thread.running = False - mock_thread.isRunning.return_value = False - - assert not mock_thread.running - assert not mock_thread.isRunning() - - -@patch('PySide6.QtCore.QThread.quit') -@patch('PySide6.QtCore.QThread.wait') -def test_network_checker_thread_stop_complete(mock_wait, mock_quit): - """Test NetworkCheckerThread stop method completely stops the thread.""" - # Arrange - thread = NetworkCheckerThread() - thread.running = True - thread.check_internet_conn = Mock() # Mock method to ensure it doesn't run - # Mock signal to avoid real signal emission - thread.network_status_signal = Mock() - thread.msleep = Mock() # Mock msleep to prevent delay - - # Mock `run` method so it doesn't loop infinitely - def mocked_run(): - while thread.running: - # This is just to simulate run behavior - thread.check_internet_conn() - thread.network_status_signal.emit(True) - thread.msleep(5000) - - # Replace `run` with mocked logic - thread.run = mocked_run - - # Act - thread.stop() - - # Assert - # Check if `running` is False after stopping - assert thread.running is False - # Ensure that `quit` and `wait` methods were called once - mock_quit.assert_called_once() - mock_wait.assert_called_once() - - -@patch('src.viewmodels.header_frame_view_model.NetworkCheckerThread') -def test_network_checker_run(mock_thread_class): - """Test NetworkCheckerThread run method.""" - # Arrange - mock_thread = mock_thread_class.return_value - mock_thread.running = True - mock_thread.check_internet_conn = Mock( - side_effect=[True, False], - ) # Mock network check - mock_thread.network_status_signal = Mock() # Mock signal - mock_thread.msleep = Mock() # Mock sleep to prevent delay - - def mocked_run(): - """Simulate the run method logic.""" - while mock_thread.running: - is_connected = mock_thread.check_internet_conn() - mock_thread.network_status_signal.emit(is_connected) - mock_thread.running = False # Stop after one iteration - mock_thread.msleep(5000) - - # Replace `run` with mocked logic - mock_thread.run = mocked_run - - # Act - mock_thread.run() - - # Assert - # Ensure `check_internet_conn` was called once - mock_thread.check_internet_conn.assert_called_once() - # Verify `msleep` was called once with the correct interval - mock_thread.msleep.assert_called_once_with(5000) - # Check that the signal was emitted with the correct value - mock_thread.network_status_signal.emit.assert_called_once_with(True) + mock_timer.assert_called_once() diff --git a/unit_tests/tests/viewmodel_tests/splash_view_model_test.py b/unit_tests/tests/viewmodel_tests/splash_view_model_test.py index dc43f99..1076d0e 100644 --- a/unit_tests/tests/viewmodel_tests/splash_view_model_test.py +++ b/unit_tests/tests/viewmodel_tests/splash_view_model_test.py @@ -49,7 +49,7 @@ def test_on_error_common_exception(mock_qapp, mock_toast_manager, mock_setting_r mock_toast_manager.error.assert_called_once_with( description='Custom error message', ) - mock_qapp.instance().quit.assert_called_once() + mock_qapp.instance().exit.assert_called_once() @patch('src.viewmodels.splash_view_model.SettingRepository') @@ -66,7 +66,7 @@ def test_on_error_general_exception(mock_qapp, mock_toast_manager, mock_setting_ mock_toast_manager.error.assert_called_once_with( description=ERROR_SOMETHING_WENT_WRONG, ) - mock_qapp.instance().quit.assert_called_once() + mock_qapp.instance().exit.assert_called_once() @patch('src.viewmodels.splash_view_model.SettingRepository') diff --git a/unit_tests/tests/viewmodel_tests/wallet_transfer_selection_view_model_test.py b/unit_tests/tests/viewmodel_tests/wallet_transfer_selection_view_model_test.py index 8de2f75..5f24121 100644 --- a/unit_tests/tests/viewmodel_tests/wallet_transfer_selection_view_model_test.py +++ b/unit_tests/tests/viewmodel_tests/wallet_transfer_selection_view_model_test.py @@ -49,7 +49,7 @@ def test_on_ln_node_error(wallet_transfer_selection_view_model): mock_message_box.assert_called_once_with( 'critical', message_text='Unable to start node,Please close application and restart', ) - mock_instance.quit.assert_called_once() + mock_instance.exit.assert_called_once() def test_on_ln_node_already_running(wallet_transfer_selection_view_model):