zapretgui/tests/test_appearance_page_construction.py
loop-uh fa7fb76f3f
All checks were successful
Zapret source guards / validate (push) Successful in 9s
Windows-сборка / Dev Dev 21.1.7.92 опубликован
Сборка страниц ещё на треть быстрее: убраны перехватчики событий на каждом элементе, «Оформление» достраивает нижние разделы позже
Продолжение блочной сборки (8bfc2dd3).

- Стили qfluentwidgets: у виджета со стилем больше нет двух перехватчиков
  событий библиотеки (ждали смены «своего стиля» и «ленивой» смены темы).
  Это было два вызова функции на Python на каждое событие каждого такого
  виджета. «Свой стиль» применяется там, где его задают; смена темы
  обновляет стили сразу (ленивой сменой программа не пользовалась).
- Закрытие выпадающих списков при потере активности слушает один сигнал
  приложения вместо перехватчика, через который шло каждое событие каждого
  объекта.
- Имя и состояние элемента для экранного диктора: служебные дочерние кнопки
  убираются из Tab-порядка за один обход детей вместо четырёх (на главной
  335 обходов дерева при сборке → 91).
- «Оформление»: новогоднее оформление, прозрачность, акцентный цвет и
  производительность — блок ниже первого экрана (ui.block_build). При
  создании страницы строится 52 элемента вместо 135. Блок, собранный позже,
  перечитывает настройки: прозрачность меняют и из меню в трее.
- У заглушки блока и слоя появления убрана лишняя установка фокуса
  (значение по умолчанию), из-за неё оба файла попадали под проверку
  доступности.

Замер на хосте против 21.1.7.85, семь страниц: сборка 255 → 171 мс, показ
75 → 54 мс, открытие рядом с занятым фоновым потоком 5644 → 3478 мс
(«Оформление» 989 → 125 мс).

Тест test_appearance_page_builds_all_sections_in_initial_pass заменён:
решение владельца от 2026-10-08 — не строить сразу то, чего не видно.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:31:34 +03:00

