1"""Test sidebar, coverage 93%""" 2import idlelib.sidebar 3from itertools import chain 4import unittest 5import unittest.mock 6from test.support import requires 7import tkinter as tk 8 9from idlelib.delegator import Delegator 10from idlelib.percolator import Percolator 11 12 13class Dummy_editwin: 14 def __init__(self, text): 15 self.text = text 16 self.text_frame = self.text.master 17 self.per = Percolator(text) 18 self.undo = Delegator() 19 self.per.insertfilter(self.undo) 20 21 def setvar(self, name, value): 22 pass 23 24 def getlineno(self, index): 25 return int(float(self.text.index(index))) 26 27 28class LineNumbersTest(unittest.TestCase): 29 30 @classmethod 31 def setUpClass(cls): 32 requires('gui') 33 cls.root = tk.Tk() 34 35 cls.text_frame = tk.Frame(cls.root) 36 cls.text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 37 cls.text_frame.rowconfigure(1, weight=1) 38 cls.text_frame.columnconfigure(1, weight=1) 39 40 cls.text = tk.Text(cls.text_frame, width=80, height=24, wrap=tk.NONE) 41 cls.text.grid(row=1, column=1, sticky=tk.NSEW) 42 43 cls.editwin = Dummy_editwin(cls.text) 44 cls.editwin.vbar = tk.Scrollbar(cls.text_frame) 45 46 @classmethod 47 def tearDownClass(cls): 48 cls.editwin.per.close() 49 cls.root.update() 50 cls.root.destroy() 51 del cls.text, cls.text_frame, cls.editwin, cls.root 52 53 def setUp(self): 54 self.linenumber = idlelib.sidebar.LineNumbers(self.editwin) 55 56 self.highlight_cfg = {"background": '#abcdef', 57 "foreground": '#123456'} 58 orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight 59 def mock_idleconf_GetHighlight(theme, element): 60 if element == 'linenumber': 61 return self.highlight_cfg 62 return orig_idleConf_GetHighlight(theme, element) 63 GetHighlight_patcher = unittest.mock.patch.object( 64 idlelib.sidebar.idleConf, 'GetHighlight', mock_idleconf_GetHighlight) 65 GetHighlight_patcher.start() 66 self.addCleanup(GetHighlight_patcher.stop) 67 68 self.font_override = 'TkFixedFont' 69 def mock_idleconf_GetFont(root, configType, section): 70 return self.font_override 71 GetFont_patcher = unittest.mock.patch.object( 72 idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont) 73 GetFont_patcher.start() 74 self.addCleanup(GetFont_patcher.stop) 75 76 def tearDown(self): 77 self.text.delete('1.0', 'end') 78 79 def get_selection(self): 80 return tuple(map(str, self.text.tag_ranges('sel'))) 81 82 def get_line_screen_position(self, line): 83 bbox = self.linenumber.sidebar_text.bbox(f'{line}.end -1c') 84 x = bbox[0] + 2 85 y = bbox[1] + 2 86 return x, y 87 88 def assert_state_disabled(self): 89 state = self.linenumber.sidebar_text.config()['state'] 90 self.assertEqual(state[-1], tk.DISABLED) 91 92 def get_sidebar_text_contents(self): 93 return self.linenumber.sidebar_text.get('1.0', tk.END) 94 95 def assert_sidebar_n_lines(self, n_lines): 96 expected = '\n'.join(chain(map(str, range(1, n_lines + 1)), [''])) 97 self.assertEqual(self.get_sidebar_text_contents(), expected) 98 99 def assert_text_equals(self, expected): 100 return self.assertEqual(self.text.get('1.0', 'end'), expected) 101 102 def test_init_empty(self): 103 self.assert_sidebar_n_lines(1) 104 105 def test_init_not_empty(self): 106 self.text.insert('insert', 'foo bar\n'*3) 107 self.assert_text_equals('foo bar\n'*3 + '\n') 108 self.assert_sidebar_n_lines(4) 109 110 def test_toggle_linenumbering(self): 111 self.assertEqual(self.linenumber.is_shown, False) 112 self.linenumber.show_sidebar() 113 self.assertEqual(self.linenumber.is_shown, True) 114 self.linenumber.hide_sidebar() 115 self.assertEqual(self.linenumber.is_shown, False) 116 self.linenumber.hide_sidebar() 117 self.assertEqual(self.linenumber.is_shown, False) 118 self.linenumber.show_sidebar() 119 self.assertEqual(self.linenumber.is_shown, True) 120 self.linenumber.show_sidebar() 121 self.assertEqual(self.linenumber.is_shown, True) 122 123 def test_insert(self): 124 self.text.insert('insert', 'foobar') 125 self.assert_text_equals('foobar\n') 126 self.assert_sidebar_n_lines(1) 127 self.assert_state_disabled() 128 129 self.text.insert('insert', '\nfoo') 130 self.assert_text_equals('foobar\nfoo\n') 131 self.assert_sidebar_n_lines(2) 132 self.assert_state_disabled() 133 134 self.text.insert('insert', 'hello\n'*2) 135 self.assert_text_equals('foobar\nfoohello\nhello\n\n') 136 self.assert_sidebar_n_lines(4) 137 self.assert_state_disabled() 138 139 self.text.insert('insert', '\nworld') 140 self.assert_text_equals('foobar\nfoohello\nhello\n\nworld\n') 141 self.assert_sidebar_n_lines(5) 142 self.assert_state_disabled() 143 144 def test_delete(self): 145 self.text.insert('insert', 'foobar') 146 self.assert_text_equals('foobar\n') 147 self.text.delete('1.1', '1.3') 148 self.assert_text_equals('fbar\n') 149 self.assert_sidebar_n_lines(1) 150 self.assert_state_disabled() 151 152 self.text.insert('insert', 'foo\n'*2) 153 self.assert_text_equals('fbarfoo\nfoo\n\n') 154 self.assert_sidebar_n_lines(3) 155 self.assert_state_disabled() 156 157 # Note: deleting up to "2.end" doesn't delete the final newline. 158 self.text.delete('2.0', '2.end') 159 self.assert_text_equals('fbarfoo\n\n\n') 160 self.assert_sidebar_n_lines(3) 161 self.assert_state_disabled() 162 163 self.text.delete('1.3', 'end') 164 self.assert_text_equals('fba\n') 165 self.assert_sidebar_n_lines(1) 166 self.assert_state_disabled() 167 168 # Note: Text widgets always keep a single '\n' character at the end. 169 self.text.delete('1.0', 'end') 170 self.assert_text_equals('\n') 171 self.assert_sidebar_n_lines(1) 172 self.assert_state_disabled() 173 174 def test_sidebar_text_width(self): 175 """ 176 Test that linenumber text widget is always at the minimum 177 width 178 """ 179 def get_width(): 180 return self.linenumber.sidebar_text.config()['width'][-1] 181 182 self.assert_sidebar_n_lines(1) 183 self.assertEqual(get_width(), 1) 184 185 self.text.insert('insert', 'foo') 186 self.assert_sidebar_n_lines(1) 187 self.assertEqual(get_width(), 1) 188 189 self.text.insert('insert', 'foo\n'*8) 190 self.assert_sidebar_n_lines(9) 191 self.assertEqual(get_width(), 1) 192 193 self.text.insert('insert', 'foo\n') 194 self.assert_sidebar_n_lines(10) 195 self.assertEqual(get_width(), 2) 196 197 self.text.insert('insert', 'foo\n') 198 self.assert_sidebar_n_lines(11) 199 self.assertEqual(get_width(), 2) 200 201 self.text.delete('insert -1l linestart', 'insert linestart') 202 self.assert_sidebar_n_lines(10) 203 self.assertEqual(get_width(), 2) 204 205 self.text.delete('insert -1l linestart', 'insert linestart') 206 self.assert_sidebar_n_lines(9) 207 self.assertEqual(get_width(), 1) 208 209 self.text.insert('insert', 'foo\n'*90) 210 self.assert_sidebar_n_lines(99) 211 self.assertEqual(get_width(), 2) 212 213 self.text.insert('insert', 'foo\n') 214 self.assert_sidebar_n_lines(100) 215 self.assertEqual(get_width(), 3) 216 217 self.text.insert('insert', 'foo\n') 218 self.assert_sidebar_n_lines(101) 219 self.assertEqual(get_width(), 3) 220 221 self.text.delete('insert -1l linestart', 'insert linestart') 222 self.assert_sidebar_n_lines(100) 223 self.assertEqual(get_width(), 3) 224 225 self.text.delete('insert -1l linestart', 'insert linestart') 226 self.assert_sidebar_n_lines(99) 227 self.assertEqual(get_width(), 2) 228 229 self.text.delete('50.0 -1c', 'end -1c') 230 self.assert_sidebar_n_lines(49) 231 self.assertEqual(get_width(), 2) 232 233 self.text.delete('5.0 -1c', 'end -1c') 234 self.assert_sidebar_n_lines(4) 235 self.assertEqual(get_width(), 1) 236 237 # Note: Text widgets always keep a single '\n' character at the end. 238 self.text.delete('1.0', 'end -1c') 239 self.assert_sidebar_n_lines(1) 240 self.assertEqual(get_width(), 1) 241 242 def test_click_selection(self): 243 self.linenumber.show_sidebar() 244 self.text.insert('1.0', 'one\ntwo\nthree\nfour\n') 245 self.root.update() 246 247 # Click on the second line. 248 x, y = self.get_line_screen_position(2) 249 self.linenumber.sidebar_text.event_generate('<Button-1>', x=x, y=y) 250 self.linenumber.sidebar_text.update() 251 self.root.update() 252 253 self.assertEqual(self.get_selection(), ('2.0', '3.0')) 254 255 def simulate_drag(self, start_line, end_line): 256 start_x, start_y = self.get_line_screen_position(start_line) 257 end_x, end_y = self.get_line_screen_position(end_line) 258 259 self.linenumber.sidebar_text.event_generate('<Button-1>', 260 x=start_x, y=start_y) 261 self.root.update() 262 263 def lerp(a, b, steps): 264 """linearly interpolate from a to b (inclusive) in equal steps""" 265 last_step = steps - 1 266 for i in range(steps): 267 yield ((last_step - i) / last_step) * a + (i / last_step) * b 268 269 for x, y in zip( 270 map(int, lerp(start_x, end_x, steps=11)), 271 map(int, lerp(start_y, end_y, steps=11)), 272 ): 273 self.linenumber.sidebar_text.event_generate('<B1-Motion>', x=x, y=y) 274 self.root.update() 275 276 self.linenumber.sidebar_text.event_generate('<ButtonRelease-1>', 277 x=end_x, y=end_y) 278 self.root.update() 279 280 def test_drag_selection_down(self): 281 self.linenumber.show_sidebar() 282 self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') 283 self.root.update() 284 285 # Drag from the second line to the fourth line. 286 self.simulate_drag(2, 4) 287 self.assertEqual(self.get_selection(), ('2.0', '5.0')) 288 289 def test_drag_selection_up(self): 290 self.linenumber.show_sidebar() 291 self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') 292 self.root.update() 293 294 # Drag from the fourth line to the second line. 295 self.simulate_drag(4, 2) 296 self.assertEqual(self.get_selection(), ('2.0', '5.0')) 297 298 def test_scroll(self): 299 self.linenumber.show_sidebar() 300 self.text.insert('1.0', 'line\n' * 100) 301 self.root.update() 302 303 # Scroll down 10 lines. 304 self.text.yview_scroll(10, 'unit') 305 self.root.update() 306 self.assertEqual(self.text.index('@0,0'), '11.0') 307 self.assertEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') 308 309 # Generate a mouse-wheel event and make sure it scrolled up or down. 310 # The meaning of the "delta" is OS-dependant, so this just checks for 311 # any change. 312 self.linenumber.sidebar_text.event_generate('<MouseWheel>', 313 x=0, y=0, 314 delta=10) 315 self.root.update() 316 self.assertNotEqual(self.text.index('@0,0'), '11.0') 317 self.assertNotEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') 318 319 def test_font(self): 320 ln = self.linenumber 321 322 orig_font = ln.sidebar_text['font'] 323 test_font = 'TkTextFont' 324 self.assertNotEqual(orig_font, test_font) 325 326 # Ensure line numbers aren't shown. 327 ln.hide_sidebar() 328 329 self.font_override = test_font 330 # Nothing breaks when line numbers aren't shown. 331 ln.update_font() 332 333 # Activate line numbers, previous font change is immediately effective. 334 ln.show_sidebar() 335 self.assertEqual(ln.sidebar_text['font'], test_font) 336 337 # Call the font update with line numbers shown, change is picked up. 338 self.font_override = orig_font 339 ln.update_font() 340 self.assertEqual(ln.sidebar_text['font'], orig_font) 341 342 def test_highlight_colors(self): 343 ln = self.linenumber 344 345 orig_colors = dict(self.highlight_cfg) 346 test_colors = {'background': '#222222', 'foreground': '#ffff00'} 347 348 def assert_colors_are_equal(colors): 349 self.assertEqual(ln.sidebar_text['background'], colors['background']) 350 self.assertEqual(ln.sidebar_text['foreground'], colors['foreground']) 351 352 # Ensure line numbers aren't shown. 353 ln.hide_sidebar() 354 355 self.highlight_cfg = test_colors 356 # Nothing breaks with inactive code context. 357 ln.update_colors() 358 359 # Show line numbers, previous colors change is immediately effective. 360 ln.show_sidebar() 361 assert_colors_are_equal(test_colors) 362 363 # Call colors update with no change to the configured colors. 364 ln.update_colors() 365 assert_colors_are_equal(test_colors) 366 367 # Call the colors update with line numbers shown, change is picked up. 368 self.highlight_cfg = orig_colors 369 ln.update_colors() 370 assert_colors_are_equal(orig_colors) 371 372 373if __name__ == '__main__': 374 unittest.main(verbosity=2) 375