• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Test sidebar, coverage 93%"""
2import idlelib.sidebar
3from sys import platform
4from itertools import chain
5import unittest
6import unittest.mock
7from test.support import requires
8import tkinter as tk
9
10from idlelib.delegator import Delegator
11from idlelib.percolator import Percolator
12
13
14class Dummy_editwin:
15    def __init__(self, text):
16        self.text = text
17        self.text_frame = self.text.master
18        self.per = Percolator(text)
19        self.undo = Delegator()
20        self.per.insertfilter(self.undo)
21
22    def setvar(self, name, value):
23        pass
24
25    def getlineno(self, index):
26        return int(float(self.text.index(index)))
27
28
29class LineNumbersTest(unittest.TestCase):
30
31    @classmethod
32    def setUpClass(cls):
33        requires('gui')
34        cls.root = tk.Tk()
35
36        cls.text_frame = tk.Frame(cls.root)
37        cls.text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
38        cls.text_frame.rowconfigure(1, weight=1)
39        cls.text_frame.columnconfigure(1, weight=1)
40
41        cls.text = tk.Text(cls.text_frame, width=80, height=24, wrap=tk.NONE)
42        cls.text.grid(row=1, column=1, sticky=tk.NSEW)
43
44        cls.editwin = Dummy_editwin(cls.text)
45        cls.editwin.vbar = tk.Scrollbar(cls.text_frame)
46
47    @classmethod
48    def tearDownClass(cls):
49        cls.editwin.per.close()
50        cls.root.update()
51        cls.root.destroy()
52        del cls.text, cls.text_frame, cls.editwin, cls.root
53
54    def setUp(self):
55        self.linenumber = idlelib.sidebar.LineNumbers(self.editwin)
56
57        self.highlight_cfg = {"background": '#abcdef',
58                              "foreground": '#123456'}
59        orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight
60        def mock_idleconf_GetHighlight(theme, element):
61            if element == 'linenumber':
62                return self.highlight_cfg
63            return orig_idleConf_GetHighlight(theme, element)
64        GetHighlight_patcher = unittest.mock.patch.object(
65            idlelib.sidebar.idleConf, 'GetHighlight', mock_idleconf_GetHighlight)
66        GetHighlight_patcher.start()
67        self.addCleanup(GetHighlight_patcher.stop)
68
69        self.font_override = 'TkFixedFont'
70        def mock_idleconf_GetFont(root, configType, section):
71            return self.font_override
72        GetFont_patcher = unittest.mock.patch.object(
73            idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont)
74        GetFont_patcher.start()
75        self.addCleanup(GetFont_patcher.stop)
76
77    def tearDown(self):
78        self.text.delete('1.0', 'end')
79
80    def get_selection(self):
81        return tuple(map(str, self.text.tag_ranges('sel')))
82
83    def get_line_screen_position(self, line):
84        bbox = self.linenumber.sidebar_text.bbox(f'{line}.end -1c')
85        x = bbox[0] + 2
86        y = bbox[1] + 2
87        return x, y
88
89    def assert_state_disabled(self):
90        state = self.linenumber.sidebar_text.config()['state']
91        self.assertEqual(state[-1], tk.DISABLED)
92
93    def get_sidebar_text_contents(self):
94        return self.linenumber.sidebar_text.get('1.0', tk.END)
95
96    def assert_sidebar_n_lines(self, n_lines):
97        expected = '\n'.join(chain(map(str, range(1, n_lines + 1)), ['']))
98        self.assertEqual(self.get_sidebar_text_contents(), expected)
99
100    def assert_text_equals(self, expected):
101        return self.assertEqual(self.text.get('1.0', 'end'), expected)
102
103    def test_init_empty(self):
104        self.assert_sidebar_n_lines(1)
105
106    def test_init_not_empty(self):
107        self.text.insert('insert', 'foo bar\n'*3)
108        self.assert_text_equals('foo bar\n'*3 + '\n')
109        self.assert_sidebar_n_lines(4)
110
111    def test_toggle_linenumbering(self):
112        self.assertEqual(self.linenumber.is_shown, False)
113        self.linenumber.show_sidebar()
114        self.assertEqual(self.linenumber.is_shown, True)
115        self.linenumber.hide_sidebar()
116        self.assertEqual(self.linenumber.is_shown, False)
117        self.linenumber.hide_sidebar()
118        self.assertEqual(self.linenumber.is_shown, False)
119        self.linenumber.show_sidebar()
120        self.assertEqual(self.linenumber.is_shown, True)
121        self.linenumber.show_sidebar()
122        self.assertEqual(self.linenumber.is_shown, True)
123
124    def test_insert(self):
125        self.text.insert('insert', 'foobar')
126        self.assert_text_equals('foobar\n')
127        self.assert_sidebar_n_lines(1)
128        self.assert_state_disabled()
129
130        self.text.insert('insert', '\nfoo')
131        self.assert_text_equals('foobar\nfoo\n')
132        self.assert_sidebar_n_lines(2)
133        self.assert_state_disabled()
134
135        self.text.insert('insert', 'hello\n'*2)
136        self.assert_text_equals('foobar\nfoohello\nhello\n\n')
137        self.assert_sidebar_n_lines(4)
138        self.assert_state_disabled()
139
140        self.text.insert('insert', '\nworld')
141        self.assert_text_equals('foobar\nfoohello\nhello\n\nworld\n')
142        self.assert_sidebar_n_lines(5)
143        self.assert_state_disabled()
144
145    def test_delete(self):
146        self.text.insert('insert', 'foobar')
147        self.assert_text_equals('foobar\n')
148        self.text.delete('1.1', '1.3')
149        self.assert_text_equals('fbar\n')
150        self.assert_sidebar_n_lines(1)
151        self.assert_state_disabled()
152
153        self.text.insert('insert', 'foo\n'*2)
154        self.assert_text_equals('fbarfoo\nfoo\n\n')
155        self.assert_sidebar_n_lines(3)
156        self.assert_state_disabled()
157
158        # Note: deleting up to "2.end" doesn't delete the final newline.
159        self.text.delete('2.0', '2.end')
160        self.assert_text_equals('fbarfoo\n\n\n')
161        self.assert_sidebar_n_lines(3)
162        self.assert_state_disabled()
163
164        self.text.delete('1.3', 'end')
165        self.assert_text_equals('fba\n')
166        self.assert_sidebar_n_lines(1)
167        self.assert_state_disabled()
168
169        # Note: Text widgets always keep a single '\n' character at the end.
170        self.text.delete('1.0', 'end')
171        self.assert_text_equals('\n')
172        self.assert_sidebar_n_lines(1)
173        self.assert_state_disabled()
174
175    def test_sidebar_text_width(self):
176        """
177        Test that linenumber text widget is always at the minimum
178        width
179        """
180        def get_width():
181            return self.linenumber.sidebar_text.config()['width'][-1]
182
183        self.assert_sidebar_n_lines(1)
184        self.assertEqual(get_width(), 1)
185
186        self.text.insert('insert', 'foo')
187        self.assert_sidebar_n_lines(1)
188        self.assertEqual(get_width(), 1)
189
190        self.text.insert('insert', 'foo\n'*8)
191        self.assert_sidebar_n_lines(9)
192        self.assertEqual(get_width(), 1)
193
194        self.text.insert('insert', 'foo\n')
195        self.assert_sidebar_n_lines(10)
196        self.assertEqual(get_width(), 2)
197
198        self.text.insert('insert', 'foo\n')
199        self.assert_sidebar_n_lines(11)
200        self.assertEqual(get_width(), 2)
201
202        self.text.delete('insert -1l linestart', 'insert linestart')
203        self.assert_sidebar_n_lines(10)
204        self.assertEqual(get_width(), 2)
205
206        self.text.delete('insert -1l linestart', 'insert linestart')
207        self.assert_sidebar_n_lines(9)
208        self.assertEqual(get_width(), 1)
209
210        self.text.insert('insert', 'foo\n'*90)
211        self.assert_sidebar_n_lines(99)
212        self.assertEqual(get_width(), 2)
213
214        self.text.insert('insert', 'foo\n')
215        self.assert_sidebar_n_lines(100)
216        self.assertEqual(get_width(), 3)
217
218        self.text.insert('insert', 'foo\n')
219        self.assert_sidebar_n_lines(101)
220        self.assertEqual(get_width(), 3)
221
222        self.text.delete('insert -1l linestart', 'insert linestart')
223        self.assert_sidebar_n_lines(100)
224        self.assertEqual(get_width(), 3)
225
226        self.text.delete('insert -1l linestart', 'insert linestart')
227        self.assert_sidebar_n_lines(99)
228        self.assertEqual(get_width(), 2)
229
230        self.text.delete('50.0 -1c', 'end -1c')
231        self.assert_sidebar_n_lines(49)
232        self.assertEqual(get_width(), 2)
233
234        self.text.delete('5.0 -1c', 'end -1c')
235        self.assert_sidebar_n_lines(4)
236        self.assertEqual(get_width(), 1)
237
238        # Note: Text widgets always keep a single '\n' character at the end.
239        self.text.delete('1.0', 'end -1c')
240        self.assert_sidebar_n_lines(1)
241        self.assertEqual(get_width(), 1)
242
243    def test_click_selection(self):
244        self.linenumber.show_sidebar()
245        self.text.insert('1.0', 'one\ntwo\nthree\nfour\n')
246        self.root.update()
247
248        # Click on the second line.
249        x, y = self.get_line_screen_position(2)
250        self.linenumber.sidebar_text.event_generate('<Button-1>', x=x, y=y)
251        self.linenumber.sidebar_text.update()
252        self.root.update()
253
254        self.assertEqual(self.get_selection(), ('2.0', '3.0'))
255
256    def simulate_drag(self, start_line, end_line):
257        start_x, start_y = self.get_line_screen_position(start_line)
258        end_x, end_y = self.get_line_screen_position(end_line)
259
260        self.linenumber.sidebar_text.event_generate('<Button-1>',
261                                                    x=start_x, y=start_y)
262        self.root.update()
263
264        def lerp(a, b, steps):
265            """linearly interpolate from a to b (inclusive) in equal steps"""
266            last_step = steps - 1
267            for i in range(steps):
268                yield ((last_step - i) / last_step) * a + (i / last_step) * b
269
270        for x, y in zip(
271                map(int, lerp(start_x, end_x, steps=11)),
272                map(int, lerp(start_y, end_y, steps=11)),
273        ):
274            self.linenumber.sidebar_text.event_generate('<B1-Motion>', x=x, y=y)
275            self.root.update()
276
277        self.linenumber.sidebar_text.event_generate('<ButtonRelease-1>',
278                                                    x=end_x, y=end_y)
279        self.root.update()
280
281    def test_drag_selection_down(self):
282        self.linenumber.show_sidebar()
283        self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n')
284        self.root.update()
285
286        # Drag from the second line to the fourth line.
287        self.simulate_drag(2, 4)
288        self.assertEqual(self.get_selection(), ('2.0', '5.0'))
289
290    def test_drag_selection_up(self):
291        self.linenumber.show_sidebar()
292        self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n')
293        self.root.update()
294
295        # Drag from the fourth line to the second line.
296        self.simulate_drag(4, 2)
297        self.assertEqual(self.get_selection(), ('2.0', '5.0'))
298
299    def test_scroll(self):
300        self.linenumber.show_sidebar()
301        self.text.insert('1.0', 'line\n' * 100)
302        self.root.update()
303
304        # Scroll down 10 lines.
305        self.text.yview_scroll(10, 'unit')
306        self.root.update()
307        self.assertEqual(self.text.index('@0,0'), '11.0')
308        self.assertEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0')
309
310        # Generate a mouse-wheel event and make sure it scrolled up or down.
311        # The meaning of the "delta" is OS-dependant, so this just checks for
312        # any change.
313        self.linenumber.sidebar_text.event_generate('<MouseWheel>',
314                                                    x=0, y=0,
315                                                    delta=10)
316        self.root.update()
317        self.assertNotEqual(self.text.index('@0,0'), '11.0')
318        self.assertNotEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0')
319
320    def test_font(self):
321        ln = self.linenumber
322
323        orig_font = ln.sidebar_text['font']
324        test_font = 'TkTextFont'
325        self.assertNotEqual(orig_font, test_font)
326
327        # Ensure line numbers aren't shown.
328        ln.hide_sidebar()
329
330        self.font_override = test_font
331        # Nothing breaks when line numbers aren't shown.
332        ln.update_font()
333
334        # Activate line numbers, previous font change is immediately effective.
335        ln.show_sidebar()
336        self.assertEqual(ln.sidebar_text['font'], test_font)
337
338        # Call the font update with line numbers shown, change is picked up.
339        self.font_override = orig_font
340        ln.update_font()
341        self.assertEqual(ln.sidebar_text['font'], orig_font)
342
343    def test_highlight_colors(self):
344        ln = self.linenumber
345
346        orig_colors = dict(self.highlight_cfg)
347        test_colors = {'background': '#222222', 'foreground': '#ffff00'}
348
349        def assert_colors_are_equal(colors):
350            self.assertEqual(ln.sidebar_text['background'], colors['background'])
351            self.assertEqual(ln.sidebar_text['foreground'], colors['foreground'])
352
353        # Ensure line numbers aren't shown.
354        ln.hide_sidebar()
355
356        self.highlight_cfg = test_colors
357        # Nothing breaks with inactive code context.
358        ln.update_colors()
359
360        # Show line numbers, previous colors change is immediately effective.
361        ln.show_sidebar()
362        assert_colors_are_equal(test_colors)
363
364        # Call colors update with no change to the configured colors.
365        ln.update_colors()
366        assert_colors_are_equal(test_colors)
367
368        # Call the colors update with line numbers shown, change is picked up.
369        self.highlight_cfg = orig_colors
370        ln.update_colors()
371        assert_colors_are_equal(orig_colors)
372
373
374if __name__ == '__main__':
375    unittest.main(verbosity=2)
376