Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] UITester support for qt TreeEditor #1713

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -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)
)
5 changes: 5 additions & 0 deletions traitsui/testing/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- :class:`~.KeyClick`
- :class:`~.KeySequence`
- :class:`~.MouseClick`
- :class:`~.MouseDClick`

Interactions (for getting GUI states)
-------------------------------------
Expand All @@ -39,6 +40,8 @@
- :class:`~.TargetById`
- :class:`~.TargetByName`
- :class:`~.Textbox`
- :class:`~.TreeNode`
- :class:`~.SelectedText`

Advanced usage
--------------
Expand All @@ -63,6 +66,7 @@
# Interactions (for changing GUI states)
from .tester.command import (
MouseClick,
MouseDClick,
KeyClick,
KeySequence
)
Expand All @@ -82,6 +86,7 @@
TargetById,
TargetByName,
Textbox,
TreeNode,
Slider
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# 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,
)
Comment on lines +74 to +85
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic and the TreeNode object having a row/column I need to think about more carefully. I had this code in a branch locally from a long time ago which I believe had originally been pulled from one of Kit's draft PRs.

There may be a simple way to do this / at the very least I need to better document the TreeNode class to say what row and column actually specify. In this case (see test_TreeEditor_demo.py) row is a tuple containing the index of the node of interest at subsequent levels of the tree, and column is the index at that last level (?). I have just been using column as 0, ... it may be possible we only need one single tuple to do this TreeNode location.


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)
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
list_editor,
range_editor,
text_editor,
tree_editor,
ui_base,
)
from ._control_widget_registry import get_widget_registry
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions traitsui/testing/tester/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading