zapretgui/tests/test_window_first_frame.py
loop-uh ae33e0c5a7
All checks were successful
Zapret source guards / validate (push) Successful in 12s
Windows-сборка / Dev Dev 21.1.7.175 опубликован
Окно из трея открывается сразу готовым, без белого кадра и без лишней подкачки памяти
Окно, спрятанное в трей, Qt рисует заново целиком, и Windows выводила его
пустым белым до отрисовки (замер на win10: белое через 9-42 мс, готовое через
31-146 мс). Шторка Windows (DWMWA_CLOAK) раньше включалась только при первом
показе; теперь при каждом своём show() и снимается, когда Windows собрала
кадр (DwmFlush) - иначе окно ещё ~20 мс оставалось белым. Показ окна самой
Windows из панели задач не трогается. Скрытое окно больше не выгружает
принудительно свою память и не сбрасывает кэш картинок: Windows и сама
отдаёт эти страницы за пару минут, а сброс только заставлял заново рисовать
значки при открытии. Сборка мусора и возврат пустой кучи остались.
Сборка на win10: 7 из 7 открытий из трея без белого кадра, 49-72 мс.

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

227 lines
7.7 KiB
Python

"""Окно появляется на экране уже нарисованным, без белой вспышки: при запуске и из трея."""
from __future__ import annotations
import os
import sys
import unittest
from pathlib import Path
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from PyQt6.QtCore import QEvent # noqa: E402
from PyQt6.QtGui import QPaintEvent # noqa: E402
from PyQt6.QtWidgets import QApplication, QWidget # noqa: E402
from ui import window_first_frame # noqa: E402
from ui.window_first_frame import FrameReveal, cloak_until_painted, set_window_cloaked # noqa: E402
_APP = QApplication.instance() or QApplication([])
class FrameRevealTests(unittest.TestCase):
def setUp(self) -> None:
self.calls: list[bool] = []
self.order: list[str] = []
self.accept = True
self.window = QWidget()
self.addCleanup(self.window.deleteLater)
def _cloak(self, hwnd: int, cloaked: bool) -> bool:
self.assertTrue(hwnd)
self.calls.append(cloaked)
self.order.append("cloak" if cloaked else "uncloak")
return self.accept
def _composed(self) -> None:
self.order.append("composed")
def _reveal(self, **kwargs) -> FrameReveal:
return FrameReveal(self.window, cloak=self._cloak, wait_composed=self._composed, **kwargs)
def _send_paint(self) -> None:
_APP.sendEvent(self.window, QPaintEvent(self.window.rect()))
def test_window_stays_hidden_until_first_paint_is_done(self) -> None:
reveal = self._reveal()
self.assertTrue(reveal.start())
self.assertEqual(self.calls, [True])
# Событие отрисовки приходит раньше самой отрисовки: открывать окно
# прямо в нём рано, на экран попал бы тот же пустой кадр.
self._send_paint()
self.assertEqual(self.calls, [True])
self.assertTrue(reveal.is_cloaked())
_APP.processEvents()
self.assertEqual(self.calls, [True, False])
self.assertFalse(reveal.is_cloaked())
def test_window_is_revealed_only_once(self) -> None:
reveal = self._reveal()
reveal.start()
self._send_paint()
_APP.processEvents()
self._send_paint()
_APP.processEvents()
reveal.reveal()
self.assertEqual(self.calls, [True, False])
def test_shade_lifts_only_after_windows_composed_the_frame(self) -> None:
# Иначе окно ещё ~20 мс показывалось белым уже без шторки.
reveal = self._reveal()
reveal.start()
self._send_paint()
_APP.processEvents()
self.assertEqual(self.order, ["cloak", "composed", "uncloak"])
def test_window_is_hidden_again_on_next_show(self) -> None:
# Возврат из трея: окно снова прячется до своей отрисовки.
reveal = self._reveal()
reveal.start()
self._send_paint()
_APP.processEvents()
self.assertTrue(reveal.start())
self.assertTrue(reveal.is_cloaked())
self._send_paint()
_APP.processEvents()
self.assertEqual(self.calls, [True, False, True, False])
def test_one_reveal_serves_every_show_of_a_window(self) -> None:
self.window._frame_reveal = self._reveal()
first = cloak_until_painted(self.window)
self._send_paint()
_APP.processEvents()
second = cloak_until_painted(self.window)
self.assertIs(first, second)
self.assertEqual(self.calls, [True, False, True])
def test_window_is_revealed_when_paint_never_comes(self) -> None:
# Белое окно лучше невидимой программы.
reveal = self._reveal(wait_max_ms=0)
reveal.start()
_APP.processEvents()
self.assertEqual(self.calls, [True, False])
def test_refused_cloak_leaves_window_alone(self) -> None:
self.accept = False
reveal = self._reveal()
self.assertFalse(reveal.start())
self._send_paint()
_APP.processEvents()
self.assertEqual(self.calls, [True])
self.assertFalse(reveal.is_cloaked())
def test_other_systems_do_nothing(self) -> None:
if sys.platform == "win32":
self.skipTest("проверка для систем без DWM")
self.assertFalse(set_window_cloaked(1, True))
def test_deadline_is_bounded(self) -> None:
self.assertLessEqual(window_first_frame.FRAME_WAIT_MAX_MS, 2_000)
class FrameRevealWiringTests(unittest.TestCase):
def _window(self):
from types import SimpleNamespace
from main.window_lifecycle import WindowLifecycleMixin
class _Base:
def installEventFilter(self, _event_filter) -> None:
pass
def showEvent(self, _event) -> None:
pass
class Window(WindowLifecycleMixin, _Base):
def __init__(self) -> None:
self.startup_state = SimpleNamespace(ttff_logged=False, ttff_ms=None)
return Window()
def _show_events(self, window, *events):
from unittest.mock import patch
with (
patch("main.window_lifecycle.QTimer.singleShot"),
patch("main.window_lifecycle.emit_startup_metric"),
patch("main.window_lifecycle.cloak_until_painted") as cloak,
):
for event in events:
window.showEvent(event)
return cloak
def test_every_own_show_hides_window_until_painted(self) -> None:
from types import SimpleNamespace
own_show = SimpleNamespace(spontaneous=lambda: False)
window = self._window()
# Запуск и два возврата из трея.
cloak = self._show_events(window, own_show, own_show, own_show)
self.assertEqual(cloak.call_count, 3)
cloak.assert_called_with(window)
def test_window_restored_by_windows_is_not_hidden(self) -> None:
from types import SimpleNamespace
system_show = SimpleNamespace(spontaneous=lambda: True)
window = self._window()
cloak = self._show_events(window, system_show)
cloak.assert_not_called()
class SidebarHeaderSettleTests(unittest.TestCase):
"""Заголовки групп меню не растут на глазах после появления окна."""
def test_running_header_animations_jump_to_the_end(self) -> None:
from types import SimpleNamespace
from qfluentwidgets import NavigationInterface, NavigationItemPosition
import ui.navigation.sidebar_builder as sidebar_builder
host = QWidget()
self.addCleanup(host.deleteLater)
nav = NavigationInterface(host, showMenuButton=True)
header = nav.addItemHeader("Группа", NavigationItemPosition.SCROLL)
nav.expand(False)
self.assertEqual(header.height(), 0)
session = SimpleNamespace(nav_headers=[(header, (), "key"), (SimpleNamespace(), (), "fake")])
sidebar_builder._settle_sidebar_header_animations(session)
self.assertEqual(header.height(), header.heightAni.endValue())
self.assertGreater(header.height(), 0)
def test_init_navigation_settles_headers_last(self) -> None:
import inspect
import ui.navigation.sidebar_builder as sidebar_builder
source = inspect.getsource(sidebar_builder.init_navigation)
self.assertLess(
source.index("_restore_sidebar_expanded_state(window)"),
source.index("_settle_sidebar_header_animations(session)"),
)
if __name__ == "__main__":
unittest.main()