mirror of
https://github.com/Screenly/Anthias.git
synced 2026-08-02 18:57:32 -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>
166 lines
5.0 KiB
Python
166 lines
5.0 KiB
Python
import os
|
|
import shutil
|
|
import sys
|
|
from collections.abc import Iterator
|
|
from contextlib import contextmanager
|
|
from typing import Any
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
user_home_dir = os.getenv('HOME')
|
|
|
|
settings1 = """
|
|
[viewer]
|
|
player_name = new player
|
|
show_splash = off
|
|
audio_output = hdmi
|
|
shuffle_playlist = on
|
|
verify_ssl = off
|
|
debug_logging = on
|
|
prefer_dark_mode = on
|
|
resolution = 1920x1080
|
|
default_duration = 45
|
|
|
|
[main]
|
|
assetdir = "{}/anthias_assets".format(user_home_dir)
|
|
database = "{}/.anthias/anthias.db".format(user_home_dir)
|
|
use_ssl = False
|
|
|
|
"""
|
|
|
|
empty_settings = """
|
|
[viewer]
|
|
|
|
[main]
|
|
|
|
"""
|
|
|
|
broken_settings = """
|
|
[viewer]
|
|
show_splash = offf
|
|
|
|
[main]
|
|
|
|
"""
|
|
|
|
# Each xdist worker gets its own /tmp root so the four tests in this
|
|
# module — which all rewrite the same on-disk config file — don't race
|
|
# against one another. Without the worker suffix, worker A's file
|
|
# write/cleanup interleaves with worker B's import/remove and tests
|
|
# fail intermittently with FileNotFoundError.
|
|
_WORKER_ID = os.environ.get('PYTEST_XDIST_WORKER', 'main')
|
|
_TMP_HOME = f'/tmp/.anthias-test-{_WORKER_ID}'
|
|
CONFIG_DIR = f'{_TMP_HOME}/.anthias/'
|
|
CONFIG_FILE = CONFIG_DIR + 'anthias.conf'
|
|
|
|
|
|
@contextmanager
|
|
def fake_settings(raw: str) -> Iterator[tuple[Any, Any]]:
|
|
with open(CONFIG_FILE, mode='w+') as f:
|
|
f.write(raw)
|
|
|
|
# Force a re-import so AnthiasSettings() is instantiated against the
|
|
# CONFIG_FILE we just wrote. Without this, a prior test that imported
|
|
# `settings` cleanly would leave the module cached, and `import
|
|
# settings` here would skip __init__ entirely — silently accepting
|
|
# any config (including the broken-by-design fixture).
|
|
# Force a fresh import: pop the submodule from sys.modules AND
|
|
# delete the cached attribute on the parent package, otherwise
|
|
# `from anthias_server import settings` returns the stale module
|
|
# object via the parent's namespace and __init__ never re-runs.
|
|
sys.modules.pop('anthias_server.settings', None)
|
|
import anthias_server as _anthias_server
|
|
|
|
if hasattr(_anthias_server, 'settings'):
|
|
del _anthias_server.settings
|
|
try:
|
|
from anthias_server import settings
|
|
|
|
yield (settings, settings.settings)
|
|
finally:
|
|
sys.modules.pop('anthias_server.settings', None)
|
|
if hasattr(_anthias_server, 'settings'):
|
|
del _anthias_server.settings
|
|
os.remove(CONFIG_FILE)
|
|
|
|
|
|
def getenv(k: str, default: Any = None) -> Any:
|
|
try:
|
|
return _TMP_HOME if k == 'HOME' else os.environ[k]
|
|
except KeyError:
|
|
return default
|
|
|
|
|
|
@pytest.fixture
|
|
def settings_env() -> Iterator[None]:
|
|
os.makedirs(CONFIG_DIR, exist_ok=True)
|
|
getenv_patcher = mock.patch.object(os, 'getenv', side_effect=getenv)
|
|
getenv_patcher.start()
|
|
try:
|
|
yield
|
|
finally:
|
|
shutil.rmtree(_TMP_HOME, ignore_errors=True)
|
|
getenv_patcher.stop()
|
|
|
|
|
|
def test_parse_settings(settings_env: None) -> None:
|
|
with fake_settings(settings1) as (mod_settings, settings):
|
|
assert settings['player_name'] == 'new player'
|
|
assert settings['show_splash'] is False
|
|
assert settings['shuffle_playlist'] is True
|
|
assert settings['debug_logging'] is True
|
|
assert settings['prefer_dark_mode'] is True
|
|
assert settings['default_duration'] == 45
|
|
|
|
|
|
def test_default_settings(settings_env: None) -> None:
|
|
with fake_settings(empty_settings) as (mod_settings, settings):
|
|
assert (
|
|
settings['player_name']
|
|
== mod_settings.DEFAULTS['viewer']['player_name']
|
|
)
|
|
assert (
|
|
settings['show_splash']
|
|
== mod_settings.DEFAULTS['viewer']['show_splash']
|
|
)
|
|
assert (
|
|
settings['shuffle_playlist']
|
|
== mod_settings.DEFAULTS['viewer']['shuffle_playlist']
|
|
)
|
|
assert (
|
|
settings['debug_logging']
|
|
== mod_settings.DEFAULTS['viewer']['debug_logging']
|
|
)
|
|
assert (
|
|
settings['prefer_dark_mode']
|
|
== mod_settings.DEFAULTS['viewer']['prefer_dark_mode']
|
|
)
|
|
assert (
|
|
settings['default_duration']
|
|
== mod_settings.DEFAULTS['viewer']['default_duration']
|
|
)
|
|
|
|
|
|
def test_broken_settings_should_raise_value_error(settings_env: None) -> None:
|
|
with pytest.raises(ValueError):
|
|
with fake_settings(broken_settings) as (mod_settings, settings):
|
|
pass
|
|
|
|
|
|
def test_save_settings(settings_env: None) -> None:
|
|
with fake_settings(settings1) as (mod_settings, settings):
|
|
settings.conf_file = CONFIG_DIR + '/new.conf'
|
|
settings['default_duration'] = 35
|
|
settings['verify_ssl'] = True
|
|
settings.save()
|
|
|
|
with open(CONFIG_DIR + '/new.conf') as f:
|
|
saved = f.read()
|
|
with fake_settings(saved) as (mod_settings, settings):
|
|
# changes saved?
|
|
assert settings['default_duration'] == 35
|
|
assert settings['verify_ssl'] is True
|
|
# no out of thin air changes?
|
|
assert settings['audio_output'] == 'hdmi'
|