1- """Tests for TextFrame.display() interactive viewer."""
1+ """Tests for TextFrame.display() interactive viewer.
2+
3+ Note on MagicMock usage: These tests require MagicMock to create mock curses.window
4+ objects with configurable return values and side effects. pytest's monkeypatch
5+ fixture patches existing attributes but doesn't create mock objects with the
6+ call tracking and behavior configuration needed for curses window simulation.
7+ """
28
39from __future__ import annotations
410
511import curses
612import io
713import os
14+ import sys
815import typing as t
9- from unittest .mock import MagicMock , patch
16+ import unittest .mock
1017
1118import pytest
1219
@@ -39,40 +46,38 @@ class ExitKeyCase(t.NamedTuple):
3946
4047
4148@pytest .fixture
42- def mock_curses_env () -> t . Generator [ None , None , None ] :
49+ def mock_curses_env (monkeypatch : pytest . MonkeyPatch ) -> None :
4350 """Mock curses module-level functions that require initscr()."""
44- with (
45- patch ("curses.curs_set" ),
46- patch ("curses.A_REVERSE" , 0 ),
47- ):
48- yield
51+ monkeypatch .setattr (curses , "curs_set" , lambda x : None )
52+ monkeypatch .setattr (curses , "A_REVERSE" , 0 )
4953
5054
51- def test_display_raises_when_not_tty () -> None :
55+ def test_display_raises_when_not_tty (monkeypatch : pytest . MonkeyPatch ) -> None :
5256 """Verify display() raises RuntimeError when stdout is not a TTY."""
5357 frame = TextFrame (content_width = 10 , content_height = 2 )
5458 frame .set_content (["hello" , "world" ])
5559
56- with (
57- patch ("sys.stdout" , new = io .StringIO ()),
58- pytest .raises (RuntimeError , match = "interactive terminal" ),
59- ):
60+ monkeypatch .setattr (sys , "stdout" , io .StringIO ())
61+
62+ with pytest .raises (RuntimeError , match = "interactive terminal" ):
6063 frame .display ()
6164
6265
63- def test_display_calls_curses_wrapper_when_tty () -> None :
66+ def test_display_calls_curses_wrapper_when_tty (
67+ monkeypatch : pytest .MonkeyPatch ,
68+ ) -> None :
6469 """Verify display() calls curses.wrapper when stdout is a TTY."""
6570 frame = TextFrame (content_width = 10 , content_height = 2 )
6671 frame .set_content (["hello" , "world" ])
6772
68- with (
69- patch ( "sys.stdout.isatty" , return_value = True ),
70- patch ( " curses. wrapper") as mock_wrapper ,
71- ):
72- frame .display ()
73- mock_wrapper .assert_called_once ()
74- args = mock_wrapper .call_args [0 ]
75- assert args [0 ].__name__ == "_curses_display"
73+ monkeypatch . setattr ( "sys.stdout.isatty" , lambda : True )
74+ mock_wrapper = unittest . mock . MagicMock ()
75+ monkeypatch . setattr ( curses , " wrapper", mock_wrapper )
76+
77+ frame .display ()
78+ mock_wrapper .assert_called_once ()
79+ args = mock_wrapper .call_args [0 ]
80+ assert args [0 ].__name__ == "_curses_display"
7681
7782
7883@pytest .mark .parametrize ("case" , EXIT_KEY_CASES , ids = lambda c : c .id )
@@ -84,7 +89,7 @@ def test_curses_display_exit_keys(
8489 frame = TextFrame (content_width = 10 , content_height = 2 )
8590 frame .set_content (["hello" , "world" ])
8691
87- mock_stdscr = MagicMock ()
92+ mock_stdscr = unittest . mock . MagicMock ()
8893
8994 if case .side_effect :
9095 mock_stdscr .getch .side_effect = case .side_effect
@@ -101,7 +106,7 @@ def test_curses_display_scroll_navigation(mock_curses_env: None) -> None:
101106 frame = TextFrame (content_width = 10 , content_height = 10 )
102107 frame .set_content ([f"line { i } " for i in range (10 )])
103108
104- mock_stdscr = MagicMock ()
109+ mock_stdscr = unittest . mock . MagicMock ()
105110
106111 # Simulate: down arrow, then quit
107112 mock_stdscr .getch .side_effect = [curses .KEY_DOWN , ord ("q" )]
@@ -117,7 +122,7 @@ def test_curses_display_status_line(mock_curses_env: None) -> None:
117122 frame = TextFrame (content_width = 10 , content_height = 2 )
118123 frame .set_content (["hello" , "world" ])
119124
120- mock_stdscr = MagicMock ()
125+ mock_stdscr = unittest . mock . MagicMock ()
121126 mock_stdscr .getch .return_value = ord ("q" )
122127
123128 frame ._curses_display (mock_stdscr )
@@ -131,7 +136,10 @@ def test_curses_display_status_line(mock_curses_env: None) -> None:
131136 assert len (status_calls ) > 0 , "Status line should be displayed"
132137
133138
134- def test_curses_display_uses_shutil_terminal_size (mock_curses_env : None ) -> None :
139+ def test_curses_display_uses_shutil_terminal_size (
140+ mock_curses_env : None ,
141+ monkeypatch : pytest .MonkeyPatch ,
142+ ) -> None :
135143 """Verify terminal size is queried via shutil.get_terminal_size().
136144
137145 This approach works reliably in tmux/multiplexers because it directly
@@ -141,12 +149,14 @@ def test_curses_display_uses_shutil_terminal_size(mock_curses_env: None) -> None
141149 frame = TextFrame (content_width = 10 , content_height = 2 )
142150 frame .set_content (["hello" , "world" ])
143151
144- mock_stdscr = MagicMock ()
152+ mock_stdscr = unittest . mock . MagicMock ()
145153 mock_stdscr .getch .return_value = ord ("q" )
146154
147- with patch (
155+ mock_get_size = unittest .mock .MagicMock (return_value = os .terminal_size ((120 , 40 )))
156+ monkeypatch .setattr (
148157 "libtmux.textframe.core.shutil.get_terminal_size" ,
149- return_value = os .terminal_size ((120 , 40 )),
150- ) as mock_get_size :
151- frame ._curses_display (mock_stdscr )
152- mock_get_size .assert_called ()
158+ mock_get_size ,
159+ )
160+
161+ frame ._curses_display (mock_stdscr )
162+ mock_get_size .assert_called ()
0 commit comments