zapretgui/tests/test_control_settings_block.py
loop-uh 8bfc2dd3a5
All checks were successful
Zapret source guards / validate (push) Successful in 9s
Windows-сборка / Dev Dev 21.1.7.85 опубликован
Страницы собираются блоками: сначала видимое, остальное позже; появление блока рисуется одной картинкой и стоит процессору вдвое меньше
Владелец попросил (2026-10-08): то, чего не видно, не строить и не рисовать
сразу, а достраивать позже; везде лёгкое появление; главное — общее ядро.

Ядро:
- ui/block_build.py: блоки страницы (LazyBlock) и наполнение списков
  (DeferredFill). Блок первого экрана собирается при показе, без таймеров;
  блок, до которого долистали, — сразу; остальные — по одному в паузах, пока
  молчит фоновая дорожка запуска. Место под несобранное занято заранее;
- BasePage.add_lazy_block / ensure_block / ensure_all_blocks; команды
  страницам, экскурсия и шаг «назад» достраивают страницу сами;
- появление (ui/widgets/stagger_float_in.py) переписано: видимая часть блока
  снимается в картинку один раз, настоящий блок на время полёта спрятан
  маской, один слой на контейнер рисует картинки по общему такту кадров,
  точка в точку экрана. Вид и длительности те же;
- меньше обращений к Qt при сборке: тексты стилей qfluentwidgets
  запоминаются (ui/qfluent_style_cache.py); у виджета со стилем и у привязки
  темы больше нет своих подключений сигналов; надпись подключает «своё меню»,
  только когда его включили; сборка страницы по щелчку ждёт своей очереди с
  фоновой дорожкой запуска, как сборка про запас.

Страницы:
- главная (Zapret 1 и 2): три группы настроек и «Последнее сообщение» —
  четыре блока ниже первого экрана; строители групп разделены;
- сетка карточек итога (BlockCheck, «Проверка домена») создаёт карточки
  порциями по 12: видимые сразу, остальные при прокрутке или в паузе;
- hosts: плитки сервисов выплывают и тогда, когда пришли на открытую страницу;
- полоса ожидания на главной и в проверке домена не крутится скрытой.

Замер на win10, поток окна за 1,3 с после показа страницы (масштаб 100% / 150%):
DNS 269→103 / 271→115 мс, Telegram Proxy 146→62 / 203→93, «Оформление»
145→92 / 274→93, «О программе» 134→58 / 157→77, главная 114→91 / 164→124.
Сборка главной 56→20 мс. Открытие главной рядом с занятым фоновым потоком
(стенд на хосте) 966→282 мс. Тесты ядра прошли и на Windows.

Прежний запрет откладывать настройки главной (test_zapret2_control_lazy_startup)
снят этим решением владельца: прошлые попытки откладывали таймерами и видимое
тоже; теперь первый экран собирается сразу, а откладывается только то, что
ниже края окна.

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

201 lines
9.3 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.

