611 lines
23 KiB
Python
611 lines
23 KiB
Python
from __future__ import annotations
|
||
|
||
import unittest
|
||
from types import SimpleNamespace
|
||
from unittest.mock import Mock, patch
|
||
|
||
|
||
class _PlainTextEditor:
|
||
def __init__(self, text: str = "") -> None:
|
||
self._text = str(text)
|
||
self.plain_text_read_calls: list[str] = []
|
||
self.plain_text_calls: list[str] = []
|
||
|
||
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
|
||
|
||
|
||
class _Button:
|
||
def __init__(self, *, enabled: bool = True) -> None:
|
||
self._enabled = bool(enabled)
|
||
self.enabled_calls: list[bool] = []
|
||
self.visible = True
|
||
|
||
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 setVisible(self, visible: bool) -> None: # noqa: N802
|
||
self.visible = bool(visible)
|
||
|
||
def isVisible(self) -> bool: # noqa: N802
|
||
return self.visible
|
||
|
||
|
||
class _Label:
|
||
def __init__(self) -> None:
|
||
self.text_value = ""
|
||
|
||
def text(self) -> str:
|
||
return self.text_value
|
||
|
||
def setText(self, text: str) -> None: # noqa: N802
|
||
self.text_value = str(text)
|
||
|
||
|
||
class _Signal:
|
||
def connect(self, _callback) -> None:
|
||
pass
|
||
|
||
|
||
class _Worker:
|
||
def __init__(self) -> None:
|
||
self.activated = _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 _RunningRuntime:
|
||
def isRunning(self) -> bool: # noqa: N802
|
||
return True
|
||
|
||
def is_running(self) -> bool:
|
||
return True
|
||
|
||
|
||
class _StartRuntime:
|
||
def is_running(self) -> bool:
|
||
return False
|
||
|
||
def start_qthread_worker(self, *, worker_factory, **_kwargs):
|
||
worker = worker_factory(0)
|
||
return 0, worker
|
||
|
||
|
||
class _FakeBreadcrumb:
|
||
def __init__(self) -> None:
|
||
self.items: list[tuple[str, str]] = []
|
||
self.clear_calls = 0
|
||
|
||
def blockSignals(self, _blocked) -> None: # noqa: N802
|
||
pass
|
||
|
||
def clear(self) -> None:
|
||
self.clear_calls += 1
|
||
self.items = []
|
||
|
||
def addItem(self, key: str, text: str) -> None: # noqa: N802
|
||
self.items.append((str(key), str(text)))
|
||
|
||
def count(self) -> int:
|
||
return len(self.items)
|
||
|
||
def setProperty(self, *_args) -> None: # noqa: N802
|
||
pass
|
||
|
||
def property(self, *_args) -> str:
|
||
return ""
|
||
|
||
|
||
class PresetSubpageUiGuardTests(unittest.TestCase):
|
||
def test_raw_preset_load_while_worker_runs_queues_latest_request(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
runtime = SimpleNamespace(
|
||
is_running=Mock(return_value=True),
|
||
start_qthread_worker=Mock(),
|
||
)
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._raw_load_runtime = runtime
|
||
page._raw_load_request_id = 2
|
||
page._raw_load_pending = False
|
||
page._raw_load_start_scheduled = False
|
||
page._is_loading = False
|
||
page._preset_file_name = "Default.txt"
|
||
page._set_footer = Mock()
|
||
page.create_raw_preset_load_worker = Mock()
|
||
|
||
PresetRawEditorPage._request_raw_preset_text(page)
|
||
|
||
self.assertEqual(page._raw_load_request_id, 3)
|
||
self.assertTrue(page._raw_load_pending)
|
||
self.assertTrue(page._is_loading)
|
||
page._set_footer.assert_called_once_with("Загрузка...")
|
||
runtime.start_qthread_worker.assert_not_called()
|
||
page.create_raw_preset_load_worker.assert_not_called()
|
||
|
||
def test_pending_raw_preset_load_restarts_after_worker_signal(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
worker = SimpleNamespace(_request_id=4)
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._cleanup_in_progress = False
|
||
page._raw_load_runtime_request_id = 4
|
||
page._raw_load_pending = True
|
||
page._raw_load_start_scheduled = False
|
||
page._request_raw_preset_text = Mock()
|
||
callbacks = []
|
||
|
||
with patch(
|
||
"presets.ui.common.preset_subpage_base.QTimer.singleShot",
|
||
side_effect=lambda _delay, callback: callbacks.append(callback),
|
||
):
|
||
PresetRawEditorPage._on_raw_preset_worker_finished(page, worker)
|
||
|
||
page._request_raw_preset_text.assert_not_called()
|
||
self.assertEqual(len(callbacks), 1)
|
||
|
||
callbacks[0]()
|
||
|
||
self.assertFalse(page._raw_load_pending)
|
||
page._request_raw_preset_text.assert_called_once_with()
|
||
|
||
def test_stale_raw_preset_load_finish_does_not_restart_pending_load(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._cleanup_in_progress = False
|
||
page._raw_load_runtime_request_id = 4
|
||
page._raw_load_pending = True
|
||
page._schedule_pending_raw_preset_load_start = Mock()
|
||
|
||
PresetRawEditorPage._on_raw_preset_worker_finished(page, SimpleNamespace(_request_id=3))
|
||
|
||
page._schedule_pending_raw_preset_load_start.assert_not_called()
|
||
|
||
def test_raw_preset_cleanup_does_not_wait_for_load_worker(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._raw_load_runtime = SimpleNamespace(stop=Mock(), cancel=Mock())
|
||
page._raw_save_runtime = SimpleNamespace(stop=Mock(), cancel=Mock())
|
||
page._raw_activate_runtime = SimpleNamespace(stop=Mock(), cancel=Mock())
|
||
page._raw_action_runtime = SimpleNamespace(stop=Mock(), cancel=Mock())
|
||
|
||
PresetRawEditorPage._stop_raw_worker_runtimes(page)
|
||
|
||
page._raw_load_runtime.stop.assert_called_once_with(
|
||
blocking=False,
|
||
warning_prefix="raw preset load worker",
|
||
)
|
||
page._raw_load_runtime.cancel.assert_called_once_with()
|
||
|
||
for runtime, prefix in (
|
||
(page._raw_save_runtime, "raw preset save worker"),
|
||
(page._raw_activate_runtime, "raw preset activate worker"),
|
||
(page._raw_action_runtime, "raw preset action worker"),
|
||
):
|
||
runtime.stop.assert_called_once_with(blocking=False, warning_prefix=prefix)
|
||
runtime.cancel.assert_called_once_with()
|
||
|
||
def test_raw_preset_load_skips_duplicate_plain_text_update(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._raw_load_request_id = 3
|
||
page._preset_file_name = "Default.txt"
|
||
page._preset_name = "Default"
|
||
page._preset_path = None
|
||
page._preset_origin = "user"
|
||
page._raw_editor_text_snapshot = "--new\n--filter-tcp=443\n"
|
||
page._is_loading = True
|
||
page.editor = _PlainTextEditor("--new\n--filter-tcp=443\n")
|
||
page._set_footer = Mock()
|
||
page._refresh_header = Mock()
|
||
callbacks = []
|
||
|
||
result = SimpleNamespace(
|
||
file_name="Default.txt",
|
||
display_name="Default",
|
||
path="C:/Zapret/Dev/presets/winws2/Default.txt",
|
||
origin="user",
|
||
text="--new\n--filter-tcp=443\n",
|
||
footer_text="Готово",
|
||
)
|
||
|
||
with patch(
|
||
"presets.ui.common.preset_subpage_base.QTimer.singleShot",
|
||
side_effect=lambda _delay, callback: callbacks.append(callback),
|
||
):
|
||
PresetRawEditorPage._on_raw_preset_text_loaded(page, 3, result)
|
||
|
||
self.assertEqual(page.editor.plain_text_calls, [])
|
||
page._set_footer.assert_not_called()
|
||
page._refresh_header.assert_not_called()
|
||
self.assertTrue(page._is_loading)
|
||
self.assertEqual(len(callbacks), 1)
|
||
|
||
callbacks[0]()
|
||
|
||
self.assertEqual(page.editor.plain_text_calls, [])
|
||
self.assertEqual(page.editor.plain_text_read_calls, [])
|
||
page._set_footer.assert_called_once_with("Готово")
|
||
page._refresh_header.assert_called_once_with()
|
||
self.assertFalse(page._is_loading)
|
||
|
||
def test_raw_preset_load_applies_editor_text_after_worker_signal(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._raw_load_request_id = 4
|
||
page._cleanup_in_progress = False
|
||
page._preset_file_name = "Default.txt"
|
||
page._preset_name = "Default"
|
||
page._preset_path = None
|
||
page._preset_origin = "user"
|
||
page._is_loading = True
|
||
page.editor = _PlainTextEditor("")
|
||
page._set_footer = Mock()
|
||
page._refresh_header = Mock()
|
||
callbacks = []
|
||
|
||
result = SimpleNamespace(
|
||
file_name="Default.txt",
|
||
display_name="Default",
|
||
path="C:/Zapret/Dev/presets/winws2/Default.txt",
|
||
origin="user",
|
||
text="--new\n--filter-tcp=443\n",
|
||
footer_text="Готово",
|
||
)
|
||
|
||
with patch(
|
||
"presets.ui.common.preset_subpage_base.QTimer.singleShot",
|
||
side_effect=lambda _delay, callback: callbacks.append(callback),
|
||
):
|
||
PresetRawEditorPage._on_raw_preset_text_loaded(page, 4, result)
|
||
|
||
self.assertEqual(page.editor.plain_text_calls, [])
|
||
page._set_footer.assert_not_called()
|
||
page._refresh_header.assert_not_called()
|
||
self.assertTrue(page._is_loading)
|
||
self.assertEqual(len(callbacks), 1)
|
||
|
||
callbacks[0]()
|
||
|
||
self.assertEqual(page.editor.plain_text_calls, [result.text])
|
||
page._set_footer.assert_called_once_with("Готово")
|
||
page._refresh_header.assert_called_once_with()
|
||
self.assertFalse(page._is_loading)
|
||
|
||
def test_raw_preset_load_updates_active_preset_state_from_worker_result(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._raw_load_request_id = 4
|
||
page._cleanup_in_progress = False
|
||
page._raw_load_pending = False
|
||
page._raw_load_start_scheduled = False
|
||
page._preset_file_name = "Default.txt"
|
||
page._preset_name = "Default"
|
||
page._preset_path = None
|
||
page._preset_origin = "user"
|
||
page._active_preset_file_name = ""
|
||
page._active_preset_name = ""
|
||
page._is_loading = True
|
||
page.editor = _PlainTextEditor("")
|
||
page._set_footer = Mock()
|
||
page._refresh_header = Mock(wraps=lambda: PresetRawEditorPage._refresh_header(page))
|
||
page._rebuild_breadcrumb = Mock()
|
||
page.statusLabel = _Label()
|
||
page.metaLabel = _Label()
|
||
page.pathLabel = _Label()
|
||
page.activateButton = _Button()
|
||
callbacks = []
|
||
|
||
result = SimpleNamespace(
|
||
file_name="Default.txt",
|
||
display_name="Default",
|
||
path="C:/Zapret/Dev/presets/winws2/Default.txt",
|
||
origin="user",
|
||
text="--new\n",
|
||
footer_text="Готово",
|
||
active_file_name="Default.txt",
|
||
active_name="Default",
|
||
)
|
||
|
||
with patch(
|
||
"presets.ui.common.preset_subpage_base.QTimer.singleShot",
|
||
side_effect=lambda _delay, callback: callbacks.append(callback),
|
||
):
|
||
PresetRawEditorPage._on_raw_preset_text_loaded(page, 4, result)
|
||
|
||
callbacks[0]()
|
||
|
||
self.assertEqual(page._active_preset_file_name, "Default.txt")
|
||
self.assertEqual(page._active_preset_name, "Default")
|
||
self.assertFalse(page.activateButton.visible)
|
||
self.assertEqual(page.statusLabel.text_value, "Активный пресет")
|
||
|
||
def test_pending_raw_text_apply_is_ignored_after_new_load_is_requested(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
runtime = SimpleNamespace(
|
||
is_running=Mock(return_value=True),
|
||
start_qthread_worker=Mock(),
|
||
)
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._raw_load_runtime = runtime
|
||
page._raw_load_request_id = 4
|
||
page._raw_load_pending = False
|
||
page._raw_load_start_scheduled = False
|
||
page._raw_text_apply_scheduled = False
|
||
page._pending_raw_text_apply = None
|
||
page._cleanup_in_progress = False
|
||
page._preset_file_name = "Old.txt"
|
||
page._preset_name = "Old"
|
||
page._preset_path = None
|
||
page._preset_origin = "user"
|
||
page._is_loading = True
|
||
page.editor = _PlainTextEditor("")
|
||
page._set_footer = Mock()
|
||
page._refresh_header = Mock()
|
||
callbacks = []
|
||
|
||
result = SimpleNamespace(
|
||
file_name="Old.txt",
|
||
display_name="Old",
|
||
path="C:/Zapret/Dev/presets/winws2/Old.txt",
|
||
origin="user",
|
||
text="--new\n--filter-tcp=443\n",
|
||
footer_text="Готово",
|
||
)
|
||
|
||
with patch(
|
||
"presets.ui.common.preset_subpage_base.QTimer.singleShot",
|
||
side_effect=lambda _delay, callback: callbacks.append(callback),
|
||
):
|
||
PresetRawEditorPage._on_raw_preset_text_loaded(page, 4, result)
|
||
|
||
PresetRawEditorPage._request_raw_preset_text(page)
|
||
|
||
self.assertTrue(page._raw_load_pending)
|
||
self.assertEqual(page._raw_load_request_id, 5)
|
||
self.assertEqual(len(callbacks), 1)
|
||
|
||
callbacks[0]()
|
||
|
||
self.assertEqual(page.editor.plain_text_calls, [])
|
||
page._refresh_header.assert_not_called()
|
||
self.assertTrue(page._is_loading)
|
||
|
||
def test_pending_raw_preset_load_ignores_old_load_error(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._raw_load_request_id = 5
|
||
page._raw_load_pending = True
|
||
page._raw_load_start_scheduled = False
|
||
page._is_loading = True
|
||
page._set_footer = Mock()
|
||
|
||
PresetRawEditorPage._on_raw_preset_text_failed(page, 5, "old error")
|
||
|
||
page._set_footer.assert_not_called()
|
||
self.assertTrue(page._is_loading)
|
||
|
||
def test_raw_preset_activation_skips_duplicate_button_disable(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
worker = _Worker()
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._raw_activate_request_id = 0
|
||
page._preset_file_name = "Default.txt"
|
||
page.activateButton = _Button(enabled=False)
|
||
page.create_raw_preset_activate_worker = Mock(return_value=worker)
|
||
|
||
PresetRawEditorPage._request_preset_activation(page)
|
||
|
||
self.assertEqual(page.activateButton.enabled_calls, [])
|
||
page.create_raw_preset_activate_worker.assert_called_once_with(1, "Default.txt", page)
|
||
self.assertEqual(worker.start_calls, 1)
|
||
|
||
def test_raw_preset_save_while_worker_runs_defers_editor_read(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._cleanup_in_progress = False
|
||
page._preset_path = object()
|
||
page._preset_file_name = "Default.txt"
|
||
page._raw_save_runtime = _RunningRuntime()
|
||
page._pending_raw_preset_save = None
|
||
page.editor = _PlainTextEditor("--new\n--filter-tcp=443\n")
|
||
|
||
self.assertTrue(PresetRawEditorPage._save_file(page, publish_content_changed=True))
|
||
|
||
self.assertEqual(page.editor.plain_text_read_calls, [])
|
||
self.assertEqual(page._pending_raw_preset_save, ("Default.txt", None, True))
|
||
|
||
def test_pending_raw_preset_save_restarts_after_worker_signal(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
worker = object()
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._cleanup_in_progress = False
|
||
page._pending_raw_preset_save = ("Default.txt", None, True)
|
||
page._after_raw_preset_save = None
|
||
page._raw_save_runtime = _StartRuntime()
|
||
page._raw_save_request_id = 1
|
||
page._raw_save_succeeded = True
|
||
page._raw_editor_text_snapshot = "--new\n--filter-tcp=443\n"
|
||
page._set_footer = Mock()
|
||
page.editor = _PlainTextEditor("--new\n--filter-tcp=443\n")
|
||
page.create_raw_preset_save_worker = Mock(return_value=worker)
|
||
callbacks = []
|
||
|
||
with patch(
|
||
"presets.ui.common.preset_subpage_base.QTimer.singleShot",
|
||
side_effect=lambda _delay, callback: callbacks.append(callback),
|
||
):
|
||
PresetRawEditorPage._on_raw_preset_save_worker_finished(page, SimpleNamespace(_request_id=1))
|
||
|
||
page.create_raw_preset_save_worker.assert_not_called()
|
||
self.assertEqual(page.editor.plain_text_read_calls, [])
|
||
self.assertEqual(len(callbacks), 1)
|
||
|
||
callbacks[0]()
|
||
|
||
self.assertEqual(page.editor.plain_text_read_calls, [])
|
||
page.create_raw_preset_save_worker.assert_called_once_with(
|
||
2,
|
||
file_name="Default.txt",
|
||
source_text="--new\n--filter-tcp=443\n",
|
||
publish_content_changed=True,
|
||
parent=page,
|
||
)
|
||
|
||
def test_raw_preset_save_uses_snapshot_when_editor_is_unchanged(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._raw_editor_text_snapshot = "--new\n--filter-tcp=443\n"
|
||
page.editor = _PlainTextEditor("--new\n--filter-tcp=443\n")
|
||
|
||
text = PresetRawEditorPage._resolve_raw_preset_save_text(page, None)
|
||
|
||
self.assertEqual(text, "--new\n--filter-tcp=443\n")
|
||
self.assertEqual(page.editor.plain_text_read_calls, [])
|
||
|
||
def test_raw_preset_save_reads_editor_when_snapshot_is_invalidated(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._raw_editor_text_snapshot = None
|
||
page.editor = _PlainTextEditor("--new\n--filter-tcp=443\n")
|
||
|
||
text = PresetRawEditorPage._resolve_raw_preset_save_text(page, None)
|
||
|
||
self.assertEqual(text, "--new\n--filter-tcp=443\n")
|
||
self.assertEqual(page._raw_editor_text_snapshot, "--new\n--filter-tcp=443\n")
|
||
self.assertEqual(page.editor.plain_text_read_calls, ["--new\n--filter-tcp=443\n"])
|
||
|
||
def test_raw_preset_save_after_text_change_reads_editor_once(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
# После правки (textChanged инвалидирует мемо) сохранение перечитывает
|
||
# живой текст из редактора ровно один раз — инкрементального кэша по
|
||
# contentsChange больше нет, он молча терял вставленный текст.
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._cleanup_in_progress = False
|
||
page._is_loading = False
|
||
page._raw_editor_text_snapshot = "--new\nold\n"
|
||
page._raw_save_runtime = _StartRuntime()
|
||
page._raw_save_request_id = 0
|
||
page._raw_save_succeeded = True
|
||
page._save_timer = Mock()
|
||
page._commit_timer = Mock()
|
||
page._set_footer = Mock()
|
||
page.editor = _PlainTextEditor("--new\nlatest\n")
|
||
page.create_raw_preset_save_worker = Mock(return_value=object())
|
||
|
||
PresetRawEditorPage._on_text_changed(page)
|
||
PresetRawEditorPage._start_raw_preset_save_worker(
|
||
page,
|
||
file_name="Default.txt",
|
||
source_text=None,
|
||
publish_content_changed=True,
|
||
)
|
||
|
||
self.assertEqual(page.editor.plain_text_read_calls, ["--new\nlatest\n"])
|
||
page.create_raw_preset_save_worker.assert_called_once_with(
|
||
1,
|
||
file_name="Default.txt",
|
||
source_text="--new\nlatest\n",
|
||
publish_content_changed=True,
|
||
parent=page,
|
||
)
|
||
|
||
def test_rebuild_breadcrumb_restores_items_after_click_truncation(self) -> None:
|
||
# Клик по крошке заставляет BreadcrumbBar удалить элементы правее
|
||
# выбранного, при этом текстовый ключ пути не меняется. Перестройка
|
||
# обязана восстановить все крошки, а не скипаться по мемо-ключу.
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
breadcrumb = _FakeBreadcrumb()
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._breadcrumb = breadcrumb
|
||
page._preset_name = "Мой пресет"
|
||
|
||
PresetRawEditorPage._rebuild_breadcrumb(page)
|
||
self.assertEqual(breadcrumb.count(), 3)
|
||
clear_calls_after_build = breadcrumb.clear_calls
|
||
|
||
# Повторный вызов с тем же ключом и полным баром — без перестройки.
|
||
PresetRawEditorPage._rebuild_breadcrumb(page)
|
||
self.assertEqual(breadcrumb.clear_calls, clear_calls_after_build)
|
||
|
||
# Симулируем клик по корневой крошке: остаётся только первый элемент.
|
||
breadcrumb.items = breadcrumb.items[:1]
|
||
PresetRawEditorPage._rebuild_breadcrumb(page)
|
||
|
||
self.assertEqual(breadcrumb.count(), 3)
|
||
self.assertEqual([key for key, _text in breadcrumb.items], ["root", "list", "raw_preset"])
|
||
|
||
def test_breadcrumb_item_click_restores_crumbs_and_navigates(self) -> None:
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
breadcrumb = _FakeBreadcrumb()
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._breadcrumb = breadcrumb
|
||
page._preset_name = "Мой пресет"
|
||
page._open_root_callback = Mock()
|
||
page._open_back_callback = Mock()
|
||
|
||
PresetRawEditorPage._rebuild_breadcrumb(page)
|
||
breadcrumb.items = breadcrumb.items[:1]
|
||
|
||
PresetRawEditorPage._on_breadcrumb_item_changed(page, "root")
|
||
|
||
self.assertEqual(breadcrumb.count(), 3)
|
||
page._open_root_callback.assert_called_once()
|
||
page._open_back_callback.assert_not_called()
|
||
|
||
def test_status_message_update_skips_runtime_toggle_render(self) -> None:
|
||
from app.state_store import AppUiState
|
||
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
||
|
||
page = PresetRawEditorPage.__new__(PresetRawEditorPage)
|
||
page._cleanup_in_progress = False
|
||
page._render_runtime_toggle = Mock(
|
||
side_effect=AssertionError("status message must not repaint runtime toggle")
|
||
)
|
||
page._render_footer_status = Mock()
|
||
|
||
PresetRawEditorPage._on_ui_state_changed(
|
||
page,
|
||
AppUiState(last_status_message="Запущено"),
|
||
frozenset({"last_status_message"}),
|
||
)
|
||
|
||
page._render_runtime_toggle.assert_not_called()
|
||
page._render_footer_status.assert_called_once()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|