You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
OSUpdate app: Major rework with improved reliability and user experience
This commit is contained in:
@@ -9,6 +9,7 @@ internal_filesystem/SDLPointer_3
|
||||
# config files etc:
|
||||
internal_filesystem/data
|
||||
internal_filesystem/sdcard
|
||||
internal_filesystem/tests
|
||||
|
||||
# these tests contain actual NWC URLs:
|
||||
tests/manual_test_nwcwallet_alby.py
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
0.3.4 (unreleased)
|
||||
==================
|
||||
OSUpdate app: Major rework with improved reliability and user experience
|
||||
- OSUpdate app: add WiFi monitoring - shows "Waiting for WiFi..." instead of error when no connection
|
||||
- OSUpdate app: add automatic pause/resume on WiFi loss during downloads using HTTP Range headers
|
||||
- OSUpdate app: add user-friendly error messages with specific guidance for each error type
|
||||
- OSUpdate app: add "Check Again" button for easy retry after errors
|
||||
- OSUpdate app: add state machine for better app state management
|
||||
- OSUpdate app: add comprehensive test coverage (42 tests: 31 unit tests + 11 graphical tests)
|
||||
- OSUpdate app: refactor code into testable components (NetworkMonitor, UpdateChecker, UpdateDownloader)
|
||||
- OSUpdate app: improve download error recovery with progress preservation
|
||||
- OSUpdate app: improve timeout handling (5-minute wait for WiFi with clear messaging)
|
||||
- Tests: add test infrastructure with mock classes for network, HTTP, and partition operations
|
||||
- Tests: add graphical test helper utilities for UI verification and screenshot capture
|
||||
|
||||
0.3.3
|
||||
=====
|
||||
- Camera app: fix one-in-two "camera image stays blank" issue
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,329 @@
|
||||
import unittest
|
||||
import lvgl as lv
|
||||
import mpos
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Import graphical test helper
|
||||
from graphical_test_helper import (
|
||||
wait_for_render,
|
||||
capture_screenshot,
|
||||
find_label_with_text,
|
||||
verify_text_present,
|
||||
print_screen_labels
|
||||
)
|
||||
|
||||
|
||||
class TestOSUpdateGraphicalUI(unittest.TestCase):
|
||||
"""Graphical tests for OSUpdate app UI state."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
self.hardware_id = mpos.info.get_hardware_id()
|
||||
self.screenshot_dir = "tests/screenshots"
|
||||
|
||||
# Ensure screenshots directory exists
|
||||
# First check if tests directory exists
|
||||
try:
|
||||
os.stat("tests")
|
||||
except OSError:
|
||||
# We're not in the right directory, maybe running from root
|
||||
pass
|
||||
|
||||
# Now create screenshots directory if needed
|
||||
try:
|
||||
os.stat(self.screenshot_dir)
|
||||
except OSError:
|
||||
try:
|
||||
os.mkdir(self.screenshot_dir)
|
||||
except OSError:
|
||||
# Might already exist or permission issue
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after each test method."""
|
||||
# Navigate back to launcher
|
||||
mpos.ui.back_screen()
|
||||
wait_for_render(5)
|
||||
|
||||
def test_app_launches_successfully(self):
|
||||
"""Test that OSUpdate app launches without errors."""
|
||||
result = mpos.apps.start_app("com.micropythonos.osupdate")
|
||||
|
||||
self.assertTrue(result, "Failed to start OSUpdate app")
|
||||
wait_for_render(10)
|
||||
|
||||
# Get active screen
|
||||
screen = lv.screen_active()
|
||||
self.assertIsNotNone(screen, "No active screen after launch")
|
||||
|
||||
def test_ui_elements_exist(self):
|
||||
"""Test that all required UI elements are created."""
|
||||
result = mpos.apps.start_app("com.micropythonos.osupdate")
|
||||
self.assertTrue(result)
|
||||
wait_for_render(15)
|
||||
|
||||
screen = lv.screen_active()
|
||||
|
||||
# Find UI elements by searching for labels/text
|
||||
current_version_label = find_label_with_text(screen, "Installed OS version")
|
||||
self.assertIsNotNone(current_version_label, "Current version label not found")
|
||||
|
||||
# Check for force update checkbox text (might be "Force" or "Update")
|
||||
force_checkbox_found = verify_text_present(screen, "Force") or verify_text_present(screen, "force")
|
||||
self.assertTrue(force_checkbox_found, "Force checkbox text not found")
|
||||
|
||||
# Check for update button text (case insensitive)
|
||||
update_button_found = verify_text_present(screen, "Update") or verify_text_present(screen, "update")
|
||||
self.assertTrue(update_button_found, "Update button text not found")
|
||||
|
||||
def test_force_checkbox_initially_unchecked(self):
|
||||
"""Test that force update checkbox starts unchecked."""
|
||||
result = mpos.apps.start_app("com.micropythonos.osupdate")
|
||||
self.assertTrue(result)
|
||||
wait_for_render(15)
|
||||
|
||||
screen = lv.screen_active()
|
||||
|
||||
# Find checkbox - it's the first checkbox on the screen
|
||||
checkbox = None
|
||||
def find_checkbox(obj):
|
||||
nonlocal checkbox
|
||||
if checkbox:
|
||||
return
|
||||
# Check if this object is a checkbox
|
||||
try:
|
||||
# In LVGL, checkboxes have specific flags/properties
|
||||
if obj.get_child_count() >= 0: # It's a valid object
|
||||
# Try to get state - checkboxes respond to STATE.CHECKED
|
||||
state = obj.get_state()
|
||||
# If it has checkbox-like text, it's probably our checkbox
|
||||
for i in range(obj.get_child_count()):
|
||||
child = obj.get_child(i)
|
||||
if hasattr(child, 'get_text'):
|
||||
text = child.get_text()
|
||||
if text and "Force Update" in text:
|
||||
checkbox = obj.get_parent() if obj.get_parent() else obj
|
||||
return
|
||||
except:
|
||||
pass
|
||||
|
||||
# Recursively search children
|
||||
for i in range(obj.get_child_count()):
|
||||
child = obj.get_child(i)
|
||||
find_checkbox(child)
|
||||
|
||||
find_checkbox(screen)
|
||||
|
||||
if checkbox:
|
||||
state = checkbox.get_state()
|
||||
is_checked = bool(state & lv.STATE.CHECKED)
|
||||
self.assertFalse(is_checked, "Force Update checkbox should start unchecked")
|
||||
|
||||
def test_install_button_initially_disabled(self):
|
||||
"""Test that install button starts in disabled state."""
|
||||
result = mpos.apps.start_app("com.micropythonos.osupdate")
|
||||
self.assertTrue(result)
|
||||
wait_for_render(15)
|
||||
|
||||
screen = lv.screen_active()
|
||||
|
||||
# Find the button
|
||||
button = None
|
||||
def find_button(obj):
|
||||
nonlocal button
|
||||
if button:
|
||||
return
|
||||
# Check if this object contains "Update OS" text
|
||||
for i in range(obj.get_child_count()):
|
||||
child = obj.get_child(i)
|
||||
if hasattr(child, 'get_text'):
|
||||
text = child.get_text()
|
||||
if text and "Update OS" in text:
|
||||
# Parent is likely the button
|
||||
button = obj
|
||||
return
|
||||
|
||||
# Recursively search children
|
||||
for i in range(obj.get_child_count()):
|
||||
child = obj.get_child(i)
|
||||
find_button(child)
|
||||
|
||||
find_button(screen)
|
||||
|
||||
if button:
|
||||
state = button.get_state()
|
||||
is_disabled = bool(state & lv.STATE.DISABLED)
|
||||
self.assertTrue(is_disabled, "Install button should start disabled")
|
||||
|
||||
def test_current_version_displayed(self):
|
||||
"""Test that current OS version is displayed correctly."""
|
||||
result = mpos.apps.start_app("com.micropythonos.osupdate")
|
||||
self.assertTrue(result)
|
||||
wait_for_render(15)
|
||||
|
||||
screen = lv.screen_active()
|
||||
|
||||
# Find version label
|
||||
version_label = find_label_with_text(screen, "Installed OS version:")
|
||||
self.assertIsNotNone(version_label, "Version label not found")
|
||||
|
||||
# Check that it contains the current version
|
||||
label_text = version_label.get_text()
|
||||
current_version = mpos.info.CURRENT_OS_VERSION
|
||||
self.assertIn(current_version, label_text,
|
||||
f"Current version {current_version} not in label text: {label_text}")
|
||||
|
||||
def test_initial_status_message_without_wifi(self):
|
||||
"""Test status message when wifi is not connected."""
|
||||
# This test assumes desktop mode where wifi check returns True
|
||||
# On actual hardware without wifi, it would show error
|
||||
result = mpos.apps.start_app("com.micropythonos.osupdate")
|
||||
self.assertTrue(result)
|
||||
wait_for_render(15)
|
||||
|
||||
screen = lv.screen_active()
|
||||
|
||||
# Should show either "Checking for OS updates..." or update info
|
||||
# or wifi error depending on network state
|
||||
checking_found = verify_text_present(screen, "Checking") or \
|
||||
verify_text_present(screen, "version") or \
|
||||
verify_text_present(screen, "WiFi")
|
||||
self.assertTrue(checking_found, "Should show some status message")
|
||||
|
||||
def test_screenshot_initial_state(self):
|
||||
"""Capture screenshot of initial app state."""
|
||||
result = mpos.apps.start_app("com.micropythonos.osupdate")
|
||||
self.assertTrue(result)
|
||||
wait_for_render(20)
|
||||
|
||||
screen = lv.screen_active()
|
||||
|
||||
# Print labels for debugging
|
||||
print("\n=== OSUpdate Initial State Labels ===")
|
||||
print_screen_labels(screen)
|
||||
|
||||
# Capture screenshot
|
||||
screenshot_path = f"{self.screenshot_dir}/osupdate_initial_{self.hardware_id}.raw"
|
||||
capture_screenshot(screenshot_path)
|
||||
print(f"Screenshot saved to: {screenshot_path}")
|
||||
|
||||
|
||||
class TestOSUpdateGraphicalStatusMessages(unittest.TestCase):
|
||||
"""Graphical tests for OSUpdate status messages."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.hardware_id = mpos.info.get_hardware_id()
|
||||
self.screenshot_dir = "tests/screenshots"
|
||||
|
||||
try:
|
||||
os.stat(self.screenshot_dir)
|
||||
except OSError:
|
||||
try:
|
||||
os.mkdir(self.screenshot_dir)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after test."""
|
||||
mpos.ui.back_screen()
|
||||
wait_for_render(5)
|
||||
|
||||
def test_status_label_exists(self):
|
||||
"""Test that status label is created and visible."""
|
||||
result = mpos.apps.start_app("com.micropythonos.osupdate")
|
||||
self.assertTrue(result)
|
||||
wait_for_render(15)
|
||||
|
||||
screen = lv.screen_active()
|
||||
|
||||
# Status label should exist and show some text
|
||||
# Look for common status messages
|
||||
has_status = (
|
||||
verify_text_present(screen, "Checking") or
|
||||
verify_text_present(screen, "version") or
|
||||
verify_text_present(screen, "WiFi") or
|
||||
verify_text_present(screen, "Error") or
|
||||
verify_text_present(screen, "Update")
|
||||
)
|
||||
self.assertTrue(has_status, "Status label should be present with some message")
|
||||
|
||||
def test_all_labels_readable(self):
|
||||
"""Test that all labels are readable (no truncation issues)."""
|
||||
result = mpos.apps.start_app("com.micropythonos.osupdate")
|
||||
self.assertTrue(result)
|
||||
wait_for_render(15)
|
||||
|
||||
screen = lv.screen_active()
|
||||
|
||||
# Print all labels to verify they're readable
|
||||
print("\n=== All OSUpdate Labels ===")
|
||||
print_screen_labels(screen)
|
||||
|
||||
# At minimum, should have version label
|
||||
version_found = verify_text_present(screen, "Installed OS version")
|
||||
self.assertTrue(version_found, "Version label should be present and readable")
|
||||
|
||||
|
||||
class TestOSUpdateGraphicalScreenshots(unittest.TestCase):
|
||||
"""Screenshot tests for visual regression testing."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.hardware_id = mpos.info.get_hardware_id()
|
||||
self.screenshot_dir = "tests/screenshots"
|
||||
|
||||
try:
|
||||
os.stat(self.screenshot_dir)
|
||||
except OSError:
|
||||
try:
|
||||
os.mkdir(self.screenshot_dir)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after test."""
|
||||
mpos.ui.back_screen()
|
||||
wait_for_render(5)
|
||||
|
||||
def test_capture_main_screen(self):
|
||||
"""Capture screenshot of main OSUpdate screen."""
|
||||
result = mpos.apps.start_app("com.micropythonos.osupdate")
|
||||
self.assertTrue(result)
|
||||
wait_for_render(20)
|
||||
|
||||
screenshot_path = f"{self.screenshot_dir}/osupdate_main_{self.hardware_id}.raw"
|
||||
capture_screenshot(screenshot_path)
|
||||
|
||||
# Verify file was created
|
||||
try:
|
||||
stat = os.stat(screenshot_path)
|
||||
self.assertTrue(stat[6] > 0, "Screenshot file should not be empty")
|
||||
except OSError:
|
||||
self.fail(f"Screenshot file not created: {screenshot_path}")
|
||||
|
||||
def test_capture_with_labels_visible(self):
|
||||
"""Capture screenshot ensuring all text is visible."""
|
||||
result = mpos.apps.start_app("com.micropythonos.osupdate")
|
||||
self.assertTrue(result)
|
||||
wait_for_render(20)
|
||||
|
||||
screen = lv.screen_active()
|
||||
|
||||
# Verify key elements are visible before screenshot (case insensitive)
|
||||
has_version = verify_text_present(screen, "Installed") or verify_text_present(screen, "version")
|
||||
has_force = verify_text_present(screen, "Force") or verify_text_present(screen, "force")
|
||||
has_button = verify_text_present(screen, "Update") or verify_text_present(screen, "update")
|
||||
|
||||
self.assertTrue(has_version, "Version label should be visible")
|
||||
self.assertTrue(has_force, "Force checkbox should be visible")
|
||||
self.assertTrue(has_button, "Update button should be visible")
|
||||
|
||||
screenshot_path = f"{self.screenshot_dir}/osupdate_labeled_{self.hardware_id}.raw"
|
||||
capture_screenshot(screenshot_path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user