zapretgui/tests/test_telegram_proxy_advanced_page.py
loop-uh 2352c31303
Some checks failed
Windows-сборка / Dev Dev 21.1.7.171 опубликован
Zapret source guards / validate (push) Has been cancelled
Стрелка «назад» снова работает: подписки снимаются по квитанции, а не по обработчику
Жалоба 15.10.2026: на «Тексте профиля» стрелка «назад» ничего не делала,
а Alt+← работал. Причина: в сборке Nuitka отписка по методу
(signal.disconnect(obj.method)) не находит подписку — метод там при каждом
обращении новый объект compiled_method; ошибка глоталась. У стрелки
оставались два обработчика: библиотечный qrouter.pop переключал страницу,
журнал тут же возвращал её обратно. Из исходников всё работало, поэтому
стенд не ловил. Проверено на win10 модулем, собранным Nuitka: отписка по
методу — «is not connected», 2 обработчика; по квитанции — 1.

- utils/qt_signal_links: подписка снимается по объекту соединения,
  который вернул connect() (disconnect_link, SignalLinks).
- Стрелка «назад»: отвязываются все прежние обработчики, библиотечный
  список страниц выключен целиком; если обработчиков не один — ошибка в
  журнале.
- Те же молчаливые отписки в сборке исправлены везде: всплывающие меню и
  выпадающие списки (обработчики копились с каждым меню), страницы
  оркестратора (каждая настройка добавляла по обработчику), Telegram Proxy,
  подписка Premium, проверка текста профиля, списки и анимация строк,
  подсказка стратегий. Карточки больше не получают вторую подписку на
  смену темы: библиотека её в конструкторе не создаёт.
- architecture_checks запрещает отписку по обработчику.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:41:30 +03:00

