Files
Anthias/tests/test_settings.py
Viktor Petersson d323bf147d feat(viewer): add "Prefer dark mode" setting for web page assets (#3050)
* 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>
2026-06-10 18:57:10 +01:00

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'