zapretgui/tests/test_orchestra_accessibility.py
2026-06-17 00:01:00 +03:00

916 lines
45 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.

import os
import unittest
from types import SimpleNamespace
from unittest.mock import Mock, patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtCore import Qt
from PyQt6.QtTest import QTest
from PyQt6.QtWidgets import QApplication, QVBoxLayout, QWidget
from PyQt6.QtWidgets import QListWidget
from qfluentwidgets import BodyLabel, CaptionLabel, ComboBox, LineEdit, PushButton, TransparentToolButton
from orchestra.ui.blocked_page import BlockedDomainRow, OrchestraBlockedPage
from orchestra.ui.locked_page import LockedDomainRow, OrchestraLockedPage
from orchestra.ui.page import OrchestraPage
from orchestra.ui.page_build import build_orchestra_log_card, build_orchestra_log_history_card, build_orchestra_status_card
from orchestra.ui.page_runtime_helpers import protocol_filter_items, set_protocol_filter_items
from orchestra.ui.ratings_page import OrchestraRatingsPage
from orchestra.ui.settings_page import OrchestraSettingsPage
from orchestra.ui.whitelist_page import OrchestraWhitelistPage, WhitelistDomainRow
class _OrchestraFeatureStub:
ASKEY_ALL = ("tcp", "udp")
class _DialogButton:
def __init__(self) -> None:
self._accessible_name = ""
self._accessible_description = ""
def accessibleName(self) -> str: # noqa: N802
return self._accessible_name
def setAccessibleName(self, text: str) -> None: # noqa: N802
self._accessible_name = str(text)
def accessibleDescription(self) -> str: # noqa: N802
return self._accessible_description
def setAccessibleDescription(self, text: str) -> None: # noqa: N802
self._accessible_description = str(text)
class _MessageBox:
instances: list["_MessageBox"] = []
def __init__(self, title: str, body: str, parent=None) -> None:
self.title = title
self.body = body
self.parent = parent
self.yesButton = _DialogButton()
self.cancelButton = _DialogButton()
self.exec_called = False
_MessageBox.instances.append(self)
def exec(self) -> bool:
self.exec_called = True
return False
class OrchestraAccessibilityTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.app = QApplication.instance() or QApplication([])
def tearDown(self) -> None:
self.app.closeAllWindows()
self.app.processEvents()
def test_settings_tabs_items_read_name_and_selection_for_screen_reader(self) -> None:
page = OrchestraSettingsPage(orchestra_feature=_OrchestraFeatureStub())
self.addCleanup(page.deleteLater)
self.assertEqual(
page.pivot.items["locked"].accessibleName(),
"Настройки Оркестратора: Залоченные, выбрано",
)
self.assertEqual(
page.pivot.items["blocked"].accessibleName(),
"Настройки Оркестратора: Заблокированные, не выбрано",
)
page.pivot.setCurrentItem("blocked")
self.assertEqual(
page.pivot.items["locked"].accessibleName(),
"Настройки Оркестратора: Залоченные, не выбрано",
)
self.assertEqual(
page.pivot.items["blocked"].accessibleName(),
"Настройки Оркестратора: Заблокированные, выбрано",
)
def test_locked_page_main_controls_are_named_for_screen_reader(self) -> None:
page = OrchestraLockedPage(orchestra_feature=_OrchestraFeatureStub())
self.addCleanup(page.deleteLater)
self.assertEqual(page.domain_input.accessibleName(), "Домен для залочки стратегии")
self.assertIn("example.com", page.domain_input.accessibleDescription())
self.assertEqual(page.proto_combo.accessibleName(), "Протокол залочки стратегии, выбрано: TCP")
self.assertEqual(
page.proto_combo.property("screenReaderStateText"),
"Протокол залочки стратегии, выбрано: TCP",
)
self.assertIn("TCP или UDP", page.proto_combo.accessibleDescription())
create_menu = getattr(page.proto_combo, "_create_accessible_combo_menu", None)
self.assertIsNotNone(create_menu)
menu = create_menu()
self.addCleanup(menu.deleteLater)
self.assertEqual(
menu.view.item(0).data(Qt.ItemDataRole.AccessibleTextRole),
"Протокол залочки стратегии: TCP, выбран",
)
self.assertEqual(
menu.view.item(1).data(Qt.ItemDataRole.AccessibleTextRole),
"Протокол залочки стратегии: UDP, не выбран",
)
self.assertEqual(page.strat_spin.accessibleName(), "Номер стратегии для залочки, выбрано: 1")
self.assertEqual(
page.strat_spin.property("screenReaderStateText"),
"Номер стратегии для залочки, выбрано: 1",
)
self.assertEqual(page.lock_btn.accessibleName(), "Залочить стратегию для домена")
self.assertEqual(page.lock_btn.property("screenReaderStateText"), "Залочить стратегию для домена")
self.assertEqual(page.search_input.accessibleName(), "Поиск по залоченным доменам")
self.assertEqual(page.search_input.property("screenReaderStateText"), "Поиск по залоченным доменам")
self.assertIn("После ввода перейдите к списку клавишей Tab", page.search_input.accessibleDescription())
self.assertIn("или нажмите Стрелка вниз", page.search_input.accessibleDescription())
self.assertEqual(page.refresh_btn.accessibleName(), "Обновить список залоченных стратегий")
self.assertEqual(
page.refresh_btn.property("screenReaderStateText"),
"Обновить список залоченных стратегий",
)
self.assertEqual(page.unlock_all_btn.accessibleName(), "Разлочить все стратегии")
self.assertEqual(page.unlock_all_btn.property("screenReaderStateText"), "Разлочить все стратегии")
self.assertEqual(
page._hint_label.property("screenReaderStateText"),
"Подсказка залоченных стратегий Оркестратора: Измените номер стратегии и она автоматически сохранится",
)
page.proto_combo.setCurrentIndex(1)
page.strat_spin.setValue(7)
self.assertEqual(page.proto_combo.accessibleName(), "Протокол залочки стратегии, выбрано: UDP")
self.assertEqual(
page.proto_combo.property("screenReaderStateText"),
"Протокол залочки стратегии, выбрано: UDP",
)
self.assertEqual(page.strat_spin.accessibleName(), "Номер стратегии для залочки, выбрано: 7")
self.assertEqual(
page.strat_spin.property("screenReaderStateText"),
"Номер стратегии для залочки, выбрано: 7",
)
def test_locked_search_arrow_down_moves_focus_to_first_visible_row(self) -> None:
page = OrchestraLockedPage(orchestra_feature=_OrchestraFeatureStub())
self.addCleanup(page.deleteLater)
row = LockedDomainRow("example.com", 3, "tcp", page.rows_container)
page.rows_layout.addWidget(row)
page._domain_rows["example.com:tcp"] = row
page.show()
self.app.processEvents()
page.search_input.setFocus()
self.app.processEvents()
QTest.keyClick(page.search_input, Qt.Key.Key_Down)
self.app.processEvents()
self.assertIs(self.app.focusWidget(), row.strat_spin)
def test_blocked_page_main_controls_are_named_for_screen_reader(self) -> None:
page = OrchestraBlockedPage(orchestra_feature=_OrchestraFeatureStub())
self.addCleanup(page.deleteLater)
self.assertEqual(page.domain_input.accessibleName(), "Домен для блокировки стратегии")
self.assertIn("example.com", page.domain_input.accessibleDescription())
self.assertEqual(page.proto_combo.accessibleName(), "Протокол блокировки стратегии, выбрано: TCP")
self.assertEqual(
page.proto_combo.property("screenReaderStateText"),
"Протокол блокировки стратегии, выбрано: TCP",
)
create_menu = getattr(page.proto_combo, "_create_accessible_combo_menu", None)
self.assertIsNotNone(create_menu)
menu = create_menu()
self.addCleanup(menu.deleteLater)
self.assertEqual(
menu.view.item(0).data(Qt.ItemDataRole.AccessibleTextRole),
"Протокол блокировки стратегии: TCP, выбран",
)
self.assertEqual(
menu.view.item(1).data(Qt.ItemDataRole.AccessibleTextRole),
"Протокол блокировки стратегии: UDP, не выбран",
)
self.assertEqual(page.strat_spin.accessibleName(), "Номер блокируемой стратегии, выбрано: 1")
self.assertEqual(
page.strat_spin.property("screenReaderStateText"),
"Номер блокируемой стратегии, выбрано: 1",
)
self.assertEqual(page.block_btn.accessibleName(), "Заблокировать стратегию для домена")
self.assertEqual(page.block_btn.property("screenReaderStateText"), "Заблокировать стратегию для домена")
self.assertEqual(page.search_input.accessibleName(), "Поиск по заблокированным доменам")
self.assertEqual(page.search_input.property("screenReaderStateText"), "Поиск по заблокированным доменам")
self.assertIn(
"После ввода перейдите к списку клавишей Tab",
page.search_input.accessibleDescription(),
)
self.assertIn("или нажмите Стрелка вниз", page.search_input.accessibleDescription())
self.assertEqual(page.refresh_btn.accessibleName(), "Обновить чёрный список стратегий")
self.assertEqual(page.refresh_btn.property("screenReaderStateText"), "Обновить чёрный список стратегий")
self.assertEqual(page.unblock_all_btn.accessibleName(), "Очистить пользовательские блокировки")
self.assertEqual(
page.unblock_all_btn.property("screenReaderStateText"),
"Очистить пользовательские блокировки",
)
self.assertEqual(
page._hint_label.property("screenReaderStateText"),
(
"Подсказка чёрного списка Оркестратора: "
"Измените номер стратегии и она автоматически сохранится • Системные блокировки неизменяемы"
),
)
page.proto_combo.setCurrentIndex(1)
page.strat_spin.setValue(9)
self.assertEqual(page.proto_combo.accessibleName(), "Протокол блокировки стратегии, выбрано: UDP")
self.assertEqual(
page.proto_combo.property("screenReaderStateText"),
"Протокол блокировки стратегии, выбрано: UDP",
)
self.assertEqual(page.strat_spin.accessibleName(), "Номер блокируемой стратегии, выбрано: 9")
self.assertEqual(
page.strat_spin.property("screenReaderStateText"),
"Номер блокируемой стратегии, выбрано: 9",
)
def test_blocked_search_arrow_down_moves_focus_to_first_visible_user_row(self) -> None:
page = OrchestraBlockedPage(orchestra_feature=_OrchestraFeatureStub())
self.addCleanup(page.deleteLater)
row = BlockedDomainRow("blocked.example", 5, "udp", is_default=False, parent=page.rows_container)
page.rows_layout.addWidget(row)
page._blocked_rows.append(row)
page.show()
self.app.processEvents()
page.search_input.setFocus()
self.app.processEvents()
QTest.keyClick(page.search_input, Qt.Key.Key_Down)
self.app.processEvents()
self.assertIs(self.app.focusWidget(), row.strat_spin)
def test_whitelist_page_main_controls_are_named_for_screen_reader(self) -> None:
page = OrchestraWhitelistPage(orchestra_feature=_OrchestraFeatureStub())
self.addCleanup(page.deleteLater)
self.assertEqual(page.restart_warning.accessibleName(), "Предупреждение: изменения белого списка применятся после перезапуска оркестратора")
self.assertEqual(
page.restart_warning.property("screenReaderStateText"),
"Предупреждение: изменения белого списка применятся после перезапуска оркестратора",
)
self.assertEqual(page.domain_input.accessibleName(), "Домен для белого списка")
self.assertEqual(page.domain_input.property("screenReaderStateText"), "Домен для белого списка")
self.assertEqual(page.add_btn.accessibleName(), "Добавить домен в белый список")
self.assertEqual(page.add_btn.property("screenReaderStateText"), "Добавить домен в белый список")
self.assertEqual(page.search_input.accessibleName(), "Поиск по белому списку")
self.assertEqual(page.search_input.property("screenReaderStateText"), "Поиск по белому списку")
self.assertIn("После ввода перейдите к списку клавишей Tab", page.search_input.accessibleDescription())
self.assertIn("или нажмите Стрелка вниз", page.search_input.accessibleDescription())
self.assertEqual(page.clear_user_btn.accessibleName(), "Очистить пользовательские домены белого списка")
self.assertEqual(
page.clear_user_btn.property("screenReaderStateText"),
"Очистить пользовательские домены белого списка",
)
def test_whitelist_search_arrow_down_moves_focus_to_first_visible_user_row(self) -> None:
page = OrchestraWhitelistPage(orchestra_feature=_OrchestraFeatureStub())
self.addCleanup(page.deleteLater)
row = WhitelistDomainRow("safe.example", is_default=False, parent=page.rows_container)
page.rows_layout.addWidget(row)
page._domain_rows.append(row)
page.show()
self.app.processEvents()
page.search_input.setFocus()
self.app.processEvents()
QTest.keyClick(page.search_input, Qt.Key.Key_Down)
self.app.processEvents()
self.assertIs(self.app.focusWidget(), row._delete_btn)
def test_locked_clear_confirmation_buttons_are_named_for_screen_reader(self) -> None:
page = OrchestraLockedPage.__new__(OrchestraLockedPage)
page._cleanup_in_progress = False
page._orchestra = SimpleNamespace(runner=object(), count_locked_strategies=lambda: 3)
page._request_managed_action = Mock()
page._tr = lambda _key, default, **kwargs: default.format(**kwargs) if kwargs else default
page.window = lambda: None
_MessageBox.instances = []
with patch("orchestra.ui.locked_page.MessageBox", _MessageBox):
OrchestraLockedPage._unlock_all(page)
dialog = _MessageBox.instances[0]
self.assertEqual(dialog.yesButton.accessibleName(), "Разлочить все стратегии")
self.assertIn("Разлочить все 3 стратегий", dialog.yesButton.accessibleDescription())
self.assertEqual(dialog.cancelButton.accessibleName(), "Отменить разлочку всех стратегий")
self.assertTrue(dialog.exec_called)
page._request_managed_action.assert_not_called()
def test_blocked_clear_confirmation_buttons_are_named_for_screen_reader(self) -> None:
page = OrchestraBlockedPage.__new__(OrchestraBlockedPage)
page._cleanup_in_progress = False
page._orchestra = SimpleNamespace(runner=object(), count_user_blocked_strategies=lambda: 4)
page._request_managed_action = Mock()
page._tr = lambda _key, default, **kwargs: default.format(**kwargs) if kwargs else default
page.window = lambda: None
_MessageBox.instances = []
with patch("orchestra.ui.blocked_page.MessageBox", _MessageBox):
OrchestraBlockedPage._unblock_all(page)
dialog = _MessageBox.instances[0]
self.assertEqual(dialog.yesButton.accessibleName(), "Очистить пользовательские блокировки")
self.assertIn("пользовательский чёрный список", dialog.yesButton.accessibleDescription())
self.assertEqual(dialog.cancelButton.accessibleName(), "Отменить очистку пользовательских блокировок")
self.assertTrue(dialog.exec_called)
page._request_managed_action.assert_not_called()
def test_whitelist_clear_confirmation_buttons_are_named_for_screen_reader(self) -> None:
page = OrchestraWhitelistPage.__new__(OrchestraWhitelistPage)
page._all_whitelist_data = (("user.example", False), ("system.example", True))
page._request_whitelist_action = Mock()
page._tr = lambda _key, default, **kwargs: default.format(**kwargs) if kwargs else default
page.window = lambda: None
_MessageBox.instances = []
with patch("orchestra.ui.whitelist_page.MessageBox", _MessageBox):
OrchestraWhitelistPage._clear_user_domains(page)
dialog = _MessageBox.instances[0]
self.assertEqual(dialog.yesButton.accessibleName(), "Очистить пользовательские домены белого списка")
self.assertIn("Удалить все пользовательские домены", dialog.yesButton.accessibleDescription())
self.assertEqual(dialog.cancelButton.accessibleName(), "Отменить очистку пользовательских доменов")
self.assertTrue(dialog.exec_called)
page._request_whitelist_action.assert_not_called()
def test_orchestra_list_counters_read_current_counts(self) -> None:
whitelist_page = OrchestraWhitelistPage(orchestra_feature=_OrchestraFeatureStub())
locked_page = OrchestraLockedPage(orchestra_feature=_OrchestraFeatureStub())
blocked_page = OrchestraBlockedPage(orchestra_feature=_OrchestraFeatureStub())
self.addCleanup(whitelist_page.deleteLater)
self.addCleanup(locked_page.deleteLater)
self.addCleanup(blocked_page.deleteLater)
whitelist_page._apply_whitelist_entries((
("system.example", True),
("user.example", False),
))
locked_page._orchestra = SimpleNamespace(
current_locked_snapshot=lambda: SimpleNamespace(total_count=3, tcp_count=2, udp_count=1),
)
locked_page._update_count()
blocked_page._orchestra = SimpleNamespace(
current_blocked_snapshot=lambda: SimpleNamespace(total_count=4, user_count=3, default_count=1),
)
blocked_page._update_count()
self.assertEqual(
whitelist_page.count_label.property("screenReaderStateText"),
"Счётчик белого списка Оркестратора: Всего: 2 (1 системных + 1 пользовательских)",
)
self.assertEqual(
locked_page.count_label.property("screenReaderStateText"),
"Счётчик залоченных стратегий Оркестратора: Всего залочено: 3 (TCP: 2, UDP: 1)",
)
self.assertEqual(
blocked_page.count_label.property("screenReaderStateText"),
"Счётчик заблокированных стратегий Оркестратора: Всего: 4 (3 пользовательских + 1 системных)",
)
def test_whitelist_group_headers_read_section_names(self) -> None:
page = OrchestraWhitelistPage(orchestra_feature=_OrchestraFeatureStub())
self.addCleanup(page.deleteLater)
page._apply_whitelist_entries((
("system.example", True),
("user.example", False),
))
sections = {}
for index in range(page.rows_layout.count()):
widget = page.rows_layout.itemAt(index).widget()
if widget is not None and widget.property("whitelistSection"):
sections[str(widget.property("whitelistSection"))] = widget
self.assertEqual(
sections["user"].property("screenReaderStateText"),
"Раздел белого списка Оркестратора: Пользовательские, 1",
)
self.assertEqual(
sections["system"].property("screenReaderStateText"),
"Раздел белого списка Оркестратора: Системные, 1, нельзя удалить",
)
def test_ratings_page_main_controls_are_named_for_screen_reader(self) -> None:
page = OrchestraRatingsPage(orchestra_feature=_OrchestraFeatureStub())
self.addCleanup(page.deleteLater)
self.assertEqual(page.filter_input.accessibleName(), "Фильтр рейтингов по домену")
self.assertIn("После ввода перейдите к истории клавишей Tab", page.filter_input.accessibleDescription())
self.assertIn("или нажмите Стрелка вниз", page.filter_input.accessibleDescription())
self.assertEqual(page.refresh_btn.accessibleName(), "Обновить рейтинги стратегий")
self.assertEqual(page.refresh_btn.property("screenReaderStateText"), "Обновить рейтинги стратегий")
self.assertEqual(page.stats_label.accessibleName(), "Статистика рейтингов: Загрузка...")
self.assertEqual(
page.history_text.accessibleName(),
"История рейтингов стратегий: история появится после обучения",
)
self.assertIn("результаты обучения", page.history_text.accessibleDescription())
self.assertEqual(
page.history_text.property("screenReaderStateText"),
"История рейтингов стратегий: история появится после обучения",
)
def test_ratings_filter_arrow_down_moves_focus_to_history(self) -> None:
page = OrchestraRatingsPage(orchestra_feature=_OrchestraFeatureStub())
self.addCleanup(page.deleteLater)
page.show()
self.app.processEvents()
page.filter_input.setFocus()
self.app.processEvents()
QTest.keyClick(page.filter_input, Qt.Key.Key_Down)
self.app.processEvents()
self.assertIs(self.app.focusWidget(), page.history_text)
def test_orchestra_search_clear_buttons_do_not_take_tab_focus(self) -> None:
pages_and_inputs = []
for page in (
OrchestraWhitelistPage(orchestra_feature=_OrchestraFeatureStub()),
OrchestraLockedPage(orchestra_feature=_OrchestraFeatureStub()),
OrchestraBlockedPage(orchestra_feature=_OrchestraFeatureStub()),
OrchestraRatingsPage(orchestra_feature=_OrchestraFeatureStub()),
):
self.addCleanup(page.deleteLater)
pages_and_inputs.append(
getattr(page, "filter_input", None) or getattr(page, "search_input", None)
)
for line_edit in pages_and_inputs:
line_edit.setText("example")
buttons = [
child
for child in line_edit.findChildren(object)
if str(getattr(child, "objectName", lambda: "")() or "") == "lineEditButton"
and hasattr(child, "setFocusPolicy")
]
self.assertTrue(buttons)
self.assertTrue(all(button.focusPolicy() == Qt.FocusPolicy.NoFocus for button in buttons))
def test_status_card_exposes_status_as_screen_reader_state(self) -> None:
def create_card(title: str):
card = QWidget()
layout = QVBoxLayout(card)
title_label = BodyLabel(title, card)
layout.addWidget(title_label)
return card, layout, title_label
widgets = build_orchestra_status_card(
create_card=create_card,
tr_fn=lambda _key, default, **_kwargs: default,
body_label_cls=BodyLabel,
caption_label_cls=CaptionLabel,
)
self.addCleanup(widgets.card.deleteLater)
self.assertEqual(widgets.status_label.accessibleName(), "Статус обучения Оркестратора: Не запущен")
self.assertEqual(
widgets.status_label.property("screenReaderStateText"),
"Статус обучения Оркестратора: Не запущен",
)
self.assertEqual(widgets.status_icon.accessibleName(), "Индикатор обучения Оркестратора: Не запущен")
self.assertEqual(
widgets.status_icon.property("screenReaderStateText"),
"Индикатор обучения Оркестратора: Не запущен",
)
def test_status_update_exposes_status_as_screen_reader_state(self) -> None:
page = OrchestraPage(
orchestra_feature=_OrchestraFeatureStub(),
is_runtime_running=lambda: False,
)
self.addCleanup(page.deleteLater)
page._update_status(page.STATE_RUNNING)
self.assertEqual(
page.status_label.accessibleName(),
"Статус обучения Оркестратора: RUNNING - работает на лучших стратегиях",
)
self.assertEqual(
page.status_label.property("screenReaderStateText"),
"Статус обучения Оркестратора: RUNNING - работает на лучших стратегиях",
)
self.assertEqual(
page.status_icon.accessibleName(),
"Индикатор обучения Оркестратора: RUNNING - работает на лучших стратегиях",
)
self.assertEqual(
page.status_icon.property("screenReaderStateText"),
"Индикатор обучения Оркестратора: RUNNING - работает на лучших стратегиях",
)
def test_log_card_controls_are_named_for_screen_reader(self) -> None:
def create_card(title: str):
card = QWidget()
layout = QVBoxLayout(card)
title_label = BodyLabel(title, card)
layout.addWidget(title_label)
return card, layout, title_label
widgets = build_orchestra_log_card(
create_card=create_card,
tr_fn=lambda _key, default, **_kwargs: default,
line_edit_cls=LineEdit,
combo_cls=ComboBox,
body_label_cls=BodyLabel,
caption_label_cls=CaptionLabel,
transparent_tool_button_cls=TransparentToolButton,
fluent_push_button_cls=PushButton,
on_show_log_context_menu=lambda *_args: None,
on_apply_log_filter=lambda *_args: None,
on_clear_log_filter=lambda: None,
on_clear_log=lambda: None,
on_clear_learned_clicked=lambda: None,
)
self.addCleanup(widgets.card.deleteLater)
self.assertEqual(widgets.log_text.accessibleName(), "Лог обучения Оркестратора: пока нет записей обучения")
self.assertIn("строки обучения", widgets.log_text.accessibleDescription())
self.assertEqual(
widgets.log_text.property("screenReaderStateText"),
"Лог обучения Оркестратора: пока нет записей обучения",
)
self.assertEqual(widgets.log_filter_input.accessibleName(), "Фильтр лога Оркестратора по домену")
self.assertIn("example.com", widgets.log_filter_input.accessibleDescription())
self.assertIn("После ввода перейдите к логу клавишей Tab", widgets.log_filter_input.accessibleDescription())
self.assertIn("или нажмите Стрелка вниз", widgets.log_filter_input.accessibleDescription())
widgets.card.show()
self.app.processEvents()
widgets.log_filter_input.setFocus()
self.app.processEvents()
QTest.keyClick(widgets.log_filter_input, Qt.Key.Key_Down)
self.app.processEvents()
self.assertIs(self.app.focusWidget(), widgets.log_text)
self.assertEqual(widgets.clear_filter_btn.accessibleName(), "Сбросить фильтр лога Оркестратора")
self.assertEqual(widgets.clear_log_btn.accessibleName(), "Очистить лог обучения Оркестратора")
self.assertEqual(widgets.clear_learned_btn.accessibleName(), "Сбросить данные обучения Оркестратора")
set_protocol_filter_items(
combo=widgets.log_protocol_filter,
items=protocol_filter_items(tr_fn=lambda _key, default, **_kwargs: default),
)
self.assertEqual(
widgets.log_protocol_filter.accessibleName(),
"Фильтр лога Оркестратора по протоколу, выбрано: Все",
)
self.assertEqual(
widgets.log_protocol_filter.property("screenReaderStateText"),
"Фильтр лога Оркестратора по протоколу, выбрано: Все",
)
create_menu = getattr(widgets.log_protocol_filter, "_create_accessible_combo_menu", None)
self.assertIsNotNone(create_menu)
menu = create_menu()
self.assertEqual(
menu.view.item(0).data(Qt.ItemDataRole.AccessibleTextRole),
"Фильтр лога Оркестратора по протоколу: Все, выбран",
)
self.assertEqual(
menu.view.item(1).data(Qt.ItemDataRole.AccessibleTextRole),
"Фильтр лога Оркестратора по протоколу: TLS, не выбран",
)
widgets.log_protocol_filter.setCurrentIndex(2)
self.assertEqual(
widgets.log_protocol_filter.accessibleName(),
"Фильтр лога Оркестратора по протоколу, выбрано: HTTP",
)
self.assertEqual(
widgets.log_protocol_filter.property("screenReaderStateText"),
"Фильтр лога Оркестратора по протоколу, выбрано: HTTP",
)
def test_log_history_controls_are_named_for_screen_reader(self) -> None:
def create_card(title: str):
card = QWidget()
layout = QVBoxLayout(card)
title_label = BodyLabel(title, card)
layout.addWidget(title_label)
return card, layout, title_label
widgets = build_orchestra_log_history_card(
create_card=create_card,
tr_fn=lambda _key, default, **kwargs: default.format(**kwargs) if kwargs else default,
max_logs=10,
list_widget_cls=QListWidget,
caption_label_cls=CaptionLabel,
fluent_push_button_cls=PushButton,
on_view_log_history=lambda *_args: None,
on_delete_log_history=lambda *_args: None,
on_clear_all_log_history=lambda: None,
)
self.addCleanup(widgets.card.deleteLater)
self.assertEqual(widgets.desc_label.accessibleName(), "Описание истории логов Оркестратора")
self.assertIn("каждый запуск", widgets.desc_label.accessibleDescription().lower())
self.assertEqual(
widgets.log_history_list.accessibleName(),
"История логов Оркестратора: список пока не загружен",
)
self.assertIn("выберите лог", widgets.log_history_list.accessibleDescription().lower())
self.assertEqual(
widgets.log_history_list.property("screenReaderStateText"),
"История логов Оркестратора: список пока не загружен",
)
self.assertEqual(widgets.view_log_btn.accessibleName(), "Просмотреть выбранный лог Оркестратора")
self.assertEqual(widgets.delete_log_btn.accessibleName(), "Удалить выбранный лог Оркестратора")
self.assertEqual(widgets.clear_all_logs_btn.accessibleName(), "Очистить всю историю логов Оркестратора")
def test_log_context_menu_actions_have_screen_reader_text(self) -> None:
from orchestra.ui import page_log_context_workflow
class _Item:
def __init__(self) -> None:
self.values = {}
def setData(self, role, value): # noqa: N802
self.values[role] = value
def data(self, role):
return self.values.get(role)
class _View:
def __init__(self) -> None:
self.items: list[_Item] = []
def item(self, row: int):
return self.items[row]
class _Menu:
instances: list["_Menu"] = []
def __init__(self, parent=None) -> None:
self.parent = parent
self.view = _View()
_Menu.instances.append(self)
def addAction(self, _action): # noqa: N802
self.view.items.append(_Item())
def insertSeparator(self, _action): # noqa: N802
pass
class _Signal:
def connect(self, _callback) -> None:
pass
class _Action:
def __init__(self, text, parent=None) -> None:
self.text = text
self.parent = parent
self.triggered = _Signal()
class _Cursor:
class SelectionType:
LineUnderCursor = object()
def select(self, _selection_type) -> None:
pass
def selectedText(self) -> str: # noqa: N802
return "strategy line"
class _LogText:
def cursorForPosition(self, _pos): # noqa: N802
return _Cursor()
def mapToGlobal(self, pos): # noqa: N802
return pos
with (
patch.object(page_log_context_workflow, "RoundMenu", _Menu),
patch.object(page_log_context_workflow, "Action", _Action),
patch.object(page_log_context_workflow, "exec_popup_menu", lambda *_args, **_kwargs: None),
patch.object(page_log_context_workflow, "parse_log_line_for_strategy", return_value=("example.com", 3, "tls")),
):
page_log_context_workflow.show_log_context_menu(
owner=object(),
log_text=_LogText(),
pos=object(),
is_strategy_blocked_fn=lambda _domain, _strategy: False,
tr_fn=lambda _key, default, **kwargs: default.format(**kwargs) if kwargs else default,
copy_line_fn=lambda _line: None,
lock_strategy_fn=lambda *_args: None,
block_strategy_fn=lambda *_args: None,
unblock_strategy_fn=lambda *_args: None,
add_to_whitelist_fn=lambda _domain: None,
)
menu = _Menu.instances[-1]
accessible_texts = [
item.data(Qt.ItemDataRole.AccessibleTextRole)
for item in menu.view.items
]
self.assertEqual(
accessible_texts,
[
"Копировать строку",
"Залочить стратегию #3 для example.com",
"Заблокировать стратегию #3 для example.com",
"Добавить example.com в белый список",
],
)
self.assertEqual(
[
item.data(Qt.ItemDataRole.AccessibleDescriptionRole)
for item in menu.view.items
],
accessible_texts,
)
def test_log_history_items_expose_screen_reader_text(self) -> None:
from orchestra.ui.page_runtime_helpers import update_log_history_view
widget = QListWidget()
self.addCleanup(widget.deleteLater)
update_log_history_view(
logs=[
{
"id": "log-1",
"created": "2026-06-08 12:30",
"size_str": "14 KB",
"is_current": True,
}
],
tr_fn=lambda _key, default, **_kwargs: default,
log_history_list=widget,
)
self.assertEqual(
widget.item(0).data(Qt.ItemDataRole.AccessibleTextRole),
"Лог Оркестратора: 2026-06-08 12:30, размер 14 KB, текущий",
)
def test_log_history_updates_screen_reader_text_when_current_log_changes(self) -> None:
from orchestra.ui.page_runtime_helpers import update_log_history_view
widget = QListWidget()
self.addCleanup(widget.deleteLater)
update_log_history_view(
logs=[
{
"id": "log-1",
"created": "2026-06-08 12:30",
"size_str": "14 KB",
"is_current": True,
},
{
"id": "log-2",
"created": "2026-06-08 13:00",
"size_str": "18 KB",
"is_current": False,
},
],
tr_fn=lambda _key, default, **_kwargs: default,
log_history_list=widget,
)
widget.setCurrentRow(1)
self.assertEqual(
widget.property("screenReaderStateText"),
"История логов Оркестратора: Лог Оркестратора: 2026-06-08 13:00, размер 18 KB",
)
def test_empty_log_history_item_exposes_screen_reader_text(self) -> None:
from orchestra.ui.page_runtime_helpers import update_log_history_view
widget = QListWidget()
self.addCleanup(widget.deleteLater)
update_log_history_view(
logs=[],
tr_fn=lambda _key, default, **_kwargs: default,
log_history_list=widget,
)
self.assertEqual(
widget.item(0).data(Qt.ItemDataRole.AccessibleTextRole),
"История логов Оркестратора: Нет сохранённых логов",
)
def test_empty_log_history_updates_screen_reader_state_text(self) -> None:
from orchestra.ui.page_runtime_helpers import update_log_history_view
widget = QListWidget()
self.addCleanup(widget.deleteLater)
update_log_history_view(
logs=[],
tr_fn=lambda _key, default, **_kwargs: default,
log_history_list=widget,
)
self.assertEqual(
widget.property("screenReaderStateText"),
"История логов Оркестратора: Нет сохранённых логов",
)
def test_row_controls_include_domain_protocol_and_strategy(self) -> None:
locked_row = LockedDomainRow("example.com", 3, "tcp")
blocked_row = BlockedDomainRow("blocked.example", 5, "udp", is_default=False)
system_blocked_row = BlockedDomainRow("system.example", 1, "tcp", is_default=True)
whitelist_row = WhitelistDomainRow("safe.example", is_default=False)
self.addCleanup(locked_row.deleteLater)
self.addCleanup(blocked_row.deleteLater)
self.addCleanup(system_blocked_row.deleteLater)
self.addCleanup(whitelist_row.deleteLater)
self.assertEqual(locked_row.accessibleName(), "Залоченная стратегия: example.com, TCP, стратегия 3")
self.assertEqual(
locked_row.property("screenReaderStateText"),
"Залоченная стратегия: example.com, TCP, стратегия 3",
)
self.assertEqual(locked_row.strat_spin.accessibleName(), "Стратегия для example.com TCP, выбрано: 3")
self.assertEqual(
locked_row.strat_spin.property("screenReaderStateText"),
"Стратегия для example.com TCP, выбрано: 3",
)
self.assertEqual(locked_row._delete_btn.accessibleName(), "Разлочить example.com TCP")
self.assertEqual(
locked_row._delete_btn.property("screenReaderStateText"),
"Разлочить example.com TCP",
)
locked_row.strat_spin.setValue(8)
self.assertEqual(locked_row.strat_spin.accessibleName(), "Стратегия для example.com TCP, выбрано: 8")
self.assertEqual(
locked_row.strat_spin.property("screenReaderStateText"),
"Стратегия для example.com TCP, выбрано: 8",
)
self.assertEqual(blocked_row.accessibleName(), "Заблокированная стратегия: blocked.example, UDP, стратегия 5")
self.assertEqual(
blocked_row.property("screenReaderStateText"),
"Заблокированная стратегия: blocked.example, UDP, стратегия 5",
)
self.assertEqual(blocked_row.strat_spin.accessibleName(), "Заблокированная стратегия для blocked.example UDP, выбрано: 5")
self.assertEqual(
blocked_row.strat_spin.property("screenReaderStateText"),
"Заблокированная стратегия для blocked.example UDP, выбрано: 5",
)
self.assertEqual(blocked_row._add_btn.accessibleName(), "Добавить ещё одну блокировку для blocked.example UDP")
self.assertEqual(
blocked_row._add_btn.property("screenReaderStateText"),
"Добавить ещё одну блокировку для blocked.example UDP",
)
self.assertEqual(blocked_row._delete_btn.accessibleName(), "Разблокировать blocked.example UDP, стратегия 5")
self.assertEqual(
blocked_row._delete_btn.property("screenReaderStateText"),
"Разблокировать blocked.example UDP, стратегия 5",
)
self.assertEqual(
system_blocked_row.accessibleName(),
"Системная заблокированная стратегия: system.example, TCP, стратегия 1",
)
self.assertEqual(
system_blocked_row._lock_icon_label.accessibleName(),
"Индикатор системной блокировки: system.example TCP, стратегия 1",
)
self.assertEqual(
system_blocked_row._lock_icon_label.property("screenReaderStateText"),
"Индикатор системной блокировки: system.example TCP, стратегия 1",
)
self.assertEqual(
system_blocked_row._default_strat_label.accessibleName(),
"Номер системной заблокированной стратегии для system.example TCP: 1",
)
self.assertEqual(
system_blocked_row._default_strat_label.property("screenReaderStateText"),
"Номер системной заблокированной стратегии для system.example TCP: 1",
)
self.assertEqual(whitelist_row.accessibleName(), "Домен белого списка: safe.example")
self.assertEqual(whitelist_row.property("screenReaderStateText"), "Домен белого списка: safe.example")
self.assertEqual(whitelist_row._delete_btn.accessibleName(), "Удалить safe.example из белого списка")
self.assertEqual(
whitelist_row._delete_btn.property("screenReaderStateText"),
"Удалить safe.example из белого списка",
)
if __name__ == "__main__":
unittest.main()