zapretgui/tests/test_base_page_accessibility.py
loop-uh fb7eb914a2
All checks were successful
Zapret source guards / validate (push) Successful in 10s
Page show: find the first focusable control lazily
The keyboard-focus search that runs on every page show wrapped every widget
of the page in Python (findChildren) before looking for the first one. Next to
a busy thread each wrapper hands the interpreter lock over, so on a loaded PC
it cost up to ~10 ms per tab switch on small pages and more on big ones.
The search now walks the page depth-first in the same order and stops at the
first match (measured: 0.01-0.06 ms with a busy thread, same result on 12 pages).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 16:18:18 -04:00

167 lines
6.7 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 inspect
import os
import unittest
from unittest.mock import patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QLabel, QPushButton, QWidget
from ui.pages.base_page import BasePage
class BasePageAccessibilityTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def test_page_scrollbar_arrow_buttons_do_not_take_tab_focus(self) -> None:
page = BasePage("Тестовая страница")
self.addCleanup(page.deleteLater)
buttons = [
child
for child in page.findChildren(object)
if type(child).__name__ == "ArrowButton"
and hasattr(child, "setFocusPolicy")
]
self.assertTrue(buttons)
self.assertTrue(all(button.focusPolicy() == Qt.FocusPolicy.NoFocus for button in buttons))
def test_page_titles_expose_screen_reader_state_text(self) -> None:
page = BasePage("Тестовая страница", "Описание тестовой страницы")
self.addCleanup(page.deleteLater)
section = page.add_section_title("Основные настройки", return_widget=True)
self.assertEqual(
page.title_label.property("screenReaderStateText"),
"Заголовок страницы: Тестовая страница",
)
self.assertEqual(
page.subtitle_label.property("screenReaderStateText"),
"Описание страницы: Описание тестовой страницы",
)
self.assertEqual(
section.property("screenReaderStateText"),
"Раздел страницы: Основные настройки",
)
def test_page_finds_first_keyboard_control_for_tab_navigation(self) -> None:
page = BasePage("Тестовая страница")
self.addCleanup(page.deleteLater)
label = QLabel("Только текст", page.content)
page.layout.addWidget(label)
first_button = QPushButton("Первая кнопка", page.content)
second_button = QPushButton("Вторая кнопка", page.content)
page.layout.addWidget(first_button)
page.layout.addWidget(second_button)
page.show()
self._app.processEvents()
self.assertIs(page._first_keyboard_focus_control(), first_button)
def test_focus_candidates_come_lazily_in_find_children_order(self) -> None:
# Рядом с занятым потоком каждый обёрнутый в Python виджет отдаёт замок
# интерпретатора, поэтому поиск не должен оборачивать всю страницу
# (findChildren), а обязан остановиться на первом подходящем виджете.
page = BasePage("Тестовая страница")
self.addCleanup(page.deleteLater)
group = QWidget(page.content)
page.layout.addWidget(group)
QLabel("Текст", group)
nested_button = QPushButton("Вложенная", group)
for index in range(30):
QPushButton(f"Кнопка {index}", page.content)
page.show()
self._app.processEvents()
candidates = page._iter_keyboard_focus_candidates()
self.assertTrue(inspect.isgenerator(candidates))
self.assertEqual(list(candidates), page.content.findChildren(QWidget))
self.assertIs(page._first_keyboard_focus_control(), nested_button)
def test_page_focuses_first_keyboard_control_when_focus_is_outside(self) -> None:
outside_button = QPushButton("Снаружи")
self.addCleanup(outside_button.deleteLater)
outside_button.show()
page = BasePage("Тестовая страница")
self.addCleanup(page.deleteLater)
first_button = QPushButton("Первая кнопка", page.content)
page.layout.addWidget(first_button)
page.show()
self._app.processEvents()
outside_button.setFocus()
self._app.processEvents()
page._focus_first_keyboard_control_if_needed()
self.assertIs(self._app.focusWidget(), first_button)
def test_page_keeps_existing_focus_inside_page(self) -> None:
page = BasePage("Тестовая страница")
self.addCleanup(page.deleteLater)
first_button = QPushButton("Первая кнопка", page.content)
second_button = QPushButton("Вторая кнопка", page.content)
page.layout.addWidget(first_button)
page.layout.addWidget(second_button)
page.show()
self._app.processEvents()
second_button.setFocus()
self._app.processEvents()
page._focus_first_keyboard_control_if_needed()
self.assertIs(self._app.focusWidget(), second_button)
def test_page_title_screen_reader_text_updates_with_language(self) -> None:
def _tr(key: str, *, language: str, default: str) -> str:
texts = {
("test.title", "ru"): "Русский заголовок",
("test.title", "en"): "English title",
("test.subtitle", "ru"): "Русское описание",
("test.subtitle", "en"): "English description",
("test.section", "ru"): "Русский раздел",
("test.section", "en"): "English section",
}
return texts.get((key, language), default)
with patch("ui.pages.base_page.tr_catalog", side_effect=_tr):
page = BasePage(
"Запасной заголовок",
"Запасное описание",
title_key="test.title",
subtitle_key="test.subtitle",
)
self.addCleanup(page.deleteLater)
section = page.add_section_title(
"Запасной раздел",
return_widget=True,
text_key="test.section",
)
page.set_ui_language("en")
self.assertEqual(
page.title_label.property("screenReaderStateText"),
"Заголовок страницы: English title",
)
self.assertEqual(
page.subtitle_label.property("screenReaderStateText"),
"Описание страницы: English description",
)
self.assertEqual(
section.property("screenReaderStateText"),
"Раздел страницы: English section",
)
if __name__ == "__main__":
unittest.main()