zapretgui/tests/test_user_presets_dependency_boundaries.py
loop-uh 2982ebd648
All checks were successful
Zapret source guards / validate (push) Successful in 7s
Пресеты и профили — точность: проверка запуска не считает флаги в комментариях, строки делятся только по переводу строки (как у парсера); при смене стратегии сохраняется действующий (последний) --payload; новый пользовательский profile не делит файлы списков с переименованным, битая запись не роняет шаблоны; кэш профиля учитывает имя и файлы списков; «v1.5» ищется как v1.5.txt, имя возвращается как на диске; занятое имя при создании/копии/переименовании/импорте становится «X (2)» и в файле, и в списке; импорт ZIP переписывает ссылки в строке с несколькими опциями и отвергает имена CON, a.txt:поток и т.п.; смена стратегии не сбрасывает папку и порядок профиля; результат и ошибка импорта показываются всегда, повторный импорт по ссылке не теряет источник, папка сворачивается сразу; шаги перемещения папок не схлопываются; устаревший план фильтра стратегий не затирает строки; сбои обновления меты пишутся в лог. Доработки волны 1: запасной пресет нельзя удалить, щелчок по нему закрепляет выбор, действия над профилями не принимаются, пока список показывает прежний пресет, результат сканирования с заблокированным файлом не кэшируется
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:08:38 +03:00

