816 lines
32 KiB
Python
816 lines
32 KiB
Python
from __future__ import annotations
|
|
|
|
import inspect
|
|
import os
|
|
import unittest
|
|
from types import SimpleNamespace
|
|
from unittest.mock import Mock, patch
|
|
|
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
|
|
from settings.mode import ZAPRET1_MODE, ZAPRET2_MODE
|
|
|
|
|
|
class _RuntimeService:
|
|
def __init__(self) -> None:
|
|
self.busy_calls: list[tuple[bool, str]] = []
|
|
|
|
def set_busy(self, busy: bool, text: str = "") -> bool:
|
|
self.busy_calls.append((bool(busy), str(text or "")))
|
|
return True
|
|
|
|
def snapshot(self):
|
|
return SimpleNamespace(launch_method=ZAPRET2_MODE)
|
|
|
|
|
|
class _StoppedRuntimeOwner:
|
|
def __init__(self) -> None:
|
|
self._presets_switch_method = ""
|
|
self._presets_switch_requested_generation = 0
|
|
self._presets_switch_completed_generation = 0
|
|
self._presets_switch_thread = None
|
|
self._dpi_start_thread = None
|
|
self._dpi_stop_thread = None
|
|
self._runtime_service_obj = _RuntimeService()
|
|
|
|
def _runtime_service(self):
|
|
return self._runtime_service_obj
|
|
|
|
def is_running(self) -> bool:
|
|
return False
|
|
|
|
|
|
class _RunningRuntimeOwner(_StoppedRuntimeOwner):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.pending_switch_calls = 0
|
|
self._presets_switch_worker = SimpleNamespace(started_pid=4321)
|
|
self.running_pid_calls: list[int | None] = []
|
|
|
|
def is_running(self) -> bool:
|
|
return True
|
|
|
|
def _process_pending_presets_switch(self) -> None:
|
|
self.pending_switch_calls += 1
|
|
|
|
def _mark_runtime_running(self, *, pid=None) -> None:
|
|
self.running_pid_calls.append(pid)
|
|
|
|
|
|
class _RuntimeToggleButton:
|
|
def __init__(self) -> None:
|
|
self.text_calls: list[str] = []
|
|
self.icon_calls: list[object] = []
|
|
self.enabled_calls: list[bool] = []
|
|
|
|
def setText(self, text: str) -> None: # noqa: N802
|
|
self.text_calls.append(str(text))
|
|
|
|
def setIcon(self, icon) -> None: # noqa: N802
|
|
self.icon_calls.append(icon)
|
|
|
|
def setEnabled(self, enabled: bool) -> None: # noqa: N802
|
|
self.enabled_calls.append(bool(enabled))
|
|
|
|
|
|
class _TextWidget:
|
|
def __init__(self, text: str = "") -> None:
|
|
self._text = str(text)
|
|
self.text_calls: list[str] = []
|
|
|
|
def text(self) -> str:
|
|
return self._text
|
|
|
|
def setText(self, text: str) -> None: # noqa: N802
|
|
value = str(text)
|
|
self.text_calls.append(value)
|
|
self._text = value
|
|
|
|
|
|
class _VisibleWidget:
|
|
def __init__(self, visible: bool = True) -> None:
|
|
self._visible = bool(visible)
|
|
self.visible_calls: list[bool] = []
|
|
|
|
def isVisible(self) -> bool: # noqa: N802
|
|
return self._visible
|
|
|
|
def setVisible(self, visible: bool) -> None: # noqa: N802
|
|
value = bool(visible)
|
|
self.visible_calls.append(value)
|
|
self._visible = value
|
|
|
|
|
|
class _StyleWidget:
|
|
def __init__(self, style: str = "") -> None:
|
|
self._style = str(style)
|
|
self.style_calls: list[str] = []
|
|
|
|
def styleSheet(self) -> str: # noqa: N802
|
|
return self._style
|
|
|
|
def setStyleSheet(self, style: str) -> None: # noqa: N802
|
|
value = str(style)
|
|
self.style_calls.append(value)
|
|
self._style = value
|
|
|
|
|
|
class _SpinnerWidget:
|
|
def __init__(self) -> None:
|
|
self.started = 0
|
|
self.stopped = 0
|
|
self.hidden = 0
|
|
|
|
def start(self) -> None:
|
|
self.started += 1
|
|
|
|
def stop(self) -> None:
|
|
self.stopped += 1
|
|
|
|
def hide(self) -> None:
|
|
self.hidden += 1
|
|
|
|
|
|
class _PulseWidget(_VisibleWidget):
|
|
def __init__(self, visible: bool = True) -> None:
|
|
super().__init__(visible)
|
|
self.started = 0
|
|
self.stopped = 0
|
|
self.hidden = 0
|
|
self.colors: list[str] = []
|
|
|
|
def start_pulse(self) -> None:
|
|
self.started += 1
|
|
|
|
def stop_pulse(self) -> None:
|
|
self.stopped += 1
|
|
|
|
def hide(self) -> None:
|
|
self.hidden += 1
|
|
|
|
def set_color(self, color: str) -> None:
|
|
self.colors.append(str(color))
|
|
|
|
|
|
class _BreadcrumbWidget:
|
|
def __init__(self) -> None:
|
|
self.block_calls: list[bool] = []
|
|
self.clear_calls = 0
|
|
self.items: list[tuple[str, str]] = []
|
|
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.clear_calls += 1
|
|
self.items.clear()
|
|
|
|
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 accessibleName(self) -> str: # noqa: N802
|
|
return self._accessible_name
|
|
|
|
def setAccessibleName(self, text: str) -> None: # noqa: N802
|
|
self._accessible_name = str(text)
|
|
|
|
def accessibleDescription(self) -> str: # noqa: N802
|
|
return self._accessible_description
|
|
|
|
def setAccessibleDescription(self, text: str) -> None: # noqa: N802
|
|
self._accessible_description = str(text)
|
|
|
|
def property(self, name: str): # noqa: A003
|
|
return self._properties.get(str(name))
|
|
|
|
def setProperty(self, name: str, value) -> None: # noqa: N802
|
|
self._properties[str(name)] = value
|
|
|
|
|
|
class PresetStatusBarPlanTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
from PyQt6.QtWidgets import QApplication
|
|
|
|
cls._app = QApplication.instance() or QApplication([])
|
|
|
|
def test_spinner_import_is_static_for_packaged_build(self) -> None:
|
|
import presets.ui.common.preset_status_bar as preset_status_bar
|
|
|
|
source = inspect.getsource(preset_status_bar)
|
|
|
|
self.assertIn("from ui.widgets.win11_spinner import Win11Spinner", source)
|
|
self.assertNotIn("from ui.widgets import Win11Spinner", source)
|
|
|
|
def test_preset_header_text_update_skips_duplicate_text(self) -> None:
|
|
from presets.ui.common.preset_subpage_base import set_text_if_changed
|
|
|
|
widget = _TextWidget("Активный пресет")
|
|
|
|
self.assertFalse(set_text_if_changed(widget, "Активный пресет"))
|
|
self.assertEqual(widget.text_calls, [])
|
|
|
|
self.assertTrue(set_text_if_changed(widget, "Пользовательский пресет"))
|
|
self.assertEqual(widget.text_calls, ["Пользовательский пресет"])
|
|
|
|
def test_preset_header_visibility_update_skips_duplicate_state(self) -> None:
|
|
from presets.ui.common.preset_subpage_base import set_visible_if_changed
|
|
|
|
widget = _VisibleWidget(False)
|
|
|
|
self.assertFalse(set_visible_if_changed(widget, False))
|
|
self.assertEqual(widget.visible_calls, [])
|
|
|
|
self.assertTrue(set_visible_if_changed(widget, True))
|
|
self.assertEqual(widget.visible_calls, [True])
|
|
|
|
def test_preset_breadcrumb_rebuild_skips_duplicate_items(self) -> None:
|
|
from presets.ui.common.preset_subpage_base import PresetRawEditorPage
|
|
|
|
breadcrumb = _BreadcrumbWidget()
|
|
page = SimpleNamespace(
|
|
_breadcrumb=breadcrumb,
|
|
_breadcrumb_root_text=lambda: "Управление",
|
|
_breadcrumb_parent_text=lambda: "Мои пресеты",
|
|
_breadcrumb_current_text=lambda: "Default",
|
|
)
|
|
|
|
PresetRawEditorPage._rebuild_breadcrumb(page)
|
|
PresetRawEditorPage._rebuild_breadcrumb(page)
|
|
|
|
self.assertEqual(breadcrumb.clear_calls, 1)
|
|
self.assertEqual(
|
|
breadcrumb.items,
|
|
[
|
|
("root", "Управление"),
|
|
("list", "Мои пресеты"),
|
|
("raw_preset", "Default"),
|
|
],
|
|
)
|
|
self.assertEqual(
|
|
breadcrumb.property("screenReaderStateText"),
|
|
"Навигация: Управление > Мои пресеты > Default",
|
|
)
|
|
self.assertEqual(
|
|
breadcrumb.accessibleDescription(),
|
|
"Показывает путь до текущей страницы. Выберите пункт, чтобы вернуться назад.",
|
|
)
|
|
|
|
def test_loaded_status_uses_success_pulse_and_clear_text(self) -> None:
|
|
from presets.ui.common.preset_status_bar import build_preset_status_plan
|
|
|
|
plan = build_preset_status_plan("loaded", launch_method=ZAPRET2_MODE)
|
|
|
|
self.assertEqual(plan.text, "Пресет загружен")
|
|
self.assertEqual(plan.mode, "success")
|
|
self.assertEqual(plan.indicator, "pulse")
|
|
|
|
def test_status_bar_theme_refresh_skips_duplicate_text(self) -> None:
|
|
from unittest.mock import Mock, patch
|
|
|
|
from presets.ui.common.preset_status_bar import PresetStatusBar, PresetStatusPlan
|
|
|
|
plan = PresetStatusPlan("Пресет применён", "success", "pulse")
|
|
bar = PresetStatusBar.__new__(PresetStatusBar)
|
|
bar._last_plan = plan
|
|
bar._last_theme_key = ("#5caee8", False)
|
|
bar._last_indicator = "pulse"
|
|
bar.spinner = _SpinnerWidget()
|
|
bar.pulse_dot = _PulseWidget(True)
|
|
bar.text_label = _TextWidget("Пресет применён")
|
|
bar._apply_mode_style = Mock()
|
|
|
|
with patch("presets.ui.common.preset_status_bar._status_theme_key", return_value=("#8cc63f", False)):
|
|
PresetStatusBar.set_plan(bar, plan)
|
|
|
|
self.assertEqual(bar.text_label.text_calls, [])
|
|
bar._apply_mode_style.assert_called_once_with("success")
|
|
|
|
def test_switch_preset_sets_busy_status_until_runtime_finishes_or_skips(self) -> None:
|
|
from winws_runtime.runtime.restart_flow import switch_presets_async
|
|
|
|
owner = _StoppedRuntimeOwner()
|
|
|
|
switch_presets_async(owner, ZAPRET2_MODE)
|
|
|
|
self.assertEqual(
|
|
owner._runtime_service_obj.busy_calls,
|
|
[(True, "Применяем пресет..."), (False, "")],
|
|
)
|
|
|
|
def test_debounced_preset_switch_coalesces_multiple_apply_requests(self) -> None:
|
|
from unittest.mock import patch
|
|
from winws_runtime.runtime.restart_flow import switch_presets_async
|
|
|
|
class _Signal:
|
|
def __init__(self) -> None:
|
|
self.callback = None
|
|
|
|
def connect(self, callback) -> None:
|
|
self.callback = callback
|
|
|
|
class _FakeTimer:
|
|
instances = []
|
|
|
|
def __init__(self) -> None:
|
|
self.timeout = _Signal()
|
|
self.start_count = 0
|
|
self.delay_ms = None
|
|
self.active = False
|
|
_FakeTimer.instances.append(self)
|
|
|
|
def setSingleShot(self, _single_shot: bool) -> None:
|
|
pass
|
|
|
|
def start(self, delay_ms: int) -> None:
|
|
self.start_count += 1
|
|
self.delay_ms = delay_ms
|
|
self.active = True
|
|
|
|
def stop(self) -> None:
|
|
self.active = False
|
|
|
|
def isActive(self) -> bool:
|
|
return self.active
|
|
|
|
def fire(self) -> None:
|
|
self.active = False
|
|
self.timeout.callback()
|
|
|
|
owner = _StoppedRuntimeOwner()
|
|
|
|
with patch("winws_runtime.runtime.restart_flow.QTimer", _FakeTimer):
|
|
switch_presets_async(owner, ZAPRET2_MODE, delay_ms=900)
|
|
switch_presets_async(owner, ZAPRET2_MODE, delay_ms=900)
|
|
|
|
self.assertEqual(owner._presets_switch_requested_generation, 0)
|
|
self.assertEqual(len(_FakeTimer.instances), 1)
|
|
self.assertEqual(_FakeTimer.instances[0].start_count, 2)
|
|
self.assertEqual(_FakeTimer.instances[0].delay_ms, 900)
|
|
|
|
_FakeTimer.instances[0].fire()
|
|
|
|
self.assertEqual(owner._presets_switch_requested_generation, 1)
|
|
self.assertEqual(
|
|
owner._runtime_service_obj.busy_calls,
|
|
[(True, "Применяем пресет..."), (False, "")],
|
|
)
|
|
|
|
def test_stale_preset_switch_finish_does_not_clear_busy_or_status(self) -> None:
|
|
from unittest.mock import patch
|
|
from winws_runtime.runtime.restart_flow import handle_presets_switch_finished
|
|
|
|
owner = _RunningRuntimeOwner()
|
|
owner._presets_switch_requested_generation = 7
|
|
owner._presets_switch_completed_generation = 5
|
|
|
|
with (
|
|
patch("winws_runtime.runtime.restart_flow.QTimer.singleShot") as single_shot,
|
|
patch("winws_runtime.runtime.restart_flow.set_runtime_owner_status") as set_status,
|
|
patch("winws_runtime.runtime.restart_flow.maybe_restart_discord_after_runtime_apply") as maybe_restart,
|
|
):
|
|
single_shot.side_effect = lambda _delay, callback: callback()
|
|
|
|
handle_presets_switch_finished(owner, True, "", 6, ZAPRET2_MODE, False)
|
|
|
|
self.assertEqual(owner._runtime_service_obj.busy_calls, [])
|
|
self.assertEqual(owner._presets_switch_completed_generation, 6)
|
|
self.assertEqual(owner.pending_switch_calls, 1)
|
|
single_shot.assert_called_once()
|
|
set_status.assert_not_called()
|
|
maybe_restart.assert_not_called()
|
|
|
|
def test_stale_preset_switch_worker_skips_snapshot_before_fast_switch(self) -> None:
|
|
from winws_runtime.runtime.control_workers import PresetSwitchWorker
|
|
|
|
presets_feature = SimpleNamespace(
|
|
get_launch_snapshot=Mock(
|
|
side_effect=AssertionError("stale preset switch must not prepare launch snapshot")
|
|
)
|
|
)
|
|
worker = PresetSwitchWorker(
|
|
presets_feature,
|
|
ZAPRET2_MODE,
|
|
6,
|
|
lambda generation: int(generation) == 7,
|
|
)
|
|
finished: list[tuple[bool, str, int, str, bool]] = []
|
|
progress: list[str] = []
|
|
worker.finished.connect(
|
|
lambda success, error, generation, launch_method, skipped: finished.append(
|
|
(bool(success), str(error), int(generation), str(launch_method), bool(skipped))
|
|
)
|
|
)
|
|
worker.progress.connect(lambda message: progress.append(str(message)))
|
|
|
|
worker.run()
|
|
|
|
presets_feature.get_launch_snapshot.assert_not_called()
|
|
self.assertEqual(finished, [(True, "", 6, ZAPRET2_MODE, True)])
|
|
self.assertEqual(progress, [])
|
|
|
|
def test_current_preset_switch_finish_clears_busy_and_sets_status(self) -> None:
|
|
from unittest.mock import patch
|
|
from winws_runtime.runtime.restart_flow import handle_presets_switch_finished
|
|
|
|
owner = _RunningRuntimeOwner()
|
|
owner._presets_switch_requested_generation = 7
|
|
owner._presets_switch_completed_generation = 6
|
|
|
|
with (
|
|
patch("winws_runtime.runtime.restart_flow.QTimer.singleShot") as single_shot,
|
|
patch("winws_runtime.runtime.restart_flow.set_runtime_owner_status") as set_status,
|
|
patch("winws_runtime.runtime.restart_flow.maybe_restart_discord_after_runtime_apply") as maybe_restart,
|
|
):
|
|
handle_presets_switch_finished(owner, True, "", 7, ZAPRET2_MODE, False)
|
|
|
|
self.assertEqual(owner._runtime_service_obj.busy_calls, [(False, "")])
|
|
self.assertEqual(owner._presets_switch_completed_generation, 7)
|
|
self.assertEqual(owner.running_pid_calls, [4321])
|
|
single_shot.assert_not_called()
|
|
set_status.assert_called_once_with(owner, "✅ Пресет успешно применён")
|
|
maybe_restart.assert_called_once_with(owner, skip_first_start=False)
|
|
|
|
def test_selected_status_names_winws_when_process_is_not_running(self) -> None:
|
|
from presets.ui.common.preset_status_bar import build_preset_status_plan
|
|
|
|
winws2_plan = build_preset_status_plan("selected_stopped", launch_method=ZAPRET2_MODE)
|
|
winws1_plan = build_preset_status_plan("selected_stopped", launch_method=ZAPRET1_MODE)
|
|
|
|
self.assertEqual(winws2_plan.text, "Пресет выбран, winws2 не запущен")
|
|
self.assertEqual(winws1_plan.text, "Пресет выбран, winws не запущен")
|
|
self.assertEqual(winws2_plan.mode, "stopped")
|
|
self.assertEqual(winws1_plan.mode, "stopped")
|
|
self.assertEqual(winws2_plan.indicator, "dot")
|
|
self.assertEqual(winws1_plan.indicator, "dot")
|
|
|
|
def test_selected_stopped_status_shows_static_red_dot(self) -> None:
|
|
from presets.ui.common.preset_status_bar import PresetStatusBar, build_preset_status_plan
|
|
|
|
status_bar = PresetStatusBar()
|
|
self.addCleanup(status_bar.deleteLater)
|
|
|
|
status_bar.set_plan(build_preset_status_plan("selected_stopped", launch_method=ZAPRET2_MODE))
|
|
|
|
self.assertFalse(status_bar.pulse_dot.isHidden())
|
|
self.assertFalse(status_bar.pulse_dot._is_pulsing)
|
|
self.assertEqual(status_bar.pulse_dot._color.name().lower(), "#d83b01")
|
|
|
|
def test_status_bar_dot_exposes_preset_state_text(self) -> None:
|
|
from presets.ui.common.preset_status_bar import PresetStatusBar, build_preset_status_plan
|
|
|
|
status_bar = PresetStatusBar()
|
|
self.addCleanup(status_bar.deleteLater)
|
|
|
|
status_bar.set_plan(build_preset_status_plan("selected_stopped", launch_method=ZAPRET2_MODE))
|
|
|
|
self.assertEqual(
|
|
status_bar.pulse_dot.property("screenReaderStateText"),
|
|
"Статус пресета: Пресет выбран, winws2 не запущен",
|
|
)
|
|
self.assertEqual(
|
|
status_bar.pulse_dot.accessibleName(),
|
|
"Статус пресета: Пресет выбран, winws2 не запущен",
|
|
)
|
|
|
|
def test_title_status_icon_selected_stopped_shows_static_red_dot(self) -> None:
|
|
from presets.ui.common.preset_status_bar import PresetStatusIcon, build_preset_status_plan
|
|
|
|
icon = PresetStatusIcon(size=24)
|
|
self.addCleanup(icon.deleteLater)
|
|
|
|
icon.set_plan(build_preset_status_plan("selected_stopped", launch_method=ZAPRET2_MODE))
|
|
|
|
self.assertTrue(icon.pulse_dot.isVisible())
|
|
self.assertFalse(icon.pulse_dot._is_pulsing)
|
|
self.assertEqual(icon.pulse_dot._color.name().lower(), "#d83b01")
|
|
|
|
def test_title_status_icon_dot_exposes_preset_state_text(self) -> None:
|
|
from presets.ui.common.preset_status_bar import PresetStatusIcon, build_preset_status_plan
|
|
|
|
icon = PresetStatusIcon(size=24)
|
|
self.addCleanup(icon.deleteLater)
|
|
|
|
icon.set_plan(build_preset_status_plan("applied", launch_method=ZAPRET2_MODE))
|
|
|
|
self.assertEqual(
|
|
icon.pulse_dot.property("screenReaderStateText"),
|
|
"Статус пресета: Пресет применён",
|
|
)
|
|
self.assertEqual(icon.pulse_dot.accessibleName(), "Статус пресета: Пресет применён")
|
|
|
|
def test_applying_status_uses_spinner_and_requested_text(self) -> None:
|
|
from presets.ui.common.preset_status_bar import build_preset_status_plan
|
|
|
|
plan = build_preset_status_plan("applying", launch_method=ZAPRET2_MODE)
|
|
|
|
self.assertEqual(plan.text, "Применяем пресет...")
|
|
self.assertEqual(plan.mode, "busy")
|
|
self.assertEqual(plan.indicator, "spinner")
|
|
|
|
def test_status_bar_spinner_exposes_preset_state_text(self) -> None:
|
|
from presets.ui.common.preset_status_bar import PresetStatusBar, build_preset_status_plan
|
|
|
|
status_bar = PresetStatusBar()
|
|
self.addCleanup(status_bar.deleteLater)
|
|
|
|
status_bar.set_plan(build_preset_status_plan("applying", launch_method=ZAPRET2_MODE))
|
|
|
|
self.assertEqual(
|
|
status_bar.spinner.property("screenReaderStateText"),
|
|
"Статус пресета: Применяем пресет...",
|
|
)
|
|
self.assertEqual(status_bar.spinner.accessibleName(), "Статус пресета: Применяем пресет...")
|
|
|
|
def test_title_status_icon_spinner_exposes_preset_state_text(self) -> None:
|
|
from presets.ui.common.preset_status_bar import PresetStatusIcon, build_preset_status_plan
|
|
|
|
icon = PresetStatusIcon(size=24)
|
|
self.addCleanup(icon.deleteLater)
|
|
|
|
icon.set_plan(build_preset_status_plan("applying", launch_method=ZAPRET2_MODE))
|
|
|
|
self.assertEqual(
|
|
icon.spinner.property("screenReaderStateText"),
|
|
"Статус пресета: Применяем пресет...",
|
|
)
|
|
self.assertEqual(icon.spinner.accessibleName(), "Статус пресета: Применяем пресет...")
|
|
|
|
def test_runtime_state_overrides_loaded_text_while_preset_switch_is_busy(self) -> None:
|
|
from presets.ui.common.preset_status_bar import build_runtime_preset_status_plan
|
|
|
|
plan = build_runtime_preset_status_plan(
|
|
base_status="loaded",
|
|
launch_method=ZAPRET2_MODE,
|
|
runtime_launch_method=ZAPRET2_MODE,
|
|
launch_busy=True,
|
|
launch_busy_text="Применяем пресет...",
|
|
last_status_message="",
|
|
)
|
|
|
|
self.assertEqual(plan.text, "Применяем пресет...")
|
|
self.assertEqual(plan.indicator, "spinner")
|
|
|
|
def test_runtime_state_keeps_applied_status_after_success_message(self) -> None:
|
|
from presets.ui.common.preset_status_bar import build_runtime_preset_status_plan
|
|
|
|
plan = build_runtime_preset_status_plan(
|
|
base_status="loaded",
|
|
launch_method=ZAPRET2_MODE,
|
|
runtime_launch_method=ZAPRET2_MODE,
|
|
launch_busy=False,
|
|
launch_busy_text="",
|
|
last_status_message="Пресет успешно применён",
|
|
)
|
|
|
|
self.assertEqual(plan.text, "Пресет применён")
|
|
self.assertEqual(plan.mode, "success")
|
|
self.assertEqual(plan.indicator, "pulse")
|
|
|
|
def test_title_status_icon_is_pulsing_dot_only_and_larger_than_text_bar_icon(self) -> None:
|
|
from presets.ui.common.preset_status_bar import PresetStatusIcon, build_preset_status_plan
|
|
|
|
icon = PresetStatusIcon(size=24)
|
|
icon.set_plan(build_preset_status_plan("applied", launch_method=ZAPRET2_MODE))
|
|
|
|
self.assertFalse(hasattr(icon, "text_label"))
|
|
self.assertEqual(icon.minimumWidth(), 28)
|
|
self.assertEqual(icon.minimumHeight(), 28)
|
|
self.assertEqual(icon.pulse_dot.size().width(), 24)
|
|
self.assertTrue(icon.pulse_dot.isVisible())
|
|
self.assertTrue(icon.pulse_dot._is_pulsing)
|
|
|
|
def test_status_bar_skips_duplicate_plan_render(self) -> None:
|
|
from presets.ui.common.preset_status_bar import PresetStatusBar, build_preset_status_plan
|
|
|
|
status_bar = PresetStatusBar()
|
|
plan = build_preset_status_plan("applied", launch_method=ZAPRET2_MODE)
|
|
status_bar.set_plan(plan)
|
|
status_bar.spinner.stop = Mock(side_effect=AssertionError("same status must not stop spinner again"))
|
|
status_bar.spinner.start = Mock(side_effect=AssertionError("same status must not start spinner again"))
|
|
status_bar.pulse_dot.setVisible = Mock(side_effect=AssertionError("same status must not update visibility"))
|
|
status_bar.text_label.setText = Mock(side_effect=AssertionError("same status must not rewrite text"))
|
|
|
|
status_bar.set_plan(plan)
|
|
|
|
status_bar.spinner.stop.assert_not_called()
|
|
status_bar.spinner.start.assert_not_called()
|
|
status_bar.pulse_dot.setVisible.assert_not_called()
|
|
status_bar.text_label.setText.assert_not_called()
|
|
|
|
def test_status_bar_text_change_keeps_same_indicator_render(self) -> None:
|
|
from presets.ui.common.preset_status_bar import PresetStatusBar, build_preset_status_plan
|
|
|
|
status_bar = PresetStatusBar()
|
|
status_bar.set_plan(build_preset_status_plan("saved", launch_method=ZAPRET2_MODE, text="Сохранено"))
|
|
status_bar.spinner.stop = Mock(side_effect=AssertionError("same indicator must not stop spinner again"))
|
|
status_bar.spinner.start = Mock(side_effect=AssertionError("same indicator must not start spinner again"))
|
|
status_bar.pulse_dot.setVisible = Mock(side_effect=AssertionError("same indicator must not update visibility"))
|
|
|
|
status_bar.set_plan(build_preset_status_plan("saved", launch_method=ZAPRET2_MODE, text="Сохранено повторно"))
|
|
|
|
status_bar.spinner.stop.assert_not_called()
|
|
status_bar.spinner.start.assert_not_called()
|
|
status_bar.pulse_dot.setVisible.assert_not_called()
|
|
|
|
def test_title_status_icon_skips_duplicate_plan_render(self) -> None:
|
|
from presets.ui.common.preset_status_bar import PresetStatusIcon, build_preset_status_plan
|
|
|
|
icon = PresetStatusIcon(size=24)
|
|
plan = build_preset_status_plan("applied", launch_method=ZAPRET2_MODE)
|
|
icon.set_plan(plan)
|
|
icon.spinner.stop = Mock(side_effect=AssertionError("same status must not stop spinner again"))
|
|
icon.spinner.start = Mock(side_effect=AssertionError("same status must not start spinner again"))
|
|
icon.pulse_dot.setVisible = Mock(side_effect=AssertionError("same status must not update visibility"))
|
|
icon.setToolTip = Mock(side_effect=AssertionError("same status must not rewrite tooltip"))
|
|
icon.setVisible = Mock(side_effect=AssertionError("same status must not update visibility"))
|
|
|
|
icon.set_plan(plan)
|
|
|
|
icon.spinner.stop.assert_not_called()
|
|
icon.spinner.start.assert_not_called()
|
|
icon.pulse_dot.setVisible.assert_not_called()
|
|
icon.setToolTip.assert_not_called()
|
|
icon.setVisible.assert_not_called()
|
|
|
|
def test_title_status_icon_theme_refresh_skips_duplicate_tooltip(self) -> None:
|
|
from unittest.mock import patch
|
|
|
|
from presets.ui.common.preset_status_bar import PresetStatusIcon, build_preset_status_plan
|
|
|
|
icon = PresetStatusIcon(size=24)
|
|
plan = build_preset_status_plan("applied", launch_method=ZAPRET2_MODE)
|
|
icon.set_plan(plan)
|
|
icon.setToolTip = Mock(side_effect=AssertionError("same tooltip must not rewrite on theme refresh"))
|
|
|
|
with patch("presets.ui.common.preset_status_bar._status_theme_key", return_value=("#8cc63f", False)):
|
|
icon.set_plan(plan)
|
|
|
|
icon.setToolTip.assert_not_called()
|
|
|
|
def test_title_status_icon_style_update_skips_duplicate_dot_color(self) -> None:
|
|
from unittest.mock import patch
|
|
|
|
from presets.ui.common.preset_status_bar import PresetStatusIcon
|
|
|
|
icon = PresetStatusIcon.__new__(PresetStatusIcon)
|
|
icon._icon_size = 24
|
|
icon._last_dot_color = None
|
|
icon.pulse_dot = _PulseWidget()
|
|
|
|
with patch(
|
|
"presets.ui.common.preset_status_bar.get_theme_tokens",
|
|
return_value=SimpleNamespace(is_light=False),
|
|
):
|
|
PresetStatusIcon._apply_mode_style(icon, "neutral")
|
|
PresetStatusIcon._apply_mode_style(icon, "neutral")
|
|
|
|
self.assertEqual(icon.pulse_dot.colors, ["#6f7378"])
|
|
|
|
def test_title_status_icon_text_change_keeps_same_indicator_render(self) -> None:
|
|
from presets.ui.common.preset_status_bar import PresetStatusIcon, build_preset_status_plan
|
|
|
|
icon = PresetStatusIcon(size=24)
|
|
icon.set_plan(build_preset_status_plan("saved", launch_method=ZAPRET2_MODE, text="Сохранено"))
|
|
icon.spinner.stop = Mock(side_effect=AssertionError("same indicator must not stop spinner again"))
|
|
icon.spinner.start = Mock(side_effect=AssertionError("same indicator must not start spinner again"))
|
|
icon.pulse_dot.setVisible = Mock(side_effect=AssertionError("same indicator must not update visibility"))
|
|
icon.setVisible = Mock(side_effect=AssertionError("same indicator must not update widget visibility"))
|
|
|
|
icon.set_plan(build_preset_status_plan("saved", launch_method=ZAPRET2_MODE, text="Сохранено повторно"))
|
|
|
|
icon.spinner.stop.assert_not_called()
|
|
icon.spinner.start.assert_not_called()
|
|
icon.pulse_dot.setVisible.assert_not_called()
|
|
icon.setVisible.assert_not_called()
|
|
|
|
def test_raw_editor_runtime_toggle_uses_single_button_plan(self) -> None:
|
|
from presets.ui.common.preset_subpage_base import build_runtime_toggle_button_plan
|
|
|
|
stopped_plan = build_runtime_toggle_button_plan(
|
|
launch_phase="stopped",
|
|
launch_running=False,
|
|
launch_busy=False,
|
|
)
|
|
running_plan = build_runtime_toggle_button_plan(
|
|
launch_phase="running",
|
|
launch_running=True,
|
|
launch_busy=False,
|
|
)
|
|
stopping_plan = build_runtime_toggle_button_plan(
|
|
launch_phase="stopping",
|
|
launch_running=True,
|
|
launch_busy=True,
|
|
)
|
|
|
|
self.assertEqual(stopped_plan.text, "Запустить")
|
|
self.assertEqual(stopped_plan.icon_name, "PLAY")
|
|
self.assertFalse(stopped_plan.should_stop)
|
|
self.assertTrue(stopped_plan.enabled)
|
|
self.assertEqual(running_plan.text, "Остановить")
|
|
self.assertEqual(running_plan.icon_name, "STOP")
|
|
self.assertTrue(running_plan.should_stop)
|
|
self.assertTrue(running_plan.enabled)
|
|
self.assertEqual(stopping_plan.text, "Остановить")
|
|
self.assertEqual(stopping_plan.icon_name, "STOP")
|
|
self.assertTrue(stopping_plan.should_stop)
|
|
self.assertFalse(stopping_plan.enabled)
|
|
|
|
def test_raw_editor_runtime_toggle_stop_icon_uses_square_stop_glyph(self) -> None:
|
|
from presets.ui.common import preset_subpage_base
|
|
|
|
with patch.object(
|
|
preset_subpage_base,
|
|
"get_themed_qta_icon",
|
|
return_value="square-stop-icon",
|
|
) as get_icon:
|
|
self.assertEqual(preset_subpage_base._fluent_icon("STOP"), "square-stop-icon")
|
|
|
|
get_icon.assert_called_once_with("fa5s.stop")
|
|
|
|
def test_raw_editor_runtime_toggle_skips_duplicate_button_render(self) -> None:
|
|
from presets.ui.common.preset_subpage_base import (
|
|
RuntimeToggleButtonPlan,
|
|
apply_runtime_toggle_button_plan,
|
|
)
|
|
|
|
button = _RuntimeToggleButton()
|
|
plan = RuntimeToggleButtonPlan(
|
|
text="Запустить",
|
|
icon_name="PLAY",
|
|
should_stop=False,
|
|
enabled=True,
|
|
)
|
|
|
|
self.assertFalse(
|
|
apply_runtime_toggle_button_plan(
|
|
button,
|
|
plan,
|
|
runtime_available=True,
|
|
icon_factory=lambda name: f"icon:{name}",
|
|
)
|
|
)
|
|
self.assertEqual(button.text_calls, ["Запустить"])
|
|
self.assertEqual(button.icon_calls, ["icon:PLAY"])
|
|
self.assertEqual(button.enabled_calls, [True])
|
|
|
|
button.setText = Mock(side_effect=AssertionError("same runtime toggle must not rewrite text"))
|
|
button.setIcon = Mock(side_effect=AssertionError("same runtime toggle must not rewrite icon"))
|
|
button.setEnabled = Mock(side_effect=AssertionError("same runtime toggle must not rewrite enabled"))
|
|
|
|
self.assertFalse(
|
|
apply_runtime_toggle_button_plan(
|
|
button,
|
|
plan,
|
|
runtime_available=True,
|
|
icon_factory=lambda name: f"icon:{name}",
|
|
)
|
|
)
|
|
|
|
button.setText.assert_not_called()
|
|
button.setIcon.assert_not_called()
|
|
button.setEnabled.assert_not_called()
|
|
|
|
def test_raw_editor_runtime_toggle_enabled_change_keeps_same_text_and_icon(self) -> None:
|
|
from presets.ui.common.preset_subpage_base import (
|
|
RuntimeToggleButtonPlan,
|
|
apply_runtime_toggle_button_plan,
|
|
)
|
|
|
|
button = _RuntimeToggleButton()
|
|
plan = RuntimeToggleButtonPlan(
|
|
text="Запустить",
|
|
icon_name="PLAY",
|
|
should_stop=False,
|
|
enabled=True,
|
|
)
|
|
|
|
apply_runtime_toggle_button_plan(
|
|
button,
|
|
plan,
|
|
runtime_available=False,
|
|
icon_factory=lambda name: f"icon:{name}",
|
|
)
|
|
button.setText = Mock(side_effect=AssertionError("enabled-only change must not rewrite text"))
|
|
button.setIcon = Mock(side_effect=AssertionError("enabled-only change must not rewrite icon"))
|
|
|
|
apply_runtime_toggle_button_plan(
|
|
button,
|
|
plan,
|
|
runtime_available=True,
|
|
icon_factory=lambda name: f"icon:{name}",
|
|
)
|
|
|
|
button.setText.assert_not_called()
|
|
button.setIcon.assert_not_called()
|
|
self.assertEqual(button.enabled_calls, [False, True])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|