1"""Run human tests of Idle's window, dialog, and popup widgets. 2 3run(*tests) Create a master Tk() htest window. Within that, run each 4callable in tests after finding the matching test spec in this file. If 5tests is empty, run an htest for each spec dict in this file after 6finding the matching callable in the module named in the spec. Close 7the master window to end testing. 8 9In a tested module, let X be a global name bound to a callable (class or 10function) whose .__name__ attribute is also X (the usual situation). The 11first parameter of X must be 'parent' or 'master'. When called, the 12first argument will be the root window. X must create a child 13Toplevel(parent/master) (or subclass thereof). The Toplevel may be a 14test widget or dialog, in which case the callable is the corresponding 15class. Or the Toplevel may contain the widget to be tested or set up a 16context in which a test widget is invoked. In this latter case, the 17callable is a wrapper function that sets up the Toplevel and other 18objects. Wrapper function names, such as _editor_window', should start 19with '_' and be lowercase. 20 21 22End the module with 23 24if __name__ == '__main__': 25 <run unittest.main with 'exit=False'> 26 from idlelib.idle_test.htest import run 27 run(callable) # There could be multiple comma-separated callables. 28 29To have wrapper functions ignored by coverage reports, tag the def 30header like so: "def _wrapper(parent): # htest #". Use the same tag 31for htest lines in widget code. Make sure that the 'if __name__' line 32matches the above. Then have make sure that .coveragerc includes the 33following: 34 35[report] 36exclude_lines = 37 .*# htest # 38 if __name__ == .__main__.: 39 40(The "." instead of "'" is intentional and necessary.) 41 42 43To run any X, this file must contain a matching instance of the 44following template, with X.__name__ prepended to '_spec'. 45When all tests are run, the prefix is use to get X. 46 47callable_spec = { 48 'file': '', 49 'kwds': {'title': ''}, 50 'msg': "" 51 } 52 53file (no .py): run() imports file.py. 54kwds: augmented with {'parent':root} and passed to X as **kwds. 55title: an example kwd; some widgets need this, delete line if not. 56msg: master window hints about testing the widget. 57 58 59TODO test these modules and classes: 60 autocomplete_w.AutoCompleteWindow 61 debugger.Debugger 62 outwin.OutputWindow (indirectly being tested with grep test) 63 pyshell.PyShellEditorWindow 64""" 65 66import idlelib.pyshell # Set Windows DPI awareness before Tk(). 67from importlib import import_module 68import textwrap 69import tkinter as tk 70from tkinter.ttk import Scrollbar 71tk.NoDefaultRoot() 72 73AboutDialog_spec = { 74 'file': 'help_about', 75 'kwds': {'title': 'help_about test', 76 '_htest': True, 77 }, 78 'msg': "Click on URL to open in default browser.\n" 79 "Verify x.y.z versions and test each button, including Close.\n " 80 } 81 82# TODO implement ^\; adding '<Control-Key-\\>' to function does not work. 83_calltip_window_spec = { 84 'file': 'calltip_w', 85 'kwds': {}, 86 'msg': "Typing '(' should display a calltip.\n" 87 "Typing ') should hide the calltip.\n" 88 "So should moving cursor out of argument area.\n" 89 "Force-open-calltip does not work here.\n" 90 } 91 92_color_delegator_spec = { 93 'file': 'colorizer', 94 'kwds': {}, 95 'msg': "The text is sample Python code.\n" 96 "Ensure components like comments, keywords, builtins,\n" 97 "string, definitions, and break are correctly colored.\n" 98 "The default color scheme is in idlelib/config-highlight.def" 99 } 100 101ConfigDialog_spec = { 102 'file': 'configdialog', 103 'kwds': {'title': 'ConfigDialogTest', 104 '_htest': True,}, 105 'msg': "IDLE preferences dialog.\n" 106 "In the 'Fonts/Tabs' tab, changing font face, should update the " 107 "font face of the text in the area below it.\nIn the " 108 "'Highlighting' tab, try different color schemes. Clicking " 109 "items in the sample program should update the choices above it." 110 "\nIn the 'Keys', 'General' and 'Extensions' tabs, test settings " 111 "of interest." 112 "\n[Ok] to close the dialog.[Apply] to apply the settings and " 113 "and [Cancel] to revert all changes.\nRe-run the test to ensure " 114 "changes made have persisted." 115 } 116 117CustomRun_spec = { 118 'file': 'query', 119 'kwds': {'title': 'Customize query.py Run', 120 '_htest': True}, 121 'msg': "Enter with <Return> or [OK]. Print valid entry to Shell\n" 122 "Arguments are parsed into a list\n" 123 "Mode is currently restart True or False\n" 124 "Close dialog with valid entry, <Escape>, [Cancel], [X]" 125 } 126 127_debug_object_browser_spec = { 128 'file': 'debugobj', 129 'kwds': {}, 130 'msg': "Double click on items up to the lowest level.\n" 131 "Attributes of the objects and related information " 132 "will be displayed side-by-side at each level." 133 } 134 135# TODO Improve message 136_dyn_option_menu_spec = { 137 'file': 'dynoption', 138 'kwds': {}, 139 'msg': "Select one of the many options in the 'old option set'.\n" 140 "Click the button to change the option set.\n" 141 "Select one of the many options in the 'new option set'." 142 } 143 144# TODO edit wrapper 145_editor_window_spec = { 146 'file': 'editor', 147 'kwds': {}, 148 'msg': "Test editor functions of interest.\n" 149 "Best to close editor first." 150 } 151 152GetKeysWindow_spec = { 153 'file': 'config_key', 154 'kwds': {'title': 'Test keybindings', 155 'action': 'find-again', 156 'current_key_sequences': [['<Control-Key-g>', '<Key-F3>', '<Control-Key-G>']], 157 '_htest': True, 158 }, 159 'msg': "Test for different key modifier sequences.\n" 160 "<nothing> is invalid.\n" 161 "No modifier key is invalid.\n" 162 "Shift key with [a-z],[0-9], function key, move key, tab, space " 163 "is invalid.\nNo validity checking if advanced key binding " 164 "entry is used." 165 } 166 167_grep_dialog_spec = { 168 'file': 'grep', 169 'kwds': {}, 170 'msg': "Click the 'Show GrepDialog' button.\n" 171 "Test the various 'Find-in-files' functions.\n" 172 "The results should be displayed in a new '*Output*' window.\n" 173 "'Right-click'->'Go to file/line' in the search results\n " 174 "should open that file in a new EditorWindow." 175 } 176 177HelpSource_spec = { 178 'file': 'query', 179 'kwds': {'title': 'Help name and source', 180 'menuitem': 'test', 181 'filepath': __file__, 182 'used_names': {'abc'}, 183 '_htest': True}, 184 'msg': "Enter menu item name and help file path\n" 185 "'', > than 30 chars, and 'abc' are invalid menu item names.\n" 186 "'' and file does not exist are invalid path items.\n" 187 "Any url ('www...', 'http...') is accepted.\n" 188 "Test Browse with and without path, as cannot unittest.\n" 189 "[Ok] or <Return> prints valid entry to shell\n" 190 "<Escape>, [Cancel], or [X] prints None to shell" 191 } 192 193_io_binding_spec = { 194 'file': 'iomenu', 195 'kwds': {}, 196 'msg': "Test the following bindings.\n" 197 "<Control-o> to open file from dialog.\n" 198 "Edit the file.\n" 199 "<Control-p> to print the file.\n" 200 "<Control-s> to save the file.\n" 201 "<Alt-s> to save-as another file.\n" 202 "<Control-c> to save-copy-as another file.\n" 203 "Check that changes were saved by opening the file elsewhere." 204 } 205 206_multi_call_spec = { 207 'file': 'multicall', 208 'kwds': {}, 209 'msg': "The following should trigger a print to console or IDLE Shell.\n" 210 "Entering and leaving the text area, key entry, <Control-Key>,\n" 211 "<Alt-Key-a>, <Control-Key-a>, <Alt-Control-Key-a>, \n" 212 "<Control-Button-1>, <Alt-Button-1> and focusing elsewhere." 213 } 214 215_module_browser_spec = { 216 'file': 'browser', 217 'kwds': {}, 218 'msg': textwrap.dedent(""" 219 "Inspect names of module, class(with superclass if applicable), 220 "methods and functions. Toggle nested items. Double clicking 221 "on items prints a traceback for an exception that is ignored.""") 222 } 223 224_multistatus_bar_spec = { 225 'file': 'statusbar', 226 'kwds': {}, 227 'msg': "Ensure presence of multi-status bar below text area.\n" 228 "Click 'Update Status' to change the status text" 229 } 230 231PathBrowser_spec = { 232 'file': 'pathbrowser', 233 'kwds': {'_htest': True}, 234 'msg': "Test for correct display of all paths in sys.path.\n" 235 "Toggle nested items out to the lowest level.\n" 236 "Double clicking on an item prints a traceback\n" 237 "for an exception that is ignored." 238 } 239 240_percolator_spec = { 241 'file': 'percolator', 242 'kwds': {}, 243 'msg': "There are two tracers which can be toggled using a checkbox.\n" 244 "Toggling a tracer 'on' by checking it should print tracer " 245 "output to the console or to the IDLE shell.\n" 246 "If both the tracers are 'on', the output from the tracer which " 247 "was switched 'on' later, should be printed first\n" 248 "Test for actions like text entry, and removal." 249 } 250 251Query_spec = { 252 'file': 'query', 253 'kwds': {'title': 'Query', 254 'message': 'Enter something', 255 'text0': 'Go', 256 '_htest': True}, 257 'msg': "Enter with <Return> or [Ok]. Print valid entry to Shell\n" 258 "Blank line, after stripping, is ignored\n" 259 "Close dialog with valid entry, <Escape>, [Cancel], [X]" 260 } 261 262 263_replace_dialog_spec = { 264 'file': 'replace', 265 'kwds': {}, 266 'msg': "Click the 'Replace' button.\n" 267 "Test various replace options in the 'Replace dialog'.\n" 268 "Click [Close] or [X] to close the 'Replace Dialog'." 269 } 270 271_scrolled_list_spec = { 272 'file': 'scrolledlist', 273 'kwds': {}, 274 'msg': "You should see a scrollable list of items\n" 275 "Selecting (clicking) or double clicking an item " 276 "prints the name to the console or Idle shell.\n" 277 "Right clicking an item will display a popup." 278 } 279 280_search_dialog_spec = { 281 'file': 'search', 282 'kwds': {}, 283 'msg': "Click the 'Search' button.\n" 284 "Test various search options in the 'Search dialog'.\n" 285 "Click [Close] or [X] to close the 'Search Dialog'." 286 } 287 288_searchbase_spec = { 289 'file': 'searchbase', 290 'kwds': {}, 291 'msg': "Check the appearance of the base search dialog\n" 292 "Its only action is to close." 293 } 294 295show_idlehelp_spec = { 296 'file': 'help', 297 'kwds': {}, 298 'msg': "If the help text displays, this works.\n" 299 "Text is selectable. Window is scrollable." 300 } 301 302_sidebar_number_scrolling_spec = { 303 'file': 'sidebar', 304 'kwds': {}, 305 'msg': textwrap.dedent("""\ 306 1. Click on the line numbers and drag down below the edge of the 307 window, moving the mouse a bit and then leaving it there for a 308 while. The text and line numbers should gradually scroll down, 309 with the selection updated continuously. 310 311 2. With the lines still selected, click on a line number above 312 or below the selected lines. Only the line whose number was 313 clicked should be selected. 314 315 3. Repeat step #1, dragging to above the window. The text and 316 line numbers should gradually scroll up, with the selection 317 updated continuously. 318 319 4. Repeat step #2, clicking a line number below the selection."""), 320 } 321 322_stackbrowser_spec = { 323 'file': 'stackviewer', 324 'kwds': {}, 325 'msg': "A stacktrace for a NameError exception.\n" 326 "Should have NameError and 1 traceback line." 327 } 328 329_tooltip_spec = { 330 'file': 'tooltip', 331 'kwds': {}, 332 'msg': "Place mouse cursor over both the buttons\n" 333 "A tooltip should appear with some text." 334 } 335 336_tree_widget_spec = { 337 'file': 'tree', 338 'kwds': {}, 339 'msg': "The canvas is scrollable.\n" 340 "Click on folders up to to the lowest level." 341 } 342 343_undo_delegator_spec = { 344 'file': 'undo', 345 'kwds': {}, 346 'msg': "Click [Undo] to undo any action.\n" 347 "Click [Redo] to redo any action.\n" 348 "Click [Dump] to dump the current state " 349 "by printing to the console or the IDLE shell.\n" 350 } 351 352ViewWindow_spec = { 353 'file': 'textview', 354 'kwds': {'title': 'Test textview', 355 'contents': 'The quick brown fox jumps over the lazy dog.\n'*35, 356 '_htest': True}, 357 'msg': "Test for read-only property of text.\n" 358 "Select text, scroll window, close" 359 } 360 361_widget_redirector_spec = { 362 'file': 'redirector', 363 'kwds': {}, 364 'msg': "Every text insert should be printed to the console " 365 "or the IDLE shell." 366 } 367 368def run(*tests): 369 "Run callables in tests." 370 root = tk.Tk() 371 root.title('IDLE htest') 372 root.resizable(0, 0) 373 374 # A scrollable Label-like constant width text widget. 375 frameLabel = tk.Frame(root, padx=10) 376 frameLabel.pack() 377 text = tk.Text(frameLabel, wrap='word') 378 text.configure(bg=root.cget('bg'), relief='flat', height=4, width=70) 379 scrollbar = Scrollbar(frameLabel, command=text.yview) 380 text.config(yscrollcommand=scrollbar.set) 381 scrollbar.pack(side='right', fill='y', expand=False) 382 text.pack(side='left', fill='both', expand=True) 383 384 test_list = [] # Make list of (spec, callable) tuples. 385 if tests: 386 for test in tests: 387 test_spec = globals()[test.__name__ + '_spec'] 388 test_spec['name'] = test.__name__ 389 test_list.append((test_spec, test)) 390 else: 391 for key, dic in globals().items(): 392 if key.endswith('_spec'): 393 test_name = key[:-5] 394 test_spec = dic 395 test_spec['name'] = test_name 396 mod = import_module('idlelib.' + test_spec['file']) 397 test = getattr(mod, test_name) 398 test_list.append((test_spec, test)) 399 test_list.reverse() # So can pop in proper order in next_test. 400 401 test_name = tk.StringVar(root) 402 callable_object = None 403 test_kwds = None 404 405 def next_test(): 406 nonlocal test_name, callable_object, test_kwds 407 if len(test_list) == 1: 408 next_button.pack_forget() 409 test_spec, callable_object = test_list.pop() 410 test_kwds = test_spec['kwds'] 411 test_name.set('Test ' + test_spec['name']) 412 413 text['state'] = 'normal' # Enable text replacement. 414 text.delete('1.0', 'end') 415 text.insert("1.0", test_spec['msg']) 416 text['state'] = 'disabled' # Restore read-only property. 417 418 def run_test(_=None): 419 widget = callable_object(root, **test_kwds) 420 try: 421 print(widget.result) # Only true for query classes(?). 422 except AttributeError: 423 pass 424 425 def close(_=None): 426 root.destroy() 427 428 button = tk.Button(root, textvariable=test_name, 429 default='active', command=run_test) 430 next_button = tk.Button(root, text="Next", command=next_test) 431 button.pack() 432 next_button.pack() 433 next_button.focus_set() 434 root.bind('<Key-Return>', run_test) 435 root.bind('<Key-Escape>', close) 436 437 next_test() 438 root.mainloop() 439 440 441if __name__ == '__main__': 442 run() 443