From 6fdce6d35af3ef43e6705d0306a866c5ed649902 Mon Sep 17 00:00:00 2001 From: Ieva Date: Fri, 29 May 2020 18:21:45 +0100 Subject: [PATCH] Add more tests for Enum Editor (#836) * Add more tests for Enum Editor * Add wx EnumEditor tests * Add missing skipif decorator * Add issue references to FIXMEs * Address review comments --- traitsui/tests/_tools.py | 26 ++ traitsui/tests/editors/test_enum_editor.py | 499 +++++++++++++++++---- 2 files changed, 446 insertions(+), 79 deletions(-) diff --git a/traitsui/tests/_tools.py b/traitsui/tests/_tools.py index a23d4e7b5..c01a424e7 100644 --- a/traitsui/tests/_tools.py +++ b/traitsui/tests/_tools.py @@ -177,6 +177,32 @@ def get_dialog_size(ui_control): return ui_control.size().width(), ui_control.size().height() +def get_all_button_status(control): + """Get status of all 2-state (wx) or checkable (qt) buttons under given + control. + + Assumes all sizer children (wx) or layout items (qt) are buttons. + """ + button_status = [] + + if is_current_backend_wx(): + for item in control.GetSizer().GetChildren(): + button = item.GetWindow() + # Ignore empty buttons (assumption that they are invisible) + if button.value != "": + button_status.append(button.GetValue()) + + elif is_current_backend_qt4(): + layout = control.layout() + for i in range(layout.count()): + button = layout.itemAt(i).widget() + button_status.append(button.isChecked()) + + else: + raise NotImplementedError() + + return button_status + # ######### Debug tools diff --git a/traitsui/tests/editors/test_enum_editor.py b/traitsui/tests/editors/test_enum_editor.py index 85bf6c159..01f856528 100644 --- a/traitsui/tests/editors/test_enum_editor.py +++ b/traitsui/tests/editors/test_enum_editor.py @@ -3,9 +3,10 @@ from pyface.gui import GUI -from traits.api import Enum, HasTraits +from traits.api import Enum, HasTraits, Int, List from traitsui.api import EnumEditor, UItem, View from traitsui.tests._tools import ( + get_all_button_status, is_current_backend_qt4, is_current_backend_wx, skip_if_null, @@ -21,31 +22,24 @@ class EnumModel(HasTraits): value = Enum("one", "two", "three", "four") -simple_view = View(UItem("value", style="simple"), resizable=True) +def get_view(style): + return View(UItem("value", style=style), resizable=True) -simple_evaluate_view = View( - UItem( - "value", - editor=EnumEditor( - evaluate=True, values=["one", "two", "three", "four"] +def get_evaluate_view(style, auto_set=True, mode="radio"): + return View( + UItem( + "value", + editor=EnumEditor( + evaluate=True, + values=["one", "two", "three", "four"], + auto_set=auto_set, + mode=mode, + ), + style=style, ), - style="simple", - ), - resizable=True, -) - - -simple_evaluate_view_popup = View( - UItem( - "value", - editor=EnumEditor( - evaluate=True, values=["one", "two", "three", "four"] - ), - style="simple", - ), - resizable=True, -) + resizable=True, + ) def get_combobox_text(combobox): @@ -57,9 +51,13 @@ def get_combobox_text(combobox): return combobox.GetString(combobox.GetSelection()) else: return combobox.GetValue() + elif is_current_backend_qt4(): return combobox.currentText() + else: + raise unittest.SkipTest("Test not implemented for this toolkit") + def set_combobox_text(combobox, text): """ Set the text given a combobox control """ @@ -81,6 +79,9 @@ def set_combobox_text(combobox, text): elif is_current_backend_qt4(): combobox.setEditText(text) + else: + raise unittest.SkipTest("Test not implemented for this toolkit") + def set_combobox_index(combobox, idx): """ Set the choice index given a combobox control and index number """ @@ -100,19 +101,237 @@ def set_combobox_index(combobox, idx): elif is_current_backend_qt4(): combobox.setCurrentIndex(idx) + else: + raise unittest.SkipTest("Test not implemented for this toolkit") -class TestEnumEditor(unittest.TestCase): - def check_enum_text_update(self, view): + +def finish_combobox_text_entry(combobox): + """ Finish text entry given combobox. """ + if is_current_backend_wx(): + import wx + + event = wx.CommandEvent(wx.EVT_TEXT_ENTER.typeId, combobox.GetId()) + wx.PostEvent(combobox, event) + + elif is_current_backend_qt4(): + combobox.lineEdit().editingFinished.emit() + + else: + raise unittest.SkipTest("Test not implemented for this toolkit") + + +def click_radio_button(widget, button_idx): + """ Simulate a radio button click given widget and button number. Assumes + all sizer children (wx) or layout items (qt) are buttons.""" + if is_current_backend_wx(): + import wx + + sizer_items = widget.GetSizer().GetChildren() + button = sizer_items[button_idx].GetWindow() + event = wx.CommandEvent(wx.EVT_RADIOBUTTON.typeId, button.GetId()) + event.SetEventObject(button) + wx.PostEvent(widget, event) + + elif is_current_backend_qt4(): + widget.layout().itemAt(button_idx).widget().click() + + else: + raise unittest.SkipTest("Test not implemented for this toolkit") + + +def get_list_widget_text(list_widget): + """ Return the text of currently selected item in given list widget. """ + if is_current_backend_wx(): + selected_item_idx = list_widget.GetSelection() + return list_widget.GetString(selected_item_idx) + + elif is_current_backend_qt4(): + return list_widget.currentItem().text() + + else: + raise unittest.SkipTest("Test not implemented for this toolkit") + + +def set_list_widget_selected_index(list_widget, idx): + """ Set the choice index given a list widget control and index number. """ + if is_current_backend_wx(): + import wx + + list_widget.SetSelection(idx) + event = wx.CommandEvent(wx.EVT_LISTBOX.typeId, list_widget.GetId()) + wx.PostEvent(list_widget, event) + + elif is_current_backend_qt4(): + list_widget.setCurrentRow(idx) + + else: + raise unittest.SkipTest("Test not implemented for this toolkit") + + +@skip_if_null +class TestEnumEditorMapping(unittest.TestCase): + + def setup_ui(self, model, view): + ui = model.edit_traits(view=view) + self.addCleanup(ui.dispose) + return ui.get_editors("value")[0] + + def check_enum_mappings_value_change(self, style, mode): + class IntEnumModel(HasTraits): + value = Int() + + enum_editor_factory = EnumEditor( + values=[0, 1], + format_func=lambda v: str(bool(v)).upper(), + mode=mode + ) + formatted_view = View( + UItem( + "value", + editor=enum_editor_factory, + style=style, + ) + ) + + with store_exceptions_on_all_threads(): + editor = self.setup_ui(IntEnumModel(), formatted_view) + + # FIXME issue enthought/traitsui#782 + with self.assertRaises(AssertionError): + self.assertEqual(editor.names, ["FALSE", "TRUE"]) + self.assertEqual(editor.mapping, {"FALSE": 0, "TRUE": 1}) + self.assertEqual( + editor.inverse_mapping, {0: "FALSE", 1: "TRUE"} + ) + self.assertEqual(editor.names, ["0", "1"]) + self.assertEqual(editor.mapping, {"0": 0, "1": 1}) + self.assertEqual( + editor.inverse_mapping, {0: "0", 1: "1"} + ) + + enum_editor_factory.values = [1, 0] + + self.assertEqual(editor.names, ["TRUE", "FALSE"]) + self.assertEqual(editor.mapping, {"TRUE": 1, "FALSE": 0}) + self.assertEqual( + editor.inverse_mapping, {1: "TRUE", 0: "FALSE"} + ) + + def check_enum_mappings_name_change(self, style, mode): + class IntEnumModel(HasTraits): + value = Int() + possible_values = List([0, 1]) + + formatted_view = View( + UItem( + 'value', + editor=EnumEditor( + name="object.possible_values", + format_func=lambda v: str(bool(v)).upper(), + mode=mode + ), + style=style, + ) + ) + model = IntEnumModel() + + with store_exceptions_on_all_threads(): + editor = self.setup_ui(model, formatted_view) + + # FIXME issue enthought/traitsui#835 + if is_current_backend_wx(): + with self.assertRaises(AssertionError): + self.assertEqual(editor.names, ["FALSE", "TRUE"]) + self.assertEqual(editor.mapping, {"FALSE": 0, "TRUE": 1}) + self.assertEqual( + editor.inverse_mapping, {0: "FALSE", 1: "TRUE"} + ) + self.assertEqual(editor.names, ["0", "1"]) + self.assertEqual(editor.mapping, {"0": 0, "1": 1}) + self.assertEqual( + editor.inverse_mapping, {0: "0", 1: "1"} + ) + else: + self.assertEqual(editor.names, ["FALSE", "TRUE"]) + self.assertEqual(editor.mapping, {"FALSE": 0, "TRUE": 1}) + self.assertEqual( + editor.inverse_mapping, {0: "FALSE", 1: "TRUE"} + ) + + model.possible_values = [1, 0] + + # FIXME issue enthought/traitsui#835 + if is_current_backend_wx(): + with self.assertRaises(AssertionError): + self.assertEqual(editor.names, ["TRUE", "FALSE"]) + self.assertEqual(editor.mapping, {"TRUE": 1, "FALSE": 0}) + self.assertEqual( + editor.inverse_mapping, {1: "TRUE", 0: "FALSE"} + ) + self.assertEqual(editor.names, ["1", "0"]) + self.assertEqual(editor.mapping, {"1": 1, "0": 0}) + self.assertEqual( + editor.inverse_mapping, {1: "1", 0: "0"} + ) + else: + self.assertEqual(editor.names, ["TRUE", "FALSE"]) + self.assertEqual(editor.mapping, {"TRUE": 1, "FALSE": 0}) + self.assertEqual( + editor.inverse_mapping, {1: "TRUE", 0: "FALSE"} + ) + + def test_simple_editor_mapping_values(self): + self.check_enum_mappings_value_change("simple", "radio") + + def test_simple_editor_mapping_name(self): + self.check_enum_mappings_name_change("simple", "radio") + + def test_radio_editor_mapping_values(self): + # FIXME issue enthought/traitsui#842 + if is_current_backend_wx(): + import wx + + with self.assertRaises(wx._core.wxAssertionError): + self.check_enum_mappings_value_change("custom", "radio") + else: + self.check_enum_mappings_value_change("custom", "radio") + + def test_radio_editor_mapping_name(self): + # FIXME issue enthought/traitsui#842 + if is_current_backend_wx(): + import wx + + with self.assertRaises(wx._core.wxAssertionError): + self.check_enum_mappings_name_change("custom", "radio") + else: + self.check_enum_mappings_name_change("custom", "radio") + + def test_list_editor_mapping_values(self): + self.check_enum_mappings_value_change("custom", "list") + + def test_list_editor_mapping_name(self): + self.check_enum_mappings_name_change("custom", "list") + + +@skip_if_null +class TestSimpleEnumEditor(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] + combobox = editor.control + + return gui, combobox + + def check_enum_text_update(self, view): enum_edit = EnumModel() with store_exceptions_on_all_threads(): - ui = enum_edit.edit_traits(view=view) - self.addCleanup(ui.dispose) - - gui.process_events() - editor = ui.get_editors("value")[0] - combobox = editor.control + gui, combobox = self.setup_gui(enum_edit, view) self.assertEqual(get_combobox_text(combobox), "one") @@ -122,16 +341,10 @@ def check_enum_text_update(self, view): self.assertEqual(get_combobox_text(combobox), "two") def check_enum_object_update(self, view): - gui = GUI() enum_edit = EnumModel() with store_exceptions_on_all_threads(): - ui = enum_edit.edit_traits(view=view) - self.addCleanup(ui.dispose) - - gui.process_events() - editor = ui.get_editors("value")[0] - combobox = editor.control + gui, combobox = self.setup_gui(enum_edit, view) self.assertEqual(enum_edit.value, "one") @@ -141,16 +354,10 @@ def check_enum_object_update(self, view): self.assertEqual(enum_edit.value, "two") def check_enum_index_update(self, view): - gui = GUI() enum_edit = EnumModel() with store_exceptions_on_all_threads(): - ui = enum_edit.edit_traits(view=view) - self.addCleanup(ui.dispose) - - gui.process_events() - editor = ui.get_editors("value")[0] - combobox = editor.control + gui, combobox = self.setup_gui(enum_edit, view) self.assertEqual(enum_edit.value, "one") @@ -160,16 +367,10 @@ def check_enum_index_update(self, view): self.assertEqual(enum_edit.value, "two") def check_enum_text_bad_update(self, view): - gui = GUI() enum_edit = EnumModel() with store_exceptions_on_all_threads(): - ui = enum_edit.edit_traits(view=view) - self.addCleanup(ui.dispose) - - gui.process_events() - editor = ui.get_editors("value")[0] - combobox = editor.control + gui, combobox = self.setup_gui(enum_edit, view) self.assertEqual(enum_edit.value, "one") @@ -178,48 +379,188 @@ def check_enum_text_bad_update(self, view): self.assertEqual(enum_edit.value, "one") - @skip_if_null def test_simple_enum_editor_text(self): - self.check_enum_text_update(simple_view) + self.check_enum_text_update(get_view("simple")) - @skip_if_null def test_simple_enum_editor_index(self): - self.check_enum_index_update(simple_view) + self.check_enum_index_update(get_view("simple")) - @skip_if_null @unittest.skipIf(is_windows, "Test needs fixing on windows") def test_simple_evaluate_editor_text(self): - self.check_enum_text_update(simple_evaluate_view) + self.check_enum_text_update(get_evaluate_view("simple")) - @skip_if_null @unittest.skipIf(is_windows, "Test needs fixing on windows") def test_simple_evaluate_editor_index(self): - self.check_enum_index_update(simple_evaluate_view) + self.check_enum_index_update(get_evaluate_view("simple")) - @skip_if_null def test_simple_evaluate_editor_bad_text(self): - self.check_enum_text_bad_update(simple_evaluate_view) + self.check_enum_text_bad_update(get_evaluate_view("simple")) - @skip_if_null @unittest.skipIf(is_windows, "Test needs fixing on windows") def test_simple_evaluate_editor_object(self): - self.check_enum_object_update(simple_evaluate_view) + self.check_enum_object_update(get_evaluate_view("simple")) - @skip_if_null - @unittest.skipIf(is_windows, "Test needs fixing on windows") - def test_simple_evaluate_popup_editor_text(self): - self.check_enum_text_update(simple_evaluate_view_popup) + def test_simple_evaluate_editor_object_no_auto_set(self): + view = get_evaluate_view("simple", auto_set=False) + enum_edit = EnumModel() - @skip_if_null - @unittest.skipIf(is_windows, "Test needs fixing on windows") - def test_simple_evaluate_popup_editor_index(self): - self.check_enum_index_update(simple_evaluate_view_popup) + with store_exceptions_on_all_threads(): + gui, combobox = self.setup_gui(enum_edit, view) - @skip_if_null - def test_simple_evaluate_popup_editor_bad_text(self): - self.check_enum_text_bad_update(simple_evaluate_view_popup) + self.assertEqual(enum_edit.value, "one") - @skip_if_null - @unittest.skipIf(is_windows, "Test needs fixing on windows") - def test_simple_evaluate_popup_editor_object(self): - self.check_enum_object_update(simple_evaluate_view_popup) + set_combobox_text(combobox, "two") + gui.process_events() + + # wx modifies the value without the need to finish entry + if is_current_backend_qt4(): + self.assertEqual(enum_edit.value, "one") + + finish_combobox_text_entry(combobox) + gui.process_events() + + self.assertEqual(enum_edit.value, "two") + + def test_simple_editor_resizable(self): + # Smoke test for `qt4.enum_editor.SimpleEditor.set_size_policy` + enum_edit = EnumModel() + resizable_view = View(UItem("value", style="simple", resizable=True)) + + with store_exceptions_on_all_threads(): + ui = enum_edit.edit_traits(view=resizable_view) + self.addCleanup(ui.dispose) + + def test_simple_editor_rebuild_editor_evaluate(self): + # Smoke test for `wx.enum_editor.SimpleEditor.rebuild_editor` + enum_editor_factory = EnumEditor( + evaluate=True, + values=["one", "two", "three", "four"], + ) + view = View(UItem("value", editor=enum_editor_factory, style="simple")) + + with store_exceptions_on_all_threads(): + gui, combobox = self.setup_gui(EnumModel(), view) + + enum_editor_factory.values = ["one", "two", "three"] + + +@skip_if_null +class TestRadioEnumEditor(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] + widget = editor.control + + return gui, widget + + def test_radio_enum_editor_button_update(self): + enum_edit = EnumModel() + + with store_exceptions_on_all_threads(): + gui, widget = self.setup_gui(enum_edit, get_view("custom")) + + # The layout is: one, three, four \n two + self.assertEqual( + get_all_button_status(widget), [True, False, False, False] + ) + + enum_edit.value = "two" + gui.process_events() + + self.assertEqual( + get_all_button_status(widget), [False, False, False, True] + ) + + def test_radio_enum_editor_pick(self): + enum_edit = EnumModel() + + with store_exceptions_on_all_threads(): + gui, widget = self.setup_gui(enum_edit, get_view("custom")) + + self.assertEqual(enum_edit.value, "one") + + # The layout is: one, three, four \n two + click_radio_button(widget, 3) + gui.process_events() + + self.assertEqual(enum_edit.value, "two") + + +@skip_if_null +class TestListEnumEditor(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] + list_widget = editor.control + + return gui, list_widget + + def check_enum_text_update(self, view): + enum_edit = EnumModel() + + with store_exceptions_on_all_threads(): + gui, list_widget = self.setup_gui(enum_edit, view) + + self.assertEqual(get_list_widget_text(list_widget), "one") + + enum_edit.value = "two" + gui.process_events() + + self.assertEqual(get_list_widget_text(list_widget), "two") + + def check_enum_index_update(self, view): + enum_edit = EnumModel() + + with store_exceptions_on_all_threads(): + gui, list_widget = self.setup_gui(enum_edit, view) + + self.assertEqual(enum_edit.value, "one") + + set_list_widget_selected_index(list_widget, 1) + gui.process_events() + + self.assertEqual(enum_edit.value, "two") + + def test_list_enum_editor_text(self): + view = View( + UItem( + "value", + editor=EnumEditor( + values=["one", "two", "three", "four"], + mode="list", + ), + style="custom", + ), + resizable=True, + ) + self.check_enum_text_update(view) + + def test_list_enum_editor_index(self): + view = View( + UItem( + "value", + editor=EnumEditor( + values=["one", "two", "three", "four"], + mode="list", + ), + style="custom", + ), + resizable=True, + ) + self.check_enum_index_update(view) + + def test_list_evaluate_editor_text(self): + self.check_enum_text_update(get_evaluate_view("custom", mode="list")) + + def test_list_evaluate_editor_index(self): + self.check_enum_index_update(get_evaluate_view("custom", mode="list"))