Страница Hosts: - Открывается сразу из готового снимка в памяти (его собирает прогрев), свежесть файла проверяет фоновая задача. Снимок только читает: каталог, текст hosts и признаки доступа; файл и папка etc не создаются. - Переключатели меняют только черновик. Панель внизу показывает «+N / −M строк», сами строки, «Отменить» и «Применить» — запись одной операцией, после неё снимок перечитывается. - Список сервисов — один рисуемый виджет с поиском, фильтрами (подписаны для экранного диктора) и «DNS для всех». - Раздел «Что сейчас в hosts»: блоки ZapretGUI, Telegram, MAX, госСМИ, Adobe и строки пользователя; переключатель Adobe тоже идёт в черновик. - Лишние строки блока ZapretGUI (не относятся ни к одному включённому сервису) черновик видит сам и уберёт при записи. - Профиль сервиса определяется по самому полному совпадению строк, а при одинаковых адресах — по сохранённому выбору: после записи comss_dns у Truth Social страница больше не показывает xbox_dns. - Чтение файла, начатое до «Применить», не затирает свежий результат. - Защиту «только чтение» программа сама не снимает, чужие строки с теми же доменами не удаляет — блок ZapretGUI просто стоит выше. - Восемь отдельных фоновых задач заменены одной HostsCallWorker; старые workflow, воркеры, матрица сервисов и неиспользуемые методы HostsManager удалены. Telegram Proxy: при открытии страница только показывает, прописаны ли записи Telegram в hosts; менять файл — кнопками «Прописать» и «Убрать». Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
213 lines
6.2 KiB
Python
213 lines
6.2 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
IMPORTANT_CONTROL_MARKERS = (
|
|
"PushButton(",
|
|
"PrimaryPushButton(",
|
|
"ToolButton(",
|
|
"PrimaryToolButton(",
|
|
"TransparentToolButton(",
|
|
"LineEdit(",
|
|
"SearchLineEdit(",
|
|
"ComboBox(",
|
|
"CheckBox(",
|
|
"SwitchButton(",
|
|
"PlainTextEdit(",
|
|
"TextEdit(",
|
|
"QListWidget(",
|
|
"QTableWidget(",
|
|
)
|
|
|
|
ACCESSIBILITY_MARKERS = (
|
|
"set_control_accessibility",
|
|
"set_state_text",
|
|
"set_item_accessible_text",
|
|
"set_segmented_items_accessibility",
|
|
"enable_keyboard_click",
|
|
"enable_keyboard_toggle",
|
|
"accessibility",
|
|
)
|
|
|
|
KEYBOARD_COLLECTION_WIDGET_MARKERS = (
|
|
"QListWidget(",
|
|
"(QListWidget):",
|
|
"ListView(",
|
|
"(ListView):",
|
|
"QTableWidget(",
|
|
"(QTableWidget):",
|
|
"TableWidget(",
|
|
"(TableWidget):",
|
|
"SegmentedWidget(",
|
|
)
|
|
|
|
KEYBOARD_COLLECTION_ACCESS_MARKERS = (
|
|
"keyPressEvent",
|
|
"focusInEvent",
|
|
"currentItemChanged",
|
|
"currentChanged",
|
|
"currentCellChanged",
|
|
"set_segmented_items_accessibility",
|
|
"AccessibleTextRole",
|
|
"setFocusPolicy(Qt.FocusPolicy.StrongFocus)",
|
|
"screenReaderStateText",
|
|
"enable_keyboard_click",
|
|
"enable_keyboard_toggle",
|
|
"set_item_accessible_text",
|
|
)
|
|
|
|
CUSTOM_MOUSE_ACTION_MARKERS = (
|
|
"def mousePressEvent",
|
|
"def mouseReleaseEvent",
|
|
"def mouseDoubleClickEvent",
|
|
".mousePressEvent =",
|
|
".mouseReleaseEvent =",
|
|
".mouseDoubleClickEvent =",
|
|
)
|
|
|
|
CUSTOM_KEYBOARD_ACTION_MARKERS = (
|
|
"def keyPressEvent",
|
|
".keyPressEvent =",
|
|
"enable_keyboard_click",
|
|
"enable_keyboard_toggle",
|
|
)
|
|
|
|
ALLOWED_NO_FOCUS_SOURCES = {
|
|
"src/ui/accessibility.py",
|
|
"src/ui/segmented_accessibility.py",
|
|
"src/ui/window_preset_file_drop.py",
|
|
"src/ui/widgets/fluent_scrollbar.py",
|
|
"src/ui/widgets/win11_controls.py",
|
|
}
|
|
|
|
|
|
def _source(path: str) -> str:
|
|
return (ROOT / path).read_text(encoding="utf-8")
|
|
|
|
|
|
def test_ui_files_with_important_controls_keep_accessibility_wiring() -> None:
|
|
missing: list[str] = []
|
|
for path in sorted((ROOT / "src").rglob("*.py")):
|
|
rel_path = path.relative_to(ROOT).as_posix()
|
|
if rel_path.startswith("src/themes/cache/"):
|
|
continue
|
|
source = path.read_text(encoding="utf-8", errors="ignore")
|
|
if not any(marker in source for marker in IMPORTANT_CONTROL_MARKERS):
|
|
continue
|
|
if any(marker in source for marker in ACCESSIBILITY_MARKERS):
|
|
continue
|
|
missing.append(rel_path)
|
|
|
|
assert missing == []
|
|
|
|
|
|
def test_collection_controls_keep_keyboard_or_row_accessibility() -> None:
|
|
missing: list[str] = []
|
|
for path in sorted((ROOT / "src").rglob("*.py")):
|
|
rel_path = path.relative_to(ROOT).as_posix()
|
|
if rel_path.startswith("src/themes/cache/"):
|
|
continue
|
|
source = path.read_text(encoding="utf-8", errors="ignore")
|
|
if not any(marker in source for marker in KEYBOARD_COLLECTION_WIDGET_MARKERS):
|
|
continue
|
|
if any(marker in source for marker in KEYBOARD_COLLECTION_ACCESS_MARKERS):
|
|
continue
|
|
missing.append(rel_path)
|
|
|
|
assert missing == []
|
|
|
|
|
|
def test_custom_mouse_actions_keep_keyboard_activation() -> None:
|
|
missing: list[str] = []
|
|
for path in sorted((ROOT / "src").rglob("*.py")):
|
|
rel_path = path.relative_to(ROOT).as_posix()
|
|
if rel_path.startswith("src/themes/cache/"):
|
|
continue
|
|
source = path.read_text(encoding="utf-8", errors="ignore")
|
|
if not any(marker in source for marker in CUSTOM_MOUSE_ACTION_MARKERS):
|
|
continue
|
|
if any(marker in source for marker in CUSTOM_KEYBOARD_ACTION_MARKERS):
|
|
continue
|
|
missing.append(rel_path)
|
|
|
|
assert missing == []
|
|
|
|
|
|
def test_no_focus_usage_stays_limited_to_decorative_helpers() -> None:
|
|
unexpected: list[str] = []
|
|
for path in sorted((ROOT / "src").rglob("*.py")):
|
|
rel_path = path.relative_to(ROOT).as_posix()
|
|
if rel_path.startswith("src/themes/cache/"):
|
|
continue
|
|
source = path.read_text(encoding="utf-8", errors="ignore")
|
|
if "FocusPolicy.NoFocus" not in source:
|
|
continue
|
|
if rel_path in ALLOWED_NO_FOCUS_SOURCES:
|
|
continue
|
|
unexpected.append(rel_path)
|
|
|
|
assert unexpected == []
|
|
|
|
|
|
def test_profile_shell_keeps_toolbar_accessibility_helper() -> None:
|
|
source = _source("src/profile/ui/shell.py")
|
|
|
|
assert "from profile.ui.shell_accessibility import apply_profile_shell_accessibility" in source
|
|
assert "apply_profile_shell_accessibility(" in source
|
|
for control_name in (
|
|
"add_profile_btn",
|
|
"request_btn",
|
|
"view_menu_btn",
|
|
"order_btn",
|
|
"info_btn",
|
|
"profile_search_input",
|
|
):
|
|
assert f"{control_name}={control_name}" in source
|
|
|
|
|
|
def test_user_presets_build_keeps_accessibility_helper_for_toolbar_and_list() -> None:
|
|
source = _source("src/presets/ui/common/user_presets_build.py")
|
|
|
|
assert (
|
|
"from presets.ui.common.user_presets_accessibility import apply_user_presets_accessibility"
|
|
in source
|
|
)
|
|
assert source.count("apply_user_presets_accessibility(") >= 2
|
|
for control_name in (
|
|
"get_configs_btn",
|
|
"create_btn",
|
|
"import_btn",
|
|
"open_folder_btn",
|
|
"reset_all_btn",
|
|
"presets_info_btn",
|
|
"info_btn",
|
|
"preset_search_input",
|
|
"presets_list",
|
|
):
|
|
assert f"{control_name}={control_name}" in source
|
|
|
|
|
|
def test_premium_build_keeps_button_accessibility_helper() -> None:
|
|
source = _source("src/donater/ui/build.py")
|
|
|
|
assert "apply_premium_button_accessibility" in source
|
|
for keyword in (
|
|
"activate_btn=activate_btn",
|
|
"open_bot_btn=open_bot_btn",
|
|
"refresh_btn=refresh_btn",
|
|
"change_key_btn=change_key_btn",
|
|
"test_btn=test_btn",
|
|
"extend_btn=extend_btn",
|
|
):
|
|
assert keyword in source
|
|
|
|
|
|
def test_notification_infobar_keeps_accessibility_for_dynamic_actions() -> None:
|
|
source = _source("src/ui/window_notification_center.py")
|
|
|
|
assert "self._set_infobar_accessibility(bar" in source
|
|
assert "self._set_infobar_action_button_accessibility(btn, action, button_text)" in source
|
|
assert 'name = f"Действие уведомления: {button_text}"' in source
|