Files
zyt/app/tests/test_screenshot_preview_size.py
T
2026-10-29 14:01:39 +08:00

323 lines
11 KiB
Python

from __future__ import annotations
import os
from types import SimpleNamespace
from typing import Any
from unittest.mock import Mock
import pytest
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PySide6.QtCore import QRect, QSize, Qt, QUrl
from PySide6.QtWidgets import QApplication, QMainWindow
from doctor_workstation.app import ApplicationController
from doctor_workstation.ui import theme
from doctor_workstation.video import window as video_window
from doctor_workstation.video.screenshot import ScreenshotPreviewSize
from doctor_workstation.video.security import TrustedDocumentPolicy
TRUSTED_URL = "https://video.example.test/companion/index.html?mode=call"
class _Window:
"""Supply geometry and state only, so preview sizing cannot alter other settings."""
def __init__(
self,
available: QRect | None = None,
geometry: QRect | None = None,
) -> None:
self.current_geometry = QRect(geometry) if geometry is not None else QRect(1362, 18, 540, 356)
self.available = QRect(available) if available is not None else QRect(0, 0, 1920, 1040)
self.compact_flag: bool | None = True
self.maximized = False
self.full_screen = False
self.geometry_changes: list[QRect] = []
def screen(self) -> SimpleNamespace:
return SimpleNamespace(availableGeometry=lambda: QRect(self.available))
def geometry(self) -> QRect:
return QRect(self.current_geometry)
def setGeometry(self, geometry: QRect) -> None: # noqa: N802 - Qt-compatible double
self.current_geometry = QRect(geometry)
self.geometry_changes.append(QRect(geometry))
def property(self, name: str) -> bool | None:
assert name == "doctorSkipOpenGeometry"
return self.compact_flag
def isMaximized(self) -> bool: # noqa: N802 - Qt-compatible double
return self.maximized
def isFullScreen(self) -> bool: # noqa: N802 - Qt-compatible double
return self.full_screen
@pytest.fixture
def qapp() -> QApplication:
return QApplication.instance() or QApplication([])
@pytest.mark.parametrize(
("available", "target", "expected_size"),
[
(QRect(0, 0, 1920, 1040), None, QSize(1120, 760)),
(QRect(-1920, -120, 1920, 1040), None, QSize(1120, 760)),
(QRect(-1280, 180, 1280, 720), QRect(0, 0, 1400, 950), QSize(1232, 672)),
(QRect(100, 50, 800, 600), None, QSize(752, 552)),
(QRect(0, 0, 1920, 1040), QRect(40, 60, 1360, 850), QSize(1360, 850)),
],
ids=["default", "negative-monitor", "bounded-target", "small-screen", "normal-target"],
)
def test_preview_expands_within_current_screen_and_restores(
available: QRect, target: QRect | None, expected_size: QSize
) -> None:
window = _Window(available, QRect(available.x() + 30, available.y() + 30, 440, 300))
original = window.geometry()
preview = ScreenshotPreviewSize()
preview.open(window, target)
expanded = window.geometry()
assert expanded.size() == expected_size
assert available.adjusted(24, 24, -24, -24).contains(expanded)
assert abs(expanded.center().x() - available.center().x()) <= 1
assert abs(expanded.center().y() - available.center().y()) <= 1
assert window.compact_flag is True
preview.restore(window)
assert window.geometry() == original
assert window.compact_flag is True
def test_repeated_open_preserves_first_geometry_and_restore_is_idempotent() -> None:
window = _Window()
original = window.geometry()
preview = ScreenshotPreviewSize()
preview.open(window)
expanded = window.geometry()
preview.open(window, QRect(0, 0, 1500, 900))
assert window.geometry() == expanded
assert len(window.geometry_changes) == 1
preview.restore(window)
preview.restore(window)
assert window.geometry() == original
assert window.geometry_changes == [expanded, original]
@pytest.mark.parametrize("state", ["maximized", "full_screen", "large", "no-screen", "tiny-screen"])
def test_preview_does_not_modify_windows_that_do_not_need_expansion(state: str) -> None:
window = _Window()
if state == "large":
window.current_geometry = QRect(25, 30, 1300, 850)
elif state == "no-screen":
window.screen = lambda: None # type: ignore[method-assign, return-value]
elif state == "tiny-screen":
window.available = QRect(0, 0, 40, 40)
else:
setattr(window, state, True)
original = window.geometry()
preview = ScreenshotPreviewSize()
preview.open(window)
preview.restore(window)
assert window.geometry() == original
assert window.geometry_changes == []
@pytest.mark.parametrize("change", ["move", "resize", "compact-flag", "maximize", "fullscreen"])
def test_restore_preserves_external_window_changes_and_forgets_old_snapshot(change: str) -> None:
window = _Window()
preview = ScreenshotPreviewSize()
preview.open(window)
expanded = window.geometry()
if change == "move":
window.current_geometry.translate(40, 15)
elif change == "resize":
window.current_geometry.setWidth(window.current_geometry.width() + 50)
elif change == "compact-flag":
window.compact_flag = False
elif change == "maximize":
window.maximized = True
else:
window.full_screen = True
external_geometry = window.geometry()
writes_before_restore = len(window.geometry_changes)
preview.restore(window)
assert window.geometry() == external_geometry
assert len(window.geometry_changes) == writes_before_restore
# Even when those external changes later disappear, a late close is harmless.
window.current_geometry = expanded
window.compact_flag = True
window.maximized = False
window.full_screen = False
preview.restore(window)
assert window.geometry() == expanded
assert len(window.geometry_changes) == writes_before_restore
def test_real_window_preserves_theme_opt_out_minimum_and_flags(qapp: QApplication) -> None:
theme._install_business_dialog_styling(qapp)
main = QMainWindow()
main.setProperty("doctorMainWindow", True)
main.setGeometry(30, 30, 980, 720)
main.show()
window = QMainWindow()
window.setProperty("doctorSkipOpenGeometry", True)
window.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True)
window.setMinimumSize(440, 300)
window.setGeometry(30, 30, 440, 300)
window.show()
qapp.processEvents()
original = window.geometry()
flags = window.windowFlags()
minimum = window.minimumSize()
preview = ScreenshotPreviewSize()
try:
preview.open(window, theme.main_window_target_rect(window))
qapp.processEvents()
assert window.width() > original.width()
assert window.height() > original.height()
assert window.minimumSize() == minimum
assert window.windowFlags() == flags
assert window.property("doctorSkipOpenGeometry") is True
preview.restore(window)
qapp.processEvents()
assert window.geometry() == original
assert window.minimumSize() == minimum
assert window.windowFlags() == flags
assert window.property("doctorSkipOpenGeometry") is True
finally:
window.close()
main.close()
def test_diagnosis_restore_clears_screenshot_layer_before_restoring_large_window(
qapp: QApplication,
) -> None:
theme._install_business_dialog_styling(qapp)
window = QMainWindow()
window.setProperty("doctorSkipOpenGeometry", True)
window.setMinimumSize(440, 300)
window.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True)
window.setGeometry(30, 30, 440, 300)
window.show()
qapp.processEvents()
compact = window.geometry()
normal = QRect(40, 50, 670, 500)
preview = ScreenshotPreviewSize()
restored_layers: list[QRect] = []
def restore_screenshot_preview() -> None:
preview.restore(window)
restored_layers.append(window.geometry())
window.restore_screenshot_preview = restore_screenshot_preview
controller = SimpleNamespace(
_video_preview_generation=4,
_video_preview_state={
"window": window,
"geometry": normal,
"minimum_size": QSize(500, 360),
"maximized": False,
"full_screen": False,
"stays_on_top": False,
},
)
try:
preview.open(window, QRect(0, 0, 600, 450))
ApplicationController._restore_video_preview(controller, 4, activate=False)
qapp.processEvents()
assert restored_layers == [compact]
assert window.geometry() == normal
assert window.minimumSize() == QSize(500, 360)
assert not window.windowFlags() & Qt.WindowType.WindowStaysOnTopHint
assert window.property("doctorSkipOpenGeometry") is False
assert controller._video_preview_state is None
assert controller._video_preview_generation == 5
restore_screenshot_preview()
assert window.geometry() == normal
finally:
window.close()
def _host(**changes: Any) -> SimpleNamespace:
return SimpleNamespace(
**{
"_closing": False,
"_released": False,
"_shutdown_requested": False,
"_media_active": True,
"_policy": TrustedDocumentPolicy.from_url(TRUSTED_URL, is_local=False),
"_page": SimpleNamespace(url=lambda: QUrl(TRUSTED_URL)),
"_screenshot_preview_size": SimpleNamespace(open=Mock()),
"restore_screenshot_preview": Mock(),
**changes,
}
)
@pytest.mark.parametrize(
"changes",
[
{"_closing": True},
{"_released": True},
{"_shutdown_requested": True},
{"_media_active": False},
{"_page": SimpleNamespace(url=lambda: QUrl("about:blank"))},
{"_page": SimpleNamespace(url=lambda: QUrl("https://untrusted.example.test/"))},
],
ids=["closing", "released", "shutdown", "inactive", "blank-document", "untrusted-document"],
)
def test_host_does_not_open_preview_when_inactive_or_untrusted(changes: dict[str, Any]) -> None:
host = _host(**changes)
video_window._EmbeddedVideoWindow._set_screenshot_preview_visible(host, True)
host._screenshot_preview_size.open.assert_not_called()
host.restore_screenshot_preview.assert_not_called()
def test_host_uses_normal_window_target_for_trusted_preview(monkeypatch: pytest.MonkeyPatch) -> None:
host = _host()
target = QRect(30, 40, 1360, 850)
target_for_window = Mock(return_value=target)
monkeypatch.setattr(theme, "main_window_target_rect", target_for_window)
video_window._EmbeddedVideoWindow._set_screenshot_preview_visible(host, True)
target_for_window.assert_called_once_with(host)
host._screenshot_preview_size.open.assert_called_once_with(host, target)
host.restore_screenshot_preview.assert_not_called()
@pytest.mark.parametrize("changes", [{}, {"_shutdown_requested": True}, {"_media_active": False}])
def test_host_allows_preview_cleanup_after_shutdown_request(changes: dict[str, Any]) -> None:
host = _host(**changes)
video_window._EmbeddedVideoWindow._set_screenshot_preview_visible(host, False)
host.restore_screenshot_preview.assert_called_once_with()
host._screenshot_preview_size.open.assert_not_called()