1"""Test sidebar, coverage 85%""" 2from textwrap import dedent 3import sys 4 5from itertools import chain 6import unittest 7import unittest.mock 8from test.support import requires, swap_attr 9import tkinter as tk 10from idlelib.idle_test.tkinter_testing_utils import run_in_tk_mainloop 11 12from idlelib.delegator import Delegator 13from idlelib.editor import fixwordbreaks 14from idlelib.percolator import Percolator 15import idlelib.pyshell 16from idlelib.pyshell import fix_x11_paste, PyShell, PyShellFileList 17from idlelib.run import fix_scaling 18import idlelib.sidebar 19from idlelib.sidebar import get_end_linenumber, get_lineno 20 21 22class Dummy_editwin: 23 def __init__(self, text): 24 self.text = text 25 self.text_frame = self.text.master 26 self.per = Percolator(text) 27 self.undo = Delegator() 28 self.per.insertfilter(self.undo) 29 30 def setvar(self, name, value): 31 pass 32 33 def getlineno(self, index): 34 return int(float(self.text.index(index))) 35 36 37class LineNumbersTest(unittest.TestCase): 38 39 @classmethod 40 def setUpClass(cls): 41 requires('gui') 42 cls.root = tk.Tk() 43 cls.root.withdraw() 44 45 cls.text_frame = tk.Frame(cls.root) 46 cls.text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) 47 cls.text_frame.rowconfigure(1, weight=1) 48 cls.text_frame.columnconfigure(1, weight=1) 49 50 cls.text = tk.Text(cls.text_frame, width=80, height=24, wrap=tk.NONE) 51 cls.text.grid(row=1, column=1, sticky=tk.NSEW) 52 53 cls.editwin = Dummy_editwin(cls.text) 54 cls.editwin.vbar = tk.Scrollbar(cls.text_frame) 55 56 @classmethod 57 def tearDownClass(cls): 58 cls.editwin.per.close() 59 cls.root.update() 60 cls.root.destroy() 61 del cls.text, cls.text_frame, cls.editwin, cls.root 62 63 def setUp(self): 64 self.linenumber = idlelib.sidebar.LineNumbers(self.editwin) 65 66 self.highlight_cfg = {"background": '#abcdef', 67 "foreground": '#123456'} 68 orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight 69 def mock_idleconf_GetHighlight(theme, element): 70 if element == 'linenumber': 71 return self.highlight_cfg 72 return orig_idleConf_GetHighlight(theme, element) 73 GetHighlight_patcher = unittest.mock.patch.object( 74 idlelib.sidebar.idleConf, 'GetHighlight', mock_idleconf_GetHighlight) 75 GetHighlight_patcher.start() 76 self.addCleanup(GetHighlight_patcher.stop) 77 78 self.font_override = 'TkFixedFont' 79 def mock_idleconf_GetFont(root, configType, section): 80 return self.font_override 81 GetFont_patcher = unittest.mock.patch.object( 82 idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont) 83 GetFont_patcher.start() 84 self.addCleanup(GetFont_patcher.stop) 85 86 def tearDown(self): 87 self.text.delete('1.0', 'end') 88 89 def get_selection(self): 90 return tuple(map(str, self.text.tag_ranges('sel'))) 91 92 def get_line_screen_position(self, line): 93 bbox = self.linenumber.sidebar_text.bbox(f'{line}.end -1c') 94 x = bbox[0] + 2 95 y = bbox[1] + 2 96 return x, y 97 98 def assert_state_disabled(self): 99 state = self.linenumber.sidebar_text.config()['state'] 100 self.assertEqual(state[-1], tk.DISABLED) 101 102 def get_sidebar_text_contents(self): 103 return self.linenumber.sidebar_text.get('1.0', tk.END) 104 105 def assert_sidebar_n_lines(self, n_lines): 106 expected = '\n'.join(chain(map(str, range(1, n_lines + 1)), [''])) 107 self.assertEqual(self.get_sidebar_text_contents(), expected) 108 109 def assert_text_equals(self, expected): 110 return self.assertEqual(self.text.get('1.0', 'end'), expected) 111 112 def test_init_empty(self): 113 self.assert_sidebar_n_lines(1) 114 115 def test_init_not_empty(self): 116 self.text.insert('insert', 'foo bar\n'*3) 117 self.assert_text_equals('foo bar\n'*3 + '\n') 118 self.assert_sidebar_n_lines(4) 119 120 def test_toggle_linenumbering(self): 121 self.assertEqual(self.linenumber.is_shown, False) 122 self.linenumber.show_sidebar() 123 self.assertEqual(self.linenumber.is_shown, True) 124 self.linenumber.hide_sidebar() 125 self.assertEqual(self.linenumber.is_shown, False) 126 self.linenumber.hide_sidebar() 127 self.assertEqual(self.linenumber.is_shown, False) 128 self.linenumber.show_sidebar() 129 self.assertEqual(self.linenumber.is_shown, True) 130 self.linenumber.show_sidebar() 131 self.assertEqual(self.linenumber.is_shown, True) 132 133 def test_insert(self): 134 self.text.insert('insert', 'foobar') 135 self.assert_text_equals('foobar\n') 136 self.assert_sidebar_n_lines(1) 137 self.assert_state_disabled() 138 139 self.text.insert('insert', '\nfoo') 140 self.assert_text_equals('foobar\nfoo\n') 141 self.assert_sidebar_n_lines(2) 142 self.assert_state_disabled() 143 144 self.text.insert('insert', 'hello\n'*2) 145 self.assert_text_equals('foobar\nfoohello\nhello\n\n') 146 self.assert_sidebar_n_lines(4) 147 self.assert_state_disabled() 148 149 self.text.insert('insert', '\nworld') 150 self.assert_text_equals('foobar\nfoohello\nhello\n\nworld\n') 151 self.assert_sidebar_n_lines(5) 152 self.assert_state_disabled() 153 154 def test_delete(self): 155 self.text.insert('insert', 'foobar') 156 self.assert_text_equals('foobar\n') 157 self.text.delete('1.1', '1.3') 158 self.assert_text_equals('fbar\n') 159 self.assert_sidebar_n_lines(1) 160 self.assert_state_disabled() 161 162 self.text.insert('insert', 'foo\n'*2) 163 self.assert_text_equals('fbarfoo\nfoo\n\n') 164 self.assert_sidebar_n_lines(3) 165 self.assert_state_disabled() 166 167 # Deleting up to "2.end" doesn't delete the final newline. 168 self.text.delete('2.0', '2.end') 169 self.assert_text_equals('fbarfoo\n\n\n') 170 self.assert_sidebar_n_lines(3) 171 self.assert_state_disabled() 172 173 self.text.delete('1.3', 'end') 174 self.assert_text_equals('fba\n') 175 self.assert_sidebar_n_lines(1) 176 self.assert_state_disabled() 177 178 # Text widgets always keep a single '\n' character at the end. 179 self.text.delete('1.0', 'end') 180 self.assert_text_equals('\n') 181 self.assert_sidebar_n_lines(1) 182 self.assert_state_disabled() 183 184 def test_sidebar_text_width(self): 185 """ 186 Test that linenumber text widget is always at the minimum 187 width 188 """ 189 def get_width(): 190 return self.linenumber.sidebar_text.config()['width'][-1] 191 192 self.assert_sidebar_n_lines(1) 193 self.assertEqual(get_width(), 1) 194 195 self.text.insert('insert', 'foo') 196 self.assert_sidebar_n_lines(1) 197 self.assertEqual(get_width(), 1) 198 199 self.text.insert('insert', 'foo\n'*8) 200 self.assert_sidebar_n_lines(9) 201 self.assertEqual(get_width(), 1) 202 203 self.text.insert('insert', 'foo\n') 204 self.assert_sidebar_n_lines(10) 205 self.assertEqual(get_width(), 2) 206 207 self.text.insert('insert', 'foo\n') 208 self.assert_sidebar_n_lines(11) 209 self.assertEqual(get_width(), 2) 210 211 self.text.delete('insert -1l linestart', 'insert linestart') 212 self.assert_sidebar_n_lines(10) 213 self.assertEqual(get_width(), 2) 214 215 self.text.delete('insert -1l linestart', 'insert linestart') 216 self.assert_sidebar_n_lines(9) 217 self.assertEqual(get_width(), 1) 218 219 self.text.insert('insert', 'foo\n'*90) 220 self.assert_sidebar_n_lines(99) 221 self.assertEqual(get_width(), 2) 222 223 self.text.insert('insert', 'foo\n') 224 self.assert_sidebar_n_lines(100) 225 self.assertEqual(get_width(), 3) 226 227 self.text.insert('insert', 'foo\n') 228 self.assert_sidebar_n_lines(101) 229 self.assertEqual(get_width(), 3) 230 231 self.text.delete('insert -1l linestart', 'insert linestart') 232 self.assert_sidebar_n_lines(100) 233 self.assertEqual(get_width(), 3) 234 235 self.text.delete('insert -1l linestart', 'insert linestart') 236 self.assert_sidebar_n_lines(99) 237 self.assertEqual(get_width(), 2) 238 239 self.text.delete('50.0 -1c', 'end -1c') 240 self.assert_sidebar_n_lines(49) 241 self.assertEqual(get_width(), 2) 242 243 self.text.delete('5.0 -1c', 'end -1c') 244 self.assert_sidebar_n_lines(4) 245 self.assertEqual(get_width(), 1) 246 247 # Text widgets always keep a single '\n' character at the end. 248 self.text.delete('1.0', 'end -1c') 249 self.assert_sidebar_n_lines(1) 250 self.assertEqual(get_width(), 1) 251 252 # The following tests are temporarily disabled due to relying on 253 # simulated user input and inspecting which text is selected, which 254 # are fragile and can fail when several GUI tests are run in parallel 255 # or when the windows created by the test lose focus. 256 # 257 # TODO: Re-work these tests or remove them from the test suite. 258 259 @unittest.skip('test disabled') 260 def test_click_selection(self): 261 self.linenumber.show_sidebar() 262 self.text.insert('1.0', 'one\ntwo\nthree\nfour\n') 263 self.root.update() 264 265 # Click on the second line. 266 x, y = self.get_line_screen_position(2) 267 self.linenumber.sidebar_text.event_generate('<Button-1>', x=x, y=y) 268 self.linenumber.sidebar_text.update() 269 self.root.update() 270 271 self.assertEqual(self.get_selection(), ('2.0', '3.0')) 272 273 def simulate_drag(self, start_line, end_line): 274 start_x, start_y = self.get_line_screen_position(start_line) 275 end_x, end_y = self.get_line_screen_position(end_line) 276 277 self.linenumber.sidebar_text.event_generate('<Button-1>', 278 x=start_x, y=start_y) 279 self.root.update() 280 281 def lerp(a, b, steps): 282 """linearly interpolate from a to b (inclusive) in equal steps""" 283 last_step = steps - 1 284 for i in range(steps): 285 yield ((last_step - i) / last_step) * a + (i / last_step) * b 286 287 for x, y in zip( 288 map(int, lerp(start_x, end_x, steps=11)), 289 map(int, lerp(start_y, end_y, steps=11)), 290 ): 291 self.linenumber.sidebar_text.event_generate('<B1-Motion>', x=x, y=y) 292 self.root.update() 293 294 self.linenumber.sidebar_text.event_generate('<ButtonRelease-1>', 295 x=end_x, y=end_y) 296 self.root.update() 297 298 @unittest.skip('test disabled') 299 def test_drag_selection_down(self): 300 self.linenumber.show_sidebar() 301 self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') 302 self.root.update() 303 304 # Drag from the second line to the fourth line. 305 self.simulate_drag(2, 4) 306 self.assertEqual(self.get_selection(), ('2.0', '5.0')) 307 308 @unittest.skip('test disabled') 309 def test_drag_selection_up(self): 310 self.linenumber.show_sidebar() 311 self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') 312 self.root.update() 313 314 # Drag from the fourth line to the second line. 315 self.simulate_drag(4, 2) 316 self.assertEqual(self.get_selection(), ('2.0', '5.0')) 317 318 def test_scroll(self): 319 self.linenumber.show_sidebar() 320 self.text.insert('1.0', 'line\n' * 100) 321 self.root.update() 322 323 # Scroll down 10 lines. 324 self.text.yview_scroll(10, 'unit') 325 self.root.update() 326 self.assertEqual(self.text.index('@0,0'), '11.0') 327 self.assertEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') 328 329 # Generate a mouse-wheel event and make sure it scrolled up or down. 330 # The meaning of the "delta" is OS-dependant, so this just checks for 331 # any change. 332 self.linenumber.sidebar_text.event_generate('<MouseWheel>', 333 x=0, y=0, 334 delta=10) 335 self.root.update() 336 self.assertNotEqual(self.text.index('@0,0'), '11.0') 337 self.assertNotEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') 338 339 def test_font(self): 340 ln = self.linenumber 341 342 orig_font = ln.sidebar_text['font'] 343 test_font = 'TkTextFont' 344 self.assertNotEqual(orig_font, test_font) 345 346 # Ensure line numbers aren't shown. 347 ln.hide_sidebar() 348 349 self.font_override = test_font 350 # Nothing breaks when line numbers aren't shown. 351 ln.update_font() 352 353 # Activate line numbers, previous font change is immediately effective. 354 ln.show_sidebar() 355 self.assertEqual(ln.sidebar_text['font'], test_font) 356 357 # Call the font update with line numbers shown, change is picked up. 358 self.font_override = orig_font 359 ln.update_font() 360 self.assertEqual(ln.sidebar_text['font'], orig_font) 361 362 def test_highlight_colors(self): 363 ln = self.linenumber 364 365 orig_colors = dict(self.highlight_cfg) 366 test_colors = {'background': '#222222', 'foreground': '#ffff00'} 367 368 def assert_colors_are_equal(colors): 369 self.assertEqual(ln.sidebar_text['background'], colors['background']) 370 self.assertEqual(ln.sidebar_text['foreground'], colors['foreground']) 371 372 # Ensure line numbers aren't shown. 373 ln.hide_sidebar() 374 375 self.highlight_cfg = test_colors 376 # Nothing breaks with inactive line numbers. 377 ln.update_colors() 378 379 # Show line numbers, previous colors change is immediately effective. 380 ln.show_sidebar() 381 assert_colors_are_equal(test_colors) 382 383 # Call colors update with no change to the configured colors. 384 ln.update_colors() 385 assert_colors_are_equal(test_colors) 386 387 # Call the colors update with line numbers shown, change is picked up. 388 self.highlight_cfg = orig_colors 389 ln.update_colors() 390 assert_colors_are_equal(orig_colors) 391 392 393class ShellSidebarTest(unittest.TestCase): 394 root: tk.Tk = None 395 shell: PyShell = None 396 397 @classmethod 398 def setUpClass(cls): 399 requires('gui') 400 401 cls.root = root = tk.Tk() 402 root.withdraw() 403 404 fix_scaling(root) 405 fixwordbreaks(root) 406 fix_x11_paste(root) 407 408 cls.flist = flist = PyShellFileList(root) 409 # See #43981 about macosx.setupApp(root, flist) causing failure. 410 root.update_idletasks() 411 412 cls.init_shell() 413 414 @classmethod 415 def tearDownClass(cls): 416 if cls.shell is not None: 417 cls.shell.executing = False 418 cls.shell.close() 419 cls.shell = None 420 cls.flist = None 421 cls.root.update_idletasks() 422 cls.root.destroy() 423 cls.root = None 424 425 @classmethod 426 def init_shell(cls): 427 cls.shell = cls.flist.open_shell() 428 cls.shell.pollinterval = 10 429 cls.root.update() 430 cls.n_preface_lines = get_lineno(cls.shell.text, 'end-1c') - 1 431 432 @classmethod 433 def reset_shell(cls): 434 cls.shell.per.bottom.delete(f'{cls.n_preface_lines+1}.0', 'end-1c') 435 cls.shell.shell_sidebar.update_sidebar() 436 cls.root.update() 437 438 def setUp(self): 439 # In some test environments, e.g. Azure Pipelines (as of 440 # Apr. 2021), sys.stdout is changed between tests. However, 441 # PyShell relies on overriding sys.stdout when run without a 442 # sub-process (as done here; see setUpClass). 443 self._saved_stdout = None 444 if sys.stdout != self.shell.stdout: 445 self._saved_stdout = sys.stdout 446 sys.stdout = self.shell.stdout 447 448 self.reset_shell() 449 450 def tearDown(self): 451 if self._saved_stdout is not None: 452 sys.stdout = self._saved_stdout 453 454 def get_sidebar_lines(self): 455 canvas = self.shell.shell_sidebar.canvas 456 texts = list(canvas.find(tk.ALL)) 457 texts_by_y_coords = { 458 canvas.bbox(text)[1]: canvas.itemcget(text, 'text') 459 for text in texts 460 } 461 line_y_coords = self.get_shell_line_y_coords() 462 return [texts_by_y_coords.get(y, None) for y in line_y_coords] 463 464 def assert_sidebar_lines_end_with(self, expected_lines): 465 self.shell.shell_sidebar.update_sidebar() 466 self.assertEqual( 467 self.get_sidebar_lines()[-len(expected_lines):], 468 expected_lines, 469 ) 470 471 def get_shell_line_y_coords(self): 472 text = self.shell.text 473 y_coords = [] 474 index = text.index("@0,0") 475 if index.split('.', 1)[1] != '0': 476 index = text.index(f"{index} +1line linestart") 477 while True: 478 lineinfo = text.dlineinfo(index) 479 if lineinfo is None: 480 break 481 y_coords.append(lineinfo[1]) 482 index = text.index(f"{index} +1line") 483 return y_coords 484 485 def get_sidebar_line_y_coords(self): 486 canvas = self.shell.shell_sidebar.canvas 487 texts = list(canvas.find(tk.ALL)) 488 texts.sort(key=lambda text: canvas.bbox(text)[1]) 489 return [canvas.bbox(text)[1] for text in texts] 490 491 def assert_sidebar_lines_synced(self): 492 self.assertLessEqual( 493 set(self.get_sidebar_line_y_coords()), 494 set(self.get_shell_line_y_coords()), 495 ) 496 497 def do_input(self, input): 498 shell = self.shell 499 text = shell.text 500 for line_index, line in enumerate(input.split('\n')): 501 if line_index > 0: 502 text.event_generate('<<newline-and-indent>>') 503 text.insert('insert', line, 'stdin') 504 505 def test_initial_state(self): 506 sidebar_lines = self.get_sidebar_lines() 507 self.assertEqual( 508 sidebar_lines, 509 [None] * (len(sidebar_lines) - 1) + ['>>>'], 510 ) 511 self.assert_sidebar_lines_synced() 512 513 @run_in_tk_mainloop() 514 def test_single_empty_input(self): 515 self.do_input('\n') 516 yield 517 self.assert_sidebar_lines_end_with(['>>>', '>>>']) 518 519 @run_in_tk_mainloop() 520 def test_single_line_statement(self): 521 self.do_input('1\n') 522 yield 523 self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) 524 525 @run_in_tk_mainloop() 526 def test_multi_line_statement(self): 527 # Block statements are not indented because IDLE auto-indents. 528 self.do_input(dedent('''\ 529 if True: 530 print(1) 531 532 ''')) 533 yield 534 self.assert_sidebar_lines_end_with([ 535 '>>>', 536 '...', 537 '...', 538 '...', 539 None, 540 '>>>', 541 ]) 542 543 @run_in_tk_mainloop() 544 def test_single_long_line_wraps(self): 545 self.do_input('1' * 200 + '\n') 546 yield 547 self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) 548 self.assert_sidebar_lines_synced() 549 550 @run_in_tk_mainloop() 551 def test_squeeze_multi_line_output(self): 552 shell = self.shell 553 text = shell.text 554 555 self.do_input('print("a\\nb\\nc")\n') 556 yield 557 self.assert_sidebar_lines_end_with(['>>>', None, None, None, '>>>']) 558 559 text.mark_set('insert', f'insert -1line linestart') 560 text.event_generate('<<squeeze-current-text>>') 561 yield 562 self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) 563 self.assert_sidebar_lines_synced() 564 565 shell.squeezer.expandingbuttons[0].expand() 566 yield 567 self.assert_sidebar_lines_end_with(['>>>', None, None, None, '>>>']) 568 self.assert_sidebar_lines_synced() 569 570 @run_in_tk_mainloop() 571 def test_interrupt_recall_undo_redo(self): 572 text = self.shell.text 573 # Block statements are not indented because IDLE auto-indents. 574 initial_sidebar_lines = self.get_sidebar_lines() 575 576 self.do_input(dedent('''\ 577 if True: 578 print(1) 579 ''')) 580 yield 581 self.assert_sidebar_lines_end_with(['>>>', '...', '...']) 582 with_block_sidebar_lines = self.get_sidebar_lines() 583 self.assertNotEqual(with_block_sidebar_lines, initial_sidebar_lines) 584 585 # Control-C 586 text.event_generate('<<interrupt-execution>>') 587 yield 588 self.assert_sidebar_lines_end_with(['>>>', '...', '...', None, '>>>']) 589 590 # Recall previous via history 591 text.event_generate('<<history-previous>>') 592 text.event_generate('<<interrupt-execution>>') 593 yield 594 self.assert_sidebar_lines_end_with(['>>>', '...', None, '>>>']) 595 596 # Recall previous via recall 597 text.mark_set('insert', text.index('insert -2l')) 598 text.event_generate('<<newline-and-indent>>') 599 yield 600 601 text.event_generate('<<undo>>') 602 yield 603 self.assert_sidebar_lines_end_with(['>>>']) 604 605 text.event_generate('<<redo>>') 606 yield 607 self.assert_sidebar_lines_end_with(['>>>', '...']) 608 609 text.event_generate('<<newline-and-indent>>') 610 text.event_generate('<<newline-and-indent>>') 611 yield 612 self.assert_sidebar_lines_end_with( 613 ['>>>', '...', '...', '...', None, '>>>'] 614 ) 615 616 @run_in_tk_mainloop() 617 def test_very_long_wrapped_line(self): 618 with swap_attr(self.shell, 'squeezer', None): 619 self.do_input('x = ' + '1'*10_000 + '\n') 620 yield 621 self.assertEqual(self.get_sidebar_lines(), ['>>>']) 622 623 def test_font(self): 624 sidebar = self.shell.shell_sidebar 625 626 test_font = 'TkTextFont' 627 628 def mock_idleconf_GetFont(root, configType, section): 629 return test_font 630 GetFont_patcher = unittest.mock.patch.object( 631 idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont) 632 GetFont_patcher.start() 633 def cleanup(): 634 GetFont_patcher.stop() 635 sidebar.update_font() 636 self.addCleanup(cleanup) 637 638 def get_sidebar_font(): 639 canvas = sidebar.canvas 640 texts = list(canvas.find(tk.ALL)) 641 fonts = {canvas.itemcget(text, 'font') for text in texts} 642 self.assertEqual(len(fonts), 1) 643 return next(iter(fonts)) 644 645 self.assertNotEqual(get_sidebar_font(), test_font) 646 sidebar.update_font() 647 self.assertEqual(get_sidebar_font(), test_font) 648 649 def test_highlight_colors(self): 650 sidebar = self.shell.shell_sidebar 651 652 test_colors = {"background": '#abcdef', "foreground": '#123456'} 653 654 orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight 655 def mock_idleconf_GetHighlight(theme, element): 656 if element in ['linenumber', 'console']: 657 return test_colors 658 return orig_idleConf_GetHighlight(theme, element) 659 GetHighlight_patcher = unittest.mock.patch.object( 660 idlelib.sidebar.idleConf, 'GetHighlight', 661 mock_idleconf_GetHighlight) 662 GetHighlight_patcher.start() 663 def cleanup(): 664 GetHighlight_patcher.stop() 665 sidebar.update_colors() 666 self.addCleanup(cleanup) 667 668 def get_sidebar_colors(): 669 canvas = sidebar.canvas 670 texts = list(canvas.find(tk.ALL)) 671 fgs = {canvas.itemcget(text, 'fill') for text in texts} 672 self.assertEqual(len(fgs), 1) 673 fg = next(iter(fgs)) 674 bg = canvas.cget('background') 675 return {"background": bg, "foreground": fg} 676 677 self.assertNotEqual(get_sidebar_colors(), test_colors) 678 sidebar.update_colors() 679 self.assertEqual(get_sidebar_colors(), test_colors) 680 681 @run_in_tk_mainloop() 682 def test_mousewheel(self): 683 sidebar = self.shell.shell_sidebar 684 text = self.shell.text 685 686 # Enter a 100-line string to scroll the shell screen down. 687 self.do_input('x = """' + '\n'*100 + '"""\n') 688 yield 689 self.assertGreater(get_lineno(text, '@0,0'), 1) 690 691 last_lineno = get_end_linenumber(text) 692 self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) 693 694 # Scroll up using the <MouseWheel> event. 695 # The meaning delta is platform-dependant. 696 delta = -1 if sys.platform == 'darwin' else 120 697 sidebar.canvas.event_generate('<MouseWheel>', x=0, y=0, delta=delta) 698 yield 699 self.assertIsNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) 700 701 # Scroll back down using the <Button-5> event. 702 sidebar.canvas.event_generate('<Button-5>', x=0, y=0) 703 yield 704 self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) 705 706 @run_in_tk_mainloop() 707 def test_copy(self): 708 sidebar = self.shell.shell_sidebar 709 text = self.shell.text 710 711 first_line = get_end_linenumber(text) 712 713 self.do_input(dedent('''\ 714 if True: 715 print(1) 716 717 ''')) 718 yield 719 720 text.tag_add('sel', f'{first_line}.0', 'end-1c') 721 selected_text = text.get('sel.first', 'sel.last') 722 self.assertTrue(selected_text.startswith('if True:\n')) 723 self.assertIn('\n1\n', selected_text) 724 725 text.event_generate('<<copy>>') 726 self.addCleanup(text.clipboard_clear) 727 728 copied_text = text.clipboard_get() 729 self.assertEqual(copied_text, selected_text) 730 731 @run_in_tk_mainloop() 732 def test_copy_with_prompts(self): 733 sidebar = self.shell.shell_sidebar 734 text = self.shell.text 735 736 first_line = get_end_linenumber(text) 737 self.do_input(dedent('''\ 738 if True: 739 print(1) 740 741 ''')) 742 yield 743 744 text.tag_add('sel', f'{first_line}.3', 'end-1c') 745 selected_text = text.get('sel.first', 'sel.last') 746 self.assertTrue(selected_text.startswith('True:\n')) 747 748 selected_lines_text = text.get('sel.first linestart', 'sel.last') 749 selected_lines = selected_lines_text.split('\n') 750 # Expect a block of input, a single output line, and a new prompt 751 expected_prompts = \ 752 ['>>>'] + ['...'] * (len(selected_lines) - 3) + [None, '>>>'] 753 selected_text_with_prompts = '\n'.join( 754 line if prompt is None else prompt + ' ' + line 755 for prompt, line in zip(expected_prompts, 756 selected_lines, 757 strict=True) 758 ) + '\n' 759 760 text.event_generate('<<copy-with-prompts>>') 761 self.addCleanup(text.clipboard_clear) 762 763 copied_text = text.clipboard_get() 764 self.assertEqual(copied_text, selected_text_with_prompts) 765 766 767if __name__ == '__main__': 768 unittest.main(verbosity=2) 769