zapretgui/tests/test_icon_loop.py
loop-uh d7be499342
All checks were successful
Zapret source guards / validate (push) Successful in 8s
Windows-сборка / Dev Dev 21.1.7.4 опубликован
Жесты значков, поблёскивание и моргание медоеда тоже замирают, пока экран никто не видит
Пауза общего такта (сеанс заблокирован, дисплей выключен) останавливала
только анимации на такте. Очередь жестов значков (раз в 2,6 с), поблёскивание
значков и моргание медоеда шли на своих таймерах и продолжали рисовать:
на win10 при отключённом сеансе это ~12 перерисовок окна в секунду и
около 2% ядра. Теперь они пропускают ход, пока такт на паузе, но
следующий назначают — после возвращения всё продолжается само.
2026-10-04 00:18:15 +03:00

181 lines
7.1 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 import mock
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtCore import QRectF
from PyQt6.QtGui import QColor, QImage, QPainter
from PyQt6.QtWidgets import QApplication, QHBoxLayout, QWidget
import ui.widgets.icon_loop as loop_module
import ui.widgets.motion_icon as motion_module
from presets.ui.control.quick_actions import build_quick_actions
from presets.ui.control.top_summary_widget import ControlTopSummaryWidget
from ui.widgets.icon_loop import LOOP_STEP_MS, icon_loop_conductor
from ui.widgets.line_icons import ANIMATED_LINE_ICONS, paint_line_icon
from ui.widgets.motion_icon import GESTURE_LOOP, MotionIcon
def _frame(name: str, t: float) -> QImage:
image = QImage(48, 48, QImage.Format.Format_ARGB32)
image.fill(0)
painter = QPainter(image)
paint_line_icon(painter, name, QRectF(0, 0, 48, 48), QColor("#3ee0e8"), t)
painter.end()
return image
class LineIconMotionTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def test_every_animated_icon_moves_and_returns_to_rest(self) -> None:
for name in sorted(ANIMATED_LINE_ICONS):
with self.subTest(icon=name):
rest = _frame(name, 0.0)
self.assertTrue(any(_frame(name, t) != rest for t in (0.25, 0.5, 0.75)))
# Конец жеста совпадает с покоем: зацикливание без рывка.
self.assertEqual(_frame(name, 1.0), rest)
class IconLoopConductorTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def setUp(self) -> None:
self._enabled = True
for module in (loop_module, motion_module):
patcher = mock.patch.object(module, "are_live_animations_enabled", side_effect=lambda: self._enabled)
patcher.start()
self.addCleanup(patcher.stop)
self.host = QWidget()
self.addCleanup(self.host.deleteLater)
layout = QHBoxLayout(self.host)
self.icons = []
for name in ("preset", "profiles", "mode"):
icon = MotionIcon(self.host, size=24)
icon.set_line_icon(name, color="#3ee0e8", size=24)
layout.addWidget(icon)
self.icons.append(icon)
self.conductor = icon_loop_conductor(self.host)
for icon in self.icons:
self.conductor.register(icon)
def test_one_conductor_per_host(self) -> None:
self.assertIs(icon_loop_conductor(self.host), self.conductor)
def test_sleeps_while_host_is_hidden(self) -> None:
self.assertFalse(self.conductor.is_running())
self.host.show()
self.assertTrue(self.conductor.is_running())
self.assertEqual(self.conductor._timer.interval(), LOOP_STEP_MS)
self.host.hide()
self.assertFalse(self.conductor.is_running())
def test_icons_play_one_at_a_time_in_turn(self) -> None:
self.host.show()
played = []
for _ in range(3):
self.conductor._step()
playing = [icon for icon in self.icons if icon.gesture() == GESTURE_LOOP]
self.assertEqual(len(playing), 1)
played.append(playing[0])
playing[0]._anim.stop()
playing[0]._on_finished()
self.assertEqual(played, self.icons)
def test_no_gesture_when_animations_are_off(self) -> None:
self.host.show()
self._enabled = False
self.conductor._step()
self.assertTrue(all(icon.gesture() == "" for icon in self.icons))
# Таймер редкий и ждёт, когда анимации снова включат.
self.assertTrue(self.conductor.is_running())
def test_no_gesture_while_the_screen_is_not_seen(self) -> None:
# Сеанс заблокирован или дисплей выключен: жест никто не увидит.
from ui.frame_clock import frame_clock
self.host.show()
self.addCleanup(frame_clock().resume_all)
frame_clock().set_paused("display_off", True)
self.conductor._step()
self.assertTrue(all(icon.gesture() == "" for icon in self.icons))
# Таймер идёт дальше: после возвращения жесты продолжаются сами.
self.assertTrue(self.conductor.is_running())
frame_clock().set_paused("display_off", False)
self.conductor._step()
self.assertEqual([icon.gesture() for icon in self.icons].count(GESTURE_LOOP), 1)
def test_twinkle_is_skipped_but_rescheduled_while_the_screen_is_not_seen(self) -> None:
from ui.frame_clock import frame_clock
self.host.show()
icon = self.icons[0]
icon.set_idle_twinkle(9000)
self.addCleanup(frame_clock().resume_all)
frame_clock().set_paused("session_locked", True)
icon._on_twinkle_timer()
self.assertEqual(icon.gesture(), "")
self.assertTrue(icon._twinkle_timer.isActive())
frame_clock().set_paused("session_locked", False)
icon._on_twinkle_timer()
self.assertNotEqual(icon.gesture(), "")
def test_icon_without_own_gesture_is_skipped(self) -> None:
star = MotionIcon(self.host, size=24)
star.set_line_icon("star", color="#f5c542", size=24)
self.assertFalse(star.can_loop())
plain = MotionIcon(self.host, size=24)
self.assertFalse(plain.can_loop())
def test_loop_frame_is_painted_by_lines(self) -> None:
self.host.show()
icon = self.icons[0]
self.assertTrue(icon.play_loop())
self.assertFalse(icon.play_loop())
icon._anim.setCurrentTime(icon._anim.duration() // 2)
image = QImage(icon.size(), QImage.Format.Format_ARGB32)
image.fill(0)
icon.render(image)
self.assertFalse(image.isNull())
class HomePageIconLoopTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def test_summary_and_quick_actions_share_one_queue(self) -> None:
content = QWidget()
self.addCleanup(content.deleteLater)
summary = ControlTopSummaryWidget(language="ru", mode_value="Zapret 2", parent=content)
noop = lambda: None # noqa: E731
actions = build_quick_actions(
tr_fn=lambda _key, default: default,
text_prefix="page.winws2_control",
on_open_onboarding_tour=noop,
on_open_connection_test=noop,
on_open_internet_cleanup=noop,
on_open_folder=noop,
on_open_docs=noop,
parent=content,
)
conductor = icon_loop_conductor(content)
icons = conductor.icons()
self.assertEqual(len(icons), 3 + 5)
self.assertIn(summary.preset_item._icon_label, icons)
self.assertNotIn(summary.premium_item._icon_label, icons)
self.assertIn(actions.docs_card.icon_widget(), icons)
if __name__ == "__main__":
unittest.main()