zapretgui/tests/test_profile_order_page.py
loop-uh 018c57b924
All checks were successful
Zapret source guards / validate (push) Successful in 5s
Windows-сборка / Dev Dev 21.1.5.70 опубликован
Починить перемещение preset-ов и профилей
2026-08-19 23:37:37 +03:00

1332 lines
54 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 inspect
import os
import unittest
from dataclasses import replace
from types import SimpleNamespace
from unittest.mock import Mock, patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QKeyEvent
from PyQt6.QtWidgets import QApplication, QAbstractItemView
from profile.state import ProfileListItem
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
def _item(name: str, *, key: str, in_preset: bool = True, profile_index: int = 0) -> ProfileListItem:
return ProfileListItem(
key=key,
persistent_key=key,
profile_index=profile_index,
display_name=name,
enabled=True,
in_preset=in_preset,
strategy_id="pass",
strategy_name="pass",
match_lines=("--filter-tcp=443", f"--hostlist=lists/{name.lower()}.txt"),
list_type="hostlist",
rating="",
favorite=False,
group="youtube",
group_name="YouTube",
order=profile_index,
profile_name=name,
)
class ProfileOrderPageTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def test_order_model_is_flat_and_keeps_only_real_preset_profiles(self) -> None:
from profile.ui.profile_order_list import ProfileOrderListModel
from profile.ui.profile_list_model import ProfileListModel
model = ProfileOrderListModel()
model.set_profiles((_item("YouTube", key="profile:0", profile_index=0), _item("Missing", key="template:0", in_preset=False)))
self.assertEqual(model.rowCount(), 1)
self.assertEqual(model.index(0, 0).data(ProfileListModel.KindRole), "profile")
self.assertEqual(model.index(0, 0).data(ProfileListModel.ProfileKeyRole), "profile:0")
self.assertEqual(model.index(0, 0).data(ProfileListModel.DisplayNameRole), "YouTube")
def test_order_model_returns_icon_roles_from_profile_icon_spec(self) -> None:
from profile.ui.profile_order_list import ProfileOrderListModel
from profile.ui.profile_list_model import ProfileListModel
model = ProfileOrderListModel()
model.set_profiles((_item("YouTube", key="profile:0", profile_index=0),))
index = model.index(0, 0)
self.assertEqual(index.data(ProfileListModel.IconNameRole), "simple:youtube:YT")
self.assertEqual(index.data(ProfileListModel.IconColorRole), "#FF0000")
def test_order_model_can_move_profile_locally(self) -> None:
from profile.ui.profile_order_list import ProfileOrderListModel
from profile.ui.profile_list_model import ProfileListModel
model = ProfileOrderListModel()
model.set_profiles((
_item("A", key="profile:a", profile_index=0),
_item("B", key="profile:b", profile_index=1),
_item("C", key="profile:c", profile_index=2),
))
self.assertTrue(model.move_profile("profile:c", "before", "profile:a"))
self.assertEqual(
[model.index(row, 0).data(ProfileListModel.ProfileKeyRole) for row in range(model.rowCount())],
["profile:c", "profile:a", "profile:b"],
)
def test_order_model_moves_profile_without_full_reset(self) -> None:
from profile.ui.profile_order_list import ProfileOrderListModel
from profile.ui.profile_list_model import ProfileListModel
model = ProfileOrderListModel()
model.set_profiles((
_item("A", key="profile:a", profile_index=0),
_item("B", key="profile:b", profile_index=1),
_item("C", key="profile:c", profile_index=2),
))
model.beginResetModel = Mock(side_effect=AssertionError("profile order move must not reset the whole model"))
self.assertTrue(model.move_profile("profile:a", "after", "profile:c"))
self.assertEqual(
[model.index(row, 0).data(ProfileListModel.ProfileKeyRole) for row in range(model.rowCount())],
["profile:b", "profile:c", "profile:a"],
)
def test_order_model_uses_exact_row_key_for_duplicate_logical_profiles(self) -> None:
from profile.ui.profile_order_list import _profile_order_identity
first = replace(_item("Same", key="profile:0", profile_index=0), persistent_key="name:Same")
second = replace(_item("Same", key="profile:1", profile_index=1), persistent_key="name:Same")
self.assertEqual(_profile_order_identity(first), "profile:0")
self.assertEqual(_profile_order_identity(second), "profile:1")
def test_order_model_skips_reset_when_profiles_are_unchanged(self) -> None:
from profile.ui.profile_order_list import ProfileOrderListModel
from profile.ui.profile_list_model import ProfileListModel
model = ProfileOrderListModel()
model.set_profiles((
_item("A", key="profile:a", profile_index=0),
_item("B", key="profile:b", profile_index=1),
))
model.beginResetModel = Mock(side_effect=AssertionError("unchanged profile order payload must not reset the whole model"))
model.endResetModel = Mock(side_effect=AssertionError("unchanged profile order payload must not reset the whole model"))
model.set_profiles((
_item("A", key="profile:a", profile_index=0),
_item("B", key="profile:b", profile_index=1),
))
self.assertEqual(model.rowCount(), 2)
self.assertEqual(
[model.index(row, 0).data(ProfileListModel.ProfileKeyRole) for row in range(model.rowCount())],
["profile:a", "profile:b"],
)
def test_order_model_updates_stable_rows_without_full_reset(self) -> None:
from profile.ui.profile_order_list import ProfileOrderListModel
from profile.ui.profile_list_model import ProfileListModel
model = ProfileOrderListModel()
model.set_profiles((
_item("A", key="profile:a", profile_index=0),
_item("B", key="profile:b", profile_index=1),
))
model.beginResetModel = Mock(side_effect=AssertionError("stable profile order rows must not reset the whole model"))
model.set_profiles((
_item("A updated", key="profile:a", profile_index=0),
_item("B", key="profile:b", profile_index=1),
))
self.assertEqual(model.rowCount(), 2)
self.assertEqual(model.index(0, 0).data(ProfileListModel.ProfileKeyRole), "profile:a")
self.assertEqual(model.index(0, 0).data(ProfileListModel.DisplayNameRole), "A updated")
def test_order_list_has_screen_reader_name_and_keyboard_help(self) -> None:
from profile.ui.profile_order_list import ProfileOrderList
order_list = ProfileOrderList()
self.addCleanup(order_list.deleteLater)
self.assertEqual(order_list.accessibleName(), "Порядок profile: список пока загружается")
self.assertEqual(
order_list.property("screenReaderStateText"),
"Порядок profile: список пока загружается",
)
self.assertIn("Ctrl со стрелкой вверх или вниз", order_list.accessibleDescription())
self.assertIn("PageUp и PageDown", order_list.accessibleDescription())
self.assertEqual(order_list._view.accessibleName(), "Порядок profile: список пока загружается")
self.assertEqual(
order_list._view.property("screenReaderStateText"),
"Порядок profile: список пока загружается",
)
self.assertIn("меняют порядок выбранного profile", order_list._view.accessibleDescription())
def test_order_list_ctrl_arrows_request_reordering_without_changing_selection(self) -> None:
from profile.ui.profile_order_list import ProfileOrderList
order_list = ProfileOrderList()
self.addCleanup(order_list.deleteLater)
order_list._model.set_profiles(
(
_item("A", key="profile:a", profile_index=0),
_item("B", key="profile:b", profile_index=1),
_item("C", key="profile:c", profile_index=2),
)
)
order_list._view.setCurrentIndex(order_list._model.index(1, 0))
before_requested: list[tuple[str, str]] = []
after_requested: list[tuple[str, str]] = []
order_list.profile_move_requested.connect(
lambda source, destination: before_requested.append((source, destination))
)
order_list.profile_move_after_requested.connect(
lambda source, destination: after_requested.append((source, destination))
)
up_event = QKeyEvent(
QKeyEvent.Type.KeyPress,
int(Qt.Key.Key_Up),
Qt.KeyboardModifier.ControlModifier,
)
QApplication.sendEvent(order_list._view, up_event)
self.assertTrue(up_event.isAccepted())
self.assertEqual(order_list._view.currentIndex().row(), 1)
self.assertEqual(before_requested, [("profile:b", "profile:a")])
down_event = QKeyEvent(
QKeyEvent.Type.KeyPress,
int(Qt.Key.Key_Down),
Qt.KeyboardModifier.ControlModifier,
)
QApplication.sendEvent(order_list._view, down_event)
self.assertTrue(down_event.isAccepted())
self.assertEqual(order_list._view.currentIndex().row(), 1)
self.assertEqual(after_requested, [("profile:b", "profile:c")])
def test_order_page_priority_hint_has_screen_reader_text(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
page = ProfileOrderPageBase(
create_profile_order_load_worker=Mock(),
create_preset_profile_order_move_worker=Mock(),
open_profiles=Mock(),
open_root=Mock(),
)
self.addCleanup(page.deleteLater)
self.assertEqual(
page._order_priority_hint.property("screenReaderStateText"),
(
"Подсказка порядка profile: Profile выше в списке имеет больший приоритет. "
"Если два profile-а подходят к одному домену или IP, будет применён тот, который находится выше."
),
)
def test_order_list_wrapper_forwards_keyboard_focus_to_view(self) -> None:
from profile.ui.profile_order_list import ProfileOrderList
order_list = ProfileOrderList()
self.addCleanup(order_list.deleteLater)
self.assertEqual(order_list.focusPolicy(), Qt.FocusPolicy.StrongFocus)
self.assertIs(order_list.focusProxy(), order_list._view)
def test_order_list_focuses_first_loaded_profile_for_screen_reader(self) -> None:
from profile.order_view_state import build_profile_order_list_view_state
from profile.ui.profile_order_list import ProfileOrderList
order_list = ProfileOrderList()
self.addCleanup(order_list.deleteLater)
state = build_profile_order_list_view_state((_item("A", key="profile:a", profile_index=0),))
order_list.apply_view_state(state)
self.assertEqual(order_list._view.currentIndex().row(), 0)
self.assertIn("Позиция 1", order_list._view.property("screenReaderStateText"))
self.assertIn("A", order_list._view.property("screenReaderStateText"))
def test_order_list_empty_loaded_state_is_read_for_screen_reader(self) -> None:
from profile.order_view_state import build_profile_order_list_view_state
from profile.ui.profile_order_list import ProfileOrderList
order_list = ProfileOrderList()
self.addCleanup(order_list.deleteLater)
state = build_profile_order_list_view_state(())
order_list.apply_view_state(state)
self.assertEqual(order_list._model.rowCount(), 0)
self.assertEqual(order_list.accessibleName(), "Порядок profile: список пуст")
self.assertEqual(order_list.property("screenReaderStateText"), "Порядок profile: список пуст")
self.assertEqual(order_list._view.accessibleName(), "Порядок profile: список пуст")
self.assertEqual(order_list._view.property("screenReaderStateText"), "Порядок profile: список пуст")
def test_order_list_moves_selected_profile_from_keyboard(self) -> None:
from profile.ui.profile_order_list import ProfileOrderList
order_list = ProfileOrderList()
self.addCleanup(order_list.deleteLater)
order_list._model.set_profiles(
(
_item("A", key="profile:a", profile_index=0),
_item("B", key="profile:b", profile_index=1),
)
)
order_list._view.setCurrentIndex(order_list._model.index(1, 0))
requested: list[tuple[str, str]] = []
order_list.profile_move_requested.connect(lambda source, destination: requested.append((source, destination)))
event = QKeyEvent(QKeyEvent.Type.KeyPress, int(Qt.Key.Key_PageUp), Qt.KeyboardModifier.NoModifier)
QApplication.sendEvent(order_list._view, event)
self.assertTrue(event.isAccepted())
self.assertEqual(requested, [("profile:b", "profile:a")])
def test_order_list_wrapper_forwards_arrow_and_pageup_keys_to_view(self) -> None:
from profile.ui.profile_order_list import ProfileOrderList
order_list = ProfileOrderList()
self.addCleanup(order_list.deleteLater)
order_list._model.set_profiles(
(
_item("A", key="profile:a", profile_index=0),
_item("B", key="profile:b", profile_index=1),
)
)
order_list._view.setCurrentIndex(order_list._model.index(0, 0))
requested: list[tuple[str, str]] = []
order_list.profile_move_requested.connect(lambda source, destination: requested.append((source, destination)))
down_event = QKeyEvent(QKeyEvent.Type.KeyPress, int(Qt.Key.Key_Down), Qt.KeyboardModifier.NoModifier)
order_list.keyPressEvent(down_event)
self.assertTrue(down_event.isAccepted())
self.assertEqual(order_list._view.currentIndex().row(), 1)
page_up_event = QKeyEvent(QKeyEvent.Type.KeyPress, int(Qt.Key.Key_PageUp), Qt.KeyboardModifier.NoModifier)
order_list.keyPressEvent(page_up_event)
self.assertTrue(page_up_event.isAccepted())
self.assertEqual(requested, [("profile:b", "profile:a")])
def test_order_list_navigation_does_not_use_native_selection_state(self) -> None:
from profile.ui.profile_order_list import ProfileOrderList
order_list = ProfileOrderList()
self.addCleanup(order_list.deleteLater)
order_list._model.set_profiles(
(
_item("A", key="profile:a", profile_index=0),
_item("B", key="profile:b", profile_index=1),
)
)
order_list._view.setCurrentIndex(order_list._model.index(0, 0))
down_event = QKeyEvent(QKeyEvent.Type.KeyPress, int(Qt.Key.Key_Down), Qt.KeyboardModifier.NoModifier)
order_list.keyPressEvent(down_event)
self.assertTrue(down_event.isAccepted())
self.assertEqual(order_list._view.selectionMode(), QAbstractItemView.SelectionMode.NoSelection)
self.assertEqual(order_list._view.currentIndex().row(), 1)
self.assertEqual(order_list._view.selectedIndexes(), [])
self.assertIn("Порядок profile: Позиция 2", order_list._view.property("screenReaderStateText"))
self.assertIn("B", order_list._view.property("screenReaderStateText"))
def test_order_inner_view_arrow_keys_move_current_row_without_native_selection(self) -> None:
from profile.ui.profile_order_list import ProfileOrderList
order_list = ProfileOrderList()
self.addCleanup(order_list.deleteLater)
order_list._model.set_profiles(
(
_item("A", key="profile:a", profile_index=0),
_item("B", key="profile:b", profile_index=1),
)
)
order_list._view.setCurrentIndex(order_list._model.index(0, 0))
event = QKeyEvent(QKeyEvent.Type.KeyPress, int(Qt.Key.Key_Down), Qt.KeyboardModifier.NoModifier)
order_list._view.keyPressEvent(event)
self.assertTrue(event.isAccepted())
self.assertEqual(order_list._view.currentIndex().row(), 1)
self.assertEqual(order_list._view.selectedIndexes(), [])
self.assertIn("Порядок profile: Позиция 2", order_list._view.property("screenReaderStateText"))
self.assertIn("B", order_list._view.property("screenReaderStateText"))
def test_order_page_explains_priority_and_uses_order_workers(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
build_source = inspect.getsource(ProfileOrderPageBase._build_content)
load_source = inspect.getsource(ProfileOrderPageBase._reload_order_profiles)
move_request_source = inspect.getsource(ProfileOrderPageBase._request_profile_order_move)
move_start_source = inspect.getsource(ProfileOrderPageBase._start_profile_order_move_worker)
before_source = inspect.getsource(ProfileOrderPageBase._on_profile_move_requested)
after_source = inspect.getsource(ProfileOrderPageBase._on_profile_move_after_requested)
end_source = inspect.getsource(ProfileOrderPageBase._on_profile_move_to_end_requested)
init_source = inspect.getsource(ProfileOrderPageBase.__init__)
self.assertIn("Profile выше в списке имеет больший приоритет", build_source)
self.assertIn("create_profile_order_load_worker", load_source)
self.assertIn("OneShotWorkerRuntime", init_source)
self.assertIn("_order_load_runtime", init_source)
self.assertIn("_order_move_runtime", init_source)
self.assertIn("start_qthread_worker", load_source)
self.assertIn("_start_profile_order_move_worker", move_request_source)
self.assertIn("start_qthread_worker", move_start_source)
self.assertNotIn("worker.start()", load_source)
self.assertNotIn("worker.start()", move_request_source)
self.assertNotIn("worker.start()", move_start_source)
self.assertIn("_request_profile_order_move", before_source)
self.assertIn("_request_profile_order_move", after_source)
self.assertIn("_request_profile_order_move", end_source)
self.assertNotIn("list_preset_order_profiles", load_source)
self.assertNotIn("move_preset_profile_before", before_source)
self.assertNotIn("move_preset_profile_after", after_source)
self.assertNotIn("move_preset_profile_to_end", end_source)
def test_order_page_repeat_activation_skips_clean_payload_reload(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page._order_payload_loaded_once = True
page._order_payload_dirty = False
page._reload_order_profiles = Mock()
ProfileOrderPageBase.on_page_activated(page)
page._reload_order_profiles.assert_not_called()
def test_order_page_clean_activation_restores_heavy_list_after_show(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
class _List:
def __init__(self) -> None:
self.visible_calls: list[bool] = []
def setVisible(self, value: bool) -> None: # noqa: N802
self.visible_calls.append(bool(value))
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
order_list = _List()
page._order_list = order_list
page._order_payload_loaded_once = True
page._order_payload_dirty = False
page._order_list_show_scheduled = False
page._cleanup_in_progress = False
page._reload_order_profiles = Mock(
side_effect=AssertionError("clean order activation must not reload payload")
)
scheduled: list[object] = []
with patch(
"profile.ui.profile_order_page.QTimer.singleShot",
side_effect=lambda _delay, callback: scheduled.append(callback),
):
ProfileOrderPageBase.on_page_hidden(page)
ProfileOrderPageBase.on_page_activated(page)
self.assertEqual(order_list.visible_calls, [False])
page._reload_order_profiles.assert_not_called()
self.assertEqual(len(scheduled), 1)
scheduled[0]()
self.assertEqual(order_list.visible_calls, [False, True])
def test_order_page_ui_state_change_marks_payload_dirty(self) -> None:
from app.state_store import MainWindowStateStore
from profile.ui.profile_order_page import ProfileOrderPageBase
store = MainWindowStateStore()
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page._ui_state_store = None
page._ui_state_unsubscribe = None
page._cleanup_in_progress = False
page._order_payload_dirty = False
page.isVisible = Mock(return_value=False)
ProfileOrderPageBase.bind_ui_state_store(page, store)
store.bump_preset_content_revision()
self.assertTrue(page._order_payload_dirty)
def test_order_page_updates_visible_list_locally_after_move_worker(self) -> None:
from profile.ui.profile_order_list import ProfileOrderList
from profile.ui.profile_order_page import ProfileOrderPageBase
moved_source = inspect.getsource(ProfileOrderPageBase._on_profile_order_moved)
local_source = inspect.getsource(ProfileOrderPageBase._apply_profile_order_move_locally)
list_source = inspect.getsource(ProfileOrderList)
self.assertIn("_apply_profile_order_move_locally", moved_source)
self.assertIn("_reload_order_profiles", moved_source)
self.assertLess(moved_source.index("_apply_profile_order_move_locally"), moved_source.rindex("_reload_order_profiles"))
self.assertIn("move_profile_item", list_source)
self.assertIn("move_profile_item", local_source)
def test_order_page_starts_workers_without_direct_profile_calls(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
class _Signal:
def __init__(self) -> None:
self.callbacks = []
def connect(self, callback) -> None:
self.callbacks.append(callback)
class _Worker:
def __init__(self) -> None:
self.loaded = _Signal()
self.moved = _Signal()
self.failed = _Signal()
self.finished = _Signal()
self.start = Mock()
self.deleteLater = Mock()
def isRunning(self) -> bool:
return False
from ui.one_shot_worker_runtime import OneShotWorkerRuntime
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page.launch_method = "zapret2_mode"
page._order_load_runtime = OneShotWorkerRuntime()
page._order_move_runtime = OneShotWorkerRuntime()
page._order_load_dirty = False
page._cleanup_in_progress = False
load_worker = _Worker()
move_worker = _Worker()
page._create_profile_order_load_worker = Mock(return_value=load_worker)
page._create_profile_order_move_worker = Mock(return_value=move_worker)
ProfileOrderPageBase._reload_order_profiles(page)
ProfileOrderPageBase._on_profile_move_requested(page, "profile-1", "profile-2")
page._create_profile_order_load_worker.assert_called_once_with(1, "zapret2_mode", page)
page._create_profile_order_move_worker.assert_called_once_with(
1,
"zapret2_mode",
action="before",
source_profile_key="profile-1",
destination_profile_key="profile-2",
parent=page,
)
load_worker.start.assert_called_once()
move_worker.start.assert_called_once()
def test_order_page_receives_worker_factories_instead_of_profile_feature(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
from ui.page_deps.presets import build_profile_order_page_kwargs
from ui.navigation_pages import PageName
init_source = inspect.getsource(ProfileOrderPageBase.__init__)
page_source = inspect.getsource(ProfileOrderPageBase)
self.assertIn("create_profile_order_load_worker", init_source)
self.assertIn("create_preset_profile_order_move_worker", init_source)
self.assertNotIn("profile_feature", init_source)
self.assertNotIn("self._profile", page_source)
profile_feature = Mock()
kwargs = build_profile_order_page_kwargs(
page_name=PageName.ZAPRET2_PROFILE_ORDER,
profile_feature=profile_feature,
show_page=Mock(),
)
self.assertIs(kwargs["create_profile_order_load_worker"], profile_feature.create_profile_order_load_worker)
self.assertIs(
kwargs["create_preset_profile_order_move_worker"],
profile_feature.create_preset_profile_order_move_worker,
)
self.assertNotIn("profile_feature", kwargs)
def test_order_page_invalidates_running_load_before_refresh_after_move(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
from ui.one_shot_worker_runtime import OneShotWorkerRuntime
class _RunningWorker:
def isRunning(self) -> bool:
return True
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page._order_load_runtime = OneShotWorkerRuntime()
page._order_load_runtime.worker = _RunningWorker()
page._order_load_runtime.request_id = 7
page._order_load_dirty = False
page._create_profile_order_load_worker = Mock()
page._payload = "current"
page._cleanup_in_progress = False
callbacks = []
ProfileOrderPageBase._reload_order_profiles(page, force=True)
ProfileOrderPageBase._on_order_profiles_loaded(page, 7, "stale")
self.assertEqual(page._order_load_runtime.request_id, 8)
self.assertTrue(page._order_load_dirty)
page._create_profile_order_load_worker.assert_not_called()
self.assertEqual(page._payload, "current")
page._order_load_runtime.worker = None
with patch(
"profile.ui.profile_order_page.QTimer.singleShot",
side_effect=lambda _delay, callback: callbacks.append(callback),
):
ProfileOrderPageBase._on_order_profiles_worker_finished(page, object())
page._create_profile_order_load_worker.assert_not_called()
self.assertEqual(len(callbacks), 1)
callbacks[0]()
page._create_profile_order_load_worker.assert_called_once_with(9, "zapret2_mode", page)
def test_order_page_reload_waits_while_restart_is_scheduled(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
from ui.one_shot_worker_runtime import OneShotWorkerRuntime
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page._cleanup_in_progress = False
page._order_load_runtime = OneShotWorkerRuntime()
page._order_load_dirty = True
page._order_load_restart_scheduled = True
page._create_profile_order_load_worker = Mock()
ProfileOrderPageBase._reload_order_profiles(page, force=True)
page._create_profile_order_load_worker.assert_not_called()
self.assertTrue(page._order_load_dirty)
def test_stale_order_load_worker_finished_does_not_schedule_reload(self) -> None:
import profile.ui.profile_order_page as order_page
from profile.ui.profile_order_page import ProfileOrderPageBase
from ui.one_shot_worker_runtime import OneShotWorkerRuntime
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page._cleanup_in_progress = False
page._order_load_runtime = OneShotWorkerRuntime()
page._order_load_runtime.request_id = 5
page._order_load_dirty = True
page._schedule_order_profiles_reload = Mock(
side_effect=AssertionError("stale profile order load worker must not restart reload")
)
with patch.object(order_page, "QTimer") as timer_mock:
ProfileOrderPageBase._on_order_profiles_worker_finished(
page,
SimpleNamespace(_request_id=4),
)
timer_mock.singleShot.assert_not_called()
self.assertTrue(page._order_load_dirty)
page._schedule_order_profiles_reload.assert_not_called()
def test_order_page_defers_list_apply_after_load_worker_signal(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
from ui.one_shot_worker_runtime import OneShotWorkerRuntime
payload = SimpleNamespace(items=(_item("A", key="profile:a"),))
callbacks = []
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page._order_load_runtime = OneShotWorkerRuntime()
page._order_load_runtime.request_id = 3
page._cleanup_in_progress = False
page._payload = None
page._order_list = Mock()
page._rebuild_breadcrumb = Mock()
with patch(
"profile.ui.profile_order_page.QTimer",
SimpleNamespace(singleShot=lambda _delay, callback: callbacks.append(callback)),
create=True,
):
ProfileOrderPageBase._on_order_profiles_loaded(page, 3, payload)
page._order_list.set_profiles.assert_not_called()
page._rebuild_breadcrumb.assert_not_called()
self.assertIs(page._payload, payload)
self.assertEqual(len(callbacks), 1)
callbacks[0]()
page._order_list.set_profiles.assert_called_once_with(payload.items)
page._rebuild_breadcrumb.assert_called_once_with()
def test_pending_order_payload_apply_is_ignored_after_reload_is_requested(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
from ui.one_shot_worker_runtime import OneShotWorkerRuntime
class _RunningWorker:
def isRunning(self) -> bool:
return True
payload = SimpleNamespace(items=(_item("Old", key="profile:old"),))
callbacks = []
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page.launch_method = "zapret2_mode"
page._order_load_runtime = OneShotWorkerRuntime()
page._order_load_runtime.worker = _RunningWorker()
page._order_load_runtime.request_id = 3
page._cleanup_in_progress = False
page._order_load_dirty = False
page._order_load_restart_scheduled = False
page._order_payload_apply_scheduled = False
page._pending_order_payload_apply = None
page._order_list = Mock()
page._rebuild_breadcrumb = Mock()
with patch(
"profile.ui.profile_order_page.QTimer",
SimpleNamespace(singleShot=lambda _delay, callback: callbacks.append(callback)),
create=True,
):
ProfileOrderPageBase._on_order_profiles_loaded(page, 3, payload)
ProfileOrderPageBase._reload_order_profiles(page, force=True)
self.assertTrue(page._order_load_dirty)
self.assertEqual(len(callbacks), 1)
callbacks[0]()
page._order_list.set_profiles.assert_not_called()
page._rebuild_breadcrumb.assert_not_called()
def test_order_page_replays_queued_moves_after_running_move_worker_finishes(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
from ui.one_shot_worker_runtime import OneShotWorkerRuntime
class _RunningWorker:
def isRunning(self) -> bool:
return True
def quit(self) -> None:
pass
class _Worker:
def __init__(self) -> None:
self.moved = _Signal()
self.failed = _Signal()
self.finished = _Signal()
self.start = Mock()
self.deleteLater = Mock()
def isRunning(self) -> bool:
return False
class _Signal:
def __init__(self) -> None:
self.callbacks = []
def connect(self, callback) -> None:
self.callbacks.append(callback)
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page.launch_method = "zapret2_mode"
page._cleanup_in_progress = False
page._order_move_runtime = OneShotWorkerRuntime()
running_worker = _RunningWorker()
page._order_move_runtime.worker = running_worker
page._order_move_runtime.request_id = 4
page._pending_profile_order_moves = []
next_worker = _Worker()
page._create_profile_order_move_worker = Mock(return_value=next_worker)
ProfileOrderPageBase._request_profile_order_move(
page,
"after",
"profile-a",
destination_profile_key="profile-b",
)
ProfileOrderPageBase._request_profile_order_move(
page,
"end",
"profile-c",
)
page._create_profile_order_move_worker.assert_not_called()
self.assertEqual(
page._pending_profile_order_moves,
[
{
"action": "after",
"source_profile_key": "profile-a",
"destination_profile_key": "profile-b",
},
{
"action": "end",
"source_profile_key": "profile-c",
"destination_profile_key": "",
},
],
)
page._order_move_runtime.worker = None
callbacks = []
with patch(
"profile.ui.profile_order_page.QTimer.singleShot",
side_effect=lambda _delay, callback: callbacks.append(callback),
):
ProfileOrderPageBase._on_profile_order_move_worker_finished(page, running_worker)
page._create_profile_order_move_worker.assert_not_called()
next_worker.start.assert_not_called()
self.assertEqual(len(callbacks), 1)
callbacks[0]()
page._create_profile_order_move_worker.assert_called_once_with(
5,
"zapret2_mode",
action="after",
source_profile_key="profile-a",
destination_profile_key="profile-b",
parent=page,
)
next_worker.start.assert_called_once()
self.assertEqual(
page._pending_profile_order_moves,
[
{
"action": "end",
"source_profile_key": "profile-c",
"destination_profile_key": "",
}
],
)
def test_stale_profile_order_move_worker_finished_does_not_start_pending_move(self) -> None:
import profile.ui.profile_order_page as order_page
from profile.ui.profile_order_page import ProfileOrderPageBase
from ui.one_shot_worker_runtime import OneShotWorkerRuntime
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page._cleanup_in_progress = False
page._order_move_runtime = OneShotWorkerRuntime()
page._order_move_runtime.request_id = 5
page._pending_profile_order_moves = [
{
"action": "after",
"source_profile_key": "profile-a",
"destination_profile_key": "profile-b",
}
]
page._schedule_next_profile_order_move_start = Mock(
side_effect=AssertionError("stale profile order move worker must not drive pending queue")
)
with patch.object(order_page, "QTimer") as timer_mock:
ProfileOrderPageBase._on_profile_order_move_worker_finished(
page,
SimpleNamespace(_request_id=4),
)
timer_mock.singleShot.assert_not_called()
self.assertEqual(
page._pending_profile_order_moves,
[
{
"action": "after",
"source_profile_key": "profile-a",
"destination_profile_key": "profile-b",
}
],
)
def test_stale_profile_order_move_worker_object_does_not_start_pending_move(self) -> None:
import profile.ui.profile_order_page as order_page
from profile.ui.profile_order_page import ProfileOrderPageBase
from ui.one_shot_worker_runtime import OneShotWorkerRuntime
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page._cleanup_in_progress = False
page._order_move_runtime = OneShotWorkerRuntime()
page._order_move_runtime.worker = object()
page._pending_profile_order_moves = [
{
"action": "after",
"source_profile_key": "profile-a",
"destination_profile_key": "profile-b",
}
]
page._schedule_next_profile_order_move_start = Mock(
side_effect=AssertionError("stale profile order move worker must not drive pending queue")
)
with patch.object(order_page, "QTimer") as timer_mock:
ProfileOrderPageBase._on_profile_order_move_worker_finished(page, object())
timer_mock.singleShot.assert_not_called()
self.assertEqual(
page._pending_profile_order_moves,
[
{
"action": "after",
"source_profile_key": "profile-a",
"destination_profile_key": "profile-b",
}
],
)
def test_order_page_move_error_ignored_when_new_move_is_pending(self) -> None:
import profile.ui.profile_order_page as order_page
from profile.ui.profile_order_page import ProfileOrderPageBase
from ui.one_shot_worker_runtime import OneShotWorkerRuntime
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page._order_move_runtime = OneShotWorkerRuntime()
page._order_move_runtime.request_id = 4
page._cleanup_in_progress = False
page._pending_profile_order_moves = [
{
"action": "before",
"source_profile_key": "profile-b",
"destination_profile_key": "profile-a",
}
]
page._order_move_reload_required = True
page._reload_order_profiles = Mock()
page.window = Mock(return_value=object())
with patch.object(order_page, "log") as log_mock, patch.object(order_page.InfoBar, "error") as error_mock:
ProfileOrderPageBase._on_profile_order_move_failed(page, 4, "stale error")
log_mock.assert_not_called()
error_mock.assert_not_called()
page._reload_order_profiles.assert_not_called()
self.assertTrue(page._order_move_reload_required)
def test_order_page_replaces_pending_move_for_same_profile(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
from ui.one_shot_worker_runtime import OneShotWorkerRuntime
class _RunningWorker:
def isRunning(self) -> bool:
return True
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page._cleanup_in_progress = False
page._order_move_runtime = OneShotWorkerRuntime()
page._order_move_runtime.worker = _RunningWorker()
page._pending_profile_order_moves = []
page._create_profile_order_move_worker = Mock()
ProfileOrderPageBase._request_profile_order_move(
page,
"before",
"profile-a",
destination_profile_key="profile-b",
)
ProfileOrderPageBase._request_profile_order_move(
page,
"after",
"profile-a",
destination_profile_key="profile-c",
)
page._create_profile_order_move_worker.assert_not_called()
self.assertEqual(
page._pending_profile_order_moves,
[
{
"action": "after",
"source_profile_key": "profile-a",
"destination_profile_key": "profile-c",
},
],
)
def test_order_page_applies_move_locally_even_when_new_move_is_pending(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page._cleanup_in_progress = False
page._order_move_runtime = SimpleNamespace(is_current=Mock(return_value=True))
page._order_move_reload_required = False
page._pending_profile_order_moves = [
{
"action": "end",
"source_profile_key": "profile-c",
"destination_profile_key": "",
}
]
page._apply_profile_order_move_locally = Mock(return_value=True)
page._reload_order_profiles = Mock()
ProfileOrderPageBase._on_profile_order_moved(
page,
4,
"after",
"profile-a",
"profile-b",
True,
)
page._apply_profile_order_move_locally.assert_called_once_with(
"after",
"profile-a",
destination_profile_key="profile-b",
)
self.assertFalse(page._order_move_reload_required)
page._reload_order_profiles.assert_not_called()
def test_order_page_reloads_from_preset_after_local_move_finishes(self) -> None:
from profile.key_resolution import PresetProfileMoveResult
from profile.ui.profile_order_page import ProfileOrderPageBase
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page._cleanup_in_progress = False
page._order_move_runtime = SimpleNamespace(is_current=Mock(return_value=True))
page._order_move_reload_required = False
page._pending_profile_order_moves = []
page._apply_profile_order_move_locally = Mock(return_value=True)
page._remap_profile_order_after_file_rebuild = Mock()
page._reload_order_profiles = Mock()
ProfileOrderPageBase._on_profile_order_moved(
page,
4,
"before",
"profile-c",
"profile-a",
PresetProfileMoveResult(
profile_key="profile:0",
key_map={"profile:2": "profile:0", "profile:0": "profile:1"},
),
)
page._apply_profile_order_move_locally.assert_called_once_with(
"before",
"profile-c",
destination_profile_key="profile-a",
)
page._remap_profile_order_after_file_rebuild.assert_called_once_with(
{"profile:2": "profile:0", "profile:0": "profile:1"}
)
page._reload_order_profiles.assert_called_once_with(force=True)
def test_order_page_reloads_after_queue_when_pending_move_cannot_apply_locally(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page._cleanup_in_progress = False
page._order_move_runtime = SimpleNamespace(is_current=Mock(return_value=True))
page._pending_profile_order_moves = [
{
"action": "end",
"source_profile_key": "profile-c",
"destination_profile_key": "",
}
]
page._apply_profile_order_move_locally = Mock(return_value=False)
page._reload_order_profiles = Mock()
ProfileOrderPageBase._on_profile_order_moved(
page,
4,
"after",
"profile-a",
"profile-b",
True,
)
page._apply_profile_order_move_locally.assert_called_once_with(
"after",
"profile-a",
destination_profile_key="profile-b",
)
page._pending_profile_order_moves = []
ProfileOrderPageBase._on_profile_order_moved(
page,
5,
"end",
"profile-c",
"",
True,
)
page._reload_order_profiles.assert_called_once_with(force=True)
def test_profile_order_move_waits_while_restart_is_scheduled(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
from ui.one_shot_worker_runtime import OneShotWorkerRuntime
class _Worker:
def __init__(self) -> None:
self.moved = Mock(connect=Mock())
self.failed = Mock(connect=Mock())
self.finished = Mock(connect=Mock())
self.start = Mock()
self.deleteLater = Mock()
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page.launch_method = "zapret2_mode"
page._cleanup_in_progress = False
page._order_move_runtime = OneShotWorkerRuntime()
page._pending_profile_order_moves = [
{
"action": "after",
"source_profile_key": "profile-a",
"destination_profile_key": "profile-b",
}
]
page._order_move_start_scheduled = True
next_worker = _Worker()
page._create_profile_order_move_worker = Mock(return_value=next_worker)
ProfileOrderPageBase._request_profile_order_move(page, "end", "profile-c")
page._create_profile_order_move_worker.assert_not_called()
next_worker.start.assert_not_called()
self.assertEqual(
page._pending_profile_order_moves,
[
{
"action": "after",
"source_profile_key": "profile-a",
"destination_profile_key": "profile-b",
},
{
"action": "end",
"source_profile_key": "profile-c",
"destination_profile_key": "",
},
],
)
def test_order_page_cleanup_stops_order_workers(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
from ui.one_shot_worker_runtime import OneShotWorkerRuntime
class _Worker:
def __init__(self) -> None:
self.quit = Mock()
def isRunning(self) -> bool:
return True
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
load_worker = _Worker()
move_worker = _Worker()
page._order_load_runtime = OneShotWorkerRuntime()
page._order_load_runtime.worker = load_worker
page._order_load_runtime.request_id = 3
page._order_move_runtime = OneShotWorkerRuntime()
page._order_move_runtime.worker = move_worker
page._order_move_runtime.request_id = 5
page._order_load_dirty = True
ProfileOrderPageBase.cleanup(page)
load_worker.quit.assert_called_once()
move_worker.quit.assert_called_once()
self.assertIsNone(page._order_load_runtime.worker)
self.assertIsNone(page._order_move_runtime.worker)
self.assertEqual(page._order_load_runtime.request_id, 4)
self.assertEqual(page._order_move_runtime.request_id, 6)
self.assertFalse(page._order_load_dirty)
def test_order_page_remaps_pending_moves_after_preset_file_rebuild(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
from ui.queued_worker_state import QueuedWorkerState
page = ProfileOrderPageBase.__new__(ProfileOrderPageBase)
page._order_list = Mock()
page._order_move_state = QueuedWorkerState(
Mock(),
pending=[
{
"action": "before",
"source_profile_key": "profile:1",
"destination_profile_key": "profile:2",
}
],
)
ProfileOrderPageBase._remap_profile_order_after_file_rebuild(
page,
{
"profile:1": "profile:2",
"profile:2": "profile:1",
},
)
page._order_list.remap_profile_keys.assert_called_once_with(
{
"profile:1": "profile:2",
"profile:2": "profile:1",
}
)
self.assertEqual(
page._order_move_state.pending,
[
{
"action": "before",
"source_profile_key": "profile:2",
"destination_profile_key": "profile:1",
}
],
)
def test_order_workers_call_profile_service(self) -> None:
from profile.profile_order_loader import ProfileOrderListLoadWorker, ProfilePresetOrderMoveWorker
load_profiles = Mock(return_value=SimpleNamespace(items=()))
load_worker = ProfileOrderListLoadWorker(2, load_profiles)
loaded = []
load_worker.loaded.connect(lambda request_id, payload: loaded.append((request_id, payload)))
load_worker.run()
load_profiles.assert_called_once_with()
self.assertEqual(len(loaded), 1)
self.assertEqual(loaded[0][0], 2)
self.assertIs(loaded[0][1].payload, load_profiles.return_value)
self.assertIsNone(loaded[0][1].view_state)
move_before = Mock(return_value="profile-1")
move_after = Mock()
move_to_end = Mock()
move_worker = ProfilePresetOrderMoveWorker(
3,
move_before,
move_after,
move_to_end,
action="before",
source_profile_key="profile-1",
destination_profile_key="profile-2",
)
moved = []
move_worker.moved.connect(
lambda request_id, action, source_key, destination_key, result: moved.append((
request_id,
action,
source_key,
destination_key,
result,
))
)
move_worker.run()
move_before.assert_called_once_with("profile-1", "profile-2")
self.assertEqual(moved, [(3, "before", "profile-1", "profile-2", "profile-1")])
def test_order_page_has_breadcrumbs_back_to_profiles_and_control(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
build_source = inspect.getsource(ProfileOrderPageBase._build_content)
breadcrumb_source = inspect.getsource(ProfileOrderPageBase._rebuild_breadcrumb)
handler_source = inspect.getsource(ProfileOrderPageBase._on_breadcrumb_item_changed)
self.assertIn("BreadcrumbBar", build_source)
self.assertIn('"profiles"', breadcrumb_source)
self.assertIn('"order"', breadcrumb_source)
self.assertIn("_open_profiles()", handler_source)
self.assertIn("_open_root()", handler_source)
def test_order_page_breadcrumb_exposes_screen_reader_path(self) -> None:
from profile.ui.profile_order_page import ProfileOrderPageBase
breadcrumb = _Breadcrumb()
page = SimpleNamespace(
_breadcrumb=breadcrumb,
control_key="missing.control.key",
profiles_key="missing.profiles.key",
profiles_default="Настройка пресета",
_ui_language="ru",
)
ProfileOrderPageBase._rebuild_breadcrumb(page)
self.assertEqual(
breadcrumb.property("screenReaderStateText"),
"Навигация: Управление > Настройка пресета > Порядок в пресете",
)
self.assertEqual(
breadcrumb.accessibleDescription(),
"Показывает путь до текущей страницы. Выберите пункт, чтобы вернуться назад.",
)
def test_order_page_breadcrumb_click_restores_crumbs_before_navigation(self) -> None:
# Клик по крошке удаляет из BreadcrumbBar элементы правее выбранного —
# обработчик обязан перестроить полный путь до навигации.
from profile.ui.profile_order_page import ProfileOrderPageBase
for key, navigate_attr in (("control", "_open_root"), ("profiles", "_open_profiles")):
with self.subTest(key=key):
page = SimpleNamespace(
_rebuild_breadcrumb=Mock(),
_open_root=Mock(),
_open_profiles=Mock(),
)
ProfileOrderPageBase._on_breadcrumb_item_changed(page, key)
page._rebuild_breadcrumb.assert_called_once()
getattr(page, navigate_attr).assert_called_once()
def test_order_list_does_not_expose_context_or_folder_actions(self) -> None:
from profile.ui.profile_order_list import ProfileOrderList
source = inspect.getsource(ProfileOrderList)
self.assertNotIn("profile_context_requested", source)
self.assertNotIn("folder_context_requested", source)
self.assertNotIn("folder_toggled", source)
if __name__ == "__main__":
unittest.main()