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