All checks were successful
Zapret source guards / validate (push) Successful in 11s
- Полосы вкладок «Готовые стратегии / Список сайтов / Текст профиля» больше нет. Страница сразу показывает готовые стратегии, а «Список сайтов» и «Текст профиля» открываются кнопками в шапке и становятся следующим шагом строки пути — так же, как подробности о стратегии. Возврат — по строке пути. - Поиск по стратегиям убран с экрана: строка появляется по Ctrl+F и закрывается по Esc или повторному Ctrl+F (запрос при этом сбрасывается). - Группировка переехала в одну строку с быстрыми отборами. - Экскурсия описывает новое устройство страницы. Список стратегий получил две строки высоты. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1727 lines
66 KiB
Python
1727 lines
66 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
from unittest.mock import Mock
|
|
|
|
|
|
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_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_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_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_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_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_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_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(),
|
|
"Статус списка профиля: Записей всего: 3 • ваших: 2 • есть несохранённые изменения",
|
|
)
|
|
self.assertEqual(
|
|
page._list_file_status_label.property("screenReaderStateText"),
|
|
"Статус списка профиля: Записей всего: 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_raw_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
|
|
|
|
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",
|
|
)
|
|
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
|
|
page._payload = payload
|
|
page._raw_tab_built = True
|
|
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)
|
|
|
|
ProfileSetupPageBase._apply_raw_tab_payload(page)
|
|
|
|
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, [])
|
|
|
|
def test_raw_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,
|
|
)
|
|
page = ProfileSetupPageBase.__new__(ProfileSetupPageBase)
|
|
page._payload = payload
|
|
page._raw_tab_built = True
|
|
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)
|
|
|
|
ProfileSetupPageBase._apply_raw_tab_payload(page)
|
|
|
|
self.assertEqual(page._raw_profile_text.plain_text_read_calls, [])
|
|
self.assertEqual(page._raw_profile_text.plain_text_calls, [])
|
|
|
|
def test_raw_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._raw_tab_built = True
|
|
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)
|
|
|
|
ProfileSetupPageBase._apply_raw_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)
|
|
page.reload_current_profile = Mock()
|
|
|
|
ProfileSetupPageBase._on_enabled_save_failed(page, 4, "boom")
|
|
|
|
self.assertEqual(page._enabled_checkbox.enabled_calls, [])
|
|
# Переключатель не должен остаться в положении, которого нет в пресете.
|
|
page.reload_current_profile.assert_called_once_with()
|
|
|
|
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._conditions_button = _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._conditions_button.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()
|