tests/test_display.py (5517B)
1 """Tests for ap_scanner.display."""
2
3 from ap_scanner.display import Display, _display_width, _pad_to_width, _truncate_to_width
4
5
6 # --- _display_width ---
7
8 def test_display_width_ascii():
9 assert _display_width("hello") == 5
10
11
12 def test_display_width_cjk():
13 # CJK characters are 2 cells wide each
14 assert _display_width("\u5317\u4eac") == 4 # 北京
15
16
17 def test_display_width_mixed():
18 assert _display_width("AB\u5317\u4eac") == 6 # 2 ASCII + 2 CJK
19
20
21 def test_display_width_empty():
22 assert _display_width("") == 0
23
24
25 # --- _truncate_to_width ---
26
27 def test_truncate_no_change():
28 assert _truncate_to_width("short", 10) == "short"
29
30
31 def test_truncate_long_ascii():
32 result = _truncate_to_width("a" * 30, 10)
33 assert _display_width(result) <= 11 # 10 + ellipsis
34
35
36 def test_truncate_cjk():
37 # 5 CJK chars = 10 cells, truncate to 7
38 result = _truncate_to_width("\u5317\u4eac\u4e0a\u6d77\u5e7f", 7)
39 assert _display_width(result) <= 8 # truncated + ellipsis
40
41
42 # --- _pad_to_width ---
43
44 def test_pad_to_width_shorter():
45 result = _pad_to_width("hi", 10)
46 assert _display_width(result) == 10
47
48
49 def test_pad_to_width_exact():
50 result = _pad_to_width("12345", 5)
51 assert result == "12345"
52
53
54 def test_pad_to_width_cjk():
55 # 2 CJK chars = 4 cells, pad to 10
56 result = _pad_to_width("\u5317\u4eac", 10)
57 assert _display_width(result) == 10
58 assert result.endswith(" " * 6)
59
60
61 # --- Display class ---
62
63 def test_cycle_sort():
64 d = Display()
65 assert d.sort_field == "rssi"
66 d.cycle_sort()
67 assert d.sort_field == "channel"
68 d.cycle_sort()
69 assert d.sort_field == "essid"
70 d.cycle_sort()
71 assert d.sort_field == "enc"
72 d.cycle_sort()
73 assert d.sort_field == "rssi" # wraps around
74
75
76 def test_cycle_band():
77 d = Display()
78 assert d.band_filter is None
79
80 result = d.cycle_band()
81 assert result == "2.4GHz"
82 assert d.band_filter == "2.4GHz"
83
84 result = d.cycle_band()
85 assert result == "5GHz"
86
87 result = d.cycle_band()
88 assert result == "6GHz"
89 assert d.band_filter == "6GHz"
90
91 result = d.cycle_band()
92 assert result == "all"
93 assert d.band_filter is None
94
95
96 def test_set_sort_valid():
97 d = Display()
98 d.set_sort("channel")
99 assert d.sort_field == "channel"
100
101
102 def test_set_sort_invalid():
103 d = Display()
104 d.set_sort("nonexistent")
105 assert d.sort_field == "rssi" # unchanged
106
107
108 # --- Selection mode ---
109
110 def test_selection_mode_toggle():
111 d = Display()
112 assert d._selection_mode is False
113 d.enter_selection_mode()
114 assert d._selection_mode is True
115 assert d._selected_index == 0
116 d.exit_selection_mode()
117 assert d._selection_mode is False
118 assert d._selected_index == 0
119
120
121 def test_move_selection_bounds():
122 d = Display()
123 d.enter_selection_mode()
124 # Move down within bounds
125 d.move_selection(1, 5)
126 assert d._selected_index == 1
127 d.move_selection(1, 5)
128 assert d._selected_index == 2
129 # Move past end — should clamp
130 d.move_selection(10, 5)
131 assert d._selected_index == 4
132 # Move past start — should clamp to 0
133 d.move_selection(-10, 5)
134 assert d._selected_index == 0
135
136
137 def test_move_selection_empty():
138 d = Display()
139 d.enter_selection_mode()
140 d.move_selection(1, 0)
141 assert d._selected_index == 0
142
143
144 def test_get_sorted_filtered(sample_ap, open_ap):
145 d = Display()
146 aps = {sample_ap.bssid: sample_ap, open_ap.bssid: open_ap}
147 result = d.get_sorted_filtered(aps)
148 assert len(result) == 2
149 # Default sort by rssi descending: -42 > -73
150 assert result[0].bssid == sample_ap.bssid
151
152
153 def test_get_sorted_filtered_band(sample_ap, open_ap):
154 d = Display()
155 d.band_filter = "5GHz"
156 aps = {sample_ap.bssid: sample_ap, open_ap.bssid: open_ap}
157 result = d.get_sorted_filtered(aps)
158 # Both are 2.4GHz, so filtering to 5GHz returns nothing
159 assert len(result) == 0
160
161
162 # --- Overlay title padding fix ---
163
164 def test_overlay_title_width():
165 """Verify overlay title line does not exceed border width (bug fix)."""
166 from ap_scanner.display import _display_width
167
168 box_width = 72
169 title = "ACTIVE SCAN: TestNet (AA:BB:CC:DD:EE:FF)"
170 title_line = f"| {title}"
171 pad = box_width - 3 - _display_width(title)
172 title_line += " " * max(0, pad) + "|"
173
174 border = "+" + "-" * (box_width - 2) + "+"
175
176 assert _display_width(title_line) == _display_width(border), (
177 f"title_line width ({_display_width(title_line)}) != "
178 f"border width ({_display_width(border)})"
179 )
180
181
182 def test_overlay_content_line_width():
183 """Verify overlay content lines match border width."""
184 from ap_scanner.display import _display_width, _truncate_to_width
185
186 box_width = 72
187 line = "Assigned IP: 192.168.1.100"
188 display_line = _truncate_to_width(line, box_width - 4)
189 pad = box_width - 2 - _display_width(display_line) - 1
190 row = f"| {display_line}" + " " * max(0, pad) + "|"
191
192 border = "+" + "-" * (box_width - 2) + "+"
193
194 assert _display_width(row) == _display_width(border)
195
196
197 def test_overlay_title_width_cjk():
198 """Overlay title with CJK characters must not exceed border width."""
199 from ap_scanner.display import _display_width
200
201 box_width = 72
202 title = "\u5317\u4eac\u7f51\u7edc (AA:BB:CC:DD:EE:FF)" # 北京网络
203 title_line = f"| {title}"
204 pad = box_width - 3 - _display_width(title)
205 title_line += " " * max(0, pad) + "|"
206
207 border = "+" + "-" * (box_width - 2) + "+"
208
209 assert _display_width(title_line) == _display_width(border)