diff --git a/traitsui/examples/demo/Standard_Editors/tests/test_TreeEditor_demo.py b/traitsui/examples/demo/Standard_Editors/tests/test_TreeEditor_demo.py new file mode 100644 index 000000000..8746482f1 --- /dev/null +++ b/traitsui/examples/demo/Standard_Editors/tests/test_TreeEditor_demo.py @@ -0,0 +1,85 @@ +# (C) Copyright 2004-2021 Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only under +# the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! + +""" +This example demonstrates how to test interacting with a TreeEditor. + +The GUI being tested is written in the demo under the same name (minus the +preceding 'test') in the outer directory. +""" + +import os +import runpy +import unittest + +from traitsui.testing.api import ( + DisplayedText, + KeyClick, + KeySequence, + MouseClick, + MouseDClick, + TreeNode, + UITester +) +from traitsui.tests._tools import requires_toolkit, ToolkitName + +#: Filename of the demo script +FILENAME = "TreeEditor_demo.py" + +#: Path of the demo script +DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME) + + +class TestTreeEditorDemo(unittest.TestCase): + + @requires_toolkit([ToolkitName.qt]) + def test_tree_editor_demo(self): + demo = runpy.run_path(DEMO_PATH)["demo"] + tester = UITester() + with tester.create_ui(demo) as ui: + root_actor = tester.find_by_name(ui, "company") + + # Enthought->Department->Business->(First employee) + node = root_actor.locate(TreeNode((0, 0, 0, 0), 0)) + node.perform(MouseClick()) + + name_actor = node.find_by_name("name") + for _ in range(5): + name_actor.perform(KeyClick("Backspace")) + name_actor.perform(KeySequence("James")) + self.assertEqual( + demo.company.departments[0].employees[0].name, + "James", + ) + + # Enthought->Department->Scientific + demo.company.departments[1].name = "Scientific Group" + node = root_actor.locate(TreeNode((0, 0, 1), 0)) + self.assertEqual( + node.inspect(DisplayedText()), "Scientific Group" + ) + + # Enthought->Department->Business + node = root_actor.locate(TreeNode((0, 0, 0), 0)) + node.perform(MouseClick()) + node.perform(MouseDClick()) + + name_actor = node.find_by_name("name") + name_actor.perform(KeySequence(" Group")) + self.assertEqual( + demo.company.departments[0].name, + "Business Group", + ) + + +# Run the test(s) +unittest.TextTestRunner().run( + unittest.TestLoader().loadTestsFromTestCase(TestTreeEditorDemo) +) diff --git a/traitsui/testing/api.py b/traitsui/testing/api.py index 0ff24d81b..e9e8cb456 100644 --- a/traitsui/testing/api.py +++ b/traitsui/testing/api.py @@ -22,6 +22,7 @@ - :class:`~.KeyClick` - :class:`~.KeySequence` - :class:`~.MouseClick` +- :class:`~.MouseDClick` Interactions (for getting GUI states) ------------------------------------- @@ -39,6 +40,8 @@ - :class:`~.TargetById` - :class:`~.TargetByName` - :class:`~.Textbox` +- :class:`~.TreeNode` +- :class:`~.SelectedText` Advanced usage -------------- @@ -63,6 +66,7 @@ # Interactions (for changing GUI states) from .tester.command import ( MouseClick, + MouseDClick, KeyClick, KeySequence ) @@ -82,6 +86,7 @@ TargetById, TargetByName, Textbox, + TreeNode, Slider ) diff --git a/traitsui/testing/tester/_ui_tester_registry/qt4/_interaction_helpers.py b/traitsui/testing/tester/_ui_tester_registry/qt4/_interaction_helpers.py index c87eed15a..2ea4211ea 100644 --- a/traitsui/testing/tester/_ui_tester_registry/qt4/_interaction_helpers.py +++ b/traitsui/testing/tester/_ui_tester_registry/qt4/_interaction_helpers.py @@ -205,6 +205,119 @@ def mouse_click_item_view(model, view, index, delay): ) +def mouse_dclick_item_view(model, view, index, delay): + """ Perform mouse double click on the given QAbstractItemModel (model) and + QAbstractItemView (view) with the given row and column. + Parameters + ---------- + model : QAbstractItemModel + Model from which QModelIndex will be obtained + view : QAbstractItemView + View from which the widget identified by the index will be + found and mouse double click be performed. + index : QModelIndex + Raises + ------ + LookupError + If the index cannot be located. + Note that the index error provides more + """ + check_q_model_index_valid(index) + rect = view.visualRect(index) + QTest.mouseDClick( + view.viewport(), + QtCore.Qt.LeftButton, + QtCore.Qt.NoModifier, + rect.center(), + delay=delay, + ) + + +def key_sequence_item_view(model, view, index, sequence, delay=0): + """ Perform Key Sequence on the given QAbstractItemModel (model) and + QAbstractItemView (view) with the given row and column. + Parameters + ---------- + model : QAbstractItemModel + Model from which QModelIndex will be obtained + view : QAbstractItemView + View from which the widget identified by the index will be + found and key sequence be performed. + index : QModelIndex + sequence : str + Sequence of characters to be inserted to the widget identifed + by the row and column. + Raises + ------ + Disabled + If the widget cannot be edited. + LookupError + If the index cannot be located. + Note that the index error provides more + """ + check_q_model_index_valid(index) + widget = view.indexWidget(index) + if widget is None: + raise Disabled( + "No editable widget for item at row {!r} and column {!r}".format( + index.row(), index.column() + ) + ) + QTest.keyClicks(widget, sequence, delay=delay) + + +def key_click_item_view(model, view, index, key, delay=0): + """ Perform key press on the given QAbstractItemModel (model) and + QAbstractItemView (view) with the given row and column. + Parameters + ---------- + model : QAbstractItemModel + Model from which QModelIndex will be obtained + view : QAbstractItemView + View from which the widget identified by the index will be + found and key press be performed. + index : int + key : str + Key to be pressed. + Raises + ------ + Disabled + If the widget cannot be edited. + LookupError + If the index cannot be located. + Note that the index error provides more + """ + check_q_model_index_valid(index) + widget = view.indexWidget(index) + if widget is None: + raise Disabled( + "No editable widget for item at row {!r} and column {!r}".format( + index.row(), index.column() + ) + ) + key_click(widget, key=key, delay=delay) + + +def get_display_text_item_view(model, view, index): + """ Return the textural representation for the given model, row and column. + Parameters + ---------- + model : QAbstractItemModel + Model from which QModelIndex will be obtained + view : QAbstractItemView + View from which the widget identified by the index will be + found and key press be performed. + index : int + Raises + ------ + LookupError + If the index cannot be located. + Note that the index error provides more + """ + check_q_model_index_valid(index) + return model.data(index, QtCore.Qt.DisplayRole) + + def mouse_click_combobox(combobox, index, delay): """ Perform a mouse click on a QComboBox at a given index. diff --git a/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/tree_editor.py b/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/tree_editor.py new file mode 100644 index 000000000..bea54ea6f --- /dev/null +++ b/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/tree_editor.py @@ -0,0 +1,126 @@ +# Copyright (c) 2005-2020, Enthought, Inc. +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only +# under the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Thanks for using Enthought open source! +# + +from traitsui.qt4.tree_editor import SimpleEditor + + +from traitsui.testing.tester.command import ( + MouseClick, MouseDClick, KeyClick, KeySequence +) +from traitsui.testing.tester.locator import TreeNode +from traitsui.testing.tester.query import DisplayedText +from traitsui.testing.tester._ui_tester_registry.qt4 import ( + _interaction_helpers +) + +from traitsui.testing.tester._ui_tester_registry._common_ui_targets import ( + BaseSourceWithLocation +) +from traitsui.testing.tester._ui_tester_registry._traitsui_ui import ( + register_traitsui_ui_solvers, +) + + +class _SimpleEditorWithTreeNode(BaseSourceWithLocation): + source_class = SimpleEditor + locator_class = TreeNode + handlers = [ + (MouseClick, lambda wrapper, _: wrapper._target._mouse_click( + delay=wrapper.delay)), + (MouseDClick, lambda wrapper, _: wrapper._target._mouse_dclick( + delay=wrapper.delay)), + (KeySequence, + lambda wrapper, action: wrapper._target._key_sequence( + sequence=action.sequence, + delay=wrapper.delay, + )), + (KeyClick, + lambda wrapper, action: wrapper._target._key_press( + key=action.key, + delay=wrapper.delay, + )), + (DisplayedText, + lambda wrapper, _: wrapper._target._get_displayed_text()), + ] + + @classmethod + def register(cls, registry): + """ Class method to register interactions on a + _SimpleEditorWithTreeNode for the given registry. + + If there are any conflicts, an error will occur. + + Parameters + ---------- + registry : TargetRegistry + The registry being registered to. + """ + super().register(registry) + register_traitsui_ui_solvers( + registry=registry, + target_class=cls, + traitsui_ui_getter=lambda target: target._get_nested_ui() + ) + + def _get_model_view_index(self): + tree_widget = self.source._tree + i_column = self.location.column + i_rows = iter(self.location.row) + item = tree_widget.topLevelItem(next(i_rows)) + for i_row in i_rows: + item = item.child(i_row) + q_model_index = tree_widget.indexFromItem(item, i_column) + return dict( + model=tree_widget.model(), + view=tree_widget, + index=q_model_index, + ) + + def _mouse_click(self, delay=0): + _interaction_helpers.mouse_click_item_view( + **self._get_model_view_index(), + delay=delay, + ) + + def _mouse_dclick(self, delay=0): + _interaction_helpers.mouse_dclick_item_view( + **self._get_model_view_index(), + delay=delay, + ) + + def _key_press(self, key, delay=0): + _interaction_helpers.key_press_item_view( + **self._get_model_view_index(), + key=key, + delay=delay, + ) + + def _key_sequence(self, sequence, delay=0): + _interaction_helpers.key_sequence_item_view( + **self._get_model_view_index(), + sequence=sequence, + delay=delay, + ) + + def _get_displayed_text(self): + return _interaction_helpers.get_display_text_item_view( + **self._get_model_view_index(), + ) + + def _get_nested_ui(self): + """ Method to get the nested ui corresponding to the List element at + the given index. + """ + return self.source._editor._node_ui + + +def register(registry): + _SimpleEditorWithTreeNode.register(registry) diff --git a/traitsui/testing/tester/_ui_tester_registry/qt4/default_registry.py b/traitsui/testing/tester/_ui_tester_registry/qt4/default_registry.py index f386f2aa3..bc508ea86 100644 --- a/traitsui/testing/tester/_ui_tester_registry/qt4/default_registry.py +++ b/traitsui/testing/tester/_ui_tester_registry/qt4/default_registry.py @@ -22,6 +22,7 @@ list_editor, range_editor, text_editor, + tree_editor, ui_base, ) from ._control_widget_registry import get_widget_registry @@ -77,6 +78,9 @@ def get_default_registries(): # Editor Factory editor_factory.register(registry) + # TreeEditor + tree_editor.register(registry) + # The more general registry goes after the more specific registry. return [ registry, diff --git a/traitsui/testing/tester/command.py b/traitsui/testing/tester/command.py index 6a9030997..0c8a4de5c 100644 --- a/traitsui/testing/tester/command.py +++ b/traitsui/testing/tester/command.py @@ -27,6 +27,16 @@ class MouseClick: pass +class MouseDClick: + """ An object representing the user double clicking a mouse button. + Currently the left mouse button is assumed. + In most circumstances, a widget can still be clicked on even if it is + disabled. Therefore unlike key events, if the widget is disabled, + implementations should not raise an exception. + """ + pass + + class KeySequence: """ An object representing the user typing a sequence of keys. diff --git a/traitsui/testing/tester/locator.py b/traitsui/testing/tester/locator.py index ea08e510c..4e9344f64 100644 --- a/traitsui/testing/tester/locator.py +++ b/traitsui/testing/tester/locator.py @@ -53,6 +53,16 @@ def __init__(self, id): self.id = id +class TreeNode: + """ A locator for locating a target in a Tree uniquely specified by a row + and a column. + """ + + def __init__(self, row, column): + self.row = row + self.column = column + + class Slider: """ A locator for locating a nested slider widget within a UI. """