zapretgui/tests/test_blockcheck_page_accessibility.py
loop-uh 0fdb937526
All checks were successful
Zapret source guards / validate (push) Successful in 10s
Windows-сборка / Dev Dev 21.1.7.191 опубликован
BlockCheck: кнопка «С обходом и без» — две проверки подряд с переключением Zapret
Проверяет как сейчас, переключает Zapret (выключает, если работал, или включает), ждёт смены состояния, проверяет ещё раз и возвращает Zapret как был; сравнение «С Zapret и без» строится по двум записям истории. «Стоп» между проверками и любой сбой возвращают Zapret в исходное состояние. Экскурсия описывает кнопку и подсвечивает её.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 12:44:25 -04:00

819 lines
37 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 unittest.mock import Mock, patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication
from blockcheck_words import report_with_words
from blockcheck.page_run_workflow import request_blockcheck_stop, reset_blockcheck_running_ui, start_blockcheck_page_run
from blockcheck.ui.check_results import BlockcheckSummaryPanel
from blockcheck.ui.page import BlockcheckPage
class _FeatureStub:
pass
def _make_page() -> BlockcheckPage:
with patch.object(BlockcheckPage, "_request_page_initial_state_load", lambda self: None):
return BlockcheckPage(
blockcheck_feature=_FeatureStub(),
dns_feature=_FeatureStub(),
create_strategy_scan_worker=lambda *_args, **_kwargs: None,
)
_REPORT = {
"scope": "all",
"zapret_running": True,
"elapsed": 7.4,
"timed_out": False,
"problems": [
{
"level": "fail",
"text": "X (Twitter) не открывается: соединение блокирует провайдер",
"advice": ["Подберите другую стратегию"],
"action": "strategy",
"target": "x.com",
},
{
"level": "warn",
"text": "DNS подменяет адреса: www.youtube.com",
"advice": ["Включите DoH"],
"action": "dns",
"target": "",
},
],
"working": ["Discord", "YouTube"],
"services": [
{
"key": "youtube",
"label": "YouTube",
"control": False,
"level": "warn",
"headline": "YouTube открывается",
"dns_note": "DNS подменяет адрес www.youtube.com",
"targets": [
{"host": "www.youtube.com", "purpose": "сайт", "ok": True, "short": "открывается", "text": "открывается"},
{"host": "i.ytimg.com", "purpose": "превью", "ok": True, "short": "открывается", "text": "открывается"},
],
},
{
"key": "x",
"label": "X (Twitter)",
"control": False,
"level": "fail",
"headline": "X (Twitter) не открывается",
"dns_note": "",
"targets": [{"host": "x.com", "purpose": "сайт", "ok": False, "short": "соединение сброшено", "text": "сброс"}],
},
],
"voice": {"level": "ok", "headline": "Голосовые звонки: UDP проходит", "advice": [], "items": []},
"freeze": {"level": "ok", "headline": "Обрыва загрузки на 16–20 КБ нет", "advice": [], "items": []},
}
class BlockcheckPageAccessibilityTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.app = QApplication.instance() or QApplication([])
def test_main_controls_are_named_for_screen_reader(self) -> None:
page = _make_page()
self.addCleanup(page.deleteLater)
self.assertEqual(page._tabs_pivot.accessibleName(), "Раздел BlockCheck, выбрано: BlockCheck")
self.assertIn("BlockCheck, Подбор стратегии, Проверка домена, DNS-серверы или DNS подмена", page._tabs_pivot.accessibleDescription())
self.assertEqual(
page._scope_combo.accessibleName(),
"Что проверить BlockCheck, выбрано: Только сайты (быстро)",
)
self.assertEqual(page._start_btn.text(), "Проверить")
self.assertEqual(page._start_btn.accessibleName(), "Запустить BlockCheck")
self.assertTrue(page._stop_btn.isHidden())
self.assertFalse(page._report_btn.isEnabled())
self.assertEqual(page._report_btn.accessibleName(), "Открыть подробный отчёт BlockCheck")
self.assertEqual(page._prepare_support_btn.accessibleName(), "Подготовить обращение по BlockCheck")
self.assertEqual(page._result_cards.accessibleName(), "Результаты BlockCheck: пока нет результатов")
self.assertEqual(page._summary_panel.level, "idle")
def test_help_link_stands_in_the_title_row(self) -> None:
from PyQt6.QtCore import QUrl
page = _make_page()
self.addCleanup(page.deleteLater)
# Ссылка на инструкцию стоит в ряду заголовка и ведёт на статью о подборе стратегии.
self.assertEqual(page.layout.indexOf(page._title_header), 0)
self.assertIs(page.title_label.parentWidget(), page._title_header)
self.assertEqual(
page._help_button.getUrl(),
QUrl("https://wiki2.zapret.moe/Zapret2/find-game-strategy"),
)
self.assertEqual(page._help_button.accessibleName(), "Как это б#&^ь работает?")
self.assertIn("инструкцию", page._help_button.accessibleDescription())
# На вложенном экране заголовок не нужен: вместе с ним уходит и ссылка.
page._set_page_header_visible(False)
self.assertTrue(page._title_header.isHidden())
page._set_page_header_visible(True)
self.assertFalse(page._title_header.isHidden())
def test_hidden_progress_bars_do_not_animate(self) -> None:
# IndeterminateProgressBar по умолчанию запускает бесконечную анимацию
# в конструкторе, и она крутится 60 раз в секунду у скрытой полосы.
from qfluentwidgets import IndeterminateProgressBar
page = _make_page()
self.addCleanup(page.deleteLater)
bars = page.findChildren(IndeterminateProgressBar)
self.assertTrue(bars)
for bar in bars:
if not bar.isVisibleTo(page):
self.assertFalse(bar.isStarted(), bar.accessibleName())
def test_scope_combo_menu_items_are_named_for_screen_reader(self) -> None:
page = _make_page()
self.addCleanup(page.deleteLater)
menu = page._scope_combo._create_accessible_combo_menu()
self.assertEqual(
menu.view.item(0).data(Qt.ItemDataRole.AccessibleTextRole),
"Что проверить BlockCheck: Только сайты (быстро), выбран",
)
def test_partial_reports_show_cards_during_the_run_with_a_pause_between_redraws(self) -> None:
page = _make_page()
self.addCleanup(page.deleteLater)
service = {"key": "discord", "label": "Discord", "level": "ok", "kind": "", "targets": [{"host": "discord.com", "purpose": "сайт", "ok": True, "state": "ok", "short": "открывается", "text": "открывается"}]}
partial = {"partial": True, "services": [service]}
# Проверка не идёт (отчёт опоздал) — экран не трогаем.
page._on_partial(partial)
page._flush_partial()
self.assertFalse(page._result_cards.has_cards())
with patch.object(page._run_runtime, "is_running", return_value=True):
# Десяток отчётов подряд — одна перерисовка, и берётся самый свежий.
with patch.object(page._result_cards, "show_partial", wraps=page._result_cards.show_partial) as shown:
for _ in range(10):
page._on_partial({"partial": True, "services": []})
page._on_partial(partial)
self.assertTrue(page._partial_timer.isActive())
shown.assert_not_called()
page._partial_timer.stop()
page._flush_partial()
shown.assert_called_once_with(partial)
self.assertTrue(page._result_cards.has_cards())
self.assertFalse(page._results_card.isHidden())
# Итог панели ждёт конца проверки.
self.assertIsNone(page._last_report)
# Конец проверки отменяет отложенную перерисовку.
page._on_partial(partial)
page._on_finished(None)
self.assertFalse(page._partial_timer.isActive())
self.assertIsNone(page._pending_partial)
def test_own_domains_row_is_hidden_while_a_check_runs(self) -> None:
# Список сайтов берётся в момент запуска: добавлять свои домены во время проверки бесполезно.
page = _make_page()
with patch("blockcheck.ui.page.start_blockcheck_page_run"):
page._on_start()
self.assertTrue(page._domains_card.isHidden())
page._on_finished(None)
self.assertFalse(page._domains_card.isHidden())
def test_cards_have_no_headers_to_save_space(self) -> None:
page = _make_page()
self.addCleanup(page.deleteLater)
for card in (page._domains_card, page._results_card, page._footer_card):
self.assertIsNone(card._title_label)
# Отдельной карточки управления нет: «Проверить» и выбор набора стоят в панели итога,
# под главной фразой, — так же, как кнопки на вкладке «DNS-серверы».
self.assertIs(page._control_card, page._summary_panel)
actions = page._summary_panel.actions
self.assertEqual(
[actions.itemAt(index).widget() for index in range(3)], [page._start_btn, page._paired_btn, page._stop_btn]
)
self.assertIs(page._scope_combo.parentWidget(), page._summary_panel.hero)
# «Свои домены» — тоже управление проверкой: стоят в шапке, под кнопками, а не под результатами.
self.assertIs(page._domains_card.parentWidget(), page._summary_panel.hero)
self.assertIs(page._summary_panel.controls.itemAt(0).widget(), page._domains_card)
# Подложка — только у шапки и у самих карточек: разделы с карточками её не рисуют.
from qfluentwidgets import CardWidget
for flat in (page._summary_panel, page._results_card, page._domains_card):
self.assertNotIsInstance(flat, CardWidget)
self.assertEqual(flat.findChildren(CardWidget, options=Qt.FindChildOption.FindDirectChildrenOnly)[1:], [])
# Пустая таблица до первой проверки не показывается.
self.assertTrue(page._results_card.isHidden())
# Отчёта ещё нет — блока «Отчёт / Подготовить обращение» тоже нет.
self.assertTrue(page._footer_card.isHidden())
page._switch_tab(0)
self.assertTrue(page._footer_card.isHidden())
def test_diagnostics_tab_is_merged_into_blockcheck(self) -> None:
page = _make_page()
self.addCleanup(page.deleteLater)
self.assertEqual(page.TAB_ORDER, ("blockcheck", "strategy_scan", "domain_lookup", "dns_servers", "dns_spoofing"))
self.assertEqual(page._normalize_tab_key("diagnostics"), "blockcheck")
self.assertEqual(page._normalize_tab_key("connection"), "blockcheck")
def test_finished_report_fills_summary_and_sites(self) -> None:
page = _make_page()
self.addCleanup(page.deleteLater)
page._report_lines = ["🔍 BlockCheck"]
page._on_finished(report_with_words(_REPORT))
self.assertEqual(page._summary_panel.level, "fail")
# Точный смысл — в начале заголовка, шутка (если есть) — после.
self.assertTrue(page._summary_panel.title_label.text().startswith("Найдены проблемы: 2"))
self.assertFalse(page._results_card.isHidden())
self.assertTrue(page._report_btn.isEnabled())
self.assertFalse(page._footer_card.isHidden())
page._switch_tab(1)
self.assertTrue(page._footer_card.isHidden())
page._switch_tab(0)
self.assertFalse(page._footer_card.isHidden())
cards = {widget.card.title: widget.card for widget in page._result_cards.cards()}
names = list(cards)
# Сначала сломанное; YouTube открывается — подмена DNS только меткой.
self.assertEqual(names[0], "X (Twitter)")
self.assertEqual(cards["YouTube"].status, "Открывается")
self.assertIn(("DNS: подменён", "warn"), cards["YouTube"].chips)
self.assertIn("Голосовые звонки (UDP)", names)
# Ход проверки после итога убирается.
self.assertTrue(page._progress_card.isHidden())
def test_stopped_run_does_not_leave_pending_summary(self) -> None:
page = _make_page()
self.addCleanup(page.deleteLater)
page._summary_panel.set_pending()
page._on_finished(None)
self.assertEqual(page._summary_panel.level, "unknown")
self.assertEqual(page._summary_panel.title_label.text(), "Проверка остановлена")
def test_failed_run_is_not_reported_as_stopped(self) -> None:
page = _make_page()
self.addCleanup(page.deleteLater)
page._on_finished({"failed": True, "error": "boom"})
self.assertEqual(page._summary_panel.title_label.text(), "Проверка завершилась с ошибкой")
def test_stop_while_waiting_in_queue_removes_the_run(self) -> None:
page = _make_page()
self.addCleanup(page.deleteLater)
runtime = Mock()
runtime.is_queued.return_value = True
page._run_runtime = runtime
page._on_stop()
runtime.stop.assert_called_once_with()
self.assertEqual(page._summary_panel.title_label.text(), "Проверка остановлена")
def test_zapret_and_dns_buttons_open_their_pages(self) -> None:
from app.page_names import PageName
page = _make_page()
self.addCleanup(page.deleteLater)
with patch("ui.window_adapter.show_page") as show_page, patch(
"ui.workflows.mode.show_active_mode_control_page"
) as show_control:
page._on_problem_action("dns", "")
page._on_problem_action("start_zapret", "x.com")
self.assertEqual(show_page.call_args.args[1], PageName.NETWORK)
show_control.assert_called_once()
def test_strategy_button_opens_strategy_tab_with_target(self) -> None:
page = _make_page()
self.addCleanup(page.deleteLater)
strategy_page = Mock()
page._strategy_tab_page = strategy_page
page.switch_to_tab = Mock()
page._on_problem_action("strategy", "x.com")
page._on_problem_action("strategy_voice", "")
page.switch_to_tab.assert_called_with("strategy_scan")
strategy_page.prefill_target.assert_any_call("x.com", protocol="tcp_https")
strategy_page.prefill_target.assert_any_call("", protocol="stun_voice")
class SummaryPanelTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.app = QApplication.instance() or QApplication([])
def test_problem_rows_have_their_action_buttons(self) -> None:
calls = []
panel = BlockcheckSummaryPanel(on_action=lambda action, target: calls.append((action, target)))
panel.show_report(dict(_REPORT))
rows = panel.problem_rows()
rows[0].action_button.click()
# У предупреждения про DNS — кнопка «Настройка DNS». Она одна на группу и стоит
# в её заголовке, а не в карточке находки.
[dns_group] = [group for group in panel.problem_groups() if group.shared_action_button is not None]
self.assertEqual(dns_group.shared_action_button.text(), "Настройка DNS")
dns_group.shared_action_button.click()
self.assertIsNone(rows[1].action_button)
# Отдельной строки «Открываются: …» нет: открывающиеся сайты показывают плитка и полоса.
self.assertEqual(len(rows), 2)
self.assertEqual(calls, [("strategy", "x.com"), ("dns", "")])
self.assertIn("Zapret включён", panel.env_label.text())
def test_no_problems_is_all_ok(self) -> None:
panel = BlockcheckSummaryPanel()
panel.show_report({"problems": [], "working": ["Discord"], "zapret_running": False, "elapsed": 3})
self.assertEqual(panel.level, "ok")
self.assertTrue(panel.title_label.text().startswith("Всё открывается"))
self.assertIn("Zapret выключен", panel.env_label.text())
def _kind_problem(kind: str, title: str, *, evidence=(), advice=("Подберите другую стратегию",), level="fail") -> dict:
return {
"level": level,
"text": f"{title} не открывается: причина",
"advice": [*evidence, *advice],
"evidence": list(evidence),
"action": "strategy",
"target": f"{title.lower()}.com",
"kind": kind,
"title": title,
}
def _kind_service(label: str, level: str, kind: str = "", *, ok: bool = False, control: bool = False) -> dict:
return {
"key": label.lower(),
"label": label,
"control": control,
"level": level,
"kind": kind,
"headline": label,
"targets": [{"host": f"{label.lower()}.com", "purpose": "сайт", "ok": ok, "short": "подробность"}],
}
_KINDS_REPORT = {
"zapret_running": True,
"elapsed": 9,
"problems": [
_kind_problem("sni", "X", evidence=("С именем x.com соединение обрывается.",)),
_kind_problem("sni", "LinkedIn", evidence=("С именем linkedin.com соединение обрывается.",)),
_kind_problem("ip", "Telegram", advice=("Попробуйте другой DNS",)),
_kind_problem("cut16", "Spotify"),
{"level": "warn", "text": "DNS подменяет ответы", "advice": ["Включите DoH"], "action": "dns", "kind": "dns"},
],
"working": ["Discord"],
"services": [
_kind_service("Discord", "ok", ok=True),
# Подмена DNS при открывающемся сайте — не блокировка сайта.
_kind_service("YouTube", "warn", ok=True),
_kind_service("X", "fail", "sni"),
_kind_service("LinkedIn", "fail", "sni"),
_kind_service("Telegram", "fail", "ip"),
_kind_service("Spotify", "fail", "cut16"),
_kind_service("Google", "ok", ok=True, control=True),
_kind_service("Reddit", "unknown"),
],
}
class BlockKindsOnScreenTests(unittest.TestCase):
"""На экране видно, где бан по адресу, где блокировка по имени, а где обрыв после 16 КБ."""
@classmethod
def setUpClass(cls) -> None:
cls.app = QApplication.instance() or QApplication([])
def test_sites_are_counted_by_block_kind(self) -> None:
from blockcheck.ui.block_kinds_view import site_groups
groups = {group.key: group.names for group in site_groups(_KINDS_REPORT)}
self.assertEqual(
groups,
{
"open": ("Discord", "YouTube"),
"ip": ("Telegram",),
"sni": ("X", "LinkedIn"),
"cut16": ("Spotify",),
"unknown": ("Reddit",),
},
)
# Сначала хорошее, затем виды по тяжести; контрольный сайт не считается.
self.assertEqual([group.key for group in site_groups(_KINDS_REPORT)], ["open", "ip", "sni", "cut16", "unknown"])
def test_overview_shows_a_tile_and_a_bar_share_for_every_kind(self) -> None:
panel = BlockcheckSummaryPanel()
self.addCleanup(panel.deleteLater)
panel.show_report(dict(_KINDS_REPORT))
tiles = panel.overview.tiles()
self.assertFalse(panel.overview.isHidden())
self.assertEqual([tile.group().title for tile in tiles][:4], ["Открываются", "По адресу (IP)", "По имени (SNI)", "Обрыв после 16 КБ"])
self.assertEqual([tile.group().count for tile in tiles], [2, 1, 2, 1, 1])
self.assertEqual([group.key for group in panel.overview.bar.groups()], ["open", "ip", "sni", "cut16", "unknown"])
self.assertIn("по имени (sni) — 2", panel.overview.accessibleDescription() + panel.overview.accessibleName())
self.assertIn("SNI", tiles[2].toolTip())
panel.set_pending()
self.assertTrue(panel.overview.isHidden())
self.assertEqual(panel.overview.tiles(), [])
def test_tile_number_ends_on_its_value_with_animations_off(self) -> None:
from blockcheck.ui import block_kinds_view as view
tile = view.KindTile(view.SiteGroup("sni", "По имени (SNI)", ("X", "LinkedIn")))
self.addCleanup(tile.deleteLater)
with patch.object(view, "are_live_animations_enabled", return_value=False):
tile.play(0)
self.assertEqual(tile.shown_count(), 2)
def test_problems_are_grouped_by_kind_with_one_explanation_per_group(self) -> None:
panel = BlockcheckSummaryPanel(on_action=lambda *_args: None)
self.addCleanup(panel.deleteLater)
panel.show_report(dict(_KINDS_REPORT))
groups = panel.problem_groups()
self.assertEqual([group.kind() for group in groups], ["ip", "sni", "cut16", "dns"])
sni = groups[1]
self.assertEqual(sni.title_label.text(), "Блокировка по имени (SNI)")
self.assertIn("SNI", sni.about_label.text())
# Одинаковый у обоих сайтов совет показан один раз — в заголовке группы.
self.assertEqual(sni.shared_advice_lines, ["→ Подберите другую стратегию"])
# Пояснение и общий совет не стоят абзацами над карточками — они в подсказке заголовка.
self.assertTrue(sni.about_label.isHidden())
self.assertIn("→ Подберите другую стратегию", sni.title_label.toolTip())
# Сайты — карточки одной сетки: в карточке название, объяснение — в подсказке.
self.assertEqual([card.title for card in sni.rows], ["X", "LinkedIn"])
self.assertEqual(sni.flow.cards(), sni.rows)
self.assertIn("С именем x.com соединение обрывается.", sni.rows[0].hint_text)
self.assertEqual(sni.rows[0].action_button.toolTip(), "Подобрать стратегию для x.com")
# В группе из одного сайта совет не выносится в заголовок: он в подсказке карточки.
self.assertEqual(groups[0].shared_advice_lines, [])
self.assertIn("→ Попробуйте другой DNS", groups[0].rows[0].hint_text)
# Все строки по-прежнему доступны списком, по группам.
self.assertEqual(len(panel.problem_rows()), 5)
def test_problem_without_kind_gets_its_own_titled_group(self) -> None:
panel = BlockcheckSummaryPanel()
self.addCleanup(panel.deleteLater)
panel.show_report(dict(_REPORT))
[group] = panel.problem_groups()
self.assertEqual(group.kind(), "other")
# Заголовок и пояснение есть: без них карточки висели между группами как случайные.
self.assertEqual(group.title_label.text(), "Остальное")
self.assertIn("не определила", group.about_label.text())
# Сайт без вида блокировки — такая же карточка: название, что с ним, причина в подсказке.
card = group.rows[0]
self.assertEqual((card.title, card.note), ("X (Twitter)", "не открывается"))
self.assertIn("Соединение блокирует провайдер.", card.hint_text)
class HistoryCardTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.app = QApplication.instance() or QApplication([])
RUNS = [
{"kind": "blockcheck", "time": "2026-10-05T09:00:00", "title": "Все сайты", "level": "ok", "headline": "Всё открывается"},
{"kind": "blockcheck", "time": "2026-10-06T10:00:00", "title": "Все сайты", "level": "fail", "headline": "YouTube не открывается"},
{"kind": "blockcheck", "time": "2026-10-07T20:15:00", "title": "Полная проверка", "level": "катастрофа", "headline": ""},
]
def test_lines_are_newest_first_with_time_scope_and_outcome(self) -> None:
from blockcheck.ui.summary_model import history_lines
lines = history_lines(self.RUNS)
self.assertEqual(lines[0], ("unknown", "? 07.10 20:15 · Полная проверка — итог не записан"))
self.assertEqual(lines[1], ("fail", "✗ 06.10 10:00 · Все сайты — YouTube не открывается"))
self.assertEqual(lines[2], ("ok", "✓ 05.10 09:00 · Все сайты — Всё открывается"))
# Показываются только последние записи.
self.assertEqual(len(history_lines(self.RUNS * 5, limit=4)), 4)
self.assertEqual(history_lines([]), [])
def test_card_is_hidden_until_there_is_history_and_follows_new_runs(self) -> None:
page = _make_page()
self.assertTrue(page._history_card.isHidden())
page._show_history(self.RUNS[:2])
self.assertFalse(page._history_card.isHidden())
self.assertEqual(len(page._history_list.lines()), 2)
self.assertIn("YouTube не открывается", page._history_list.accessibleName())
# Свежая проверка приносит обновлённую историю вместе с отчётом.
page._on_finished({"problems": [], "services": [], "elapsed": 1.0, "history": self.RUNS})
self.assertEqual(len(page._history_list.lines()), 3)
# На другой вкладке карточка скрыта, при возврате — снова видна.
page._switch_tab(page.TAB_ORDER.index("strategy_scan"))
self.assertTrue(page._history_card.isHidden())
page._switch_tab(0)
self.assertFalse(page._history_card.isHidden())
def test_history_comes_with_initial_page_state(self) -> None:
from blockcheck import page_runtime
settings = {"blockcheck": {"user_domains": ["example.com"], "check_history": self.RUNS[:1] + ["мусор"]}}
with patch("settings.store.read_settings", return_value=settings):
state = page_runtime.load_page_initial_state()
self.assertEqual(state.user_domains, ("example.com",))
self.assertEqual(len(state.check_history), 1)
with patch("settings.store.read_settings", side_effect=OSError("нет базы")):
self.assertEqual(page_runtime.load_page_initial_state().check_history, ())
class ScopeChoiceTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.app = QApplication.instance() or QApplication([])
def test_sites_are_the_default_and_deep_diagnostics_is_the_last_choice(self) -> None:
# По умолчанию — проверка, которая ходит как браузер; шумные пробы — только по выбору человека.
page = _make_page()
combo = page._scope_combo
self.assertEqual([combo.itemData(index) for index in range(combo.count())], ["all", "main", "full"])
self.assertEqual(page._current_scope(), "all")
combo.setCurrentIndex(2)
self.assertEqual(page._current_scope(), "full")
self.assertIn("Глубокая диагностика", combo.currentText())
page.set_ui_language("en")
self.assertEqual(combo.itemText(2), "Deep diagnostics (about a minute)")
class SummaryChangesTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.app = QApplication.instance() or QApplication([])
def test_changes_since_last_run_are_shown_and_cleared(self) -> None:
panel = BlockcheckSummaryPanel()
self.assertTrue(panel.changes_label.isHidden())
panel.show_report(
{"problems": [], "changes": ["перестали открываться: YouTube"], "previous_time": "2026-10-06T10:00:00"}
)
self.assertFalse(panel.changes_label.isHidden())
self.assertEqual(panel.changes_label.text(), "С прошлой проверки (06.10 10:00) перестали открываться: YouTube.")
# Отчёт без перемен и новая проверка строку убирают.
panel.show_report({"problems": [], "changes": []})
self.assertTrue(panel.changes_label.isHidden())
panel.show_report({"problems": [], "changes": ["снова открываются: Discord"], "previous_time": "x"})
panel.set_pending()
self.assertTrue(panel.changes_label.isHidden())
class _ButtonStub:
def __init__(self, text: str = "") -> None:
self._enabled = True
self._visible = True
self.properties = {}
self.accessible_name = ""
self.accessible_description = ""
self._text = text
def text(self) -> str:
return self._text
def setText(self, text: str) -> None: # noqa: N802
self._text = str(text)
def setEnabled(self, enabled: bool) -> None: # noqa: N802
self._enabled = bool(enabled)
def isEnabled(self) -> bool: # noqa: N802
return self._enabled
def setVisible(self, visible: bool) -> None: # noqa: N802
self._visible = bool(visible)
def isVisible(self) -> bool: # noqa: N802
return self._visible
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:
return self.properties.get(name)
def setProperty(self, name: str, value: object) -> None: # noqa: N802
self.properties[name] = value
class _FakeProgressBar(_ButtonStub):
def start(self) -> None:
self.started = True
def stop(self) -> None:
self.started = False
class _SignalStub:
def connect(self, _callback) -> None:
pass
class _WorkerStub:
def __init__(self) -> None:
self.log_message = _SignalStub()
self.run_log_started = _SignalStub()
self.finished = _SignalStub()
self.stopped = False
def stop(self) -> None:
self.stopped = True
class _RunFeatureStub:
def __init__(self) -> None:
self.kwargs = {}
def create_blockcheck_worker(self, **kwargs):
self.kwargs = kwargs
return _WorkerStub()
class _RunRuntimeStub:
def __init__(self) -> None:
self.worker = None
def start_qobject_worker(self, *, parent, worker_factory) -> None:
_ = parent
self.worker = worker_factory(1)
class BlockcheckWorkerTests(unittest.TestCase):
def _worker(self):
from types import SimpleNamespace
from blockcheck.worker import BlockcheckWorker
return BlockcheckWorker(
report_path=lambda *_a: "",
save_report=lambda *_a: "",
)
def test_stop_pressed_before_start_is_not_lost(self) -> None:
seen = []
worker = self._worker()
worker.stop()
def _fake_run(*_args, should_stop, **_kwargs):
seen.append(should_stop())
return {"stopped": True}
with patch("diagnostics.engine.run_blockcheck", side_effect=_fake_run):
worker.run()
self.assertEqual(seen, [True])
def test_crash_is_reported_as_failure_not_stop(self) -> None:
results = []
worker = self._worker()
worker.finished.connect(results.append)
with patch("diagnostics.engine.run_blockcheck", side_effect=RuntimeError("boom")):
worker.run()
self.assertEqual(results, [{"failed": True, "error": "boom"}])
class RunWorkflowTests(unittest.TestCase):
def test_run_controls_read_running_and_idle_states(self) -> None:
progress_bar = _FakeProgressBar()
start_button = _ButtonStub()
stop_button = _ButtonStub()
scope_combo = _ButtonStub()
status_label = _ButtonStub()
feature = _RunFeatureStub()
start_blockcheck_page_run(
blockcheck_feature=feature,
scope="all",
user_domains=["example.org"],
parent=None,
run_runtime=_RunRuntimeStub(),
start_button=start_button,
stop_button=stop_button,
scope_combo=scope_combo,
progress_bar=progress_bar,
status_label=status_label,
set_support_status=lambda _text: None,
tr_fn=lambda _key, default: default,
on_log=lambda *_args: None,
on_run_log_started=lambda *_args: None,
on_finished=lambda *_args: None,
)
self.assertEqual(feature.kwargs["scope"], "all")
self.assertEqual(feature.kwargs["user_domains"], ["example.org"])
self.assertEqual(progress_bar.accessibleName(), "Ход BlockCheck: выполняется")
self.assertEqual(start_button.accessibleName(), "Запустить BlockCheck, недоступно")
self.assertEqual(stop_button.accessibleName(), "Остановить BlockCheck, доступно")
self.assertTrue(stop_button.isVisible())
self.assertEqual(scope_combo.accessibleName(), "Что проверить BlockCheck, недоступно во время проверки")
self.assertTrue(status_label.accessibleName().startswith("Статус BlockCheck: Проверяем"))
reset_blockcheck_running_ui(
start_button=start_button,
stop_button=stop_button,
scope_combo=scope_combo,
progress_bar=progress_bar,
)
self.assertEqual(progress_bar.accessibleName(), "Ход BlockCheck: не выполняется")
self.assertEqual(start_button.accessibleName(), "Запустить BlockCheck, доступно")
self.assertFalse(stop_button.isVisible())
self.assertEqual(scope_combo.accessibleName(), "Что проверить BlockCheck, доступно")
def test_stop_request_reads_stopping_state(self) -> None:
worker = _WorkerStub()
stop_button = _ButtonStub()
status_label = _ButtonStub()
request_blockcheck_stop(
worker=worker,
stop_button=stop_button,
status_label=status_label,
force_stop=lambda _worker: None,
tr_fn=lambda _key, default: default,
)
self.assertTrue(worker.stopped)
self.assertEqual(stop_button.accessibleName(), "Остановить BlockCheck, недоступно")
self.assertEqual(status_label.accessibleName(), "Статус BlockCheck: Останавливаем…")
if __name__ == "__main__":
unittest.main()
class HonestSummaryTests(unittest.TestCase):
"""Итог не пугает зря и не молчит, когда ничего не изменилось."""
@classmethod
def setUpClass(cls) -> None:
cls.app = QApplication.instance() or QApplication([])
def test_sites_open_and_only_side_remarks_is_not_a_red_alarm(self) -> None:
panel = BlockcheckSummaryPanel()
self.addCleanup(panel.deleteLater)
report = {
"problems": [{"level": "warn", "text": "IPv6 не работает", "kind": "network", "section": "ipv6", "advice": []}],
"services": [{"key": "youtube", "label": "YouTube", "level": "ok", "targets": []}],
}
panel.show_report(report)
self.assertEqual(panel.title_label.text(), "Сайты открываются, есть замечания: 1")
def test_nothing_changed_since_the_same_check_is_said(self) -> None:
panel = BlockcheckSummaryPanel()
self.addCleanup(panel.deleteLater)
report = {**_REPORT, "changes": [], "previous_time": "2026-10-12T10:00:00"}
panel.show_report(report)
self.assertIn("ничего не изменилось", panel.changes_label.text())
self.assertFalse(panel.changes_label.isHidden())
class StartZapretButtonTests(unittest.TestCase):
def test_start_button_starts_zapret(self) -> None:
from blockcheck.ui.page import BlockcheckPage
page = BlockcheckPage.__new__(BlockcheckPage)
started = []
page._start_zapret = lambda: started.append(True) or True
page.window = lambda: None
with patch("blockcheck.ui.page.InfoBarHelper") as bar:
BlockcheckPage._on_problem_action(page, "start_zapret", "")
self.assertEqual(started, [True])
bar.success.assert_called_once()