564 lines
22 KiB
Python
564 lines
22 KiB
Python
from __future__ import annotations
|
||
|
||
import unittest
|
||
from types import SimpleNamespace
|
||
from unittest.mock import Mock, patch
|
||
|
||
|
||
class _TextTarget:
|
||
def __init__(self) -> None:
|
||
self.text = ""
|
||
self.accessible_name = ""
|
||
self.accessible_description = ""
|
||
self.properties: dict[str, str] = {}
|
||
|
||
def setText(self, text: str) -> None: # noqa: N802
|
||
self.text = 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) -> object:
|
||
return self.properties.get(name)
|
||
|
||
def setProperty(self, name: str, value: object) -> None: # noqa: N802
|
||
self.properties[str(name)] = str(value)
|
||
|
||
|
||
class _VisibleTarget:
|
||
def __init__(self) -> None:
|
||
self.visible = None
|
||
|
||
def setVisible(self, visible: bool) -> None: # noqa: N802
|
||
self.visible = bool(visible)
|
||
|
||
|
||
class _StatusDot:
|
||
def __init__(self) -> None:
|
||
self.color = ""
|
||
self.started = 0
|
||
self.stopped = 0
|
||
self.accessible_name = ""
|
||
self.properties: dict[str, str] = {}
|
||
|
||
def set_color(self, color: str) -> None:
|
||
self.color = color
|
||
|
||
def start_pulse(self) -> None:
|
||
self.started += 1
|
||
|
||
def stop_pulse(self) -> None:
|
||
self.stopped += 1
|
||
|
||
def accessibleName(self) -> str: # noqa: N802
|
||
return self.accessible_name
|
||
|
||
def setAccessibleName(self, text: str) -> None: # noqa: N802
|
||
self.accessible_name = str(text)
|
||
|
||
def setProperty(self, name: str, value: str) -> None: # noqa: N802
|
||
self.properties[str(name)] = str(value)
|
||
|
||
|
||
class _ToggleTarget:
|
||
def __init__(self, checked: bool) -> None:
|
||
self.checked = bool(checked)
|
||
self.calls: list[tuple[bool, bool]] = []
|
||
|
||
def isChecked(self) -> bool: # noqa: N802
|
||
return self.checked
|
||
|
||
def setChecked(self, checked: bool, block_signals: bool = False) -> None: # noqa: N802
|
||
self.calls.append((bool(checked), bool(block_signals)))
|
||
self.checked = bool(checked)
|
||
|
||
|
||
class _WidgetStateTarget:
|
||
def __init__(self, *, text: str = "", visible: bool = True, enabled: bool = True) -> None:
|
||
self._text = text
|
||
self._hidden = not bool(visible)
|
||
self._enabled = bool(enabled)
|
||
self.text_calls: list[str] = []
|
||
self.visible_calls: list[bool] = []
|
||
self.enabled_calls: list[bool] = []
|
||
|
||
def text(self) -> str:
|
||
return self._text
|
||
|
||
def setText(self, text: str) -> None: # noqa: N802
|
||
self.text_calls.append(str(text))
|
||
self._text = str(text)
|
||
|
||
def isHidden(self) -> bool: # noqa: N802
|
||
return self._hidden
|
||
|
||
def setVisible(self, visible: bool) -> None: # noqa: N802
|
||
self.visible_calls.append(bool(visible))
|
||
self._hidden = not bool(visible)
|
||
|
||
def isEnabled(self) -> bool: # noqa: N802
|
||
return self._enabled
|
||
|
||
def setEnabled(self, enabled: bool) -> None: # noqa: N802
|
||
self.enabled_calls.append(bool(enabled))
|
||
self._enabled = bool(enabled)
|
||
|
||
|
||
class _ProgressTarget:
|
||
def __init__(self) -> None:
|
||
self.started = 0
|
||
self.stopped = 0
|
||
self.accessible_name = ""
|
||
self.properties: dict[str, str] = {}
|
||
|
||
def start(self) -> None:
|
||
self.started += 1
|
||
|
||
def stop(self) -> None:
|
||
self.stopped += 1
|
||
|
||
def accessibleName(self) -> str: # noqa: N802
|
||
return self.accessible_name
|
||
|
||
def setAccessibleName(self, text: str) -> None: # noqa: N802
|
||
self.accessible_name = str(text)
|
||
|
||
def property(self, name: str) -> object:
|
||
return self.properties.get(name)
|
||
|
||
def setProperty(self, name: str, value: object) -> None: # noqa: N802
|
||
self.properties[str(name)] = str(value)
|
||
|
||
|
||
class ControlStatusDotPulseTests(unittest.TestCase):
|
||
def _apply_plan(self, *, pulsing: bool) -> _StatusDot:
|
||
from presets.ui.control.control_page_runtime_shared import apply_status_plan
|
||
|
||
dot = _StatusDot()
|
||
apply_status_plan(
|
||
SimpleNamespace(
|
||
phase="running",
|
||
title="Zapret работает",
|
||
description="Обход блокировок активен",
|
||
dot_color="#6ccb5f",
|
||
pulsing=pulsing,
|
||
show_start=False,
|
||
show_stop_only=True,
|
||
show_stop_and_exit=True,
|
||
),
|
||
status_title=_TextTarget(),
|
||
status_desc=_TextTarget(),
|
||
status_dot=dot,
|
||
start_btn=_VisibleTarget(),
|
||
stop_winws_btn=_VisibleTarget(),
|
||
stop_and_exit_btn=_VisibleTarget(),
|
||
update_stop_button_text=lambda: None,
|
||
)
|
||
return dot
|
||
|
||
def test_apply_status_plan_starts_pulse_when_plan_requests_it(self) -> None:
|
||
dot = self._apply_plan(pulsing=True)
|
||
|
||
self.assertEqual(dot.started, 1)
|
||
self.assertEqual(dot.stopped, 0)
|
||
|
||
def test_apply_status_plan_stops_pulse_when_plan_is_static(self) -> None:
|
||
dot = self._apply_plan(pulsing=False)
|
||
|
||
self.assertEqual(dot.started, 0)
|
||
self.assertEqual(dot.stopped, 1)
|
||
|
||
def test_apply_status_plan_sets_screen_reader_status_text(self) -> None:
|
||
from presets.ui.control.control_page_runtime_shared import apply_status_plan
|
||
|
||
status_title = _TextTarget()
|
||
status_desc = _TextTarget()
|
||
|
||
apply_status_plan(
|
||
SimpleNamespace(
|
||
phase="failed",
|
||
title="Ошибка запуска",
|
||
description="Не удалось запустить процесс обхода блокировок",
|
||
dot_color="#f5c04d",
|
||
pulsing=False,
|
||
show_start=True,
|
||
show_stop_only=False,
|
||
show_stop_and_exit=False,
|
||
),
|
||
status_title=status_title,
|
||
status_desc=status_desc,
|
||
status_dot=_StatusDot(),
|
||
start_btn=_VisibleTarget(),
|
||
stop_winws_btn=_VisibleTarget(),
|
||
stop_and_exit_btn=_VisibleTarget(),
|
||
update_stop_button_text=lambda: None,
|
||
)
|
||
|
||
self.assertEqual(status_title.accessible_name, "Статус Zapret: Ошибка запуска")
|
||
self.assertEqual(
|
||
status_title.properties.get("screenReaderStateText"),
|
||
"Статус Zapret: Ошибка запуска",
|
||
)
|
||
self.assertEqual(status_title.accessible_description, "Не удалось запустить процесс обхода блокировок")
|
||
self.assertEqual(
|
||
status_desc.accessible_name,
|
||
"Описание состояния Zapret: Не удалось запустить процесс обхода блокировок",
|
||
)
|
||
self.assertEqual(
|
||
status_desc.properties.get("screenReaderStateText"),
|
||
"Описание состояния Zapret: Не удалось запустить процесс обхода блокировок",
|
||
)
|
||
|
||
def test_apply_status_plan_sets_screen_reader_dot_state_text(self) -> None:
|
||
from presets.ui.control.control_page_runtime_shared import apply_status_plan
|
||
|
||
status_dot = _StatusDot()
|
||
|
||
apply_status_plan(
|
||
SimpleNamespace(
|
||
phase="failed",
|
||
title="Ошибка запуска",
|
||
description="Не удалось запустить процесс обхода блокировок",
|
||
dot_color="#f5c04d",
|
||
pulsing=False,
|
||
show_start=True,
|
||
show_stop_only=False,
|
||
show_stop_and_exit=False,
|
||
),
|
||
status_title=_TextTarget(),
|
||
status_desc=_TextTarget(),
|
||
status_dot=status_dot,
|
||
start_btn=_VisibleTarget(),
|
||
stop_winws_btn=_VisibleTarget(),
|
||
stop_and_exit_btn=_VisibleTarget(),
|
||
update_stop_button_text=lambda: None,
|
||
)
|
||
|
||
self.assertEqual(status_dot.accessible_name, "Индикатор состояния Zapret: Ошибка запуска")
|
||
self.assertEqual(
|
||
status_dot.properties.get("screenReaderStateText"),
|
||
"Индикатор состояния Zapret: Ошибка запуска",
|
||
)
|
||
|
||
def test_apply_status_plan_skips_duplicate_render(self) -> None:
|
||
from presets.ui.control.control_page_runtime_shared import apply_status_plan
|
||
|
||
plan = SimpleNamespace(
|
||
phase="running",
|
||
title="Zapret работает",
|
||
description="Обход блокировок активен",
|
||
dot_color="#6ccb5f",
|
||
pulsing=True,
|
||
show_start=False,
|
||
show_stop_only=True,
|
||
show_stop_and_exit=True,
|
||
)
|
||
status_title = _TextTarget()
|
||
status_desc = _TextTarget()
|
||
status_dot = _StatusDot()
|
||
start_btn = _VisibleTarget()
|
||
stop_winws_btn = _VisibleTarget()
|
||
stop_and_exit_btn = _VisibleTarget()
|
||
update_stop_button_text = Mock()
|
||
|
||
apply_status_plan(
|
||
plan,
|
||
status_title=status_title,
|
||
status_desc=status_desc,
|
||
status_dot=status_dot,
|
||
start_btn=start_btn,
|
||
stop_winws_btn=stop_winws_btn,
|
||
stop_and_exit_btn=stop_and_exit_btn,
|
||
update_stop_button_text=update_stop_button_text,
|
||
)
|
||
status_title.setText = Mock(side_effect=AssertionError("same status must not rewrite title"))
|
||
status_desc.setText = Mock(side_effect=AssertionError("same status must not rewrite description"))
|
||
status_dot.set_color = Mock(side_effect=AssertionError("same status must not rewrite dot color"))
|
||
status_dot.start_pulse = Mock(side_effect=AssertionError("same status must not restart pulse"))
|
||
status_dot.stop_pulse = Mock(side_effect=AssertionError("same status must not stop pulse"))
|
||
start_btn.setVisible = Mock(side_effect=AssertionError("same status must not rewrite start visibility"))
|
||
stop_winws_btn.setVisible = Mock(side_effect=AssertionError("same status must not rewrite stop visibility"))
|
||
stop_and_exit_btn.setVisible = Mock(side_effect=AssertionError("same status must not rewrite exit visibility"))
|
||
update_stop_button_text.side_effect = AssertionError("same status must not rewrite stop button text")
|
||
|
||
self.assertTrue(apply_status_plan(
|
||
plan,
|
||
status_title=status_title,
|
||
status_desc=status_desc,
|
||
status_dot=status_dot,
|
||
start_btn=start_btn,
|
||
stop_winws_btn=stop_winws_btn,
|
||
stop_and_exit_btn=stop_and_exit_btn,
|
||
update_stop_button_text=update_stop_button_text,
|
||
))
|
||
|
||
status_title.setText.assert_not_called()
|
||
status_desc.setText.assert_not_called()
|
||
status_dot.set_color.assert_not_called()
|
||
status_dot.start_pulse.assert_not_called()
|
||
status_dot.stop_pulse.assert_not_called()
|
||
start_btn.setVisible.assert_not_called()
|
||
stop_winws_btn.setVisible.assert_not_called()
|
||
stop_and_exit_btn.setVisible.assert_not_called()
|
||
|
||
def test_apply_status_plan_skips_unchanged_text_and_visibility_when_dot_changes(self) -> None:
|
||
from presets.ui.control.control_page_runtime_shared import apply_status_plan
|
||
|
||
first_plan = SimpleNamespace(
|
||
phase="running",
|
||
title="Zapret работает",
|
||
description="Обход блокировок активен",
|
||
dot_color="#6ccb5f",
|
||
pulsing=True,
|
||
show_start=False,
|
||
show_stop_only=True,
|
||
show_stop_and_exit=True,
|
||
)
|
||
second_plan = SimpleNamespace(
|
||
phase="running",
|
||
title="Zapret работает",
|
||
description="Обход блокировок активен",
|
||
dot_color="#7aa7ff",
|
||
pulsing=True,
|
||
show_start=False,
|
||
show_stop_only=True,
|
||
show_stop_and_exit=True,
|
||
)
|
||
status_title = _WidgetStateTarget(text="Zapret работает")
|
||
status_desc = _WidgetStateTarget(text="Обход блокировок активен")
|
||
status_dot = _StatusDot()
|
||
start_btn = _WidgetStateTarget(visible=False)
|
||
stop_winws_btn = _WidgetStateTarget(visible=True)
|
||
stop_and_exit_btn = _WidgetStateTarget(visible=True)
|
||
|
||
apply_status_plan(
|
||
first_plan,
|
||
status_title=status_title,
|
||
status_desc=status_desc,
|
||
status_dot=status_dot,
|
||
start_btn=start_btn,
|
||
stop_winws_btn=stop_winws_btn,
|
||
stop_and_exit_btn=stop_and_exit_btn,
|
||
update_stop_button_text=lambda: None,
|
||
)
|
||
status_title.text_calls.clear()
|
||
status_desc.text_calls.clear()
|
||
start_btn.visible_calls.clear()
|
||
stop_winws_btn.visible_calls.clear()
|
||
stop_and_exit_btn.visible_calls.clear()
|
||
|
||
apply_status_plan(
|
||
second_plan,
|
||
status_title=status_title,
|
||
status_desc=status_desc,
|
||
status_dot=status_dot,
|
||
start_btn=start_btn,
|
||
stop_winws_btn=stop_winws_btn,
|
||
stop_and_exit_btn=stop_and_exit_btn,
|
||
update_stop_button_text=lambda: None,
|
||
)
|
||
|
||
self.assertEqual(status_dot.color, "#7aa7ff")
|
||
self.assertEqual(status_title.text_calls, [])
|
||
self.assertEqual(status_desc.text_calls, [])
|
||
self.assertEqual(start_btn.visible_calls, [])
|
||
self.assertEqual(stop_winws_btn.visible_calls, [])
|
||
self.assertEqual(stop_and_exit_btn.visible_calls, [])
|
||
|
||
def test_last_status_message_skips_duplicate_render(self) -> None:
|
||
from presets.ui.control.control_page_runtime_shared import apply_last_status_message
|
||
|
||
message_label = _TextTarget()
|
||
message_dot = _StatusDot()
|
||
|
||
apply_last_status_message(
|
||
"Пресет успешно применён",
|
||
message_label=message_label,
|
||
message_dot=message_dot,
|
||
empty_text="Нет сообщений",
|
||
)
|
||
message_label.setText = Mock(side_effect=AssertionError("same message must not rewrite label"))
|
||
message_dot.set_color = Mock(side_effect=AssertionError("same message must not rewrite dot color"))
|
||
message_dot.stop_pulse = Mock(side_effect=AssertionError("same message must not stop pulse again"))
|
||
|
||
apply_last_status_message(
|
||
"Пресет успешно применён",
|
||
message_label=message_label,
|
||
message_dot=message_dot,
|
||
empty_text="Нет сообщений",
|
||
)
|
||
|
||
message_label.setText.assert_not_called()
|
||
message_dot.set_color.assert_not_called()
|
||
message_dot.stop_pulse.assert_not_called()
|
||
|
||
def test_last_status_message_sets_screen_reader_dot_state_text(self) -> None:
|
||
from presets.ui.control.control_page_runtime_shared import apply_last_status_message
|
||
|
||
message_dot = _StatusDot()
|
||
|
||
apply_last_status_message(
|
||
"Пресет успешно применён",
|
||
message_label=_TextTarget(),
|
||
message_dot=message_dot,
|
||
empty_text="Нет сообщений",
|
||
)
|
||
|
||
self.assertEqual(
|
||
message_dot.accessible_name,
|
||
"Индикатор последнего сообщения: Пресет успешно применён",
|
||
)
|
||
self.assertEqual(
|
||
message_dot.properties.get("screenReaderStateText"),
|
||
"Индикатор последнего сообщения: Пресет успешно применён",
|
||
)
|
||
|
||
def test_last_status_message_sets_screen_reader_label_context(self) -> None:
|
||
from presets.ui.control.control_page_runtime_shared import apply_last_status_message
|
||
|
||
message_label = _TextTarget()
|
||
|
||
apply_last_status_message(
|
||
"Пресет успешно применён",
|
||
message_label=message_label,
|
||
message_dot=_StatusDot(),
|
||
empty_text="Нет сообщений",
|
||
)
|
||
|
||
self.assertEqual(
|
||
message_label.accessible_name,
|
||
"Последнее сообщение Zapret: Пресет успешно применён",
|
||
)
|
||
self.assertEqual(
|
||
message_label.properties.get("screenReaderStateText"),
|
||
"Последнее сообщение Zapret: Пресет успешно применён",
|
||
)
|
||
|
||
def test_set_toggle_checked_skips_duplicate_state(self) -> None:
|
||
from presets.ui.control.control_page_runtime_shared import set_toggle_checked
|
||
|
||
toggle = _ToggleTarget(True)
|
||
|
||
set_toggle_checked(toggle, True)
|
||
|
||
self.assertEqual(toggle.calls, [])
|
||
|
||
def test_set_toggle_checked_applies_changed_state_with_blocked_signals(self) -> None:
|
||
from presets.ui.control.control_page_runtime_shared import set_toggle_checked
|
||
|
||
toggle = _ToggleTarget(False)
|
||
|
||
set_toggle_checked(toggle, True)
|
||
|
||
self.assertEqual(toggle.calls, [(True, True)])
|
||
self.assertTrue(toggle.checked)
|
||
|
||
def test_zapret2_additional_settings_state_skips_duplicate_toggles(self) -> None:
|
||
from presets.ui.control.zapret2.runtime_helpers import apply_additional_settings_state
|
||
|
||
state = SimpleNamespace(
|
||
discord_restart=True,
|
||
wssize_enabled=False,
|
||
debug_log_enabled=True,
|
||
)
|
||
discord_toggle = _ToggleTarget(True)
|
||
wssize_toggle = _ToggleTarget(False)
|
||
debug_log_toggle = _ToggleTarget(True)
|
||
|
||
apply_additional_settings_state(
|
||
state,
|
||
discord_restart_toggle=discord_toggle,
|
||
wssize_toggle=wssize_toggle,
|
||
debug_log_toggle=debug_log_toggle,
|
||
)
|
||
|
||
self.assertEqual(discord_toggle.calls, [])
|
||
self.assertEqual(wssize_toggle.calls, [])
|
||
self.assertEqual(debug_log_toggle.calls, [])
|
||
|
||
def test_widget_text_visibility_and_enabled_updates_skip_duplicate_state(self) -> None:
|
||
from presets.ui.control.control_page_runtime_shared import (
|
||
set_enabled_if_changed,
|
||
set_text_if_changed,
|
||
set_visible_if_changed,
|
||
)
|
||
|
||
widget = _WidgetStateTarget(text="Запуск", visible=True, enabled=False)
|
||
|
||
self.assertFalse(set_text_if_changed(widget, "Запуск"))
|
||
self.assertFalse(set_visible_if_changed(widget, True))
|
||
self.assertFalse(set_enabled_if_changed(widget, False))
|
||
|
||
self.assertEqual(widget.text_calls, [])
|
||
self.assertEqual(widget.visible_calls, [])
|
||
self.assertEqual(widget.enabled_calls, [])
|
||
|
||
def test_widget_text_visibility_and_enabled_updates_apply_changed_state(self) -> None:
|
||
from presets.ui.control.control_page_runtime_shared import (
|
||
set_enabled_if_changed,
|
||
set_text_if_changed,
|
||
set_visible_if_changed,
|
||
)
|
||
|
||
widget = _WidgetStateTarget(text="Запуск", visible=True, enabled=False)
|
||
|
||
self.assertTrue(set_text_if_changed(widget, "Остановка"))
|
||
self.assertTrue(set_visible_if_changed(widget, False))
|
||
self.assertTrue(set_enabled_if_changed(widget, True))
|
||
|
||
self.assertEqual(widget.text_calls, ["Остановка"])
|
||
self.assertEqual(widget.visible_calls, [False])
|
||
self.assertEqual(widget.enabled_calls, [True])
|
||
|
||
def test_progress_active_update_skips_duplicate_start_and_stop(self) -> None:
|
||
from presets.ui.control.control_page_runtime_shared import set_progress_active_if_changed
|
||
|
||
progress = _ProgressTarget()
|
||
|
||
self.assertTrue(set_progress_active_if_changed(progress, True))
|
||
self.assertFalse(set_progress_active_if_changed(progress, True))
|
||
self.assertTrue(set_progress_active_if_changed(progress, False))
|
||
self.assertFalse(set_progress_active_if_changed(progress, False))
|
||
|
||
self.assertEqual(progress.started, 1)
|
||
self.assertEqual(progress.stopped, 1)
|
||
self.assertEqual(progress.accessibleName(), "Ход запуска Zapret: не выполняется")
|
||
self.assertEqual(progress.property("screenReaderStateText"), "Ход запуска Zapret: не выполняется")
|
||
|
||
def test_loading_status_text_updates_screen_reader_state(self) -> None:
|
||
from presets.ui.control.control_page_runtime_shared import set_loading_status_accessibility
|
||
|
||
label = _TextTarget()
|
||
|
||
set_loading_status_accessibility(label, active=True, text="Запуск winws...")
|
||
|
||
self.assertEqual(label.accessibleName(), "Статус запуска Zapret: Запуск winws...")
|
||
self.assertEqual(label.property("screenReaderStateText"), "Статус запуска Zapret: Запуск winws...")
|
||
|
||
set_loading_status_accessibility(label, active=False, text="")
|
||
|
||
self.assertEqual(label.accessibleName(), "Статус запуска Zapret: нет активного запуска")
|
||
self.assertEqual(label.property("screenReaderStateText"), "Статус запуска Zapret: нет активного запуска")
|
||
|
||
def test_running_status_pulses_for_both_control_modes(self) -> None:
|
||
from presets.ui.control import control_runtime
|
||
from presets.ui.control.zapret2 import page_runtime as zapret2_page_runtime
|
||
|
||
winws1_plan = control_runtime.build_status_plan(state="running", last_error="", language="ru")
|
||
winws2_plan = zapret2_page_runtime.build_status_plan(state="running", last_error="", language="ru")
|
||
|
||
self.assertTrue(winws1_plan.pulsing)
|
||
self.assertTrue(winws2_plan.pulsing)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|