"""Главная страница: первый экран сразу, настройки — блоком позже (ui.block_build)."""
from __future__ import annotations
import inspect
import os
import time
import unittest
from importlib import import_module
from types import SimpleNamespace
from unittest import mock
from unittest.mock import MagicMock
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtWidgets import QApplication, QVBoxLayout, QWidget
import ui.block_build as block_build
from app.page_names import PageName
from presets.ui.control.control_page_shared import (
FINE_TUNING_BLOCK,
LAST_MESSAGE_BLOCK,
PROGRAM_SETTINGS_BLOCK,
SETTINGS_BLOCK_HEIGHTS,
WINDOWS_SETTINGS_BLOCK,
)
from ui.page_registry import PAGE_CLASS_SPECS
_PAGES = (PageName.ZAPRET2_MODE_CONTROL, PageName.ZAPRET1_MODE_CONTROL)
_BLOCKS = (PROGRAM_SETTINGS_BLOCK, WINDOWS_SETTINGS_BLOCK, FINE_TUNING_BLOCK, LAST_MESSAGE_BLOCK)
_SETTINGS_WIDGETS = (
"program_settings_card",
"windows_settings_card",
"additional_settings_card",
"gui_autostart_toggle",
"auto_dpi_toggle",
"tray_close_mode_combo",
"defender_toggle",
"max_block_toggle",
"state_media_block_toggle",
"discord_restart_toggle",
"wssize_toggle",
"debug_log_toggle",
"last_status_message_card",
)
def _pump(seconds: float = 0.0) -> None:
deadline = time.monotonic() + seconds
QApplication.processEvents()
while time.monotonic() < deadline:
QApplication.processEvents()
class ControlSettingsBlockTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def setUp(self) -> None:
# Фоновая достройка в этих тестах не нужна: проверяем, что делает показ.
patcher = mock.patch.object(block_build, "BACKGROUND_START_DELAY_MS", 600_000)
patcher.start()
self.addCleanup(patcher.stop)
def _page(self, page_name: PageName, *, window_size=(1100, 750)):
module_name, class_name = PAGE_CLASS_SPECS[page_name]
page_cls = getattr(import_module(module_name), class_name)
deps = {
parameter.name: MagicMock()
for parameter in inspect.signature(page_cls.__init__).parameters.values()
if parameter.kind is parameter.KEYWORD_ONLY and parameter.default is parameter.empty
}
window = QWidget()
self.addCleanup(window.deleteLater)
window.resize(*window_size)
layout = QVBoxLayout(window)
layout.setContentsMargins(0, 0, 0, 0)
page = page_cls(parent=window, **deps)
self.addCleanup(page.cleanup)
layout.addWidget(page)
return window, page
def test_constructor_builds_the_first_screen_and_reserves_the_settings(self) -> None:
for page_name in _PAGES:
with self.subTest(page=page_name.name):
_window, page = self._page(page_name)
# Первый экран готов сразу.
for name in ("status_card", "top_summary", "quick_actions_grid", "onboarding_tour_card"):
self.assertIsNotNone(getattr(page, name), name)
# Настройки ещё не собраны, но место под них занято.
for name in _SETTINGS_WIDGETS:
self.assertIsNone(getattr(page, name), name)
for name in _BLOCKS:
block = page.lazy_block(name)
self.assertFalse(block.is_built(), name)
self.assertGreater(block.minimumHeight(), 50, name)
def test_settings_stay_unbuilt_while_they_are_below_the_window_edge(self) -> None:
for page_name in _PAGES:
with self.subTest(page=page_name.name):
window, page = self._page(page_name, window_size=(1100, 700))
window.show()
_pump(0.05)
self.assertFalse(page.is_page_built())
self.assertIsNone(page.program_settings_card)
self.assertGreater(page.verticalScrollBar().maximum(), sum(SETTINGS_BLOCK_HEIGHTS.values()) * 0.8)
def test_tall_window_gets_the_visible_settings_right_at_show(self) -> None:
for page_name in _PAGES:
with self.subTest(page=page_name.name):
window, page = self._page(page_name, window_size=(1600, 900))
window.show()
# Ни одного оборота цикла событий: группа попала в первый экран
# и собрана самим показом, без таймеров и пустого места.
self.assertIsNotNone(page.program_settings_card)
self.assertTrue(page.lazy_block(PROGRAM_SETTINGS_BLOCK).is_built())
# А то, что и в высоком окне ниже края, показ не собирает.
self.assertFalse(page.lazy_block(LAST_MESSAGE_BLOCK).is_built())
def test_scrolling_down_builds_the_settings(self) -> None:
for page_name in _PAGES:
with self.subTest(page=page_name.name):
window, page = self._page(page_name, window_size=(1100, 700))
window.show()
_pump(0.05)
bar = page.verticalScrollBar()
bar.setValue(300)
_pump(0.1)
self.assertIsNotNone(page.program_settings_card)
self.assertFalse(page.is_page_built(), "собирается то, до чего долистали, а не всё сразу")
bar.setValue(bar.maximum())
_pump(0.15)
bar.setValue(bar.maximum())
_pump(0.15)
for name in ("additional_settings_card", "last_status_message_card", "wssize_toggle"):
self.assertIsNotNone(getattr(page, name), name)
def test_reserved_height_is_close_to_the_real_one(self) -> None:
for page_name in _PAGES:
with self.subTest(page=page_name.name):
window, page = self._page(page_name, window_size=(1100, 700))
window.show()
_pump(0.05)
reserved = {name: page.lazy_block(name).height() for name in _BLOCKS}
length = page.verticalScrollBar().maximum()
page.ensure_all_blocks()
_pump(0.05)
for name in _BLOCKS:
real = page.lazy_block(name).height()
self.assertLess(abs(real - reserved[name]), 30, f"{name}: место {reserved[name]}, блок {real}")
self.assertLess(abs(page.verticalScrollBar().maximum() - length), 40)
def test_settings_that_arrived_before_the_block_are_applied_when_it_is_built(self) -> None:
for page_name in _PAGES:
with self.subTest(page=page_name.name):
_window, page = self._page(page_name)
snapshot = SimpleNamespace(
auto_dpi_enabled=True,
gui_autostart_enabled=True,
tray_close_mode="minimize_only",
defender_disabled=False,
max_blocked=True,
state_media_blocked=False,
)
plan = SimpleNamespace(discord_restart=True, wssize_enabled=True, debug_log_enabled=False)
# Блока ещё нет: применять некуда, но и падать нельзя.
page._apply_program_settings_snapshot(snapshot)
page._apply_additional_settings_state(plan)
page.ensure_all_blocks()
self.assertTrue(page.auto_dpi_toggle.isChecked())
self.assertTrue(page.gui_autostart_toggle.isChecked())
self.assertEqual(page.tray_close_mode_combo.currentData(), "minimize_only")
self.assertTrue(page.max_block_toggle.isChecked())
self.assertFalse(page.defender_toggle.isChecked())
self.assertTrue(page.discord_restart_toggle.isChecked())
self.assertTrue(page.wssize_toggle.isChecked())
self.assertFalse(page.debug_log_toggle.isChecked())
def test_tour_targets_and_language_change_build_the_block_themselves(self) -> None:
for page_name in _PAGES:
with self.subTest(page=page_name.name):
_window, page = self._page(page_name)
self.assertIsNotNone(page.onboarding_target("quick_actions"))
self.assertFalse(page.is_page_built(), "цель первого экрана блок не достраивает")
self.assertIsNotNone(page.onboarding_target("program_settings"))
self.assertTrue(page.lazy_block(PROGRAM_SETTINGS_BLOCK).is_built())
self.assertFalse(page.lazy_block(WINDOWS_SETTINGS_BLOCK).is_built(), "достраивается только нужная группа")
self.assertIsNotNone(page.onboarding_target("windows_settings"))
self.assertIsNotNone(page.onboarding_target("fine_tuning"))
_window, page = self._page(page_name)
page.set_ui_language("en")
self.assertTrue(page.is_page_built())
self.assertIsNotNone(page.additional_settings_card)
if __name__ == "__main__":
unittest.main()