Home | History | Annotate | Download | only in idle_test
      1 """Test configdialog, coverage 94%.
      2 
      3 Half the class creates dialog, half works with user customizations.
      4 """
      5 from idlelib import configdialog
      6 from test.support import requires
      7 requires('gui')
      8 import unittest
      9 from unittest import mock
     10 from idlelib.idle_test.mock_idle import Func
     11 from tkinter import Tk, StringVar, IntVar, BooleanVar, DISABLED, NORMAL
     12 from idlelib import config
     13 from 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.
     18 usercfg = idleConf.userCfg
     19 testcfg = {
     20     'main': config.IdleUserConfParser(''),
     21     'highlight': config.IdleUserConfParser(''),
     22     'keys': config.IdleUserConfParser(''),
     23     'extensions': config.IdleUserConfParser(''),
     24 }
     25 
     26 root = None
     27 dialog = None
     28 mainpage = changes['main']
     29 highpage = changes['highlight']
     30 keyspage = changes['keys']
     31 extpage = changes['extensions']
     32 
     33 def 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 
     40 def 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 
     51 class 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 
    209 class 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 
    230 class 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 
    686 class 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 
   1079 class 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 
   1313 class 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 
   1417 if __name__ == '__main__':
   1418     unittest.main(verbosity=2)
   1419