1# Common tests for test_tkinter/test_widgets.py and test_ttk/test_widgets.py
2
3import unittest
4import sys
5import Tkinter as tkinter
6from ttk import Scale
7from test_ttk.support import (AbstractTkTest, tcl_version, requires_tcl,
8                              get_tk_patchlevel, pixels_conv, tcl_obj_eq)
9import test.test_support
10
11
12noconv = noconv_meth = False
13if get_tk_patchlevel() < (8, 5, 11):
14    noconv = str
15noconv_meth = noconv and staticmethod(noconv)
16
17def int_round(x):
18    return int(round(x))
19
20pixels_round = int_round
21if get_tk_patchlevel()[:3] == (8, 5, 11):
22    # Issue #19085: Workaround a bug in Tk
23    # http://core.tcl.tk/tk/info/3497848
24    pixels_round = int
25
26
27_sentinel = object()
28
29class AbstractWidgetTest(AbstractTkTest):
30    _conv_pixels = staticmethod(pixels_round)
31    _conv_pad_pixels = None
32    _stringify = False
33
34    @property
35    def scaling(self):
36        try:
37            return self._scaling
38        except AttributeError:
39            self._scaling = float(self.root.call('tk', 'scaling'))
40            return self._scaling
41
42    def _str(self, value):
43        if not self._stringify and self.wantobjects and tcl_version >= (8, 6):
44            return value
45        if isinstance(value, tuple):
46            return ' '.join(map(self._str, value))
47        return str(value)
48
49    def assertEqual2(self, actual, expected, msg=None, eq=object.__eq__):
50        if eq(actual, expected):
51            return
52        self.assertEqual(actual, expected, msg)
53
54    def checkParam(self, widget, name, value, expected=_sentinel,
55                   conv=False, eq=None):
56        widget[name] = value
57        if expected is _sentinel:
58            expected = value
59        if conv:
60            expected = conv(expected)
61        if self._stringify or not self.wantobjects:
62            if isinstance(expected, tuple):
63                expected = tkinter._join(expected)
64            else:
65                expected = str(expected)
66        if eq is None:
67            eq = tcl_obj_eq
68        self.assertEqual2(widget[name], expected, eq=eq)
69        self.assertEqual2(widget.cget(name), expected, eq=eq)
70        # XXX
71        if not isinstance(widget, Scale):
72            t = widget.configure(name)
73            self.assertEqual(len(t), 5)
74            self.assertEqual2(t[4], expected, eq=eq)
75
76    def checkInvalidParam(self, widget, name, value, errmsg=None,
77                          keep_orig=True):
78        orig = widget[name]
79        if errmsg is not None:
80            errmsg = errmsg.format(value)
81        with self.assertRaises(tkinter.TclError) as cm:
82            widget[name] = value
83        if errmsg is not None:
84            self.assertEqual(str(cm.exception), errmsg)
85        if keep_orig:
86            self.assertEqual(widget[name], orig)
87        else:
88            widget[name] = orig
89        with self.assertRaises(tkinter.TclError) as cm:
90            widget.configure({name: value})
91        if errmsg is not None:
92            self.assertEqual(str(cm.exception), errmsg)
93        if keep_orig:
94            self.assertEqual(widget[name], orig)
95        else:
96            widget[name] = orig
97
98    def checkParams(self, widget, name, *values, **kwargs):
99        for value in values:
100            self.checkParam(widget, name, value, **kwargs)
101
102    def checkIntegerParam(self, widget, name, *values, **kwargs):
103        self.checkParams(widget, name, *values, **kwargs)
104        self.checkInvalidParam(widget, name, '',
105                errmsg='expected integer but got ""')
106        self.checkInvalidParam(widget, name, '10p',
107                errmsg='expected integer but got "10p"')
108        self.checkInvalidParam(widget, name, 3.2,
109                errmsg='expected integer but got "3.2"')
110
111    def checkFloatParam(self, widget, name, *values, **kwargs):
112        if 'conv' in kwargs:
113            conv = kwargs.pop('conv')
114        else:
115            conv = float
116        for value in values:
117            self.checkParam(widget, name, value, conv=conv, **kwargs)
118        self.checkInvalidParam(widget, name, '',
119                errmsg='expected floating-point number but got ""')
120        self.checkInvalidParam(widget, name, 'spam',
121                errmsg='expected floating-point number but got "spam"')
122
123    def checkBooleanParam(self, widget, name):
124        for value in (False, 0, 'false', 'no', 'off'):
125            self.checkParam(widget, name, value, expected=0)
126        for value in (True, 1, 'true', 'yes', 'on'):
127            self.checkParam(widget, name, value, expected=1)
128        self.checkInvalidParam(widget, name, '',
129                errmsg='expected boolean value but got ""')
130        self.checkInvalidParam(widget, name, 'spam',
131                errmsg='expected boolean value but got "spam"')
132
133    def checkColorParam(self, widget, name, allow_empty=None, **kwargs):
134        self.checkParams(widget, name,
135                         '#ff0000', '#00ff00', '#0000ff', '#123456',
136                         'red', 'green', 'blue', 'white', 'black', 'grey',
137                         **kwargs)
138        self.checkInvalidParam(widget, name, 'spam',
139                errmsg='unknown color name "spam"')
140
141    def checkCursorParam(self, widget, name, **kwargs):
142        self.checkParams(widget, name, 'arrow', 'watch', 'cross', '',**kwargs)
143        if tcl_version >= (8, 5):
144            self.checkParam(widget, name, 'none')
145        self.checkInvalidParam(widget, name, 'spam',
146                errmsg='bad cursor spec "spam"')
147
148    def checkCommandParam(self, widget, name):
149        def command(*args):
150            pass
151        widget[name] = command
152        self.assertTrue(widget[name])
153        self.checkParams(widget, name, '')
154
155    def checkEnumParam(self, widget, name, *values, **kwargs):
156        if 'errmsg' in kwargs:
157            errmsg = kwargs.pop('errmsg')
158        else:
159            errmsg = None
160        self.checkParams(widget, name, *values, **kwargs)
161        if errmsg is None:
162            errmsg2 = ' %s "{}": must be %s%s or %s' % (
163                    name,
164                    ', '.join(values[:-1]),
165                    ',' if len(values) > 2 else '',
166                    values[-1])
167            self.checkInvalidParam(widget, name, '',
168                                   errmsg='ambiguous' + errmsg2)
169            errmsg = 'bad' + errmsg2
170        self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg)
171
172    def checkPixelsParam(self, widget, name, *values, **kwargs):
173        if 'conv' in kwargs:
174            conv = kwargs.pop('conv')
175        else:
176            conv = None
177        if conv is None:
178            conv = self._conv_pixels
179        if 'keep_orig' in kwargs:
180            keep_orig = kwargs.pop('keep_orig')
181        else:
182            keep_orig = True
183        for value in values:
184            expected = _sentinel
185            conv1 = conv
186            if isinstance(value, str):
187                if conv1 and conv1 is not str:
188                    expected = pixels_conv(value) * self.scaling
189                    conv1 = int_round
190            self.checkParam(widget, name, value, expected=expected,
191                            conv=conv1, **kwargs)
192        self.checkInvalidParam(widget, name, '6x',
193                errmsg='bad screen distance "6x"', keep_orig=keep_orig)
194        self.checkInvalidParam(widget, name, 'spam',
195                errmsg='bad screen distance "spam"', keep_orig=keep_orig)
196
197    def checkReliefParam(self, widget, name):
198        self.checkParams(widget, name,
199                         'flat', 'groove', 'raised', 'ridge', 'solid', 'sunken')
200        errmsg='bad relief "spam": must be '\
201               'flat, groove, raised, ridge, solid, or sunken'
202        if tcl_version < (8, 6):
203            errmsg = None
204        self.checkInvalidParam(widget, name, 'spam',
205                errmsg=errmsg)
206
207    def checkImageParam(self, widget, name):
208        image = tkinter.PhotoImage(master=self.root, name='image1')
209        self.checkParam(widget, name, image, conv=str)
210        self.checkInvalidParam(widget, name, 'spam',
211                errmsg='image "spam" doesn\'t exist')
212        widget[name] = ''
213
214    def checkVariableParam(self, widget, name, var):
215        self.checkParam(widget, name, var, conv=str)
216
217    def assertIsBoundingBox(self, bbox):
218        self.assertIsNotNone(bbox)
219        self.assertIsInstance(bbox, tuple)
220        if len(bbox) != 4:
221            self.fail('Invalid bounding box: %r' % (bbox,))
222        for item in bbox:
223            if not isinstance(item, int):
224                self.fail('Invalid bounding box: %r' % (bbox,))
225                break
226
227    def test_keys(self):
228        widget = self.create()
229        keys = widget.keys()
230        # XXX
231        if not isinstance(widget, Scale):
232            self.assertEqual(sorted(keys), sorted(widget.configure()))
233        for k in keys:
234            widget[k]
235        # Test if OPTIONS contains all keys
236        if test.test_support.verbose:
237            aliases = {
238                'bd': 'borderwidth',
239                'bg': 'background',
240                'fg': 'foreground',
241                'invcmd': 'invalidcommand',
242                'vcmd': 'validatecommand',
243            }
244            keys = set(keys)
245            expected = set(self.OPTIONS)
246            for k in sorted(keys - expected):
247                if not (k in aliases and
248                        aliases[k] in keys and
249                        aliases[k] in expected):
250                    print('%s.OPTIONS doesn\'t contain "%s"' %
251                          (self.__class__.__name__, k))
252
253
254class StandardOptionsTests(object):
255    STANDARD_OPTIONS = (
256        'activebackground', 'activeborderwidth', 'activeforeground', 'anchor',
257        'background', 'bitmap', 'borderwidth', 'compound', 'cursor',
258        'disabledforeground', 'exportselection', 'font', 'foreground',
259        'highlightbackground', 'highlightcolor', 'highlightthickness',
260        'image', 'insertbackground', 'insertborderwidth',
261        'insertofftime', 'insertontime', 'insertwidth',
262        'jump', 'justify', 'orient', 'padx', 'pady', 'relief',
263        'repeatdelay', 'repeatinterval',
264        'selectbackground', 'selectborderwidth', 'selectforeground',
265        'setgrid', 'takefocus', 'text', 'textvariable', 'troughcolor',
266        'underline', 'wraplength', 'xscrollcommand', 'yscrollcommand',
267    )
268
269    def test_activebackground(self):
270        widget = self.create()
271        self.checkColorParam(widget, 'activebackground')
272
273    def test_activeborderwidth(self):
274        widget = self.create()
275        self.checkPixelsParam(widget, 'activeborderwidth',
276                              0, 1.3, 2.9, 6, -2, '10p')
277
278    def test_activeforeground(self):
279        widget = self.create()
280        self.checkColorParam(widget, 'activeforeground')
281
282    def test_anchor(self):
283        widget = self.create()
284        self.checkEnumParam(widget, 'anchor',
285                'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'center')
286
287    def test_background(self):
288        widget = self.create()
289        self.checkColorParam(widget, 'background')
290        if 'bg' in self.OPTIONS:
291            self.checkColorParam(widget, 'bg')
292
293    def test_bitmap(self):
294        widget = self.create()
295        self.checkParam(widget, 'bitmap', 'questhead')
296        self.checkParam(widget, 'bitmap', 'gray50')
297        filename = test.test_support.findfile('python.xbm', subdir='imghdrdata')
298        self.checkParam(widget, 'bitmap', '@' + filename)
299        # Cocoa Tk widgets don't detect invalid -bitmap values
300        # See https://core.tcl.tk/tk/info/31cd33dbf0
301        if not ('aqua' in self.root.tk.call('tk', 'windowingsystem') and
302                'AppKit' in self.root.winfo_server()):
303            self.checkInvalidParam(widget, 'bitmap', 'spam',
304                    errmsg='bitmap "spam" not defined')
305
306    def test_borderwidth(self):
307        widget = self.create()
308        self.checkPixelsParam(widget, 'borderwidth',
309                              0, 1.3, 2.6, 6, -2, '10p')
310        if 'bd' in self.OPTIONS:
311            self.checkPixelsParam(widget, 'bd', 0, 1.3, 2.6, 6, -2, '10p')
312
313    def test_compound(self):
314        widget = self.create()
315        self.checkEnumParam(widget, 'compound',
316                'bottom', 'center', 'left', 'none', 'right', 'top')
317
318    def test_cursor(self):
319        widget = self.create()
320        self.checkCursorParam(widget, 'cursor')
321
322    def test_disabledforeground(self):
323        widget = self.create()
324        self.checkColorParam(widget, 'disabledforeground')
325
326    def test_exportselection(self):
327        widget = self.create()
328        self.checkBooleanParam(widget, 'exportselection')
329
330    def test_font(self):
331        widget = self.create()
332        self.checkParam(widget, 'font',
333                        '-Adobe-Helvetica-Medium-R-Normal--*-120-*-*-*-*-*-*')
334        self.checkInvalidParam(widget, 'font', '',
335                               errmsg='font "" doesn\'t exist')
336
337    def test_foreground(self):
338        widget = self.create()
339        self.checkColorParam(widget, 'foreground')
340        if 'fg' in self.OPTIONS:
341            self.checkColorParam(widget, 'fg')
342
343    def test_highlightbackground(self):
344        widget = self.create()
345        self.checkColorParam(widget, 'highlightbackground')
346
347    def test_highlightcolor(self):
348        widget = self.create()
349        self.checkColorParam(widget, 'highlightcolor')
350
351    def test_highlightthickness(self):
352        widget = self.create()
353        self.checkPixelsParam(widget, 'highlightthickness',
354                              0, 1.3, 2.6, 6, '10p')
355        self.checkParam(widget, 'highlightthickness', -2, expected=0,
356                        conv=self._conv_pixels)
357
358    @unittest.skipIf(sys.platform == 'darwin',
359                     'crashes with Cocoa Tk (issue19733)')
360    def test_image(self):
361        widget = self.create()
362        self.checkImageParam(widget, 'image')
363
364    def test_insertbackground(self):
365        widget = self.create()
366        self.checkColorParam(widget, 'insertbackground')
367
368    def test_insertborderwidth(self):
369        widget = self.create()
370        self.checkPixelsParam(widget, 'insertborderwidth',
371                              0, 1.3, 2.6, 6, -2, '10p')
372
373    def test_insertofftime(self):
374        widget = self.create()
375        self.checkIntegerParam(widget, 'insertofftime', 100)
376
377    def test_insertontime(self):
378        widget = self.create()
379        self.checkIntegerParam(widget, 'insertontime', 100)
380
381    def test_insertwidth(self):
382        widget = self.create()
383        self.checkPixelsParam(widget, 'insertwidth', 1.3, 2.6, -2, '10p')
384
385    def test_jump(self):
386        widget = self.create()
387        self.checkBooleanParam(widget, 'jump')
388
389    def test_justify(self):
390        widget = self.create()
391        self.checkEnumParam(widget, 'justify', 'left', 'right', 'center',
392                errmsg='bad justification "{}": must be '
393                       'left, right, or center')
394        self.checkInvalidParam(widget, 'justify', '',
395                errmsg='ambiguous justification "": must be '
396                       'left, right, or center')
397
398    def test_orient(self):
399        widget = self.create()
400        self.assertEqual(str(widget['orient']), self.default_orient)
401        self.checkEnumParam(widget, 'orient', 'horizontal', 'vertical')
402
403    def test_padx(self):
404        widget = self.create()
405        self.checkPixelsParam(widget, 'padx', 3, 4.4, 5.6, -2, '12m',
406                              conv=self._conv_pad_pixels)
407
408    def test_pady(self):
409        widget = self.create()
410        self.checkPixelsParam(widget, 'pady', 3, 4.4, 5.6, -2, '12m',
411                              conv=self._conv_pad_pixels)
412
413    def test_relief(self):
414        widget = self.create()
415        self.checkReliefParam(widget, 'relief')
416
417    def test_repeatdelay(self):
418        widget = self.create()
419        self.checkIntegerParam(widget, 'repeatdelay', -500, 500)
420
421    def test_repeatinterval(self):
422        widget = self.create()
423        self.checkIntegerParam(widget, 'repeatinterval', -500, 500)
424
425    def test_selectbackground(self):
426        widget = self.create()
427        self.checkColorParam(widget, 'selectbackground')
428
429    def test_selectborderwidth(self):
430        widget = self.create()
431        self.checkPixelsParam(widget, 'selectborderwidth', 1.3, 2.6, -2, '10p')
432
433    def test_selectforeground(self):
434        widget = self.create()
435        self.checkColorParam(widget, 'selectforeground')
436
437    def test_setgrid(self):
438        widget = self.create()
439        self.checkBooleanParam(widget, 'setgrid')
440
441    def test_state(self):
442        widget = self.create()
443        self.checkEnumParam(widget, 'state', 'active', 'disabled', 'normal')
444
445    def test_takefocus(self):
446        widget = self.create()
447        self.checkParams(widget, 'takefocus', '0', '1', '')
448
449    def test_text(self):
450        widget = self.create()
451        self.checkParams(widget, 'text', '', 'any string')
452
453    def test_textvariable(self):
454        widget = self.create()
455        var = tkinter.StringVar(self.root)
456        self.checkVariableParam(widget, 'textvariable', var)
457
458    def test_troughcolor(self):
459        widget = self.create()
460        self.checkColorParam(widget, 'troughcolor')
461
462    def test_underline(self):
463        widget = self.create()
464        self.checkIntegerParam(widget, 'underline', 0, 1, 10)
465
466    def test_wraplength(self):
467        widget = self.create()
468        self.checkPixelsParam(widget, 'wraplength', 100)
469
470    def test_xscrollcommand(self):
471        widget = self.create()
472        self.checkCommandParam(widget, 'xscrollcommand')
473
474    def test_yscrollcommand(self):
475        widget = self.create()
476        self.checkCommandParam(widget, 'yscrollcommand')
477
478    # non-standard but common options
479
480    def test_command(self):
481        widget = self.create()
482        self.checkCommandParam(widget, 'command')
483
484    def test_indicatoron(self):
485        widget = self.create()
486        self.checkBooleanParam(widget, 'indicatoron')
487
488    def test_offrelief(self):
489        widget = self.create()
490        self.checkReliefParam(widget, 'offrelief')
491
492    def test_overrelief(self):
493        widget = self.create()
494        self.checkReliefParam(widget, 'overrelief')
495
496    def test_selectcolor(self):
497        widget = self.create()
498        self.checkColorParam(widget, 'selectcolor')
499
500    def test_selectimage(self):
501        widget = self.create()
502        self.checkImageParam(widget, 'selectimage')
503
504    @requires_tcl(8, 5)
505    def test_tristateimage(self):
506        widget = self.create()
507        self.checkImageParam(widget, 'tristateimage')
508
509    @requires_tcl(8, 5)
510    def test_tristatevalue(self):
511        widget = self.create()
512        self.checkParam(widget, 'tristatevalue', 'unknowable')
513
514    def test_variable(self):
515        widget = self.create()
516        var = tkinter.DoubleVar(self.root)
517        self.checkVariableParam(widget, 'variable', var)
518
519
520class IntegerSizeTests(object):
521    def test_height(self):
522        widget = self.create()
523        self.checkIntegerParam(widget, 'height', 100, -100, 0)
524
525    def test_width(self):
526        widget = self.create()
527        self.checkIntegerParam(widget, 'width', 402, -402, 0)
528
529
530class PixelSizeTests(object):
531    def test_height(self):
532        widget = self.create()
533        self.checkPixelsParam(widget, 'height', 100, 101.2, 102.6, -100, 0, '3c')
534
535    def test_width(self):
536        widget = self.create()
537        self.checkPixelsParam(widget, 'width', 402, 403.4, 404.6, -402, 0, '5i')
538
539
540def add_standard_options(*source_classes):
541    # This decorator adds test_xxx methods from source classes for every xxx
542    # option in the OPTIONS class attribute if they are not defined explicitly.
543    def decorator(cls):
544        for option in cls.OPTIONS:
545            methodname = 'test_' + option
546            if not hasattr(cls, methodname):
547                for source_class in source_classes:
548                    if hasattr(source_class, methodname):
549                        setattr(cls, methodname,
550                                getattr(source_class, methodname).im_func)
551                        break
552                else:
553                    def test(self, option=option):
554                        widget = self.create()
555                        widget[option]
556                        raise AssertionError('Option "%s" is not tested in %s' %
557                                             (option, cls.__name__))
558                    test.__name__ = methodname
559                    setattr(cls, methodname, test)
560        return cls
561    return decorator
562
563def setUpModule():
564    if test.test_support.verbose:
565        tcl = tkinter.Tcl()
566        print 'patchlevel =', tcl.call('info', 'patchlevel')
567