zapretgui/tests/test_preset_row_quiet_actions.py
loop-uh e47f381da7
All checks were successful
Zapret source guards / validate (push) Successful in 9s
Windows-сборка / Dev Dev 21.1.7.74 опубликован
Наведение на плитки и строки списков: быстрый ровный отклик вместо блика и наклона значка
Подсветка догоняет цель с первого кадра (около 0,1 с на появление) и
разворачивается без рывка; таймер точный. Блик и наклон значка убраны.
На плитках пресетов кнопки проявляются вместе с подсветкой, пометки
гаснут, а имя больше не перескакивает при наведении.

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

224 lines
9.4 KiB
Python

from __future__ import annotations
import os
import unittest
from unittest.mock import patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtCore import QRect
from PyQt6.QtGui import QFontMetrics, QImage, QPainter
from PyQt6.QtWidgets import QApplication, QStyle, QStyleOptionViewItem
from ui.presets_menu import delegate as delegate_module
from ui.presets_menu.delegate import PresetListDelegate
from ui.presets_menu.model import PresetListModel, repeated_folder_prefix_length
from ui.presets_menu.view import LinkedWheelListView
from ui.widgets.row_hover_motion import row_hover_motion
DATE = "05.10.2026 17:46"
def _rows() -> list[dict[str, object]]:
return [
{"kind": "folder", "folder_key": "pinned", "name": "Закрепленные", "is_collapsed": False, "count": 1},
{
"kind": "preset",
"file_name": "pinned.txt",
"name": "ALL TCP & UDP v1",
"folder_key": "all",
"is_pinned": True,
"date": DATE,
},
{"kind": "folder", "folder_key": "all", "name": "ALL TCP & UDP", "is_collapsed": False, "count": 2},
{"kind": "preset", "file_name": "a.txt", "name": "ALL TCP & UDP multisplit_sni", "folder_key": "all", "date": DATE},
{
"kind": "preset",
"file_name": "b.txt",
"name": "Default v1 (game filter)",
"folder_key": "all",
"date": DATE,
"rating": 4,
},
]
class RepeatedFolderPrefixTests(unittest.TestCase):
def test_prefix_is_the_folder_name_with_its_space(self) -> None:
self.assertEqual(repeated_folder_prefix_length("ALL TCP & UDP v3_1", "ALL TCP & UDP"), 14)
self.assertEqual(repeated_folder_prefix_length("all tcp & udp v3_1", "ALL TCP & UDP"), 14)
def test_no_prefix_when_name_only_looks_similar(self) -> None:
self.assertEqual(repeated_folder_prefix_length("ALL TCP & UDPv2", "ALL TCP & UDP"), 0)
self.assertEqual(repeated_folder_prefix_length("ALL TCP & UDP", "ALL TCP & UDP"), 0)
self.assertEqual(repeated_folder_prefix_length("ALL TCP & UDP ", "ALL TCP & UDP"), 0)
self.assertEqual(repeated_folder_prefix_length("general ALT10 1.10.3", "1.10.3"), 0)
self.assertEqual(repeated_folder_prefix_length("Default v1", ""), 0)
def test_model_compares_with_the_header_shown_above_the_row(self) -> None:
model = PresetListModel()
model.set_rows(_rows())
role = PresetListModel.RepeatedPrefixLengthRole
# Закреплённый пресет стоит под «Закрепленные»: повтора над ним нет.
self.assertEqual(model.index(1, 0).data(role), 0)
self.assertEqual(model.index(3, 0).data(role), 14)
self.assertEqual(model.index(4, 0).data(role), 0)
self.assertEqual(model.index(2, 0).data(role), 0)
class _SpyMetrics(QFontMetrics):
elided: list[str] = []
def elidedText(self, text, mode, width, flags=0): # noqa: N802
type(self).elided.append(str(text))
return super().elidedText(text, mode, width, flags)
class PresetRowQuietActionsTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def setUp(self) -> None:
self.view = LinkedWheelListView()
self.addCleanup(self.view.deleteLater)
self.model = PresetListModel(self.view)
self.model.set_rows(_rows())
self.view.setModel(self.model)
self.delegate = PresetListDelegate(self.view)
self.view.setItemDelegate(self.delegate)
def _paint(self, row: int, state: QStyle.StateFlag | None = None) -> tuple[list[str], list[str]]:
"""Рисует строку и возвращает (нарисованные значки, тексты, прошедшие через сокращение)."""
index = self.model.index(row, 0)
option = QStyleOptionViewItem()
option.rect = QRect(0, 0, 900, self.delegate.sizeHint(option, index).height())
option.state = QStyle.StateFlag.State_Enabled
if state is not None:
option.state |= state
image = QImage(900, 60, QImage.Format.Format_ARGB32_Premultiplied)
icons: list[str] = []
real_cached_icon = delegate_module.cached_icon
def spy_icon(name: str, color: str):
icons.append(name)
return real_cached_icon(name, color)
_SpyMetrics.elided = []
painter = QPainter(image)
try:
with patch.object(delegate_module, "cached_icon", spy_icon), patch.object(
delegate_module, "QFontMetrics", _SpyMetrics
):
self.delegate.paint(painter, option, index)
finally:
painter.end()
return icons, list(_SpyMetrics.elided)
def test_row_is_one_line_high(self) -> None:
self.assertEqual(self.delegate.sizeHint(QStyleOptionViewItem(), self.model.index(3, 0)).height(), 32)
def test_idle_tile_shows_only_icon_and_name(self) -> None:
icons, texts = self._paint(3)
self.assertEqual(icons, ["fa5s.file-alt"])
self.assertNotIn(DATE, texts)
def test_tile_hides_the_folder_name_repeated_above_it(self) -> None:
_icons, texts = self._paint(3)
self.assertEqual(texts, ["multisplit_sni"])
def test_pinned_tile_under_another_header_keeps_its_full_name(self) -> None:
_icons, texts = self._paint(1)
self.assertEqual(texts, ["ALL TCP & UDP v1"])
def test_hovered_tile_reveals_pin_and_menu_buttons(self) -> None:
# «Живые анимации» выключены: кнопки показываются сразу по наведению.
with patch("ui.widgets.row_hover_motion.are_live_animations_enabled", return_value=False):
icons, texts = self._paint(3, QStyle.StateFlag.State_MouseOver)
self.assertIn("fa5s.thumbtack", icons)
self.assertIn("fa5s.ellipsis-v", icons)
# Дата на плитку не выводится: она в подсказке.
self.assertNotIn(DATE, texts)
def test_buttons_appear_together_with_the_hover_light(self) -> None:
motion = row_hover_motion(self.view)
index = self.model.index(4, 0)
opacities: dict[str, float] = {}
def paint_at(level: float) -> list[str]:
real_paint = self.delegate._paint_action_icon
def spy(painter, icon_name, *args, **kwargs):
opacities[icon_name] = painter.opacity()
return real_paint(painter, icon_name, *args, **kwargs)
with patch.object(motion, "hover_level", lambda _index: level), patch.object(
self.delegate, "_paint_action_icon", spy
):
return self._paint(index.row())[1]
idle_texts = paint_at(0.0)
self.assertNotIn("fa5s.ellipsis-v", opacities)
half_texts = paint_at(0.5)
self.assertAlmostEqual(opacities["fa5s.ellipsis-v"], 0.5)
full_texts = paint_at(1.0)
self.assertAlmostEqual(opacities["fa5s.ellipsis-v"], 1.0)
# Имя обрезается один раз и при наведении не перескакивает.
self.assertEqual(idle_texts, half_texts)
self.assertEqual(idle_texts, full_texts)
def test_keyboard_focus_reveals_the_same_buttons(self) -> None:
icons, _texts = self._paint(3, QStyle.StateFlag.State_HasFocus)
self.assertIn("fa5s.thumbtack", icons)
self.assertIn("fa5s.ellipsis-v", icons)
def test_pin_of_pinned_preset_and_user_rating_stay_visible(self) -> None:
pinned_icons, _texts = self._paint(1)
rated_icons, _texts = self._paint(4)
self.assertIn("fa5s.thumbtack", pinned_icons)
self.assertNotIn("fa5s.ellipsis-v", pinned_icons)
self.assertIn("fa5s.star", rated_icons)
self.assertNotIn("fa5s.thumbtack", rated_icons)
def test_active_tile_shows_a_check_mark_instead_of_the_file_icon(self) -> None:
self.model.set_active_preset("a.txt")
icons, _texts = self._paint(3)
self.assertEqual(icons, ["fa5s.check-circle"])
def test_hidden_button_place_still_answers_clicks(self) -> None:
# Кнопки скрыты только на вид: наведённая туда мышь попадает в ту же кнопку.
row_rect = QRect(0, 0, 300, 32)
actions = dict(self.delegate._action_rects(row_rect))
self.assertEqual(self.delegate._action_at(row_rect, actions["edit"].center()), "edit")
self.assertEqual(self.delegate._action_at(row_rect, actions["pin"].center()), "pin")
def test_column_width_follows_the_names_not_the_window(self) -> None:
self.view.resize(1400, 400)
self.view.show()
QApplication.processEvents()
count, width = self.delegate.column_layout()
# Имена короткие — плитки узкие, столбцов много; место не простаивает.
self.assertGreaterEqual(count, 4)
self.assertLessEqual(width, 350)
self.model.update_preset_row("a.txt", name="ALL TCP & UDP " + "очень длинное имя пресета " * 3)
self.model.update_preset_row("b.txt", name="Ещё одно " + "очень длинное имя пресета " * 3)
self.assertLess(self.delegate.column_layout()[0], count)
if __name__ == "__main__":
unittest.main()