(root)/
Python-3.12.0/
Lib/
idlelib/
idle_test/
test_sidebar.py
       1  """Test sidebar, coverage 85%"""
       2  from textwrap import dedent
       3  import sys
       4  
       5  from itertools import chain
       6  import unittest
       7  import unittest.mock
       8  from test.support import requires, swap_attr
       9  from test import support
      10  import tkinter as tk
      11  from idlelib.idle_test.tkinter_testing_utils import run_in_tk_mainloop
      12  
      13  from idlelib.delegator import Delegator
      14  from idlelib.editor import fixwordbreaks
      15  from idlelib.percolator import Percolator
      16  import idlelib.pyshell
      17  from idlelib.pyshell import fix_x11_paste, PyShell, PyShellFileList
      18  from idlelib.run import fix_scaling
      19  import idlelib.sidebar
      20  from idlelib.sidebar import get_end_linenumber, get_lineno
      21  
      22  
      23  class ESC[4;38;5;81mDummy_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  
      38  class ESC[4;38;5;81mLineNumbersTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
      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  
     394  class ESC[4;38;5;81mShellSidebarTest(ESC[4;38;5;149munittestESC[4;38;5;149m.ESC[4;38;5;149mTestCase):
     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  
     768  if __name__ == '__main__':
     769      unittest.main(verbosity=2)