1"""Test configdialog, coverage 94%.
2
3Half the class creates dialog, half works with user customizations.
4"""
5from idlelib import configdialog
6from test.support import requires
7requires('gui')
8import unittest
9from unittest import mock
10from idlelib.idle_test.mock_idle import Func
11from tkinter import (Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL)
12from idlelib import config
13from idlelib.configdialog import idleConf, changes, tracers
14
15# Tests should not depend on fortuitous user configurations.
16# They must not affect actual user .cfg files.
17# Use solution from test_config: empty parsers with no filename.
18usercfg = idleConf.userCfg
19testcfg = {
20    'main': config.IdleUserConfParser(''),
21    'highlight': config.IdleUserConfParser(''),
22    'keys': config.IdleUserConfParser(''),
23    'extensions': config.IdleUserConfParser(''),
24}
25
26root = None
27dialog = None
28mainpage = changes['main']
29highpage = changes['highlight']
30keyspage = changes['keys']
31extpage = changes['extensions']
32
33
34def setUpModule():
35    global root, dialog
36    idleConf.userCfg = testcfg
37    root = Tk()
38    # root.withdraw()    # Comment out, see issue 30870
39    dialog = configdialog.ConfigDialog(root, 'Test', _utest=True)
40
41
42def tearDownModule():
43    global root, dialog
44    idleConf.userCfg = usercfg
45    tracers.detach()
46    tracers.clear()
47    changes.clear()
48    root.update_idletasks()
49    root.destroy()
50    root = dialog = None
51
52
53class ConfigDialogTest(unittest.TestCase):
54
55    def test_deactivate_current_config(self):
56        pass
57
58    def activate_config_changes(self):
59        pass
60
61
62class ButtonTest(unittest.TestCase):
63
64    def test_click_ok(self):
65        d = dialog
66        apply = d.apply = mock.Mock()
67        destroy = d.destroy = mock.Mock()
68        d.buttons['Ok'].invoke()
69        apply.assert_called_once()
70        destroy.assert_called_once()
71        del d.destroy, d.apply
72
73    def test_click_apply(self):
74        d = dialog
75        deactivate = d.deactivate_current_config = mock.Mock()
76        save_ext = d.save_all_changed_extensions = mock.Mock()
77        activate = d.activate_config_changes = mock.Mock()
78        d.buttons['Apply'].invoke()
79        deactivate.assert_called_once()
80        save_ext.assert_called_once()
81        activate.assert_called_once()
82        del d.save_all_changed_extensions
83        del d.activate_config_changes, d.deactivate_current_config
84
85    def test_click_cancel(self):
86        d = dialog
87        d.destroy = Func()
88        changes['main']['something'] = 1
89        d.buttons['Cancel'].invoke()
90        self.assertEqual(changes['main'], {})
91        self.assertEqual(d.destroy.called, 1)
92        del d.destroy
93
94    def test_click_help(self):
95        dialog.note.select(dialog.keyspage)
96        with mock.patch.object(configdialog, 'view_text',
97                               new_callable=Func) as view:
98            dialog.buttons['Help'].invoke()
99            title, contents = view.kwds['title'], view.kwds['contents']
100        self.assertEqual(title, 'Help for IDLE preferences')
101        self.assertTrue(contents.startswith('When you click') and
102                        contents.endswith('a different name.\n'))
103
104
105class FontPageTest(unittest.TestCase):
106    """Test that font widgets enable users to make font changes.
107
108    Test that widget actions set vars, that var changes add three
109    options to changes and call set_samples, and that set_samples
110    changes the font of both sample boxes.
111    """
112    @classmethod
113    def setUpClass(cls):
114        page = cls.page = dialog.fontpage
115        dialog.note.select(page)
116        page.set_samples = Func()  # Mask instance method.
117        page.update()
118
119    @classmethod
120    def tearDownClass(cls):
121        del cls.page.set_samples  # Unmask instance method.
122
123    def setUp(self):
124        changes.clear()
125
126    def test_load_font_cfg(self):
127        # Leave widget load test to human visual check.
128        # TODO Improve checks when add IdleConf.get_font_values.
129        tracers.detach()
130        d = self.page
131        d.font_name.set('Fake')
132        d.font_size.set('1')
133        d.font_bold.set(True)
134        d.set_samples.called = 0
135        d.load_font_cfg()
136        self.assertNotEqual(d.font_name.get(), 'Fake')
137        self.assertNotEqual(d.font_size.get(), '1')
138        self.assertFalse(d.font_bold.get())
139        self.assertEqual(d.set_samples.called, 1)
140        tracers.attach()
141
142    def test_fontlist_key(self):
143        # Up and Down keys should select a new font.
144        d = self.page
145        if d.fontlist.size() < 2:
146            self.skipTest('need at least 2 fonts')
147        fontlist = d.fontlist
148        fontlist.activate(0)
149        font = d.fontlist.get('active')
150
151        # Test Down key.
152        fontlist.focus_force()
153        fontlist.update()
154        fontlist.event_generate('<Key-Down>')
155        fontlist.event_generate('<KeyRelease-Down>')
156
157        down_font = fontlist.get('active')
158        self.assertNotEqual(down_font, font)
159        self.assertIn(d.font_name.get(), down_font.lower())
160
161        # Test Up key.
162        fontlist.focus_force()
163        fontlist.update()
164        fontlist.event_generate('<Key-Up>')
165        fontlist.event_generate('<KeyRelease-Up>')
166
167        up_font = fontlist.get('active')
168        self.assertEqual(up_font, font)
169        self.assertIn(d.font_name.get(), up_font.lower())
170
171    def test_fontlist_mouse(self):
172        # Click on item should select that item.
173        d = self.page
174        if d.fontlist.size() < 2:
175            self.skipTest('need at least 2 fonts')
176        fontlist = d.fontlist
177        fontlist.activate(0)
178
179        # Select next item in listbox
180        fontlist.focus_force()
181        fontlist.see(1)
182        fontlist.update()
183        x, y, dx, dy = fontlist.bbox(1)
184        x += dx // 2
185        y += dy // 2
186        fontlist.event_generate('<Button-1>', x=x, y=y)
187        fontlist.event_generate('<ButtonRelease-1>', x=x, y=y)
188
189        font1 = fontlist.get(1)
190        select_font = fontlist.get('anchor')
191        self.assertEqual(select_font, font1)
192        self.assertIn(d.font_name.get(), font1.lower())
193
194    def test_sizelist(self):
195        # Click on number should select that number
196        d = self.page
197        d.sizelist.variable.set(40)
198        self.assertEqual(d.font_size.get(), '40')
199
200    def test_bold_toggle(self):
201        # Click on checkbutton should invert it.
202        d = self.page
203        d.font_bold.set(False)
204        d.bold_toggle.invoke()
205        self.assertTrue(d.font_bold.get())
206        d.bold_toggle.invoke()
207        self.assertFalse(d.font_bold.get())
208
209    def test_font_set(self):
210        # Test that setting a font Variable results in 3 provisional
211        # change entries and a call to set_samples. Use values sure to
212        # not be defaults.
213
214        default_font = idleConf.GetFont(root, 'main', 'EditorWindow')
215        default_size = str(default_font[1])
216        default_bold = default_font[2] == 'bold'
217        d = self.page
218        d.font_size.set(default_size)
219        d.font_bold.set(default_bold)
220        d.set_samples.called = 0
221
222        d.font_name.set('Test Font')
223        expected = {'EditorWindow': {'font': 'Test Font',
224                                     'font-size': default_size,
225                                     'font-bold': str(default_bold)}}
226        self.assertEqual(mainpage, expected)
227        self.assertEqual(d.set_samples.called, 1)
228        changes.clear()
229
230        d.font_size.set('20')
231        expected = {'EditorWindow': {'font': 'Test Font',
232                                     'font-size': '20',
233                                     'font-bold': str(default_bold)}}
234        self.assertEqual(mainpage, expected)
235        self.assertEqual(d.set_samples.called, 2)
236        changes.clear()
237
238        d.font_bold.set(not default_bold)
239        expected = {'EditorWindow': {'font': 'Test Font',
240                                     'font-size': '20',
241                                     'font-bold': str(not default_bold)}}
242        self.assertEqual(mainpage, expected)
243        self.assertEqual(d.set_samples.called, 3)
244
245    def test_set_samples(self):
246        d = self.page
247        del d.set_samples  # Unmask method for test
248        orig_samples = d.font_sample, d.highlight_sample
249        d.font_sample, d.highlight_sample = {}, {}
250        d.font_name.set('test')
251        d.font_size.set('5')
252        d.font_bold.set(1)
253        expected = {'font': ('test', '5', 'bold')}
254
255        # Test set_samples.
256        d.set_samples()
257        self.assertTrue(d.font_sample == d.highlight_sample == expected)
258
259        d.font_sample, d.highlight_sample = orig_samples
260        d.set_samples = Func()  # Re-mask for other tests.
261
262
263class IndentTest(unittest.TestCase):
264
265    @classmethod
266    def setUpClass(cls):
267        cls.page = dialog.fontpage
268        cls.page.update()
269
270    def test_load_tab_cfg(self):
271        d = self.page
272        d.space_num.set(16)
273        d.load_tab_cfg()
274        self.assertEqual(d.space_num.get(), 4)
275
276    def test_indent_scale(self):
277        d = self.page
278        changes.clear()
279        d.indent_scale.set(20)
280        self.assertEqual(d.space_num.get(), 16)
281        self.assertEqual(mainpage, {'Indent': {'num-spaces': '16'}})
282
283
284class HighPageTest(unittest.TestCase):
285    """Test that highlight tab widgets enable users to make changes.
286
287    Test that widget actions set vars, that var changes add
288    options to changes and that themes work correctly.
289    """
290
291    @classmethod
292    def setUpClass(cls):
293        page = cls.page = dialog.highpage
294        dialog.note.select(page)
295        page.set_theme_type = Func()
296        page.paint_theme_sample = Func()
297        page.set_highlight_target = Func()
298        page.set_color_sample = Func()
299        page.update()
300
301    @classmethod
302    def tearDownClass(cls):
303        d = cls.page
304        del d.set_theme_type, d.paint_theme_sample
305        del d.set_highlight_target, d.set_color_sample
306
307    def setUp(self):
308        d = self.page
309        # The following is needed for test_load_key_cfg, _delete_custom_keys.
310        # This may indicate a defect in some test or function.
311        for section in idleConf.GetSectionList('user', 'highlight'):
312            idleConf.userCfg['highlight'].remove_section(section)
313        changes.clear()
314        d.set_theme_type.called = 0
315        d.paint_theme_sample.called = 0
316        d.set_highlight_target.called = 0
317        d.set_color_sample.called = 0
318
319    def test_load_theme_cfg(self):
320        tracers.detach()
321        d = self.page
322        eq = self.assertEqual
323
324        # Use builtin theme with no user themes created.
325        idleConf.CurrentTheme = mock.Mock(return_value='IDLE Classic')
326        d.load_theme_cfg()
327        self.assertTrue(d.theme_source.get())
328        # builtinlist sets variable builtin_name to the CurrentTheme default.
329        eq(d.builtin_name.get(), 'IDLE Classic')
330        eq(d.custom_name.get(), '- no custom themes -')
331        eq(d.custom_theme_on.state(), ('disabled',))
332        eq(d.set_theme_type.called, 1)
333        eq(d.paint_theme_sample.called, 1)
334        eq(d.set_highlight_target.called, 1)
335
336        # Builtin theme with non-empty user theme list.
337        idleConf.SetOption('highlight', 'test1', 'option', 'value')
338        idleConf.SetOption('highlight', 'test2', 'option2', 'value2')
339        d.load_theme_cfg()
340        eq(d.builtin_name.get(), 'IDLE Classic')
341        eq(d.custom_name.get(), 'test1')
342        eq(d.set_theme_type.called, 2)
343        eq(d.paint_theme_sample.called, 2)
344        eq(d.set_highlight_target.called, 2)
345
346        # Use custom theme.
347        idleConf.CurrentTheme = mock.Mock(return_value='test2')
348        idleConf.SetOption('main', 'Theme', 'default', '0')
349        d.load_theme_cfg()
350        self.assertFalse(d.theme_source.get())
351        eq(d.builtin_name.get(), 'IDLE Classic')
352        eq(d.custom_name.get(), 'test2')
353        eq(d.set_theme_type.called, 3)
354        eq(d.paint_theme_sample.called, 3)
355        eq(d.set_highlight_target.called, 3)
356
357        del idleConf.CurrentTheme
358        tracers.attach()
359
360    def test_theme_source(self):
361        eq = self.assertEqual
362        d = self.page
363        # Test these separately.
364        d.var_changed_builtin_name = Func()
365        d.var_changed_custom_name = Func()
366        # Builtin selected.
367        d.builtin_theme_on.invoke()
368        eq(mainpage, {'Theme': {'default': 'True'}})
369        eq(d.var_changed_builtin_name.called, 1)
370        eq(d.var_changed_custom_name.called, 0)
371        changes.clear()
372
373        # Custom selected.
374        d.custom_theme_on.state(('!disabled',))
375        d.custom_theme_on.invoke()
376        self.assertEqual(mainpage, {'Theme': {'default': 'False'}})
377        eq(d.var_changed_builtin_name.called, 1)
378        eq(d.var_changed_custom_name.called, 1)
379        del d.var_changed_builtin_name, d.var_changed_custom_name
380
381    def test_builtin_name(self):
382        eq = self.assertEqual
383        d = self.page
384        item_list = ['IDLE Classic', 'IDLE Dark', 'IDLE New']
385
386        # Not in old_themes, defaults name to first item.
387        idleConf.SetOption('main', 'Theme', 'name', 'spam')
388        d.builtinlist.SetMenu(item_list, 'IDLE Dark')
389        eq(mainpage, {'Theme': {'name': 'IDLE Classic',
390                                'name2': 'IDLE Dark'}})
391        eq(d.theme_message['text'], 'New theme, see Help')
392        eq(d.paint_theme_sample.called, 1)
393
394        # Not in old themes - uses name2.
395        changes.clear()
396        idleConf.SetOption('main', 'Theme', 'name', 'IDLE New')
397        d.builtinlist.SetMenu(item_list, 'IDLE Dark')
398        eq(mainpage, {'Theme': {'name2': 'IDLE Dark'}})
399        eq(d.theme_message['text'], 'New theme, see Help')
400        eq(d.paint_theme_sample.called, 2)
401
402        # Builtin name in old_themes.
403        changes.clear()
404        d.builtinlist.SetMenu(item_list, 'IDLE Classic')
405        eq(mainpage, {'Theme': {'name': 'IDLE Classic', 'name2': ''}})
406        eq(d.theme_message['text'], '')
407        eq(d.paint_theme_sample.called, 3)
408
409    def test_custom_name(self):
410        d = self.page
411
412        # If no selections, doesn't get added.
413        d.customlist.SetMenu([], '- no custom themes -')
414        self.assertNotIn('Theme', mainpage)
415        self.assertEqual(d.paint_theme_sample.called, 0)
416
417        # Custom name selected.
418        changes.clear()
419        d.customlist.SetMenu(['a', 'b', 'c'], 'c')
420        self.assertEqual(mainpage, {'Theme': {'name': 'c'}})
421        self.assertEqual(d.paint_theme_sample.called, 1)
422
423    def test_color(self):
424        d = self.page
425        d.on_new_color_set = Func()
426        # self.color is only set in get_color through colorchooser.
427        d.color.set('green')
428        self.assertEqual(d.on_new_color_set.called, 1)
429        del d.on_new_color_set
430
431    def test_highlight_target_list_mouse(self):
432        # Set highlight_target through targetlist.
433        eq = self.assertEqual
434        d = self.page
435
436        d.targetlist.SetMenu(['a', 'b', 'c'], 'c')
437        eq(d.highlight_target.get(), 'c')
438        eq(d.set_highlight_target.called, 1)
439
440    def test_highlight_target_text_mouse(self):
441        # Set highlight_target through clicking highlight_sample.
442        eq = self.assertEqual
443        d = self.page
444
445        elem = {}
446        count = 0
447        hs = d.highlight_sample
448        hs.focus_force()
449        hs.see(1.0)
450        hs.update_idletasks()
451
452        def tag_to_element(elem):
453            for element, tag in d.theme_elements.items():
454                elem[tag[0]] = element
455
456        def click_it(start):
457            x, y, dx, dy = hs.bbox(start)
458            x += dx // 2
459            y += dy // 2
460            hs.event_generate('<Enter>', x=0, y=0)
461            hs.event_generate('<Motion>', x=x, y=y)
462            hs.event_generate('<ButtonPress-1>', x=x, y=y)
463            hs.event_generate('<ButtonRelease-1>', x=x, y=y)
464
465        # Flip theme_elements to make the tag the key.
466        tag_to_element(elem)
467
468        # If highlight_sample has a tag that isn't in theme_elements, there
469        # will be a KeyError in the test run.
470        for tag in hs.tag_names():
471            for start_index in hs.tag_ranges(tag)[0::2]:
472                count += 1
473                click_it(start_index)
474                eq(d.highlight_target.get(), elem[tag])
475                eq(d.set_highlight_target.called, count)
476
477    def test_highlight_sample_double_click(self):
478        # Test double click on highlight_sample.
479        eq = self.assertEqual
480        d = self.page
481
482        hs = d.highlight_sample
483        hs.focus_force()
484        hs.see(1.0)
485        hs.update_idletasks()
486
487        # Test binding from configdialog.
488        hs.event_generate('<Enter>', x=0, y=0)
489        hs.event_generate('<Motion>', x=0, y=0)
490        # Double click is a sequence of two clicks in a row.
491        for _ in range(2):
492            hs.event_generate('<ButtonPress-1>', x=0, y=0)
493            hs.event_generate('<ButtonRelease-1>', x=0, y=0)
494
495        eq(hs.tag_ranges('sel'), ())
496
497    def test_highlight_sample_b1_motion(self):
498        # Test button motion on highlight_sample.
499        eq = self.assertEqual
500        d = self.page
501
502        hs = d.highlight_sample
503        hs.focus_force()
504        hs.see(1.0)
505        hs.update_idletasks()
506
507        x, y, dx, dy, offset = hs.dlineinfo('1.0')
508
509        # Test binding from configdialog.
510        hs.event_generate('<Leave>')
511        hs.event_generate('<Enter>')
512        hs.event_generate('<Motion>', x=x, y=y)
513        hs.event_generate('<ButtonPress-1>', x=x, y=y)
514        hs.event_generate('<B1-Motion>', x=dx, y=dy)
515        hs.event_generate('<ButtonRelease-1>', x=dx, y=dy)
516
517        eq(hs.tag_ranges('sel'), ())
518
519    def test_set_theme_type(self):
520        eq = self.assertEqual
521        d = self.page
522        del d.set_theme_type
523
524        # Builtin theme selected.
525        d.theme_source.set(True)
526        d.set_theme_type()
527        eq(d.builtinlist['state'], NORMAL)
528        eq(d.customlist['state'], DISABLED)
529        eq(d.button_delete_custom.state(), ('disabled',))
530
531        # Custom theme selected.
532        d.theme_source.set(False)
533        d.set_theme_type()
534        eq(d.builtinlist['state'], DISABLED)
535        eq(d.custom_theme_on.state(), ('selected',))
536        eq(d.customlist['state'], NORMAL)
537        eq(d.button_delete_custom.state(), ())
538        d.set_theme_type = Func()
539
540    def test_get_color(self):
541        eq = self.assertEqual
542        d = self.page
543        orig_chooser = configdialog.colorchooser.askcolor
544        chooser = configdialog.colorchooser.askcolor = Func()
545        gntn = d.get_new_theme_name = Func()
546
547        d.highlight_target.set('Editor Breakpoint')
548        d.color.set('#ffffff')
549
550        # Nothing selected.
551        chooser.result = (None, None)
552        d.button_set_color.invoke()
553        eq(d.color.get(), '#ffffff')
554
555        # Selection same as previous color.
556        chooser.result = ('', d.style.lookup(d.frame_color_set['style'], 'background'))
557        d.button_set_color.invoke()
558        eq(d.color.get(), '#ffffff')
559
560        # Select different color.
561        chooser.result = ((222.8671875, 0.0, 0.0), '#de0000')
562
563        # Default theme.
564        d.color.set('#ffffff')
565        d.theme_source.set(True)
566
567        # No theme name selected therefore color not saved.
568        gntn.result = ''
569        d.button_set_color.invoke()
570        eq(gntn.called, 1)
571        eq(d.color.get(), '#ffffff')
572        # Theme name selected.
573        gntn.result = 'My New Theme'
574        d.button_set_color.invoke()
575        eq(d.custom_name.get(), gntn.result)
576        eq(d.color.get(), '#de0000')
577
578        # Custom theme.
579        d.color.set('#ffffff')
580        d.theme_source.set(False)
581        d.button_set_color.invoke()
582        eq(d.color.get(), '#de0000')
583
584        del d.get_new_theme_name
585        configdialog.colorchooser.askcolor = orig_chooser
586
587    def test_on_new_color_set(self):
588        d = self.page
589        color = '#3f7cae'
590        d.custom_name.set('Python')
591        d.highlight_target.set('Selected Text')
592        d.fg_bg_toggle.set(True)
593
594        d.color.set(color)
595        self.assertEqual(d.style.lookup(d.frame_color_set['style'], 'background'), color)
596        self.assertEqual(d.highlight_sample.tag_cget('hilite', 'foreground'), color)
597        self.assertEqual(highpage,
598                         {'Python': {'hilite-foreground': color}})
599
600    def test_get_new_theme_name(self):
601        orig_sectionname = configdialog.SectionName
602        sn = configdialog.SectionName = Func(return_self=True)
603        d = self.page
604
605        sn.result = 'New Theme'
606        self.assertEqual(d.get_new_theme_name(''), 'New Theme')
607
608        configdialog.SectionName = orig_sectionname
609
610    def test_save_as_new_theme(self):
611        d = self.page
612        gntn = d.get_new_theme_name = Func()
613        d.theme_source.set(True)
614
615        # No name entered.
616        gntn.result = ''
617        d.button_save_custom.invoke()
618        self.assertNotIn(gntn.result, idleConf.userCfg['highlight'])
619
620        # Name entered.
621        gntn.result = 'my new theme'
622        gntn.called = 0
623        self.assertNotIn(gntn.result, idleConf.userCfg['highlight'])
624        d.button_save_custom.invoke()
625        self.assertIn(gntn.result, idleConf.userCfg['highlight'])
626
627        del d.get_new_theme_name
628
629    def test_create_new_and_save_new(self):
630        eq = self.assertEqual
631        d = self.page
632
633        # Use default as previously active theme.
634        d.theme_source.set(True)
635        d.builtin_name.set('IDLE Classic')
636        first_new = 'my new custom theme'
637        second_new = 'my second custom theme'
638
639        # No changes, so themes are an exact copy.
640        self.assertNotIn(first_new, idleConf.userCfg)
641        d.create_new(first_new)
642        eq(idleConf.GetSectionList('user', 'highlight'), [first_new])
643        eq(idleConf.GetThemeDict('default', 'IDLE Classic'),
644           idleConf.GetThemeDict('user', first_new))
645        eq(d.custom_name.get(), first_new)
646        self.assertFalse(d.theme_source.get())  # Use custom set.
647        eq(d.set_theme_type.called, 1)
648
649        # Test that changed targets are in new theme.
650        changes.add_option('highlight', first_new, 'hit-background', 'yellow')
651        self.assertNotIn(second_new, idleConf.userCfg)
652        d.create_new(second_new)
653        eq(idleConf.GetSectionList('user', 'highlight'), [first_new, second_new])
654        self.assertNotEqual(idleConf.GetThemeDict('user', first_new),
655                            idleConf.GetThemeDict('user', second_new))
656        # Check that difference in themes was in `hit-background` from `changes`.
657        idleConf.SetOption('highlight', first_new, 'hit-background', 'yellow')
658        eq(idleConf.GetThemeDict('user', first_new),
659           idleConf.GetThemeDict('user', second_new))
660
661    def test_set_highlight_target(self):
662        eq = self.assertEqual
663        d = self.page
664        del d.set_highlight_target
665
666        # Target is cursor.
667        d.highlight_target.set('Cursor')
668        eq(d.fg_on.state(), ('disabled', 'selected'))
669        eq(d.bg_on.state(), ('disabled',))
670        self.assertTrue(d.fg_bg_toggle)
671        eq(d.set_color_sample.called, 1)
672
673        # Target is not cursor.
674        d.highlight_target.set('Comment')
675        eq(d.fg_on.state(), ('selected',))
676        eq(d.bg_on.state(), ())
677        self.assertTrue(d.fg_bg_toggle)
678        eq(d.set_color_sample.called, 2)
679
680        d.set_highlight_target = Func()
681
682    def test_set_color_sample_binding(self):
683        d = self.page
684        scs = d.set_color_sample
685
686        d.fg_on.invoke()
687        self.assertEqual(scs.called, 1)
688
689        d.bg_on.invoke()
690        self.assertEqual(scs.called, 2)
691
692    def test_set_color_sample(self):
693        d = self.page
694        del d.set_color_sample
695        d.highlight_target.set('Selected Text')
696        d.fg_bg_toggle.set(True)
697        d.set_color_sample()
698        self.assertEqual(
699                d.style.lookup(d.frame_color_set['style'], 'background'),
700                d.highlight_sample.tag_cget('hilite', 'foreground'))
701        d.set_color_sample = Func()
702
703    def test_paint_theme_sample(self):
704        eq = self.assertEqual
705        page = self.page
706        del page.paint_theme_sample  # Delete masking mock.
707        hs_tag = page.highlight_sample.tag_cget
708        gh = idleConf.GetHighlight
709
710        # Create custom theme based on IDLE Dark.
711        page.theme_source.set(True)
712        page.builtin_name.set('IDLE Dark')
713        theme = 'IDLE Test'
714        page.create_new(theme)
715        page.set_color_sample.called = 0
716
717        # Base theme with nothing in `changes`.
718        page.paint_theme_sample()
719        new_console = {'foreground': 'blue',
720                       'background': 'yellow',}
721        for key, value in new_console.items():
722            self.assertNotEqual(hs_tag('console', key), value)
723        eq(page.set_color_sample.called, 1)
724
725        # Apply changes.
726        for key, value in new_console.items():
727            changes.add_option('highlight', theme, 'console-'+key, value)
728        page.paint_theme_sample()
729        for key, value in new_console.items():
730            eq(hs_tag('console', key), value)
731        eq(page.set_color_sample.called, 2)
732
733        page.paint_theme_sample = Func()
734
735    def test_delete_custom(self):
736        eq = self.assertEqual
737        d = self.page
738        d.button_delete_custom.state(('!disabled',))
739        yesno = d.askyesno = Func()
740        dialog.deactivate_current_config = Func()
741        dialog.activate_config_changes = Func()
742
743        theme_name = 'spam theme'
744        idleConf.userCfg['highlight'].SetOption(theme_name, 'name', 'value')
745        highpage[theme_name] = {'option': 'True'}
746
747        theme_name2 = 'other theme'
748        idleConf.userCfg['highlight'].SetOption(theme_name2, 'name', 'value')
749        highpage[theme_name2] = {'option': 'False'}
750
751        # Force custom theme.
752        d.custom_theme_on.state(('!disabled',))
753        d.custom_theme_on.invoke()
754        d.custom_name.set(theme_name)
755
756        # Cancel deletion.
757        yesno.result = False
758        d.button_delete_custom.invoke()
759        eq(yesno.called, 1)
760        eq(highpage[theme_name], {'option': 'True'})
761        eq(idleConf.GetSectionList('user', 'highlight'), [theme_name, theme_name2])
762        eq(dialog.deactivate_current_config.called, 0)
763        eq(dialog.activate_config_changes.called, 0)
764        eq(d.set_theme_type.called, 0)
765
766        # Confirm deletion.
767        yesno.result = True
768        d.button_delete_custom.invoke()
769        eq(yesno.called, 2)
770        self.assertNotIn(theme_name, highpage)
771        eq(idleConf.GetSectionList('user', 'highlight'), [theme_name2])
772        eq(d.custom_theme_on.state(), ())
773        eq(d.custom_name.get(), theme_name2)
774        eq(dialog.deactivate_current_config.called, 1)
775        eq(dialog.activate_config_changes.called, 1)
776        eq(d.set_theme_type.called, 1)
777
778        # Confirm deletion of second theme - empties list.
779        d.custom_name.set(theme_name2)
780        yesno.result = True
781        d.button_delete_custom.invoke()
782        eq(yesno.called, 3)
783        self.assertNotIn(theme_name, highpage)
784        eq(idleConf.GetSectionList('user', 'highlight'), [])
785        eq(d.custom_theme_on.state(), ('disabled',))
786        eq(d.custom_name.get(), '- no custom themes -')
787        eq(dialog.deactivate_current_config.called, 2)
788        eq(dialog.activate_config_changes.called, 2)
789        eq(d.set_theme_type.called, 2)
790
791        del dialog.activate_config_changes, dialog.deactivate_current_config
792        del d.askyesno
793
794
795class KeysPageTest(unittest.TestCase):
796    """Test that keys tab widgets enable users to make changes.
797
798    Test that widget actions set vars, that var changes add
799    options to changes and that key sets works correctly.
800    """
801
802    @classmethod
803    def setUpClass(cls):
804        page = cls.page = dialog.keyspage
805        dialog.note.select(page)
806        page.set_keys_type = Func()
807        page.load_keys_list = Func()
808
809    @classmethod
810    def tearDownClass(cls):
811        page = cls.page
812        del page.set_keys_type, page.load_keys_list
813
814    def setUp(self):
815        d = self.page
816        # The following is needed for test_load_key_cfg, _delete_custom_keys.
817        # This may indicate a defect in some test or function.
818        for section in idleConf.GetSectionList('user', 'keys'):
819            idleConf.userCfg['keys'].remove_section(section)
820        changes.clear()
821        d.set_keys_type.called = 0
822        d.load_keys_list.called = 0
823
824    def test_load_key_cfg(self):
825        tracers.detach()
826        d = self.page
827        eq = self.assertEqual
828
829        # Use builtin keyset with no user keysets created.
830        idleConf.CurrentKeys = mock.Mock(return_value='IDLE Classic OSX')
831        d.load_key_cfg()
832        self.assertTrue(d.keyset_source.get())
833        # builtinlist sets variable builtin_name to the CurrentKeys default.
834        eq(d.builtin_name.get(), 'IDLE Classic OSX')
835        eq(d.custom_name.get(), '- no custom keys -')
836        eq(d.custom_keyset_on.state(), ('disabled',))
837        eq(d.set_keys_type.called, 1)
838        eq(d.load_keys_list.called, 1)
839        eq(d.load_keys_list.args, ('IDLE Classic OSX', ))
840
841        # Builtin keyset with non-empty user keyset list.
842        idleConf.SetOption('keys', 'test1', 'option', 'value')
843        idleConf.SetOption('keys', 'test2', 'option2', 'value2')
844        d.load_key_cfg()
845        eq(d.builtin_name.get(), 'IDLE Classic OSX')
846        eq(d.custom_name.get(), 'test1')
847        eq(d.set_keys_type.called, 2)
848        eq(d.load_keys_list.called, 2)
849        eq(d.load_keys_list.args, ('IDLE Classic OSX', ))
850
851        # Use custom keyset.
852        idleConf.CurrentKeys = mock.Mock(return_value='test2')
853        idleConf.default_keys = mock.Mock(return_value='IDLE Modern Unix')
854        idleConf.SetOption('main', 'Keys', 'default', '0')
855        d.load_key_cfg()
856        self.assertFalse(d.keyset_source.get())
857        eq(d.builtin_name.get(), 'IDLE Modern Unix')
858        eq(d.custom_name.get(), 'test2')
859        eq(d.set_keys_type.called, 3)
860        eq(d.load_keys_list.called, 3)
861        eq(d.load_keys_list.args, ('test2', ))
862
863        del idleConf.CurrentKeys, idleConf.default_keys
864        tracers.attach()
865
866    def test_keyset_source(self):
867        eq = self.assertEqual
868        d = self.page
869        # Test these separately.
870        d.var_changed_builtin_name = Func()
871        d.var_changed_custom_name = Func()
872        # Builtin selected.
873        d.builtin_keyset_on.invoke()
874        eq(mainpage, {'Keys': {'default': 'True'}})
875        eq(d.var_changed_builtin_name.called, 1)
876        eq(d.var_changed_custom_name.called, 0)
877        changes.clear()
878
879        # Custom selected.
880        d.custom_keyset_on.state(('!disabled',))
881        d.custom_keyset_on.invoke()
882        self.assertEqual(mainpage, {'Keys': {'default': 'False'}})
883        eq(d.var_changed_builtin_name.called, 1)
884        eq(d.var_changed_custom_name.called, 1)
885        del d.var_changed_builtin_name, d.var_changed_custom_name
886
887    def test_builtin_name(self):
888        eq = self.assertEqual
889        d = self.page
890        idleConf.userCfg['main'].remove_section('Keys')
891        item_list = ['IDLE Classic Windows', 'IDLE Classic OSX',
892                     'IDLE Modern UNIX']
893
894        # Not in old_keys, defaults name to first item.
895        d.builtinlist.SetMenu(item_list, 'IDLE Modern UNIX')
896        eq(mainpage, {'Keys': {'name': 'IDLE Classic Windows',
897                               'name2': 'IDLE Modern UNIX'}})
898        eq(d.keys_message['text'], 'New key set, see Help')
899        eq(d.load_keys_list.called, 1)
900        eq(d.load_keys_list.args, ('IDLE Modern UNIX', ))
901
902        # Not in old keys - uses name2.
903        changes.clear()
904        idleConf.SetOption('main', 'Keys', 'name', 'IDLE Classic Unix')
905        d.builtinlist.SetMenu(item_list, 'IDLE Modern UNIX')
906        eq(mainpage, {'Keys': {'name2': 'IDLE Modern UNIX'}})
907        eq(d.keys_message['text'], 'New key set, see Help')
908        eq(d.load_keys_list.called, 2)
909        eq(d.load_keys_list.args, ('IDLE Modern UNIX', ))
910
911        # Builtin name in old_keys.
912        changes.clear()
913        d.builtinlist.SetMenu(item_list, 'IDLE Classic OSX')
914        eq(mainpage, {'Keys': {'name': 'IDLE Classic OSX', 'name2': ''}})
915        eq(d.keys_message['text'], '')
916        eq(d.load_keys_list.called, 3)
917        eq(d.load_keys_list.args, ('IDLE Classic OSX', ))
918
919    def test_custom_name(self):
920        d = self.page
921
922        # If no selections, doesn't get added.
923        d.customlist.SetMenu([], '- no custom keys -')
924        self.assertNotIn('Keys', mainpage)
925        self.assertEqual(d.load_keys_list.called, 0)
926
927        # Custom name selected.
928        changes.clear()
929        d.customlist.SetMenu(['a', 'b', 'c'], 'c')
930        self.assertEqual(mainpage, {'Keys': {'name': 'c'}})
931        self.assertEqual(d.load_keys_list.called, 1)
932
933    def test_keybinding(self):
934        idleConf.SetOption('extensions', 'ZzDummy', 'enable', 'True')
935        d = self.page
936        d.custom_name.set('my custom keys')
937        d.bindingslist.delete(0, 'end')
938        d.bindingslist.insert(0, 'copy')
939        d.bindingslist.insert(1, 'z-in')
940        d.bindingslist.selection_set(0)
941        d.bindingslist.selection_anchor(0)
942        # Core binding - adds to keys.
943        d.keybinding.set('<Key-F11>')
944        self.assertEqual(keyspage,
945                         {'my custom keys': {'copy': '<Key-F11>'}})
946
947        # Not a core binding - adds to extensions.
948        d.bindingslist.selection_set(1)
949        d.bindingslist.selection_anchor(1)
950        d.keybinding.set('<Key-F11>')
951        self.assertEqual(extpage,
952                         {'ZzDummy_cfgBindings': {'z-in': '<Key-F11>'}})
953
954    def test_set_keys_type(self):
955        eq = self.assertEqual
956        d = self.page
957        del d.set_keys_type
958
959        # Builtin keyset selected.
960        d.keyset_source.set(True)
961        d.set_keys_type()
962        eq(d.builtinlist['state'], NORMAL)
963        eq(d.customlist['state'], DISABLED)
964        eq(d.button_delete_custom_keys.state(), ('disabled',))
965
966        # Custom keyset selected.
967        d.keyset_source.set(False)
968        d.set_keys_type()
969        eq(d.builtinlist['state'], DISABLED)
970        eq(d.custom_keyset_on.state(), ('selected',))
971        eq(d.customlist['state'], NORMAL)
972        eq(d.button_delete_custom_keys.state(), ())
973        d.set_keys_type = Func()
974
975    def test_get_new_keys(self):
976        eq = self.assertEqual
977        d = self.page
978        orig_getkeysdialog = configdialog.GetKeysDialog
979        gkd = configdialog.GetKeysDialog = Func(return_self=True)
980        gnkn = d.get_new_keys_name = Func()
981
982        d.button_new_keys.state(('!disabled',))
983        d.bindingslist.delete(0, 'end')
984        d.bindingslist.insert(0, 'copy - <Control-Shift-Key-C>')
985        d.bindingslist.selection_set(0)
986        d.bindingslist.selection_anchor(0)
987        d.keybinding.set('Key-a')
988        d.keyset_source.set(True)  # Default keyset.
989
990        # Default keyset; no change to binding.
991        gkd.result = ''
992        d.button_new_keys.invoke()
993        eq(d.bindingslist.get('anchor'), 'copy - <Control-Shift-Key-C>')
994        # Keybinding isn't changed when there isn't a change entered.
995        eq(d.keybinding.get(), 'Key-a')
996
997        # Default keyset; binding changed.
998        gkd.result = '<Key-F11>'
999        # No keyset name selected therefore binding not saved.
1000        gnkn.result = ''
1001        d.button_new_keys.invoke()
1002        eq(gnkn.called, 1)
1003        eq(d.bindingslist.get('anchor'), 'copy - <Control-Shift-Key-C>')
1004        # Keyset name selected.
1005        gnkn.result = 'My New Key Set'
1006        d.button_new_keys.invoke()
1007        eq(d.custom_name.get(), gnkn.result)
1008        eq(d.bindingslist.get('anchor'), 'copy - <Key-F11>')
1009        eq(d.keybinding.get(), '<Key-F11>')
1010
1011        # User keyset; binding changed.
1012        d.keyset_source.set(False)  # Custom keyset.
1013        gnkn.called = 0
1014        gkd.result = '<Key-p>'
1015        d.button_new_keys.invoke()
1016        eq(gnkn.called, 0)
1017        eq(d.bindingslist.get('anchor'), 'copy - <Key-p>')
1018        eq(d.keybinding.get(), '<Key-p>')
1019
1020        del d.get_new_keys_name
1021        configdialog.GetKeysDialog = orig_getkeysdialog
1022
1023    def test_get_new_keys_name(self):
1024        orig_sectionname = configdialog.SectionName
1025        sn = configdialog.SectionName = Func(return_self=True)
1026        d = self.page
1027
1028        sn.result = 'New Keys'
1029        self.assertEqual(d.get_new_keys_name(''), 'New Keys')
1030
1031        configdialog.SectionName = orig_sectionname
1032
1033    def test_save_as_new_key_set(self):
1034        d = self.page
1035        gnkn = d.get_new_keys_name = Func()
1036        d.keyset_source.set(True)
1037
1038        # No name entered.
1039        gnkn.result = ''
1040        d.button_save_custom_keys.invoke()
1041
1042        # Name entered.
1043        gnkn.result = 'my new key set'
1044        gnkn.called = 0
1045        self.assertNotIn(gnkn.result, idleConf.userCfg['keys'])
1046        d.button_save_custom_keys.invoke()
1047        self.assertIn(gnkn.result, idleConf.userCfg['keys'])
1048
1049        del d.get_new_keys_name
1050
1051    def test_on_bindingslist_select(self):
1052        d = self.page
1053        b = d.bindingslist
1054        b.delete(0, 'end')
1055        b.insert(0, 'copy')
1056        b.insert(1, 'find')
1057        b.activate(0)
1058
1059        b.focus_force()
1060        b.see(1)
1061        b.update()
1062        x, y, dx, dy = b.bbox(1)
1063        x += dx // 2
1064        y += dy // 2
1065        b.event_generate('<Enter>', x=0, y=0)
1066        b.event_generate('<Motion>', x=x, y=y)
1067        b.event_generate('<Button-1>', x=x, y=y)
1068        b.event_generate('<ButtonRelease-1>', x=x, y=y)
1069        self.assertEqual(b.get('anchor'), 'find')
1070        self.assertEqual(d.button_new_keys.state(), ())
1071
1072    def test_create_new_key_set_and_save_new_key_set(self):
1073        eq = self.assertEqual
1074        d = self.page
1075
1076        # Use default as previously active keyset.
1077        d.keyset_source.set(True)
1078        d.builtin_name.set('IDLE Classic Windows')
1079        first_new = 'my new custom key set'
1080        second_new = 'my second custom keyset'
1081
1082        # No changes, so keysets are an exact copy.
1083        self.assertNotIn(first_new, idleConf.userCfg)
1084        d.create_new_key_set(first_new)
1085        eq(idleConf.GetSectionList('user', 'keys'), [first_new])
1086        eq(idleConf.GetKeySet('IDLE Classic Windows'),
1087           idleConf.GetKeySet(first_new))
1088        eq(d.custom_name.get(), first_new)
1089        self.assertFalse(d.keyset_source.get())  # Use custom set.
1090        eq(d.set_keys_type.called, 1)
1091
1092        # Test that changed keybindings are in new keyset.
1093        changes.add_option('keys', first_new, 'copy', '<Key-F11>')
1094        self.assertNotIn(second_new, idleConf.userCfg)
1095        d.create_new_key_set(second_new)
1096        eq(idleConf.GetSectionList('user', 'keys'), [first_new, second_new])
1097        self.assertNotEqual(idleConf.GetKeySet(first_new),
1098                            idleConf.GetKeySet(second_new))
1099        # Check that difference in keysets was in option `copy` from `changes`.
1100        idleConf.SetOption('keys', first_new, 'copy', '<Key-F11>')
1101        eq(idleConf.GetKeySet(first_new), idleConf.GetKeySet(second_new))
1102
1103    def test_load_keys_list(self):
1104        eq = self.assertEqual
1105        d = self.page
1106        gks = idleConf.GetKeySet = Func()
1107        del d.load_keys_list
1108        b = d.bindingslist
1109
1110        b.delete(0, 'end')
1111        b.insert(0, '<<find>>')
1112        b.insert(1, '<<help>>')
1113        gks.result = {'<<copy>>': ['<Control-Key-c>', '<Control-Key-C>'],
1114                      '<<force-open-completions>>': ['<Control-Key-space>'],
1115                      '<<spam>>': ['<Key-F11>']}
1116        changes.add_option('keys', 'my keys', 'spam', '<Shift-Key-a>')
1117        expected = ('copy - <Control-Key-c> <Control-Key-C>',
1118                    'force-open-completions - <Control-Key-space>',
1119                    'spam - <Shift-Key-a>')
1120
1121        # No current selection.
1122        d.load_keys_list('my keys')
1123        eq(b.get(0, 'end'), expected)
1124        eq(b.get('anchor'), '')
1125        eq(b.curselection(), ())
1126
1127        # Check selection.
1128        b.selection_set(1)
1129        b.selection_anchor(1)
1130        d.load_keys_list('my keys')
1131        eq(b.get(0, 'end'), expected)
1132        eq(b.get('anchor'), 'force-open-completions - <Control-Key-space>')
1133        eq(b.curselection(), (1, ))
1134
1135        # Change selection.
1136        b.selection_set(2)
1137        b.selection_anchor(2)
1138        d.load_keys_list('my keys')
1139        eq(b.get(0, 'end'), expected)
1140        eq(b.get('anchor'), 'spam - <Shift-Key-a>')
1141        eq(b.curselection(), (2, ))
1142        d.load_keys_list = Func()
1143
1144        del idleConf.GetKeySet
1145
1146    def test_delete_custom_keys(self):
1147        eq = self.assertEqual
1148        d = self.page
1149        d.button_delete_custom_keys.state(('!disabled',))
1150        yesno = d.askyesno = Func()
1151        dialog.deactivate_current_config = Func()
1152        dialog.activate_config_changes = Func()
1153
1154        keyset_name = 'spam key set'
1155        idleConf.userCfg['keys'].SetOption(keyset_name, 'name', 'value')
1156        keyspage[keyset_name] = {'option': 'True'}
1157
1158        keyset_name2 = 'other key set'
1159        idleConf.userCfg['keys'].SetOption(keyset_name2, 'name', 'value')
1160        keyspage[keyset_name2] = {'option': 'False'}
1161
1162        # Force custom keyset.
1163        d.custom_keyset_on.state(('!disabled',))
1164        d.custom_keyset_on.invoke()
1165        d.custom_name.set(keyset_name)
1166
1167        # Cancel deletion.
1168        yesno.result = False
1169        d.button_delete_custom_keys.invoke()
1170        eq(yesno.called, 1)
1171        eq(keyspage[keyset_name], {'option': 'True'})
1172        eq(idleConf.GetSectionList('user', 'keys'), [keyset_name, keyset_name2])
1173        eq(dialog.deactivate_current_config.called, 0)
1174        eq(dialog.activate_config_changes.called, 0)
1175        eq(d.set_keys_type.called, 0)
1176
1177        # Confirm deletion.
1178        yesno.result = True
1179        d.button_delete_custom_keys.invoke()
1180        eq(yesno.called, 2)
1181        self.assertNotIn(keyset_name, keyspage)
1182        eq(idleConf.GetSectionList('user', 'keys'), [keyset_name2])
1183        eq(d.custom_keyset_on.state(), ())
1184        eq(d.custom_name.get(), keyset_name2)
1185        eq(dialog.deactivate_current_config.called, 1)
1186        eq(dialog.activate_config_changes.called, 1)
1187        eq(d.set_keys_type.called, 1)
1188
1189        # Confirm deletion of second keyset - empties list.
1190        d.custom_name.set(keyset_name2)
1191        yesno.result = True
1192        d.button_delete_custom_keys.invoke()
1193        eq(yesno.called, 3)
1194        self.assertNotIn(keyset_name, keyspage)
1195        eq(idleConf.GetSectionList('user', 'keys'), [])
1196        eq(d.custom_keyset_on.state(), ('disabled',))
1197        eq(d.custom_name.get(), '- no custom keys -')
1198        eq(dialog.deactivate_current_config.called, 2)
1199        eq(dialog.activate_config_changes.called, 2)
1200        eq(d.set_keys_type.called, 2)
1201
1202        del dialog.activate_config_changes, dialog.deactivate_current_config
1203        del d.askyesno
1204
1205
1206class GenPageTest(unittest.TestCase):
1207    """Test that general tab widgets enable users to make changes.
1208
1209    Test that widget actions set vars, that var changes add
1210    options to changes and that helplist works correctly.
1211    """
1212    @classmethod
1213    def setUpClass(cls):
1214        page = cls.page = dialog.genpage
1215        dialog.note.select(page)
1216        page.set = page.set_add_delete_state = Func()
1217        page.upc = page.update_help_changes = Func()
1218        page.update()
1219
1220    @classmethod
1221    def tearDownClass(cls):
1222        page = cls.page
1223        del page.set, page.set_add_delete_state
1224        del page.upc, page.update_help_changes
1225        page.helplist.delete(0, 'end')
1226        page.user_helplist.clear()
1227
1228    def setUp(self):
1229        changes.clear()
1230
1231    def test_load_general_cfg(self):
1232        # Set to wrong values, load, check right values.
1233        eq = self.assertEqual
1234        d = self.page
1235        d.startup_edit.set(1)
1236        d.autosave.set(1)
1237        d.win_width.set(1)
1238        d.win_height.set(1)
1239        d.helplist.insert('end', 'bad')
1240        d.user_helplist = ['bad', 'worse']
1241        idleConf.SetOption('main', 'HelpFiles', '1', 'name;file')
1242        d.load_general_cfg()
1243        eq(d.startup_edit.get(), 0)
1244        eq(d.autosave.get(), 0)
1245        eq(d.win_width.get(), '80')
1246        eq(d.win_height.get(), '40')
1247        eq(d.helplist.get(0, 'end'), ('name',))
1248        eq(d.user_helplist, [('name', 'file', '1')])
1249
1250    def test_startup(self):
1251        d = self.page
1252        d.startup_editor_on.invoke()
1253        self.assertEqual(mainpage,
1254                         {'General': {'editor-on-startup': '1'}})
1255        changes.clear()
1256        d.startup_shell_on.invoke()
1257        self.assertEqual(mainpage,
1258                         {'General': {'editor-on-startup': '0'}})
1259
1260    def test_editor_size(self):
1261        d = self.page
1262        d.win_height_int.delete(0, 'end')
1263        d.win_height_int.insert(0, '11')
1264        self.assertEqual(mainpage, {'EditorWindow': {'height': '11'}})
1265        changes.clear()
1266        d.win_width_int.delete(0, 'end')
1267        d.win_width_int.insert(0, '11')
1268        self.assertEqual(mainpage, {'EditorWindow': {'width': '11'}})
1269
1270    def test_cursor_blink(self):
1271        self.page.cursor_blink_bool.invoke()
1272        self.assertEqual(mainpage, {'EditorWindow': {'cursor-blink': 'False'}})
1273
1274    def test_autocomplete_wait(self):
1275        self.page.auto_wait_int.delete(0, 'end')
1276        self.page.auto_wait_int.insert(0, '11')
1277        self.assertEqual(extpage, {'AutoComplete': {'popupwait': '11'}})
1278
1279    def test_parenmatch(self):
1280        d = self.page
1281        eq = self.assertEqual
1282        d.paren_style_type['menu'].invoke(0)
1283        eq(extpage, {'ParenMatch': {'style': 'opener'}})
1284        changes.clear()
1285        d.paren_flash_time.delete(0, 'end')
1286        d.paren_flash_time.insert(0, '11')
1287        eq(extpage, {'ParenMatch': {'flash-delay': '11'}})
1288        changes.clear()
1289        d.bell_on.invoke()
1290        eq(extpage, {'ParenMatch': {'bell': 'False'}})
1291
1292    def test_autosave(self):
1293        d = self.page
1294        d.save_auto_on.invoke()
1295        self.assertEqual(mainpage, {'General': {'autosave': '1'}})
1296        d.save_ask_on.invoke()
1297        self.assertEqual(mainpage, {'General': {'autosave': '0'}})
1298
1299    def test_paragraph(self):
1300        self.page.format_width_int.delete(0, 'end')
1301        self.page.format_width_int.insert(0, '11')
1302        self.assertEqual(extpage, {'FormatParagraph': {'max-width': '11'}})
1303
1304    def test_context(self):
1305        self.page.context_int.delete(0, 'end')
1306        self.page.context_int.insert(0, '1')
1307        self.assertEqual(extpage, {'CodeContext': {'maxlines': '1'}})
1308
1309    def test_source_selected(self):
1310        d = self.page
1311        d.set = d.set_add_delete_state
1312        d.upc = d.update_help_changes
1313        helplist = d.helplist
1314        dex = 'end'
1315        helplist.insert(dex, 'source')
1316        helplist.activate(dex)
1317
1318        helplist.focus_force()
1319        helplist.see(dex)
1320        helplist.update()
1321        x, y, dx, dy = helplist.bbox(dex)
1322        x += dx // 2
1323        y += dy // 2
1324        d.set.called = d.upc.called = 0
1325        helplist.event_generate('<Enter>', x=0, y=0)
1326        helplist.event_generate('<Motion>', x=x, y=y)
1327        helplist.event_generate('<Button-1>', x=x, y=y)
1328        helplist.event_generate('<ButtonRelease-1>', x=x, y=y)
1329        self.assertEqual(helplist.get('anchor'), 'source')
1330        self.assertTrue(d.set.called)
1331        self.assertFalse(d.upc.called)
1332
1333    def test_set_add_delete_state(self):
1334        # Call with 0 items, 1 unselected item, 1 selected item.
1335        eq = self.assertEqual
1336        d = self.page
1337        del d.set_add_delete_state  # Unmask method.
1338        sad = d.set_add_delete_state
1339        h = d.helplist
1340
1341        h.delete(0, 'end')
1342        sad()
1343        eq(d.button_helplist_edit.state(), ('disabled',))
1344        eq(d.button_helplist_remove.state(), ('disabled',))
1345
1346        h.insert(0, 'source')
1347        sad()
1348        eq(d.button_helplist_edit.state(), ('disabled',))
1349        eq(d.button_helplist_remove.state(), ('disabled',))
1350
1351        h.selection_set(0)
1352        sad()
1353        eq(d.button_helplist_edit.state(), ())
1354        eq(d.button_helplist_remove.state(), ())
1355        d.set_add_delete_state = Func()  # Mask method.
1356
1357    def test_helplist_item_add(self):
1358        # Call without and twice with HelpSource result.
1359        # Double call enables check on order.
1360        eq = self.assertEqual
1361        orig_helpsource = configdialog.HelpSource
1362        hs = configdialog.HelpSource = Func(return_self=True)
1363        d = self.page
1364        d.helplist.delete(0, 'end')
1365        d.user_helplist.clear()
1366        d.set.called = d.upc.called = 0
1367
1368        hs.result = ''
1369        d.helplist_item_add()
1370        self.assertTrue(list(d.helplist.get(0, 'end')) ==
1371                        d.user_helplist == [])
1372        self.assertFalse(d.upc.called)
1373
1374        hs.result = ('name1', 'file1')
1375        d.helplist_item_add()
1376        hs.result = ('name2', 'file2')
1377        d.helplist_item_add()
1378        eq(d.helplist.get(0, 'end'), ('name1', 'name2'))
1379        eq(d.user_helplist, [('name1', 'file1'), ('name2', 'file2')])
1380        eq(d.upc.called, 2)
1381        self.assertFalse(d.set.called)
1382
1383        configdialog.HelpSource = orig_helpsource
1384
1385    def test_helplist_item_edit(self):
1386        # Call without and with HelpSource change.
1387        eq = self.assertEqual
1388        orig_helpsource = configdialog.HelpSource
1389        hs = configdialog.HelpSource = Func(return_self=True)
1390        d = self.page
1391        d.helplist.delete(0, 'end')
1392        d.helplist.insert(0, 'name1')
1393        d.helplist.selection_set(0)
1394        d.helplist.selection_anchor(0)
1395        d.user_helplist.clear()
1396        d.user_helplist.append(('name1', 'file1'))
1397        d.set.called = d.upc.called = 0
1398
1399        hs.result = ''
1400        d.helplist_item_edit()
1401        hs.result = ('name1', 'file1')
1402        d.helplist_item_edit()
1403        eq(d.helplist.get(0, 'end'), ('name1',))
1404        eq(d.user_helplist, [('name1', 'file1')])
1405        self.assertFalse(d.upc.called)
1406
1407        hs.result = ('name2', 'file2')
1408        d.helplist_item_edit()
1409        eq(d.helplist.get(0, 'end'), ('name2',))
1410        eq(d.user_helplist, [('name2', 'file2')])
1411        self.assertTrue(d.upc.called == d.set.called == 1)
1412
1413        configdialog.HelpSource = orig_helpsource
1414
1415    def test_helplist_item_remove(self):
1416        eq = self.assertEqual
1417        d = self.page
1418        d.helplist.delete(0, 'end')
1419        d.helplist.insert(0, 'name1')
1420        d.helplist.selection_set(0)
1421        d.helplist.selection_anchor(0)
1422        d.user_helplist.clear()
1423        d.user_helplist.append(('name1', 'file1'))
1424        d.set.called = d.upc.called = 0
1425
1426        d.helplist_item_remove()
1427        eq(d.helplist.get(0, 'end'), ())
1428        eq(d.user_helplist, [])
1429        self.assertTrue(d.upc.called == d.set.called == 1)
1430
1431    def test_update_help_changes(self):
1432        d = self.page
1433        del d.update_help_changes
1434        d.user_helplist.clear()
1435        d.user_helplist.append(('name1', 'file1'))
1436        d.user_helplist.append(('name2', 'file2'))
1437
1438        d.update_help_changes()
1439        self.assertEqual(mainpage['HelpFiles'],
1440                         {'1': 'name1;file1', '2': 'name2;file2'})
1441        d.update_help_changes = Func()
1442
1443
1444class VarTraceTest(unittest.TestCase):
1445
1446    @classmethod
1447    def setUpClass(cls):
1448        cls.tracers = configdialog.VarTrace()
1449        cls.iv = IntVar(root)
1450        cls.bv = BooleanVar(root)
1451
1452    @classmethod
1453    def tearDownClass(cls):
1454        del cls.tracers, cls.iv, cls.bv
1455
1456    def setUp(self):
1457        self.tracers.clear()
1458        self.called = 0
1459
1460    def var_changed_increment(self, *params):
1461        self.called += 13
1462
1463    def var_changed_boolean(self, *params):
1464        pass
1465
1466    def test_init(self):
1467        tr = self.tracers
1468        tr.__init__()
1469        self.assertEqual(tr.untraced, [])
1470        self.assertEqual(tr.traced, [])
1471
1472    def test_clear(self):
1473        tr = self.tracers
1474        tr.untraced.append(0)
1475        tr.traced.append(1)
1476        tr.clear()
1477        self.assertEqual(tr.untraced, [])
1478        self.assertEqual(tr.traced, [])
1479
1480    def test_add(self):
1481        tr = self.tracers
1482        func = Func()
1483        cb = tr.make_callback = mock.Mock(return_value=func)
1484
1485        iv = tr.add(self.iv, self.var_changed_increment)
1486        self.assertIs(iv, self.iv)
1487        bv = tr.add(self.bv, self.var_changed_boolean)
1488        self.assertIs(bv, self.bv)
1489
1490        sv = StringVar(root)
1491        sv2 = tr.add(sv, ('main', 'section', 'option'))
1492        self.assertIs(sv2, sv)
1493        cb.assert_called_once()
1494        cb.assert_called_with(sv, ('main', 'section', 'option'))
1495
1496        expected = [(iv, self.var_changed_increment),
1497                    (bv, self.var_changed_boolean),
1498                    (sv, func)]
1499        self.assertEqual(tr.traced, [])
1500        self.assertEqual(tr.untraced, expected)
1501
1502        del tr.make_callback
1503
1504    def test_make_callback(self):
1505        cb = self.tracers.make_callback(self.iv, ('main', 'section', 'option'))
1506        self.assertTrue(callable(cb))
1507        self.iv.set(42)
1508        # Not attached, so set didn't invoke the callback.
1509        self.assertNotIn('section', changes['main'])
1510        # Invoke callback manually.
1511        cb()
1512        self.assertIn('section', changes['main'])
1513        self.assertEqual(changes['main']['section']['option'], '42')
1514        changes.clear()
1515
1516    def test_attach_detach(self):
1517        tr = self.tracers
1518        iv = tr.add(self.iv, self.var_changed_increment)
1519        bv = tr.add(self.bv, self.var_changed_boolean)
1520        expected = [(iv, self.var_changed_increment),
1521                    (bv, self.var_changed_boolean)]
1522
1523        # Attach callbacks and test call increment.
1524        tr.attach()
1525        self.assertEqual(tr.untraced, [])
1526        self.assertCountEqual(tr.traced, expected)
1527        iv.set(1)
1528        self.assertEqual(iv.get(), 1)
1529        self.assertEqual(self.called, 13)
1530
1531        # Check that only one callback is attached to a variable.
1532        # If more than one callback were attached, then var_changed_increment
1533        # would be called twice and the counter would be 2.
1534        self.called = 0
1535        tr.attach()
1536        iv.set(1)
1537        self.assertEqual(self.called, 13)
1538
1539        # Detach callbacks.
1540        self.called = 0
1541        tr.detach()
1542        self.assertEqual(tr.traced, [])
1543        self.assertCountEqual(tr.untraced, expected)
1544        iv.set(1)
1545        self.assertEqual(self.called, 0)
1546
1547
1548if __name__ == '__main__':
1549    unittest.main(verbosity=2)
1550