aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_zoom.py
blob: 821987f89c08c816e56c4457c48d38dd75a566c0 (plain) (blame)
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
#!/usr/bin/env python3
"""Zooming a pane, and the buttons in the frame that do it.

Zoom is intent, not a derived state: nothing about the tree says a pane fills
its tab, you said so. So the things worth checking are that it survives the
layout being recomputed, that it cannot outlive the pane it names, and that it
is per tab.
"""

import os
import re
import subprocess
import sys
import tempfile

from harness import BIN, Session, check, report

SH = ["/bin/sh", "-c", 'printf "\\033]2;p\\007"; stty raw -echo; cat']


def mark(name):
    """A chrome mark as the binary currently draws it. Read from
    `--dump-config`, which is generated from the code, rather than written down
    here -- a test that pins a glyph starts failing the day somebody picks a
    nicer one, and says nothing useful when it does."""
    out = subprocess.run([BIN, "--dump-config"], capture_output=True, text=True).stdout
    m = re.search(name + r' "(.+)"', out)
    return m.group(1) if m else "?"


ZOOM, ZOOM_ON = mark("zoom_mark"), mark("zoom_on_mark")


def cfg(text):
    f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
    f.write(text)
    f.close()
    return f.name


def visible(s):
    """Visible panes *in the current tab*.

    Only the current tab is laid out, so a pane in any other one carries
    whatever rect and hidden flag it had when its tab was last on screen --
    stale by design, and not an answer to "is it visible now"."""
    active = [t["index"] for t in s.tabs() if t["active"]][0]
    return [p for p in s.panes() if p["tab"] == active and not p["hidden"]]


def button(s, verb, pane_id=None):
    """The button's *current* rect. Zooming moves it, because the pane it
    belongs to just changed size."""
    if pane_id is None:
        pane_id = [p for p in s.panes() if p["focused"]][0]["id"]
    hits = [h for h in s.snapshot().hits if h["action"] == f"{verb}:{pane_id}"]
    return hits[0] if hits else None


def test_zoom_fills_the_tab_and_puts_it_back():
    with Session(SH, cols=90, rows=18) as s:
        s.settle(20)
        s.api("split", dir="cols")
        s.settle(20)
        check("two panes to start", len(visible(s)) == 2, str(s.panes()))

        s.send(r"\x01z")
        s.settle(20)
        check("zooming leaves one pane on screen", len(visible(s)) == 1, str(s.panes()))
        check("but does not close the other", len(s.panes()) == 2, str(s.panes()))
        check(
            "and the zoomed pane has the whole tab",
            visible(s)[0]["w"] > 80,
            str(visible(s)[0]),
        )

        s.send(r"\x01z")
        s.settle(20)
        check("unzooming puts them both back", len(visible(s)) == 2, str(s.panes()))


def test_the_button_toggles_and_says_which_way():
    with Session(SH, cols=90, rows=18) as s:
        s.settle(20)
        s.api("split", dir="cols")
        s.settle(20)

        b = button(s, "zoom")
        check("a pane's frame carries a zoom button", b is not None, "")
        if not b:
            return
        s.click(b["x"] + 1, b["y"])
        s.settle(20)
        check("clicking it zooms", len(visible(s)) == 1, str(s.panes()))
        check(
            "and the mark says so",
            ZOOM_ON in s.snapshot().line(2),
            repr(s.snapshot().line(2)[-14:]),
        )

        # The button moved: the pane it belongs to just became the whole tab.
        b = button(s, "zoom")
        s.click(b["x"] + 1, b["y"])
        s.settle(20)
        check("clicking it again puts it back", len(visible(s)) == 2, str(s.panes()))
        check(
            "and the mark goes back too",
            ZOOM in s.snapshot().line(2),
            repr(s.snapshot().line(2)[-14:]),
        )


def test_the_close_button_closes_that_pane():
    with Session(SH, cols=90, rows=18) as s:
        s.settle(20)
        s.api("split", dir="cols")
        s.settle(20)
        panes = s.panes()
        victim = panes[0]["id"]
        b = button(s, "close", victim)
        check("a pane's frame carries a close button", b is not None, "")
        if not b:
            return
        s.click(b["x"] + 1, b["y"])
        s.settle(20)
        left = [p["id"] for p in s.panes()]
        check(
            "it closes the pane whose frame it is on",
            victim not in left and len(left) == 1,
            str(left),
        )


def test_a_zoom_cannot_outlive_its_pane():
    with Session(SH, cols=90, rows=18) as s:
        s.settle(20)
        s.api("split", dir="cols")
        s.settle(20)
        s.send(r"\x01z")
        s.settle(20)
        check("zoomed", len(visible(s)) == 1, str(s.panes()))

        s.api("close", id=[p for p in s.panes() if p["focused"]][0]["id"])
        s.settle(20)
        check(
            "closing the zoomed pane leaves the tab usable",
            len(visible(s)) == 1 and len(s.panes()) == 1,
            str(s.panes()),
        )
        check(
            "and the survivor is not stuck hidden",
            not s.panes()[0]["hidden"],
            str(s.panes()),
        )


