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