zapretgui/tests/test_window_premium_appearance.py
loop-uh 6dab6845d2
All checks were successful
Zapret source guards / validate (push) Successful in 5s
Починить падение «Оформления» и сделать одного хозяина Premium-правил темы
Страница «Оформление» падала при каждом открытии: статус оформления
возвращался набором из пяти значений по порядку, а одно место ждало четыре.
Набор по порядку убран, страница берёт статус подписки из общего store по
именам полей и подписана на него ещё до построения.

Правило «что можно Free, что только Premium» теперь одно —
settings.appearance.resolve_premium_access. Применяет его к окну только
ui/window_premium_appearance.py: пока статус неизвестен, показывается
последнее сохранённое; у Free-версии окно само ставит обычный фон, гасит
гирлянду и снежинки и одной транзакцией сбрасывает их в настройках в
фоновом потоке. Страница только показывает доступность и больше ничего не
сохраняет за пользователя. Убраны параллельные пути: init_holiday_effects
из проверки подписки, отдельное включение эффектов без проверки Premium,
копии гирлянды и снежинок в AppUiState.

Ошибка записи настроек оформления больше не проглатывается и не меняет
кэш. Удалена мёртвая настройка Mica (переключатель давно не строился).
Добавлены тесты: настоящее построение страницы для Free/Premium/неизвестно
и поведение окна.
2026-09-27 17:42:33 +03:00

