mirror of
https://github.com/Screenly/Anthias.git
synced 2026-07-30 17:25:50 -04:00
* feat(viewer): add "Prefer dark mode" setting for web page assets Adds a "Prefer dark mode" toggle in Settings that instructs the Qt webview to render web page assets dark. The C++ webview owns the realization: applyDarkModePreference() reads ANTHIAS_PREFER_DARK_MODE and injects --blink-settings=forceDarkModeEnabled=true into Chromium before QtWebEngine inits, working uniformly across Qt5 (Pi 1-4) and Qt6 (Pi 5/x86) without a version macro. Python owns the setting: the viewer exports the env var per board in _build_webview_env() and respawns the webview when the operator toggles the setting (reusing the rotation-bounce handoff). Wired through the settings page, page_context, the form POST, and the v2 device-settings API. Validated on a Pi 5 testbed: with the setting on, AnthiasViewer's env carries ANTHIAS_PREFER_DARK_MODE=1 and the QtWebEngine renderer processes run with --blink-settings=forceDarkModeEnabled=true; with it off, neither is present. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(test): annotate pixel list so mypy doesn't flag no-any-return PIL's getdata() is untyped, so sum(list(...)) was Any and the -> float return tripped mypy's no-any-return. Annotate the list as list[int]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(viewer): address review on dark-mode flag and its test - main.cpp: merge forceDarkModeEnabled into an existing --blink-settings switch instead of appending a duplicate (Chromium keeps only the last occurrence, so a second switch would silently drop any Blink settings already set); no-op if already requested. - test_webview_dark_mode: launch via the suite's browser_type / browser_type_launch_args fixtures so the conftest --no-sandbox override (and any future launch config) is inherited rather than hardcoded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
4.8 KiB
Python
124 lines
4.8 KiB
Python
"""Integration coverage for the "Prefer dark mode" setting.
|
|
|
|
The setting is realised entirely inside the C++ webview: when the
|
|
operator enables it, the Python viewer exports
|
|
``ANTHIAS_PREFER_DARK_MODE=1`` (see ``_build_webview_env`` in
|
|
``src/anthias_viewer/__init__.py``) and ``applyDarkModePreference`` in
|
|
``src/anthias_webview/src/main.cpp`` translates that into the Chromium
|
|
switch ``--blink-settings=forceDarkModeEnabled=true`` before QtWebEngine
|
|
boots its Chromium context.
|
|
|
|
These tests pin the behaviour of that exact switch by driving Chromium —
|
|
the same Blink engine QtWebEngine embeds — headless, rendering a plain
|
|
white page, screenshotting it, and asserting on the average pixel
|
|
luminance: dark with the flag, light without it. Running the full
|
|
AnthiasViewer binary end-to-end (offscreen QPA + screenshot) belongs on
|
|
a real testbed, not CI; this validates the engine-level mechanism the
|
|
device relies on.
|
|
|
|
The flag literal below is duplicated from main.cpp on purpose — keep the
|
|
two in sync; the test fails loudly if Chromium ever stops honouring it.
|
|
"""
|
|
|
|
import io
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from PIL import Image
|
|
from playwright.sync_api import BrowserType
|
|
|
|
# Must match applyDarkModePreference() in src/anthias_webview/src/main.cpp.
|
|
FORCE_DARK_FLAG = '--blink-settings=forceDarkModeEnabled=true'
|
|
|
|
# A deliberately light page: a pure-white viewport with no
|
|
# ``prefers-color-scheme`` hints, so Chromium's force-dark feature has
|
|
# something to invert and the no-flag baseline stays bright.
|
|
WHITE_PAGE = (
|
|
'<!doctype html><html><head><meta charset="utf-8">'
|
|
'<style>html,body{margin:0;padding:0;height:100%;'
|
|
'background:#ffffff;color:#000;}</style></head>'
|
|
'<body><div style="width:100vw;height:100vh;"></div></body></html>'
|
|
)
|
|
|
|
# Pure white renders at luminance ~255; Chromium force-darkens it to a
|
|
# near-black canvas (~#121212). The thresholds leave a wide margin either
|
|
# side of the midpoint so anti-aliasing or a future engine tweak to the
|
|
# exact dark shade can't flip the result.
|
|
LIGHT_THRESHOLD = 200
|
|
DARK_THRESHOLD = 120
|
|
|
|
|
|
def _mean_center_luminance(
|
|
browser_type: BrowserType,
|
|
launch_args: dict[str, Any],
|
|
extra_args: list[str],
|
|
) -> float:
|
|
"""Render the white page in headless Chromium and return the mean
|
|
luminance (0-255) of the centre of the screenshot.
|
|
|
|
Launches through the suite's ``browser_type`` /
|
|
``browser_type_launch_args`` fixtures (rather than a bare
|
|
``sync_playwright`` call) so any suite-wide launch configuration —
|
|
notably the ``--no-sandbox`` override in conftest.py needed because
|
|
the test container runs as root — is inherited here too and can't
|
|
drift. We must launch our own browser instead of reusing the shared
|
|
``page`` fixture because the force-dark switch differs per test and
|
|
Chromium only reads it at startup.
|
|
|
|
Cropping to the centre quarter avoids any window-chrome or
|
|
scrollbar pixels skewing a full-frame average.
|
|
"""
|
|
args = [*launch_args.get('args', []), '--hide-scrollbars', *extra_args]
|
|
browser = browser_type.launch(**{**launch_args, 'args': args})
|
|
try:
|
|
page = browser.new_page(viewport={'width': 400, 'height': 300})
|
|
page.set_content(WHITE_PAGE, wait_until='load')
|
|
# Force-dark is applied during paint; give the compositor a
|
|
# beat so the screenshot captures the darkened frame rather
|
|
# than the initial white flash.
|
|
page.wait_for_timeout(300)
|
|
png = page.screenshot()
|
|
finally:
|
|
browser.close()
|
|
|
|
image = Image.open(io.BytesIO(png)).convert('L')
|
|
width, height = image.size
|
|
centre = image.crop(
|
|
(width // 4, height // 4, 3 * width // 4, 3 * height // 4)
|
|
)
|
|
pixels: list[int] = list(centre.getdata())
|
|
return sum(pixels) / len(pixels)
|
|
|
|
|
|
@pytest.mark.integration
|
|
def test_force_dark_flag_renders_web_page_dark(
|
|
browser_type: BrowserType,
|
|
browser_type_launch_args: dict[str, Any],
|
|
) -> None:
|
|
""" "Prefer dark mode" on: the Chromium switch the webview injects
|
|
must darken an otherwise-white page."""
|
|
luminance = _mean_center_luminance(
|
|
browser_type, browser_type_launch_args, [FORCE_DARK_FLAG]
|
|
)
|
|
assert luminance < DARK_THRESHOLD, (
|
|
f'expected a dark render with {FORCE_DARK_FLAG}, '
|
|
f'got mean luminance {luminance:.1f}'
|
|
)
|
|
|
|
|
|
@pytest.mark.integration
|
|
def test_without_flag_web_page_stays_light(
|
|
browser_type: BrowserType,
|
|
browser_type_launch_args: dict[str, Any],
|
|
) -> None:
|
|
""" "Prefer dark mode" off: with no flag the same white page must
|
|
render light, proving the dark result above is caused by the flag
|
|
and not the harness."""
|
|
luminance = _mean_center_luminance(
|
|
browser_type, browser_type_launch_args, []
|
|
)
|
|
assert luminance > LIGHT_THRESHOLD, (
|
|
'expected a light render without the force-dark flag, '
|
|
f'got mean luminance {luminance:.1f}'
|
|
)
|