1"""Test tooltip, coverage 100%.
2
3Coverage is 100% after excluding 6 lines with "# pragma: no cover".
4They involve TclErrors that either should or should not happen in a
5particular situation, and which are 'pass'ed if they do.
6"""
7
8from idlelib.tooltip import TooltipBase, Hovertip
9from test.support import requires
10requires('gui')
11
12from functools import wraps
13import time
14from tkinter import Button, Tk, Toplevel
15import unittest
16
17
18def setUpModule():
19    global root
20    root = Tk()
21
22def tearDownModule():
23    global root
24    root.update_idletasks()
25    root.destroy()
26    del root
27
28
29def add_call_counting(func):
30    @wraps(func)
31    def wrapped_func(*args, **kwargs):
32        wrapped_func.call_args_list.append((args, kwargs))
33        return func(*args, **kwargs)
34    wrapped_func.call_args_list = []
35    return wrapped_func
36
37
38def _make_top_and_button(testobj):
39    global root
40    top = Toplevel(root)
41    testobj.addCleanup(top.destroy)
42    top.title("Test tooltip")
43    button = Button(top, text='ToolTip test button')
44    button.pack()
45    testobj.addCleanup(button.destroy)
46    top.lift()
47    return top, button
48
49
50class ToolTipBaseTest(unittest.TestCase):
51    def setUp(self):
52        self.top, self.button = _make_top_and_button(self)
53
54    def test_base_class_is_unusable(self):
55        global root
56        top = Toplevel(root)
57        self.addCleanup(top.destroy)
58
59        button = Button(top, text='ToolTip test button')
60        button.pack()
61        self.addCleanup(button.destroy)
62
63        with self.assertRaises(NotImplementedError):
64            tooltip = TooltipBase(button)
65            tooltip.showtip()
66
67
68class HovertipTest(unittest.TestCase):
69    def setUp(self):
70        self.top, self.button = _make_top_and_button(self)
71
72    def is_tipwindow_shown(self, tooltip):
73        return tooltip.tipwindow and tooltip.tipwindow.winfo_viewable()
74
75    def test_showtip(self):
76        tooltip = Hovertip(self.button, 'ToolTip text')
77        self.addCleanup(tooltip.hidetip)
78        self.assertFalse(self.is_tipwindow_shown(tooltip))
79        tooltip.showtip()
80        self.assertTrue(self.is_tipwindow_shown(tooltip))
81
82    def test_showtip_twice(self):
83        tooltip = Hovertip(self.button, 'ToolTip text')
84        self.addCleanup(tooltip.hidetip)
85        self.assertFalse(self.is_tipwindow_shown(tooltip))
86        tooltip.showtip()
87        self.assertTrue(self.is_tipwindow_shown(tooltip))
88        orig_tipwindow = tooltip.tipwindow
89        tooltip.showtip()
90        self.assertTrue(self.is_tipwindow_shown(tooltip))
91        self.assertIs(tooltip.tipwindow, orig_tipwindow)
92
93    def test_hidetip(self):
94        tooltip = Hovertip(self.button, 'ToolTip text')
95        self.addCleanup(tooltip.hidetip)
96        tooltip.showtip()
97        tooltip.hidetip()
98        self.assertFalse(self.is_tipwindow_shown(tooltip))
99
100    def test_showtip_on_mouse_enter_no_delay(self):
101        tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=None)
102        self.addCleanup(tooltip.hidetip)
103        tooltip.showtip = add_call_counting(tooltip.showtip)
104        root.update()
105        self.assertFalse(self.is_tipwindow_shown(tooltip))
106        self.button.event_generate('<Enter>', x=0, y=0)
107        root.update()
108        self.assertTrue(self.is_tipwindow_shown(tooltip))
109        self.assertGreater(len(tooltip.showtip.call_args_list), 0)
110
111    def test_hover_with_delay(self):
112        # Run multiple tests requiring an actual delay simultaneously.
113
114        # Test #1: A hover tip with a non-zero delay appears after the delay.
115        tooltip1 = Hovertip(self.button, 'ToolTip text', hover_delay=100)
116        self.addCleanup(tooltip1.hidetip)
117        tooltip1.showtip = add_call_counting(tooltip1.showtip)
118        root.update()
119        self.assertFalse(self.is_tipwindow_shown(tooltip1))
120        self.button.event_generate('<Enter>', x=0, y=0)
121        root.update()
122        self.assertFalse(self.is_tipwindow_shown(tooltip1))
123
124        # Test #2: A hover tip with a non-zero delay doesn't appear when
125        # the mouse stops hovering over the base widget before the delay
126        # expires.
127        tooltip2 = Hovertip(self.button, 'ToolTip text', hover_delay=100)
128        self.addCleanup(tooltip2.hidetip)
129        tooltip2.showtip = add_call_counting(tooltip2.showtip)
130        root.update()
131        self.button.event_generate('<Enter>', x=0, y=0)
132        root.update()
133        self.button.event_generate('<Leave>', x=0, y=0)
134        root.update()
135
136        time.sleep(0.15)
137        root.update()
138
139        # Test #1 assertions.
140        self.assertTrue(self.is_tipwindow_shown(tooltip1))
141        self.assertGreater(len(tooltip1.showtip.call_args_list), 0)
142
143        # Test #2 assertions.
144        self.assertFalse(self.is_tipwindow_shown(tooltip2))
145        self.assertEqual(tooltip2.showtip.call_args_list, [])
146
147    def test_hidetip_on_mouse_leave(self):
148        tooltip = Hovertip(self.button, 'ToolTip text', hover_delay=None)
149        self.addCleanup(tooltip.hidetip)
150        tooltip.showtip = add_call_counting(tooltip.showtip)
151        root.update()
152        self.button.event_generate('<Enter>', x=0, y=0)
153        root.update()
154        self.button.event_generate('<Leave>', x=0, y=0)
155        root.update()
156        self.assertFalse(self.is_tipwindow_shown(tooltip))
157        self.assertGreater(len(tooltip.showtip.call_args_list), 0)
158
159
160if __name__ == '__main__':
161    unittest.main(verbosity=2)
162