mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
fix: web UI and src.common bugs (wifi wrong-password, plugin icon, starlark toggle, API caching, scroll/logo/font helpers) (#646)
- wifi: keep the "wrong_password:" prefix through the restore/AP fallback so the UI's incorrect-password prompt fires again. - /plugins/installed returns the manifest's icon (string only). - /starlark/apps/<id>/toggle coerces `enabled` and delegates to _toggle_starlark_app (disk before memory, no KeyError, "false" is false). - /api/v3/ JSON GETs are sent Cache-Control: no-store; non-JSON keeps 5s. - ScrollHelper.set_scrolling_image converts non-RGB input (alpha onto black); create/set_scrolling_image reset last_update_time like reset_scroll. - LogoHelper backs off a failed download per path for MISSING_LOGO_RECHECK_SECONDS; cleared on invalidate/clear_cache. - refresh_placeholder_timestamp saves atomically. - FontManager.clear_cache / _clear_plugin_font_cache bump cache_generation. - Odds manager: per-game logs to DEBUG; JSON decode error caught before RequestException (same cooldown). - element_style mangled continuations; startup validator skips null plugin blocks and reuses the controller's discovery. - src/common/README lists frame_timing, json_body, render_gate. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""ScrollHelper: what a new scrolling image does on the way in.
|
||||
|
||||
- set_scrolling_image cached a non-RGB image as-is, and every frame is cut
|
||||
with Image.frombytes('RGB', ...): an RGBA strip came out as garbage and an
|
||||
L strip raised.
|
||||
- Only reset_scroll() reset last_update_time. In time-based mode the first
|
||||
update after a new image (e.g. the plugin coming back on screen) advanced
|
||||
the position by the whole idle gap, skipping the start of the content.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from src.common.scroll_helper import ScrollHelper
|
||||
|
||||
W, H = 16, 8
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def helper():
|
||||
return ScrollHelper(display_width=W, display_height=H)
|
||||
|
||||
|
||||
class TestNonRgbImages:
|
||||
def test_rgba_is_composited_onto_black(self, helper):
|
||||
img = Image.new('RGBA', (40, H), (0, 0, 0, 0))
|
||||
for x in range(10):
|
||||
for y in range(H):
|
||||
img.putpixel((x, y), (255, 0, 0, 255))
|
||||
# Hidden colour under full transparency must not leak through.
|
||||
img.putpixel((20, 0), (0, 255, 0, 0))
|
||||
helper.set_scrolling_image(img)
|
||||
assert helper.cached_image.mode == 'RGB'
|
||||
frame = helper.get_visible_portion()
|
||||
assert frame.size == (W, H)
|
||||
assert frame.getpixel((0, 0)) == (255, 0, 0)
|
||||
assert frame.getpixel((12, 0)) == (0, 0, 0)
|
||||
helper.scroll_position = 20.0
|
||||
assert helper.get_visible_portion().getpixel((0, 0)) == (0, 0, 0)
|
||||
|
||||
def test_greyscale_does_not_raise(self, helper):
|
||||
helper.set_scrolling_image(Image.new('L', (40, H), 200))
|
||||
frame = helper.get_visible_portion()
|
||||
assert frame.getpixel((0, 0)) == (200, 200, 200)
|
||||
|
||||
def test_palette_with_transparency(self, helper):
|
||||
img = Image.new('P', (40, H), 1)
|
||||
img.putpalette([0, 0, 0, 0, 0, 255] + [0] * 762)
|
||||
img.info['transparency'] = 1
|
||||
helper.set_scrolling_image(img)
|
||||
assert helper.get_visible_portion().getpixel((0, 0)) == (0, 0, 0)
|
||||
|
||||
def test_an_rgb_image_is_kept_as_the_same_object(self, helper):
|
||||
img = Image.new('RGB', (40, H), (1, 2, 3))
|
||||
helper.set_scrolling_image(img)
|
||||
assert helper.cached_image is img
|
||||
|
||||
|
||||
class TestNoJumpAfterIdle:
|
||||
def _idle_then_new_image(self, helper, install):
|
||||
helper.scroll_speed = 100.0 # px/s, time-based
|
||||
install(helper)
|
||||
helper.update_scroll_position()
|
||||
# Off screen for 10 seconds, then a fresh image.
|
||||
helper.last_update_time = time.time() - 10.0
|
||||
install(helper)
|
||||
helper.update_scroll_position()
|
||||
return helper.scroll_position
|
||||
|
||||
def test_set_scrolling_image(self, helper):
|
||||
pos = self._idle_then_new_image(
|
||||
helper, lambda h: h.set_scrolling_image(Image.new('RGB', (5000, H))))
|
||||
assert pos < 50, f"jumped {pos}px on the first update of a new image"
|
||||
|
||||
def test_create_scrolling_image(self, helper):
|
||||
pos = self._idle_then_new_image(
|
||||
helper, lambda h: h.create_scrolling_image([Image.new('RGB', (5000, H))]))
|
||||
assert pos < 50, f"jumped {pos}px on the first update of a new image"
|
||||
|
||||
def test_fixed_step_mode_still_steps_exactly(self, helper):
|
||||
helper.set_pixels_per_frame(2)
|
||||
helper.set_scrolling_image(Image.new('RGB', (5000, H)))
|
||||
helper.update_scroll_position()
|
||||
helper.update_scroll_position()
|
||||
assert helper.scroll_position == 4.0
|
||||
Reference in New Issue
Block a user