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