Coverage for tests/test_display.py: 100%
124 statements
« prev ^ index » next coverage.py v7.11.1, created at 2026-03-27 20:23 -0700
« prev ^ index » next coverage.py v7.11.1, created at 2026-03-27 20:23 -0700
1"""Test cases for the display module."""
3import os
4from textwrap import dedent
6from countdown import display
7from countdown.digits import DIGIT_SIZES
10def fake_size(columns, lines):
11 """Create a fake terminal size function for testing."""
13 def get_terminal_size(fallback=(columns, lines)):
14 return os.terminal_size(fallback)
16 return get_terminal_size
19def test_print_full_screen_tiny_terminal(capsys, monkeypatch):
20 monkeypatch.setattr(
21 "countdown.display.get_terminal_size",
22 fake_size(40, 10),
23 )
24 display.print_full_screen(["hello world"])
25 out, err = capsys.readouterr()
26 assert out[:6] == "\x1b[H\x1b[J"
27 assert out[6:] == "\n\n\n\n hello world"
30def test_print_full_screen_larger_terminal(capsys, monkeypatch):
31 monkeypatch.setattr(
32 "countdown.display.get_terminal_size",
33 fake_size(80, 24),
34 )
35 display.print_full_screen(["hello world"])
36 out, err = capsys.readouterr()
37 assert out[:6] == "\x1b[H\x1b[J"
38 # 24 height - 1 line = 23, 23//2 = 11 newlines
39 # 80 width - 11 chars = 69, 69//2 = 34 spaces
40 assert out[6:] == "\n" * 11 + " " * 34 + "hello world"
43def test_print_full_screen_multiline_text(capsys, monkeypatch):
44 monkeypatch.setattr(
45 "countdown.display.get_terminal_size",
46 fake_size(100, 30),
47 )
48 display.print_full_screen(
49 dedent(
50 """\
51 ██████ ██████ ██ ████
52 ██ ██ ██ ███ ██ ██
53 █████ ██████ ██ ████
54 ██ ██ ██ ██ ██ ██
55 ██████ ██████ ██ ████
56 """
57 ).splitlines()
58 )
59 out, err = capsys.readouterr()
60 assert out[:6] == "\x1b[H\x1b[J"
61 assert out[6:] == (
62 "\n\n\n\n\n\n\n\n\n\n\n\n"
63 " ██████ ██████ ██ ████\n"
64 " ██ ██ ██ ███ ██ ██\n"
65 " █████ ██████ ██ ████\n"
66 " ██ ██ ██ ██ ██ ██\n"
67 " ██████ ██████ ██ ████"
68 )
71def test_print_full_screen_paused_shows_red_and_message(capsys, monkeypatch):
72 """Test that paused=True shows colored timer and PAUSED message."""
73 monkeypatch.setattr(
74 "countdown.display.get_terminal_size",
75 fake_size(80, 24),
76 )
77 lines = ["00:05"]
78 display.print_full_screen(lines, paused=True)
79 out, err = capsys.readouterr()
80 # Should contain intense magenta color code
81 assert "\x1b[95m" in out, (
82 "Should contain intense magenta color code when paused"
83 )
84 # Should contain reset code
85 assert "\033[0m" in out, "Should contain color reset code"
86 # Should contain PAUSED message
87 assert "PAUSED - Press any key to resume" in out
90def test_print_full_screen_not_paused_no_red_or_message(capsys, monkeypatch):
91 """Test that paused=False shows normal timer without PAUSED message."""
92 monkeypatch.setattr(
93 "countdown.display.get_terminal_size",
94 fake_size(80, 24),
95 )
96 lines = ["00:05"]
97 display.print_full_screen(lines, paused=False)
98 out, err = capsys.readouterr()
99 # Should NOT contain PAUSED message
100 assert "PAUSED" not in out
101 # Red color may or may not be present depending on other features, but
102 # the important thing is the PAUSED message is not shown
105def test_print_full_screen_paused_tiny_terminal_no_message(capsys, monkeypatch):
106 """Test that PAUSED message is hidden in tiny terminals with no room."""
107 # Create a 3-line terminal with 3-line timer (no room for PAUSED text)
108 monkeypatch.setattr("countdown.display.get_terminal_size", fake_size(20, 3))
109 lines = ["line1", "line2", "line3"]
110 display.print_full_screen(lines, paused=True)
111 out, err = capsys.readouterr()
112 # Should still show intense magenta color
113 assert "\x1b[95m" in out, (
114 "Should contain intense magenta color code when paused"
115 )
116 # Should NOT show PAUSED message (no room)
117 assert "PAUSED" not in out, (
118 "PAUSED message should not appear in tiny terminal"
119 )
122def test_digit_sizes_available():
123 """Test that expected digit sizes are available."""
124 assert 16 in DIGIT_SIZES, "Size 16 digits should be available"
125 assert 7 in DIGIT_SIZES, "Size 7 digits should be available"
126 assert 5 in DIGIT_SIZES, "Size 5 digits should be available"
127 assert 3 in DIGIT_SIZES, "Size 3 digits should be available"
128 assert 1 in DIGIT_SIZES, "Size 1 digits should be available"
131def test_all_characters_in_each_size():
132 """Test that all digit characters exist in each size."""
133 from countdown.digits import CHARS_BY_SIZE
135 expected_chars = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":"}
136 for size in DIGIT_SIZES:
137 chars = CHARS_BY_SIZE[size]
138 assert set(chars.keys()) == expected_chars, (
139 f"Size {size} should have all characters"
140 )
143def test_char_heights_match_size():
144 """Test that character heights match the expected size."""
145 from countdown.digits import CHARS_BY_SIZE
147 for size in DIGIT_SIZES:
148 chars = CHARS_BY_SIZE[size]
149 for char, text in chars.items():
150 height = len(text.splitlines())
151 assert height == size, (
152 f"Character '{char}' in size {size} should have height {size}, got {height}"
153 )
156def test_get_chars_for_terminal_selects_largest_that_fits(monkeypatch):
157 """Test that get_chars_for_terminal selects the largest size that fits both dimensions."""
158 # Size requirements for displaying 00:00:
159 # 16(93w), 7(57w), 5(33w), 3(20w), 1(10w)
161 # 80x24 terminal - size 7 fits (57w <= 80, 7h <= 24)
162 monkeypatch.setattr(
163 "countdown.display.get_terminal_size",
164 fake_size(80, 24),
165 )
166 chars = display.get_chars_for_terminal()
167 height = len(chars["0"].splitlines())
168 assert height == 7, "80x24 terminal should select size 7"
170 # 100x24 terminal - size 16 fits (93w <= 100, 16h <= 24)
171 monkeypatch.setattr(
172 "countdown.display.get_terminal_size",
173 fake_size(100, 24),
174 )
175 chars = display.get_chars_for_terminal()
176 height = len(chars["0"].splitlines())
177 assert height == 16, "100x24 terminal should select size 16"
179 # 60x20 terminal - size 7 fits (57w <= 60, 7h <= 20)
180 monkeypatch.setattr(
181 "countdown.display.get_terminal_size",
182 fake_size(60, 20),
183 )
184 chars = display.get_chars_for_terminal()
185 height = len(chars["0"].splitlines())
186 assert height == 7, "60x20 terminal should select size 7"
188 # 32x10 terminal - size 3 fits (20w <= 32, 3h <= 10)
189 monkeypatch.setattr(
190 "countdown.display.get_terminal_size",
191 fake_size(32, 10),
192 )
193 chars = display.get_chars_for_terminal()
194 height = len(chars["0"].splitlines())
195 assert height == 3, "32x10 terminal should select size 3"
197 # 15x5 terminal - size 1 fits (10w <= 15, 1h <= 5)
198 monkeypatch.setattr("countdown.display.get_terminal_size", fake_size(15, 5))
199 chars = display.get_chars_for_terminal()
200 height = len(chars["0"].splitlines())
201 assert height == 1, "15x5 terminal should select size 1"
203 # Very small terminal - falls back to smallest
204 monkeypatch.setattr("countdown.display.get_terminal_size", fake_size(5, 1))
205 chars = display.get_chars_for_terminal()
206 height = len(chars["0"].splitlines())
207 assert height == 1, "5x1 terminal should fall back to size 1"
210def test_different_sizes_render_correctly(monkeypatch):
211 """Test that different sizes render correctly."""
212 from countdown import timer
214 # Test size 7 rendering (80x24 selects size 7)
215 monkeypatch.setattr(
216 "countdown.display.get_terminal_size",
217 fake_size(80, 24),
218 )
219 chars = display.get_chars_for_terminal()
220 lines = timer.get_number_lines(0, chars) # 00:00
221 assert len(lines) == 7, "80x24 terminal should render 7 lines"
223 # Test size 3 rendering (32x10 selects size 3)
224 monkeypatch.setattr(
225 "countdown.display.get_terminal_size",
226 fake_size(32, 10),
227 )
228 chars = display.get_chars_for_terminal()
229 lines = timer.get_number_lines(0, chars) # 00:00
230 assert len(lines) == 3, "32x10 terminal should render 3 lines"
232 # Test size 1 rendering (15x5 selects size 1)
233 monkeypatch.setattr("countdown.display.get_terminal_size", fake_size(15, 5))
234 chars = display.get_chars_for_terminal()
235 lines = timer.get_number_lines(0, chars) # 00:00
236 assert len(lines) == 1, "15x5 terminal should render 1 line"
239def test_width_constraints_force_smaller_size(monkeypatch):
240 """Test that narrow terminal widths force selection of smaller digit sizes."""
241 # Size 7 requires 57 width - a 56x20 terminal should select size 5 instead
242 monkeypatch.setattr(
243 "countdown.display.get_terminal_size",
244 fake_size(56, 20),
245 )
246 chars = display.get_chars_for_terminal()
247 height = len(chars["0"].splitlines())
248 assert height == 5, (
249 "56x20 terminal too narrow for size 7, should select size 5"
250 )
252 # Size 5 requires 33 width - a 32x10 terminal should select size 3 instead
253 monkeypatch.setattr(
254 "countdown.display.get_terminal_size",
255 fake_size(32, 10),
256 )
257 chars = display.get_chars_for_terminal()
258 height = len(chars["0"].splitlines())
259 assert height == 3, (
260 "32x10 terminal too narrow for size 5, should select size 3"
261 )
263 # Size 3 requires 20 width - a 19x5 terminal should select size 1 instead
264 monkeypatch.setattr("countdown.display.get_terminal_size", fake_size(19, 5))
265 chars = display.get_chars_for_terminal()
266 height = len(chars["0"].splitlines())
267 assert height == 1, (
268 "19x5 terminal too narrow for size 3, should select size 1"
269 )
272def test_three_digit_minutes_force_smaller_chars(monkeypatch):
273 """Wide minute values should fall back to smaller glyphs if needed."""
274 # 60x20 terminal can show size 7 for two-digit minutes
275 monkeypatch.setattr(
276 "countdown.display.get_terminal_size",
277 fake_size(60, 20),
278 )
279 chars = display.get_chars_for_terminal(0)
280 assert len(chars["0"].splitlines()) == 7
282 # But 100 minutes needs 70 columns at size 7, so we should drop to size 5
283 chars = display.get_chars_for_terminal(6000) # 100 minutes
284 assert len(chars["0"].splitlines()) == 5