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