From 52f0508d4f042f007a8a4d5306071cb4078ef17d Mon Sep 17 00:00:00 2001 From: Aaron Ayres Date: Thu, 1 Jul 2021 13:20:50 -0500 Subject: [PATCH 1/8] add start of TreeEditor support for Qt --- .../qt4/_traitsui/tree_editor.py | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/tree_editor.py 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..3246351c1 --- /dev/null +++ b/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/tree_editor.py @@ -0,0 +1,114 @@ +# 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.qt4 import helpers +from traitsui.testing.tester import command, locator, registry_helper, query + +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 = locator.TreeNode + handlers = [ + (command.MouseClick, lambda wrapper, interaction: _mouse_click(delay=wrapper.delay)), + (command.KeySequence, + lambda wrapper, action: _key_sequence( + sequence=action.sequence, + delay=wrapper.delay, + )), + (command.KeyClick, + lambda wrapper, action: _key_press( + key=action.key, + delay=wrapper.delay, + )), + (query.DisplayedText, + lambda wrapper, _: _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_view = self.source._tree + i_column = self.location.column + i_rows = iter(self.location.row) + item = tree_view.topLevelItem(next(i_rows)) + for i_row in i_rows: + item = item.child(i_row) + q_model_index = tree_view.indexFromItem(item, i_column) + return dict( + model=tree_view.model(), + view=tree_view, + index=q_model_index, + ) + + def _mouse_click(self, delay=0): + helpers.mouse_click_item_view( + **self._get_model_view_index(), + delay=delay, + ) + + def _mouse_dclick(self, delay=0): + helpers.mouse_dclick_item_view( + **self._get_model_view_index(), + delay=delay, + ) + + def _key_press(self, key, delay=0): + helpers.key_press_item_view( + **self._get_model_view_index(), + key=key, + delay=delay, + ) + + def _key_sequence(self, sequence, delay=0): + helpers.key_sequence_item_view( + **self._get_model_view_index(), + sequence=sequence, + delay=delay, + ) + + def _get_displayed_text(self): + return 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) From 65687cbcc9f5fa9bc78b2124f88cb4861d8d7d4a Mon Sep 17 00:00:00 2001 From: Aaron Ayres Date: Thu, 1 Jul 2021 13:22:29 -0500 Subject: [PATCH 2/8] we need _interaction_helpers for QAbstractItemModel (these are also introduced in PR #1707) --- .../qt4/_interaction_helpers.py | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) 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. From 691b3420a2ebcf3a4c825b9a58c53151f4c4a7e4 Mon Sep 17 00:00:00 2001 From: Aaron Ayres Date: Thu, 1 Jul 2021 13:29:51 -0500 Subject: [PATCH 3/8] we need MouseDClick and TreeNode in the api --- traitsui/testing/api.py | 5 +++++ traitsui/testing/tester/command.py | 10 ++++++++++ traitsui/testing/tester/locator.py | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+) 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/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..4d1763792 100644 --- a/traitsui/testing/tester/locator.py +++ b/traitsui/testing/tester/locator.py @@ -53,6 +53,24 @@ def __init__(self, id): self.id = id +class TreeNode: + """ A locator for locating a target uniquely specified by a row index and a + column index. + + Attributes + ---------- + row : int + 0-based index + column : int + 0-based index + """ + + def __init__(self, row, column): + self.row = row + self.column = column + + + class Slider: """ A locator for locating a nested slider widget within a UI. """ From eccc2df3e26adc54633f7efbf110c181335cdbf9 Mon Sep 17 00:00:00 2001 From: Aaron Ayres Date: Thu, 1 Jul 2021 13:38:34 -0500 Subject: [PATCH 4/8] lots of clean up of implementation and expose in default_registry --- .../qt4/_traitsui/tree_editor.py | 41 ++++++++++++------- .../qt4/default_registry.py | 4 ++ 2 files changed, 30 insertions(+), 15 deletions(-) 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 index 3246351c1..848bed8d1 100644 --- a/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/tree_editor.py +++ b/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/tree_editor.py @@ -10,8 +10,16 @@ # from traitsui.qt4.tree_editor import SimpleEditor -from traitsui.testing.tester.qt4 import helpers -from traitsui.testing.tester import command, locator, registry_helper, query + + +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 @@ -22,21 +30,24 @@ class _SimpleEditorWithTreeNode(BaseSourceWithLocation): source_class = SimpleEditor - locator_class = locator.TreeNode + locator_class = TreeNode handlers = [ - (command.MouseClick, lambda wrapper, interaction: _mouse_click(delay=wrapper.delay)), - (command.KeySequence, - lambda wrapper, action: _key_sequence( + (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, )), - (command.KeyClick, - lambda wrapper, action: _key_press( + (KeyClick, + lambda wrapper, action: wrapper._target._key_press( key=action.key, delay=wrapper.delay, )), - (query.DisplayedText, - lambda wrapper, _: _get_displayed_text()), + (DisplayedText, + lambda wrapper, _: wrapper._target._get_displayed_text()), ] @classmethod @@ -73,33 +84,33 @@ def _get_model_view_index(self): ) def _mouse_click(self, delay=0): - helpers.mouse_click_item_view( + _interaction_helpers.mouse_click_item_view( **self._get_model_view_index(), delay=delay, ) def _mouse_dclick(self, delay=0): - helpers.mouse_dclick_item_view( + _interaction_helpers.mouse_dclick_item_view( **self._get_model_view_index(), delay=delay, ) def _key_press(self, key, delay=0): - helpers.key_press_item_view( + _interaction_helpers.key_press_item_view( **self._get_model_view_index(), key=key, delay=delay, ) def _key_sequence(self, sequence, delay=0): - helpers.key_sequence_item_view( + _interaction_helpers.key_sequence_item_view( **self._get_model_view_index(), sequence=sequence, delay=delay, ) def _get_displayed_text(self): - return helpers.get_display_text_item_view( + return _interaction_helpers.get_display_text_item_view( **self._get_model_view_index(), ) 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 7d4aff509..5948be4ed 100644 --- a/traitsui/testing/tester/_ui_tester_registry/qt4/default_registry.py +++ b/traitsui/testing/tester/_ui_tester_registry/qt4/default_registry.py @@ -21,6 +21,7 @@ list_editor, range_editor, text_editor, + tree_editor, ui_base, ) from ._control_widget_registry import get_widget_registry @@ -73,6 +74,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, From 51a735fbb0c5258e11f44b2de338807f7a85b88e Mon Sep 17 00:00:00 2001 From: Aaron Ayres Date: Thu, 1 Jul 2021 13:57:39 -0500 Subject: [PATCH 5/8] add a test for the TreeEditor_demo --- .../tests/test_TreeEditor_demo.py | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 traitsui/examples/demo/Standard_Editors/tests/test_TreeEditor_demo.py 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) +) From 61c4c43c201ec0a9fe8cba6cefb680d273736321 Mon Sep 17 00:00:00 2001 From: Aaron Ayres Date: Thu, 1 Jul 2021 14:02:59 -0500 Subject: [PATCH 6/8] we are really using a _TreeWidget which subclasses QTreeWidget which subclasses QTreeView --- .../_ui_tester_registry/qt4/_traitsui/tree_editor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 index 848bed8d1..84841a22a 100644 --- a/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/tree_editor.py +++ b/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/tree_editor.py @@ -70,16 +70,16 @@ def register(cls, registry): ) def _get_model_view_index(self): - tree_view = self.source._tree + tree_widget = self.source._tree i_column = self.location.column i_rows = iter(self.location.row) - item = tree_view.topLevelItem(next(i_rows)) + item = tree_widget.topLevelItem(next(i_rows)) for i_row in i_rows: item = item.child(i_row) - q_model_index = tree_view.indexFromItem(item, i_column) + q_model_index = tree_widget.indexFromItem(item, i_column) return dict( - model=tree_view.model(), - view=tree_view, + model=tree_widget.model(), + view=tree_widget, index=q_model_index, ) From 5c004c7320423b196e135f6b2c06da345ae53617 Mon Sep 17 00:00:00 2001 From: Aaron Ayres Date: Thu, 1 Jul 2021 14:20:27 -0500 Subject: [PATCH 7/8] update docstring for TreeNode --- traitsui/testing/tester/locator.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/traitsui/testing/tester/locator.py b/traitsui/testing/tester/locator.py index 4d1763792..fd2262312 100644 --- a/traitsui/testing/tester/locator.py +++ b/traitsui/testing/tester/locator.py @@ -54,15 +54,8 @@ def __init__(self, id): class TreeNode: - """ A locator for locating a target uniquely specified by a row index and a - column index. - - Attributes - ---------- - row : int - 0-based index - column : int - 0-based index + """ A locator for locating a target in a Tree uniquely specified by a row + and a column. """ def __init__(self, row, column): From 6fce17397d2acb31ea0b38db9c9f9f9e05529624 Mon Sep 17 00:00:00 2001 From: Aaron Ayres Date: Thu, 1 Jul 2021 15:00:01 -0500 Subject: [PATCH 8/8] flake8 --- .../tester/_ui_tester_registry/qt4/_traitsui/tree_editor.py | 1 + traitsui/testing/tester/locator.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) 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 index 84841a22a..bea54ea6f 100644 --- a/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/tree_editor.py +++ b/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/tree_editor.py @@ -28,6 +28,7 @@ register_traitsui_ui_solvers, ) + class _SimpleEditorWithTreeNode(BaseSourceWithLocation): source_class = SimpleEditor locator_class = TreeNode diff --git a/traitsui/testing/tester/locator.py b/traitsui/testing/tester/locator.py index fd2262312..4e9344f64 100644 --- a/traitsui/testing/tester/locator.py +++ b/traitsui/testing/tester/locator.py @@ -63,7 +63,6 @@ def __init__(self, row, column): self.column = column - class Slider: """ A locator for locating a nested slider widget within a UI. """