Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 206 additions & 58 deletions tests/test_frontend_tkinter_flightcontroller_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
SPDX-License-Identifier: GPL-3.0-or-later
"""

import tkinter as tk
from unittest.mock import Mock, patch

import pytest
Expand Down Expand Up @@ -907,15 +908,15 @@ def mock_successful_download(callback) -> ParDict:
for name in expected_defaults:
assert name in result

def test_user_receives_none_when_no_parameters_downloaded(
def test_user_receives_empty_parameter_defaults_when_no_parameters_downloaded(
self, mock_tkinter_context, configured_flight_controller: Mock
) -> None:
"""
User receives None when no parameters have been downloaded.
User receives empty parameter defaults when no parameters have been downloaded.

GIVEN: A window that has not completed parameter download
WHEN: The user requests parameter default values
THEN: The method should return None
THEN: An empty but valid parameter dictionary is returned
AND: No errors should occur
"""
# Given
Expand All @@ -936,61 +937,6 @@ def test_user_receives_none_when_no_parameters_downloaded(
assert isinstance(result, ParDict)
assert len(result) == 0

def test_user_sees_accurate_progress_tracking_during_parameter_download(
self, mock_tkinter_context, configured_flight_controller: Mock
) -> None:
"""
User sees accurate progress tracking during parameter download.

GIVEN: A flight controller with a known number of parameters
WHEN: The user starts parameter download
THEN: Progress should start at 0%
AND: Progress should increment accurately with each parameter
AND: Progress should reach 100% upon completion
"""
# Given
stack, patches = mock_tkinter_context()
total_parameters = 10
progress_history = []

def mock_tracked_download(callback) -> ParDict:
for current in range(1, total_parameters + 1):
if callback:
callback(current, total_parameters)
progress_history.append((current, total_parameters))
# Return a ParDict with test parameters
result = ParDict()
for i in range(total_parameters):
result[f"PARAM_{i}"] = Par(float(i), f"Test param {i}")
return result

with stack:
for patch_obj in patches:
stack.enter_context(patch_obj)

window = FlightControllerInfoWindow.__new__(FlightControllerInfoWindow)
window.presenter = FlightControllerInfoPresenter(configured_flight_controller)
window.presenter.download_parameters = Mock(side_effect=mock_tracked_download)
window.root = Mock()
window.progress_bar = Mock()
window.progress_label = Mock()
window.progress_frame = Mock()

# Mock the update_progress_bar method to track calls
progress_calls = []
window.update_progress_bar = Mock(side_effect=lambda c, m: progress_calls.append((c, m)))

# When
window._download_flight_controller_parameters()

# Then - The download method was called
window.presenter.download_parameters.assert_called_once()

# And progress tracking would be accurate if callback was used
# (This tests the structure is in place for progress reporting)
assert hasattr(window, "update_progress_bar")
assert callable(window.update_progress_bar)


class TestFlightControllerErrorHandling:
"""Test error handling scenarios for flight controller operations in BDD style."""
Expand Down Expand Up @@ -1075,3 +1021,205 @@ def test_user_sees_informative_display_when_flight_controller_info_unavailable(

# And no information rows are created for empty info
assert mock_entry.call_count == 0


class TestFlightControllerInfoWindowInitialization: # pylint: disable=too-few-public-methods
"""Test window initialization and setup behavior in BDD style."""

def test_user_experiences_proper_window_initialization_and_setup(
self, mock_tkinter_context, configured_flight_controller: Mock
) -> None:
"""
Test that users experience proper window initialization and setup.

GIVEN: A flight controller is available and UI environment is ready
WHEN: A user creates a FlightControllerInfoWindow
THEN: The window is properly initialized with correct title, geometry, and components
AND: Flight controller information is logged
AND: Parameter download is scheduled
"""
# Given
stack, patches = mock_tkinter_context()

with stack:
# Start all patches for UI isolation
for patch_obj in patches:
stack.enter_context(patch_obj)

mock_root = Mock()
mock_mainloop = Mock()

with (
patch("tkinter.ttk.Frame"),
patch("tkinter.ttk.Progressbar"),
patch("tkinter.ttk.Label"),
patch.object(FlightControllerInfoWindow, "_create_info_display") as mock_create_display,
patch.object(FlightControllerInfoWindow, "_download_flight_controller_parameters") as mock_download,
patch("tkinter.Tk", return_value=mock_root) as mock_tk,
patch.object(mock_root, "mainloop", mock_mainloop),
patch.object(mock_root, "after") as mock_after,
):
# When
window = FlightControllerInfoWindow(configured_flight_controller)

# Then - Window is properly initialized
mock_tk.assert_called_once()
assert window.root == mock_root
assert isinstance(window.presenter, FlightControllerInfoPresenter)
assert window.presenter.flight_controller == configured_flight_controller

# And window properties are set correctly
mock_root.title.assert_called_once()
mock_root.geometry.assert_called_once_with("500x420")

# And UI components are created
mock_create_display.assert_called_once()

# And flight controller info is logged
configured_flight_controller.info.log_flight_controller_info.assert_called_once()

# And parameter download is scheduled
mock_after.assert_called_once_with(50, mock_download)

# And mainloop is called to start the UI
mock_mainloop.assert_called_once()


class TestFlightControllerInfoProgressBarErrorHandling:
"""Test progress bar error handling in BDD style."""

def test_user_sees_graceful_handling_when_progress_bar_widgets_are_destroyed(
self, mock_tkinter_context, configured_flight_controller: Mock
) -> None:
"""
Test that users see graceful handling when progress bar widgets are destroyed.

