diff --git a/docs/releases/upcoming/1707.feature.rst b/docs/releases/upcoming/1707.feature.rst new file mode 100644 index 000000000..6fed922bc --- /dev/null +++ b/docs/releases/upcoming/1707.feature.rst @@ -0,0 +1 @@ +Add UITester support for qt TableEditor (#1707) \ No newline at end of file diff --git a/traitsui/examples/demo/Standard_Editors/tests/test_TableEditor_demo.py b/traitsui/examples/demo/Standard_Editors/tests/test_TableEditor_demo.py new file mode 100644 index 000000000..c6b1e7e9d --- /dev/null +++ b/traitsui/examples/demo/Standard_Editors/tests/test_TableEditor_demo.py @@ -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) +) diff --git a/traitsui/testing/api.py b/traitsui/testing/api.py index 298c931bf..0942a8945 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) ------------------------------------- @@ -29,11 +30,15 @@ - :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` @@ -61,7 +66,12 @@ 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 ( @@ -69,11 +79,20 @@ 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, + Index, + TargetById, + TargetByName, + Textbox, + Slider +) # Advanced usage from .tester.target_registry import TargetRegistry 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 c034b8d14..e90c07d5e 100644 --- a/traitsui/testing/tester/_ui_tester_registry/qt4/_interaction_helpers.py +++ b/traitsui/testing/tester/_ui_tester_registry/qt4/_interaction_helpers.py @@ -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) + + +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/table_editor.py b/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/table_editor.py new file mode 100644 index 000000000..ab261142e --- /dev/null +++ b/traitsui/testing/tester/_ui_tester_registry/qt4/_traitsui/table_editor.py @@ -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 + ) 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 b6c726a53..936c3c600 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 @@ instance_editor, list_editor, range_editor, + table_editor, text_editor, ui_base, ) @@ -77,6 +78,9 @@ def get_default_registries(): # Editor Factory editor_factory.register(registry) + # TableEditor + table_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 cbf119722..efdf0b03f 100644 --- a/traitsui/testing/tester/command.py +++ b/traitsui/testing/tester/command.py @@ -28,6 +28,17 @@ 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 febf25459..efae68771 100644 --- a/traitsui/testing/tester/locator.py +++ b/traitsui/testing/tester/locator.py @@ -17,6 +17,23 @@ """ +class Cell: + """ 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 Index: """A locator for locating a target that is uniquely specified by a single 0-based index. diff --git a/traitsui/testing/tester/query.py b/traitsui/testing/tester/query.py index 7c441c7de..dad2e5e77 100644 --- a/traitsui/testing/tester/query.py +++ b/traitsui/testing/tester/query.py @@ -16,6 +16,30 @@ """ +class Selected: + """ Represents an interaction to obtain the currently selected object(s). + + Implementations should return a list of selected objects, or an empty list + if nothing is selected. + """ + pass + + +class SelectedIndices: + """ Represents an interaction to obtain the indices of the currently + selected objects. + + Implementations should return a list of indicies of the selected + objects or an empty list if nothing is selected. + + Note that an index could be an integer (e.g. when selecting from a list or + enumerataion, or selecting entire rows or columns of a table), or it could + be a tuple (e.g. corresponding to a specific cell at some (row, column) in + a table). + """ + pass + + class SelectedText: """An object representing an interaction to obtain the displayed (echoed) plain text which is currently selected. diff --git a/traitsui/tests/editors/test_table_editor.py b/traitsui/tests/editors/test_table_editor.py index c29c86d29..6e14b753a 100644 --- a/traitsui/tests/editors/test_table_editor.py +++ b/traitsui/tests/editors/test_table_editor.py @@ -23,14 +23,21 @@ ) from traitsui.tests._tools import ( BaseTestMixin, - create_ui, - is_qt, - is_wx, - process_cascade_events, requires_toolkit, - reraise_exceptions, ToolkitName, ) +from traitsui.testing.api import ( + Cell, + Disabled, + DisplayedText, + KeySequence, + KeyClick, + MouseClick, + MouseDClick, + Selected, + SelectedIndices, + UITester, +) class ListItem(HasTraits): @@ -77,11 +84,12 @@ class ObjectList(HasTraits): "values", show_label=False, editor=TableEditor( + sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), ], - filter=EvalTableFilter(expression="other_value >= 2"), + filter=EvalTableFilter(expression="other_value > 4"), ), ), buttons=["OK"], @@ -92,6 +100,7 @@ class ObjectList(HasTraits): "values", show_label=False, editor=TableEditor( + sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), @@ -108,6 +117,7 @@ class ObjectList(HasTraits): "values", show_label=False, editor=TableEditor( + sortable=False, columns=[ObjectColumn(name="value")], selection_mode="rows", selected="selections", @@ -121,6 +131,7 @@ class ObjectList(HasTraits): "values", show_label=False, editor=TableEditor( + sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), @@ -137,6 +148,7 @@ class ObjectList(HasTraits): "values", show_label=False, editor=TableEditor( + sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), @@ -153,6 +165,7 @@ class ObjectList(HasTraits): "values", show_label=False, editor=TableEditor( + sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), @@ -169,6 +182,7 @@ class ObjectList(HasTraits): "values", show_label=False, editor=TableEditor( + sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), @@ -185,6 +199,7 @@ class ObjectList(HasTraits): "values", show_label=False, editor=TableEditor( + sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), @@ -201,6 +216,7 @@ class ObjectList(HasTraits): "values", show_label=False, editor=TableEditor( + sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), @@ -217,6 +233,7 @@ class ObjectList(HasTraits): "values", show_label=False, editor=TableEditor( + sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), @@ -233,6 +250,7 @@ class ObjectList(HasTraits): "values", show_label=False, editor=TableEditor( + sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), @@ -249,6 +267,7 @@ class ObjectList(HasTraits): "values", show_label=False, editor=TableEditor( + sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), @@ -265,6 +284,7 @@ class ObjectList(HasTraits): "values", show_label=False, editor=TableEditor( + sortable=False, columns=[ ObjectColumn(name="value"), ObjectColumn(name="other_value"), @@ -276,7 +296,26 @@ class ObjectList(HasTraits): buttons=["OK"], ) +edit_on_first_click_false_view = View( + Item( + "values", + show_label=False, + editor=TableEditor( + sortable=False, + columns=[ + ObjectColumn(name="value"), + ObjectColumn(name="other_value"), + ], + selection_mode="row", + selected="selected", + edit_on_first_click=False + ), + ), + buttons=["OK"], +) + +@requires_toolkit([ToolkitName.qt]) class TestTableEditor(BaseTestMixin, unittest.TestCase): def setUp(self): BaseTestMixin.setUp(self) @@ -284,98 +323,71 @@ def setUp(self): def tearDown(self): BaseTestMixin.tearDown(self) - @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_table_editor(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) + tester = UITester() + with tester.create_ui(object_list, dict(view=simple_view)): + pass - with reraise_exceptions(), create_ui( - object_list, dict(view=simple_view) - ): - process_cascade_events() - - @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_filtered_table_editor(self): object_list = ObjectListWithSelection( - values=[ListItem(value=str(i ** 2)) for i in range(10)] + values=[ListItem(other_value=i ** 2) for i in range(10)] ) + tester = UITester() + with tester.create_ui(object_list, dict(view=filtered_view)) as ui: + values = tester.find_by_name(ui, "values") + filter = values._target.filter + num_filtered_indices = len(values._target.filtered_indices) + self.assertIsNotNone(filter) + self.assertEqual(num_filtered_indices, 7) - with reraise_exceptions(), create_ui( - object_list, dict(view=filtered_view) - ) as ui: - process_cascade_events() - - filter = ui.get_editors("values")[0].filter - - process_cascade_events() - - self.assertIsNotNone(filter) - - @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_table_editor_select_row(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) - object_list.selected = object_list.values[5] - - with reraise_exceptions(), create_ui( - object_list, dict(view=select_row_view) - ) as ui: - editor = ui.get_editors("values")[0] - process_cascade_events() - if is_qt(): - selected = editor.selected - elif is_wx(): - selected = editor.selected_row - process_cascade_events() + tester = UITester() + with tester.create_ui(object_list, dict(view=select_row_view)) as ui: + # click the first cell in the 6th row to select the row + values_table = tester.find_by_name(ui, "values") + row6_cell = values_table.locate(Cell(5, 0)) + row6_cell.perform(MouseClick()) + selected = values_table.inspect(Selected()) - self.assertIs(selected, object_list.values[5]) + self.assertEqual(selected, [object_list.values[5]]) + self.assertIs(selected[0], object_list.values[5]) + self.assertIs(object_list.selected, selected[0]) - @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_table_editor_select_rows(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) object_list.selections = object_list.values[5:7] - with reraise_exceptions(), create_ui( - object_list, dict(view=select_rows_view) - ) as ui: - editor = ui.get_editors("values")[0] - process_cascade_events() - if is_qt(): - selected = editor.selected - elif is_wx(): - selected = editor.selected_rows - - process_cascade_events() + tester = UITester() + with tester.create_ui(object_list, dict(view=select_rows_view)) as ui: + values_table = tester.find_by_name(ui, "values") + selected = values_table.inspect(Selected()) self.assertEqual(selected, object_list.values[5:7]) - @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_table_editor_select_row_index(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) object_list.selected_index = 5 - with reraise_exceptions(), create_ui( - object_list, dict(view=select_row_index_view) - ) as ui: - editor = ui.get_editors("values")[0] - process_cascade_events() - if is_qt(): - selected = editor.selected_indices - elif is_wx(): - selected = editor.selected_row_index - - process_cascade_events() + tester = UITester() + with tester.create_ui(object_list, dict(view=select_row_index_view)) \ + as ui: + values_table = tester.find_by_name(ui, "values") + values_table.locate(Cell(5, 0)).perform(MouseClick()) + selected = values_table.inspect(SelectedIndices()) - self.assertEqual(selected, 5) + self.assertEqual(selected, [5]) - @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_table_editor_select_row_indices(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] @@ -383,85 +395,63 @@ def test_table_editor_select_row_indices(self): object_list.selected_indices = [5, 7, 8] view = select_row_indices_view - with reraise_exceptions(), create_ui( - object_list, dict(view=view) - ) as ui: - editor = ui.get_editors("values")[0] - process_cascade_events() - if is_qt(): - selected = editor.selected_indices - elif is_wx(): - selected = editor.selected_row_indices - - process_cascade_events() + tester = UITester() + with tester.create_ui(object_list, dict(view=view)) as ui: + values_table = tester.find_by_name(ui, "values") + selected = values_table.inspect(SelectedIndices()) self.assertEqual(selected, [5, 7, 8]) - @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_table_editor_select_column(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) - object_list.selected_column = "value" - with reraise_exceptions(), create_ui( - object_list, dict(view=select_column_view) - ) as ui: - editor = ui.get_editors("values")[0] - process_cascade_events() - if is_qt(): - selected = editor.selected - elif is_wx(): - selected = editor.selected_column + tester = UITester() + with tester.create_ui(object_list, dict(view=select_column_view)) \ + as ui: + values_table = tester.find_by_name(ui, "values") + # click a cell in the first column (the "value" column) + first_cell = values_table.locate(Cell(0, 0)) + first_cell.perform(MouseClick()) - process_cascade_events() + selected = values_table.inspect(Selected()) - self.assertEqual(selected, "value") + self.assertEqual(selected, ["value"]) + self.assertIs(selected[0], "value") + self.assertIs(selected[0], object_list.selected_column) - @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_table_editor_select_columns(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) object_list.selected_columns = ["value", "other_value"] - with reraise_exceptions(), create_ui( - object_list, dict(view=select_columns_view) - ) as ui: - editor = ui.get_editors("values")[0] - process_cascade_events() - if is_qt(): - selected = editor.selected - elif is_wx(): - selected = editor.selected_columns - - process_cascade_events() + tester = UITester() + with tester.create_ui(object_list, dict(view=select_columns_view)) \ + as ui: + values_table = tester.find_by_name(ui, "values") + selected = values_table.inspect(Selected()) self.assertEqual(selected, ["value", "other_value"]) - @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_table_editor_select_column_index(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) - object_list.selected_index = 1 view = select_column_index_view - with reraise_exceptions(), create_ui( - object_list, dict(view=view) - ) as ui: - editor = ui.get_editors("values")[0] - process_cascade_events() - if is_qt(): - selected = editor.selected_indices - elif is_wx(): - selected = editor.selected_column_index + tester = UITester() + with tester.create_ui(object_list, dict(view=view)) as ui: + # click a cell in the index 1 column + values_table = tester.find_by_name(ui, "values") + col1_cell = values_table.locate(Cell(0, 1)) + col1_cell.perform(MouseClick()) - process_cascade_events() + selected = values_table.inspect(SelectedIndices()) - self.assertEqual(selected, 1) + self.assertEqual(selected, [1]) - @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_table_editor_select_column_indices(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] @@ -469,42 +459,83 @@ def test_table_editor_select_column_indices(self): object_list.selected_indices = [0, 1] view = select_column_indices_view - with reraise_exceptions(), create_ui( - object_list, dict(view=view) - ) as ui: - editor = ui.get_editors("values")[0] - process_cascade_events() - if is_qt(): - selected = editor.selected_indices - elif is_wx(): - selected = editor.selected_column_indices - - process_cascade_events() + tester = UITester() + with tester.create_ui(object_list, dict(view=view)) as ui: + values_table = tester.find_by_name(ui, "values") + selected = values_table.inspect(SelectedIndices()) self.assertEqual(selected, [0, 1]) - @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_table_editor_select_cell(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) - object_list.selected_cell = (object_list.values[5], "value") - with reraise_exceptions(), create_ui( - object_list, dict(view=select_cell_view) - ) as ui: - editor = ui.get_editors("values")[0] - process_cascade_events() - if is_qt(): - selected = editor.selected - elif is_wx(): - selected = editor.selected_cell + tester = UITester() + with tester.create_ui(object_list, dict(view=select_cell_view)) as ui: + # click the cell at (5,0) + values_table = tester.find_by_name(ui, "values") + cell_5_0 = values_table.locate(Cell(5, 0)) + cell_5_0.perform(MouseClick()) + + selected = values_table.inspect(Selected()) + + self.assertEqual(selected, [(object_list.values[5], "value")]) + self.assertIs(selected[0], object_list.selected_cell) + + def test_table_editor_modify_cell_with_tester(self): + object_list = ObjectListWithSelection( + values=[ListItem(value=str(i ** 2)) for i in range(10)] + ) + tester = UITester() + with tester.create_ui(object_list, dict(view=select_row_view)) as ui: + wrapper = tester.find_by_name(ui, "values").locate(Cell(5, 0)) + wrapper.perform(MouseClick()) # activate edit mode + wrapper.perform(KeySequence("abc")) + self.assertEqual(object_list.selected.value, "abc") + + # second column refers to an Int type + original = object_list.selected.other_value + wrapper = tester.find_by_name(ui, "values").locate(Cell(5, 1)) + wrapper.perform(MouseClick()) + wrapper.perform(KeySequence("abc")) # invalid + self.assertEqual(object_list.selected.other_value, original) + + for _ in range(3): + wrapper.perform(KeyClick("Backspace")) + wrapper.perform(KeySequence("12")) # now ok + self.assertEqual(object_list.selected.other_value, 12) + + def test_table_editor_check_display_with_tester(self): + object_list = ObjectListWithSelection( + values=[ListItem(other_value=0)] + ) + tester = UITester() + with tester.create_ui(object_list, dict(view=select_row_view)) as ui: + wrapper = tester.find_by_name(ui, "values").locate(Cell(0, 1)) + + actual = wrapper.inspect(DisplayedText()) + self.assertEqual(actual, "0") + + object_list.values[0].other_value = 123 + + actual = wrapper.inspect(DisplayedText()) + self.assertEqual(actual, "123") - process_cascade_events() + def test_table_editor_escape_retain_edit(self): + object_list = ObjectListWithSelection( + values=[ListItem(other_value=0)] + ) + tester = UITester() + with tester.create_ui(object_list, dict(view=select_row_view)) as ui: + cell = tester.find_by_name(ui, "values").locate(Cell(0, 1)) - self.assertEqual(selected, (object_list.values[5], "value")) + cell.perform(MouseClick()) + cell.perform(KeySequence("123")) + cell.perform(KeyClick("Esc")) # exit edit mode, did not revert + + self.assertEqual(object_list.values[0].other_value, 123) - @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_table_editor_select_cells(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] @@ -515,17 +546,10 @@ def test_table_editor_select_cells(self): (object_list.values[8], "value"), ] - with reraise_exceptions(), create_ui( - object_list, dict(view=select_cells_view) - ) as ui: - editor = ui.get_editors("values")[0] - process_cascade_events() - if is_qt(): - selected = editor.selected - elif is_wx(): - selected = editor.selected_cells - - process_cascade_events() + tester = UITester() + with tester.create_ui(object_list, dict(view=select_cells_view)) as ui: + values_table = tester.find_by_name(ui, "values") + selected = values_table.inspect(Selected()) self.assertEqual( selected, @@ -536,29 +560,22 @@ def test_table_editor_select_cells(self): ], ) - @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_table_editor_select_cell_index(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) - object_list.selected_cell_index = (5, 1) view = select_cell_index_view - with reraise_exceptions(), create_ui( - object_list, dict(view=view) - ) as ui: - editor = ui.get_editors("values")[0] - process_cascade_events() - if is_qt(): - selected = editor.selected_indices - elif is_wx(): - selected = editor.selected_cell_index + tester = UITester() + with tester.create_ui(object_list, dict(view=view)) as ui: + # click the cell at (5,1) + values_table = tester.find_by_name(ui, "values") + cell_5_1 = values_table.locate(Cell(5, 1)) + cell_5_1.perform(MouseClick()) + selected = values_table.inspect(SelectedIndices()) - process_cascade_events() + self.assertEqual(selected, [(5, 1)]) - self.assertEqual(selected, (5, 1)) - - @requires_toolkit([ToolkitName.qt, ToolkitName.wx]) def test_table_editor_select_cell_indices(self): object_list = ObjectListWithSelection( values=[ListItem(value=str(i ** 2)) for i in range(10)] @@ -566,21 +583,13 @@ def test_table_editor_select_cell_indices(self): object_list.selected_cell_indices = [(5, 0), (6, 1), (8, 0)] view = select_cell_indices_view - with reraise_exceptions(), create_ui( - object_list, dict(view=view) - ) as ui: - editor = ui.get_editors("values")[0] - process_cascade_events() - if is_qt(): - selected = editor.selected_indices - elif is_wx(): - selected = editor.selected_cell_indices - - process_cascade_events() + tester = UITester() + with tester.create_ui(object_list, dict(view=view)) as ui: + values_table = tester.find_by_name(ui, "values") + selected = values_table.inspect(SelectedIndices()) self.assertEqual(selected, [(5, 0), (6, 1), (8, 0)]) - @requires_toolkit([ToolkitName.qt]) def test_progress_column(self): from traitsui.extras.progress_column import ProgressColumn @@ -600,13 +609,10 @@ def test_progress_column(self): object_list = ObjectList( values=[ListItem(value=str(i ** 2)) for i in range(10)] ) + tester = UITester() + with tester.create_ui(object_list, dict(view=progress_view)): + pass - with reraise_exceptions(), create_ui( - object_list, dict(view=progress_view) - ): - process_cascade_events() - - @requires_toolkit([ToolkitName.qt]) def test_on_perform_action(self): # A test for issue #741, where actions with an on_perform function set # would get called twice @@ -616,11 +622,29 @@ def test_on_perform_action(self): mock_function = Mock() action = Action(on_perform=mock_function) - with reraise_exceptions(), create_ui( - object_list, dict(view=simple_view) - ) as ui: - editor = ui.get_editors("values")[0] - process_cascade_events() + tester = UITester() + with tester.create_ui(object_list, dict(view=simple_view)) as ui: + editor = tester.find_by_name(ui, "values")._target editor.set_menu_context(None, None, None) editor.perform(action) mock_function.assert_called_once() + + def test_edit_on_first_click_false(self): + object_list = ObjectListWithSelection( + values=[ListItem(value=str(i ** 2)) for i in range(10)] + ) + tester = UITester() + with tester.create_ui( + object_list, + dict(view=edit_on_first_click_false_view) + ) as ui: + wrapper = tester.find_by_name(ui, "values").locate(Cell(5, 0)) + # single click will not activate edit mode + wrapper.perform(MouseClick()) + with self.assertRaises(Disabled): + wrapper.perform(KeySequence("abc")) + + # double click will activate edit mode + wrapper.perform(MouseDClick()) + wrapper.perform(KeySequence("abc")) + self.assertEqual(object_list.values[5].value, "abc")