Coverage for idle_test/test_sidebar.py: 22%
519 statements
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
« prev ^ index » next coverage.py v7.2.5, created at 2023-05-11 13:22 -0700
1"""Test sidebar, coverage 85%"""
2from textwrap import dedent
3import sys
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
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
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)
31 def setvar(self, name, value):
32 pass
34 def getlineno(self, index):
35 return int(float(self.text.index(index)))
38class LineNumbersTest(unittest.TestCase):
40 @classmethod
41 def setUpClass(cls):
42 requires('gui')
43 cls.root = tk.Tk()
44 cls.root.withdraw()
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)
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)
54 cls.editwin = Dummy_editwin(cls.text)
55 cls.editwin.vbar = tk.Scrollbar(cls.text_frame)
57 @classmethod
58 def tearDownClass(cls):
59 cls.editwin.per.close()
60 cls.root.update()
61 cls.root.destroy()
62 del cls.text, cls.text_frame, cls.editwin, cls.root
64 def setUp(self):
65 self.linenumber = idlelib.sidebar.LineNumbers(self.editwin)
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)
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)
87 def tearDown(self):
88 self.text.delete('1.0', 'end')
90 def get_selection(self):
91 return tuple(map(str, self.text.tag_ranges('sel')))
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
99 def assert_state_disabled(self):
100 state = self.linenumber.sidebar_text.config()['state']
101 self.assertEqual(state[-1], tk.DISABLED)
103 def get_sidebar_text_contents(self):
104 return self.linenumber.sidebar_text.get('1.0', tk.END)
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)
110 def assert_text_equals(self, expected):
111 return self.assertEqual(self.text.get('1.0', 'end'), expected)
113 def test_init_empty(self):
114 self.assert_sidebar_n_lines(1)
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)
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)
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()
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()
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()
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()
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()
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()
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()
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()
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()
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]
193 self.assert_sidebar_n_lines(1)
194 self.assertEqual(get_width(), 1)
196 self.text.insert('insert', 'foo')
197 self.assert_sidebar_n_lines(1)
198 self.assertEqual(get_width(), 1)
200 self.text.insert('insert', 'foo\n'*8)
201 self.assert_sidebar_n_lines(9)
202 self.assertEqual(get_width(), 1)
204 self.text.insert('insert', 'foo\n')
205 self.assert_sidebar_n_lines(10)
206 self.assertEqual(get_width(), 2)
208 self.text.insert('insert', 'foo\n')
209 self.assert_sidebar_n_lines(11)
210 self.assertEqual(get_width(), 2)
212 self.text.delete('insert -1l linestart', 'insert linestart')
213 self.assert_sidebar_n_lines(10)
214 self.assertEqual(get_width(), 2)
216 self.text.delete('insert -1l linestart', 'insert linestart')
217 self.assert_sidebar_n_lines(9)
218 self.assertEqual(get_width(), 1)
220 self.text.insert('insert', 'foo\n'*90)
221 self.assert_sidebar_n_lines(99)
222 self.assertEqual(get_width(), 2)
224 self.text.insert('insert', 'foo\n')
225 self.assert_sidebar_n_lines(100)
226 self.assertEqual(get_width(), 3)
228 self.text.insert('insert', 'foo\n')
229 self.assert_sidebar_n_lines(101)
230 self.assertEqual(get_width(), 3)
232 self.text.delete('insert -1l linestart', 'insert linestart')
233 self.assert_sidebar_n_lines(100)
234 self.assertEqual(get_width(), 3)
236 self.text.delete('insert -1l linestart', 'insert linestart')
237 self.assert_sidebar_n_lines(99)
238 self.assertEqual(get_width(), 2)
240 self.text.delete('50.0 -1c', 'end -1c')
241 self.assert_sidebar_n_lines(49)
242 self.assertEqual(get_width(), 2)
244 self.text.delete('5.0 -1c', 'end -1c')
245 self.assert_sidebar_n_lines(4)
246 self.assertEqual(get_width(), 1)
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)
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.
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()
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()
272 self.assertEqual(self.get_selection(), ('2.0', '3.0'))
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)
278 self.linenumber.sidebar_text.event_generate('<Button-1>',
279 x=start_x, y=start_y)
280 self.root.update()
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
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()
295 self.linenumber.sidebar_text.event_generate('<ButtonRelease-1>',
296 x=end_x, y=end_y)
297 self.root.update()
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()
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'))
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()
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'))
319 def test_scroll(self):
320 self.linenumber.show_sidebar()
321 self.text.insert('1.0', 'line\n' * 100)
322 self.root.update()
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')
330 # Generate a mouse-wheel event and make sure it scrolled up or down.
331 # The meaning of the "delta" is OS-dependant, 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')
340 def test_font(self):
341 ln = self.linenumber
343 orig_font = ln.sidebar_text['font']
344 test_font = 'TkTextFont'
345 self.assertNotEqual(orig_font, test_font)
347 # Ensure line numbers aren't shown.
348 ln.hide_sidebar()
350 self.font_override = test_font
351 # Nothing breaks when line numbers aren't shown.
352 ln.update_font()
354 # Activate line numbers, previous font change is immediately effective.
355 ln.show_sidebar()
356 self.assertEqual(ln.sidebar_text['font'], test_font)
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)
363 def test_highlight_colors(self):
364 ln = self.linenumber
366 orig_colors = dict(self.highlight_cfg)
367 test_colors = {'background': '#222222', 'foreground': '#ffff00'}
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'])
373 # Ensure line numbers aren't shown.
374 ln.hide_sidebar()
376 self.highlight_cfg = test_colors
377 # Nothing breaks with inactive line numbers.
378 ln.update_colors()
380 # Show line numbers, previous colors change is immediately effective.
381 ln.show_sidebar()
382 assert_colors_are_equal(test_colors)
384 # Call colors update with no change to the configured colors.
385 ln.update_colors()
386 assert_colors_are_equal(test_colors)
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)
394class ShellSidebarTest(unittest.TestCase):
395 root: tk.Tk = None
396 shell: PyShell = None
398 @classmethod
399 def setUpClass(cls):
400 requires('gui')
402 cls.root = root = tk.Tk()
403 root.withdraw()
405 fix_scaling(root)
406 fixwordbreaks(root)
407 fix_x11_paste(root)
409 cls.flist = flist = PyShellFileList(root)
410 # See #43981 about macosx.setupApp(root, flist) causing failure.
411 root.update_idletasks()
413 cls.init_shell()
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
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
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()
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
449 self.reset_shell()
451 def tearDown(self):
452 if self._saved_stdout is not None:
453 sys.stdout = self._saved_stdout
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]
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 )
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
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]
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 )
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')
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()
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(['>>>', '>>>'])
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, '>>>'])
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)
530 '''))
531 yield
532 self.assert_sidebar_lines_end_with([
533 '>>>',
534 '...',
535 '...',
536 '...',
537 None,
538 '>>>',
539 ])
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()
548 @run_in_tk_mainloop()
549 def test_squeeze_multi_line_output(self):
550 shell = self.shell
551 text = shell.text
553 self.do_input('print("a\\nb\\nc")\n')
554 yield
555 self.assert_sidebar_lines_end_with(['>>>', None, None, None, '>>>'])
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()
563 shell.squeezer.expandingbuttons[0].expand()
564 yield
565 self.assert_sidebar_lines_end_with(['>>>', None, None, None, '>>>'])
566 self.assert_sidebar_lines_synced()
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()
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)
583 # Control-C
584 text.event_generate('<<interrupt-execution>>')
585 yield
586 self.assert_sidebar_lines_end_with(['>>>', '...', '...', None, '>>>'])
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, '>>>'])
594 # Recall previous via recall
595 text.mark_set('insert', text.index('insert -2l'))
596 text.event_generate('<<newline-and-indent>>')
597 yield
599 text.event_generate('<<undo>>')
600 yield
601 self.assert_sidebar_lines_end_with(['>>>'])
603 text.event_generate('<<redo>>')
604 yield
605 self.assert_sidebar_lines_end_with(['>>>', '...'])
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 )
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(), ['>>>'])
622 def test_font(self):
623 sidebar = self.shell.shell_sidebar
625 test_font = 'TkTextFont'
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)
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))
644 self.assertNotEqual(get_sidebar_font(), test_font)
645 sidebar.update_font()
646 self.assertEqual(get_sidebar_font(), test_font)
648 def test_highlight_colors(self):
649 sidebar = self.shell.shell_sidebar
651 test_colors = {"background": '#abcdef', "foreground": '#123456'}
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)
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}
676 self.assertNotEqual(get_sidebar_colors(), test_colors)
677 sidebar.update_colors()
678 self.assertEqual(get_sidebar_colors(), test_colors)
680 @run_in_tk_mainloop()
681 def test_mousewheel(self):
682 sidebar = self.shell.shell_sidebar
683 text = self.shell.text
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)
690 last_lineno = get_end_linenumber(text)
691 self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0')))
693 # Scroll up using the <MouseWheel> event.
694 # The meaning delta is platform-dependant.
695 delta = -1 if sys.platform == 'darwin' else 120
696 sidebar.canvas.event_generate('<MouseWheel>', x=0, y=0, delta=delta)
697 yield
698 self.assertIsNone(text.dlineinfo(text.index(f'{last_lineno}.0')))
700 # Scroll back down using the <Button-5> event.
701 sidebar.canvas.event_generate('<Button-5>', x=0, y=0)
702 yield
703 self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0')))
705 @run_in_tk_mainloop()
706 def test_copy(self):
707 sidebar = self.shell.shell_sidebar
708 text = self.shell.text
710 first_line = get_end_linenumber(text)
712 self.do_input(dedent('''\
713 if True:
714 print(1)
716 '''))
717 yield
719 text.tag_add('sel', f'{first_line}.0', 'end-1c')
720 selected_text = text.get('sel.first', 'sel.last')
721 self.assertTrue(selected_text.startswith('if True:\n'))
722 self.assertIn('\n1\n', selected_text)
724 text.event_generate('<<copy>>')
725 self.addCleanup(text.clipboard_clear)
727 copied_text = text.clipboard_get()
728 self.assertEqual(copied_text, selected_text)
730 @run_in_tk_mainloop()
731 def test_copy_with_prompts(self):
732 sidebar = self.shell.shell_sidebar
733 text = self.shell.text
735 first_line = get_end_linenumber(text)
736 self.do_input(dedent('''\
737 if True:
738 print(1)
740 '''))
741 yield
743 text.tag_add('sel', f'{first_line}.3', 'end-1c')
744 selected_text = text.get('sel.first', 'sel.last')
745 self.assertTrue(selected_text.startswith('True:\n'))
747 selected_lines_text = text.get('sel.first linestart', 'sel.last')
748 selected_lines = selected_lines_text.split('\n')
749 selected_lines.pop() # Final '' is a split artifact, not a line.
750 # Expect a block of input and a single output line.
751 expected_prompts = \
752 ['>>>'] + ['...'] * (len(selected_lines) - 2) + [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'
760 text.event_generate('<<copy-with-prompts>>')
761 self.addCleanup(text.clipboard_clear)
763 copied_text = text.clipboard_get()
764 self.assertEqual(copied_text, selected_text_with_prompts)
767if __name__ == '__main__': 767 ↛ 768line 767 didn't jump to line 768, because the condition on line 767 was never true
768 unittest.main(verbosity=2)