def test_zoom_is_per_tab():
    lay = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
    lay.write(
        'layout {\n tab name="a" {\n  pane\n  pane\n }\n'
        ' tab name="b" {\n  pane\n  pane\n }\n}\n'
    )
    lay.close()
    with Session(SH, cols=90, rows=18, layout=lay.name) as s:
        s.settle(20)
        s.send(r"\x01z")
        s.settle(20)
        check("tab a is zoomed", len(visible(s)) == 1, str(s.panes()))

        tabs = s.tabs()
        s.api("select-tab", id=tabs[1]["id"])
        s.settle(20)
        check(
            "tab b is not, because zoom belongs to a tab",
            len(visible(s)) == 2,
            str(s.panes()),
        )

        s.api("select-tab", id=tabs[0]["id"])
        s.settle(20)
        check(
            "and tab a is still zoomed when you come back",
            len(visible(s)) == 1,
            str(s.panes()),
        )
    os.unlink(lay.name)


def test_the_status_line_says_zoomed():
    with Session(SH, cols=90, rows=18) as s:
        s.settle(20)
        s.api("split", dir="cols")
        s.settle(20)
        check(
            "nothing said while it is not",
            "zoomed" not in s.snapshot().text[-2],
            repr(s.snapshot().text[-2]),
        )
        s.send(r"\x01z")
        s.settle(20)
        check(
            "a zoomed pane says so",
            "zoomed" in s.snapshot().text[-2],
            repr(s.snapshot().text[-2]),
        )


def test_the_buttons_can_be_turned_off():
    conf = cfg("pane_buttons false\n")
    with Session(SH, cols=90, rows=18, config=conf) as s:
        s.settle(20)
        hits = [
            h for h in s.snapshot().hits if h["action"].startswith(("zoom:", "close:"))
        ]
        check("pane_buttons false draws none", hits == [], str(hits))
        # ...but the verb is still there from the keyboard: the setting is
        # about the affordance, not about zooming.
        s.send(r"\x01z")
        s.settle(20)
        check(
            "while the keybinding still zooms",
            "zoomed" in s.snapshot().text[-2],
            repr(s.snapshot().text[-2]),
        )
    os.unlink(conf)


def test_a_new_pane_unzooms():
    """A pane arriving in a zoomed tab would land behind the pane filling it,
    which looks exactly like the split having failed — so asking for a new
    pane implicitly asks for the layout back."""
    with Session(SH, cols=90, rows=24) as s:
        s.settle(20)
        s.api("split", dir="cols")
        s.settle(20)
        s.send(r"\x01z")
        s.settle(20)
        check("zoomed: one pane on screen", len(visible(s)) == 1, str(visible(s)))
        s.send(r"\x01-")
        s.settle(30)
        vis = visible(s)
        check("a split unzooms", len(vis) == 3, str(vis))
        check(
            "and the status stops saying so",
            "zoomed" not in s.snapshot().text[-2],
            repr(s.snapshot().text[-2]),
        )
        check(
            "the new pane holds focus",
            [p for p in vis if p["focused"]] != [],
            str(vis),
        )


def test_a_new_float_unzooms():
    """Floats are hidden while a tab is zoomed, so a new float opened over a
    zoomed tab would be invisible on arrival. Same rule, same fix."""
    with Session(SH, cols=90, rows=24) as s:
        s.settle(20)
        s.api("split", dir="cols")
        s.settle(20)
        s.send(r"\x01z")
        s.settle(20)
        r = s.api("new-float")
        s.settle(30)
        check("the float opened", r.get("ok") and r.get("id"), str(r))
        fl = [p for p in visible(s) if p["floating"] and p["w"] > 1]
        check("and is on screen, not behind a zoom", len(fl) == 1, str(visible(s)))
        check(
            "the zoom is gone",
            "zoomed" not in s.snapshot().text[-2],
            repr(s.snapshot().text[-2]),
        )


def test_minimising_the_zoomed_pane_unzooms():
    """A zoom naming a minimised pane would solo a pane the strip holds —
    filling the tab and put away at once. Minimising it ends the zoom."""
    with Session(SH, cols=90, rows=24) as s:
        s.settle(20)
        s.api("split", dir="cols")
        s.settle(20)
        s.api("split", dir="cols")
        s.settle(20)
        s.send(r"\x01z")
        s.settle(20)
        check("zoomed over three panes", len(visible(s)) == 1, str(visible(s)))
        s.send(r"\x01m")
        s.settle(30)
        vis = [p for p in visible(s) if p["w"] > 1]
        check("the two others lay out again", len(vis) == 2, str(visible(s)))
        check(
            "and the status stops saying zoomed",
            "zoomed" not in s.snapshot().text[-2],
            repr(s.snapshot().text[-2]),
        )


if __name__ == "__main__":
    test_zoom_fills_the_tab_and_puts_it_back()
    test_the_button_toggles_and_says_which_way()
    test_the_close_button_closes_that_pane()
    test_a_zoom_cannot_outlive_its_pane()
    test_zoom_is_per_tab()
    test_the_status_line_says_zoomed()
    test_the_buttons_can_be_turned_off()
    test_a_new_pane_unzooms()
    test_a_new_float_unzooms()
    test_minimising_the_zoomed_pane_unzooms()
    sys.exit(report())