Skip to content

Commit ed57e33

Browse files
committed
tests(hooks): add comprehensive hook test grid
why: Ensure 100% API coverage for tmux hooks with proper set/show/unset validation what: - Add HookTestCase NamedTuple for parametrized testing - Add 62 test cases covering all hook categories: - Alert hooks (3): activity, bell, silence - Client hooks (7): active, attached, detached, focus-in/out, resized, session-changed - Session hooks (3): created, closed, renamed - Window hooks (5): linked, renamed, resized, unlinked, session-window-changed - Pane hooks (6): died, exited, focus-in/out, mode-changed, set-clipboard - After-* hooks (38): all command completion hooks - Add version-gated tests for tmux 3.5+ hooks (pane-title-changed, client-light/dark-theme)
1 parent 30c905d commit ed57e33

File tree

1 file changed

+218
-0
lines changed

1 file changed

+218
-0
lines changed

tests/test_hooks.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,221 @@ def test_hooks_mixin(
307307
0: "set-option -g status-left-style bg=red",
308308
},
309309
)
310+
311+
312+
# =============================================================================
313+
# Comprehensive Hook Test Grid
314+
# =============================================================================
315+
316+
317+
class HookTestCase(t.NamedTuple):
318+
"""Test case for hook validation."""
319+
320+
test_id: str
321+
hook: str # tmux hook name (hyphenated)
322+
min_version: str = "3.0" # Minimum tmux version required
323+
xfail_reason: str | None = None # Mark as expected failure with reason
324+
325+
326+
# --- Alert Hooks ---
327+
ALERT_HOOKS: list[HookTestCase] = [
328+
HookTestCase("alert_activity", "alert-activity"),
329+
HookTestCase("alert_bell", "alert-bell"),
330+
HookTestCase("alert_silence", "alert-silence"),
331+
]
332+
333+
# --- Client Hooks ---
334+
CLIENT_HOOKS: list[HookTestCase] = [
335+
HookTestCase("client_active", "client-active"),
336+
HookTestCase("client_attached", "client-attached"),
337+
HookTestCase("client_detached", "client-detached"),
338+
HookTestCase("client_focus_in", "client-focus-in"),
339+
HookTestCase("client_focus_out", "client-focus-out"),
340+
HookTestCase("client_resized", "client-resized"),
341+
HookTestCase("client_session_changed", "client-session-changed"),
342+
]
343+
344+
# --- Session Hooks ---
345+
SESSION_HOOKS: list[HookTestCase] = [
346+
HookTestCase("session_created", "session-created"),
347+
HookTestCase("session_closed", "session-closed"),
348+
HookTestCase("session_renamed", "session-renamed"),
349+
]
350+
351+
# --- Window Hooks ---
352+
WINDOW_HOOKS: list[HookTestCase] = [
353+
HookTestCase("window_linked", "window-linked"),
354+
HookTestCase("window_renamed", "window-renamed"),
355+
HookTestCase("window_resized", "window-resized"),
356+
HookTestCase("window_unlinked", "window-unlinked"),
357+
HookTestCase("session_window_changed", "session-window-changed"),
358+
]
359+
360+
# --- Pane Hooks ---
361+
PANE_HOOKS: list[HookTestCase] = [
362+
HookTestCase("pane_died", "pane-died"),
363+
HookTestCase("pane_exited", "pane-exited"),
364+
HookTestCase("pane_focus_in", "pane-focus-in"),
365+
HookTestCase("pane_focus_out", "pane-focus-out"),
366+
HookTestCase("pane_mode_changed", "pane-mode-changed"),
367+
HookTestCase("pane_set_clipboard", "pane-set-clipboard"),
368+
]
369+
370+
# --- After-* Hooks ---
371+
AFTER_HOOKS: list[HookTestCase] = [
372+
HookTestCase("after_bind_key", "after-bind-key"),
373+
HookTestCase("after_capture_pane", "after-capture-pane"),
374+
HookTestCase("after_copy_mode", "after-copy-mode"),
375+
HookTestCase("after_display_message", "after-display-message"),
376+
HookTestCase("after_display_panes", "after-display-panes"),
377+
HookTestCase("after_kill_pane", "after-kill-pane"),
378+
HookTestCase("after_list_buffers", "after-list-buffers"),
379+
HookTestCase("after_list_clients", "after-list-clients"),
380+
HookTestCase("after_list_keys", "after-list-keys"),
381+
HookTestCase("after_list_panes", "after-list-panes"),
382+
HookTestCase("after_list_sessions", "after-list-sessions"),
383+
HookTestCase("after_list_windows", "after-list-windows"),
384+
HookTestCase("after_load_buffer", "after-load-buffer"),
385+
HookTestCase("after_lock_server", "after-lock-server"),
386+
HookTestCase("after_new_session", "after-new-session"),
387+
HookTestCase("after_new_window", "after-new-window"),
388+
HookTestCase("after_paste_buffer", "after-paste-buffer"),
389+
HookTestCase("after_pipe_pane", "after-pipe-pane"),
390+
HookTestCase("after_queue", "after-queue"),
391+
HookTestCase("after_refresh_client", "after-refresh-client"),
392+
HookTestCase("after_rename_session", "after-rename-session"),
393+
HookTestCase("after_rename_window", "after-rename-window"),
394+
HookTestCase("after_resize_pane", "after-resize-pane"),
395+
HookTestCase("after_resize_window", "after-resize-window"),
396+
HookTestCase("after_save_buffer", "after-save-buffer"),
397+
HookTestCase("after_select_layout", "after-select-layout"),
398+
HookTestCase("after_select_pane", "after-select-pane"),
399+
HookTestCase("after_select_window", "after-select-window"),
400+
HookTestCase("after_send_keys", "after-send-keys"),
401+
HookTestCase("after_set_buffer", "after-set-buffer"),
402+
HookTestCase("after_set_environment", "after-set-environment"),
403+
HookTestCase("after_set_hook", "after-set-hook"),
404+
HookTestCase("after_set_option", "after-set-option"),
405+
HookTestCase("after_show_environment", "after-show-environment"),
406+
HookTestCase("after_show_messages", "after-show-messages"),
407+
HookTestCase("after_show_options", "after-show-options"),
408+
HookTestCase("after_split_window", "after-split-window"),
409+
HookTestCase("after_unbind_key", "after-unbind-key"),
410+
]
411+
412+
# --- New Hooks (tmux 3.5+) ---
413+
NEW_HOOKS: list[HookTestCase] = [
414+
HookTestCase(
415+
"pane_title_changed",
416+
"pane-title-changed",
417+
"3.5",
418+
xfail_reason="pane-title-changed requires tmux 3.5+",
419+
),
420+
HookTestCase(
421+
"client_light_theme",
422+
"client-light-theme",
423+
"3.5",
424+
xfail_reason="client-light-theme requires tmux 3.5+",
425+
),
426+
HookTestCase(
427+
"client_dark_theme",
428+
"client-dark-theme",
429+
"3.5",
430+
xfail_reason="client-dark-theme requires tmux 3.5+",
431+
),
432+
]
433+
434+
# Combine all hook test cases
435+
ALL_HOOK_TEST_CASES: list[HookTestCase] = (
436+
ALERT_HOOKS + CLIENT_HOOKS + SESSION_HOOKS + WINDOW_HOOKS + PANE_HOOKS + AFTER_HOOKS
437+
)
438+
439+
440+
def _build_hook_params() -> list[t.Any]:
441+
"""Build pytest params with appropriate marks."""
442+
params = []
443+
for tc in ALL_HOOK_TEST_CASES:
444+
marks: list[t.Any] = []
445+
if tc.xfail_reason:
446+
marks.append(pytest.mark.xfail(reason=tc.xfail_reason))
447+
params.append(pytest.param(tc, id=tc.test_id, marks=marks))
448+
return params
449+
450+
451+
@pytest.mark.parametrize("test_case", _build_hook_params())
452+
def test_hook_set_show_unset_cycle(server: Server, test_case: HookTestCase) -> None:
453+
"""Test set/show/unset cycle for each hook.
454+
455+
This parametrized test ensures all hooks in the libtmux constants can be:
456+
1. Set with a command
457+
2. Shown and verified
458+
3. Unset cleanly
459+
"""
460+
if not has_gte_version(test_case.min_version):
461+
pytest.skip(f"Requires tmux {test_case.min_version}+")
462+
463+
session = server.new_session(session_name="test_hook_cycle")
464+
window = session.active_window
465+
assert window is not None
466+
pane = window.active_pane
467+
assert pane is not None
468+
469+
hook_cmd = "display-message 'test hook fired'"
470+
471+
# Test set_hook (using session-level hook which works on all tmux versions)
472+
session.set_hook(f"{test_case.hook}[0]", hook_cmd)
473+
474+
# Test show_hook
475+
result = session._show_hook(f"{test_case.hook}[0]")
476+
assert result is not None, f"Expected hook {test_case.hook} to be set"
477+
478+
# Parse and verify
479+
hooks = Hooks.from_stdout(result)
480+
hook_attr = test_case.hook.replace("-", "_")
481+
hook_value = getattr(hooks, hook_attr, None)
482+
assert hook_value is not None, f"Hook attribute {hook_attr} not found in Hooks"
483+
assert len(hook_value) > 0, f"Expected hook {test_case.hook} to have values"
484+
assert "display-message" in hook_value[0], (
485+
f"Expected 'display-message' in hook value, got: {hook_value[0]}"
486+
)
487+
488+
# Test unset_hook
489+
session.unset_hook(f"{test_case.hook}[0]")
490+
491+
# Verify unset
492+
result_after_unset = session._show_hook(f"{test_case.hook}[0]")
493+
if result_after_unset:
494+
hooks_after = Hooks.from_stdout(result_after_unset)
495+
hook_value_after = getattr(hooks_after, hook_attr, None)
496+
# After unset, the hook should be empty or have empty value
497+
if hook_value_after:
498+
assert len(hook_value_after.as_list()) == 0 or hook_value_after[0] == "", (
499+
f"Expected hook {test_case.hook} to be unset"
500+
)
501+
502+
503+
@pytest.mark.parametrize(
504+
"test_case",
505+
[pytest.param(tc, id=tc.test_id) for tc in NEW_HOOKS],
506+
)
507+
def test_new_hooks_version_gated(server: Server, test_case: HookTestCase) -> None:
508+
"""Test new hooks that require tmux 3.5+.
509+
510+
These hooks are version-gated and will skip on older tmux versions.
511+
"""
512+
if not has_gte_version(test_case.min_version):
513+
pytest.skip(f"Requires tmux {test_case.min_version}+")
514+
515+
session = server.new_session(session_name="test_new_hooks")
516+
517+
hook_cmd = "display-message 'new hook fired'"
518+
519+
# Test set_hook
520+
session.set_hook(f"{test_case.hook}[0]", hook_cmd)
521+
522+
# Test show_hook
523+
result = session._show_hook(f"{test_case.hook}[0]")
524+
assert result is not None, f"Expected hook {test_case.hook} to be set"
525+
526+
# Cleanup
527+
session.unset_hook(f"{test_case.hook}[0]")

0 commit comments

Comments
 (0)