diff --git a/traitsui/testing/tester/compat.py b/traitsui/testing/tester/compat.py new file mode 100644 index 000000000..00053d8f5 --- /dev/null +++ b/traitsui/testing/tester/compat.py @@ -0,0 +1,36 @@ +# Copyright (c) 2005-2020, Enthought, Inc. +# 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! +# +""" This module contains functions used by toolkit specific implementation for +normalizing differences among toolkits (Qt and Wx). +""" + + +def check_key_compat(key): + """ Check if the given key is a unicode character within the range of + values currently supported for emulating key sequences on both Qt and Wx + textboxes. + + Parameters + ---------- + key : str + A unicode character + + Raises + ------ + ValueError + If the unicode character is not within the supported range of values. + """ + # Support for more characters can be added when there are needs. + if ord(key) < 32 or ord(key) >= 127: + raise ValueError( + f"Key {key!r} is currently not supported. " + f"Supported characters between code point 32 - 126." + ) diff --git a/traitsui/testing/tester/qt4/common_ui_targets.py b/traitsui/testing/tester/qt4/common_ui_targets.py index 211b4b658..32a59fde9 100644 --- a/traitsui/testing/tester/qt4/common_ui_targets.py +++ b/traitsui/testing/tester/qt4/common_ui_targets.py @@ -46,7 +46,7 @@ def register(cls, registry): """ handlers = [ (command.KeySequence, - (lambda wrapper, interaction: helpers.key_sequence_qwidget( + (lambda wrapper, interaction: helpers.key_sequence_textbox( wrapper.target.textbox, interaction, wrapper.delay))), (command.KeyClick, (lambda wrapper, interaction: helpers.key_click_qwidget( diff --git a/traitsui/testing/tester/qt4/helpers.py b/traitsui/testing/tester/qt4/helpers.py index b7452c750..8f6f68aa1 100644 --- a/traitsui/testing/tester/qt4/helpers.py +++ b/traitsui/testing/tester/qt4/helpers.py @@ -12,6 +12,7 @@ from pyface.qt import QtCore, QtGui from pyface.qt.QtTest import QTest +from traitsui.testing.tester.compat import check_key_compat from traitsui.testing.tester.exceptions import Disabled from traitsui.qt4.key_event_to_name import key_map as _KEY_MAP @@ -110,6 +111,30 @@ def key_sequence_qwidget(control, interaction, delay): QTest.keyClicks(control, interaction.sequence, delay=delay) +def key_sequence_textbox(control, interaction, delay): + """ Performs simulated typing of a sequence of keys on a widget that is + a textbox. The keys are restricted to values also supported for testing + wx.TextCtrl. + + Parameters + ---------- + control : QWidget + The Qt widget intended to hold text for editing. + e.g. QLineEdit and QTextEdit + interaction : instance of command.KeySequence + The interaction object holding the sequence of key inputs + to be simulated being typed + delay : int + Time delay (in ms) in which each key click in the sequence will be + performed. + """ + for key in interaction.sequence: + check_key_compat(key) + if not control.hasFocus(): + key_click(widget=control, key="End", delay=0) + key_sequence_qwidget(control=control, interaction=interaction, delay=delay) + + def key_click_qwidget(control, interaction, delay): """ Performs simulated typing of a key on the given widget after a delay. diff --git a/traitsui/testing/tester/qt4/implementation/text_editor.py b/traitsui/testing/tester/qt4/implementation/text_editor.py index c69f735e5..128395633 100644 --- a/traitsui/testing/tester/qt4/implementation/text_editor.py +++ b/traitsui/testing/tester/qt4/implementation/text_editor.py @@ -27,7 +27,7 @@ def register(registry): handlers = [ (command.KeySequence, - (lambda wrapper, interaction: helpers.key_sequence_qwidget( + (lambda wrapper, interaction: helpers.key_sequence_textbox( wrapper.target.control, interaction, wrapper.delay))), (command.KeyClick, (lambda wrapper, interaction: helpers.key_click_qwidget( diff --git a/traitsui/testing/tester/qt4/tests/test_helpers.py b/traitsui/testing/tester/qt4/tests/test_helpers.py index 24abd2c7a..d5f2f9c12 100644 --- a/traitsui/testing/tester/qt4/tests/test_helpers.py +++ b/traitsui/testing/tester/qt4/tests/test_helpers.py @@ -82,6 +82,87 @@ def test_key_sequence(self): 0) self.assertEqual(textbox.text(), "") + def test_key_sequence_textbox_with_unicode(self): + for code in range(32, 127): + with self.subTest(code=code, word=chr(code)): + textbox = QtGui.QLineEdit() + change_slot = mock.Mock() + textbox.textChanged.connect(change_slot) + + # when + helpers.key_sequence_textbox( + textbox, + command.KeySequence(chr(code) * 3), + delay=0, + ) + + # then + self.assertEqual(textbox.text(), chr(code) * 3) + self.assertEqual(change_slot.call_count, 3) + + def test_key_sequence_unsupported_key(self): + textbox = QtGui.QLineEdit() + + with self.assertRaises(ValueError) as exception_context: + # QTest does not support this character. + helpers.key_sequence_textbox( + textbox, + command.KeySequence(chr(31)), + delay=0, + ) + + self.assertIn( + "is currently not supported.", + str(exception_context.exception), + ) + + def test_key_sequence_backspace_character(self): + # Qt does convert backspace character to the backspace key + # But we disallow it for now to be consistent with wx. + textbox = QtGui.QLineEdit() + + with self.assertRaises(ValueError) as exception_context: + helpers.key_sequence_textbox( + textbox, + command.KeySequence("\b"), + delay=0, + ) + + self.assertIn( + "is currently not supported.", + str(exception_context.exception), + ) + + def test_key_sequence_insert_point_qlineedit(self): + textbox = QtGui.QLineEdit() + textbox.setText("123") + + # when + helpers.key_sequence_textbox( + textbox, + command.KeySequence("abc"), + delay=0, + ) + + # then + self.assertEqual(textbox.text(), "123abc") + + def test_key_sequence_insert_point_qtextedit(self): + # The default insertion point moved to the end to be consistent + # with QLineEdit + textbox = QtGui.QTextEdit() + textbox.setText("123") + + # when + helpers.key_sequence_textbox( + textbox, + command.KeySequence("abc"), + delay=0, + ) + + # then + self.assertEqual(textbox.toPlainText(), "123abc") + def test_key_sequence_disabled(self): textbox = QtGui.QLineEdit() textbox.setEnabled(False) diff --git a/traitsui/testing/tester/wx/helpers.py b/traitsui/testing/tester/wx/helpers.py index 985329d1b..d0ec801f4 100644 --- a/traitsui/testing/tester/wx/helpers.py +++ b/traitsui/testing/tester/wx/helpers.py @@ -11,6 +11,7 @@ import wx +from traitsui.testing.tester.compat import check_key_compat from traitsui.testing.tester.exceptions import Disabled from traitsui.wx.key_event_to_name import key_map as _KEY_MAP @@ -135,9 +136,15 @@ def key_sequence_text_ctrl(control, interaction, delay): Time delay (in ms) in which each key click in the sequence will be performed. """ + # fail early + for char in interaction.sequence: + check_key_compat(char) + if not control.IsEditable(): raise Disabled("{!r} is disabled.".format(control)) if not control.HasFocus(): control.SetFocus() + control.SetInsertionPointEnd() for char in interaction.sequence: - key_click(control, char, delay) + wx.MilliSleep(delay) + control.WriteText(char) diff --git a/traitsui/testing/tester/wx/tests/test_helpers.py b/traitsui/testing/tester/wx/tests/test_helpers.py index 2e5e81c35..81affca85 100644 --- a/traitsui/testing/tester/wx/tests/test_helpers.py +++ b/traitsui/testing/tester/wx/tests/test_helpers.py @@ -64,11 +64,50 @@ def test_mouse_click_disabled_button(self): self.assertEqual(handler.call_count, 0) def test_key_sequence(self): + # The insertion point is moved to the end textbox = wx.TextCtrl(self.frame) + textbox.SetValue("123") + handler = mock.Mock() + textbox.Bind(wx.EVT_TEXT, handler) helpers.key_sequence_text_ctrl(textbox, command.KeySequence("abc"), 0) - self.assertEqual(textbox.Value, "abc") + self.assertEqual(textbox.GetValue(), "123abc") + self.assertEqual(handler.call_count, 3) + + def test_key_sequence_with_unicode(self): + handler = mock.Mock() + textbox = wx.TextCtrl(self.frame) + textbox.Bind(wx.EVT_TEXT, handler) + # This range is supported by Qt + for code in range(32, 127): + with self.subTest(code=code, word=chr(code)): + textbox.Clear() + handler.reset_mock() + + # when + helpers.key_sequence_text_ctrl( + textbox, + command.KeySequence(chr(code) * 3), + delay=0, + ) + + # then + self.assertEqual(textbox.Value, chr(code) * 3) + self.assertEqual(handler.call_count, 3) + + def test_key_sequence_with_backspace_unsupported(self): + textbox = wx.TextCtrl(self.frame) + + with self.assertRaises(ValueError) as exception_context: + helpers.key_sequence_text_ctrl( + textbox, command.KeySequence("\b"), 0 + ) + + self.assertIn( + "is currently not supported.", + str(exception_context.exception), + ) def test_key_sequence_disabled(self): textbox = wx.TextCtrl(self.frame)