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 (lineinfo := text.dlineinfo(index)) is not None: 478 y_coords.append(lineinfo[1]) 479 index = text.index(f"{index} +1line") 480 return y_coords 481 482 def get_sidebar_line_y_coords(self): 483 canvas = self.shell.shell_sidebar.canvas 484 texts = list(canvas.find(tk.ALL)) 485 texts.sort(key=lambda text: canvas.bbox(text)[1]) 486 return [canvas.bbox(text)[1] for text in texts] 487 488 def assert_sidebar_lines_synced(self): 489 self.assertLessEqual( 490 set(self.get_sidebar_line_y_coords()), 491 set(self.get_shell_line_y_coords()), 492 ) 493 494 def do_input(self, input): 495 shell = self.shell 496 text = shell.text 497 for line_index, line in enumerate(input.split('\n')): 498 if line_index > 0: 499 text.event_generate('<<newline-and-indent>>') 500 text.insert('insert', line, 'stdin') 501 502 def test_initial_state(self): 503 sidebar_lines = self.get_sidebar_lines() 504 self.assertEqual( 505 sidebar_lines, 506 [None] * (len(sidebar_lines) - 1) + ['>>>'], 507 ) 508 self.assert_sidebar_lines_synced() 509 510 @run_in_tk_mainloop() 511 def test_single_empty_input(self): 512 self.do_input('\n') 513 yield 514 self.assert_sidebar_lines_end_with(['>>>', '>>>']) 515 516 @run_in_tk_mainloop() 517 def test_single_line_statement(self): 518 self.do_input('1\n') 519 yield 520 self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) 521 522 @run_in_tk_mainloop() 523 def test_multi_line_statement(self): 524 # Block statements are not indented because IDLE auto-indents. 525 self.do_input(dedent('''\ 526 if True: 527 print(1) 528 529 ''')) 530 yield 531 self.assert_sidebar_lines_end_with([ 532 '>>>', 533 '...', 534 '...', 535 '...', 536 None, 537 '>>>', 538 ]) 539 540 @run_in_tk_mainloop() 541 def test_single_long_line_wraps(self): 542 self.do_input('1' * 200 + '\n') 543 yield 544 self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) 545 self.assert_sidebar_lines_synced() 546 547 @run_in_tk_mainloop() 548 def test_squeeze_multi_line_output(self): 549 shell = self.shell 550 text = shell.text 551 552 self.do_input('print("a\\nb\\nc")\n') 553 yield 554 self.assert_sidebar_lines_end_with(['>>>', None, None, None, '>>>']) 555 556 text.mark_set('insert', f'insert -1line linestart') 557 text.event_generate('<<squeeze-current-text>>') 558 yield 559 self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) 560 self.assert_sidebar_lines_synced() 561 562 shell.squeezer.expandingbuttons[0].expand() 563 yield 564 self.assert_sidebar_lines_end_with(['>>>', None, None, None, '>>>']) 565 self.assert_sidebar_lines_synced() 566 567 @run_in_tk_mainloop() 568 def test_interrupt_recall_undo_redo(self): 569 text = self.shell.text 570 # Block statements are not indented because IDLE auto-indents. 571 initial_sidebar_lines = self.get_sidebar_lines() 572 573 self.do_input(dedent('''\ 574 if True: 575 print(1) 576 ''')) 577 yield 578 self.assert_sidebar_lines_end_with(['>>>', '...', '...']) 579 with_block_sidebar_lines = self.get_sidebar_lines() 580 self.assertNotEqual(with_block_sidebar_lines, initial_sidebar_lines) 581 582 # Control-C 583 text.event_generate('<<interrupt-execution>>') 584 yield 585 self.assert_sidebar_lines_end_with(['>>>', '...', '...', None, '>>>']) 586 587 # Recall previous via history 588 text.event_generate('<<history-previous>>') 589 text.event_generate('<<interrupt-execution>>') 590 yield 591 self.assert_sidebar_lines_end_with(['>>>', '...', None, '>>>']) 592 593 # Recall previous via recall 594 text.mark_set('insert', text.index('insert -2l')) 595 text.event_generate('<<newline-and-indent>>') 596 yield 597 598 text.event_generate('<<undo>>') 599 yield 600 self.assert_sidebar_lines_end_with(['>>>']) 601 602 text.event_generate('<<redo>>') 603 yield 604 self.assert_sidebar_lines_end_with(['>>>', '...']) 605 606 text.event_generate('<<newline-and-indent>>') 607 text.event_generate('<<newline-and-indent>>') 608 yield 609 self.assert_sidebar_lines_end_with( 610 ['>>>', '...', '...', '...', None, '>>>'] 611 ) 612 613 @run_in_tk_mainloop() 614 def test_very_long_wrapped_line(self): 615 with swap_attr(self.shell, 'squeezer', None): 616 self.do_input('x = ' + '1'*10_000 + '\n') 617 yield 618 self.assertEqual(self.get_sidebar_lines(), ['>>>']) 619 620 def test_font(self): 621 sidebar = self.shell.shell_sidebar 622 623 test_font = 'TkTextFont' 624 625 def mock_idleconf_GetFont(root, configType, section): 626 return test_font 627 GetFont_patcher = unittest.mock.patch.object( 628 idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont) 629 GetFont_patcher.start() 630 def cleanup(): 631 GetFont_patcher.stop() 632 sidebar.update_font() 633 self.addCleanup(cleanup) 634 635 def get_sidebar_font(): 636 canvas = sidebar.canvas 637 texts = list(canvas.find(tk.ALL)) 638 fonts = {canvas.itemcget(text, 'font') for text in texts} 639 self.assertEqual(len(fonts), 1) 640 return next(iter(fonts)) 641 642 self.assertNotEqual(get_sidebar_font(), test_font) 643 sidebar.update_font() 644 self.assertEqual(get_sidebar_font(), test_font) 645 646 def test_highlight_colors(self): 647 sidebar = self.shell.shell_sidebar 648 649 test_colors = {"background": '#abcdef', "foreground": '#123456'} 650 651 orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight 652 def mock_idleconf_GetHighlight(theme, element): 653 if element in ['linenumber', 'console']: 654 return test_colors 655 return orig_idleConf_GetHighlight(theme, element) 656 GetHighlight_patcher = unittest.mock.patch.object( 657 idlelib.sidebar.idleConf, 'GetHighlight', 658 mock_idleconf_GetHighlight) 659 GetHighlight_patcher.start() 660 def cleanup(): 661 GetHighlight_patcher.stop() 662 sidebar.update_colors() 663 self.addCleanup(cleanup) 664 665 def get_sidebar_colors(): 666 canvas = sidebar.canvas 667 texts = list(canvas.find(tk.ALL)) 668 fgs = {canvas.itemcget(text, 'fill') for text in texts} 669 self.assertEqual(len(fgs), 1) 670 fg = next(iter(fgs)) 671 bg = canvas.cget('background') 672 return {"background": bg, "foreground": fg} 673 674 self.assertNotEqual(get_sidebar_colors(), test_colors) 675 sidebar.update_colors() 676 self.assertEqual(get_sidebar_colors(), test_colors) 677 678 @run_in_tk_mainloop() 679 def test_mousewheel(self): 680 sidebar = self.shell.shell_sidebar 681 text = self.shell.text 682 683 # Enter a 100-line string to scroll the shell screen down. 684 self.do_input('x = """' + '\n'*100 + '"""\n') 685 yield 686 self.assertGreater(get_lineno(text, '@0,0'), 1) 687 688 last_lineno = get_end_linenumber(text) 689 self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) 690 691 # Scroll up using the <MouseWheel> event. 692 # The meaning delta is platform-dependant. 693 delta = -1 if sys.platform == 'darwin' else 120 694 sidebar.canvas.event_generate('<MouseWheel>', x=0, y=0, delta=delta) 695 yield 696 self.assertIsNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) 697 698 # Scroll back down using the <Button-5> event. 699 sidebar.canvas.event_generate('<Button-5>', x=0, y=0) 700 yield 701 self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) 702 703 @run_in_tk_mainloop() 704 def test_copy(self): 705 sidebar = self.shell.shell_sidebar 706 text = self.shell.text 707 708 first_line = get_end_linenumber(text) 709 710 self.do_input(dedent('''\ 711 if True: 712 print(1) 713 714 ''')) 715 yield 716 717 text.tag_add('sel', f'{first_line}.0', 'end-1c') 718 selected_text = text.get('sel.first', 'sel.last') 719 self.assertTrue(selected_text.startswith('if True:\n')) 720 self.assertIn('\n1\n', selected_text) 721 722 text.event_generate('<<copy>>') 723 self.addCleanup(text.clipboard_clear) 724 725 copied_text = text.clipboard_get() 726 self.assertEqual(copied_text, selected_text) 727 728 @run_in_tk_mainloop() 729 def test_copy_with_prompts(self): 730 sidebar = self.shell.shell_sidebar 731 text = self.shell.text 732 733 first_line = get_end_linenumber(text) 734 self.do_input(dedent('''\ 735 if True: 736 print(1) 737 738 ''')) 739 yield 740 741 text.tag_add('sel', f'{first_line}.3', 'end-1c') 742 selected_text = text.get('sel.first', 'sel.last') 743 self.assertTrue(selected_text.startswith('True:\n')) 744 745 selected_lines_text = text.get('sel.first linestart', 'sel.last') 746 selected_lines = selected_lines_text.split('\n') 747 # Expect a block of input, a single output line, and a new prompt 748 expected_prompts = \ 749 ['>>>'] + ['...'] * (len(selected_lines) - 3) + [None, '>>>'] 750 selected_text_with_prompts = '\n'.join( 751 line if prompt is None else prompt + ' ' + line 752 for prompt, line in zip(expected_prompts, 753 selected_lines, 754 strict=True) 755 ) + '\n' 756 757 text.event_generate('<<copy-with-prompts>>') 758 self.addCleanup(text.clipboard_clear) 759 760 copied_text = text.clipboard_get() 761 self.assertEqual(copied_text, selected_text_with_prompts) 762 763 764if __name__ == '__main__': 765 unittest.main(verbosity=2) 766