853 lines
37 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
from types import SimpleNamespace
import unittest
from unittest.mock import Mock, patch
class UserPresetsDependencyBoundaryTests(unittest.TestCase):
def test_user_presets_page_receives_link_worker_factory_without_open_url_callable(self) -> None:
from app.page_names import PageName
from presets.ui.common.user_presets_page import UserPresetsPageBase
from ui.page_deps.presets import build_user_presets_page_kwargs
init_source = inspect.getsource(UserPresetsPageBase.__init__)
page_source = inspect.getsource(UserPresetsPageBase)
runtime_source = inspect.getsource(UserPresetsPageBase._build_page_runtime)
self.assertNotIn("open_url", init_source)
self.assertNotIn("self._open_url", page_source)
self.assertNotIn("external_actions_feature", init_source)
self.assertNotIn("self._external_actions", page_source)
self.assertNotIn("open_url=self._open_url", runtime_source)
self.assertNotIn(
"open_url=self._open_url",
inspect.getsource(UserPresetsPageBase.create_preset_link_action_worker),
)
external_actions = Mock()
presets = Mock()
kwargs = build_user_presets_page_kwargs(
page_name=PageName.ZAPRET2_USER_PRESETS,
presets_feature=presets,
external_actions_feature=external_actions,
open_preset_raw_editor=Mock(),
ui_state_store=Mock(),
)
self.assertNotIn("open_url", kwargs)
self.assertNotIn("external_actions_feature", kwargs)
parent = object()
kwargs["create_preset_link_action_worker"](7, action="info", parent=parent)
presets.create_preset_link_action_worker.assert_called_once_with(
7,
open_url=external_actions.open_url,
action="info",
parent=parent,
)
def test_user_presets_activation_error_uses_shared_notification_center(self) -> None:
from presets.ui.common import user_presets_page as page_module
from presets.ui.common.user_presets_page import UserPresetsPageBase
notify = Mock()
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._preset_activate_request_id = 5
page._pending_preset_activation = None
page._notify = notify
page._runtime_service = SimpleNamespace(apply_active_preset_marker_for_file=Mock())
page._restore_preset_activation_marker_file_name = ""
page._tr = lambda _key, default, **_kwargs: default
result = SimpleNamespace(
ok=False,
log_message="Ошибка активации пресета: long details",
log_level="ERROR",
infobar_level="error",
infobar_title="Ошибка",
infobar_content="Preset содержит ссылки на отсутствующие файлы (2), например: --hostlist=lists/example.txt",
activated_file_name=None,
)
with (
patch.object(page_module, "log"),
patch.object(page_module.InfoBar, "error") as direct_error,
):
UserPresetsPageBase._on_preset_activation_finished(page, 5, result)
direct_error.assert_not_called()
notify.assert_called_once()
payload = notify.call_args.args[0]
self.assertEqual(payload["level"], "error")
self.assertEqual(payload["source"], "presets.user.activation")
self.assertEqual(payload["title"], "Ошибка")
self.assertIn("hostlist=lists/example.txt", payload["content"])
self.assertGreaterEqual(int(payload["dedupe_window_ms"]), 10000)
def test_user_presets_page_receives_concrete_preset_actions_instead_of_feature(self) -> None:
from app.page_names import PageName
from presets.ui.common.user_presets_page import UserPresetsPageBase
from presets.ui.common.user_presets_page_runtime import (
UserPresetsPageRuntime,
UserPresetsPageRuntimeConfig,
UserPresetsRuntimeActions,
)
from ui.page_deps.presets import build_user_presets_page_kwargs
init_source = inspect.getsource(UserPresetsPageBase.__init__)
page_source = inspect.getsource(UserPresetsPageBase)
runtime_actions_source = inspect.getsource(UserPresetsRuntimeActions)
runtime_config_source = inspect.getsource(UserPresetsPageRuntimeConfig)
runtime_source = inspect.getsource(UserPresetsPageRuntime)
runtime_build_source = inspect.getsource(UserPresetsPageBase._build_page_runtime)
deps_source = inspect.getsource(build_user_presets_page_kwargs)
self.assertNotIn("presets_feature", init_source)
self.assertNotIn("self._presets_feature", page_source)
self.assertNotIn("get_presets_feature", runtime_config_source)
self.assertNotIn("get_presets_feature", runtime_build_source)
self.assertNotIn("get_preset_manifest_by_file_name", runtime_actions_source)
self.assertNotIn("get_selected_source_preset_manifest", runtime_actions_source)
self.assertNotIn("get_preset_manifest_by_file_name", runtime_source)
self.assertNotIn("get_selected_source_preset_manifest", runtime_source)
self.assertNotIn("get_preset_manifest_by_file_name=", deps_source)
self.assertNotIn("get_selected_source_preset_manifest=", deps_source)
self.assertIn("preset_runtime_actions", init_source)
self.assertIn("connect_preset_signals", init_source)
self.assertIn("create_user_presets_open_folder_worker", init_source)
self.assertIn("create_preset_edit_action_worker", init_source)
self.assertIn("create_preset_bulk_action_worker", init_source)
self.assertIn("create_preset_activate_worker", init_source)
self.assertIn("create_preset_item_action_worker", init_source)
self.assertIn("create_preset_link_action_worker", init_source)
self.assertIn("create_preset_folder_action_worker", init_source)
self.assertIn("create_preset_storage_action_worker", init_source)
self.assertIn("load_preset_folder_state", init_source)
self.assertNotIn("delete_preset_item_meta", init_source)
self.assertNotIn("from presets.folders import", page_source)
presets = Mock()
external_actions = Mock()
kwargs = build_user_presets_page_kwargs(
page_name=PageName.ZAPRET2_USER_PRESETS,
presets_feature=presets,
external_actions_feature=external_actions,
open_preset_raw_editor=Mock(),
ui_state_store=Mock(),
)
self.assertNotIn("presets_feature", kwargs)
self.assertIn("preset_runtime_actions", kwargs)
self.assertIs(kwargs["connect_preset_signals"], presets.connect_preset_signals)
self.assertIs(
kwargs["create_user_presets_open_folder_worker"],
presets.create_user_presets_open_folder_worker,
)
self.assertIs(
kwargs["create_preset_edit_action_worker"],
presets.create_preset_edit_action_worker,
)
self.assertIs(
kwargs["create_preset_bulk_action_worker"],
presets.create_preset_bulk_action_worker,
)
self.assertIs(
kwargs["create_preset_activate_worker"],
presets.create_preset_activate_worker,
)
self.assertIs(
kwargs["create_preset_item_action_worker"],
presets.create_preset_item_action_worker,
)
self.assertTrue(callable(kwargs["create_preset_link_action_worker"]))
self.assertNotIn("open_url", kwargs)
self.assertIs(
kwargs["create_preset_folder_action_worker"],
presets.create_preset_folder_action_worker,
)
self.assertIs(
kwargs["create_preset_storage_action_worker"],
presets.create_preset_storage_action_worker,
)
self.assertIs(kwargs["load_preset_folder_state"], presets.load_preset_folder_state)
self.assertNotIn("delete_preset_item_meta", kwargs)
def test_user_presets_page_uses_worker_runtime_instead_of_manual_worker_fields(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
init_source = inspect.getsource(UserPresetsPageBase.__init__)
page_source = inspect.getsource(UserPresetsPageBase)
request_sources = (
inspect.getsource(UserPresetsPageBase._start_preset_open_folder_worker),
inspect.getsource(UserPresetsPageBase._request_preset_edit_action)
+ inspect.getsource(UserPresetsPageBase._start_preset_edit_action_worker),
inspect.getsource(UserPresetsPageBase._start_preset_bulk_action_worker),
inspect.getsource(UserPresetsPageBase._request_preset_folder_action),
inspect.getsource(UserPresetsPageBase._request_preset_storage_action)
+ inspect.getsource(UserPresetsPageBase._start_preset_storage_action_worker),
inspect.getsource(UserPresetsPageBase._request_preset_activation)
+ inspect.getsource(UserPresetsPageBase._start_preset_activation_worker),
inspect.getsource(UserPresetsPageBase._request_preset_item_action)
+ inspect.getsource(UserPresetsPageBase._start_preset_item_action_worker),
inspect.getsource(UserPresetsPageBase._request_preset_link_action),
)
self.assertIn("OneShotWorkerRuntime", init_source)
for attr in (
"_preset_activate_runtime",
"_preset_item_action_runtime",
"_preset_bulk_action_runtime",
"_preset_edit_action_runtime",
"_preset_storage_action_runtime",
"_preset_folder_action_runtime",
"_preset_open_folder_runtime",
"_preset_link_action_runtime",
):
self.assertIn(attr, init_source)
for source in request_sources:
self.assertIn("start_qthread_worker", source)
self.assertNotIn("worker.start()", source)
for attr in (
"_preset_activate_worker =",
"_preset_item_action_worker =",
"_preset_bulk_action_worker =",
"_preset_edit_action_worker =",
"_preset_storage_action_worker =",
"_preset_folder_action_worker =",
"_preset_open_folder_worker =",
"_preset_link_action_worker =",
):
self.assertNotIn(attr, page_source)
for attr in (
"_preset_activate_runtime_worker",
"_preset_item_action_runtime_worker",
"_preset_bulk_action_runtime_worker",
"_preset_edit_action_runtime_worker",
"_preset_storage_action_runtime_worker",
"_preset_folder_action_runtime_worker",
"_preset_open_folder_runtime_worker",
"_preset_link_action_runtime_worker",
):
self.assertNotIn(attr, page_source)
def test_user_presets_link_actions_queue_while_worker_runs(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
class _Runtime:
def is_running(self) -> bool:
return True
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._preset_link_action_runtime = _Runtime()
page._preset_link_action_pending = []
page.create_preset_link_action_worker = Mock()
UserPresetsPageBase._request_preset_link_action(page, "info")
UserPresetsPageBase._request_preset_link_action(page, "new_configs")
self.assertEqual(page._preset_link_action_pending, ["info", "new_configs"])
page.create_preset_link_action_worker.assert_not_called()
def test_user_presets_link_action_queue_lives_in_queued_worker_state(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
from ui.queued_worker_state import QueuedWorkerState
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
init_source = inspect.getsource(UserPresetsPageBase.__init__)
request_source = inspect.getsource(UserPresetsPageBase._request_preset_link_action)
queue_source = inspect.getsource(UserPresetsPageBase._queue_preset_link_action)
finished_source = inspect.getsource(UserPresetsPageBase._on_preset_link_action_worker_finished)
schedule_source = inspect.getsource(UserPresetsPageBase._schedule_preset_link_action_start)
run_source = inspect.getsource(UserPresetsPageBase._run_scheduled_preset_link_action_start)
cleanup_source = inspect.getsource(UserPresetsPageBase._stop_action_workers_for_cleanup)
self.assertIsInstance(UserPresetsPageBase._preset_link_action_state_obj(page), QueuedWorkerState)
self.assertIn("_preset_link_action_state = QueuedWorkerState", init_source)
self.assertIn("_preset_link_action_state_obj()", request_source)
self.assertIn("_preset_link_action_state_obj().pending", queue_source)
self.assertIn("state.pop_next()", finished_source)
self.assertIn("state.start_scheduled", schedule_source)
self.assertIn("state.start_scheduled = False", run_source)
self.assertIn("_preset_link_action_state_obj().reset()", cleanup_source)
self.assertNotIn("self._preset_link_action_pending: list", init_source)
self.assertNotIn("self._preset_link_action_start_scheduled = False", init_source)
def test_duplicate_user_presets_link_action_is_queued_once(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
class _Runtime:
def is_running(self) -> bool:
return True
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._preset_link_action_runtime = _Runtime()
page._preset_link_action_start_scheduled = False
page._preset_link_action_pending = []
page.create_preset_link_action_worker = Mock()
UserPresetsPageBase._request_preset_link_action(page, "info")
UserPresetsPageBase._request_preset_link_action(page, "info")
self.assertEqual(page._preset_link_action_pending, ["info"])
page.create_preset_link_action_worker.assert_not_called()
def test_user_presets_link_action_waits_while_restart_is_scheduled(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._preset_link_action_start_scheduled = True
page._preset_link_action_pending = []
page._preset_link_action_runtime = SimpleNamespace(is_running=Mock(return_value=False), start_qthread_worker=Mock())
UserPresetsPageBase._request_preset_link_action(page, "info")
page._preset_link_action_runtime.start_qthread_worker.assert_not_called()
self.assertEqual(page._preset_link_action_pending, ["info"])
def test_user_presets_link_worker_finished_starts_next_queued_action(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
from ui.one_shot_worker_runtime import OneShotWorkerRuntime
class _Signal:
def connect(self, _callback) -> None:
return None
class _Worker:
completed = _Signal()
failed = _Signal()
finished = _Signal()
def __init__(self) -> None:
self.start = Mock()
self.deleteLater = Mock()
def isRunning(self) -> bool:
return False
worker = _Worker()
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._cleanup_in_progress = False
page._preset_link_action_runtime = OneShotWorkerRuntime()
page._preset_link_action_request_id = 4
page._preset_link_action_pending = ["info", "new_configs"]
page.create_preset_link_action_worker = Mock(return_value=worker)
callbacks = []
with patch(
"presets.ui.common.user_presets_page.QTimer.singleShot",
side_effect=lambda _delay, callback: callbacks.append(callback),
):
UserPresetsPageBase._on_preset_link_action_worker_finished(page, SimpleNamespace(_request_id=4))
page.create_preset_link_action_worker.assert_not_called()
worker.start.assert_not_called()
self.assertEqual(len(callbacks), 1)
callbacks[0]()
page.create_preset_link_action_worker.assert_called_once_with(5, action="info")
worker.start.assert_called_once()
self.assertEqual(page._preset_link_action_pending, ["new_configs"])
def test_stale_user_presets_link_worker_finished_does_not_start_pending_action(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._cleanup_in_progress = False
page._preset_link_action_request_id = 4
page._preset_link_action_pending = ["info"]
page._schedule_preset_link_action_start = Mock(
side_effect=AssertionError("stale link worker must not start pending action")
)
UserPresetsPageBase._on_preset_link_action_worker_finished(page, SimpleNamespace(_request_id=3))
self.assertEqual(page._preset_link_action_pending, ["info"])
self.assertEqual(page._preset_link_action_request_id, 4)
def test_user_presets_link_scheduled_start_queues_next_action(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._cleanup_in_progress = False
page._preset_link_action_start_scheduled = False
page._preset_link_action_pending = []
page._request_preset_link_action = Mock()
callbacks = []
with patch(
"presets.ui.common.user_presets_page.QTimer.singleShot",
side_effect=lambda _delay, callback: callbacks.append(callback),
):
UserPresetsPageBase._schedule_preset_link_action_start(page, "info")
UserPresetsPageBase._schedule_preset_link_action_start(page, "new_configs")
self.assertEqual(len(callbacks), 1)
self.assertEqual(page._preset_link_action_pending, ["new_configs"])
page._request_preset_link_action.assert_not_called()
callbacks[0]()
page._request_preset_link_action.assert_called_once_with("info")
self.assertEqual(page._preset_link_action_pending, ["new_configs"])
def test_user_presets_link_result_is_ignored_when_new_action_is_pending(self) -> None:
from presets.ui.common import user_presets_page
from presets.ui.common.user_presets_page import UserPresetsPageBase
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._preset_link_action_request_id = 4
page._preset_link_action_pending = ["new_configs"]
page._tr = Mock(return_value="Ошибка")
page.window = Mock(return_value=object())
result = SimpleNamespace(
ok=False,
log_message="old message",
log_level="WARNING",
infobar_level="warning",
infobar_title="Old",
infobar_content="old content",
)
with (
patch.object(user_presets_page, "log") as log_mock,
patch.object(user_presets_page.InfoBar, "warning") as warning_mock,
):
UserPresetsPageBase._on_preset_link_action_finished(page, 4, "info", result, {})
log_mock.assert_not_called()
warning_mock.assert_not_called()
def test_user_presets_link_error_is_ignored_when_new_action_is_pending(self) -> None:
from presets.ui.common import user_presets_page
from presets.ui.common.user_presets_page import UserPresetsPageBase
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._preset_link_action_request_id = 4
page._preset_link_action_pending = ["new_configs"]
page._tr = Mock(return_value="Ошибка")
page.window = Mock(return_value=object())
with (
patch.object(user_presets_page, "log") as log_mock,
patch.object(user_presets_page.InfoBar, "warning") as warning_mock,
):
UserPresetsPageBase._on_preset_link_action_failed(page, 4, "info", "old error", {})
log_mock.assert_not_called()
warning_mock.assert_not_called()
def test_user_presets_open_folder_pending_restarts_later_after_worker_finished(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._cleanup_in_progress = False
page._preset_open_folder_request_id = 4
page._preset_open_folder_pending = True
page._start_preset_open_folder_worker = Mock()
callbacks = []
with patch(
"presets.ui.common.user_presets_page.QTimer.singleShot",
side_effect=lambda _delay, callback: callbacks.append(callback),
):
UserPresetsPageBase._on_preset_open_folder_worker_finished(page, SimpleNamespace(_request_id=4))
page._start_preset_open_folder_worker.assert_not_called()
self.assertEqual(len(callbacks), 1)
callbacks[0]()
page._start_preset_open_folder_worker.assert_called_once_with()
def test_stale_user_presets_open_folder_worker_finish_does_not_restart_pending_open(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._cleanup_in_progress = False
page._preset_open_folder_pending = True
page._preset_open_folder_request_id = 4
page._schedule_preset_open_folder_worker_start = Mock(
side_effect=AssertionError("stale open-folder worker must not restart pending open")
)
UserPresetsPageBase._on_preset_open_folder_worker_finished(page, SimpleNamespace(_request_id=3))
self.assertTrue(page._preset_open_folder_pending)
self.assertEqual(page._preset_open_folder_request_id, 4)
def test_user_presets_open_folder_waits_while_restart_is_scheduled(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._preset_open_folder_start_scheduled = True
page._preset_open_folder_pending = False
page._worker_runtime_is_running = Mock(return_value=False)
page._start_preset_open_folder_worker = Mock()
UserPresetsPageBase._request_preset_open_folder_action(page)
page._start_preset_open_folder_worker.assert_not_called()
self.assertTrue(page._preset_open_folder_pending)
def test_user_presets_open_folder_queue_uses_shared_latest_worker_state(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
from ui.latest_value_worker_state import LatestValueWorkerState
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._preset_open_folder_runtime = SimpleNamespace(is_running=Mock(return_value=False))
init_source = inspect.getsource(UserPresetsPageBase.__init__)
cleanup_source = inspect.getsource(UserPresetsPageBase._stop_action_workers_for_cleanup)
self.assertTrue(hasattr(UserPresetsPageBase, "_preset_open_folder_state_obj"))
self.assertIsInstance(page._preset_open_folder_state_obj(), LatestValueWorkerState)
self.assertIn("_preset_open_folder_state = LatestValueWorkerState", init_source)
self.assertIn("_preset_open_folder_state_obj().reset()", cleanup_source)
self.assertNotIn("self._preset_open_folder_pending = False", init_source)
self.assertNotIn("self._preset_open_folder_start_scheduled = False", init_source)
def test_user_presets_folder_action_pending_restarts_later_after_worker_finished(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._cleanup_in_progress = False
page._preset_folder_action_request_id = 4
page._preset_folder_action_pending = [
{
"action": "move",
"folder_key": "favorites",
"name": "Preset.txt",
"direction": 1,
"collapsed": False,
"context_extra": {"source": "menu"},
}
]
page._request_preset_folder_action = Mock()
callbacks = []
with patch(
"presets.ui.common.user_presets_page.QTimer.singleShot",
side_effect=lambda _delay, callback: callbacks.append(callback),
):
UserPresetsPageBase._on_preset_folder_action_worker_finished(page, SimpleNamespace(_request_id=4))
page._request_preset_folder_action.assert_not_called()
self.assertEqual(len(callbacks), 1)
callbacks[0]()
page._request_preset_folder_action.assert_called_once_with(
"move",
folder_key="favorites",
name="Preset.txt",
direction=1,
collapsed=False,
context_extra={"source": "menu"},
)
def test_user_presets_folder_scheduled_start_queues_next_action(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
first = {
"action": "move",
"folder_key": "favorites",
"name": "First.txt",
"direction": 1,
"collapsed": False,
"context_extra": {"source": "menu"},
}
second = {
"action": "toggle",
"folder_key": "favorites",
"name": "",
"direction": 0,
"collapsed": True,
"context_extra": {"source": "toolbar"},
}
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._cleanup_in_progress = False
page._preset_folder_action_start_scheduled = False
page._preset_folder_action_pending = []
page._request_preset_folder_action = Mock()
callbacks = []
with patch(
"presets.ui.common.user_presets_page.QTimer.singleShot",
side_effect=lambda _delay, callback: callbacks.append(callback),
):
UserPresetsPageBase._schedule_preset_folder_action_start(page, first)
UserPresetsPageBase._schedule_preset_folder_action_start(page, second)
self.assertEqual(len(callbacks), 1)
self.assertEqual(page._preset_folder_action_pending, [second])
page._request_preset_folder_action.assert_not_called()
callbacks[0]()
page._request_preset_folder_action.assert_called_once_with(
"move",
folder_key="favorites",
name="First.txt",
direction=1,
collapsed=False,
context_extra={"source": "menu"},
)
self.assertEqual(page._preset_folder_action_pending, [second])
def test_user_presets_folder_action_waits_while_restart_is_scheduled(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._preset_folder_action_start_scheduled = True
page._preset_folder_action_pending = []
page._preset_folder_action_runtime = SimpleNamespace(is_running=Mock(return_value=False), start_qthread_worker=Mock())
UserPresetsPageBase._request_preset_folder_action(
page,
"move",
folder_key="favorites",
name="Preset.txt",
direction=1,
collapsed=False,
context_extra={"source": "menu"},
)
page._preset_folder_action_runtime.start_qthread_worker.assert_not_called()
self.assertEqual(
page._preset_folder_action_pending,
[
{
"action": "move",
"folder_key": "favorites",
"name": "Preset.txt",
"direction": 1,
"collapsed": False,
"context_extra": {"source": "menu"},
}
],
)
def test_duplicate_user_presets_folder_action_keeps_each_step(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._preset_folder_action_start_scheduled = True
page._preset_folder_action_pending = []
page._preset_folder_action_runtime = SimpleNamespace(is_running=Mock(return_value=False), start_qthread_worker=Mock())
for _ in range(2):
UserPresetsPageBase._request_preset_folder_action(
page,
"move",
folder_key="favorites",
name="Preset.txt",
direction=1,
collapsed=False,
context_extra={"source": "menu"},
)
page._preset_folder_action_runtime.start_qthread_worker.assert_not_called()
self.assertEqual(
page._preset_folder_action_pending,
# Относительные шаги: два щелчка «вниз» — два шага, а не один.
[
{
"action": "move",
"folder_key": "favorites",
"name": "Preset.txt",
"direction": 1,
"collapsed": False,
"context_extra": {"source": "menu"},
},
{
"action": "move",
"folder_key": "favorites",
"name": "Preset.txt",
"direction": 1,
"collapsed": False,
"context_extra": {"source": "menu"},
}
],
)
def test_user_presets_folder_set_collapsed_keeps_latest_pending_state(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._preset_folder_action_start_scheduled = True
page._preset_folder_action_pending = []
page._preset_folder_action_runtime = SimpleNamespace(is_running=Mock(return_value=False), start_qthread_worker=Mock())
UserPresetsPageBase._request_preset_folder_action(
page,
"set_collapsed",
folder_key="games",
collapsed=True,
)
UserPresetsPageBase._request_preset_folder_action(
page,
"set_collapsed",
folder_key="games",
collapsed=False,
)
page._preset_folder_action_runtime.start_qthread_worker.assert_not_called()
self.assertEqual(
page._preset_folder_action_pending,
[
{
"action": "set_collapsed",
"folder_key": "games",
"name": "",
"direction": 0,
"collapsed": False,
"context_extra": {},
}
],
)
def test_user_presets_folder_toggle_fallback_keeps_order(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._preset_folder_action_start_scheduled = True
page._preset_folder_action_pending = []
page._preset_folder_action_runtime = SimpleNamespace(is_running=Mock(return_value=False), start_qthread_worker=Mock())
UserPresetsPageBase._request_preset_folder_action(page, "toggle_collapsed", folder_key="games")
UserPresetsPageBase._request_preset_folder_action(page, "toggle_collapsed", folder_key="games")
page._preset_folder_action_runtime.start_qthread_worker.assert_not_called()
self.assertEqual(len(page._preset_folder_action_pending), 2)
self.assertEqual(
[item["action"] for item in page._preset_folder_action_pending],
["toggle_collapsed", "toggle_collapsed"],
)
def test_user_presets_folder_action_queue_uses_shared_queued_worker_state(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
from ui.queued_worker_state import QueuedWorkerState
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._preset_folder_action_runtime = SimpleNamespace(is_running=Mock(return_value=False))
init_source = inspect.getsource(UserPresetsPageBase.__init__)
queue_source = inspect.getsource(UserPresetsPageBase._queue_preset_folder_action)
cleanup_source = inspect.getsource(UserPresetsPageBase._stop_action_workers_for_cleanup)
self.assertTrue(hasattr(UserPresetsPageBase, "_preset_folder_action_state_obj"))
self.assertIsInstance(page._preset_folder_action_state_obj(), QueuedWorkerState)
self.assertIn("_preset_folder_action_state = QueuedWorkerState", init_source)
self.assertIn("_preset_folder_action_state_obj()", queue_source)
self.assertIn("_preset_folder_action_state_obj().reset()", cleanup_source)
self.assertNotIn("self._preset_folder_action_pending: list", init_source)
self.assertNotIn("self._preset_folder_action_start_scheduled = False", init_source)
def test_user_presets_folder_click_requests_explicit_collapsed_state(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
class _Index:
def __init__(self, folder_key: str, collapsed: bool):
self._folder_key = folder_key
self._collapsed = collapsed
def isValid(self) -> bool:
return True
def data(self, role):
if role == _Model.FolderKeyRole:
return self._folder_key
if role == _Model.CollapsedRole:
return self._collapsed
return None
class _Model:
FolderKeyRole = 1
CollapsedRole = 2
def rowCount(self) -> int:
return 1
def index(self, row: int, column: int):
return _Index("games", True)
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._presets_model = _Model()
page._request_preset_folder_action = Mock()
UserPresetsPageBase._on_toggle_folder(page, "games")
page._request_preset_folder_action.assert_called_once_with(
"set_collapsed",
folder_key="games",
collapsed=False,
)
def test_user_presets_folder_click_ignores_pinned_preset_rows_of_same_folder(self) -> None:
from presets.ui.common.user_presets_page import UserPresetsPageBase
from ui.presets_menu.model import PresetListModel
model = PresetListModel()
model.set_rows(
[
{"kind": "folder", "folder_key": "pinned", "name": "Закрепленные", "is_collapsed": False, "count": 1},
{"kind": "preset", "file_name": "Default v1.txt", "name": "Default v1", "folder_key": "all-tcp-udp"},
{"kind": "folder", "folder_key": "all-tcp-udp", "name": "ALL TCP & UDP", "is_collapsed": True, "count": 12},
{"kind": "folder", "folder_key": "common", "name": "Общие", "is_collapsed": False, "count": 0},
]
)
page = UserPresetsPageBase.__new__(UserPresetsPageBase)
page._presets_model = model
page._request_preset_folder_action = Mock()
UserPresetsPageBase._on_toggle_folder(page, "all-tcp-udp")
page._request_preset_folder_action.assert_called_once_with(
"set_collapsed",
folder_key="all-tcp-udp",
collapsed=False,
)
def test_user_presets_runtime_actions_do_not_expose_mutating_preset_commands(self) -> None:
from dataclasses import fields
from presets.ui.common.user_presets_page_runtime import UserPresetsRuntimeActions
from ui.page_deps.presets import build_user_presets_page_kwargs
from app.page_names import PageName
field_names = {field.name for field in fields(UserPresetsRuntimeActions)}
forbidden = {
"create_preset",
"rename_preset_by_file_name",
"import_preset_from_file",
"reset_all_presets_to_builtin",
"duplicate_preset_by_file_name",
"reset_preset_to_builtin_by_file_name",
"delete_preset_by_file_name",
"export_preset_plain_text",
"activate_preset_file",
}
self.assertFalse(field_names & forbidden)
presets = Mock()
kwargs = build_user_presets_page_kwargs(
page_name=PageName.ZAPRET2_USER_PRESETS,
presets_feature=presets,
external_actions_feature=Mock(),
open_preset_raw_editor=Mock(),
ui_state_store=Mock(),
)
runtime_actions = kwargs["preset_runtime_actions"]
for name in forbidden:
self.assertFalse(hasattr(runtime_actions, name), name)
if __name__ == "__main__":
unittest.main()