234 lines
9.5 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 os
import unittest
from unittest.mock import MagicMock, Mock, patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtWidgets import QApplication, QWidget
from app.page_names import PageName
from telegram_proxy.config.upstream_catalog import UpstreamCatalog
from telegram_proxy.proxy.upstream_controller import UpstreamRuntimeSnapshot
def _feature(*, pending: bool = False, running: bool = True) -> MagicMock:
feature = MagicMock()
feature.has_pending_settings_saves.return_value = pending
feature.get_proxy_manager.return_value.upstream_state = None
feature.get_proxy_manager.return_value.is_running = running
return feature
def _fallback_snapshot() -> UpstreamRuntimeSnapshot:
return UpstreamRuntimeSnapshot(
selected_preset_id="nl",
selected_name="Нидерланды",
active_preset_id="ee",
active_name="Эстония",
state="fallback",
generation=1,
)
def _catalog() -> UpstreamCatalog:
return UpstreamCatalog(
[
{"id": "nl", "name": "Нидерланды", "type": "socks5", "host": "10.0.0.1", "port": 1080},
{"id": "ee", "name": "Эстония", "type": "socks5", "host": "10.0.0.2", "port": 1080},
]
)
class TelegramProxyAdvancedRouteTests(unittest.TestCase):
def test_route_is_hidden_nested_page_under_telegram_proxy(self) -> None:
from ui.navigation.schema import INNER_PAGE_NAMES, PAGE_CLEANUP_ORDER, get_page_spec
spec = get_page_spec(PageName.TELEGRAM_PROXY_ADVANCED)
self.assertEqual(spec.module_name, "telegram_proxy.ui.advanced_page")
self.assertEqual(spec.class_name, "TelegramProxyAdvancedPage")
self.assertFalse(spec.is_top_level)
self.assertTrue(spec.is_hidden)
self.assertIsNone(spec.sidebar_group)
self.assertEqual(spec.breadcrumb_parent, PageName.TELEGRAM_PROXY)
self.assertNotIn(PageName.TELEGRAM_PROXY_ADVANCED, INNER_PAGE_NAMES)
# Вложенная страница закрывается раньше основной.
self.assertLess(
PAGE_CLEANUP_ORDER.index(PageName.TELEGRAM_PROXY_ADVANCED),
PAGE_CLEANUP_ORDER.index(PageName.TELEGRAM_PROXY),
)
def test_page_deps_open_each_other(self) -> None:
from ui.page_composition import PAGE_DEPS_BUILDERS
from ui.page_deps.system import (
build_telegram_proxy_advanced_page_kwargs,
build_telegram_proxy_page_kwargs,
)
self.assertIn(PageName.TELEGRAM_PROXY_ADVANCED, PAGE_DEPS_BUILDERS)
show_page = Mock()
feature = Mock()
advanced_kwargs = build_telegram_proxy_advanced_page_kwargs(
page_name=PageName.TELEGRAM_PROXY_ADVANCED,
telegram_proxy_feature=feature,
show_page=show_page,
)
main_kwargs = build_telegram_proxy_page_kwargs(
page_name=PageName.TELEGRAM_PROXY,
runtime_feature=Mock(),
telegram_proxy_feature=feature,
show_page=show_page,
)
self.assertIs(advanced_kwargs["telegram_proxy_feature"], feature)
advanced_kwargs["open_telegram_proxy"]()
# Строка пути «Telegram Proxy» — шаг «наверх»: назад, если пришли оттуда.
show_page.assert_called_once_with(PageName.TELEGRAM_PROXY, up=True)
show_page.reset_mock()
main_kwargs["open_advanced_settings"]()
show_page.assert_called_once_with(PageName.TELEGRAM_PROXY_ADVANCED, allow_internal=True)
def test_not_in_search_index(self) -> None:
from app.search_index import SEARCH_ENTRIES
self.assertFalse(any(entry.page_name == PageName.TELEGRAM_PROXY_ADVANCED for entry in SEARCH_ENTRIES))
class TelegramProxyAdvancedPageTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.app = QApplication.instance() or QApplication([])
def _page(self, feature=None, open_telegram_proxy=None):
from telegram_proxy.ui.advanced_page import TelegramProxyAdvancedPage
host = QWidget()
self.addCleanup(host.deleteLater)
return TelegramProxyAdvancedPage(
host,
telegram_proxy_feature=feature or _feature(),
open_telegram_proxy=open_telegram_proxy or (lambda: None),
)
def test_reload_waits_for_pending_saves(self) -> None:
feature = _feature(pending=True)
page = self._page(feature)
flushed_listener = feature.add_settings_flushed_listener.call_args.args[0]
page.on_page_activated()
feature.create_page_initial_state_worker.assert_not_called()
self.assertTrue(page._reload_after_flush)
feature.has_pending_settings_saves.return_value = False
flushed_listener("")
feature.create_page_initial_state_worker.assert_called_once()
self.assertFalse(page._reload_after_flush)
def test_result_arriving_while_saves_pending_is_dropped_and_rerequested(self) -> None:
feature = _feature()
page = self._page(feature)
page._apply_settings_state = Mock()
page._state_runtime.request_id = 5
feature.has_pending_settings_saves.return_value = True
page._on_state_loaded(5, Mock())
page._apply_settings_state.assert_not_called()
self.assertTrue(page._reload_after_flush)
def test_runtime_state_is_shown_in_server_row_description(self) -> None:
page = self._page()
page._apply_upstream_catalog(_catalog())
page._upstream_toggle.setChecked(True, block_signals=True)
page._apply_upstream_preset_ui(0)
page._on_upstream_state_changed(
UpstreamRuntimeSnapshot(
selected_preset_id="nl",
selected_name="Нидерланды",
active_preset_id="ee",
active_name="Эстония",
state="fallback",
generation=1,
)
)
self.assertEqual(
page._upstream_preset_row.contentLabel.text(),
"Выбрано: Нидерланды · Сейчас используется: Эстония (резерв)",
)
self.assertFalse(page._upstream_preset_row.isHidden())
def test_stale_runtime_state_is_not_shown(self) -> None:
feature = _feature(running=True)
page = self._page(feature)
page._apply_upstream_catalog(_catalog())
page._upstream_toggle.setChecked(True, block_signals=True)
page._apply_upstream_preset_ui(0)
page._on_upstream_state_changed(_fallback_snapshot())
default = "Выберите сервер из списка или переключитесь на ручной ввод"
# Пользователь выбрал другой сервер: старый «Сейчас используется» уже неверен.
page._upstream_preset_row.combo.setCurrentIndex(1)
self.assertEqual(page._upstream_preset_row.contentLabel.text(), default)
# Прокси остановлен: прошлый снимок не показываем.
page._on_upstream_state_changed(_fallback_snapshot())
feature.get_proxy_manager.return_value.is_running = False
page._refresh_upstream_preset_description()
self.assertEqual(page._upstream_preset_row.contentLabel.text(), default)
def test_missing_catalog_hint_is_server_row_description(self) -> None:
page = self._page()
page._apply_upstream_catalog(UpstreamCatalog())
page._upstream_toggle.setChecked(True, block_signals=True)
page._apply_upstream_preset_ui(0)
self.assertFalse(page._upstream_preset_row.isHidden())
self.assertIn("Доступен только ручной ввод", page._upstream_preset_row.contentLabel.text())
self.assertFalse(page._upstream_address_row.isHidden())
def test_edits_are_saved_through_facade(self) -> None:
feature = _feature()
page = self._page(feature)
page._pool_size_spin.setValue(6)
page._cloudflare_toggle._switch_button.setChecked(True) # как клик пользователя
feature.request_settings_save.assert_any_call("pool_size", value=6, restart="schedule")
feature.request_settings_save.assert_any_call("cloudflare_enabled", enabled=True, restart="now")
self.assertFalse(page._cloudflare_domains_row.isHidden())
def test_breadcrumb_root_returns_to_telegram_proxy(self) -> None:
open_telegram_proxy = Mock()
page = self._page(open_telegram_proxy=open_telegram_proxy)
page._breadcrumb.setCurrentItem("telegram_proxy")
open_telegram_proxy.assert_called_once_with()
self.assertEqual(len(page._breadcrumb.items), 2)
def test_cleanup_unsubscribes_without_stopping_proxy(self) -> None:
feature = _feature()
page = self._page(feature)
manager = feature.get_proxy_manager.return_value
link = manager.upstream_state_changed.connect.return_value
with patch("telegram_proxy.ui.advanced_page.disconnect_link") as disconnect_link:
page.cleanup()
feature.remove_settings_flushed_listener.assert_called_once_with(page._on_settings_flushed)
# Подписка снимается по квитанции connect(), а не по обработчику.
disconnect_link.assert_called_once_with(link)
manager.upstream_state_changed.disconnect.assert_not_called()
manager.cleanup.assert_not_called()
if __name__ == "__main__":
unittest.main()