zapretgui/tests/test_tray_menu_contract.py
loop-uh 07a1f55017
All checks were successful
Zapret source guards / validate (push) Successful in 10s
Windows-сборка / Dev Dev 21.1.7.183 опубликован
Возврат окна из фона: защита памяти берётся до того, как Windows сожмёт набор свёрнутого окна
Раньше размер удерживаемой памяти и снимок нужных страниц брались уже ПОСЛЕ ухода окна, когда Windows успела сжать рабочий набор: удержание не включалось (меньше 24 МБ) или держало обрезок, а предзагрузка тянула скудный снимок. Теперь самый ранний сигнал — команда «свернуть» и сворачивание в трей (away_intent): в этот момент запоминается «горячий» размер (самое большое значение за время показа) и снимок страниц, тут же ставится жёсткий минимум; поздний снимок не затирает полный. Если окно так и не ушло, защита снимается через 2 с. Порог «свободно меньше 1 ГБ — не удерживать» заменён запасом в 256 МБ после удержания: на слабом ПК защита больше не отключается. psutil на пути возврата убран (подкачки читаются через WinAPI).

Тише при скрытом окне: монитор процессов winws опрашивает раз в 8 с вместо 2 с и не будит окно сигналами проверки; опрос мыши для подсказки переноса файла (8 раз в секунду) идёт только пока окно на экране. Меню трея создаётся и один раз рисуется в картинку через 6 с после значка, а не под первым щелчком после долгого простоя.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 16:41:24 -04:00

