aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/tests/test_screenshot.py
blob: 2a01eb139e7db68aa813fe1cb46c81673b168569 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
"""Tests for screenshot.py — the pure helpers behind Wayland screenshot capture.

The script is mostly thin wrappers around grim/hyprctl (I/O boundaries, verified
functionally). The logic worth testing is pure: size parsing, grim geometry
strings, window matching, the Hyprland exec-rule body, and floating-window
centering. Those are imported and exercised directly here; the subprocess
wrappers and the capture orchestration are not unit-tested.
"""

import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).parent.parent))
import screenshot  # noqa: E402


# ------------------------------- parse_size ----------------------------------

def test_parse_size_normal():
    assert screenshot.parse_size("1600x1000") == (1600, 1000)

def test_parse_size_boundary_zero_and_one():
    assert screenshot.parse_size("0x0") == (0, 0)
    assert screenshot.parse_size("1x1") == (1, 1)

@pytest.mark.parametrize("bad", ["", "bad", "100", "100x", "x100", "100X100", " 1x1"])
def test_parse_size_error_malformed_dies(bad):
    with pytest.raises(SystemExit):
        screenshot.parse_size(bad)


# ------------------------------ geometry_str ---------------------------------

def test_geometry_str_normal():
    assert screenshot.geometry_str({"at": [10, 20], "size": [800, 600]}) == "10,20 800x600"

def test_geometry_str_boundary_origin_and_unit():
    assert screenshot.geometry_str({"at": [0, 0], "size": [1, 1]}) == "0,0 1x1"


# ------------------------------ match_windows --------------------------------

CLIENTS = [
    {"class": "Emacs", "title": "Emacs 30.2 : agent [.emacs.d]"},
    {"class": "firefox", "title": "TrueNAS — Mozilla Firefox"},
    {"class": "foot", "title": "foot"},
]

def test_match_windows_normal_by_class():
    assert screenshot.match_windows(CLIENTS, "firefox") == [CLIENTS[1]]

def test_match_windows_normal_by_title():
    assert screenshot.match_windows(CLIENTS, "TrueNAS") == [CLIENTS[1]]

def test_match_windows_case_insensitive():
    assert screenshot.match_windows(CLIENTS, "EMACS") == [CLIENTS[0]]

def test_match_windows_multiple():
    # "o" appears in firefox and foot classes
    assert len(screenshot.match_windows(CLIENTS, "foo")) == 1
    assert len(screenshot.match_windows(CLIENTS, "e")) >= 2  # Emacs + firefox (title/class)

def test_match_windows_boundary_empty_clients():
    assert screenshot.match_windows([], "anything") == []

def test_match_windows_boundary_missing_fields():
    # client with no class/title keys must not raise
    assert screenshot.match_windows([{"class": None, "title": None}], "x") == []

def test_match_windows_error_no_match():
    assert screenshot.match_windows(CLIENTS, "nonexistent-zzz") == []


# ------------------------------- launch_rule ---------------------------------

def test_launch_rule_tiled_default():
    assert screenshot.launch_rule(2, "tiled") == "workspace 2 silent"

def test_launch_rule_monocle_adds_fullscreen():
    assert screenshot.launch_rule(2, "monocle") == "workspace 2 silent;fullscreen 1"

def test_launch_rule_floating_adds_float():
    assert screenshot.launch_rule(7, "floating") == "workspace 7 silent;float"


# ------------------------------ center_offset --------------------------------

def test_center_offset_normal():
    assert screenshot.center_offset(1920, 1080, 800, 600) == (560, 240)

def test_center_offset_boundary_exact_fit():
    assert screenshot.center_offset(1920, 1080, 1920, 1080) == (0, 0)

def test_center_offset_error_window_larger_than_output_clamps():
    assert screenshot.center_offset(800, 600, 1000, 700) == (0, 0)


# ------------------------------- wayland_cmd ---------------------------------
# --launch must not let the app map via XWayland: an XWayland configure request
# can race the headless-output teardown and crash the compositor.

def test_wayland_cmd_unsets_display_and_forces_wayland():
    wrapped = screenshot.wayland_cmd("emacs -Q")
    assert wrapped.startswith("env -u DISPLAY ")
    assert "GDK_BACKEND=wayland" in wrapped
    assert "QT_QPA_PLATFORM=wayland" in wrapped
    assert wrapped.endswith(" emacs -Q")

def test_wayland_cmd_boundary_preserves_quoted_args():
    wrapped = screenshot.wayland_cmd("foot -e sh -c 'echo hi'")
    assert wrapped.endswith(" foot -e sh -c 'echo hi'")


# --------------------------- clients_on_workspace ----------------------------

WS_CLIENTS = [
    {"address": "0x1", "workspace": {"id": 3}},
    {"address": "0x2", "workspace": {"id": 5}},
    {"address": "0x3", "workspace": {"id": 3}},
]

def test_clients_on_workspace_normal():
    got = screenshot.clients_on_workspace(WS_CLIENTS, 3)
    assert [c["address"] for c in got] == ["0x1", "0x3"]

def test_clients_on_workspace_boundary_empty_list():
    assert screenshot.clients_on_workspace([], 3) == []

def test_clients_on_workspace_boundary_missing_workspace_key():
    assert screenshot.clients_on_workspace([{"address": "0x9"}], 3) == []

def test_clients_on_workspace_error_no_match():
    assert screenshot.clients_on_workspace(WS_CLIENTS, 99) == []