zapretgui/tests/test_hosts_status_accessibility.py
2026-06-14 18:14:48 +03:00

343 lines
14 KiB
Python

from __future__ import annotations
import os
from types import SimpleNamespace
import unittest
from unittest.mock import Mock, patch
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 hosts.ui.page import HostsPage
from hosts.ui.sections_build import build_hosts_status_section
from hosts.ui.sections_build import build_hosts_adobe_section
from qfluentwidgets import SwitchButton
class _DialogButton:
def __init__(self) -> None:
self._text = ""
self._accessible_name = ""
self._accessible_description = ""
self._properties = {}
def setText(self, text: str) -> None: # noqa: N802
self._text = str(text)
def text(self) -> str:
return self._text
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)
def property(self, name: str) -> object: # noqa: A003
return self._properties.get(name)
def setProperty(self, name: str, value: object) -> None: # noqa: N802
self._properties[name] = value
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 _FakeSignal:
def connect(self, callback) -> None:
self.callback = callback
class _RestoreAccessButton:
def __init__(self, text: str) -> None:
self._text = str(text)
self._accessible_name = ""
self._accessible_description = ""
self._properties = {}
self.clicked = _FakeSignal()
def text(self) -> str:
return self._text
def setText(self, text: str) -> None: # noqa: N802
self._text = str(text)
def setFixedWidth(self, width: int) -> None: # noqa: N802
self.fixed_width = int(width)
def setEnabled(self, enabled: bool) -> None: # noqa: N802
self.enabled = bool(enabled)
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)
def property(self, name: str) -> object: # noqa: A003
return self._properties.get(name)
def setProperty(self, name: str, value: object) -> None: # noqa: N802
self._properties[name] = value
class _HostsInfoBar:
instances: list["_HostsInfoBar"] = []
def __init__(self) -> None:
self.widgets = []
_HostsInfoBar.instances.append(self)
@classmethod
def error(cls, **_kwargs):
return cls()
def addWidget(self, widget) -> None: # noqa: N802
self.widgets.append(widget)
class HostsStatusAccessibilityTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def test_info_note_reads_purpose_for_screen_reader(self) -> None:
from hosts.ui.sections_build import build_hosts_info_note
widgets = build_hosts_info_note(
tr_fn=lambda _key, default, **kwargs: default.format(**kwargs) if kwargs else default,
)
self.assertEqual(widgets.info_text_label.accessibleName(), "Заметка hosts: зачем нужен hosts")
self.assertIn("сами блокируют доступ", widgets.info_text_label.accessibleDescription())
self.assertIn(
"Заметка hosts: зачем нужен hosts",
widgets.info_text_label.property("screenReaderStateText"),
)
def test_browser_warning_reads_restart_requirement_for_screen_reader(self) -> None:
from hosts.ui.sections_build import build_hosts_browser_warning
warning = build_hosts_browser_warning(
tr_fn=lambda _key, default, **kwargs: default.format(**kwargs) if kwargs else default,
)
self.assertEqual(warning.accessibleName(), "Предупреждение hosts: перезапустите браузер")
self.assertIn("изменения вступили в силу", warning.accessibleDescription())
self.assertEqual(
warning.property("screenReaderStateText"),
"Предупреждение hosts: перезапустите браузер",
)
def test_status_buttons_have_screen_reader_text(self) -> None:
widgets = build_hosts_status_section(
tr_fn=lambda _key, default, **kwargs: default.format(**kwargs) if kwargs else default,
active_count=3,
on_clear_clicked=lambda: None,
on_open_hosts_file=lambda: None,
)
self.assertEqual(widgets.clear_button.accessibleName(), "Очистить hosts")
self.assertEqual(
widgets.clear_button.property("screenReaderStateText"),
"Очистить hosts",
)
self.assertIn("Удаляет активные домены", widgets.clear_button.accessibleDescription())
self.assertEqual(widgets.open_hosts_button.accessibleName(), "Открыть файл hosts")
self.assertEqual(
widgets.open_hosts_button.property("screenReaderStateText"),
"Открыть файл hosts",
)
self.assertIn("Открывает системный файл hosts", widgets.open_hosts_button.accessibleDescription())
def test_language_refresh_updates_status_button_screen_reader_state(self) -> None:
from hosts.ui.page_lifecycle_helpers import apply_hosts_page_language
clear_btn = _DialogButton()
open_btn = _DialogButton()
apply_hosts_page_language(
tr_fn=lambda _key, default, **kwargs: default.format(**kwargs) if kwargs else default,
clear_btn=clear_btn,
open_hosts_button=open_btn,
info_text_label=None,
browser_warning_label=None,
adobe_desc_label=None,
adobe_title_label=None,
startup_initialized=False,
applying=False,
rebuild_services_selectors_fn=lambda: None,
check_hosts_access_fn=lambda: None,
update_ui_fn=lambda: None,
)
self.assertEqual(clear_btn.accessibleName(), "Очистить hosts")
self.assertEqual(clear_btn.property("screenReaderStateText"), "Очистить hosts")
self.assertIn("Удаляет активные домены", clear_btn.accessibleDescription())
self.assertEqual(open_btn.accessibleName(), "Открыть файл hosts")
self.assertEqual(open_btn.property("screenReaderStateText"), "Открыть файл hosts")
self.assertIn("Открывает системный файл hosts", open_btn.accessibleDescription())
def test_status_state_is_text_for_screen_reader(self) -> None:
active_widgets = build_hosts_status_section(
tr_fn=lambda _key, default, **kwargs: default.format(**kwargs) if kwargs else default,
active_count=3,
on_clear_clicked=lambda: None,
on_open_hosts_file=lambda: None,
)
inactive_widgets = build_hosts_status_section(
tr_fn=lambda _key, default, **kwargs: default.format(**kwargs) if kwargs else default,
active_count=0,
on_clear_clicked=lambda: None,
on_open_hosts_file=lambda: None,
)
self.assertEqual(active_widgets.status_label.accessibleName(), "Статус hosts: Активно 3 доменов")
self.assertEqual(active_widgets.status_dot.accessibleName(), "Индикатор hosts: есть активные домены")
self.assertEqual(active_widgets.card.accessibleName(), "Статус hosts: Активно 3 доменов")
self.assertEqual(active_widgets.card.property("screenReaderStateText"), "Статус hosts: Активно 3 доменов")
self.assertEqual(inactive_widgets.status_label.accessibleName(), "Статус hosts: Нет активных")
self.assertEqual(inactive_widgets.status_dot.accessibleName(), "Индикатор hosts: нет активных доменов")
self.assertEqual(inactive_widgets.card.accessibleName(), "Статус hosts: Нет активных")
self.assertEqual(inactive_widgets.card.property("screenReaderStateText"), "Статус hosts: Нет активных")
def test_runtime_status_update_refreshes_screen_reader_state(self) -> None:
page = HostsPage.__new__(HostsPage)
page.status_dot = QLabel("")
page.status_label = QLabel("Нет активных")
page.status_card = QWidget()
page.service_combos = {}
page.adobe_switch = SwitchButton()
page._adobe_active = False
page._update_profile_row_visual = Mock()
page._log_ui_timing = Mock()
page._tr = lambda _key, default, **kwargs: default.format(**kwargs) if kwargs else default
HostsPage._apply_hosts_runtime_state_to_ui(
page,
SimpleNamespace(active_domains={"one.example", "two.example"}, adobe_active=True),
)
self.assertEqual(page.status_label.text(), "Активно 2 доменов")
self.assertEqual(page.status_label.accessibleName(), "Статус hosts: Активно 2 доменов")
self.assertEqual(
page.status_label.property("screenReaderStateText"),
"Статус hosts: Активно 2 доменов",
)
self.assertEqual(page.status_dot.accessibleName(), "Индикатор hosts: есть активные домены")
self.assertEqual(page.status_card.accessibleName(), "Статус hosts: Активно 2 доменов")
self.assertEqual(page.status_card.property("screenReaderStateText"), "Статус hosts: Активно 2 доменов")
def test_adobe_switch_reads_current_state(self) -> None:
widgets = build_hosts_adobe_section(
tr_fn=lambda _key, default, **_kwargs: default,
adobe_active=False,
on_toggle_adobe=lambda _checked: None,
switch_button_cls=SwitchButton,
)
self.assertEqual(widgets.switch.accessibleName(), "Блокировка Adobe, выключено")
self.assertEqual(widgets.switch.property("screenReaderStateText"), "Блокировка Adobe, выключено")
self.assertIn("Блокирует серверы проверки активации Adobe", widgets.switch.accessibleDescription())
widgets.switch.setChecked(True)
self.assertEqual(widgets.switch.accessibleName(), "Блокировка Adobe, включено")
self.assertEqual(widgets.switch.property("screenReaderStateText"), "Блокировка Adobe, включено")
def test_adobe_switch_works_from_keyboard(self) -> None:
events: list[bool] = []
widgets = build_hosts_adobe_section(
tr_fn=lambda _key, default, **_kwargs: default,
adobe_active=False,
on_toggle_adobe=events.append,
switch_button_cls=SwitchButton,
)
self.assertEqual(widgets.switch.focusPolicy(), Qt.FocusPolicy.StrongFocus)
event = QKeyEvent(QEvent.Type.KeyPress, Qt.Key.Key_Space, Qt.KeyboardModifier.NoModifier)
QApplication.sendEvent(widgets.switch, event)
self.assertTrue(event.isAccepted())
self.assertEqual(events, [True])
self.assertEqual(widgets.switch.accessibleName(), "Блокировка Adobe, включено")
def test_hosts_access_restore_button_is_named_for_screen_reader(self) -> None:
from hosts.ui.access_workflow import show_hosts_access_error
_HostsInfoBar.instances = []
bar, _last_error = show_hosts_access_error(
current_bar=None,
last_error=None,
message="Нет доступа",
tr_fn=lambda _key, default, **kwargs: default.format(**kwargs) if kwargs else default,
info_bar_cls=_HostsInfoBar,
push_button_cls=_RestoreAccessButton,
window=None,
on_restore=lambda _bar, _button: None,
log_warning=lambda _message: None,
log_debug=lambda _message: None,
)
self.assertIs(bar, _HostsInfoBar.instances[0])
restore_btn = bar.widgets[0]
self.assertEqual(restore_btn.accessibleName(), "Восстановить права доступа к hosts")
self.assertEqual(
restore_btn.property("screenReaderStateText"),
"Восстановить права доступа к hosts",
)
self.assertIn("вернуть программе право изменять файл hosts", restore_btn.accessibleDescription())
def test_clear_hosts_confirmation_buttons_are_named_for_screen_reader(self) -> None:
page = HostsPage.__new__(HostsPage)
page._applying = False
page._clear_hosts = Mock()
page._tr = lambda _key, default, **kwargs: default.format(**kwargs) if kwargs else default
page.window = lambda: None
_MessageBox.instances = []
with patch("hosts.ui.page.MessageBox", _MessageBox):
HostsPage._on_clear_clicked(page)
dialog = _MessageBox.instances[0]
self.assertEqual(dialog.yesButton.accessibleName(), "Очистить записи ZapretGUI из hosts")
self.assertIn("Будет удалён только блок записей ZapretGUI", dialog.yesButton.accessibleDescription())
self.assertEqual(dialog.cancelButton.accessibleName(), "Отменить очистку hosts")
self.assertTrue(dialog.exec_called)
page._clear_hosts.assert_not_called()
if __name__ == "__main__":
unittest.main()