You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
377 lines
13 KiB
Python
377 lines
13 KiB
Python
"""
|
|
Graphical test for on-screen keyboard button styling.
|
|
|
|
This test verifies that keyboard buttons have proper visible contrast
|
|
in both light and dark modes. It checks for the bug where keyboard buttons
|
|
appear white-on-white in light mode on ESP32.
|
|
|
|
The test uses two approaches:
|
|
1. Programmatic: Query LVGL style properties to verify button background colors
|
|
2. Visual: Capture screenshots for manual verification and regression testing
|
|
|
|
This test should INITIALLY FAIL, demonstrating the bug before the fix is applied.
|
|
|
|
Usage:
|
|
Desktop: ./tests/unittest.sh tests/test_graphical_keyboard_styling.py
|
|
Device: ./tests/unittest.sh tests/test_graphical_keyboard_styling.py --ondevice
|
|
"""
|
|
|
|
import unittest
|
|
import lvgl as lv
|
|
import mpos.ui
|
|
import mpos.config
|
|
import sys
|
|
import os
|
|
from mpos import (
|
|
wait_for_render,
|
|
capture_screenshot,
|
|
AppearanceManager,
|
|
)
|
|
|
|
|
|
class TestKeyboardStyling(unittest.TestCase):
|
|
"""Test suite for keyboard button visibility and styling."""
|
|
|
|
def setUp(self):
|
|
"""Set up test fixtures before each test method."""
|
|
# Determine screenshot directory
|
|
if sys.platform == "esp32":
|
|
self.screenshot_dir = "tests/screenshots"
|
|
else:
|
|
self.screenshot_dir = "../tests/screenshots"
|
|
|
|
# Ensure screenshots directory exists
|
|
try:
|
|
os.mkdir(self.screenshot_dir)
|
|
except OSError:
|
|
pass # Directory already exists
|
|
|
|
# Save current theme setting
|
|
prefs = mpos.config.SharedPreferences("theme_settings")
|
|
self.original_theme = prefs.get_string("theme_light_dark", "light")
|
|
|
|
print(f"\n=== Keyboard Styling Test Setup ===")
|
|
print(f"Platform: {sys.platform}")
|
|
print(f"Original theme: {self.original_theme}")
|
|
|
|
def tearDown(self):
|
|
"""Clean up after each test method."""
|
|
# Restore original theme
|
|
prefs = mpos.config.SharedPreferences("theme_settings")
|
|
editor = prefs.edit()
|
|
editor.put_string("theme_light_dark", self.original_theme)
|
|
editor.commit()
|
|
|
|
# Reapply original theme
|
|
AppearanceManager.set_theme(prefs)
|
|
|
|
print("=== Test cleanup complete ===\n")
|
|
|
|
def _create_test_keyboard(self):
|
|
"""
|
|
Create a test keyboard widget for inspection.
|
|
|
|
Returns:
|
|
tuple: (screen, keyboard, textarea) widgets
|
|
"""
|
|
# Create a clean screen
|
|
screen = lv.obj()
|
|
screen.set_size(320, 240)
|
|
|
|
# Create a text area for the keyboard to target
|
|
textarea = lv.textarea(screen)
|
|
textarea.set_size(280, 40)
|
|
textarea.align(lv.ALIGN.TOP_MID, 0, 10)
|
|
textarea.set_placeholder_text("Type here...")
|
|
|
|
# Create the keyboard
|
|
keyboard = lv.keyboard(screen)
|
|
keyboard.set_textarea(textarea)
|
|
keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0)
|
|
keyboard.set_style_min_height(160, 0)
|
|
|
|
# Apply the keyboard button fix
|
|
AppearanceManager.apply_keyboard_fix(keyboard)
|
|
|
|
# Load the screen and wait for rendering
|
|
lv.screen_load(screen)
|
|
wait_for_render(iterations=20)
|
|
|
|
return screen, keyboard, textarea
|
|
|
|
def _get_button_background_color(self, keyboard):
|
|
"""
|
|
Extract the background color of keyboard buttons.
|
|
|
|
This queries LVGL's style system to get the actual rendered
|
|
background color of the keyboard's button parts (LV_PART_ITEMS).
|
|
|
|
Args:
|
|
keyboard: LVGL keyboard widget
|
|
|
|
Returns:
|
|
dict: Color information with 'r', 'g', 'b' values (0-255)
|
|
"""
|
|
# Get the style property for button background color
|
|
# LV_PART_ITEMS is the part that represents individual buttons
|
|
bg_color = keyboard.get_style_bg_color(lv.PART.ITEMS)
|
|
|
|
# Extract RGB values from LVGL color
|
|
# Note: LVGL colors are in RGB565 or RGB888 depending on config
|
|
# We convert to RGB888 for comparison
|
|
r = lv.color_brightness(bg_color) if hasattr(lv, 'color_brightness') else 0
|
|
|
|
# Try to get RGB components directly
|
|
try:
|
|
# For LVGL 9.x, colors have direct accessors
|
|
color_dict = {
|
|
'r': bg_color.red() if hasattr(bg_color, 'red') else 0,
|
|
'g': bg_color.green() if hasattr(bg_color, 'green') else 0,
|
|
'b': bg_color.blue() if hasattr(bg_color, 'blue') else 0,
|
|
}
|
|
except:
|
|
# Fallback: use color as hex value
|
|
try:
|
|
color_int = bg_color.to_int() if hasattr(bg_color, 'to_int') else 0
|
|
color_dict = {
|
|
'r': (color_int >> 16) & 0xFF,
|
|
'g': (color_int >> 8) & 0xFF,
|
|
'b': color_int & 0xFF,
|
|
'hex': f"#{color_int:06x}"
|
|
}
|
|
except:
|
|
# Last resort: just store the color object
|
|
color_dict = {'color_obj': bg_color}
|
|
|
|
return color_dict
|
|
|
|
def _get_screen_background_color(self, screen):
|
|
"""
|
|
Extract the background color of the screen.
|
|
|
|
Args:
|
|
screen: LVGL screen object
|
|
|
|
Returns:
|
|
dict: Color information with 'r', 'g', 'b' values (0-255)
|
|
"""
|
|
bg_color = screen.get_style_bg_color(lv.PART.MAIN)
|
|
|
|
try:
|
|
color_dict = {
|
|
'r': bg_color.red() if hasattr(bg_color, 'red') else 0,
|
|
'g': bg_color.green() if hasattr(bg_color, 'green') else 0,
|
|
'b': bg_color.blue() if hasattr(bg_color, 'blue') else 0,
|
|
}
|
|
except:
|
|
try:
|
|
color_int = bg_color.to_int() if hasattr(bg_color, 'to_int') else 0
|
|
color_dict = {
|
|
'r': (color_int >> 16) & 0xFF,
|
|
'g': (color_int >> 8) & 0xFF,
|
|
'b': color_int & 0xFF,
|
|
'hex': f"#{color_int:06x}"
|
|
}
|
|
except:
|
|
color_dict = {'color_obj': bg_color}
|
|
|
|
return color_dict
|
|
|
|
def _color_contrast_sufficient(self, color1, color2, min_difference=20):
|
|
"""
|
|
Check if two colors have sufficient contrast.
|
|
|
|
Uses simple RGB distance. For production, you might want to use
|
|
proper contrast ratio calculation (WCAG).
|
|
|
|
Args:
|
|
color1: Dict with 'r', 'g', 'b' keys
|
|
color2: Dict with 'r', 'g', 'b' keys
|
|
min_difference: Minimum RGB distance for sufficient contrast
|
|
|
|
Returns:
|
|
bool: True if contrast is sufficient
|
|
"""
|
|
if 'r' not in color1 or 'r' not in color2:
|
|
# Can't determine, assume failure
|
|
return False
|
|
|
|
# Calculate Euclidean distance in RGB space
|
|
r_diff = abs(color1['r'] - color2['r'])
|
|
g_diff = abs(color1['g'] - color2['g'])
|
|
b_diff = abs(color1['b'] - color2['b'])
|
|
|
|
# Simple average difference
|
|
avg_diff = (r_diff + g_diff + b_diff) / 3
|
|
|
|
print(f" Color 1: RGB({color1['r']}, {color1['g']}, {color1['b']})")
|
|
print(f" Color 2: RGB({color2['r']}, {color2['g']}, {color2['b']})")
|
|
print(f" Average difference: {avg_diff:.1f} (min required: {min_difference})")
|
|
|
|
return avg_diff >= min_difference
|
|
|
|
def test_keyboard_buttons_visible_in_light_mode(self):
|
|
"""
|
|
Test that keyboard buttons are visible in light mode.
|
|
|
|
In light mode, the screen background is white. Keyboard buttons
|
|
should NOT be white - they should be a light gray color to provide
|
|
contrast.
|
|
|
|
This test will FAIL initially, demonstrating the bug.
|
|
"""
|
|
print("\n=== Testing keyboard buttons in LIGHT mode ===")
|
|
|
|
# Set theme to light mode
|
|
prefs = mpos.config.SharedPreferences("theme_settings")
|
|
editor = prefs.edit()
|
|
editor.put_string("theme_light_dark", "light")
|
|
editor.commit()
|
|
|
|
# Apply theme
|
|
AppearanceManager.set_theme(prefs)
|
|
wait_for_render(iterations=10)
|
|
|
|
# Create test keyboard
|
|
screen, keyboard, textarea = self._create_test_keyboard()
|
|
|
|
# Get colors
|
|
button_bg = self._get_button_background_color(keyboard)
|
|
screen_bg = self._get_screen_background_color(screen)
|
|
|
|
print("\nLight mode colors:")
|
|
print(f" Screen background: {screen_bg}")
|
|
print(f" Button background: {button_bg}")
|
|
|
|
# Capture screenshot
|
|
screenshot_path = f"{self.screenshot_dir}/keyboard_light_mode.raw"
|
|
print(f"\nCapturing screenshot: {screenshot_path}")
|
|
capture_screenshot(screenshot_path, width=320, height=240)
|
|
|
|
# Verify contrast
|
|
print("\nChecking button/screen contrast...")
|
|
has_contrast = self._color_contrast_sufficient(button_bg, screen_bg, min_difference=20)
|
|
|
|
# Clean up
|
|
lv.screen_load(lv.obj())
|
|
wait_for_render(5)
|
|
|
|
# Assert: buttons should have sufficient contrast with background
|
|
self.assertTrue(
|
|
has_contrast,
|
|
f"Keyboard buttons lack sufficient contrast in light mode!\n"
|
|
f"Button color: {button_bg}\n"
|
|
f"Screen color: {screen_bg}\n"
|
|
f"This is the BUG we're trying to fix - buttons are white on white."
|
|
)
|
|
|
|
print("=== Light mode test PASSED ===")
|
|
|
|
def test_keyboard_buttons_visible_in_dark_mode(self):
|
|
"""
|
|
Test that keyboard buttons are visible in dark mode.
|
|
|
|
In dark mode, buttons should have proper contrast with the
|
|
dark background. This typically works correctly.
|
|
"""
|
|
print("\n=== Testing keyboard buttons in DARK mode ===")
|
|
|
|
# Set theme to dark mode
|
|
prefs = mpos.config.SharedPreferences("theme_settings")
|
|
editor = prefs.edit()
|
|
editor.put_string("theme_light_dark", "dark")
|
|
editor.commit()
|
|
|
|
# Apply theme
|
|
AppearanceManager.set_theme(prefs)
|
|
wait_for_render(iterations=10)
|
|
|
|
# Create test keyboard
|
|
screen, keyboard, textarea = self._create_test_keyboard()
|
|
|
|
# Get colors
|
|
button_bg = self._get_button_background_color(keyboard)
|
|
screen_bg = self._get_screen_background_color(screen)
|
|
|
|
print("\nDark mode colors:")
|
|
print(f" Screen background: {screen_bg}")
|
|
print(f" Button background: {button_bg}")
|
|
|
|
# Capture screenshot
|
|
screenshot_path = f"{self.screenshot_dir}/keyboard_dark_mode.raw"
|
|
print(f"\nCapturing screenshot: {screenshot_path}")
|
|
capture_screenshot(screenshot_path, width=320, height=240)
|
|
|
|
# Verify contrast
|
|
print("\nChecking button/screen contrast...")
|
|
has_contrast = self._color_contrast_sufficient(button_bg, screen_bg, min_difference=20)
|
|
|
|
# Clean up
|
|
lv.screen_load(lv.obj())
|
|
wait_for_render(5)
|
|
|
|
# Assert: buttons should have sufficient contrast
|
|
self.assertTrue(
|
|
has_contrast,
|
|
f"Keyboard buttons lack sufficient contrast in dark mode!\n"
|
|
f"Button color: {button_bg}\n"
|
|
f"Screen color: {screen_bg}"
|
|
)
|
|
|
|
print("=== Dark mode test PASSED ===")
|
|
|
|
def test_keyboard_buttons_not_pure_white_in_light_mode(self):
|
|
"""
|
|
Specific test: In light mode, buttons should NOT be pure white.
|
|
|
|
They should be a light gray (approximately RGB(238, 238, 238) or similar).
|
|
Pure white (255, 255, 255) means they're invisible on white background.
|
|
"""
|
|
print("\n=== Testing that buttons are NOT pure white in light mode ===")
|
|
|
|
# Set theme to light mode
|
|
prefs = mpos.config.SharedPreferences("theme_settings")
|
|
editor = prefs.edit()
|
|
editor.put_string("theme_light_dark", "light")
|
|
editor.commit()
|
|
|
|
# Apply theme
|
|
AppearanceManager.set_theme(prefs)
|
|
wait_for_render(iterations=10)
|
|
|
|
# Create test keyboard
|
|
screen, keyboard, textarea = self._create_test_keyboard()
|
|
|
|
# Get button color
|
|
button_bg = self._get_button_background_color(keyboard)
|
|
|
|
print(f"\nButton background color: {button_bg}")
|
|
|
|
# Clean up
|
|
lv.screen_load(lv.obj())
|
|
wait_for_render(5)
|
|
|
|
# Check if button is pure white (or very close to it)
|
|
if 'r' in button_bg:
|
|
is_white = (button_bg['r'] >= 250 and
|
|
button_bg['g'] >= 250 and
|
|
button_bg['b'] >= 250)
|
|
|
|
print(f"Is button pure white? {is_white}")
|
|
|
|
# Assert: buttons should NOT be pure white
|
|
self.assertFalse(
|
|
is_white,
|
|
f"Keyboard buttons are pure white in light mode!\n"
|
|
f"Button color: RGB({button_bg['r']}, {button_bg['g']}, {button_bg['b']})\n"
|
|
f"Expected: Light gray around RGB(238, 238, 238) or similar\n"
|
|
f"This is the BUG - white buttons on white background are invisible."
|
|
)
|
|
else:
|
|
# Couldn't extract RGB, fail the test
|
|
self.fail(f"Could not extract RGB values from button color: {button_bg}")
|
|
|
|
print("=== Pure white test PASSED ===")
|
|
|
|
|