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