• 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        d = self.page
610        del d.paint_theme_sample
611        hs_tag = d.highlight_sample.tag_cget
612        gh = idleConf.GetHighlight
613        fg = 'foreground'
614        bg = 'background'
615
616        # Create custom theme based on IDLE Dark.
617        d.theme_source.set(True)
618        d.builtin_name.set('IDLE Dark')
619        theme = 'IDLE Test'
620        d.create_new(theme)
621        d.set_color_sample.called = 0
622
623        # Base theme with nothing in `changes`.
624        d.paint_theme_sample()
625        eq(hs_tag('break', fg), gh(theme, 'break', fgBg='fg'))
626        eq(hs_tag('cursor', bg), gh(theme, 'normal', fgBg='bg'))
627        self.assertNotEqual(hs_tag('console', fg), 'blue')
628        self.assertNotEqual(hs_tag('console', bg), 'yellow')
629        eq(d.set_color_sample.called, 1)
630
631        # Apply changes.
632        changes.add_option('highlight', theme, 'console-foreground', 'blue')
633        changes.add_option('highlight', theme, 'console-background', 'yellow')
634        d.paint_theme_sample()
635
636        eq(hs_tag('break', fg), gh(theme, 'break', fgBg='fg'))
637        eq(hs_tag('cursor', bg), gh(theme, 'normal', fgBg='bg'))
638        eq(hs_tag('console', fg), 'blue')
639        eq(hs_tag('console', bg), 'yellow')
640        eq(d.set_color_sample.called, 2)
641
642        d.paint_theme_sample = Func()
643
644    def test_delete_custom(self):
645        eq = self.assertEqual
646        d = self.page
647        d.button_delete_custom.state(('!disabled',))
648        yesno = d.askyesno = Func()
649        dialog.deactivate_current_config = Func()
650        dialog.activate_config_changes = Func()
651
652        theme_name = 'spam theme'
653        idleConf.userCfg['highlight'].SetOption(theme_name, 'name', 'value')
654        highpage[theme_name] = {'option': 'True'}
655
656        # Force custom theme.
657        d.theme_source.set(False)
658        d.custom_name.set(theme_name)
659
660        # Cancel deletion.
661        yesno.result = False
662        d.button_delete_custom.invoke()
663        eq(yesno.called, 1)
664        eq(highpage[theme_name], {'option': 'True'})
665        eq(idleConf.GetSectionList('user', 'highlight'), ['spam theme'])
666        eq(dialog.deactivate_current_config.called, 0)
667        eq(dialog.activate_config_changes.called, 0)
668        eq(d.set_theme_type.called, 0)
669
670        # Confirm deletion.
671        yesno.result = True
672        d.button_delete_custom.invoke()
673        eq(yesno.called, 2)
674        self.assertNotIn(theme_name, highpage)
675        eq(idleConf.GetSectionList('user', 'highlight'), [])
676        eq(d.custom_theme_on.state(), ('disabled',))
677        eq(d.custom_name.get(), '- no custom themes -')
678        eq(dialog.deactivate_current_config.called, 1)
679        eq(dialog.activate_config_changes.called, 1)
680        eq(d.set_theme_type.called, 1)
681
682        del dialog.activate_config_changes, dialog.deactivate_current_config
683        del d.askyesno
684
685
686class KeysPageTest(unittest.TestCase):
687    """Test that keys tab widgets enable users to make changes.
688
689    Test that widget actions set vars, that var changes add
690    options to changes and that key sets works correctly.
691    """
692
693    @classmethod
694    def setUpClass(cls):
695        page = cls.page = dialog.keyspage
696        dialog.note.select(page)
697        page.set_keys_type = Func()
698        page.load_keys_list = Func()
699
700    @classmethod
701    def tearDownClass(cls):
702        page = cls.page
703        del page.set_keys_type, page.load_keys_list
704
705    def setUp(self):
706        d = self.page
707        # The following is needed for test_load_key_cfg, _delete_custom_keys.
708        # This may indicate a defect in some test or function.
709        for section in idleConf.GetSectionList('user', 'keys'):
710            idleConf.userCfg['keys'].remove_section(section)
711        changes.clear()
712        d.set_keys_type.called = 0
713        d.load_keys_list.called = 0
714
715    def test_load_key_cfg(self):
716        tracers.detach()
717        d = self.page
718        eq = self.assertEqual
719
720        # Use builtin keyset with no user keysets created.
721        idleConf.CurrentKeys = mock.Mock(return_value='IDLE Classic OSX')
722        d.load_key_cfg()
723        self.assertTrue(d.keyset_source.get())
724        # builtinlist sets variable builtin_name to the CurrentKeys default.
725        eq(d.builtin_name.get(), 'IDLE Classic OSX')
726        eq(d.custom_name.get(), '- no custom keys -')
727        eq(d.custom_keyset_on.state(), ('disabled',))
728        eq(d.set_keys_type.called, 1)
729        eq(d.load_keys_list.called, 1)
730        eq(d.load_keys_list.args, ('IDLE Classic OSX', ))
731
732        # Builtin keyset with non-empty user keyset list.
733        idleConf.SetOption('keys', 'test1', 'option', 'value')
734        idleConf.SetOption('keys', 'test2', 'option2', 'value2')
735        d.load_key_cfg()
736        eq(d.builtin_name.get(), 'IDLE Classic OSX')
737        eq(d.custom_name.get(), 'test1')
738        eq(d.set_keys_type.called, 2)
739        eq(d.load_keys_list.called, 2)
740        eq(d.load_keys_list.args, ('IDLE Classic OSX', ))
741
742        # Use custom keyset.
743        idleConf.CurrentKeys = mock.Mock(return_value='test2')
744        idleConf.default_keys = mock.Mock(return_value='IDLE Modern Unix')
745        idleConf.SetOption('main', 'Keys', 'default', '0')
746        d.load_key_cfg()
747        self.assertFalse(d.keyset_source.get())
748        eq(d.builtin_name.get(), 'IDLE Modern Unix')
749        eq(d.custom_name.get(), 'test2')
750        eq(d.set_keys_type.called, 3)
751        eq(d.load_keys_list.called, 3)
752        eq(d.load_keys_list.args, ('test2', ))
753
754        del idleConf.CurrentKeys, idleConf.default_keys
755        tracers.attach()
756
757    def test_keyset_source(self):
758        eq = self.assertEqual
759        d = self.page
760        # Test these separately.
761        d.var_changed_builtin_name = Func()
762        d.var_changed_custom_name = Func()
763        # Builtin selected.
764        d.builtin_keyset_on.invoke()
765        eq(mainpage, {'Keys': {'default': 'True'}})
766        eq(d.var_changed_builtin_name.called, 1)
767        eq(d.var_changed_custom_name.called, 0)
768        changes.clear()
769
770        # Custom selected.
771        d.custom_keyset_on.state(('!disabled',))
772        d.custom_keyset_on.invoke()
773        self.assertEqual(mainpage, {'Keys': {'default': 'False'}})
774        eq(d.var_changed_builtin_name.called, 1)
775        eq(d.var_changed_custom_name.called, 1)
776        del d.var_changed_builtin_name, d.var_changed_custom_name
777
778    def test_builtin_name(self):
779        eq = self.assertEqual
780        d = self.page
781        idleConf.userCfg['main'].remove_section('Keys')
782        item_list = ['IDLE Classic Windows', 'IDLE Classic OSX',
783                     'IDLE Modern UNIX']
784
785        # Not in old_keys, defaults name to first item.
786        d.builtinlist.SetMenu(item_list, 'IDLE Modern UNIX')
787        eq(mainpage, {'Keys': {'name': 'IDLE Classic Windows',
788                               'name2': 'IDLE Modern UNIX'}})
789        eq(d.keys_message['text'], 'New key set, see Help')
790        eq(d.load_keys_list.called, 1)
791        eq(d.load_keys_list.args, ('IDLE Modern UNIX', ))
792
793        # Not in old keys - uses name2.
794        changes.clear()
795        idleConf.SetOption('main', 'Keys', 'name', 'IDLE Classic Unix')
796        d.builtinlist.SetMenu(item_list, 'IDLE Modern UNIX')
797        eq(mainpage, {'Keys': {'name2': 'IDLE Modern UNIX'}})
798        eq(d.keys_message['text'], 'New key set, see Help')
799        eq(d.load_keys_list.called, 2)
800        eq(d.load_keys_list.args, ('IDLE Modern UNIX', ))
801
802        # Builtin name in old_keys.
803        changes.clear()
804        d.builtinlist.SetMenu(item_list, 'IDLE Classic OSX')
805        eq(mainpage, {'Keys': {'name': 'IDLE Classic OSX', 'name2': ''}})
806        eq(d.keys_message['text'], '')
807        eq(d.load_keys_list.called, 3)
808        eq(d.load_keys_list.args, ('IDLE Classic OSX', ))
809
810    def test_custom_name(self):
811        d = self.page
812
813        # If no selections, doesn't get added.
814        d.customlist.SetMenu([], '- no custom keys -')
815        self.assertNotIn('Keys', mainpage)
816        self.assertEqual(d.load_keys_list.called, 0)
817
818        # Custom name selected.
819        changes.clear()
820        d.customlist.SetMenu(['a', 'b', 'c'], 'c')
821        self.assertEqual(mainpage, {'Keys': {'name': 'c'}})
822        self.assertEqual(d.load_keys_list.called, 1)
823
824    def test_keybinding(self):
825        idleConf.SetOption('extensions', 'ZzDummy', 'enable', 'True')
826        d = self.page
827        d.custom_name.set('my custom keys')
828        d.bindingslist.delete(0, 'end')
829        d.bindingslist.insert(0, 'copy')
830        d.bindingslist.insert(1, 'z-in')
831        d.bindingslist.selection_set(0)
832        d.bindingslist.selection_anchor(0)
833        # Core binding - adds to keys.
834        d.keybinding.set('<Key-F11>')
835        self.assertEqual(keyspage,
836                         {'my custom keys': {'copy': '<Key-F11>'}})
837
838        # Not a core binding - adds to extensions.
839        d.bindingslist.selection_set(1)
840        d.bindingslist.selection_anchor(1)
841        d.keybinding.set('<Key-F11>')
842        self.assertEqual(extpage,
843                         {'ZzDummy_cfgBindings': {'z-in': '<Key-F11>'}})
844
845    def test_set_keys_type(self):
846        eq = self.assertEqual
847        d = self.page
848        del d.set_keys_type
849
850        # Builtin keyset selected.
851        d.keyset_source.set(True)
852        d.set_keys_type()
853        eq(d.builtinlist['state'], NORMAL)
854        eq(d.customlist['state'], DISABLED)
855        eq(d.button_delete_custom_keys.state(), ('disabled',))
856
857        # Custom keyset selected.
858        d.keyset_source.set(False)
859        d.set_keys_type()
860        eq(d.builtinlist['state'], DISABLED)
861        eq(d.custom_keyset_on.state(), ('selected',))
862        eq(d.customlist['state'], NORMAL)
863        eq(d.button_delete_custom_keys.state(), ())
864        d.set_keys_type = Func()
865
866    def test_get_new_keys(self):
867        eq = self.assertEqual
868        d = self.page
869        orig_getkeysdialog = configdialog.GetKeysDialog
870        gkd = configdialog.GetKeysDialog = Func(return_self=True)
871        gnkn = d.get_new_keys_name = Func()
872
873        d.button_new_keys.state(('!disabled',))
874        d.bindingslist.delete(0, 'end')
875        d.bindingslist.insert(0, 'copy - <Control-Shift-Key-C>')
876        d.bindingslist.selection_set(0)
877        d.bindingslist.selection_anchor(0)
878        d.keybinding.set('Key-a')
879        d.keyset_source.set(True)  # Default keyset.
880
881        # Default keyset; no change to binding.
882        gkd.result = ''
883        d.button_new_keys.invoke()
884        eq(d.bindingslist.get('anchor'), 'copy - <Control-Shift-Key-C>')
885        # Keybinding isn't changed when there isn't a change entered.
886        eq(d.keybinding.get(), 'Key-a')
887
888        # Default keyset; binding changed.
889        gkd.result = '<Key-F11>'
890        # No keyset name selected therefore binding not saved.
891        gnkn.result = ''
892        d.button_new_keys.invoke()
893        eq(gnkn.called, 1)
894        eq(d.bindingslist.get('anchor'), 'copy - <Control-Shift-Key-C>')
895        # Keyset name selected.
896        gnkn.result = 'My New Key Set'
897        d.button_new_keys.invoke()
898        eq(d.custom_name.get(), gnkn.result)
899        eq(d.bindingslist.get('anchor'), 'copy - <Key-F11>')
900        eq(d.keybinding.get(), '<Key-F11>')
901
902        # User keyset; binding changed.
903        d.keyset_source.set(False)  # Custom keyset.
904        gnkn.called = 0
905        gkd.result = '<Key-p>'
906        d.button_new_keys.invoke()
907        eq(gnkn.called, 0)
908        eq(d.bindingslist.get('anchor'), 'copy - <Key-p>')
909        eq(d.keybinding.get(), '<Key-p>')
910
911        del d.get_new_keys_name
912        configdialog.GetKeysDialog = orig_getkeysdialog
913
914    def test_get_new_keys_name(self):
915        orig_sectionname = configdialog.SectionName
916        sn = configdialog.SectionName = Func(return_self=True)
917        d = self.page
918
919        sn.result = 'New Keys'
920        self.assertEqual(d.get_new_keys_name(''), 'New Keys')
921
922        configdialog.SectionName = orig_sectionname
923
924    def test_save_as_new_key_set(self):
925        d = self.page
926        gnkn = d.get_new_keys_name = Func()
927        d.keyset_source.set(True)
928
929        # No name entered.
930        gnkn.result = ''
931        d.button_save_custom_keys.invoke()
932
933        # Name entered.
934        gnkn.result = 'my new key set'
935        gnkn.called = 0
936        self.assertNotIn(gnkn.result, idleConf.userCfg['keys'])
937        d.button_save_custom_keys.invoke()
938        self.assertIn(gnkn.result, idleConf.userCfg['keys'])
939
940        del d.get_new_keys_name
941
942    def test_on_bindingslist_select(self):
943        d = self.page
944        b = d.bindingslist
945        b.delete(0, 'end')
946        b.insert(0, 'copy')
947        b.insert(1, 'find')
948        b.activate(0)
949
950        b.focus_force()
951        b.see(1)
952        b.update()
953        x, y, dx, dy = b.bbox(1)
954        x += dx // 2
955        y += dy // 2
956        b.event_generate('<Enter>', x=0, y=0)
957        b.event_generate('<Motion>', x=x, y=y)
958        b.event_generate('<Button-1>', x=x, y=y)
959        b.event_generate('<ButtonRelease-1>', x=x, y=y)
960        self.assertEqual(b.get('anchor'), 'find')
961        self.assertEqual(d.button_new_keys.state(), ())
962
963    def test_create_new_key_set_and_save_new_key_set(self):
964        eq = self.assertEqual
965        d = self.page
966
967        # Use default as previously active keyset.
968        d.keyset_source.set(True)
969        d.builtin_name.set('IDLE Classic Windows')
970        first_new = 'my new custom key set'
971        second_new = 'my second custom keyset'
972
973        # No changes, so keysets are an exact copy.
974        self.assertNotIn(first_new, idleConf.userCfg)
975        d.create_new_key_set(first_new)
976        eq(idleConf.GetSectionList('user', 'keys'), [first_new])
977        eq(idleConf.GetKeySet('IDLE Classic Windows'),
978           idleConf.GetKeySet(first_new))
979        eq(d.custom_name.get(), first_new)
980        self.assertFalse(d.keyset_source.get())  # Use custom set.
981        eq(d.set_keys_type.called, 1)
982
983        # Test that changed keybindings are in new keyset.
984        changes.add_option('keys', first_new, 'copy', '<Key-F11>')
985        self.assertNotIn(second_new, idleConf.userCfg)
986        d.create_new_key_set(second_new)
987        eq(idleConf.GetSectionList('user', 'keys'), [first_new, second_new])
988        self.assertNotEqual(idleConf.GetKeySet(first_new),
989                            idleConf.GetKeySet(second_new))
990        # Check that difference in keysets was in option `copy` from `changes`.
991        idleConf.SetOption('keys', first_new, 'copy', '<Key-F11>')
992        eq(idleConf.GetKeySet(first_new), idleConf.GetKeySet(second_new))
993
994    def test_load_keys_list(self):
995        eq = self.assertEqual
996        d = self.page
997        gks = idleConf.GetKeySet = Func()
998        del d.load_keys_list
999        b = d.bindingslist
1000
1001        b.delete(0, 'end')
1002        b.insert(0, '<<find>>')
1003        b.insert(1, '<<help>>')
1004        gks.result = {'<<copy>>': ['<Control-Key-c>', '<Control-Key-C>'],
1005                      '<<force-open-completions>>': ['<Control-Key-space>'],
1006                      '<<spam>>': ['<Key-F11>']}
1007        changes.add_option('keys', 'my keys', 'spam', '<Shift-Key-a>')
1008        expected = ('copy - <Control-Key-c> <Control-Key-C>',
1009                    'force-open-completions - <Control-Key-space>',
1010                    'spam - <Shift-Key-a>')
1011
1012        # No current selection.
1013        d.load_keys_list('my keys')
1014        eq(b.get(0, 'end'), expected)
1015        eq(b.get('anchor'), '')
1016        eq(b.curselection(), ())
1017
1018        # Check selection.
1019        b.selection_set(1)
1020        b.selection_anchor(1)
1021        d.load_keys_list('my keys')
1022        eq(b.get(0, 'end'), expected)
1023        eq(b.get('anchor'), 'force-open-completions - <Control-Key-space>')
1024        eq(b.curselection(), (1, ))
1025
1026        # Change selection.
1027        b.selection_set(2)
1028        b.selection_anchor(2)
1029        d.load_keys_list('my keys')
1030        eq(b.get(0, 'end'), expected)
1031        eq(b.get('anchor'), 'spam - <Shift-Key-a>')
1032        eq(b.curselection(), (2, ))
1033        d.load_keys_list = Func()
1034
1035        del idleConf.GetKeySet
1036
1037    def test_delete_custom_keys(self):
1038        eq = self.assertEqual
1039        d = self.page
1040        d.button_delete_custom_keys.state(('!disabled',))
1041        yesno = d.askyesno = Func()
1042        dialog.deactivate_current_config = Func()
1043        dialog.activate_config_changes = Func()
1044
1045        keyset_name = 'spam key set'
1046        idleConf.userCfg['keys'].SetOption(keyset_name, 'name', 'value')
1047        keyspage[keyset_name] = {'option': 'True'}
1048
1049        # Force custom keyset.
1050        d.keyset_source.set(False)
1051        d.custom_name.set(keyset_name)
1052
1053        # Cancel deletion.
1054        yesno.result = False
1055        d.button_delete_custom_keys.invoke()
1056        eq(yesno.called, 1)
1057        eq(keyspage[keyset_name], {'option': 'True'})
1058        eq(idleConf.GetSectionList('user', 'keys'), ['spam key set'])
1059        eq(dialog.deactivate_current_config.called, 0)
1060        eq(dialog.activate_config_changes.called, 0)
1061        eq(d.set_keys_type.called, 0)
1062
1063        # Confirm deletion.
1064        yesno.result = True
1065        d.button_delete_custom_keys.invoke()
1066        eq(yesno.called, 2)
1067        self.assertNotIn(keyset_name, keyspage)
1068        eq(idleConf.GetSectionList('user', 'keys'), [])
1069        eq(d.custom_keyset_on.state(), ('disabled',))
1070        eq(d.custom_name.get(), '- no custom keys -')
1071        eq(dialog.deactivate_current_config.called, 1)
1072        eq(dialog.activate_config_changes.called, 1)
1073        eq(d.set_keys_type.called, 1)
1074
1075        del dialog.activate_config_changes, dialog.deactivate_current_config
1076        del d.askyesno
1077
1078
1079class GenPageTest(unittest.TestCase):
1080    """Test that general tab widgets enable users to make changes.
1081
1082    Test that widget actions set vars, that var changes add
1083    options to changes and that helplist works correctly.
1084    """
1085    @classmethod
1086    def setUpClass(cls):
1087        page = cls.page = dialog.genpage
1088        dialog.note.select(page)
1089        page.set = page.set_add_delete_state = Func()
1090        page.upc = page.update_help_changes = Func()
1091        page.update()
1092
1093    @classmethod
1094    def tearDownClass(cls):
1095        page = cls.page
1096        del page.set, page.set_add_delete_state
1097        del page.upc, page.update_help_changes
1098        page.helplist.delete(0, 'end')
1099        page.user_helplist.clear()
1100
1101    def setUp(self):
1102        changes.clear()
1103
1104    def test_load_general_cfg(self):
1105        # Set to wrong values, load, check right values.
1106        eq = self.assertEqual
1107        d = self.page
1108        d.startup_edit.set(1)
1109        d.autosave.set(1)
1110        d.win_width.set(1)
1111        d.win_height.set(1)
1112        d.helplist.insert('end', 'bad')
1113        d.user_helplist = ['bad', 'worse']
1114        idleConf.SetOption('main', 'HelpFiles', '1', 'name;file')
1115        d.load_general_cfg()
1116        eq(d.startup_edit.get(), 0)
1117        eq(d.autosave.get(), 0)
1118        eq(d.win_width.get(), '80')
1119        eq(d.win_height.get(), '40')
1120        eq(d.helplist.get(0, 'end'), ('name',))
1121        eq(d.user_helplist, [('name', 'file', '1')])
1122
1123    def test_startup(self):
1124        d = self.page
1125        d.startup_editor_on.invoke()
1126        self.assertEqual(mainpage,
1127                         {'General': {'editor-on-startup': '1'}})
1128        changes.clear()
1129        d.startup_shell_on.invoke()
1130        self.assertEqual(mainpage,
1131                         {'General': {'editor-on-startup': '0'}})
1132
1133    def test_editor_size(self):
1134        d = self.page
1135        d.win_height_int.delete(0, 'end')
1136        d.win_height_int.insert(0, '11')
1137        self.assertEqual(mainpage, {'EditorWindow': {'height': '11'}})
1138        changes.clear()
1139        d.win_width_int.delete(0, 'end')
1140        d.win_width_int.insert(0, '11')
1141        self.assertEqual(mainpage, {'EditorWindow': {'width': '11'}})
1142
1143    def test_autocomplete_wait(self):
1144        self.page.auto_wait_int.delete(0, 'end')
1145        self.page.auto_wait_int.insert(0, '11')
1146        self.assertEqual(extpage, {'AutoComplete': {'popupwait': '11'}})
1147
1148    def test_parenmatch(self):
1149        d = self.page
1150        eq = self.assertEqual
1151        d.paren_style_type['menu'].invoke(0)
1152        eq(extpage, {'ParenMatch': {'style': 'opener'}})
1153        changes.clear()
1154        d.paren_flash_time.delete(0, 'end')
1155        d.paren_flash_time.insert(0, '11')
1156        eq(extpage, {'ParenMatch': {'flash-delay': '11'}})
1157        changes.clear()
1158        d.bell_on.invoke()
1159        eq(extpage, {'ParenMatch': {'bell': 'False'}})
1160
1161    def test_autosave(self):
1162        d = self.page
1163        d.save_auto_on.invoke()
1164        self.assertEqual(mainpage, {'General': {'autosave': '1'}})
1165        d.save_ask_on.invoke()
1166        self.assertEqual(mainpage, {'General': {'autosave': '0'}})
1167
1168    def test_paragraph(self):
1169        self.page.format_width_int.delete(0, 'end')
1170        self.page.format_width_int.insert(0, '11')
1171        self.assertEqual(extpage, {'FormatParagraph': {'max-width': '11'}})
1172
1173    def test_context(self):
1174        self.page.context_int.delete(0, 'end')
1175        self.page.context_int.insert(0, '1')
1176        self.assertEqual(extpage, {'CodeContext': {'maxlines': '1'}})
1177
1178    def test_source_selected(self):
1179        d = self.page
1180        d.set = d.set_add_delete_state
1181        d.upc = d.update_help_changes
1182        helplist = d.helplist
1183        dex = 'end'
1184        helplist.insert(dex, 'source')
1185        helplist.activate(dex)
1186
1187        helplist.focus_force()
1188        helplist.see(dex)
1189        helplist.update()
1190        x, y, dx, dy = helplist.bbox(dex)
1191        x += dx // 2
1192        y += dy // 2
1193        d.set.called = d.upc.called = 0
1194        helplist.event_generate('<Enter>', x=0, y=0)
1195        helplist.event_generate('<Motion>', x=x, y=y)
1196        helplist.event_generate('<Button-1>', x=x, y=y)
1197        helplist.event_generate('<ButtonRelease-1>', x=x, y=y)
1198        self.assertEqual(helplist.get('anchor'), 'source')
1199        self.assertTrue(d.set.called)
1200        self.assertFalse(d.upc.called)
1201
1202    def test_set_add_delete_state(self):
1203        # Call with 0 items, 1 unselected item, 1 selected item.
1204        eq = self.assertEqual
1205        d = self.page
1206        del d.set_add_delete_state  # Unmask method.
1207        sad = d.set_add_delete_state
1208        h = d.helplist
1209
1210        h.delete(0, 'end')
1211        sad()
1212        eq(d.button_helplist_edit.state(), ('disabled',))
1213        eq(d.button_helplist_remove.state(), ('disabled',))
1214
1215        h.insert(0, 'source')
1216        sad()
1217        eq(d.button_helplist_edit.state(), ('disabled',))
1218        eq(d.button_helplist_remove.state(), ('disabled',))
1219
1220        h.selection_set(0)
1221        sad()
1222        eq(d.button_helplist_edit.state(), ())
1223        eq(d.button_helplist_remove.state(), ())
1224        d.set_add_delete_state = Func()  # Mask method.
1225
1226    def test_helplist_item_add(self):
1227        # Call without and twice with HelpSource result.
1228        # Double call enables check on order.
1229        eq = self.assertEqual
1230        orig_helpsource = configdialog.HelpSource
1231        hs = configdialog.HelpSource = Func(return_self=True)
1232        d = self.page
1233        d.helplist.delete(0, 'end')
1234        d.user_helplist.clear()
1235        d.set.called = d.upc.called = 0
1236
1237        hs.result = ''
1238        d.helplist_item_add()
1239        self.assertTrue(list(d.helplist.get(0, 'end')) ==
1240                        d.user_helplist == [])
1241        self.assertFalse(d.upc.called)
1242
1243        hs.result = ('name1', 'file1')
1244        d.helplist_item_add()
1245        hs.result = ('name2', 'file2')
1246        d.helplist_item_add()
1247        eq(d.helplist.get(0, 'end'), ('name1', 'name2'))
1248        eq(d.user_helplist, [('name1', 'file1'), ('name2', 'file2')])
1249        eq(d.upc.called, 2)
1250        self.assertFalse(d.set.called)
1251
1252        configdialog.HelpSource = orig_helpsource
1253
1254    def test_helplist_item_edit(self):
1255        # Call without and with HelpSource change.
1256        eq = self.assertEqual
1257        orig_helpsource = configdialog.HelpSource
1258        hs = configdialog.HelpSource = Func(return_self=True)
1259        d = self.page
1260        d.helplist.delete(0, 'end')
1261        d.helplist.insert(0, 'name1')
1262        d.helplist.selection_set(0)
1263        d.helplist.selection_anchor(0)
1264        d.user_helplist.clear()
1265        d.user_helplist.append(('name1', 'file1'))
1266        d.set.called = d.upc.called = 0
1267
1268        hs.result = ''
1269        d.helplist_item_edit()
1270        hs.result = ('name1', 'file1')
1271        d.helplist_item_edit()
1272        eq(d.helplist.get(0, 'end'), ('name1',))
1273        eq(d.user_helplist, [('name1', 'file1')])
1274        self.assertFalse(d.upc.called)
1275
1276        hs.result = ('name2', 'file2')
1277        d.helplist_item_edit()
1278        eq(d.helplist.get(0, 'end'), ('name2',))
1279        eq(d.user_helplist, [('name2', 'file2')])
1280        self.assertTrue(d.upc.called == d.set.called == 1)
1281
1282        configdialog.HelpSource = orig_helpsource
1283
1284    def test_helplist_item_remove(self):
1285        eq = self.assertEqual
1286        d = self.page
1287        d.helplist.delete(0, 'end')
1288        d.helplist.insert(0, 'name1')
1289        d.helplist.selection_set(0)
1290        d.helplist.selection_anchor(0)
1291        d.user_helplist.clear()
1292        d.user_helplist.append(('name1', 'file1'))
1293        d.set.called = d.upc.called = 0
1294
1295        d.helplist_item_remove()
1296        eq(d.helplist.get(0, 'end'), ())
1297        eq(d.user_helplist, [])
1298        self.assertTrue(d.upc.called == d.set.called == 1)
1299
1300    def test_update_help_changes(self):
1301        d = self.page
1302        del d.update_help_changes
1303        d.user_helplist.clear()
1304        d.user_helplist.append(('name1', 'file1'))
1305        d.user_helplist.append(('name2', 'file2'))
1306
1307        d.update_help_changes()
1308        self.assertEqual(mainpage['HelpFiles'],
1309                         {'1': 'name1;file1', '2': 'name2;file2'})
1310        d.update_help_changes = Func()
1311
1312
1313class VarTraceTest(unittest.TestCase):
1314
1315    @classmethod
1316    def setUpClass(cls):
1317        cls.tracers = configdialog.VarTrace()
1318        cls.iv = IntVar(root)
1319        cls.bv = BooleanVar(root)
1320
1321    @classmethod
1322    def tearDownClass(cls):
1323        del cls.tracers, cls.iv, cls.bv
1324
1325    def setUp(self):
1326        self.tracers.clear()
1327        self.called = 0
1328
1329    def var_changed_increment(self, *params):
1330        self.called += 13
1331
1332    def var_changed_boolean(self, *params):
1333        pass
1334
1335    def test_init(self):
1336        tr = self.tracers
1337        tr.__init__()
1338        self.assertEqual(tr.untraced, [])
1339        self.assertEqual(tr.traced, [])
1340
1341    def test_clear(self):
1342        tr = self.tracers
1343        tr.untraced.append(0)
1344        tr.traced.append(1)
1345        tr.clear()
1346        self.assertEqual(tr.untraced, [])
1347        self.assertEqual(tr.traced, [])
1348
1349    def test_add(self):
1350        tr = self.tracers
1351        func = Func()
1352        cb = tr.make_callback = mock.Mock(return_value=func)
1353
1354        iv = tr.add(self.iv, self.var_changed_increment)
1355        self.assertIs(iv, self.iv)
1356        bv = tr.add(self.bv, self.var_changed_boolean)
1357        self.assertIs(bv, self.bv)
1358
1359        sv = StringVar(root)
1360        sv2 = tr.add(sv, ('main', 'section', 'option'))
1361        self.assertIs(sv2, sv)
1362        cb.assert_called_once()
1363        cb.assert_called_with(sv, ('main', 'section', 'option'))
1364
1365        expected = [(iv, self.var_changed_increment),
1366                    (bv, self.var_changed_boolean),
1367                    (sv, func)]
1368        self.assertEqual(tr.traced, [])
1369        self.assertEqual(tr.untraced, expected)
1370
1371        del tr.make_callback
1372
1373    def test_make_callback(self):
1374        cb = self.tracers.make_callback(self.iv, ('main', 'section', 'option'))
1375        self.assertTrue(callable(cb))
1376        self.iv.set(42)
1377        # Not attached, so set didn't invoke the callback.
1378        self.assertNotIn('section', changes['main'])
1379        # Invoke callback manually.
1380        cb()
1381        self.assertIn('section', changes['main'])
1382        self.assertEqual(changes['main']['section']['option'], '42')
1383        changes.clear()
1384
1385    def test_attach_detach(self):
1386        tr = self.tracers
1387        iv = tr.add(self.iv, self.var_changed_increment)
1388        bv = tr.add(self.bv, self.var_changed_boolean)
1389        expected = [(iv, self.var_changed_increment),
1390                    (bv, self.var_changed_boolean)]
1391
1392        # Attach callbacks and test call increment.
1393        tr.attach()
1394        self.assertEqual(tr.untraced, [])
1395        self.assertCountEqual(tr.traced, expected)
1396        iv.set(1)
1397        self.assertEqual(iv.get(), 1)
1398        self.assertEqual(self.called, 13)
1399
1400        # Check that only one callback is attached to a variable.
1401        # If more than one callback were attached, then var_changed_increment
1402        # would be called twice and the counter would be 2.
1403        self.called = 0
1404        tr.attach()
1405        iv.set(1)
1406        self.assertEqual(self.called, 13)
1407
1408        # Detach callbacks.
1409        self.called = 0
1410        tr.detach()
1411        self.assertEqual(tr.traced, [])
1412        self.assertCountEqual(tr.untraced, expected)
1413        iv.set(1)
1414        self.assertEqual(self.called, 0)
1415
1416
1417if __name__ == '__main__':
1418    unittest.main(verbosity=2)
1419