GIVEN: A flight controller info window with progress bar widgets that get destroyed
WHEN: The system attempts to update progress
THEN: The update is safely skipped without errors
"""
# Given
stack, patches = mock_tkinter_context()

with stack:
# Start all patches for UI isolation
for patch_obj in patches:
stack.enter_context(patch_obj)

with (
patch("tkinter.ttk.Frame"),
patch("tkinter.ttk.Progressbar"),
patch("tkinter.ttk.Label"),
patch.object(FlightControllerInfoWindow, "_create_info_display"),
patch.object(FlightControllerInfoWindow, "_download_flight_controller_parameters"),
patch("tkinter.Tk.mainloop"),
):
window = FlightControllerInfoWindow.__new__(FlightControllerInfoWindow)
window.presenter = FlightControllerInfoPresenter(configured_flight_controller)

# Simulate progress bar widget being destroyed
window.progress_bar = None

# When
window.update_progress_bar(5, 10)

# Then - No errors occur, method returns early safely

def test_user_sees_graceful_handling_when_window_lift_fails_due_to_tcl_error(
self, mock_tkinter_context, configured_flight_controller: Mock
) -> None:
"""
Test that users see graceful handling when window lift fails due to TclError.

GIVEN: A flight controller info window where tkinter operations may fail
WHEN: The system attempts to update progress but window lift fails
THEN: The error is logged and progress update continues safely
"""
# Given
stack, patches = mock_tkinter_context()

with stack:
# Start all patches for UI isolation
for patch_obj in patches:
stack.enter_context(patch_obj)

with (
patch("tkinter.ttk.Frame"),
patch("tkinter.ttk.Progressbar"),
patch("tkinter.ttk.Label"),
patch.object(FlightControllerInfoWindow, "_create_info_display"),
patch.object(FlightControllerInfoWindow, "_download_flight_controller_parameters"),
patch("tkinter.Tk.mainloop"),
):
window = FlightControllerInfoWindow.__new__(FlightControllerInfoWindow)
window.presenter = FlightControllerInfoPresenter(configured_flight_controller)

# Set up mock widgets
mock_progress_bar = Mock()
mock_progress_bar.__setitem__ = Mock()
mock_progress_label = Mock()
mock_root = Mock()
mock_root.lift.side_effect = tk.TclError("window not found")

window.progress_bar = mock_progress_bar
window.progress_label = mock_progress_label
window.root = mock_root
window.progress_frame = Mock()

# When
with patch("logging.error") as mock_log_error:
window.update_progress_bar(5, 10)

# Then - Error is logged
mock_log_error.assert_called_once()
assert "Lifting window:" in mock_log_error.call_args[0][0]

# And progress update does NOT continue due to the return statement in the except block
mock_progress_bar.__setitem__.assert_not_called()

def test_user_sees_progress_bar_hidden_when_download_completes(
self, mock_tkinter_context, configured_flight_controller: Mock
) -> None:
"""
Test that users see progress bar hidden when download completes.

GIVEN: A flight controller info window with active progress tracking
WHEN: The parameter download completes (current == max)
THEN: The progress bar is automatically hidden
"""
# Given
stack, patches = mock_tkinter_context()

with stack:
# Start all patches for UI isolation
for patch_obj in patches:
stack.enter_context(patch_obj)

with (
patch("tkinter.ttk.Frame"),
patch("tkinter.ttk.Progressbar"),
patch("tkinter.ttk.Label"),
patch.object(FlightControllerInfoWindow, "_create_info_display"),
patch.object(FlightControllerInfoWindow, "_download_flight_controller_parameters"),
patch("tkinter.Tk.mainloop"),
):
window = FlightControllerInfoWindow.__new__(FlightControllerInfoWindow)
window.presenter = FlightControllerInfoPresenter(configured_flight_controller)

# Set up mock widgets
mock_progress_bar = Mock()
mock_progress_bar.__setitem__ = Mock()
mock_progress_label = Mock()
mock_progress_frame = Mock()
mock_root = Mock()

window.progress_bar = mock_progress_bar
window.progress_label = mock_progress_label
window.progress_frame = mock_progress_frame
window.root = mock_root

# When - Download completes
window.update_progress_bar(10, 10)

# Then - Progress bar is hidden
mock_progress_frame.pack_forget.assert_called_once()
10 changes: 5 additions & 5 deletions tests/test_frontend_tkinter_software_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,11 +344,11 @@ def test_status_messages(self) -> None:

# Test empty status
dialog.update_progress(50, "")
dialog.status_label.__setitem__.assert_not_called() # pylint: disable=no-member
dialog.status_label.__setitem__.assert_not_called()

# Test with status message
dialog.update_progress(75, "Processing...")
dialog.status_label.__setitem__.assert_called_with("text", "Processing...") # pylint: disable=no-member
dialog.status_label.__setitem__.assert_called_with("text", "Processing...")

def test_progress_value_bounds(self) -> None:
"""Test progress bar value bounds."""
Expand All @@ -357,15 +357,15 @@ def test_progress_value_bounds(self) -> None:

# Test minimum value
dialog.update_progress(0)
dialog.progress.__setitem__.assert_called_with("value", 0) # pylint: disable=no-member
dialog.progress.__setitem__.assert_called_with("value", 0)

# Test maximum value
dialog.update_progress(100)
dialog.progress.__setitem__.assert_called_with("value", 100) # pylint: disable=no-member
dialog.progress.__setitem__.assert_called_with("value", 100)

# Test value between bounds
dialog.update_progress(50)
dialog.progress.__setitem__.assert_called_with("value", 50) # pylint: disable=no-member
dialog.progress.__setitem__.assert_called_with("value", 50)

def test_version_info_display(self) -> None:
"""Test version info display in dialog."""
Expand Down