zapretgui/tests/test_profile_setup_ui_guards.py
2026-07-06 23:23:23 +03:00

2197 lines
87 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 unittest
class _TextWidget:
def __init__(self, text: str = "") -> None:
self._text = str(text)
self.calls: list[str] = []
def text(self) -> str:
return self._text
def setText(self, text: str) -> None: # noqa: N802
value = str(text)
self.calls.append(value)
self._text = value
class _Breadcrumb:
def __init__(self) -> None:
self.items: list[tuple[str, str]] = []
self.block_calls: list[bool] = []
self._accessible_name = ""
self._accessible_description = ""
self._properties = {}
def blockSignals(self, blocked: bool) -> None: # noqa: N802
self.block_calls.append(bool(blocked))
def clear(self) -> None:
self.items.clear()
def addItem(self, key: str, text: str) -> None: # noqa: N802
self.items.append((str(key), str(text)))
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): # noqa: A003
return self._properties.get(str(name))
def setProperty(self, name: str, value) -> None: # noqa: N802
self._properties[str(name)] = value
class _BoolWidget:
def __init__(self, *, checked: bool = False, enabled: bool = True, visible: bool = True) -> None:
self._checked = bool(checked)
self._enabled = bool(enabled)
self._visible = bool(visible)
self.checked_calls: list[bool] = []
self.enabled_calls: list[bool] = []
self.visible_calls: list[bool] = []
def isChecked(self) -> bool: # noqa: N802
return self._checked
def setChecked(self, checked: bool) -> None: # noqa: N802
value = bool(checked)
self.checked_calls.append(value)
self._checked = value
def isEnabled(self) -> bool: # noqa: N802
return self._enabled
def setEnabled(self, enabled: bool) -> None: # noqa: N802
value = bool(enabled)
self.enabled_calls.append(value)
self._enabled = value
def isVisible(self) -> bool: # noqa: N802
return self._visible
def setVisible(self, visible: bool) -> None: # noqa: N802
value = bool(visible)
self.visible_calls.append(value)
self._visible = value
class _PropertyWidget(_BoolWidget):
def __init__(
self,
*,
checked: bool = False,
enabled: bool = True,
visible: bool = True,
properties: dict[str, object] | None = None,
) -> None:
super().__init__(checked=checked, enabled=enabled, visible=visible)
self._properties = dict(properties or {})
self._text = ""
self._accessible_name = ""
self.text_calls: list[str] = []
self.property_calls: list[tuple[str, object]] = []
def text(self) -> str:
return self._text
def setText(self, text: str) -> None: # noqa: N802
value = str(text)
self.text_calls.append(value)
self._text = value
def accessibleName(self) -> str: # noqa: N802
return self._accessible_name
def setAccessibleName(self, text: str) -> None: # noqa: N802
self._accessible_name = str(text)
def property(self, name: str): # noqa: A003
return self._properties.get(str(name))
def setProperty(self, name: str, value) -> None: # noqa: N802
key = str(name)
self.property_calls.append((key, value))
self._properties[key] = value
class _LabelWidget(_PropertyWidget):
def __init__(self, text: str = "", *, visible: bool = True) -> None:
super().__init__(visible=visible)
self._text = str(text)
self.show_calls = 0
self.hide_calls = 0
self.clear_calls = 0
def show(self) -> None:
self.show_calls += 1
self._visible = True
def hide(self) -> None:
self.hide_calls += 1
self._visible = False
def clear(self) -> None:
self.clear_calls += 1
self._text = ""
class _PlainTextWidget(_BoolWidget):
def __init__(
self,
text: str = "",
*,
read_only: bool = False,
placeholder: str = "",
visible: bool = True,
) -> None:
super().__init__(visible=visible)
self._text = str(text)
self._read_only = bool(read_only)
self._placeholder = str(placeholder)
self.block_calls: list[bool] = []
self.plain_text_read_calls: list[str] = []
self.plain_text_calls: list[str] = []
self.read_only_calls: list[bool] = []
self.placeholder_calls: list[str] = []
def blockSignals(self, blocked: bool) -> None: # noqa: N802
self.block_calls.append(bool(blocked))
def toPlainText(self) -> str: # noqa: N802
self.plain_text_read_calls.append(self._text)
return self._text
def setPlainText(self, text: str) -> None: # noqa: N802
value = str(text)
self.plain_text_calls.append(value)
self._text = value
def isReadOnly(self) -> bool: # noqa: N802
return self._read_only
def setReadOnly(self, read_only: bool) -> None: # noqa: N802
value = bool(read_only)
self.read_only_calls.append(value)
self._read_only = value
def placeholderText(self) -> str: # noqa: N802
return self._placeholder
def setPlaceholderText(self, text: str) -> None: # noqa: N802
value = str(text)
self.placeholder_calls.append(value)
self._placeholder = value
class _StyleWidget:
def __init__(self, style: str = "") -> None:
self._style = str(style)
self.style_calls: list[str] = []
def styleSheet(self) -> str: # noqa: N802
return self._style
def setStyleSheet(self, style: str) -> None: # noqa: N802
value = str(style)
self.style_calls.append(value)
self._style = value
class _IndexWidget:
def __init__(self, index: int = 0) -> None:
self._index = int(index)
self.calls: list[int] = []
def currentIndex(self) -> int: # noqa: N802
return self._index
def setCurrentIndex(self, index: int) -> None: # noqa: N802
value = int(index)
self.calls.append(value)
self._index = value
class _TabTextWidget:
def __init__(self, texts: dict[str, str] | None = None) -> None:
self._texts = dict(texts or {})
self.calls: list[tuple[str, str]] = []
def itemText(self, key: str): # noqa: N802
return self._texts.get(str(key), "")
def setItemText(self, key: str, text: str) -> None: # noqa: N802
route_key = str(key)
value = str(text)
self.calls.append((route_key, value))
self._texts[route_key] = value
class _ListWidget:
def __init__(self) -> None:
self.clear_calls = 0
self.items = []
self.current_item = None
self.current_item_calls = []
self.update_calls = []
self.properties = {}
def clear(self) -> None:
self.clear_calls += 1
self.items.clear()
def addItem(self, item) -> None: # noqa: N802
self.items.append(item)
def count(self) -> int:
return len(self.items)
def item(self, row: int):
return self.items[row] if 0 <= row < len(self.items) else None
def currentItem(self): # noqa: N802
return self.current_item
def setCurrentItem(self, item) -> None: # noqa: N802
self.current_item_calls.append(item)
self.current_item = item
def viewport(self):
return self
def visualItemRect(self, item): # noqa: N802
return ("rect", item)
def update(self, rect) -> None:
self.update_calls.append(rect)
def setProperty(self, name, value) -> None: # noqa: N802
self.properties[str(name)] = value
def property(self, name):
return self.properties.get(str(name))
class _StrategyItem:
def __init__(self, *, data: dict[int, object] | None = None, selected: bool = False) -> None:
self._data = dict(data or {})
self._selected = bool(selected)
self.data_calls: list[tuple[int, object]] = []
self.selected_calls: list[bool] = []
def data(self, role: int):
return self._data.get(role)
def setData(self, role: int, value) -> None: # noqa: N802
self.data_calls.append((role, value))
self._data[role] = value
def isSelected(self) -> bool: # noqa: N802
return self._selected
def setSelected(self, selected: bool) -> None: # noqa: N802
value = bool(selected)
self.selected_calls.append(value)
self._selected = value
class _KeyEvent:
def __init__(self, key: int, event_type) -> None:
self._key = int(key)
self._event_type = event_type
self.accepted = False
def type(self): # noqa: A003
return self._event_type
def key(self) -> int:
return self._key
def accept(self) -> None:
self.accepted = True
class _Signal:
def connect(self, _callback) -> None:
pass
class _Timer:
def __init__(self) -> None:
self.start_calls: list[int | None] = []
self.stop_calls = 0
def start(self, interval: int | None = None) -> None:
self.start_calls.append(interval)
def stop(self) -> None:
self.stop_calls += 1
class _LoadWorker:
def __init__(self) -> None:
self.loaded = _Signal()
self.failed = _Signal()
self.finished = _Signal()
self.start_calls = 0
def start(self) -> None:
self.start_calls += 1
def deleteLater(self) -> None: # noqa: N802
pass
class _SaveWorker:
def __init__(self) -> None:
self.saved = _Signal()
self.failed = _Signal()
self.finished = _Signal()
self.start_calls = 0
def start(self) -> None:
self.start_calls += 1
def deleteLater(self) -> None: # noqa: N802
pass
class _ValidationWorker:
def __init__(self) -> None:
self.validated = _Signal()
self.failed = _Signal()
self.finished = _Signal()
self.start_calls = 0
def start(self) -> None:
self.start_calls += 1
def deleteLater(self) -> None: # noqa: N802
pass
class ProfileSetupUiGuardTests(unittest.TestCase):
def test_strategy_tabs_items_read_name_and_selection_for_screen_reader(self) -> None:
import os
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtWidgets import QApplication
from qfluentwidgets import SegmentedWidget
from profile.ui.profile_setup_page import ProfileSetupPageBase
type(self)._app = QApplication.instance() or QApplication([])
self.assertIsNotNone(type(self)._app)
tabs = SegmentedWidget()
self.addCleanup(tabs.deleteLater)
tabs.addItem("strategies", "Готовые стратегии", lambda: None)
tabs.addItem("editor", "Редактор", lambda: None)
tabs.addItem("match", "Когда применяется", lambda: None)
tabs.setCurrentItem("strategies")
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._strategy_tabs = tabs
page._editor_tab_available = True
page._payload = None
ProfileSetupPageBase._update_strategy_tabs_accessibility(page, "strategies")
self.assertEqual(
tabs.items["strategies"].accessibleName(),
"Разделы profile: Готовые стратегии, выбрано",
)
self.assertEqual(
tabs.items["editor"].accessibleName(),
"Разделы profile: Редактор, не выбрано",
)
tabs.setCurrentItem("match")
ProfileSetupPageBase._update_strategy_tabs_accessibility(page, "match")
self.assertEqual(
tabs.items["strategies"].accessibleName(),
"Разделы profile: Готовые стратегии, не выбрано",
)
self.assertEqual(
tabs.items["match"].accessibleName(),
"Разделы profile: Когда применяется, выбрано",
)
def test_text_update_skips_duplicate_value(self) -> None:
from profile.ui.profile_setup_page import set_widget_text_if_changed
widget = _TextWidget("Готово")
self.assertFalse(set_widget_text_if_changed(widget, "Готово"))
self.assertEqual(widget.calls, [])
self.assertTrue(set_widget_text_if_changed(widget, "Обновлено"))
self.assertEqual(widget.calls, ["Обновлено"])
def test_boolean_updates_skip_duplicate_values(self) -> None:
from profile.ui.profile_setup_page import (
set_widget_checked_if_changed,
set_widget_enabled_if_changed,
set_widget_visible_if_changed,
)
widget = _BoolWidget(checked=True, enabled=False, visible=True)
self.assertFalse(set_widget_checked_if_changed(widget, True))
self.assertFalse(set_widget_enabled_if_changed(widget, False))
self.assertFalse(set_widget_visible_if_changed(widget, True))
self.assertEqual(widget.checked_calls, [])
self.assertEqual(widget.enabled_calls, [])
self.assertEqual(widget.visible_calls, [])
self.assertTrue(set_widget_checked_if_changed(widget, False))
self.assertTrue(set_widget_enabled_if_changed(widget, True))
self.assertTrue(set_widget_visible_if_changed(widget, False))
self.assertEqual(widget.checked_calls, [False])
self.assertEqual(widget.enabled_calls, [True])
self.assertEqual(widget.visible_calls, [False])
def test_current_index_update_skips_duplicate_value(self) -> None:
from profile.ui.profile_setup_page import set_current_index_if_changed
widget = _IndexWidget(1)
self.assertFalse(set_current_index_if_changed(widget, 1))
self.assertEqual(widget.calls, [])
self.assertTrue(set_current_index_if_changed(widget, 0))
self.assertEqual(widget.calls, [0])
def test_tab_item_text_update_skips_duplicate_value(self) -> None:
from profile.ui.profile_setup_page import set_tab_item_text_if_changed
widget = _TabTextWidget({"editor": "Редактор"})
self.assertFalse(set_tab_item_text_if_changed(widget, "editor", "Редактор"))
self.assertEqual(widget.calls, [])
self.assertTrue(set_tab_item_text_if_changed(widget, "editor", "Hostlist"))
self.assertEqual(widget.calls, [("editor", "Hostlist")])
def test_property_update_skips_duplicate_value(self) -> None:
from profile.ui.profile_setup_page import set_widget_property_if_changed
widget = _PropertyWidget(properties={"selected": True})
self.assertFalse(set_widget_property_if_changed(widget, "selected", True))
self.assertEqual(widget.property_calls, [])
self.assertTrue(set_widget_property_if_changed(widget, "selected", False))
self.assertEqual(widget.property_calls, [("selected", False)])
def test_feedback_buttons_skip_duplicate_state(self) -> None:
from types import SimpleNamespace
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._work_button = _PropertyWidget(
enabled=True,
properties={
"selected": True,
"screenReaderStateText": "Отметить стратегию как рабочую. Оценка стратегии: выбрана.",
},
)
page._work_button._accessible_name = "Отметить стратегию как рабочую. Оценка стратегии: выбрана."
page._notwork_button = _PropertyWidget(
enabled=True,
properties={
"selected": False,
"screenReaderStateText": "Отметить стратегию как нерабочую. Оценка стратегии: не выбрана.",
},
)
page._notwork_button._accessible_name = "Отметить стратегию как нерабочую. Оценка стратегии: не выбрана."
page._favorite_button = _PropertyWidget(
enabled=True,
properties={
"screenReaderStateText": "Убрать стратегию из избранного. Избранное: включено.",
},
)
page._favorite_button._accessible_name = "Убрать стратегию из избранного. Избранное: включено."
page._favorite_button._text = "Убрать из избранного"
page._clear_feedback_button = _PropertyWidget(
enabled=True,
properties={
"screenReaderStateText": "Убрать оценку стратегии. Текущая оценка: работает.",
},
)
page._clear_feedback_button._accessible_name = "Убрать оценку стратегии. Текущая оценка: работает."
payload = SimpleNamespace(
item=SimpleNamespace(in_preset=True, enabled=True, strategy_id="tls_fake"),
current_strategy_state=SimpleNamespace(favorite=True, rating="work"),
)
ProfileSetupPageBase._apply_feedback_buttons(page, payload)
self.assertEqual(page._work_button.enabled_calls, [])
self.assertEqual(page._notwork_button.enabled_calls, [])
self.assertEqual(page._favorite_button.enabled_calls, [])
self.assertEqual(page._clear_feedback_button.enabled_calls, [])
self.assertEqual(page._work_button.property_calls, [])
self.assertEqual(page._notwork_button.property_calls, [])
self.assertEqual(page._clear_feedback_button.property_calls, [])
self.assertEqual(page._favorite_button.text_calls, [])
self.assertEqual(page._favorite_button._text, "Убрать из избранного")
def test_feedback_buttons_expose_selected_state_to_screen_reader(self) -> None:
from types import SimpleNamespace
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._work_button = _PropertyWidget(enabled=True, properties={"selected": False})
page._notwork_button = _PropertyWidget(enabled=True, properties={"selected": False})
page._favorite_button = _PropertyWidget(enabled=True)
page._clear_feedback_button = _PropertyWidget(enabled=True)
payload = SimpleNamespace(
item=SimpleNamespace(in_preset=True, enabled=True, strategy_id="tls_fake"),
current_strategy_state=SimpleNamespace(favorite=False, rating="work"),
)
ProfileSetupPageBase._apply_feedback_buttons(page, payload)
self.assertEqual(
page._work_button.accessibleName(),
"Отметить стратегию как рабочую. Оценка стратегии: выбрана.",
)
self.assertEqual(
page._work_button.property("screenReaderStateText"),
"Отметить стратегию как рабочую. Оценка стратегии: выбрана.",
)
self.assertEqual(
page._notwork_button.accessibleName(),
"Отметить стратегию как нерабочую. Оценка стратегии: не выбрана.",
)
self.assertEqual(
page._notwork_button.property("screenReaderStateText"),
"Отметить стратегию как нерабочую. Оценка стратегии: не выбрана.",
)
def test_favorite_button_exposes_state_to_screen_reader(self) -> None:
from types import SimpleNamespace
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._work_button = _PropertyWidget(enabled=True)
page._notwork_button = _PropertyWidget(enabled=True)
page._favorite_button = _PropertyWidget(enabled=True)
page._clear_feedback_button = _PropertyWidget(enabled=True)
payload = SimpleNamespace(
item=SimpleNamespace(in_preset=True, enabled=True, strategy_id="tls_fake"),
current_strategy_state=SimpleNamespace(favorite=False, rating=""),
)
ProfileSetupPageBase._apply_feedback_buttons(page, payload)
self.assertEqual(
page._favorite_button.accessibleName(),
"Добавить стратегию в избранное. Избранное: не включено.",
)
self.assertEqual(
page._favorite_button.property("screenReaderStateText"),
"Добавить стратегию в избранное. Избранное: не включено.",
)
def test_clear_feedback_button_exposes_current_rating_to_screen_reader(self) -> None:
from types import SimpleNamespace
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._work_button = _PropertyWidget(enabled=True)
page._notwork_button = _PropertyWidget(enabled=True)
page._favorite_button = _PropertyWidget(enabled=True)
page._clear_feedback_button = _PropertyWidget(enabled=True)
payload = SimpleNamespace(
item=SimpleNamespace(in_preset=True, enabled=True, strategy_id="tls_fake"),
current_strategy_state=SimpleNamespace(favorite=False, rating="work"),
)
ProfileSetupPageBase._apply_feedback_buttons(page, payload)
self.assertEqual(
page._clear_feedback_button.accessibleName(),
"Убрать оценку стратегии. Текущая оценка: работает.",
)
self.assertEqual(
page._clear_feedback_button.property("screenReaderStateText"),
"Убрать оценку стратегии. Текущая оценка: работает.",
)
def test_enabled_checkbox_exposes_state_text_to_screen_reader(self) -> None:
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._enabled_checkbox = _PropertyWidget(checked=True)
ProfileSetupPageBase._update_profile_setup_accessibility(page)
self.assertEqual(page._enabled_checkbox.accessibleName(), "Profile, включено")
self.assertEqual(
page._enabled_checkbox.property("screenReaderStateText"),
"Profile, включено",
)
def test_user_profile_buttons_skip_duplicate_enabled_state(self) -> None:
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._update_user_profile_button = _BoolWidget(enabled=False)
page._delete_user_profile_button = _BoolWidget(enabled=False)
ProfileSetupPageBase._set_user_profile_buttons_enabled(page, False)
self.assertEqual(page._update_user_profile_button.enabled_calls, [])
self.assertEqual(page._delete_user_profile_button.enabled_calls, [])
def test_profile_payload_request_skips_duplicate_loading_state(self) -> None:
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
from settings.mode import ZAPRET2_MODE
worker = _LoadWorker()
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._profile_key = "profile-1"
page._setup_load_request_id = 0
page._summary = _TextWidget("Загрузка profile...")
page._enabled_checkbox = _BoolWidget(enabled=False)
page._create_profile_setup_load_worker_fn = Mock(return_value=worker)
ProfileSetupPageBase._request_profile_setup_payload(page)
self.assertEqual(page._summary.calls, [])
self.assertEqual(page._enabled_checkbox.enabled_calls, [])
page._create_profile_setup_load_worker_fn.assert_called_once_with(
1,
ZAPRET2_MODE,
profile_key="profile-1",
parent=page,
)
self.assertEqual(worker.start_calls, 1)
def test_strategy_list_summary_skips_duplicate_text(self) -> None:
from types import SimpleNamespace
from profile.ui.profile_setup_page import ProfileStrategyListWidget
widget = ProfileStrategyListWidget.__new__(ProfileStrategyListWidget)
widget._search = _TextWidget("")
widget._list = _ListWidget()
widget._entries = {
"tls_fake": SimpleNamespace(
name="TLS fake",
args="--lua-desync=fake",
visual=SimpleNamespace(
label="",
description="",
icon_name="",
color="",
),
)
}
widget._states = {}
widget._current_strategy_id = "none"
widget._item_by_strategy_id = {}
widget._summary = _TextWidget("1 из 1")
ProfileStrategyListWidget._rebuild_tree(widget)
self.assertEqual(widget._summary.calls, [])
def test_strategy_list_rows_store_screen_reader_text(self) -> None:
from types import SimpleNamespace
from PyQt6.QtCore import Qt
from profile.strategy_state import ProfileStrategyState
from profile.ui.profile_setup_page import ProfileStrategyListWidget
widget = ProfileStrategyListWidget.__new__(ProfileStrategyListWidget)
widget._search = _TextWidget("")
widget._list = _ListWidget()
widget._entries = {
"tls_fake": SimpleNamespace(
name="TLS fake",
args="--lua-desync=fake",
visual=SimpleNamespace(
label="Fake",
description="Подмена TLS",
icon_name="",
color="",
),
)
}
widget._states = {"tls_fake": ProfileStrategyState(rating="work", favorite=True)}
widget._current_strategy_id = "tls_fake"
widget._item_by_strategy_id = {}
widget._summary = _TextWidget("")
ProfileStrategyListWidget._rebuild_tree(widget)
self.assertEqual(len(widget._list.items), 1)
accessible_text = widget._list.items[0].data(Qt.ItemDataRole.AccessibleTextRole)
self.assertEqual(
accessible_text,
"TLS fake, выбрана, в избранном, работает, Fake, Подмена TLS. "
"Нажмите Enter или Пробел, чтобы выбрать стратегию.",
)
def test_strategy_list_focuses_first_visible_row_when_current_strategy_is_hidden(self) -> None:
from types import SimpleNamespace
from profile.ui.profile_setup_page import ProfileStrategyListWidget
widget = ProfileStrategyListWidget.__new__(ProfileStrategyListWidget)
widget._search = _TextWidget("")
widget._list = _ListWidget()
widget._entries = {
"tls_fake": SimpleNamespace(
name="TLS fake",
args="--lua-desync=fake",
visual=SimpleNamespace(
label="Fake",
description="Подмена TLS",
icon_name="",
color="",
),
)
}
widget._states = {}
widget._current_strategy_id = "hidden_strategy"
widget._item_by_strategy_id = {}
widget._summary = _TextWidget("")
ProfileStrategyListWidget._rebuild_tree(widget)
self.assertEqual(widget._list.current_item, widget._list.items[0])
self.assertIn("TLS fake, не выбрана", widget._list.property("screenReaderStateText"))
def test_strategy_list_plan_focuses_first_visible_row_when_current_strategy_is_hidden(self) -> None:
from profile.strategy_list_filter import ProfileStrategyListPlan, ProfileStrategyListRow
from profile.ui.profile_setup_page import ProfileStrategyListWidget
widget = ProfileStrategyListWidget.__new__(ProfileStrategyListWidget)
widget._list = _ListWidget()
widget._item_by_strategy_id = {}
widget._summary = _TextWidget("")
plan = ProfileStrategyListPlan(
rows=(
ProfileStrategyListRow(
strategy_id="tls_fake",
name="TLS fake",
status_text="",
accessible_text="TLS fake, не выбрана. Нажмите Enter или Пробел, чтобы выбрать стратегию.",
is_current=False,
visual_icon_name="",
visual_color="",
visual_label="",
visual_description="",
tooltip_text="",
),
),
visible_count=1,
total_count=1,
current_strategy_id="hidden_strategy",
)
ProfileStrategyListWidget._apply_strategy_list_plan(widget, plan)
self.assertEqual(widget._list.current_item, widget._list.items[0])
self.assertIn("TLS fake, не выбрана", widget._list.property("screenReaderStateText"))
def test_strategy_list_keyboard_activation_uses_current_row(self) -> None:
from types import SimpleNamespace
from PyQt6.QtCore import QEvent, Qt
from profile.ui.profile_setup_page import ProfileStrategyListWidget
widget = ProfileStrategyListWidget.__new__(ProfileStrategyListWidget)
item = _StrategyItem(data={ProfileStrategyListWidget._ROLE_STRATEGY_ID: "tls_fake"})
widget._current_strategy_id = "none"
widget._list = _ListWidget()
widget._list.current_item = item
activated: list[str] = []
widget.strategy_activated = SimpleNamespace(emit=lambda strategy_id: activated.append(strategy_id))
event = _KeyEvent(int(Qt.Key.Key_Return), QEvent.Type.KeyPress)
handled = ProfileStrategyListWidget.eventFilter(widget, widget._list, event)
self.assertTrue(handled)
self.assertTrue(event.accepted)
self.assertEqual(activated, ["tls_fake"])
def test_profile_setup_breadcrumb_exposes_screen_reader_path(self) -> None:
from types import SimpleNamespace
from profile.ui.profile_setup_page import ProfileSetupPageBase
breadcrumb = _Breadcrumb()
page = SimpleNamespace(
_breadcrumb=breadcrumb,
control_key="missing.control.key",
profiles_key="missing.profiles.key",
profiles_default="Настройка пресета",
_ui_language="ru",
_payload=SimpleNamespace(item=SimpleNamespace(display_name="Discord")),
)
ProfileSetupPageBase._rebuild_breadcrumb(page)
self.assertEqual(
breadcrumb.property("screenReaderStateText"),
"Навигация: Управление > Настройка пресета > Discord",
)
self.assertEqual(
breadcrumb.accessibleDescription(),
"Показывает путь до текущей страницы. Выберите пункт, чтобы вернуться назад.",
)
def test_strategy_list_focus_in_selects_first_row_for_keyboard(self) -> None:
from PyQt6.QtCore import QEvent, Qt
from profile.ui.profile_setup_page import ProfileStrategyListWidget
widget = ProfileStrategyListWidget.__new__(ProfileStrategyListWidget)
item = _StrategyItem(
data={
ProfileStrategyListWidget._ROLE_STRATEGY_ID: "tls_fake",
Qt.ItemDataRole.AccessibleTextRole: (
"TLS fake, не выбрана. Нажмите Enter или Пробел, чтобы выбрать стратегию."
),
}
)
widget._list = _ListWidget()
widget._list.items.append(item)
event = _KeyEvent(0, QEvent.Type.FocusIn)
handled = ProfileStrategyListWidget.eventFilter(widget, widget._list, event)
self.assertFalse(handled)
self.assertIs(widget._list.current_item, item)
self.assertIn("TLS fake, не выбрана", widget._list.property("screenReaderStateText"))
def test_strategy_item_refresh_updates_screen_reader_text(self) -> None:
from types import SimpleNamespace
from PyQt6.QtCore import Qt
from profile.ui.profile_setup_page import ProfileStrategyListWidget
widget = ProfileStrategyListWidget.__new__(ProfileStrategyListWidget)
widget._states = {"tls_fake": SimpleNamespace(favorite=False, rating="")}
widget._list = _ListWidget()
item = _StrategyItem(
data={
ProfileStrategyListWidget._ROLE_NAME_TEXT: "TLS fake",
ProfileStrategyListWidget._ROLE_STATUS_TEXT: "",
ProfileStrategyListWidget._ROLE_IS_ACTIVE: False,
ProfileStrategyListWidget._ROLE_VISUAL_LABEL_TEXT: "Fake",
ProfileStrategyListWidget._ROLE_VISUAL_DESCRIPTION: "Подмена TLS",
Qt.ItemDataRole.AccessibleTextRole: (
"TLS fake, не выбрана, Fake, Подмена TLS. "
"Нажмите Enter или Пробел, чтобы выбрать стратегию."
),
},
selected=False,
)
ProfileStrategyListWidget._refresh_strategy_item(widget, item, "tls_fake", is_current=True)
self.assertIn(
(
Qt.ItemDataRole.AccessibleTextRole,
"TLS fake, выбрана, Fake, Подмена TLS. Нажмите Enter или Пробел, чтобы выбрать стратегию.",
),
item.data_calls,
)
def test_strategy_item_refresh_skips_duplicate_repaint(self) -> None:
from types import SimpleNamespace
from profile.ui.profile_setup_page import ProfileStrategyListWidget
widget = ProfileStrategyListWidget.__new__(ProfileStrategyListWidget)
widget._states = {"tls_fake": SimpleNamespace(favorite=True, rating="work")}
widget._list = _ListWidget()
item = _StrategyItem(
data={
ProfileStrategyListWidget._ROLE_STATUS_TEXT: "В избранном • Работает",
ProfileStrategyListWidget._ROLE_IS_ACTIVE: False,
},
selected=False,
)
ProfileStrategyListWidget._refresh_strategy_item(widget, item, "tls_fake", is_current=False)
self.assertEqual(item.data_calls, [])
self.assertEqual(item.selected_calls, [])
self.assertEqual(widget._list.current_item_calls, [])
self.assertEqual(widget._list.update_calls, [])
def test_list_file_request_skips_duplicate_loading_status(self) -> None:
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
worker = _LoadWorker()
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._editor_tab_built = True
page._profile_key = "profile-1"
page._list_file_load_worker = None
page._list_file_load_request_id = 0
page._list_file_status_label = _TextWidget("Загрузка файла списка...")
page._current_filter_kind = lambda: "hostlist"
page._current_filter_value = lambda: "lists/youtube.txt"
page.create_profile_list_file_load_worker = Mock(return_value=worker)
ProfileSetupPageBase._request_list_file_editor_state(page)
self.assertEqual(page._list_file_status_label.calls, [])
page.create_profile_list_file_load_worker.assert_called_once_with(
1,
"profile-1",
filter_kind="hostlist",
filter_value="lists/youtube.txt",
parent=page,
)
self.assertEqual(worker.start_calls, 1)
def test_list_file_editor_state_skips_duplicate_plain_text(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._list_file_title = None
page._list_file_base_title = None
page._list_file_base_text = _PlainTextWidget("base.example")
page._list_file_user_title = None
page._list_file_text = _PlainTextWidget("user.example", read_only=False)
page._list_file_base_text_snapshot = "base.example"
page._list_file_text_snapshot = "user.example"
page._list_file_text_dirty = False
page._list_file_save_button = _BoolWidget(enabled=True)
page._list_file_status_label = _TextWidget("Записей всего: 2 • ваших: 1")
page._render_list_file_validation = Mock()
state = SimpleNamespace(
kind="hostlist",
display_path="",
text="user.example",
base_text="base.example",
user_text="user.example",
base_entries_count=1,
user_entries_count=1,
base_display_path="",
user_display_path="",
editable=True,
error_text="",
invalid_lines=(),
)
ProfileSetupPageBase._apply_list_file_editor_state(page, state)
self.assertEqual(page._list_file_base_text.plain_text_calls, [])
self.assertEqual(page._list_file_base_text.plain_text_read_calls, [])
self.assertEqual(page._list_file_text.plain_text_calls, [])
self.assertEqual(page._list_file_text.plain_text_read_calls, [])
self.assertEqual(page._list_file_text.read_only_calls, [])
self.assertEqual(page._list_file_status_label.calls, [])
def test_list_file_editor_state_uses_worker_entry_counts(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock
import inspect
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._list_file_title = None
page._list_file_base_title = None
page._list_file_base_text = _PlainTextWidget("")
page._list_file_user_title = None
page._list_file_text = _PlainTextWidget("", read_only=False)
page._list_file_base_text_snapshot = ""
page._list_file_text_snapshot = ""
page._list_file_save_button = _BoolWidget(enabled=True)
page._list_file_status_label = _TextWidget("")
page._render_list_file_validation = Mock()
state = SimpleNamespace(
kind="hostlist",
display_path="",
text="visible.example\nignored-for-count.example\n",
base_text="base.example\nignored-for-count.example\n",
user_text="user.example\nignored-for-count.example\n",
base_entries_count=10,
user_entries_count=3,
base_display_path="",
user_display_path="",
editable=True,
error_text="",
invalid_lines=(),
)
ProfileSetupPageBase._apply_list_file_editor_state(page, state)
source = inspect.getsource(ProfileSetupPageBase._apply_list_file_editor_state)
self.assertNotIn("_list_file_entries_count", source)
self.assertEqual(page._list_file_base_entries_count, 10)
self.assertEqual(page._list_file_user_entries_count, 3)
self.assertEqual(page._list_file_status_label.text(), "Записей всего: 13 • ваших: 3")
def test_list_file_editor_state_apply_is_deferred_after_worker_signal(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock, patch
from profile.ui.profile_setup_page import ProfileSetupPageBase
state = SimpleNamespace(
kind="hostlist",
display_path="lists/youtube.txt",
text="example.com",
base_text="base.example",
user_text="user.example",
editable=True,
error_text="",
invalid_lines=(),
)
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._profile_key = "profile-1"
page._list_file_load_request_id = 9
page._cleanup_in_progress = False
page._list_file_dirty = True
page._current_filter_kind = Mock(return_value="hostlist")
page._current_filter_value = Mock(return_value="lists/youtube.txt")
page._apply_list_file_editor_state = Mock()
callbacks = []
from profile.profile_setup_loader import ProfileListFileLoadResult
with patch(
"profile.ui.profile_setup_page.QTimer.singleShot",
side_effect=lambda _delay, callback: callbacks.append(callback),
):
ProfileSetupPageBase._on_list_file_editor_state_loaded(
page,
9,
ProfileListFileLoadResult(
profile_key="profile-1",
filter_kind="hostlist",
filter_value="lists/youtube.txt",
file_name="youtube.txt",
state=state,
),
)
page._apply_list_file_editor_state.assert_not_called()
self.assertFalse(page._list_file_dirty)
self.assertEqual(len(callbacks), 1)
callbacks[0]()
page._apply_list_file_editor_state.assert_called_once_with(state)
def test_pending_list_file_state_apply_is_ignored_after_new_load_is_pending(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
state = SimpleNamespace(
kind="hostlist",
display_path="lists/old.txt",
text="old.example",
base_text="",
user_text="old.example",
editable=True,
error_text="",
invalid_lines=(),
)
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._pending_list_file_state_apply = state
page._list_file_state_apply_scheduled = True
page._cleanup_in_progress = False
page._pending_list_file_load = True
page._apply_list_file_editor_state = Mock(
side_effect=AssertionError("pending newer list file load must own the visible editor state")
)
ProfileSetupPageBase._run_scheduled_list_file_editor_state_apply(page)
page._apply_list_file_editor_state.assert_not_called()
self.assertTrue(page._pending_list_file_load)
def test_pending_list_file_load_ignores_old_load_error(self) -> None:
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._list_file_load_request_id = 9
page._pending_list_file_load = True
page._list_file_status_label = _TextWidget("Загружаем новый список...")
ProfileSetupPageBase._on_list_file_editor_state_failed(page, 9, "old error")
self.assertEqual(page._list_file_status_label.text(), "Загружаем новый список...")
self.assertEqual(page._list_file_status_label.calls, [])
def test_pending_profile_setup_payload_apply_is_ignored_after_new_load_is_pending(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
payload = SimpleNamespace(
item=SimpleNamespace(enabled=True),
match_summary="TCP 443",
)
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._pending_profile_setup_payload_apply = payload
page._profile_setup_payload_apply_scheduled = True
page._cleanup_in_progress = False
page._setup_load_dirty = True
page._apply_payload = Mock(
side_effect=AssertionError("pending newer profile setup load must own the visible payload")
)
ProfileSetupPageBase._run_scheduled_profile_setup_payload_apply(page)
page._apply_payload.assert_not_called()
self.assertTrue(page._setup_load_dirty)
def test_pending_profile_setup_load_ignores_old_load_error(self) -> None:
from unittest.mock import patch
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._setup_load_request_id = 7
page._setup_load_dirty = True
page._profile_key = "profile-1"
page._summary = _TextWidget("Загружаем новый профиль...")
page._enabled_checkbox = _BoolWidget(enabled=True)
with patch("profile.ui.profile_setup_page.log") as log_mock:
ProfileSetupPageBase._on_profile_setup_payload_failed(page, 7, "old error")
log_mock.assert_not_called()
self.assertEqual(page._summary.text(), "Загружаем новый профиль...")
self.assertEqual(page._summary.calls, [])
self.assertEqual(page._enabled_checkbox.enabled_calls, [])
def test_pending_list_file_load_restarts_after_worker_signal(self) -> None:
from unittest.mock import Mock, patch
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._pending_list_file_load = True
page._request_list_file_editor_state = Mock()
callbacks = []
with patch(
"profile.ui.profile_setup_page.QTimer.singleShot",
side_effect=lambda _delay, callback: callbacks.append(callback),
):
ProfileSetupPageBase._on_list_file_worker_finished(page, object())
page._request_list_file_editor_state.assert_not_called()
self.assertTrue(page._pending_list_file_load)
self.assertEqual(len(callbacks), 1)
callbacks[0]()
page._request_list_file_editor_state.assert_called_once_with()
self.assertFalse(page._pending_list_file_load)
def test_stale_list_file_load_worker_finished_does_not_restart_pending_load(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock, patch
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
current_worker = SimpleNamespace()
old_worker = SimpleNamespace()
page._list_file_load_runtime_worker = current_worker
page._pending_list_file_load = True
page._request_list_file_editor_state = Mock()
callbacks = []
with patch(
"profile.ui.profile_setup_page.QTimer.singleShot",
side_effect=lambda _delay, callback: callbacks.append(callback),
):
ProfileSetupPageBase._on_list_file_worker_finished(page, old_worker)
page._request_list_file_editor_state.assert_not_called()
self.assertTrue(page._pending_list_file_load)
self.assertEqual(callbacks, [])
self.assertIs(page._list_file_load_runtime_worker, current_worker)
def test_list_file_validation_skips_duplicate_status_updates(self) -> None:
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._list_file_validation_request_id = 7
page._pending_list_file_validation = None
page._list_file_kind = "hostlist"
page._list_file_text = _PlainTextWidget("user.example", read_only=False)
page._list_file_base_text = _PlainTextWidget("base.example")
page._list_file_save_button = _BoolWidget(enabled=True)
page._list_file_status_label = _TextWidget(
"Записей всего: 2 • ваших: 1 • есть несохранённые изменения"
)
page._list_file_text_snapshot = "user.example"
page._list_file_user_entries_count = 1
page._list_file_base_entries_count = 1
page._render_list_file_validation = Mock()
ProfileSetupPageBase._on_list_file_validation_finished(
page,
7,
"hostlist",
"user.example",
(),
)
self.assertEqual(page._list_file_save_button.enabled_calls, [])
self.assertEqual(page._list_file_status_label.calls, [])
page._render_list_file_validation.assert_called_once_with(())
def test_list_file_validation_result_uses_cached_text_and_counts(self) -> None:
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._list_file_validation_request_id = 7
page._pending_list_file_validation = None
page._list_file_kind = "hostlist"
page._list_file_text = _PlainTextWidget("user.example\nsecond.example", read_only=False)
page._list_file_base_text = _PlainTextWidget("base.example")
page._list_file_save_button = _BoolWidget(enabled=False)
page._list_file_status_label = _TextWidget("")
page._list_file_text_snapshot = "user.example\nsecond.example"
page._list_file_text_dirty = False
page._list_file_user_entries_count = 2
page._list_file_base_entries_count = 1
page._render_list_file_validation = Mock()
ProfileSetupPageBase._on_list_file_validation_finished(
page,
7,
"hostlist",
"user.example\nsecond.example",
(),
)
self.assertEqual(page._list_file_text.plain_text_read_calls, [])
self.assertEqual(page._list_file_base_text.plain_text_read_calls, [])
self.assertEqual(
page._list_file_status_label.text(),
"Записей всего: 3 • ваших: 2 • есть несохранённые изменения",
)
def test_list_file_status_updates_screen_reader_state(self) -> None:
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._list_file_validation_request_id = 7
page._pending_list_file_validation = None
page._list_file_kind = "hostlist"
page._list_file_text = _PlainTextWidget("user.example\nsecond.example", read_only=False)
page._list_file_save_button = _BoolWidget(enabled=False)
page._list_file_status_label = _PropertyWidget()
page._list_file_text_snapshot = "user.example\nsecond.example"
page._list_file_user_entries_count = 2
page._list_file_base_entries_count = 1
page._render_list_file_validation = Mock()
ProfileSetupPageBase._on_list_file_validation_finished(
page,
7,
"hostlist",
"user.example\nsecond.example",
(),
)
self.assertEqual(
page._list_file_status_label.accessibleName(),
"Статус списка profile: Записей всего: 3 • ваших: 2 • есть несохранённые изменения",
)
self.assertEqual(
page._list_file_status_label.property("screenReaderStateText"),
"Статус списка profile: Записей всего: 3 • ваших: 2 • есть несохранённые изменения",
)
def test_list_file_text_change_defers_large_editor_read_until_timer(self) -> None:
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._loading = False
page._list_file_kind = "hostlist"
page._list_file_text = _PlainTextWidget("user.example\nsecond.example")
page._list_file_status_label = _TextWidget("")
page._list_file_validation_timer = _Timer()
page._request_list_file_validation = Mock(
side_effect=AssertionError("validation must wait for the timer")
)
ProfileSetupPageBase._on_list_file_text_changed(page)
self.assertEqual(page._list_file_text.plain_text_read_calls, [])
self.assertEqual(page._list_file_validation_timer.start_calls, [180])
page._request_list_file_validation.assert_not_called()
def test_list_file_text_change_disables_save_until_worker_validation_finishes(self) -> None:
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._loading = False
page._list_file_text = _PlainTextWidget("user.example\nsecond.example")
page._list_file_status_label = _TextWidget("")
page._list_file_save_button = _BoolWidget(enabled=True)
page._list_file_validation_timer = _Timer()
ProfileSetupPageBase._on_list_file_text_changed(page)
self.assertEqual(page._list_file_text.plain_text_read_calls, [])
self.assertEqual(page._list_file_save_button.enabled_calls, [False])
def test_scheduled_list_file_validation_does_not_count_entries_in_gui(self) -> None:
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._loading = False
page._list_file_kind = "hostlist"
page._list_file_text = _PlainTextWidget("user.example\nsecond.example")
page._list_file_text_snapshot = ""
page._list_file_user_entries_count = 0
page._request_list_file_validation = Mock()
ProfileSetupPageBase._run_scheduled_list_file_validation(page)
self.assertEqual(page._list_file_text.plain_text_read_calls, [])
self.assertEqual(page._list_file_text_snapshot, "")
self.assertEqual(page._list_file_user_entries_count, 0)
page._request_list_file_validation.assert_called_once_with({
"kind": "hostlist",
"text": None,
})
def test_list_file_validation_uses_snapshot_when_editor_is_unchanged(self) -> None:
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._list_file_text = _PlainTextWidget("user.example\nsecond.example")
page._list_file_text_snapshot = "user.example\nsecond.example"
page._list_file_text_dirty = False
request = ProfileSetupPageBase._resolve_list_file_validation_request(
page,
{"kind": "hostlist", "text": None},
)
self.assertEqual(request, {
"kind": "hostlist",
"text": "user.example\nsecond.example",
})
self.assertEqual(page._list_file_text.plain_text_read_calls, [])
def test_list_file_validation_after_text_change_reads_editor_once(self) -> None:
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
# Документ — единственный источник правды: после правки (dirty) запуск
# валидации перечитывает текст из редактора целиком и ровно один раз.
# Инкрементального кэша по contentsChange больше нет — он терял
# вставленный из буфера текст (Qt учитывает финальный разделитель
# блока в charsAdded) и валидация ругалась на фантомные строки.
worker = _ValidationWorker()
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._loading = False
page._list_file_kind = "hostlist"
page._list_file_text = _PlainTextWidget("new.example\nsecond.example")
page._list_file_text_snapshot = "old.example"
page._list_file_text_dirty = True
page._list_file_validation_request_id = 0
page.create_profile_list_file_validation_worker = Mock(return_value=worker)
ProfileSetupPageBase._start_list_file_validation_worker(page, {
"kind": "hostlist",
"text": None,
})
self.assertEqual(
page._list_file_text.plain_text_read_calls,
["new.example\nsecond.example"],
)
page.create_profile_list_file_validation_worker.assert_called_once_with(
1,
kind="hostlist",
text="new.example\nsecond.example",
parent=page,
)
self.assertEqual(worker.start_calls, 1)
# Мемо обновлено — повторное обращение без правок не читает редактор.
self.assertEqual(
ProfileSetupPageBase._current_list_file_text(page),
"new.example\nsecond.example",
)
self.assertEqual(
page._list_file_text.plain_text_read_calls,
["new.example\nsecond.example"],
)
def test_list_file_validation_result_updates_entries_count_from_worker(self) -> None:
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._list_file_validation_request_id = 7
page._pending_list_file_validation = None
page._list_file_kind = "hostlist"
page._list_file_text = _PlainTextWidget("user.example\nsecond.example", read_only=False)
page._list_file_save_button = _BoolWidget(enabled=False)
page._list_file_status_label = _TextWidget("")
page._list_file_text_snapshot = "user.example\nsecond.example"
page._list_file_user_entries_count = 0
page._list_file_base_entries_count = 1
page._render_list_file_validation = Mock()
ProfileSetupPageBase._on_list_file_validation_finished(
page,
7,
"hostlist",
"user.example\nsecond.example",
{"invalid_lines": (), "entries_count": 2},
)
self.assertEqual(page._list_file_user_entries_count, 2)
self.assertEqual(
page._list_file_status_label.text(),
"Записей всего: 3 • ваших: 2 • есть несохранённые изменения",
)
def test_list_file_validation_error_is_ignored_when_new_validation_is_pending(self) -> None:
from unittest.mock import Mock, patch
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._list_file_validation_request_id = 7
page._pending_list_file_validation = {"kind": "hostlist", "text": "new.example"}
page._render_list_file_validation = Mock()
page._list_file_save_button = _BoolWidget(enabled=True)
page._list_file_status_label = _TextWidget("Проверка списка...")
with patch("profile.ui.profile_setup_page.log") as log_mock:
ProfileSetupPageBase._on_list_file_validation_failed(page, 7, "old error")
log_mock.assert_not_called()
page._render_list_file_validation.assert_not_called()
self.assertEqual(page._list_file_save_button.enabled_calls, [])
self.assertEqual(page._list_file_status_label.calls, [])
def test_pending_list_file_validation_restarts_after_worker_signal(self) -> None:
from unittest.mock import Mock, patch
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
pending = {"kind": "hostlist", "text": "next.example"}
page._pending_list_file_validation = pending
page._start_list_file_validation_worker = Mock()
callbacks = []
with patch(
"profile.ui.profile_setup_page.QTimer.singleShot",
side_effect=lambda _delay, callback: callbacks.append(callback),
):
ProfileSetupPageBase._on_list_file_validation_worker_finished(page, object())
page._start_list_file_validation_worker.assert_not_called()
self.assertEqual(page._pending_list_file_validation, pending)
self.assertEqual(len(callbacks), 1)
callbacks[0]()
page._start_list_file_validation_worker.assert_called_once_with(pending)
self.assertIsNone(page._pending_list_file_validation)
def test_stale_list_file_validation_worker_finished_does_not_restart_pending_validation(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock, patch
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
current_worker = SimpleNamespace()
old_worker = SimpleNamespace()
pending = {"kind": "hostlist", "text": "next.example"}
page._list_file_validation_runtime_worker = current_worker
page._pending_list_file_validation = pending
page._start_list_file_validation_worker = Mock()
callbacks = []
with patch(
"profile.ui.profile_setup_page.QTimer.singleShot",
side_effect=lambda _delay, callback: callbacks.append(callback),
):
ProfileSetupPageBase._on_list_file_validation_worker_finished(page, old_worker)
page._start_list_file_validation_worker.assert_not_called()
self.assertEqual(page._pending_list_file_validation, pending)
self.assertEqual(callbacks, [])
self.assertIs(page._list_file_validation_runtime_worker, current_worker)
def test_list_file_save_uses_cached_text_snapshot(self) -> None:
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
worker = _SaveWorker()
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._loading = False
page._profile_key = "profile-1"
page._list_file_text = _PlainTextWidget("user.example\nsecond.example")
page._list_file_text_snapshot = "user.example\nsecond.example"
page._list_file_text_dirty = False
page._list_file_save_worker = None
page._list_file_save_request_id = 0
page._list_file_status_label = _TextWidget("")
page._list_file_save_button = _BoolWidget(enabled=False)
page._current_filter_kind = Mock(return_value="hostlist")
page._current_filter_value = Mock(return_value="lists/youtube.txt")
page.create_profile_list_file_save_worker = Mock(return_value=worker)
ProfileSetupPageBase._on_list_file_save_clicked(page)
self.assertEqual(page._list_file_text.plain_text_read_calls, [])
page.create_profile_list_file_save_worker.assert_called_once_with(
1,
"profile-1",
"user.example\nsecond.example",
filter_kind="hostlist",
filter_value="lists/youtube.txt",
parent=page,
)
self.assertEqual(worker.start_calls, 1)
def test_pending_list_file_save_restarts_after_worker_signal(self) -> None:
from unittest.mock import Mock, patch
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
pending = ("profile-1", "latest.example")
page._pending_list_file_save = pending
page._start_next_profile_setup_write_operation = Mock(return_value=False)
page._start_list_file_save_worker = Mock()
callbacks = []
with patch(
"profile.ui.profile_setup_page.QTimer.singleShot",
side_effect=lambda _delay, callback: callbacks.append(callback),
):
ProfileSetupPageBase._on_list_file_save_worker_finished(page, object())
page._start_list_file_save_worker.assert_not_called()
self.assertEqual(page._list_file_save_state_obj().pending, pending)
self.assertTrue(page._list_file_save_state_obj().start_scheduled)
self.assertEqual(len(callbacks), 1)
callbacks[0]()
page._start_list_file_save_worker.assert_called_once_with(
"profile-1",
"latest.example",
filter_kind="",
filter_value="",
)
def test_list_file_save_waits_while_scheduled_save_start_is_pending(self) -> None:
from unittest.mock import Mock, patch
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
first_pending = ("profile-1", "first.example")
page._pending_list_file_save = first_pending
page._pending_profile_setup_write_operations = []
page._start_next_profile_setup_write_operation = Mock(return_value=False)
page._start_list_file_save_worker = Mock()
callbacks = []
with patch(
"profile.ui.profile_setup_page.QTimer.singleShot",
side_effect=lambda _delay, callback: callbacks.append(callback),
):
ProfileSetupPageBase._on_list_file_save_worker_finished(page, object())
ProfileSetupPageBase._request_list_file_save(page, "profile-1", "second.example")
page._start_list_file_save_worker.assert_not_called()
self.assertEqual(page._list_file_save_state_obj().pending, ("profile-1", "first.example"))
self.assertEqual(
page._pending_profile_setup_write_operations,
[
{
"kind": "list_file_save",
"profile_key": "profile-1",
"text": "second.example",
"filter_kind": "",
"filter_value": "",
}
],
)
self.assertEqual(len(callbacks), 1)
callbacks[0]()
page._start_list_file_save_worker.assert_called_once_with(
"profile-1",
"first.example",
filter_kind="",
filter_value="",
)
self.assertEqual(
page._pending_profile_setup_write_operations,
[
{
"kind": "list_file_save",
"profile_key": "profile-1",
"text": "second.example",
"filter_kind": "",
"filter_value": "",
}
],
)
def test_list_file_save_error_ignored_when_new_save_is_pending(self) -> None:
from unittest.mock import Mock, patch
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._list_file_save_request_id = 5
page._pending_list_file_save = ("profile-1", "latest.example")
page._list_file_save_button = _BoolWidget(enabled=False)
page._render_list_file_validation = Mock()
page.window = Mock(return_value=object())
with patch("profile.ui.profile_setup_page.InfoBar.error") as error_mock:
ProfileSetupPageBase._on_list_file_save_failed(page, 5, "old error")
page._render_list_file_validation.assert_not_called()
self.assertEqual(page._list_file_save_button.enabled_calls, [])
error_mock.assert_not_called()
def test_list_file_validation_label_skips_duplicate_error_render(self) -> None:
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._list_file_text = _PlainTextWidget("bad value")
page._refresh_list_file_editor_style = Mock()
page._list_file_error_label = _LabelWidget("Неверные строки:\nСтрока 1: bad value", visible=True)
ProfileSetupPageBase._render_list_file_validation(page, ((1, "bad value"),))
self.assertEqual(page._list_file_error_label.text_calls, [])
self.assertEqual(page._list_file_error_label.visible_calls, [])
self.assertEqual(page._list_file_error_label.show_calls, 0)
page._refresh_list_file_editor_style.assert_called_once_with(has_error=True)
def test_list_file_error_label_exposes_screen_reader_state(self) -> None:
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._list_file_text = _PlainTextWidget("bad value")
page._refresh_list_file_editor_style = Mock()
page._list_file_error_label = _LabelWidget("", visible=False)
ProfileSetupPageBase._render_list_file_validation(page, ((1, "bad value"),))
expected = "Ошибка списка profile: Неверные строки: Строка 1: bad value"
self.assertEqual(page._list_file_error_label.accessibleName(), expected)
self.assertEqual(page._list_file_error_label.property("screenReaderStateText"), expected)
def test_list_file_editor_style_skips_duplicate_stylesheet_updates(self) -> None:
from types import SimpleNamespace
from unittest.mock import patch
from profile.ui.profile_setup_page import ProfileSetupPageBase
tokens = SimpleNamespace(
surface_bg="#111111",
surface_border="#222222",
surface_bg_hover="#333333",
surface_border_hover="#444444",
accent_hex="#55aaff",
fg="#eeeeee",
fg_faint="#999999",
)
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._list_file_base_text = _StyleWidget()
page._list_file_text = _StyleWidget()
page._list_file_error_label = _StyleWidget()
page._list_file_status_label = _StyleWidget()
with patch("profile.ui.profile_setup_page.get_theme_tokens", return_value=tokens):
ProfileSetupPageBase._refresh_list_file_editor_style(page, has_error=True)
page._list_file_base_text.style_calls.clear()
page._list_file_text.style_calls.clear()
page._list_file_error_label.style_calls.clear()
page._list_file_status_label.style_calls.clear()
ProfileSetupPageBase._refresh_list_file_editor_style(page, has_error=True)
self.assertEqual(page._list_file_base_text.style_calls, [])
self.assertEqual(page._list_file_text.style_calls, [])
self.assertEqual(page._list_file_error_label.style_calls, [])
self.assertEqual(page._list_file_status_label.style_calls, [])
def test_match_tab_payload_skips_duplicate_plain_text(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
match_text = "prepared match text"
payload = SimpleNamespace(
item=SimpleNamespace(
in_preset=True,
strategy_id="tls_fake",
strategy_name="Fake TLS",
),
strategy_entries={
"tls_fake": SimpleNamespace(args="--lua-desync=fake"),
},
match_summary="hostlist: youtube.txt",
raw_profile_text="--new\n--lua-desync=fake",
match_tab_text=match_text,
)
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._payload = payload
page._match_tab_built = True
page._match_text = _PlainTextWidget(match_text)
page._match_text_snapshot = match_text
page._raw_profile_text = _PlainTextWidget(payload.raw_profile_text, read_only=False)
page._raw_profile_text_cache = payload.raw_profile_text
page._raw_profile_save_button = _BoolWidget(enabled=True)
page._apply_feedback_buttons = Mock()
ProfileSetupPageBase._apply_match_tab_payload(page)
self.assertEqual(page._match_text.plain_text_calls, [])
self.assertEqual(page._match_text.plain_text_read_calls, [])
self.assertEqual(page._raw_profile_text.plain_text_read_calls, [])
self.assertEqual(page._raw_profile_text.plain_text_calls, [])
self.assertEqual(page._raw_profile_text.read_only_calls, [])
self.assertEqual(page._raw_profile_save_button.enabled_calls, [])
page._apply_feedback_buttons.assert_called_once_with(payload)
def test_match_tab_payload_uses_cached_raw_profile_text(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
raw_text = "--new\n--lua-desync=fake"
payload = SimpleNamespace(
item=SimpleNamespace(
in_preset=True,
strategy_id="tls_fake",
strategy_name="Fake TLS",
),
strategy_entries={},
match_summary="hostlist: youtube.txt",
raw_profile_text=raw_text,
match_tab_text="prepared match text",
)
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._payload = payload
page._match_tab_built = True
page._match_text = None
page._raw_profile_text = _PlainTextWidget(raw_text, read_only=False)
page._raw_profile_text_cache = raw_text
page._raw_profile_save_button = _BoolWidget(enabled=True)
page._apply_feedback_buttons = Mock()
ProfileSetupPageBase._apply_match_tab_payload(page)
self.assertEqual(page._raw_profile_text.plain_text_read_calls, [])
self.assertEqual(page._raw_profile_text.plain_text_calls, [])
page._apply_feedback_buttons.assert_called_once_with(payload)
def test_match_tab_payload_sets_match_text_without_reading_editor(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
match_text = "prepared match text"
payload = SimpleNamespace(
item=SimpleNamespace(in_preset=True, strategy_id="tls_fake", strategy_name="Fake TLS"),
strategy_entries={"tls_fake": SimpleNamespace(args="--lua-desync=fake")},
match_summary="hostlist: youtube.txt",
raw_profile_text="--new\n--lua-desync=fake",
match_tab_text=match_text,
)
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._payload = payload
page._match_tab_built = True
page._match_text = _PlainTextWidget("old match text")
page._match_text_snapshot = "old match text"
page._raw_profile_text = None
page._raw_profile_save_button = None
page._apply_feedback_buttons = Mock()
ProfileSetupPageBase._apply_match_tab_payload(page)
self.assertEqual(page._match_text.plain_text_read_calls, [])
self.assertEqual(page._match_text.plain_text_calls, [match_text])
self.assertEqual(page._match_text_snapshot, match_text)
def test_match_tab_payload_sets_raw_profile_text_without_reading_editor(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
raw_text = "--new\n--lua-desync=fake"
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._payload = SimpleNamespace(
item=SimpleNamespace(in_preset=True, strategy_id="tls_fake", strategy_name="Fake TLS"),
strategy_entries={},
match_summary="hostlist: youtube.txt",
raw_profile_text=raw_text,
)
page._match_tab_built = True
page._match_text = None
page._raw_profile_text = _PlainTextWidget("--old\n", read_only=False)
page._raw_profile_text_cache = "--old\n"
page._raw_profile_save_button = _BoolWidget(enabled=True)
page._apply_feedback_buttons = Mock()
ProfileSetupPageBase._apply_match_tab_payload(page)
self.assertEqual(page._raw_profile_text.plain_text_read_calls, [])
self.assertEqual(page._raw_profile_text.plain_text_calls, [raw_text])
self.assertEqual(page._raw_profile_text_cache, raw_text)
def test_raw_profile_save_uses_cached_payload_text_when_unchanged(self) -> None:
from profile.ui.profile_setup_page import ProfileSetupPageBase
raw_text = "--new\n--lua-desync=fake"
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._raw_profile_text = _PlainTextWidget(raw_text)
page._raw_profile_text_cache = raw_text
text = ProfileSetupPageBase._resolve_raw_profile_save_text(page, None)
self.assertEqual(text, raw_text)
self.assertEqual(page._raw_profile_text.plain_text_read_calls, [])
def test_raw_profile_save_after_text_change_reads_editor_once(self) -> None:
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
# После правки (textChanged инвалидирует мемо) сохранение перечитывает
# живой текст из редактора ровно один раз — инкрементального кэша по
# contentsChange больше нет, он молча терял вставленный текст.
raw_text = "--new\n--lua-desync=fake"
worker = _SaveWorker()
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._loading = False
page._profile_key = "profile-1"
page._raw_profile_text = _PlainTextWidget(raw_text)
page._raw_profile_text_cache = "--old\n"
page._raw_profile_save_request_id = 0
page._raw_profile_save_button = None
page.create_profile_raw_text_save_worker = Mock(return_value=worker)
ProfileSetupPageBase._on_raw_profile_text_changed(page)
ProfileSetupPageBase._on_raw_profile_save_clicked(page)
self.assertEqual(page._raw_profile_text.plain_text_read_calls, [raw_text])
page.create_profile_raw_text_save_worker.assert_called_once_with(
1,
"profile-1",
raw_text,
parent=page,
)
self.assertEqual(worker.start_calls, 1)
self.assertEqual(page._raw_profile_text_cache, raw_text)
def test_raw_profile_save_skips_duplicate_button_disable(self) -> None:
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
worker = _SaveWorker()
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._loading = False
page._profile_key = "profile-1"
page._raw_profile_text = _PlainTextWidget("--new\n")
page._raw_profile_text_cache = "--new\n"
page._raw_profile_save_request_id = 0
page._raw_profile_save_button = _BoolWidget(enabled=False)
page.create_profile_raw_text_save_worker = Mock(return_value=worker)
ProfileSetupPageBase._on_raw_profile_save_clicked(page)
self.assertEqual(page._raw_profile_save_button.enabled_calls, [])
page.create_profile_raw_text_save_worker.assert_called_once_with(
1,
"profile-1",
"--new\n",
parent=page,
)
self.assertEqual(worker.start_calls, 1)
def test_enabled_change_skips_duplicate_checkbox_disable(self) -> None:
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
worker = _SaveWorker()
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._loading = False
page._profile_key = "profile-1"
page._enabled_save_request_id = 0
page._enabled_checkbox = _BoolWidget(checked=True, enabled=False)
page._current_filter_kind = lambda: "hostlist"
page._current_filter_value = lambda: "lists/youtube.txt"
page.create_profile_enabled_save_worker = Mock(return_value=worker)
ProfileSetupPageBase._on_enabled_changed(page, 2)
self.assertEqual(page._enabled_checkbox.enabled_calls, [])
page.create_profile_enabled_save_worker.assert_called_once_with(
1,
profile_key="profile-1",
enabled=True,
filter_kind="hostlist",
filter_value="lists/youtube.txt",
parent=page,
)
self.assertEqual(worker.start_calls, 1)
def test_enabled_change_skips_same_payload_state(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._loading = False
page._profile_key = "profile-1"
page._payload = SimpleNamespace(item=SimpleNamespace(enabled=True))
page._enabled_save_request_id = 0
page._enabled_checkbox = _BoolWidget(checked=True, enabled=True)
page._current_filter_kind = lambda: "hostlist"
page._current_filter_value = lambda: "lists/youtube.txt"
page.create_profile_enabled_save_worker = Mock(
side_effect=AssertionError("same enabled state must not start worker")
)
ProfileSetupPageBase._on_enabled_changed(page, 2)
page.create_profile_enabled_save_worker.assert_not_called()
self.assertEqual(page._enabled_checkbox.enabled_calls, [])
def test_enabled_save_failure_skips_duplicate_checkbox_enable(self) -> None:
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._enabled_save_request_id = 4
page._enabled_checkbox = _BoolWidget(enabled=True)
ProfileSetupPageBase._on_enabled_save_failed(page, 4, "boom")
self.assertEqual(page._enabled_checkbox.enabled_calls, [])
def test_enabled_save_error_ignored_when_new_toggle_is_pending(self) -> None:
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._enabled_save_request_id = 5
page._pending_enabled_save = False
page._enabled_checkbox = _BoolWidget(enabled=False)
ProfileSetupPageBase._on_enabled_save_failed(page, 5, "old error")
self.assertEqual(page._enabled_checkbox.enabled_calls, [])
def test_raw_profile_save_error_ignored_when_new_save_is_pending(self) -> None:
from unittest.mock import Mock, patch
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._raw_profile_save_request_id = 6
page._pending_raw_profile_save = ("profile-1", "--new\nlatest")
page._raw_profile_save_button = _BoolWidget(enabled=False)
page.window = Mock(return_value=object())
with patch("profile.ui.profile_setup_page.InfoBar.error") as error_mock:
ProfileSetupPageBase._on_raw_profile_save_failed(page, 6, "old error")
self.assertEqual(page._raw_profile_save_button.enabled_calls, [])
error_mock.assert_not_called()
def test_apply_enabled_locally_skips_duplicate_checkbox_state(self) -> None:
from dataclasses import dataclass, replace
from profile.ui.profile_setup_page import ProfileSetupPageBase
@dataclass(frozen=True)
class _Item:
enabled: bool
@dataclass(frozen=True)
class _Payload:
item: _Item
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._payload = _Payload(item=_Item(enabled=True))
page._enabled_checkbox = _BoolWidget(checked=True, enabled=True)
page._loading = False
updated_item = ProfileSetupPageBase._apply_enabled_locally(page, True)
self.assertEqual(updated_item, replace(page._payload.item, enabled=True))
self.assertEqual(page._enabled_checkbox.checked_calls, [])
self.assertEqual(page._enabled_checkbox.enabled_calls, [])
self.assertFalse(page._loading)
def test_strategy_apply_error_ignored_when_new_strategy_is_pending(self) -> None:
from unittest.mock import Mock, patch
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._strategy_apply_request_id = 8
page._pending_strategy_apply = "new-strategy"
page.reload_current_profile = Mock()
with patch("profile.ui.profile_setup_page.log") as log_mock:
ProfileSetupPageBase._on_strategy_apply_failed(page, 8, "old error")
log_mock.assert_not_called()
page.reload_current_profile.assert_not_called()
def test_strategy_feedback_error_ignored_when_new_feedback_is_pending(self) -> None:
from unittest.mock import Mock, patch
from profile.ui.profile_setup_page import ProfileSetupPageBase
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page._strategy_feedback_save_request_id = 9
page._pending_strategy_feedback_save = {"rating": "work", "favorite": None}
page.reload_current_profile = Mock()
with patch("profile.ui.profile_setup_page.log") as log_mock:
ProfileSetupPageBase._on_strategy_feedback_save_failed(page, 9, "old error")
log_mock.assert_not_called()
page.reload_current_profile.assert_not_called()
def test_editable_settings_skip_duplicate_text_and_visibility(self) -> None:
from types import SimpleNamespace
from unittest.mock import Mock, patch
from profile.ui.profile_setup_page import ProfileSetupPageBase
from settings.mode import ZAPRET2_MODE
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
page.launch_method = ZAPRET2_MODE
page._settings_container = _BoolWidget(visible=True)
page._filter_combo = _BoolWidget(visible=False)
page._filter_value = _PropertyWidget(visible=False)
page._filter_value._text = "youtube.txt"
page._in_range_label = _BoolWidget(visible=True)
page._in_range_mode = _BoolWidget(visible=True)
page._in_range_value = _BoolWidget(visible=True)
page._out_range_label = _BoolWidget(visible=True)
page._out_range_mode = _BoolWidget(visible=True)
page._out_range_value = _BoolWidget(visible=True)
page._rebuild_filter_kind_combo = Mock()
page._update_all_range_tooltips = Mock()
payload = SimpleNamespace(
editable_filter_enabled=True,
editable_filter_kinds=("hostlist",),
editable_filter_kind="hostlist",
editable_filter_value="youtube.txt",
in_range="x",
out_range="a",
)
with (
patch("profile.ui.profile_setup_page.set_combo_by_data"),
patch("profile.ui.profile_setup_page.set_range_controls"),
):
ProfileSetupPageBase._apply_editable_settings(page, payload)
self.assertEqual(page._settings_container.visible_calls, [])
self.assertEqual(page._filter_combo.visible_calls, [])
self.assertEqual(page._filter_value.visible_calls, [])
self.assertEqual(page._filter_value.text_calls, [])
for widget in (
page._in_range_label,
page._in_range_mode,
page._in_range_value,
page._out_range_label,
page._out_range_mode,
page._out_range_value,
):
self.assertEqual(widget.visible_calls, [])
if __name__ == "__main__":
unittest.main()