613 lines
26 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 os
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock, patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
class TrayMenuContractTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
from PyQt6.QtWidgets import QApplication
cls._app = QApplication.instance() or QApplication([])
def test_tray_launch_state_reads_runtime_snapshot_contract(self) -> None:
from app.feature_facades.tray import TrayFeature
runtime_feature = SimpleNamespace(
snapshot=Mock(return_value=SimpleNamespace(running=True, phase="running"))
)
feature = TrayFeature(
_deps=SimpleNamespace(),
_runtime_feature=runtime_feature,
_telegram_proxy_feature=SimpleNamespace(),
)
self.assertEqual(feature.launch_state(), (True, "running"))
def test_legacy_right_click_callback_opens_context_menu_at_cursor(self) -> None:
import tray
tray.WM_CONTEXTMENU = 0x007B
tray.WM_RBUTTONUP = 0x0205
tray.WM_LBUTTONUP = 0x0202
tray.WM_LBUTTONDBLCLK = 0x0203
tray.NIN_SELECT = 0x0400
tray.NIN_KEYSELECT = 0x0401
manager = tray.SystemTrayManager.__new__(tray.SystemTrayManager)
manager.show_context_menu = Mock()
manager._schedule_visibility_toggle = Mock()
with patch.object(tray.QTimer, "singleShot", side_effect=lambda _ms, callback: callback()):
tray.SystemTrayManager._handle_native_callback(manager, tray.WM_RBUTTONUP, anchor_x=1, anchor_y=0)
manager.show_context_menu.assert_called_once_with(anchor_x=None, anchor_y=None)
manager._schedule_visibility_toggle.assert_not_called()
def _manager(self, *, phase: str = "running", snapshot=None, visible: bool = False):
import tray
feature = SimpleNamespace(
launch_phase=Mock(return_value=phase),
preset_snapshot=Mock(return_value=snapshot),
telegram_proxy_label=Mock(return_value="Telegram Proxy: выкл"),
refresh_preset_snapshot=Mock(),
restart_dpi=Mock(),
toggle_dpi=Mock(),
activate_preset=Mock(return_value=True),
apply_window_opacity=Mock(),
)
port = SimpleNamespace(
is_visible=Mock(return_value=visible),
ui_language=Mock(return_value="ru"),
)
manager = tray.SystemTrayManager.__new__(tray.SystemTrayManager)
manager.window_port = port
manager._tray_feature = feature
manager._launch_phase = ""
manager._launch_method = "zapret2_mode"
manager._preset_name = ""
manager._popup = None
manager._stopped_notify_timer = None
manager._status_icon_key = ""
manager._status_icon_handles = {}
manager._tooltip = ""
manager._icon_visible = False
manager._message_window = None
return manager, feature, port
@staticmethod
def _snapshot(count: int = 0):
from tray_workers import TrayPresetSnapshot
presets = (("default.txt", "Default v1"), ("game.txt", "Game filter"))
presets += tuple((f"extra{i}.txt", f"Extra {i}") for i in range(count))
return TrayPresetSnapshot(
launch_method="zapret2_mode",
presets=presets,
selected_file_name="default.txt",
selected_display_name="Default v1",
)
@staticmethod
def _rows(manager, page: str = "main"):
return manager._menu_model().page(page).rows
def _popup(self, manager):
popup = manager._ensure_popup()
self.addCleanup(popup.deleteLater)
self.addCleanup(popup.hide)
popup.set_model(manager._menu_model())
return popup
def test_menu_is_built_and_drawn_before_the_first_click(self) -> None:
manager, _feature, _port = self._manager(snapshot=self._snapshot())
manager.prewarm_menu()
popup = manager._popup
self.addCleanup(popup.deleteLater)
self.assertIsNotNone(popup)
self.assertFalse(popup.isVisible()) # на экран прогрев ничего не выводит
# Первый щелчок открывает уже созданное меню, а не строит новое.
self.assertIs(manager._ensure_popup(), popup)
def test_warm_up_really_paints_the_menu(self) -> None:
manager, _feature, _port = self._manager(snapshot=self._snapshot())
popup = manager._ensure_popup()
self.addCleanup(popup.deleteLater)
popup.set_model(manager._menu_model())
with patch.object(type(popup), "paintEvent", autospec=True, side_effect=lambda self_, event: None) as paint:
popup.warm_up()
self.assertTrue(paint.called)
self.assertFalse(popup.isVisible())
def test_prewarm_failure_is_swallowed(self) -> None:
manager, _feature, _port = self._manager()
manager._create_popup = Mock(side_effect=RuntimeError("нет экрана"))
manager.prewarm_menu() # не падает
self.assertIsNone(manager._popup)
def test_native_backend_schedules_the_prewarm(self) -> None:
import inspect
import tray
source = inspect.getsource(tray.SystemTrayManager._create_native_backend)
self.assertIn("prewarm_menu", source)
def test_tray_menu_main_action_follows_launch_phase(self) -> None:
cases = {
"running": ("Остановить Zapret", True),
"starting": ("Остановить Zapret", True),
"stopping": ("Zapret останавливается…", False),
"stopped": ("Запустить Zapret", True),
"failed": ("Запустить Zapret", True),
}
for phase, (text, enabled) in cases.items():
with self.subTest(phase=phase):
manager, feature, _port = self._manager(phase=phase)
rows = self._rows(manager)
launch = next(row for row in rows if row.command == "toggle_dpi")
self.assertEqual((launch.text, launch.enabled), (text, enabled))
self.assertEqual("Перезапустить" in [row.text for row in rows], phase == "running")
manager._run_menu_command(launch.command, launch.arg)
feature.toggle_dpi.assert_called_once_with()
def test_tray_menu_header_shows_mode_status_and_preset(self) -> None:
manager, _feature, _port = self._manager(phase="running", snapshot=self._snapshot())
header = self._rows(manager)[0]
self.assertEqual(header.kind, "status")
self.assertEqual(header.text, "Zapret 2 · работает")
self.assertEqual(header.detail, "Default v1")
self.assertEqual(header.command, "show_window")
def test_tray_preset_page_marks_active_and_activates_choice(self) -> None:
manager, feature, _port = self._manager(snapshot=self._snapshot())
entry = next(row for row in self._rows(manager) if row.page == "presets")
self.assertEqual((entry.text, entry.detail), ("Пресет", "Default v1"))
items = [row for row in self._rows(manager, "presets") if row.kind == "item"]
self.assertEqual([(row.text, row.checked) for row in items], [("Default v1", True), ("Game filter", False)])
manager._run_menu_command(items[1].command, items[1].arg)
feature.activate_preset.assert_called_once_with("game.txt", "Game filter")
def test_tray_preset_page_hidden_for_orchestra(self) -> None:
from tray_workers import TrayPresetSnapshot
manager, _feature, _port = self._manager(snapshot=TrayPresetSnapshot(launch_method="orchestra"))
model = manager._menu_model()
self.assertNotIn("presets", model.pages)
self.assertNotIn("presets", [row.page for row in model.page("main").rows])
def test_tray_menu_commands_are_all_handled(self) -> None:
manager, _feature, _port = self._manager(snapshot=self._snapshot())
model = manager._menu_model()
commands = {row.command for page in model.pages.values() for row in page.rows if row.command}
for name in ("show_window", "_toggle_primary_visibility_action", "show_console", "exit_only",
"exit_and_stop", "_toggle_tg_proxy", "_set_window_opacity"):
setattr(manager, name, Mock())
with patch("tray.log") as log:
for command in sorted(commands):
arg = ("game.txt", "Game filter") if command == "activate_preset" else 50
manager._run_menu_command(command, arg)
log.assert_not_called()
def test_tray_menu_window_is_reused_and_presets_are_refreshed_on_each_open(self) -> None:
manager, feature, _port = self._manager(snapshot=self._snapshot())
popup = self._popup(manager)
manager.show_context_menu()
self.assertTrue(popup.isVisible())
manager.show_context_menu()
popup.hide()
manager.show_context_menu()
self.assertIs(manager._ensure_popup(), popup)
self.assertEqual(feature.refresh_preset_snapshot.call_count, 2)
def test_open_tray_menu_follows_launch_status(self) -> None:
manager, feature, _port = self._manager(phase="running", snapshot=self._snapshot())
manager._status_icon_handle_for = Mock(return_value=None)
manager._modify_icon = Mock()
manager.show_notification = Mock()
popup = self._popup(manager)
manager.show_context_menu()
feature.launch_phase.return_value = "stopped"
manager.apply_launch_status(phase="stopped", launch_method="zapret2_mode", preset_name="Default v1")
self.assertEqual(popup.rows()[0].text, "Zapret 2 · остановлен")
self.assertIn("Запустить Zapret", [row.text for row in popup.rows()])
def test_long_preset_list_scrolls_inside_a_small_window(self) -> None:
from ui.tray_menu import style
manager, _feature, _port = self._manager(snapshot=self._snapshot(count=150))
popup = self._popup(manager)
manager.show_context_menu()
main_height = popup.height()
popup.open_page("presets")
self.assertEqual(len(popup.rows()), 154)
self.assertLessEqual(popup._view_height, style.MAX_LIST_ROWS * style.ROW_HEIGHT)
self.assertLess(popup.height(), main_height + 6 * style.ROW_HEIGHT)
self.assertEqual(popup.width(), style.WIDTH + 2 * style.SHADOW)
def test_preset_page_opens_scrolled_to_the_active_preset(self) -> None:
from tray_workers import TrayPresetSnapshot
base = self._snapshot(count=150)
snapshot = TrayPresetSnapshot(
launch_method=base.launch_method,
presets=base.presets,
selected_file_name="extra100.txt",
selected_display_name="Extra 100",
)
manager, _feature, _port = self._manager(snapshot=snapshot)
popup = self._popup(manager)
manager.show_context_menu()
popup.open_page("presets")
checked = next(index for index, row in enumerate(popup.rows()) if row.checked)
self.assertTrue(popup._body_rect().contains(popup._row_rect(checked)))
def test_typing_filters_presets_and_escape_steps_back(self) -> None:
from PyQt6.QtCore import Qt
from PyQt6.QtTest import QTest
manager, _feature, _port = self._manager(snapshot=self._snapshot(count=20))
popup = self._popup(manager)
manager.show_context_menu()
popup.open_page("presets")
height = popup.height()
QTest.keyClicks(popup, "game")
self.assertEqual([row.text for row in popup.rows() if row.kind == "item"], ["Game filter"])
self.assertEqual(popup.height(), height)
QTest.keyClicks(popup, "zz")
self.assertEqual([row.kind for row in popup.rows()][-1], "note")
QTest.keyClick(popup, Qt.Key.Key_Escape)
self.assertEqual((popup.query(), popup.page_key()), ("", "presets"))
QTest.keyClick(popup, Qt.Key.Key_Escape)
self.assertEqual(popup.page_key(), "main")
QTest.keyClick(popup, Qt.Key.Key_Escape)
self.assertFalse(popup.isVisible())
def test_click_on_row_closes_menu_and_runs_command(self) -> None:
from PyQt6.QtCore import Qt
from PyQt6.QtTest import QTest
manager, feature, _port = self._manager(phase="running", snapshot=self._snapshot())
popup = self._popup(manager)
manager.show_context_menu()
rows = popup.rows()
restart = next(index for index, row in enumerate(rows) if row.command == "restart_dpi")
QTest.mouseClick(popup, Qt.MouseButton.LeftButton, pos=popup._row_rect(restart).center())
self.assertFalse(popup.isVisible())
self._app.processEvents()
feature.restart_dpi.assert_called_once_with()
def test_click_on_disabled_row_does_nothing(self) -> None:
from PyQt6.QtCore import Qt
from PyQt6.QtTest import QTest
manager, feature, _port = self._manager(phase="stopping", snapshot=self._snapshot())
popup = self._popup(manager)
manager.show_context_menu()
launch = next(index for index, row in enumerate(popup.rows()) if row.command == "toggle_dpi")
QTest.mouseClick(popup, Qt.MouseButton.LeftButton, pos=popup._row_rect(launch).center())
self._app.processEvents()
self.assertTrue(popup.isVisible())
feature.toggle_dpi.assert_not_called()
def test_keyboard_walks_rows_and_opens_preset_page(self) -> None:
from PyQt6.QtCore import Qt
from PyQt6.QtTest import QTest
manager, _feature, _port = self._manager(phase="stopped", snapshot=self._snapshot())
popup = self._popup(manager)
manager.show_context_menu()
visited = []
for _ in range(3):
QTest.keyClick(popup, Qt.Key.Key_Down)
visited.append(popup.current_row().text)
self.assertEqual(visited, ["Zapret 2 · остановлен", "Запустить Zapret", "Пресет"])
QTest.keyClick(popup, Qt.Key.Key_Right)
self.assertEqual(popup.page_key(), "presets")
QTest.keyClick(popup, Qt.Key.Key_Left)
self.assertEqual(popup.page_key(), "main")
def test_menu_stays_inside_the_screen_and_keeps_its_edge(self) -> None:
from PyQt6.QtCore import QPoint, QRect
from ui.tray_menu.popup import menu_origin
available = QRect(0, 0, 1920, 1040)
anchor = QPoint(1900, 1070)
short = menu_origin(anchor=anchor, edge_y=1062, opens_up=True, width=292, height=300, available=available)
tall = menu_origin(anchor=anchor, edge_y=1062, opens_up=True, width=292, height=450, available=available)
down = menu_origin(anchor=QPoint(-50, 5), edge_y=13, opens_up=False, width=292, height=300, available=available)
self.assertEqual((short.x(), short.y() + 300), (1920 - 292 - 8, 1040 - 8))
self.assertEqual(tall.y() + 450, short.y() + 300)
self.assertEqual((down.x(), down.y()), (8, 13))
def test_every_menu_icon_is_drawn(self) -> None:
from PyQt6.QtCore import QRectF
from PyQt6.QtGui import QColor, QImage, QPainter
from ui.tray_menu import style
manager, _feature, _port = self._manager(phase="running", snapshot=self._snapshot())
used = {row.icon for page in manager._menu_model().pages.values() for row in page.rows if row.icon}
self.assertLessEqual(used, set(style.tray_icon_names()))
for name in style.tray_icon_names():
with self.subTest(icon=name):
image = QImage(32, 32, QImage.Format.Format_ARGB32_Premultiplied)
image.fill(0)
painter = QPainter(image)
style.paint_tray_icon(painter, name, QRectF(0, 0, 32, 32), QColor("#ffffff"))
painter.end()
self.assertTrue(any(image.pixel(x, y) for x in range(32) for y in range(32)))
def test_tray_menu_does_not_use_library_menu(self) -> None:
root = Path(__file__).resolve().parents[1] / "src"
sources = [root / "tray.py", *sorted((root / "ui" / "tray_menu").glob("*.py"))]
for path in sources:
with self.subTest(file=path.name):
source = path.read_text(encoding="utf-8")
self.assertNotIn("RoundMenu", source)
self.assertNotIn("QMenu", source)
def test_tray_icon_dot_color_by_phase(self) -> None:
import tray
self.assertEqual(tray.tray_icon_dot_color("running"), "#6ccb5f")
self.assertEqual(tray.tray_icon_dot_color("starting"), "#f5a623")
self.assertEqual(tray.tray_icon_dot_color("stopping"), "#f5a623")
self.assertIsNone(tray.tray_icon_dot_color("stopped"))
self.assertIsNone(tray.tray_icon_dot_color("failed"))
def test_status_icon_draws_dot_in_corner_and_keeps_logo(self) -> None:
import tray
from PyQt6.QtGui import QColor, QImage
base = QImage(64, 64, QImage.Format.Format_ARGB32)
base.fill(QColor("#3366cc"))
image = tray.render_status_icon_image(base, "#6ccb5f", 20)
plain = tray.render_status_icon_image(base, None, 20)
self.assertEqual(QColor(image.pixel(16, 16)).name(), "#6ccb5f")
self.assertEqual(QColor(image.pixel(4, 4)).name(), "#3366cc")
self.assertEqual(QColor(plain.pixel(16, 16)).name(), "#3366cc")
def test_tray_tooltip_has_mode_status_and_preset(self) -> None:
import tray
tooltip = tray.build_tray_tooltip(
phase="running",
launch_method="zapret2_mode",
preset_name="Default v1",
language="ru",
)
self.assertEqual(tooltip, "Zapret 2 — работает\nПресет: Default v1")
def test_start_notification_only_when_window_hidden(self) -> None:
for visible, expected in ((False, 1), (True, 0)):
with self.subTest(window_visible=visible):
manager, _feature, _port = self._manager(visible=visible)
manager.show_notification = Mock()
manager._status_icon_handle_for = Mock(return_value=None)
manager._modify_icon = Mock()
manager.apply_launch_status(phase="starting", launch_method="zapret2_mode", preset_name="Default v1")
manager.apply_launch_status(phase="running", launch_method="zapret2_mode", preset_name="Default v1")
self.assertEqual(manager.show_notification.call_count, expected)
if expected:
title, body = manager.show_notification.call_args.args
self.assertEqual(title, "Zapret запущен")
self.assertIn("Default v1", body)
def test_first_status_does_not_notify(self) -> None:
manager, _feature, _port = self._manager()
manager.show_notification = Mock()
manager._status_icon_handle_for = Mock(return_value=None)
manager._modify_icon = Mock()
manager.apply_launch_status(phase="running", launch_method="zapret2_mode", preset_name="")
manager.show_notification.assert_not_called()
manager._modify_icon.assert_called_once_with()
def test_stop_notification_is_skipped_when_restart_follows(self) -> None:
import tray
manager, _feature, _port = self._manager()
manager.show_notification = Mock()
manager._status_icon_handle_for = Mock(return_value=None)
manager._modify_icon = Mock()
timers = []
class _Timer:
def __init__(self) -> None:
self.stopped = False
self.callback = None
timers.append(self)
def setSingleShot(self, _value) -> None: # noqa: N802
return None
@property
def timeout(self):
return SimpleNamespace(connect=lambda callback: setattr(self, "callback", callback))
def start(self, _ms) -> None:
return None
def stop(self) -> None:
self.stopped = True
with patch.object(tray, "QTimer", _Timer):
manager.apply_launch_status(phase="running", launch_method="zapret2_mode", preset_name="")
manager.apply_launch_status(phase="stopping", launch_method="zapret2_mode", preset_name="")
manager.apply_launch_status(phase="stopped", launch_method="zapret2_mode", preset_name="")
self.assertEqual(len(timers), 1)
manager.apply_launch_status(phase="starting", launch_method="zapret2_mode", preset_name="")
self.assertTrue(timers[0].stopped)
manager.show_notification.assert_not_called()
manager._launch_phase = "stopped"
manager._show_stopped_notification()
manager.show_notification.assert_called_once_with("Zapret остановлен", "Обход блокировок выключен")
def test_tray_feature_toggles_through_launch_control(self) -> None:
from app.feature_facades.tray import TrayFeature
launch_control = Mock()
runtime_feature = Mock()
feature = TrayFeature(
_deps=SimpleNamespace(),
_runtime_feature=runtime_feature,
_telegram_proxy_feature=SimpleNamespace(),
)
feature.configure(launch_control=launch_control)
feature.toggle_dpi()
feature.restart_dpi()
launch_control.toggle.assert_called_once_with()
launch_control.restart.assert_called_once_with()
runtime_feature.start.assert_not_called()
runtime_feature.stop.assert_not_called()
def test_tray_feature_reads_phase_from_ui_state_store(self) -> None:
from app.feature_facades.tray import TrayFeature
store = SimpleNamespace(snapshot=lambda: SimpleNamespace(launch_phase="stopping", launch_running=True))
feature = TrayFeature(
_deps=SimpleNamespace(),
_runtime_feature=SimpleNamespace(),
_telegram_proxy_feature=SimpleNamespace(),
_ui_state_store=store,
)
self.assertEqual(feature.launch_phase(), "stopping")
def test_tray_feature_activate_preset_moves_checkmark_and_starts_worker(self) -> None:
from app.feature_facades.tray import TrayFeature
presets_feature = Mock()
feature = TrayFeature(
_deps=SimpleNamespace(),
_runtime_feature=SimpleNamespace(),
_telegram_proxy_feature=SimpleNamespace(),
_presets_feature=presets_feature,
)
feature._preset_snapshot = self._snapshot()
started = []
feature._preset_activate_runtime = SimpleNamespace(
start_qthread_worker=lambda **kwargs: started.append(kwargs)
)
self.assertFalse(feature.activate_preset("default.txt", "Default v1"))
self.assertTrue(feature.activate_preset("game.txt", "Game filter"))
self.assertTrue(feature.preset_snapshot().is_selected("game.txt"))
self.assertEqual(len(started), 1)
started[0]["worker_factory"](7)
presets_feature.create_preset_activate_worker.assert_called_once_with(
7,
launch_method="zapret2_mode",
file_name="game.txt",
display_name="Game filter",
activate_error_level="error",
activate_error_mode="friendly",
parent=None,
)
def test_tray_preset_snapshot_loader_marks_selected_preset(self) -> None:
from tray_workers import load_tray_preset_snapshot
presets_feature = SimpleNamespace(
list_preset_manifests=lambda _method: [
SimpleNamespace(file_name="Default.txt", name="Default v1"),
SimpleNamespace(file_name="game.txt", name="Game filter"),
],
get_selected_source_preset_file_name=lambda _method: "default.txt",
)
snapshot = load_tray_preset_snapshot(get_launch_method=lambda: "zapret2_mode", presets_feature=presets_feature)
orchestra = load_tray_preset_snapshot(get_launch_method=lambda: "orchestra", presets_feature=presets_feature)
self.assertEqual(snapshot.selected_display_name, "Default v1")
self.assertTrue(snapshot.is_selected("DEFAULT.TXT"))
self.assertEqual(orchestra.presets, ())
def test_native_tray_winapi_calls_have_pointer_safe_ctypes_signatures(self) -> None:
source = (Path(__file__).resolve().parents[1] / "src" / "tray.py").read_text(encoding="utf-8")
required_signatures = [
"kernel32.GetModuleHandleW.restype",
"user32.RegisterClassExW.argtypes",
"user32.RegisterClassExW.restype",
"user32.UnregisterClassW.argtypes",
"user32.UnregisterClassW.restype",
"user32.CreateWindowExW.argtypes",
"user32.CreateWindowExW.restype",
"user32.DestroyWindow.argtypes",
"user32.DestroyWindow.restype",
"user32.LoadImageW.argtypes",
"user32.LoadImageW.restype",
"user32.LoadIconW.argtypes",
"user32.LoadIconW.restype",
"user32.DestroyIcon.argtypes",
"user32.DestroyIcon.restype",
"shell32.Shell_NotifyIconW.argtypes",
"shell32.Shell_NotifyIconW.restype",
]
missing = [signature for signature in required_signatures if signature not in source]
self.assertEqual(missing, [])
def test_native_tray_message_window_unregisters_class_on_destroy(self) -> None:
source = (Path(__file__).resolve().parents[1] / "src" / "tray.py").read_text(encoding="utf-8")
self.assertIn("user32.UnregisterClassW(self.owner._class_name, instance)", source)
def test_native_tray_class_name_is_unique_per_manager_instance(self) -> None:
source = (Path(__file__).resolve().parents[1] / "src" / "tray.py").read_text(encoding="utf-8")
self.assertIn('self._class_name = f"Zapret2TrayWindow_{os.getpid()}_{id(self):x}"', source)
if __name__ == "__main__":
unittest.main()