200 lines
8.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 dataclasses import replace
from unittest.mock import Mock, patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtWidgets import QApplication
from app.state_store import AppUiState, MainWindowStateStore
from settings import appearance as appearance_settings
_CALLBACK_NAMES = (
"on_garland_changed",
"on_snowflakes_changed",
"on_background_refresh_needed",
"on_background_preset_changed",
"on_opacity_changed",
"on_animations_changed",
"on_smooth_scroll_changed",
"on_editor_smooth_scroll_changed",
"on_ui_language_changed",
"on_sidebar_icon_style_changed",
)
_PREMIUM_SAVE_ACTIONS = {"background_preset", "garland_enabled", "snowflakes_enabled"}
def _clear_appearance_caches() -> None:
for name in dir(appearance_settings):
if name.startswith("clear_warmed_") and name.endswith("_cache"):
getattr(appearance_settings, name)()
class AppearancePageConstructionTests(unittest.TestCase):
"""Страница строится настоящим __init__ — так ловятся ошибки построения.
Именно такого теста не было, когда страница падала при каждом открытии
из-за распаковки кортежа Premium-статуса.
"""
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def _build_page(
self,
state: AppUiState,
*,
preset: str = "amoled",
effects: bool = True,
build_lower_sections: bool = True,
):
from ui.pages.appearance_page import AppearancePage
plan = replace(
appearance_settings.build_default_page_initial_state(),
background_preset=preset,
animations_enabled=True,
garland_enabled=effects,
snowflakes_enabled=effects,
)
_clear_appearance_caches()
self.addCleanup(_clear_appearance_caches)
appearance_settings.store_warmed_page_initial_state(plan)
store = MainWindowStateStore(state)
callbacks = {name: Mock() for name in _CALLBACK_NAMES}
save_patch = patch.object(AppearancePage, "_request_appearance_save")
save = save_patch.start()
self.addCleanup(save_patch.stop)
page = AppearancePage(
None,
**callbacks,
appearance_feature=Mock(),
ui_state_store=store,
)
self.addCleanup(page.deleteLater)
self.addCleanup(page.cleanup)
if build_lower_sections:
# Разделы ниже первого экрана собираются позже страницы (блок
# ui.block_build); этим тестам нужна страница целиком.
page.ensure_all_blocks()
return page, store, save, callbacks
def _premium_saves(self, save: Mock) -> list[str]:
return [call.args[0] for call in save.call_args_list if call.args and call.args[0] in _PREMIUM_SAVE_ACTIONS]
def test_unknown_status_shows_last_saved_premium_appearance(self) -> None:
page, _store, save, callbacks = self._build_page(AppUiState(subscription_known=False))
self.assertTrue(page._bg_radio_amoled.isEnabled())
self.assertTrue(page._bg_radio_amoled.isChecked())
self.assertTrue(page._garland_checkbox.isEnabled())
self.assertTrue(page._garland_checkbox.isChecked())
self.assertTrue(page._snowflakes_checkbox.isChecked())
self.assertEqual(self._premium_saves(save), [])
callbacks["on_background_preset_changed"].assert_not_called()
def test_known_free_status_locks_premium_controls_without_saving(self) -> None:
page, _store, save, callbacks = self._build_page(
AppUiState(subscription_known=True, subscription_is_premium=False)
)
self.assertFalse(page._bg_radio_amoled.isEnabled())
self.assertFalse(page._bg_radio_rkn_chan.isEnabled())
self.assertTrue(page._bg_radio_standard.isChecked())
self.assertFalse(page._garland_checkbox.isEnabled())
self.assertFalse(page._garland_checkbox.isChecked())
self.assertFalse(page._snowflakes_checkbox.isChecked())
self.assertFalse(page._rkn_background_combo.isEnabled())
# Сохраняет сброс и применяет фон окно (ui/window_premium_appearance.py).
self.assertEqual(self._premium_saves(save), [])
callbacks["on_background_preset_changed"].assert_not_called()
callbacks["on_garland_changed"].assert_not_called()
def test_known_premium_status_keeps_premium_background(self) -> None:
page, _store, _save, _callbacks = self._build_page(
AppUiState(subscription_known=True, subscription_is_premium=True),
preset="rkn_chan",
)
self.assertTrue(page._bg_radio_rkn_chan.isEnabled())
self.assertTrue(page._bg_radio_rkn_chan.isChecked())
self.assertTrue(page._garland_checkbox.isEnabled())
def test_subscription_answer_after_construction_updates_controls(self) -> None:
page, store, save, callbacks = self._build_page(AppUiState(subscription_known=False))
store.set_subscription(False)
self.assertFalse(page._bg_radio_amoled.isEnabled())
self.assertTrue(page._bg_radio_standard.isChecked())
self.assertFalse(page._garland_checkbox.isChecked())
self.assertEqual(self._premium_saves(save), [])
callbacks["on_background_preset_changed"].assert_not_called()
def test_lower_sections_are_a_block_built_later_than_the_page(self) -> None:
from ui.pages.appearance_page import LOWER_SECTIONS_BLOCK, LOWER_SECTIONS_HEIGHT
page, _store, _save, _callbacks = self._build_page(
AppUiState(subscription_known=False),
build_lower_sections=False,
)
# Первый экран готов сразу.
self.assertIsNotNone(page._display_mode_card)
self.assertIsNotNone(page._bg_radio_amoled)
# Нижние разделы ещё не собраны, но место под них занято.
for name in ("_garland_checkbox", "_snowflakes_checkbox", "_opacity_row", "_accent_group", "_performance_group"):
self.assertIsNone(getattr(page, name), name)
block = page.lazy_block(LOWER_SECTIONS_BLOCK)
self.assertFalse(block.is_built())
self.assertEqual(block.minimumHeight(), LOWER_SECTIONS_HEIGHT)
# Экскурсии нужен раздел из блока — она достраивает его сама.
self.assertIsNotNone(page.onboarding_target("theme"))
self.assertFalse(block.is_built(), "цель первого экрана блок не достраивает")
self.assertIsNotNone(page.onboarding_target("performance"))
self.assertTrue(block.is_built())
self.assertIsNotNone(page._garland_checkbox)
real = block.sizeHint().height()
self.assertLess(abs(real - LOWER_SECTIONS_HEIGHT), 120, f"настоящая высота блока {real}")
def test_block_built_after_the_subscription_answer_shows_the_current_state(self) -> None:
# Ответ о подписке пришёл, пока нижние разделы ещё не собраны: когда
# блок соберётся, он должен показать уже новое состояние.
page, store, save, callbacks = self._build_page(
AppUiState(subscription_known=False),
build_lower_sections=False,
)
store.set_subscription(False)
self.assertIsNone(page._garland_checkbox)
page.ensure_all_blocks()
self.assertFalse(page._garland_checkbox.isEnabled())
self.assertFalse(page._garland_checkbox.isChecked())
self.assertFalse(page._snowflakes_checkbox.isChecked())
self.assertEqual(self._premium_saves(save), [])
callbacks["on_garland_changed"].assert_not_called()
def test_block_built_later_reads_settings_changed_meanwhile(self) -> None:
# Прозрачность меняют и из меню в трее: блок, собранный позже, берёт
# текущие настройки, а не те, что были при создании страницы.
page, store, _save, _callbacks = self._build_page(
AppUiState(subscription_known=True, subscription_is_premium=True),
build_lower_sections=False,
)
fresh = replace(appearance_settings.load_page_initial_state(), animations_enabled=False)
appearance_settings.store_warmed_page_initial_state(fresh)
page.ensure_all_blocks()
self.assertFalse(page._animations_switch.isChecked())
if __name__ == "__main__":
unittest.main()