python (3.11.7)
       1  """Run human tests of Idle's window, dialog, and popup widgets.
       2  
       3  run(*tests) Create a master Tk() htest window.  Within that, run each
       4  callable in tests after finding the matching test spec in this file.  If
       5  tests is empty, run an htest for each spec dict in this file after
       6  finding the matching callable in the module named in the spec.  Close
       7  the master window to end testing.
       8  
       9  In a tested module, let X be a global name bound to a callable (class or
      10  function) whose .__name__ attribute is also X (the usual situation). The
      11  first parameter of X must be 'parent' or 'master'.  When called, the
      12  first argument will be the root window.  X must create a child
      13  Toplevel(parent/master) (or subclass thereof).  The Toplevel may be a
      14  test widget or dialog, in which case the callable is the corresponding
      15  class.  Or the Toplevel may contain the widget to be tested or set up a
      16  context in which a test widget is invoked.  In this latter case, the
      17  callable is a wrapper function that sets up the Toplevel and other
      18  objects.  Wrapper function names, such as _editor_window', should start
      19  with '_' and be lowercase.
      20  
      21  
      22  End the module with
      23  
      24  if __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  
      29  To have wrapper functions ignored by coverage reports, tag the def
      30  header like so: "def _wrapper(parent):  # htest #".  Use the same tag
      31  for htest lines in widget code.  Make sure that the 'if __name__' line
      32  matches the above.  Then have make sure that .coveragerc includes the
      33  following:
      34  
      35  [report]
      36  exclude_lines =
      37      .*# htest #
      38      if __name__ == .__main__.:
      39  
      40  (The "." instead of "'" is intentional and necessary.)
      41  
      42  
      43  To run any X, this file must contain a matching instance of the
      44  following template, with X.__name__ prepended to '_spec'.
      45  When all tests are run, the prefix is use to get X.
      46  
      47  callable_spec = {
      48      'file': '',
      49      'kwds': {'title': ''},
      50      'msg': ""
      51      }
      52  
      53  file (no .py): run() imports file.py.
      54  kwds: augmented with {'parent':root} and passed to X as **kwds.
      55  title: an example kwd; some widgets need this, delete line if not.
      56  msg: master window hints about testing the widget.
      57  
      58  
      59  TODO 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  
      66  import idlelib.pyshell  # Set Windows DPI awareness before Tk().
      67  from importlib import import_module
      68  import textwrap
      69  import tkinter as tk
      70  from tkinter.ttk import Scrollbar
      71  tk.NoDefaultRoot()
      72  
      73  AboutDialog_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  
     101  ConfigDialog_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  
     117  CustomRun_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  
     152  GetKeysWindow_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  
     177  HelpSource_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  _helpwindow_spec = {
     194      'file': 'help',
     195      'kwds': {},
     196      'msg': "If the help text displays, this works.\n"
     197             "Text is selectable. Window is scrollable."
     198      }
     199  
     200  _io_binding_spec = {
     201      'file': 'iomenu',
     202      'kwds': {},
     203      'msg': "Test the following bindings.\n"
     204             "<Control-o> to open file from dialog.\n"
     205             "Edit the file.\n"
     206             "<Control-p> to print the file.\n"
     207             "<Control-s> to save the file.\n"
     208             "<Alt-s> to save-as another file.\n"
     209             "<Control-c> to save-copy-as another file.\n"
     210             "Check that changes were saved by opening the file elsewhere."
     211      }
     212  
     213  _multi_call_spec = {
     214      'file': 'multicall',
     215      'kwds': {},
     216      'msg': "The following should trigger a print to console or IDLE Shell.\n"
     217             "Entering and leaving the text area, key entry, <Control-Key>,\n"
     218             "<Alt-Key-a>, <Control-Key-a>, <Alt-Control-Key-a>, \n"
     219             "<Control-Button-1>, <Alt-Button-1> and focusing elsewhere."
     220      }
     221  
     222  _module_browser_spec = {
     223      'file': 'browser',
     224      'kwds': {},
     225      'msg': textwrap.dedent("""
     226          "Inspect names of module, class(with superclass if applicable),
     227          "methods and functions.  Toggle nested items.  Double clicking
     228          "on items prints a traceback for an exception that is ignored.""")
     229      }
     230  
     231  _multistatus_bar_spec = {
     232      'file': 'statusbar',
     233      'kwds': {},
     234      'msg': "Ensure presence of multi-status bar below text area.\n"
     235             "Click 'Update Status' to change the status text"
     236      }
     237  
     238  PathBrowser_spec = {
     239      'file': 'pathbrowser',
     240      'kwds': {'_htest': True},
     241      'msg': "Test for correct display of all paths in sys.path.\n"
     242             "Toggle nested items out to the lowest level.\n"
     243             "Double clicking on an item prints a traceback\n"
     244             "for an exception that is ignored."
     245      }
     246  
     247  _percolator_spec = {
     248      'file': 'percolator',
     249      'kwds': {},
     250      'msg': "There are two tracers which can be toggled using a checkbox.\n"
     251             "Toggling a tracer 'on' by checking it should print tracer "
     252             "output to the console or to the IDLE shell.\n"
     253             "If both the tracers are 'on', the output from the tracer which "
     254             "was switched 'on' later, should be printed first\n"
     255             "Test for actions like text entry, and removal."
     256      }
     257  
     258  Query_spec = {
     259      'file': 'query',
     260      'kwds': {'title': 'Query',
     261               'message': 'Enter something',
     262               'text0': 'Go',
     263               '_htest': True},
     264      'msg': "Enter with <Return> or [Ok].  Print valid entry to Shell\n"
     265             "Blank line, after stripping, is ignored\n"
     266             "Close dialog with valid entry, <Escape>, [Cancel], [X]"
     267      }
     268  
     269  
     270  _replace_dialog_spec = {
     271      'file': 'replace',
     272      'kwds': {},
     273      'msg': "Click the 'Replace' button.\n"
     274             "Test various replace options in the 'Replace dialog'.\n"
     275             "Click [Close] or [X] to close the 'Replace Dialog'."
     276      }
     277  
     278  _scrolled_list_spec = {
     279      'file': 'scrolledlist',
     280      'kwds': {},
     281      'msg': "You should see a scrollable list of items\n"
     282             "Selecting (clicking) or double clicking an item "
     283             "prints the name to the console or Idle shell.\n"
     284             "Right clicking an item will display a popup."
     285      }
     286  
     287  _search_dialog_spec = {
     288      'file': 'search',
     289      'kwds': {},
     290      'msg': "Click the 'Search' button.\n"
     291             "Test various search options in the 'Search dialog'.\n"
     292             "Click [Close] or [X] to close the 'Search Dialog'."
     293      }
     294  
     295  _searchbase_spec = {
     296      'file': 'searchbase',
     297      'kwds': {},
     298      'msg': "Check the appearance of the base search dialog\n"
     299             "Its only action is to close."
     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  
     352  ViewWindow_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  
     368  def 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  
     441  if __name__ == '__main__':
     442      run()