From de3c6cbe351b18e59518b51c77ac47ec14cb751b Mon Sep 17 00:00:00 2001 From: Ieva Date: Mon, 8 Jun 2020 16:36:16 +0100 Subject: [PATCH] Add more ListStrEditor tests (#869) * Add more tests for ListStrEditor * Minor wx ListStrEditor fix * Add gui.process_events to fix pyface2 errors * Rename TestListStrEditor with adapter test to TestListStrAdapter * Add docstrings and clarification comments * Skip tests on Windows due to potential test interactions * Fix clear_selection usage --- traitsui/tests/editors/test_liststr_editor.py | 2 +- .../editors/test_liststr_editor_selection.py | 589 +++++++++++++++++- traitsui/wx/list_str_editor.py | 2 +- 3 files changed, 573 insertions(+), 20 deletions(-) diff --git a/traitsui/tests/editors/test_liststr_editor.py b/traitsui/tests/editors/test_liststr_editor.py index 687f3cc2a..237136c2e 100644 --- a/traitsui/tests/editors/test_liststr_editor.py +++ b/traitsui/tests/editors/test_liststr_editor.py @@ -26,7 +26,7 @@ class TraitObject(HasTraits): list_str = List(Str) -class TestListStrEditor(unittest.TestCase): +class TestListStrAdapter(unittest.TestCase): def test_list_str_adapter_length(self): """Test the ListStringAdapter len method""" diff --git a/traitsui/tests/editors/test_liststr_editor_selection.py b/traitsui/tests/editors/test_liststr_editor_selection.py index ab601f39c..3d05be0ee 100644 --- a/traitsui/tests/editors/test_liststr_editor_selection.py +++ b/traitsui/tests/editors/test_liststr_editor_selection.py @@ -19,8 +19,10 @@ A ListStrEditor was not checking for valid item indexes under Wx. This was most noticeable when the selected_index was set in the editor factory. """ +import platform import unittest +from pyface.gui import GUI from traits.has_traits import HasTraits from traits.trait_types import List, Int, Str from traitsui.item import Item @@ -29,12 +31,21 @@ from traitsui.tests._tools import ( create_ui, + is_current_backend_wx, + is_current_backend_qt4, press_ok_button, skip_if_not_qt4, skip_if_not_wx, + skip_if_null, store_exceptions_on_all_threads, ) +is_windows = platform.system() == "Windows" + + +class ListStrModel(HasTraits): + value = List(["one", "two", "three"]) + class ListStrEditorWithSelectedIndex(HasTraits): values = List(Str()) @@ -43,6 +54,15 @@ class ListStrEditorWithSelectedIndex(HasTraits): selected = Str() +def get_view(**kwargs): + return View( + Item( + "value", + editor=ListStrEditor(**kwargs), + ) + ) + + single_select_view = View( Item( "values", @@ -75,21 +95,555 @@ class ListStrEditorWithSelectedIndex(HasTraits): ) -def get_selected(control): +def get_selected_indices(editor): """ Returns a list of the indices of all currently selected list items. """ - import wx + if is_current_backend_wx(): + import wx + # "item" in this context means "index of the item" + item = -1 + selected = [] + while True: + item = editor.control.GetNextItem( + item, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED + ) + if item == -1: + break + selected.append(item) + return selected + + elif is_current_backend_qt4(): + indices = editor.list_view.selectionModel().selectedRows() + return [i.row() for i in indices] + + else: + raise unittest.SkipTest("Test not implemented for this toolkit") + + +def set_selected_single(editor, index): + """ Selects a specified item in an editor with multi_select=False. + """ + if is_current_backend_wx(): + editor.control.Select(index) + + elif is_current_backend_qt4(): + from pyface.qt.QtGui import QItemSelectionModel + + smodel = editor.list_view.selectionModel() + mi = editor.model.index(index) + smodel.select(mi, QItemSelectionModel.ClearAndSelect) + + else: + raise unittest.SkipTest("Test not implemented for this toolkit") + + +def set_selected_multiple(editor, indices): + """ Clears old selection and selects specified items in an editor with + multi_select=True. + """ + if is_current_backend_wx(): + clear_selection(editor) + for index in indices: + editor.control.Select(index) + + elif is_current_backend_qt4(): + from pyface.qt.QtGui import QItemSelectionModel + + clear_selection(editor) + smodel = editor.list_view.selectionModel() + for index in indices: + mi = editor.model.index(index) + smodel.select(mi, QItemSelectionModel.Select) + + else: + raise unittest.SkipTest("Test not implemented for this toolkit") + + +def clear_selection(editor): + """ Clears existing selection. + """ + if is_current_backend_wx(): + import wx + + currently_selected = get_selected_indices(editor) + # Deselect all currently selected items + for selected_index in currently_selected: + editor.control.SetItemState( + selected_index, 0, wx.LIST_STATE_SELECTED + ) + + elif is_current_backend_qt4(): + editor.list_view.selectionModel().clearSelection() + + else: + raise unittest.SkipTest("Test not implemented for this toolkit") + + +def right_click_item(control, index): + """ Right clicks on the specified item. + """ + + if is_current_backend_wx(): + import wx + + event = wx.ListEvent( + wx.EVT_LIST_ITEM_RIGHT_CLICK.typeId, control.GetId() + ) + event.SetIndex(index) + wx.PostEvent(control, event) + + elif is_current_backend_qt4(): + # Couldn't figure out how to close the context menu programatically + raise unittest.SkipTest("Test not implemented for this toolkit") + + else: + raise unittest.SkipTest("Test not implemented for this toolkit") + + +@unittest.skipIf( + is_windows and is_current_backend_qt4(), + "Issue enthought/traitsui#854; possible test interactions on Windows" +) +@unittest.skipIf(is_current_backend_wx(), "Issue enthought/traitsui#752") +@skip_if_null +class TestListStrEditor(unittest.TestCase): + + def setup_gui(self, model, view): + gui = GUI() + ui = model.edit_traits(view=view) + self.addCleanup(ui.dispose) + + gui.process_events() + editor = ui.get_editors("value")[0] + + return gui, editor + + def test_list_str_editor_single_selection(self): + with store_exceptions_on_all_threads(): + gui, editor = self.setup_gui(ListStrModel(), get_view()) + + if is_current_backend_qt4(): # No initial selection + self.assertEqual(editor.selected_index, -1) + self.assertEqual(editor.selected, None) + elif is_current_backend_wx(): # First element selected initially + self.assertEqual(editor.selected_index, 0) + self.assertEqual(editor.selected, "one") + + set_selected_single(editor, 1) + gui.process_events() + + self.assertEqual(editor.selected_index, 1) + self.assertEqual(editor.selected, "two") + + set_selected_single(editor, 2) + gui.process_events() + + self.assertEqual(editor.selected_index, 2) + self.assertEqual(editor.selected, "three") + + clear_selection(editor) + gui.process_events() + + self.assertEqual(editor.selected_index, -1) + self.assertEqual(editor.selected, None) + + def test_list_str_editor_multi_selection(self): + view = get_view(multi_select=True) + + with store_exceptions_on_all_threads(): + gui, editor = self.setup_gui(ListStrModel(), view) + + self.assertEqual(editor.multi_selected_indices, []) + self.assertEqual(editor.multi_selected, []) + + set_selected_multiple(editor, [0, 1]) + gui.process_events() + + self.assertEqual(editor.multi_selected_indices, [0, 1]) + self.assertEqual(editor.multi_selected, ["one", "two"]) + + set_selected_multiple(editor, [2]) + gui.process_events() + + self.assertEqual(editor.multi_selected_indices, [2]) + self.assertEqual(editor.multi_selected, ["three"]) + + clear_selection(editor) + gui.process_events() + + self.assertEqual(editor.multi_selected_indices, []) + self.assertEqual(editor.multi_selected, []) + + def test_list_str_editor_single_selection_changed(self): + with store_exceptions_on_all_threads(): + gui, editor = self.setup_gui(ListStrModel(), get_view()) + + if is_current_backend_qt4(): # No initial selection + self.assertEqual(get_selected_indices(editor), []) + elif is_current_backend_wx(): # First element selected initially + self.assertEqual(get_selected_indices(editor), [0]) + + editor.selected_index = 1 + gui.process_events() + + self.assertEqual(get_selected_indices(editor), [1]) + self.assertEqual(editor.selected, "two") + + editor.selected = "three" + gui.process_events() + + self.assertEqual(get_selected_indices(editor), [2]) + self.assertEqual(editor.selected_index, 2) + + # Selected set to invalid value doesn't change anything + editor.selected = "four" + gui.process_events() + + self.assertEqual(get_selected_indices(editor), [2]) + self.assertEqual(editor.selected_index, 2) + + # Selected index changed to + editor.selected_index = -1 + gui.process_events() + + if is_current_backend_qt4(): + # -1 clears selection + self.assertEqual(get_selected_indices(editor), []) + self.assertEqual(editor.selected, None) + elif is_current_backend_wx(): + # Visually selects everything but doesn't update `selected` + self.assertEqual(editor.selected, "four") + self.assertEqual(get_selected_indices(editor), [0, 1, 2]) + + def test_list_str_editor_multi_selection_changed(self): + view = get_view(multi_select=True) + + with store_exceptions_on_all_threads(): + gui, editor = self.setup_gui(ListStrModel(), view) - selected = [] - item = -1 - while True: - item = control.GetNextItem( - item, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED + self.assertEqual(get_selected_indices(editor), []) + + editor.multi_selected_indices = [0, 1] + gui.process_events() + + self.assertEqual(get_selected_indices(editor), [0, 1]) + self.assertEqual(editor.multi_selected, ["one", "two"]) + + editor.multi_selected = ["three", "one"] + gui.process_events() + + self.assertEqual(sorted(get_selected_indices(editor)), [0, 2]) + self.assertEqual(sorted(editor.multi_selected_indices), [0, 2]) + + editor.multi_selected = ["three", "four"] + gui.process_events() + + if is_current_backend_qt4(): + # Invalid values assigned to multi_selected are ignored + self.assertEqual(get_selected_indices(editor), [2]) + self.assertEqual(editor.multi_selected_indices, [2]) + elif is_current_backend_wx(): + # Selection indices are not updated at all + self.assertEqual(get_selected_indices(editor), [0, 2]) + self.assertEqual(editor.multi_selected_indices, [0, 2]) + + # Setting selected indices to an empty list clears selection + editor.multi_selected_indices = [] + gui.process_events() + + self.assertEqual(get_selected_indices(editor), []) + self.assertEqual(editor.multi_selected, []) + + def test_list_str_editor_multi_selection_items_changed(self): + view = get_view(multi_select=True) + + with store_exceptions_on_all_threads(): + gui, editor = self.setup_gui(ListStrModel(), view) + + self.assertEqual(get_selected_indices(editor), []) + + editor.multi_selected_indices.extend([0, 1]) + gui.process_events() + + self.assertEqual(get_selected_indices(editor), [0, 1]) + self.assertEqual(editor.multi_selected, ["one", "two"]) + + editor.multi_selected_indices[1] = 2 + gui.process_events() + + self.assertEqual(get_selected_indices(editor), [0, 2]) + self.assertEqual(editor.multi_selected, ["one", "three"]) + + editor.multi_selected[0] = "two" + gui.process_events() + + # FIXME issue enthought/traitsui#791 + if is_current_backend_qt4(): + with self.assertRaises(AssertionError): + self.assertEqual(get_selected_indices(editor), [1, 2]) + self.assertEqual(editor.multi_selected_indices, [1, 2]) + self.assertEqual(get_selected_indices(editor), [2, 0]) + self.assertEqual(editor.multi_selected_indices, [0, 2]) + else: + self.assertEqual(sorted(get_selected_indices(editor)), [1, 2]) + self.assertEqual(sorted(editor.multi_selected_indices), [1, 2]) + + # If a change in multi_selected involves an invalid value, nothing + # is changed + editor.multi_selected[0] = "four" + gui.process_events() + + # FIXME issue enthought/traitsui#791 + if is_current_backend_qt4(): + with self.assertRaises(AssertionError): + self.assertEqual(get_selected_indices(editor), [1, 2]) + self.assertEqual(editor.multi_selected_indices, [1, 2]) + self.assertEqual(get_selected_indices(editor), [2, 0]) + self.assertEqual(editor.multi_selected_indices, [0, 2]) + else: + self.assertEqual(sorted(get_selected_indices(editor)), [1, 2]) + self.assertEqual(sorted(editor.multi_selected_indices), [1, 2]) + + def test_list_str_editor_item_count(self): + gui = GUI() + model = ListStrModel() + + # Without auto_add + with store_exceptions_on_all_threads(), \ + create_ui(model, dict(view=get_view())) as ui: + gui.process_events() + editor = ui.get_editors("value")[0] + self.assertEqual(editor.item_count, 3) + + # With auto_add + with store_exceptions_on_all_threads(), \ + create_ui(model, dict(view=get_view(auto_add=True))) as ui: + gui.process_events() + editor = ui.get_editors("value")[0] + self.assertEqual(editor.item_count, 3) + + def test_list_str_editor_refresh_editor(self): + # Smoke test for refresh_editor/refresh_ + with store_exceptions_on_all_threads(): + gui, editor = self.setup_gui(ListStrModel(), get_view()) + if is_current_backend_qt4(): + editor.refresh_editor() + elif is_current_backend_wx(): + editor._refresh() + gui.process_events() + + @skip_if_not_qt4 + def test_list_str_editor_update_editor_single_qt(self): + # QT editor uses selected items as the source of truth when updating + model = ListStrModel() + + with store_exceptions_on_all_threads(): + gui, editor = self.setup_gui(model, get_view()) + + set_selected_single(editor, 0) + gui.process_events() + # Sanity check + self.assertEqual(editor.selected_index, 0) + self.assertEqual(editor.selected, "one") + + model.value = ["two", "one"] + gui.process_events() + + # Selected remains "one" and indices are updated accordingly + self.assertEqual(get_selected_indices(editor), [1]) + self.assertEqual(editor.selected_index, 1) + self.assertEqual(editor.selected, "one") + + # Removing "one" creates a case of no longer valid selection + model.value = ["two", "three"] + gui.process_events() + + # Internal view model selection is reset, but editor selection + # values are not (see issue enthought/traitsui#872) + self.assertEqual(get_selected_indices(editor), []) + self.assertEqual(editor.selected_index, 1) + self.assertEqual(editor.selected, "one") + + @skip_if_not_wx + def test_list_str_editor_update_editor_single_wx(self): + # WX editor uses selected indices as the source of truth when updating + model = ListStrModel() + + with store_exceptions_on_all_threads(): + gui, editor = self.setup_gui(model, get_view()) + + set_selected_single(editor, 0) + gui.process_events() + # Sanity check + self.assertEqual(editor.selected_index, 0) + self.assertEqual(editor.selected, "one") + + model.value = ["two", "one"] + gui.process_events() + + # Selected_index remains 0 and selected is updated accordingly + self.assertEqual(get_selected_indices(editor), [0]) + self.assertEqual(editor.selected_index, 0) + self.assertEqual(editor.selected, "two") + + # Empty list creates a case of no longer valid selection + model.value = [] + gui.process_events() + + # Internal view model selection is reset, but editor selection + # values are not (see issue enthought/traitsui#872) + self.assertEqual(get_selected_indices(editor), []) + self.assertEqual(editor.selected_index, 0) + self.assertEqual(editor.selected, "two") + + @skip_if_not_qt4 + def test_list_str_editor_update_editor_multi_qt(self): + # QT editor uses selected items as the source of truth when updating + model = ListStrModel() + view = get_view(multi_select=True) + + with store_exceptions_on_all_threads(): + gui, editor = self.setup_gui(model, view) + + set_selected_multiple(editor, [0]) + gui.process_events() + # Sanity check + self.assertEqual(editor.multi_selected_indices, [0]) + self.assertEqual(editor.multi_selected, ["one"]) + + model.value = ["two", "one"] + gui.process_events() + + # Selected remains "one" and indices are updated accordingly + self.assertEqual(get_selected_indices(editor), [1]) + self.assertEqual(editor.multi_selected_indices, [1]) + self.assertEqual(editor.multi_selected, ["one"]) + + # Removing "one" creates a case of no longer valid selection. + model.value = ["two", "three"] + gui.process_events() + + # Internal view model selection is reset, but editor selection + # values are not (see issue enthought/traitsui#872) + self.assertEqual(get_selected_indices(editor), []) + self.assertEqual(editor.multi_selected_indices, [1]) + self.assertEqual(editor.multi_selected, ["one"]) + + @skip_if_not_wx + def test_list_str_editor_update_editor_multi_wx(self): + # WX editor uses selected indices as the source of truth when updating + model = ListStrModel() + view = get_view(multi_select=True) + + with store_exceptions_on_all_threads(): + gui, editor = self.setup_gui(model, view) + + set_selected_multiple(editor, [0]) + gui.process_events() + # Sanity check + self.assertEqual(editor.multi_selected_indices, [0]) + self.assertEqual(editor.multi_selected, ["one"]) + + model.value = ["two", "one"] + gui.process_events() + + # Selected_index remains 0 and selected is updated accordingly + self.assertEqual(get_selected_indices(editor), [0]) + self.assertEqual(editor.multi_selected_indices, [0]) + self.assertEqual(editor.multi_selected, ["two"]) + + # Empty list creates a case of no longer valid selection + model.value = [] + gui.process_events() + + # Internal view model selection is reset, but editor selection + # values are not (see issue enthought/traitsui#872) + self.assertEqual(get_selected_indices(editor), []) + self.assertEqual(editor.multi_selected_indices, [0]) + self.assertEqual(editor.multi_selected, ["two"]) + + @skip_if_not_qt4 # wx editor doesn't have a `callx` method + def test_list_str_editor_callx(self): + model = ListStrModel() + + def change_value(model, value): + model.value = value + + with store_exceptions_on_all_threads(): + gui, editor = self.setup_gui(model, get_view()) + + set_selected_single(editor, 0) + gui.process_events() + # Sanity check + self.assertEqual(editor.selected_index, 0) + self.assertEqual(editor.selected, "one") + + editor.callx(change_value, model, ["two", "one"]) + gui.process_events() + + # Nothing is updated + self.assertEqual(get_selected_indices(editor), [0]) + self.assertEqual(editor.selected_index, 0) + self.assertEqual(editor.selected, "one") + + @skip_if_not_qt4 # wx editor doesn't have a `setx` method + def test_list_str_editor_setx(self): + with store_exceptions_on_all_threads(): + gui, editor = self.setup_gui(ListStrModel(), get_view()) + + set_selected_single(editor, 0) + gui.process_events() + # Sanity check + self.assertEqual(editor.selected_index, 0) + self.assertEqual(editor.selected, "one") + + editor.setx(selected="two") + gui.process_events() + + # Specified attribute is modified + self.assertEqual(editor.selected, "two") + # But nothing else is updated + # FIXME issue enthought/traitsui#867 + with self.assertRaises(AssertionError): + self.assertEqual(get_selected_indices(editor), [0]) + self.assertEqual(editor.selected_index, 0) + self.assertEqual(get_selected_indices(editor), [1]) + self.assertEqual(editor.selected_index, 1) + + def test_list_str_editor_horizontal_lines(self): + # Smoke test for painting horizontal lines + with store_exceptions_on_all_threads(): + self.setup_gui(ListStrModel(), get_view(horizontal_lines=True)) + + def test_list_str_editor_title(self): + # Smoke test for adding a title + with store_exceptions_on_all_threads(): + self.setup_gui(ListStrModel(), get_view(title="testing")) + + @skip_if_not_wx # see `right_click_item` and issue enthought/traitsui#868 + def test_list_str_editor_right_click(self): + class ListStrModelRightClick(HasTraits): + value = List(["one", "two", "three"]) + right_clicked = Str() + right_clicked_index = Int() + + model = ListStrModelRightClick() + view = get_view( + right_clicked="object.right_clicked", + right_clicked_index="object.right_clicked_index", ) - if item == -1: - break - selected.append(item) - return selected + + with store_exceptions_on_all_threads(): + gui, editor = self.setup_gui(model, view) + + self.assertEqual(model.right_clicked, "") + self.assertEqual(model.right_clicked_index, 0) + + right_click_item(editor.control, 1) + gui.process_events() + + self.assertEqual(model.right_clicked, "two") + self.assertEqual(model.right_clicked_index, 1) class TestListStrEditorSelection(unittest.TestCase): @@ -103,14 +657,14 @@ def test_wx_list_str_selected_index(self): ) with store_exceptions_on_all_threads(), \ create_ui(obj, dict(view=single_select_view)) as ui: + editor = ui.get_editors("values")[0] # the following is equivalent to setting the text in the text # control, then pressing OK - liststrctrl = ui.control.FindWindowByName("listCtrl") - selected_1 = get_selected(liststrctrl) + selected_1 = get_selected_indices(editor) obj.selected_index = 0 - selected_2 = get_selected(liststrctrl) + selected_2 = get_selected_indices(editor) # press the OK button and close the dialog press_ok_button(ui) @@ -128,15 +682,14 @@ def test_wx_list_str_multi_selected_index(self): ) with store_exceptions_on_all_threads(), \ create_ui(obj, dict(view=multi_select_view)) as ui: - + editor = ui.get_editors("values")[0] # the following is equivalent to setting the text in the text # control, then pressing OK - liststrctrl = ui.control.FindWindowByName("listCtrl", ui.control) - selected_1 = get_selected(liststrctrl) + selected_1 = get_selected_indices(editor) obj.selected_indices = [0] - selected_2 = get_selected(liststrctrl) + selected_2 = get_selected_indices(editor) # press the OK button and close the dialog press_ok_button(ui) diff --git a/traitsui/wx/list_str_editor.py b/traitsui/wx/list_str_editor.py index 85b17c77a..f3726efef 100644 --- a/traitsui/wx/list_str_editor.py +++ b/traitsui/wx/list_str_editor.py @@ -404,7 +404,7 @@ def _multi_selected_changed(self, selected): def _multi_selected_items_changed(self, event): """ Handles the editor's 'multi_selected' trait being modified. """ - values = self.values + values = self.value try: self._multi_selected_indices_items_changed( TraitListEvent(