-
Notifications
You must be signed in to change notification settings - Fork 99
UITester support for TableEditor #1707
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
Changes from all commits
073c9a2
61246c3
2795f7f
5079b8c
450a53f
2fffea6
de1326f
863b60f
d2866cc
86e18ad
e27351e
cdb8743
ef06bec
69c0bba
de73681
7e93c8e
5d17dcf
bd1d58e
0aaf7a3
b25cfd6
f842120
469174c
9fdc526
dc4f4a1
d34f0c2
b83e6b2
5f07956
9b418ff
036dc51
567ac88
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Add UITester support for qt TableEditor (#1707) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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.
|
||
|
|
||
|
|
||
| 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) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, |
||
|
|
||
|
|
||
| def mouse_click_combobox(combobox, index, delay): | ||
| """Perform a mouse click on a QComboBox at a given index. | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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 | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've just added a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. traitsui/traitsui/qt4/table_editor.py Lines 626 to 643 in 347f0b5
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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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. |
||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.