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