utils/gatelock/gatelock/tests/test_window.py
Krzysztof kuhy Rudnicki 68ec316b28
Some checks failed
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
freedays pre-commit / pre-commit (push) Has been cancelled
freedays tests / test (push) Has been cancelled
gatelock 0.8.0: LockConfig as behaviour + GrabPolicy + theme records; pylint at 10
LockConfig was one 35-field record: the lock's behaviour, the grab
policy, nine colours, a type scale and a spacing scale, flat. It is
now the seven behavioural knobs plus four nested records --
GrabPolicy(kind, disable_vt, retry_ms, log_every, preempt_weaker_holder),
LockPalette, LockTypography and LockSpacing -- each overridden as a
unit. Breaking for every constructor that passed grab=/disable_vt=/
grab_retry_ms=/preempt_weaker_holder= or a colour, and for every read
of config.<colour>; hence the minor bump. screen-locker, leetcode-guard
and diet-guard follow in their own commits.

The two fail-open guards in the recovery loop carry per-line
suppressions: catching everything is the property they exist for, and
the check stays live for the next careless except. That was the last
finding, so the gate now says 10.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHCbkJWXHjtTUaqvnHRAqA
2026-09-12 19:36:31 +02:00

151 lines
5.8 KiB
Python

# Copyright (c) 2026 Krzysztof Rudnicki
"""Tests for LockConfig and LockWindow."""
from __future__ import annotations
import tkinter as tk
from unittest.mock import MagicMock, patch
from gatelock._config import GrabPolicy
from gatelock._theme import LockPalette
from gatelock._window import LockConfig
from gatelock.tests.conftest import make_window
class TestLockConfigResolution:
"""Tests for LockConfig's per-axis resolution logic."""
def test_hard_mode_defaults(self) -> None:
"""Hard mode resolves to overrideredirect + global grab + VT disable."""
config = LockConfig(mode="hard")
assert config.resolved_overrideredirect() is True
assert config.resolved_grab() == "global"
assert config.resolved_disable_vt() is True
def test_soft_mode_defaults(self) -> None:
"""Soft mode resolves to no overrideredirect, no grab, no VT disable."""
config = LockConfig(mode="soft")
assert config.resolved_overrideredirect() is False
assert config.resolved_grab() == "none"
assert config.resolved_disable_vt() is False
def test_explicit_overrides_win_over_mode(self) -> None:
"""Explicit fields override the mode preset (screen-locker demo case)."""
config = LockConfig(
mode="soft", overrideredirect=True, grab=GrabPolicy(kind="local")
)
assert config.resolved_overrideredirect() is True
assert config.resolved_grab() == "local"
assert config.resolved_disable_vt() is False
def test_explicit_false_overrides_hard_mode(self) -> None:
"""An explicit False/none is respected even under mode="hard"."""
config = LockConfig(
mode="hard",
overrideredirect=False,
grab=GrabPolicy(kind="none", disable_vt=False),
)
assert config.resolved_overrideredirect() is False
assert config.resolved_grab() == "none"
assert config.resolved_disable_vt() is False
class TestSetup:
"""Tests for LockWindow.setup."""
def test_hard_mode_sets_overrideredirect_and_disables_vt(
self, mock_root: MagicMock
) -> None:
"""Hard mode calls overrideredirect and disables VT switching."""
window, _hooks = make_window(mock_root, config=LockConfig(mode="hard"))
with patch(
"gatelock._arming.disable_vt_switching", return_value=True
) as mock_disable:
window.setup()
mock_root.overrideredirect.assert_called_once_with(boolean=True)
# No attributes(fullscreen=True): that is an EWMH request an
# override-redirect window is invisible to, and where it does apply it
# snaps to one monitor or the whole bounding box. Per-output surfaces
# use explicit geometry instead.
assert not any(
call.kwargs.get("fullscreen") or "-fullscreen" in call.args
for call in mock_root.attributes.call_args_list
)
mock_disable.assert_called_once()
assert window._vt_disabled is True
def test_soft_mode_skips_overrideredirect_and_vt(
self, mock_root: MagicMock
) -> None:
"""Soft mode never calls overrideredirect or disables VT switching."""
window, _hooks = make_window(mock_root, config=LockConfig(mode="soft"))
with patch("gatelock._arming.disable_vt_switching") as mock_disable:
window.setup()
mock_root.overrideredirect.assert_not_called()
mock_disable.assert_not_called()
assert window._vt_disabled is False
def test_uses_configured_background(self, mock_root: MagicMock) -> None:
"""The configured bg color is passed to root.configure."""
window, _hooks = make_window(
mock_root, config=LockConfig(mode="soft", palette=LockPalette(bg="#000000"))
)
window.setup()
# Soft mode with no grab needs no backdrop, so the root is left to the
# window manager and only the surfaces carry the colour.
assert window.surfaces.infos()
topmost_on = True
mock_root.attributes.assert_any_call("-topmost", topmost_on)
class TestGrabInput:
"""Tests for LockWindow.grab_input."""
def test_global_grab_dispatches_to_acquire(self, mock_root: MagicMock) -> None:
"""grab="global" triggers the retry-aware acquisition path."""
window, _hooks = make_window(
mock_root, config=LockConfig(mode="soft", grab=GrabPolicy(kind="global"))
)
with patch("gatelock._arming.acquire_global_grab") as mock_acquire:
window.grab_input()
mock_acquire.assert_called_once_with(window.root, window._arming, attempt=1)
mock_root.after.assert_any_call(100, window._notify_focus_ready)
def test_local_grab_calls_grab_set(self, mock_root: MagicMock) -> None:
"""grab="local" calls grab_set directly, no retry logic."""
window, _hooks = make_window(
mock_root, config=LockConfig(mode="soft", grab=GrabPolicy(kind="local"))
)
window.grab_input()
mock_root.grab_set.assert_called_once_with()
def test_local_grab_swallows_tclerror(self, mock_root: MagicMock) -> None:
"""A TclError from grab_set (e.g. window already gone) is swallowed."""
mock_root.grab_set.side_effect = tk.TclError("gone")
window, _hooks = make_window(
mock_root, config=LockConfig(mode="soft", grab=GrabPolicy(kind="local"))
)
window.grab_input() # must not raise
def test_none_grab_takes_no_grab_action(self, mock_root: MagicMock) -> None:
"""grab="none" calls neither grab_set nor grab_set_global."""
window, _hooks = make_window(
mock_root, config=LockConfig(mode="soft", grab=GrabPolicy(kind="none"))
)
window.grab_input()
mock_root.grab_set.assert_not_called()
mock_root.grab_set_global.assert_not_called()