zapretgui/tests/test_quick_actions_bar_layout.py
loop-uh 299679cdbd
All checks were successful
Zapret source guards / validate (push) Successful in 10s
Мои пресеты: плотная сетка плиток и шапка в одну строку
Список: плитка пресета шириной под имена (а не на треть окна), без отступа
слева и без повтора названия папки в имени — на широком экране 5-6 столбцов.
Включённый пресет отмечен галочкой, оценка и булавка — пометки справа,
под мышью кнопки «закрепить» и «меню». Дата и полное имя — в подсказке.

Шапка: заголовок, кнопки, «Получить конфиги» и поиск стоят в одном ряду;
отдельная полоса про обмен пресетами убрана, её текст стал подсказкой кнопки.
Поиск встаёт в последний ряд кнопок, если помещается; исправлена обрезка
нижнего ряда панели при переносе кнопок.

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

319 lines
13 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
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtCore import QEvent, Qt
from PyQt6.QtGui import QKeyEvent
from PyQt6.QtWidgets import QApplication, QLabel, QWidget
from qfluentwidgets import FluentIcon, LineEdit, PrimaryToolButton, PushButton, SettingCardGroup
from ui.fluent_widgets import (
QuickActionsBar,
SettingsCard,
_SettingCardGroupAutoHeightFilter,
enable_setting_card_group_auto_height,
insert_widget_into_setting_card_group,
)
from ui.presets_menu.toolbar import PresetsToolbarLayout
from ui.widgets.win11_controls import Win11ToggleRow
class QuickActionsBarLayoutTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def test_setting_card_group_height_keeps_quick_action_buttons_visible(self) -> None:
group = SettingCardGroup("Действия")
actions = QuickActionsBar()
actions.add_buttons(
[
PushButton("Открыть файл", icon=FluentIcon.LINK),
PushButton("Сбросить файл", icon=FluentIcon.RETURN),
PushButton("Очистить всё", icon=FluentIcon.DELETE),
]
)
insert_widget_into_setting_card_group(group, 1, actions)
needed_height = group.vBoxLayout.minimumSize().height()
self.assertGreaterEqual(group.minimumHeight(), needed_height)
self.assertGreaterEqual(group.maximumHeight(), needed_height)
self.assertGreaterEqual(actions.sizeHint().height(), actions.actions_layout.minimumSize().height())
def test_presets_toolbar_places_search_on_the_right_when_width_allows(self) -> None:
parent = QWidget()
toolbar = PresetsToolbarLayout(parent, row_count=3, button_spacing=8)
first = PushButton("Импорт")
second = PushButton("Папка")
first.setFixedWidth(80)
second.setFixedWidth(80)
search = LineEdit()
toolbar.set_buttons([first, second])
toolbar.set_trailing_widget(search, minimum_width=180)
toolbar.refresh_layout(420)
row_layout = toolbar._rows[0][1]
widgets = [row_layout.itemAt(index).widget() for index in range(row_layout.count())]
self.assertEqual(widgets[:2], [first, second])
self.assertIn(search, widgets)
def test_presets_toolbar_action_button_is_named_and_keyboard_clickable(self) -> None:
parent = QWidget()
toolbar = PresetsToolbarLayout(parent)
button = toolbar.create_action_button("Импорт", FluentIcon.DOWNLOAD)
clicked: list[bool] = []
button.clicked.connect(lambda: clicked.append(True))
event = QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_Return, Qt.KeyboardModifier.NoModifier)
button.keyPressEvent(event)
self.assertEqual(button.accessibleName(), "Импорт")
self.assertEqual(button.property("screenReaderStateText"), "Импорт")
self.assertEqual(button.focusPolicy(), Qt.FocusPolicy.StrongFocus)
self.assertTrue(event.isAccepted())
self.assertEqual(clicked, [True])
def test_presets_toolbar_icon_button_can_be_named_for_screen_reader(self) -> None:
parent = QWidget()
toolbar = PresetsToolbarLayout(parent)
button = toolbar.create_primary_tool_button(
PrimaryToolButton,
FluentIcon.ADD,
accessible_name="Создать пресет",
accessible_description="Создаёт новый пользовательский пресет.",
)
self.assertEqual(button.accessibleName(), "Создать пресет")
self.assertEqual(button.property("screenReaderStateText"), "Создать пресет")
self.assertIn("новый пользовательский пресет", button.accessibleDescription())
self.assertEqual(button.focusPolicy(), Qt.FocusPolicy.StrongFocus)
def test_presets_toolbar_moves_search_to_next_row_when_width_is_tight(self) -> None:
parent = QWidget()
toolbar = PresetsToolbarLayout(parent, row_count=3, button_spacing=8)
first = PushButton("Импорт")
second = PushButton("Папка")
first.setFixedWidth(80)
second.setFixedWidth(80)
search = LineEdit()
toolbar.set_buttons([first, second])
toolbar.set_trailing_widget(search, minimum_width=180)
toolbar.refresh_layout(230)
first_row_widgets = [
toolbar._rows[0][1].itemAt(index).widget()
for index in range(toolbar._rows[0][1].count())
]
second_row_widgets = [
toolbar._rows[1][1].itemAt(index).widget()
for index in range(toolbar._rows[1][1].count())
]
self.assertIn(first, first_row_widgets)
self.assertIn(second, first_row_widgets)
self.assertIn(search, second_row_widgets)
def test_presets_toolbar_reserves_height_when_search_moves_to_next_row(self) -> None:
parent = QWidget()
toolbar = PresetsToolbarLayout(parent, row_count=3, button_spacing=8)
first = PushButton("Импорт")
second = PushButton("Папка")
first.setFixedWidth(80)
second.setFixedWidth(80)
search = LineEdit()
toolbar.set_buttons([first, second])
toolbar.set_trailing_widget(search, minimum_width=180)
toolbar.refresh_layout(230)
needed_height = toolbar.container.layout().sizeHint().height()
self.assertGreater(needed_height, toolbar._rows[0][0].sizeHint().height())
self.assertGreaterEqual(toolbar.container.minimumHeight(), needed_height)
self.assertGreaterEqual(toolbar.container.maximumHeight(), needed_height)
def test_presets_toolbar_uses_visible_container_width_for_wrapping(self) -> None:
parent = QWidget()
toolbar = PresetsToolbarLayout(parent, row_count=3, button_spacing=8)
first = PushButton("Импорт")
second = PushButton("Папка")
first.setFixedWidth(80)
second.setFixedWidth(80)
search = LineEdit()
toolbar.set_buttons([first, second])
toolbar.set_trailing_widget(search, minimum_width=180)
toolbar.container.setFixedWidth(230)
parent.show()
self._app.processEvents()
toolbar.refresh_layout(420)
first_row_widgets = [
toolbar._rows[0][1].itemAt(index).widget()
for index in range(toolbar._rows[0][1].count())
]
second_row_widgets = [
toolbar._rows[1][1].itemAt(index).widget()
for index in range(toolbar._rows[1][1].count())
]
self.assertIn(first, first_row_widgets)
self.assertIn(second, first_row_widgets)
self.assertNotIn(search, first_row_widgets)
self.assertIn(search, second_row_widgets)
def test_presets_toolbar_skips_relayout_when_state_is_unchanged(self) -> None:
parent = QWidget()
toolbar = PresetsToolbarLayout(parent, row_count=3, button_spacing=8)
first = PushButton("Импорт")
second = PushButton("Папка")
search = LineEdit()
toolbar.set_buttons([first, second])
toolbar.set_trailing_widget(search, minimum_width=180)
toolbar.refresh_layout(420)
first_row_layout = toolbar._rows[0][1]
original_clear_row = toolbar._clear_row
clear_calls: list[int] = []
def _count_clear(row_layout):
clear_calls.append(row_layout.count())
original_clear_row(row_layout)
toolbar._clear_row = _count_clear
toolbar.refresh_layout(420)
self.assertEqual(clear_calls, [])
self.assertIs(first_row_layout.itemAt(0).widget(), first)
self.assertIs(first_row_layout.itemAt(1).widget(), second)
def test_presets_toolbar_puts_page_title_before_the_buttons(self) -> None:
parent = QWidget()
self.addCleanup(parent.deleteLater)
toolbar = PresetsToolbarLayout(parent, row_count=3, button_spacing=8)
title = QLabel("Мои пресеты")
button = PushButton("Импорт")
search = LineEdit()
toolbar.set_buttons([button])
toolbar.set_trailing_widget(search, minimum_width=120)
toolbar.set_leading_widget(title)
toolbar.refresh_layout(900)
first_row = toolbar._rows[0][1]
self.assertIs(first_row.itemAt(0).widget(), title)
self.assertIs(first_row.itemAt(1).widget(), button)
self.assertIs(first_row.itemAt(first_row.count() - 1).widget(), search)
def test_presets_toolbar_keeps_search_in_the_last_button_row_when_it_fits(self) -> None:
parent = QWidget()
self.addCleanup(parent.deleteLater)
toolbar = PresetsToolbarLayout(parent, row_count=4, button_spacing=8)
buttons = [PushButton("Вернуть встроенные пресеты") for _ in range(3)]
search = LineEdit()
toolbar.set_buttons(buttons)
toolbar.set_trailing_widget(search, minimum_width=120)
one_button = buttons[0].sizeHint().width()
# Влезают две кнопки в ряд: третья уходит на второй, поиск встаёт рядом с ней.
toolbar.refresh_layout(2 * one_button + 8 + 60)
second_row = toolbar._rows[1][1]
self.assertIs(second_row.itemAt(0).widget(), buttons[2])
self.assertIs(second_row.itemAt(second_row.count() - 1).widget(), search)
self.assertEqual(toolbar._rows[2][1].count(), 0)
def test_presets_toolbar_height_grows_with_every_new_row(self) -> None:
parent = QWidget()
self.addCleanup(parent.deleteLater)
parent.resize(1200, 300)
toolbar = PresetsToolbarLayout(parent, row_count=4, button_spacing=8)
buttons = [PushButton("Вернуть встроенные пресеты") for _ in range(3)]
toolbar.set_buttons(buttons)
parent.show()
QApplication.processEvents()
one_button = buttons[0].sizeHint().width()
toolbar.refresh_layout(3 * one_button + 16)
one_row_height = toolbar.container.maximumHeight()
toolbar.refresh_layout(one_button)
# Раньше высота считалась до показа новых рядов, и нижний ряд обрезался.
self.assertGreaterEqual(toolbar.container.maximumHeight(), 3 * one_row_height)
def test_setting_card_group_height_updates_after_late_card_addition(self) -> None:
group = SettingCardGroup("Управление")
actions = QuickActionsBar()
actions.add_buttons(
[
PushButton("Открыть", icon=FluentIcon.FOLDER),
PushButton("Перестроить", icon=FluentIcon.SYNC),
]
)
insert_widget_into_setting_card_group(group, 1, actions)
info_card = SettingsCard()
info_card.add_widget(QLabel("Загрузка информации..."))
group.addSettingCard(info_card)
group.resize(638, 200)
group.show()
self._app.processEvents()
needed_info_bottom = info_card.geometry().y() + info_card.sizeHint().height()
self.assertGreaterEqual(group.minimumHeight(), needed_info_bottom)
self.assertGreaterEqual(group.maximumHeight(), needed_info_bottom)
self.assertLessEqual(group.minimumHeight(), needed_info_bottom + 1)
self.assertLessEqual(group.maximumHeight(), needed_info_bottom + 1)
def test_setting_card_group_auto_height_keeps_fluent_setting_card_height(self) -> None:
group = SettingCardGroup("Настройки программы")
rows = [
Win11ToggleRow(
"fa5s.bolt",
"Автозапуск DPI после старта программы",
"После запуска ZapretGUI автоматически запускать текущий DPI-режим",
),
Win11ToggleRow(
"fa5s.shield-alt",
"Отключить Windows Defender",
"Требуются права администратора",
),
]
original_heights = [row.minimumHeight() for row in rows]
for row in rows:
group.addSettingCard(row)
group.resize(710, 300)
group.show()
self._app.processEvents()
enable_setting_card_group_auto_height(group)
self._app.processEvents()
for row, original_height in zip(rows, original_heights):
self.assertGreaterEqual(row.minimumHeight(), original_height)
self.assertGreaterEqual(row.maximumHeight(), original_height)
def test_setting_card_group_auto_height_does_not_refresh_on_own_layout_requests(self) -> None:
events = _SettingCardGroupAutoHeightFilter._EVENTS
self.assertNotIn(QEvent.Type.LayoutRequest, events)
self.assertNotIn(QEvent.Type.Resize, events)
self.assertIn(QEvent.Type.ChildAdded, events)
self.assertIn(QEvent.Type.ChildRemoved, events)
if __name__ == "__main__":
unittest.main()