182 lines
7.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import os
import unittest
from unittest.mock import Mock, patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtCore import QThread
from PyQt6.QtWidgets import QApplication
from app.state_store import AppUiState, MainWindowStateStore
from settings import appearance as appearance_settings
from ui.window_premium_appearance import WindowPremiumAppearance
class WindowPremiumAppearanceTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def setUp(self) -> None:
appearance_settings.store_warmed_background_preset("amoled")
appearance_settings.store_warmed_premium_effects(True, True)
appearance_settings.store_warmed_animations_enabled(True)
self.addCleanup(appearance_settings.clear_warmed_background_preset_cache)
self.addCleanup(appearance_settings.clear_warmed_premium_effects_cache)
self.addCleanup(appearance_settings.clear_warmed_animations_enabled_cache)
self.window = object()
patches = {
"background": patch("ui.theme.apply_window_background"),
"garland": patch("ui.window_appearance_state.apply_garland_enabled"),
"snowflakes": patch("ui.window_appearance_state.apply_snowflakes_enabled"),
}
self.mocks = {name: item.start() for name, item in patches.items()}
for item in patches.values():
self.addCleanup(item.stop)
def _start(self, state: AppUiState):
store = MainWindowStateStore(state)
create_reset_worker = Mock()
appearance = WindowPremiumAppearance(
window=self.window,
ui_state_store=store,
create_reset_worker=create_reset_worker,
)
appearance._start_reset = Mock()
appearance.start()
self.addCleanup(appearance.stop)
return appearance, store
def test_unknown_status_applies_saved_premium_appearance_without_reset(self) -> None:
appearance, _store = self._start(AppUiState(subscription_known=False))
self.mocks["background"].assert_called_once_with(self.window, preset="amoled")
self.mocks["garland"].assert_called_with(self.window, True)
self.mocks["snowflakes"].assert_called_with(self.window, True)
appearance._start_reset.assert_not_called()
def test_known_free_status_applies_standard_and_resets_saved_settings(self) -> None:
appearance, _store = self._start(AppUiState(subscription_known=True, subscription_is_premium=False))
self.mocks["background"].assert_called_once_with(self.window, preset="standard")
self.mocks["garland"].assert_called_with(self.window, False)
self.mocks["snowflakes"].assert_called_with(self.window, False)
appearance._start_reset.assert_called_once_with()
def test_free_answer_after_start_switches_window_to_standard(self) -> None:
appearance, store = self._start(AppUiState(subscription_known=False))
self.mocks["background"].reset_mock()
store.set_subscription(False)
self.mocks["background"].assert_called_once_with(self.window, preset="standard")
self.mocks["garland"].assert_called_with(self.window, False)
appearance._start_reset.assert_called_once_with()
def test_premium_answer_keeps_background_without_reapplying(self) -> None:
appearance, store = self._start(AppUiState(subscription_known=False))
self.mocks["background"].reset_mock()
store.set_subscription(True, 30)
self.mocks["background"].assert_not_called()
self.mocks["garland"].assert_called_with(self.window, True)
appearance._start_reset.assert_not_called()
def test_free_without_premium_settings_does_not_reset(self) -> None:
appearance_settings.store_warmed_background_preset("standard")
appearance_settings.store_warmed_premium_effects(False, False)
appearance, _store = self._start(AppUiState(subscription_known=True, subscription_is_premium=False))
appearance._start_reset.assert_not_called()
def test_page_toggle_is_blocked_for_known_free_status(self) -> None:
appearance, _store = self._start(AppUiState(subscription_known=True, subscription_is_premium=False))
self.mocks["garland"].reset_mock()
appearance.sync_holiday_effects(garland=True)
self.mocks["garland"].assert_called_once_with(self.window, False)
class WindowPremiumAppearanceResetWorkerTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def test_reset_runs_in_background_thread_and_resyncs_effects(self) -> None:
from settings.appearance_workers import AppearancePremiumResetWorker
reset = Mock(return_value=appearance_settings.AppearancePremiumEffectsPlan(False, False))
workers: list[AppearancePremiumResetWorker] = []
def _create_worker():
worker = AppearancePremiumResetWorker(reset_premium_appearance=reset)
workers.append(worker)
return worker
appearance = WindowPremiumAppearance(
window=object(),
ui_state_store=MainWindowStateStore(AppUiState(subscription_known=True)),
create_reset_worker=_create_worker,
)
callback_threads = []
appearance.sync_holiday_effects = Mock(
side_effect=lambda: callback_threads.append(QThread.currentThread())
)
appearance._start_reset()
appearance._start_reset() # второй запуск, пока первый не закончился, не нужен
self.assertEqual(len(workers), 1)
self.assertTrue(workers[0].wait(5000))
QApplication.processEvents()
reset.assert_called_once_with()
appearance.sync_holiday_effects.assert_called_once_with()
# Эффекты — виджеты окна: итог сброса обязан прийти в GUI-поток.
self.assertIs(callback_threads[0], QApplication.instance().thread())
def test_subscription_handler_logs_instead_of_breaking_store_update(self) -> None:
store = MainWindowStateStore(AppUiState(subscription_known=False))
appearance = WindowPremiumAppearance(window=object(), ui_state_store=store, create_reset_worker=Mock())
with (
patch("ui.theme.apply_window_background", side_effect=RuntimeError("wrapped C/C++ object deleted")),
patch("ui.window_premium_appearance.log") as log,
):
appearance.start()
self.addCleanup(appearance.stop)
self.assertTrue(store.set_subscription(True, 10))
self.assertEqual(log.call_count, 2)
self.assertIn("ERROR", log.call_args.args)
class AppearanceSaveFailureTests(unittest.TestCase):
def test_failed_database_write_keeps_cache_unchanged(self) -> None:
appearance_settings.store_warmed_premium_effects(False, False)
self.addCleanup(appearance_settings.clear_warmed_premium_effects_cache)
with patch("settings.store.set_garland_enabled", side_effect=OSError("disk full")):
with self.assertRaises(OSError):
appearance_settings.save_garland_enabled(True)
self.assertFalse(appearance_settings.peek_warmed_premium_effects().garland_enabled)
def test_failed_reset_keeps_premium_background_in_cache(self) -> None:
appearance_settings.store_warmed_background_preset("amoled")
self.addCleanup(appearance_settings.clear_warmed_background_preset_cache)
with patch("settings.store.reset_premium_appearance", side_effect=OSError("locked")):
with self.assertRaises(OSError):
appearance_settings.reset_premium_appearance()
self.assertEqual(appearance_settings.peek_warmed_background_preset(), "amoled")
if __name__ == "__main__":
unittest.main()