zapret-kvn/tests/test_app_zapret_target_ui.py
loop-uh 672e41fdd1
Some checks failed
Windows project source guards / test (push) Failing after 28s
Zapret: макет с нуля — список ↔ карточка вместо выпадающих списков
- Пресет выбирается на странице пресетов: поиск, в строке — что пресет
  обходит (цели из --hostlist/--ipset/--hostlist-domains/--filter-l7 и
  «остальной трафик»), в карточке — цели, порты перехвата, профили и
  кнопки «Выбрать», «Изменить», «Удалить». Выпадающий список на 33
  пункта, не влезавший в окно, убран.
- Стратегия вида сервера: поиск, фильтр по метке с количеством, строки
  «название + описание» с галочкой у используемой; карточка с описанием,
  автором и аргументами по параметрам, кнопка «Использовать».
  Подсветка ничего не меняет, кнопка и переключатель обхода применяют
  сразу — отдельного «Применить» больше нет.
- На узком окне карточка уходит под список, ничего не обрезается.
- Сводка пресета считается при чтении списка (presets.summarize_arguments).
2026-09-28 21:28:46 +03:00

329 lines
15 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.

"""Zapret section: status card, selected server, per-kind pages, presets.
Keep the ``test_app_*`` prefix (QApplication before tests/test_engine_process_stop.py).
"""
from __future__ import annotations
import os
import unittest
from pathlib import Path
from unittest.mock import patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtWidgets import QApplication, QWidget
_existing = QApplication.instance()
if _existing is not None and not isinstance(_existing, QApplication):
raise RuntimeError("Zapret widget tests require QApplication")
app = _existing or QApplication([])
from xray_fluent.engines.zapret.command import ServerRule
from xray_fluent.engines.zapret.endpoint import ResolvedEndpoint, endpoint_for_node
from xray_fluent.engines.zapret.presets import PresetInfo
from xray_fluent.engines.zapret.strategies import CUSTOM_STRATEGY_ID, load_strategy_catalog
from xray_fluent.profiles.models import Node, ZapretTargetSettings
from xray_fluent.ui.zapret_browser import ID_ROLE
from xray_fluent.ui.zapret_page import ZapretPage, ZapretStatus
_page = ZapretPage()
_TCP = Node(
name="Tiraru", scheme="vless", server="tcp.example", port=443,
outbound={"protocol": "vless", "streamSettings": {"network": "tcp"}},
)
_H2 = Node(name="H2", scheme="hysteria2", server="udp.example", port=443, outbound={"protocol": "hysteria2"})
def _preset(name: str) -> PresetInfo:
return PresetInfo(name, "", "", "", 3, Path(f"{name}.txt"))
class ZapretOverviewTests(unittest.TestCase):
def setUp(self) -> None:
_page.show_root()
_page.set_target_settings(ZapretTargetSettings())
_page.set_presets([_preset("Default"), _preset("Other")], "Default")
_page.set_status(ZapretStatus("stopped"))
_page.set_selected_node(None)
_page.set_rule(None)
_page.set_server_status(False, "")
def test_status_card_follows_the_process(self) -> None:
_page.set_status(ZapretStatus("running", "Default"))
self.assertEqual(_page.status_title.text(), "Zapret работает")
self.assertEqual(_page.toggle_btn.text(), "Остановить")
_page.set_status(ZapretStatus("error", message="winws2 завершился с кодом 3"))
self.assertIn("ошибки", _page.status_title.text())
self.assertEqual(_page.status_detail.text(), "winws2 завершился с кодом 3")
self.assertEqual(_page.toggle_btn.text(), "Запустить")
def test_toggle_starts_the_chosen_preset_and_stops(self) -> None:
started: list[str] = []
stopped: list[bool] = []
_page.start_requested.connect(started.append)
_page.stop_requested.connect(lambda: stopped.append(True))
try:
_page.toggle_btn.click()
_page.set_status(ZapretStatus("running", "Default"))
_page.toggle_btn.click()
finally:
_page.start_requested.disconnect(started.append)
_page.stop_requested.disconnect()
self.assertEqual(started, ["Default"])
self.assertEqual(stopped, [True])
def test_choosing_a_preset_while_running_restarts_on_it(self) -> None:
started: list[str] = []
chosen: list[str] = []
_page.set_status(ZapretStatus("running", "Default"))
_page.preset_link.click()
self.assertIs(_page._stack.currentWidget(), _page._presets_page)
browser = _page._presets_page.browser
self.assertEqual(browser.highlighted(), "Default")
_page.start_requested.connect(started.append)
_page.preset_selected.connect(chosen.append)
try:
browser.highlight("Other")
self.assertEqual(chosen, []) # highlighting only shows the preset
browser.choose_btn.click()
finally:
_page.start_requested.disconnect(started.append)
_page.preset_selected.disconnect(chosen.append)
self.assertEqual((chosen, started), (["Other"], ["Other"]))
self.assertEqual(_page.preset_link.text(), "Other")
self.assertEqual(browser.choose_btn.text(), "Выбран")
_page.show_root()
def test_stopped_card_says_when_zapret_will_start_by_itself(self) -> None:
_page.set_selected_node(_TCP)
self.assertIn("Tiraru", _page.status_detail.text())
_page.set_target_settings(ZapretTargetSettings(tcp_proxy_enabled=False))
self.assertEqual(_page.status_detail.text(), "")
def test_selected_server_shows_endpoint_rule_and_live_ips(self) -> None:
_page.set_selected_node(_TCP)
self.assertIn("tcp.example", _page.server_endpoint.text())
self.assertIn("TCP 443", _page.server_endpoint.text())
self.assertIn("alt v9", _page.server_rule.text())
rule = ServerRule(
ResolvedEndpoint(endpoint_for_node(_TCP), ("203.0.113.5",)), load_strategy_catalog("tcp")["alt9"],
)
_page.set_rule(rule)
_page.set_status(ZapretStatus("running", "Default"))
self.assertIn("203.0.113.5", _page.server_rule.text())
_page.set_server_status(True, "DNS выбранного VPN-сервера...")
self.assertIn("DNS", _page.server_rule.text())
def test_off_meaning_is_spelled_out_and_never_says_pass(self) -> None:
_page.set_selected_node(_H2)
self.assertIn("не трогать", _page.server_rule.text())
self.assertNotIn("pass", _page.server_rule.text())
_page.set_target_settings(ZapretTargetSettings(tcp_proxy_enabled=False))
self.assertIn("по пресету", _page.kind_rows["tcp"].value.text())
self.assertIn("не трогать", _page.kind_rows["wireguard"].value.text())
def test_the_selected_server_kind_is_marked(self) -> None:
_page.set_selected_node(_H2)
self.assertFalse(_page.kind_rows["quic"].current_mark.isHidden())
self.assertTrue(_page.kind_rows["tcp"].current_mark.isHidden())
def test_help_link_opens_the_zapret_guide(self) -> None:
with patch("xray_fluent.ui.zapret_page.open_guide") as open_guide:
_page.help_link.click()
open_guide.assert_called_once()
self.assertEqual(open_guide.call_args.args[0], "zapret")
class KindPageTests(unittest.TestCase):
def setUp(self) -> None:
_page.show_root()
_page.set_target_settings(ZapretTargetSettings())
self.emitted: list[ZapretTargetSettings] = []
_page.target_settings_changed.connect(self.emitted.append)
def tearDown(self) -> None:
_page.target_settings_changed.disconnect(self.emitted.append)
_page.show_root()
def _open(self, kind: str):
_page.kind_rows[kind].clicked.emit()
return _page._kind_page
def test_row_opens_the_kind_page_on_the_strategy_in_use(self) -> None:
page = self._open("tcp")
self.assertIs(_page._stack.currentWidget(), page)
self.assertEqual(page.title_label.text(), "TCP-серверы")
self.assertEqual(page.browser.highlighted_id(), "alt9")
self.assertEqual(page.browser.use_btn.text(), "Используется")
self.assertFalse(page.browser.use_btn.isEnabled())
def test_switch_applies_at_once_and_shows_the_catalog(self) -> None:
page = self._open("quic")
self.assertTrue(page.browser.isHidden())
self.assertIn("не трогать", page.off_title.text())
page.switch.setChecked(True)
self.assertFalse(page.browser.isHidden())
self.assertEqual(len(self.emitted), 1)
self.assertTrue(self.emitted[0].quic_proxy_enabled)
def test_use_button_changes_only_that_kind(self) -> None:
page = self._open("wireguard")
page.switch.setChecked(True)
page.browser.highlight("fake_zero")
self.assertEqual(len(self.emitted), 1) # highlighting applies nothing
page.browser.use_btn.click()
latest = self.emitted[-1]
self.assertTrue(latest.wireguard_enabled)
self.assertEqual(latest.wireguard_strategy_id, "fake_zero")
self.assertEqual(latest.quic_strategy_id, "general_bf_32")
self.assertEqual(page.browser.chosen_id(), "fake_zero")
self.assertIn("Fake 0x00", _page.kind_rows["wireguard"].value.text())
def test_invalid_own_strategy_is_refused(self) -> None:
page = self._open("tcp")
page.browser.highlight(CUSTOM_STRATEGY_ID)
self.assertFalse(page.browser.custom_edit.isHidden())
page.browser.custom_edit.setPlainText("--new")
page.browser.use_btn.click()
self.assertEqual(self.emitted, [])
self.assertTrue(page.browser.error_label.text())
page.browser.custom_edit.setPlainText("--lua-desync=fake:repeats=2")
page.browser.use_btn.click()
self.assertEqual(self.emitted[-1].tcp_strategy_id, CUSTOM_STRATEGY_ID)
self.assertEqual(self.emitted[-1].tcp_custom_args, "--lua-desync=fake:repeats=2")
def test_filter_keeps_the_highlight(self) -> None:
browser = self._open("tcp").browser
browser.search.setText("zzz-no-such-strategy")
browser._rebuild()
self.assertEqual(browser.highlighted_id(), "alt9")
self.assertEqual(browser.list.count(), 0)
browser.search.setText("")
browser._rebuild()
self.assertEqual(browser.list.currentItem().data(ID_ROLE), "alt9")
def test_group_filter_lists_labels_with_counts(self) -> None:
browser = self._open("tcp").browser
titles = [browser.group.itemText(index) for index in range(browser.group.count())]
self.assertTrue(titles[0].startswith("Все · "))
self.assertTrue(any(title.startswith("Рекомендуется · ") for title in titles))
browser.group.setCurrentIndex(titles.index(next(t for t in titles if t.startswith("Осторожно"))))
shown = [browser.list.item(row).data(ID_ROLE) for row in range(browser.list.count())]
self.assertTrue(all(load_strategy_catalog("tcp")[i].label == "caution" for i in shown if i != CUSTOM_STRATEGY_ID))
browser.group.setCurrentIndex(0)
def test_arguments_are_split_into_readable_lines(self) -> None:
from xray_fluent.ui.zapret_browser import readable_arguments
self.assertEqual(
readable_arguments(("--payload=tls_client_hello", "--lua-desync=fake:blob=tls_google:repeats=6")),
"--payload=tls_client_hello\n--lua-desync=fake\n blob=tls_google\n repeats=6",
)
def test_card_moves_under_the_list_on_a_narrow_window(self) -> None:
from xray_fluent.ui.zapret_browser import SplitView
split = SplitView(QWidget(), QWidget())
split.resize(1000, 600)
split.resize(700, 600)
split.show()
app.processEvents()
self.assertTrue(split.stacked)
split.resize(1100, 600)
app.processEvents()
self.assertFalse(split.stacked)
split.deleteLater()
class PresetsPageTests(unittest.TestCase):
def test_running_preset_is_badged(self) -> None:
_page.set_presets([_preset("Default"), _preset("Other")], "Default")
_page.set_status(ZapretStatus("running", "Other"))
from PyQt6.QtCore import Qt
badge = _page._presets_page.browser.list.item(1).data(Qt.ItemDataRole.UserRole + 2)
self.assertEqual(badge, "Работает")
_page.set_status(ZapretStatus("stopped"))
def test_editor_returns_to_the_preset_list(self) -> None:
_page.show_sub_page(_page._presets_page)
_page._on_create()
self.assertIs(_page._stack.currentWidget(), _page._editor)
_page._editor.request_back()
self.assertIs(_page._stack.currentWidget(), _page._presets_page)
_page.show_root()
def test_rename_does_not_leave_a_copy_behind(self) -> None:
import tempfile
from xray_fluent.engines.zapret import presets
with tempfile.TemporaryDirectory() as tmp, patch.object(presets, "PRESETS_DIR", Path(tmp)):
presets.save_preset("Old", "--wf-tcp-out=443\n")
_page._on_save_preset("Old", "New", "", "--wf-tcp-out=443\n")
self.assertEqual(presets.list_presets(), ["New"])
presets.save_preset("Taken", "--wf-tcp-out=443\n")
_page._on_save_preset("New", "Taken", "", "--wf-tcp-out=443\n")
self.assertIn("уже есть", _page._editor.error_label.text())
_page.show_root()
def test_saving_the_running_preset_restarts_it(self) -> None:
import tempfile
from xray_fluent.engines.zapret import presets
started: list[str] = []
with tempfile.TemporaryDirectory() as tmp, patch.object(presets, "PRESETS_DIR", Path(tmp)):
presets.save_preset("Live", "--wf-tcp-out=443\n")
_page.set_presets(presets.list_preset_infos(), "Live")
_page.set_status(ZapretStatus("running", "Live"))
_page.start_requested.connect(started.append)
try:
_page._on_save_preset("Live", "Live 2", "", "--wf-tcp-out=80\n")
finally:
_page.start_requested.disconnect(started.append)
self.assertEqual(started, ["Live 2"])
_page.set_status(ZapretStatus("stopped"))
_page.show_root()
def test_lists_scroll_by_whole_rows(self) -> None:
from PyQt6.QtWidgets import QAbstractItemView
per_item = QAbstractItemView.ScrollMode.ScrollPerItem
self.assertEqual(_page._presets_page.browser.list.verticalScrollMode(), per_item)
self.assertEqual(_page._kind_page.browser.list.verticalScrollMode(), per_item)
def test_preset_summary_names_its_targets(self) -> None:
from xray_fluent.engines.zapret.presets import summarize_arguments
summary = summarize_arguments([
"--wf-tcp-out=80,443", "--filter-tcp=443", "--hostlist=lists/youtube.txt",
"--hostlist-domains=updates.discord.com", "--new", "--filter-udp=443",
"--ipset=lists/ipset-cloudflare1.txt", "--new", "--filter-tcp=80,443",
"--hostlist-exclude=lists/netrogat.txt",
])
self.assertEqual(summary.profiles, 3)
self.assertEqual(summary.targets, ("youtube", "discord", "cloudflare"))
self.assertTrue(summary.catch_all)
self.assertEqual(summary.tcp_ports, "80,443")
self.assertIn("остальной трафик", summary.short())
class SourceRulesTests(unittest.TestCase):
def test_page_does_not_force_translucent_or_opaque_styles(self) -> None:
source = (Path(__file__).resolve().parents[1] / "xray_fluent" / "ui" / "zapret_page.py").read_text(
encoding="utf-8"
)
self.assertNotIn("WA_TranslucentBackground", source)
self.assertNotIn("background-color:", source)
def test_overview_holds_only_data_and_the_guide_link(self) -> None:
from xray_fluent.ui.singbox.art import ArtCanvas
self.assertEqual(_page.findChildren(ArtCanvas), [])
if __name__ == "__main__":
unittest.main()