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

UITester support for TableEditor #1707

Merged
merged 30 commits into from Feb 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
073c9a2
initial qt implementation of table editor, updating some tests, and a…
aaronayres35 Oct 14, 2020
61246c3
inital attempts at a wx implementation (all stille very ugly as tests…
aaronayres35 Oct 14, 2020
2795f7f
use UI Tester for all tests ui creation, disposal, and event processing
aaronayres35 Oct 14, 2020
5079b8c
set all views to have sortable = false for consistency (should add a …
aaronayres35 Oct 14, 2020
450a53f
update test_table_editor_select_row to use UI Tester to select row
aaronayres35 Oct 14, 2020
2fffea6
use UI Tester for all single selections
aaronayres35 Oct 14, 2020
de1326f
first pass of a test for TableEditor_demo.py
aaronayres35 Oct 15, 2020
863b60f
flake8
aaronayres35 Jun 29, 2021
d2866cc
update some comments
aaronayres35 Jun 29, 2021
86e18ad
remove start of wx support
aaronayres35 Jun 30, 2021
e27351e
require qt on test_TableEditor_demo.py
aaronayres35 Jun 30, 2021
cdb8743
TableEditor tests all require qt
aaronayres35 Jun 30, 2021
ef06bec
update a couple of tests to use Selected
aaronayres35 Jun 30, 2021
69c0bba
use UITester to find editor and remove wx conditional block
aaronayres35 Jun 30, 2021
de73681
More cases of using UITester to get an editor, and removing wx condit…
aaronayres35 Jun 30, 2021
7e93c8e
typo and style
aaronayres35 Jun 30, 2021
5d17dcf
uncomment esc line in test
aaronayres35 Jun 30, 2021
bd1d58e
no longer need is_qt and is_wx, also couple more uses of UITester to …
aaronayres35 Jun 30, 2021
0aaf7a3
remove redundant test
aaronayres35 Jun 30, 2021
b25cfd6
remove redundant view definition
aaronayres35 Jun 30, 2021
f842120
add test using MouseDClick
aaronayres35 Jun 30, 2021
469174c
add SelectedIndices query class
aaronayres35 Jun 30, 2021
9fdc526
Docstring re-wording
aaronayres35 Jul 7, 2021
dc4f4a1
dont import from api internally
aaronayres35 Jul 7, 2021
d34f0c2
Add Cell and MouseDClick to the api module docstring
aaronayres35 Jul 7, 2021
b83e6b2
update SelectedIndices api
aaronayres35 Jul 7, 2021
5f07956
add news fragment
aaronayres35 Jul 7, 2021
9b418ff
update Selected to also return list
aaronayres35 Jul 7, 2021
036dc51
upadte query object docstrings
aaronayres35 Jul 7, 2021
567ac88
Merge branch 'main' into ui-tester-updates-TableEditor
aaronayres35 Dec 22, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/releases/upcoming/1707.feature.rst
@@ -0,0 +1 @@
Add UITester support for qt TableEditor (#1707)
@@ -0,0 +1,51 @@
"""
This example demonstrates how to test interacting with a TableEditor.

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 (
Cell, KeyClick, KeySequence, MouseClick, UITester
)
from traitsui.tests._tools import requires_toolkit, ToolkitName

#: Filename of the demo script
FILENAME = "TableEditor_demo.py"

#: Path of the demo script
DEMO_PATH = os.path.join(os.path.dirname(__file__), "..", FILENAME)


class TestTableEditorDemo(unittest.TestCase):

@requires_toolkit([ToolkitName.qt])
def test_list_editor_demo(self):
demo = runpy.run_path(DEMO_PATH)["demo"]

tester = UITester()
with tester.create_ui(demo) as ui:
employees_table = tester.find_by_name(ui, "employees")

# clicking a cell enters edit mode and selects full text
cell_21 = employees_table.locate(Cell(2, 1))
cell_21.perform(MouseClick())
cell_21.perform(KeySequence("Jones"))
cell_21.perform(KeyClick("Enter"))

self.assertEqual(demo.employees[0].last_name, 'Jones')

# third column corresponds to Full Name property
cell_32 = employees_table.locate(Cell(3, 2))
cell_32.perform(MouseClick())


# Run the test(s)
unittest.TextTestRunner().run(
unittest.TestLoader().loadTestsFromTestCase(TestTableEditorDemo)
)
25 changes: 22 additions & 3 deletions traitsui/testing/api.py
Expand Up @@ -22,18 +22,23 @@
- :class:`~.KeyClick`
- :class:`~.KeySequence`
- :class:`~.MouseClick`
- :class:`~.MouseDClick`

Interactions (for getting GUI states)
-------------------------------------

- :class:`~.DisplayedText`
- :class:`~.IsChecked`
- :class:`~.IsEnabled`
- :class:`~.IsVisible`
- :class:`~.Selected`
- :class:`~.SelectedIndices`
- :class:`~.SelectedText`

Locations (for locating GUI elements)
-------------------------------------

- :class:`~.Cell`
- :class:`~.Index`
- :class:`~.Slider`
- :class:`~.TargetById`
Expand Down Expand Up @@ -61,19 +66,33 @@
from .tester.ui_tester import UITester

# Interactions (for changing GUI states)
from .tester.command import MouseClick, KeyClick, KeySequence
from .tester.command import (
MouseClick,
MouseDClick,
KeyClick,
KeySequence
)

# Interactions (for getting GUI states)
from .tester.query import (
DisplayedText,
IsChecked,
IsEnabled,
IsVisible,
SelectedText,
Selected,
SelectedIndices,
SelectedText
)

# Locations (for locating GUI elements)
from .tester.locator import Index, TargetById, TargetByName, Textbox, Slider
from .tester.locator import (
Cell,
aaronayres35 marked this conversation as resolved.
Show resolved Hide resolved
Index,
TargetById,
TargetByName,
Textbox,
Slider
)

# Advanced usage
from .tester.target_registry import TargetRegistry
Expand Down
Expand Up @@ -187,7 +187,7 @@ def mouse_click_item_view(model, view, index, delay):
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.
found and mouse click be performed.
index : QModelIndex

Raises
Expand All @@ -207,6 +207,127 @@ 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)
aaronayres35 marked this conversation as resolved.
Show resolved Hide resolved


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)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

is data the right thing here for displayed text? (look at Qt docs for QAbstractItemModel)

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, data with DisplayRole should provide the text to display.



def mouse_click_combobox(combobox, index, delay):
"""Perform a mouse click on a QComboBox at a given index.

Expand Down
@@ -0,0 +1,141 @@
# (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!

from traitsui.qt4.table_editor import SimpleEditor

from traitsui.testing.tester.command import (
MouseClick,
MouseDClick,
KeyClick,
KeySequence,
)
from traitsui.testing.tester.locator import Cell
from traitsui.testing.tester.query import (
DisplayedText,
Selected,
SelectedIndices,
)
from traitsui.testing.tester._ui_tester_registry._common_ui_targets import (
BaseSourceWithLocation
)
from traitsui.testing.tester._ui_tester_registry.qt4 import (
_interaction_helpers
)


def _query_table_editor_selected(wrapper, interaction):
selected = wrapper._target.selected
if not isinstance(selected, list):
if selected is None:
return []
else:
return [selected]
else:
return selected


def _query_table_editor_selected_indices(wrapper, interaction):
selected_indices = wrapper._target.selected_indices
if not isinstance(selected_indices, list):
if selected_indices == -1:
return []
else:
return [selected_indices]
else:
return selected_indices


class _SimpleEditorWithCell(BaseSourceWithLocation):
source_class = SimpleEditor
locator_class = Cell
handlers = [
(MouseClick, lambda wrapper, _: wrapper._target._mouse_click(
delay=wrapper.delay)),
(KeyClick, lambda wrapper, interaction: wrapper._target._key_click(
key=interaction.key,
delay=wrapper.delay,)),
(
KeySequence,
lambda wrapper, interaction: wrapper._target._key_sequence(
sequence=interaction.sequence,
delay=wrapper.delay,
)
),
(
DisplayedText,
lambda wrapper, _: wrapper._target._get_displayed_text()
),
(MouseDClick, lambda wrapper, _: wrapper._target._mouse_dclick(
delay=wrapper.delay,)),
]

def _get_model_view_index(self):
table_view = self.source.table_view
return dict(
model=table_view.model(),
view=table_view,
index=table_view.model().index(
self.location.row, self.location.column
),
)

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_sequence(self, sequence, delay=0):
_interaction_helpers.key_sequence_item_view(
**self._get_model_view_index(),
sequence=sequence,
delay=delay,
)

def _key_click(self, key, delay=0):
_interaction_helpers.key_click_item_view(
**self._get_model_view_index(),
key=key,
delay=delay,
)

def _get_displayed_text(self):
return _interaction_helpers.get_display_text_item_view(
**self._get_model_view_index(),
)


def register(registry):
""" Register interactions for the given registry.

If there are any conflicts, an error will occur.

Parameters
----------
registry : TargetRegistry
The registry being registered to.
"""
_SimpleEditorWithCell.register(registry)
registry.register_interaction(
target_class=SimpleEditor,
interaction_class=Selected,
handler=_query_table_editor_selected
)
registry.register_interaction(
target_class=SimpleEditor,
interaction_class=SelectedIndices,
handler=_query_table_editor_selected_indices
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've just added a SelectedIndices query class.
For the current implementation, for TableEditor, Selected and SelectedIndices effectively just serve as proxies to the selected and selected_indices traits on the table editor (see code above.)
In theory it may be better for these to dive down to the Qt level (?)

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 is one of the weird things with UITester support though IMO. We want to test traitsui editors are working correctly.
So having tests be down at qt level is really what we want. Ie I do something in traitsUI and we verify that does what we expect down in qt land.
However, say I were implementing the handler for the TableEditor to do a SelectedIndices query. I would end up basically rewriting:

@cached_property
def _get_selected_indices(self):
"""Gets the row,column indices which match the selected trait"""
selection_items = self.table_view.selectionModel().selection()
indices = self.model.mapSelectionToSource(selection_items).indexes()
if self.factory.selection_mode.startswith("row"):
indices = sorted(set(index.row() for index in indices))
elif self.factory.selection_mode.startswith("column"):
indices = sorted(set(index.column() for index in indices))
else:
indices = [(index.row(), index.column()) for index in indices]
if self.factory.selection_mode in {"rows", "columns", "cells"}:
return indices
elif len(indices) > 0:
return indices[0]
else:
return -1

It creates sort of a weird chasing your own tail scenario 🤔

For downstream users they just want to see that when they run their code, ____ is selected. For which this (just using traits on the editor) is perfectly fine.

Copy link
Contributor

Choose a reason for hiding this comment

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

UITester strikes me as being useful for two things:

  • testing that TraitsUI editors are integrating correctly with the toolkit
  • as a framework for integration tests for TraitsUI apps

For unit tests of applications you can usually get away with tests at the traits level (does changing this trait in a UI change the corresponding thing in the model? or vice-versa) where you don't really care/can trust that the UI then does the right thing.