Merge lp:~veebers/autopilot/add_OSK_keyboard_backend into lp:autopilot

Proposed by Christopher Lee
Status: Merged
Approved by: Thomi Richards
Approved revision: 327
Merged at revision: 313
Proposed branch: lp:~veebers/autopilot/add_OSK_keyboard_backend
Merge into: lp:autopilot
Diff against target: 601 lines (+444/-8)
5 files modified
autopilot/input/__init__.py (+67/-3)
autopilot/input/_osk.py (+121/-0)
autopilot/tests/functional/test_input_stack.py (+130/-1)
docs/faq/faq.rst (+30/-3)
docs/tutorial/advanced_autopilot.rst (+96/-1)
To merge this branch: bzr merge lp:~veebers/autopilot/add_OSK_keyboard_backend
Reviewer Review Type Date Requested Status
Thomi Richards (community) Approve
PS Jenkins bot continuous-integration Approve
Review via email: mp+181456@code.launchpad.net

Commit message

Adds the Ubuntu Keyboard OSK as an input backend

Description of the change

Adds the Ubuntu Keyboard OSK as an input backend

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
323. By Christopher Lee

Documentation updates.

324. By Christopher Lee

Fix exception raised and docs

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
325. By Christopher Lee

Further docs improvements

326. By Christopher Lee

Minor doc change

Revision history for this message
Thomi Richards (thomir-deactivatedaccount) wrote :

LGTM

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
327. By Christopher Lee

Re-order OSK Keyboard backend priority

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Thomi Richards (thomir-deactivatedaccount) wrote :

Still LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'autopilot/input/__init__.py'
2--- autopilot/input/__init__.py 2013-07-23 04:04:43 +0000
3+++ autopilot/input/__init__.py 2013-08-23 05:01:40 +0000
4@@ -56,6 +56,7 @@
5 """
6
7 from collections import OrderedDict
8+from contextlib import contextmanager
9 from autopilot.utilities import _pick_backend, CleanupRegistered
10 from autopilot.input._common import get_center_point
11
12@@ -81,12 +82,21 @@
13 For more infomration on picking specific backends, see
14 :ref:`tut-picking-backends`
15
16+ For details regarding backend limitations please see:
17+ :ref:`Keyboard backend limitations<keyboard_backend_limitations>`
18+
19+ .. warning:: The **OSK** (On Screen Keyboard) backend option does not
20+ implement either :py:meth:`press` or :py:meth:`release` methods due to
21+ technical implementation details and will raise a NotImplementedError
22+ exception if used.
23+
24 :param preferred_backend: A string containing a hint as to which
25 backend you would like. Possible backends are:
26
27 * ``X11`` - Generate keyboard events using the X11 client
28 libraries.
29 * ``UInput`` - Use UInput kernel-level device driver.
30+ * ``OSK`` - Use the graphical On Screen Keyboard as a backend.
31
32 :raises: RuntimeError if autopilot cannot instantate any of the
33 possible backends.
34@@ -104,17 +114,67 @@
35 from autopilot.input._uinput import Keyboard
36 return Keyboard()
37
38+ def get_osk_kb():
39+ try:
40+ from autopilot.input._osk import Keyboard
41+ return Keyboard()
42+ except ImportError as e:
43+ e.args += ("Unable to import the OSK backend",)
44+ raise
45+
46 backends = OrderedDict()
47 backends['X11'] = get_x11_kb
48 backends['UInput'] = get_uinput_kb
49+ backends['OSK'] = get_osk_kb
50 return _pick_backend(backends, preferred_backend)
51
52+ @contextmanager
53+ def focused_type(self, input_target, pointer=None):
54+ """Type into an input widget.
55+
56+ This context manager takes care of making sure a particular
57+ *input_target* UI control is selected before any text is entered.
58+
59+ Some backends extend this method to perform cleanup actions at the end
60+ of the context manager block. For example, the OSK backend dismisses
61+ the keyboard.
62+
63+ If the *pointer* argument is None (default) then either a Mouse or
64+ Touch pointer will be created based on the current platform.
65+
66+ An example of using the context manager (with an OSK backend)::
67+
68+ from autopilot.input import Keyboard
69+
70+ text_area = self._launch_test_input_area()
71+ keyboard = Keyboard.create('OSK')
72+ with keyboard.focused_type(text_area) as kb:
73+ kb.type("Hello World.")
74+ self.assertThat(text_area.text, Equals("Hello World"))
75+ # Upon leaving the context managers scope the keyboard is dismissed
76+ # with a swipe
77+
78+ """
79+ if pointer is None:
80+ from autopilot.platform import model
81+ if model() == 'Desktop':
82+ pointer = Pointer(Mouse.create())
83+ else:
84+ pointer = Pointer(Touch.create())
85+
86+ pointer.click_object(input_target)
87+ yield self
88+
89 def press(self, keys, delay=0.2):
90 """Send key press events only.
91
92 :param keys: Keys you want pressed.
93 :param delay: The delay (in Seconds) after pressing the keys before
94 returning control to the caller.
95+ :raises: NotImplementedError If called when using the OSK Backend.
96+
97+ .. warning:: The **OSK** backend does not implement the press method
98+ and will raise a NotImplementedError if called.
99
100 Example:
101
102@@ -131,6 +191,10 @@
103 :param keys: Keys you want released.
104 :param delay: The delay (in Seconds) after releasing the keys before
105 returning control to the caller.
106+ :raises: NotImplementedError If called when using the OSK Backend.
107+
108+ .. warning:: The **OSK** backend does not implement the press method
109+ and will raise a NotImplementedError if called.
110
111 Example:
112
113@@ -219,14 +283,14 @@
114 from autopilot.platform import model
115 if model() != 'Desktop':
116 logger.info(
117- "You cannot create a Mouse on the phablet devices. "
118- "consider using a Touch or Pointer device. "
119+ "You cannot create a Mouse on the devices where X11 is not "
120+ "available. consider using a Touch or Pointer device. "
121 "For more information, see: "
122 "http://unity.ubuntu.com/autopilot/api/input.html"
123 "#autopilot-unified-input-system"
124 )
125 raise RuntimeError(
126- "Cannot create a Mouse on the phablet devices."
127+ "Cannot create a Mouse on devices where X11 is not available."
128 )
129
130 backends = OrderedDict()
131
132=== added file 'autopilot/input/_osk.py'
133--- autopilot/input/_osk.py 1970-01-01 00:00:00 +0000
134+++ autopilot/input/_osk.py 2013-08-23 05:01:40 +0000
135@@ -0,0 +1,121 @@
136+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
137+#
138+# Autopilot Functional Test Tool
139+# Copyright (C) 2013 Canonical
140+#
141+# This program is free software: you can redistribute it and/or modify
142+# it under the terms of the GNU General Public License as published by
143+# the Free Software Foundation, either version 3 of the License, or
144+# (at your option) any later version.
145+#
146+# This program is distributed in the hope that it will be useful,
147+# but WITHOUT ANY WARRANTY; without even the implied warranty of
148+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
149+# GNU General Public License for more details.
150+#
151+# You should have received a copy of the GNU General Public License
152+# along with this program. If not, see <http://www.gnu.org/licenses/>.
153+#
154+
155+import logging
156+from time import sleep
157+from contextlib import contextmanager
158+
159+from ubuntu_keyboard.emulators.keyboard import (
160+ Keyboard as KeyboardDriver,
161+ UnsupportedKey,
162+)
163+
164+from autopilot.input import Keyboard as KeyboardBase
165+
166+
167+logger = logging.getLogger(__name__)
168+
169+
170+class Keyboard(KeyboardBase):
171+
172+ _keyboard = KeyboardDriver()
173+
174+ @contextmanager
175+ def focused_type(self, input_target, pointer=None):
176+ """Ensures that the keyboard is up and ready for input as well as
177+ dismisses the keyboard afterward.
178+
179+ """
180+ with super(Keyboard, self).focused_type(input_target, pointer):
181+ try:
182+ self._keyboard.wait_for_keyboard_ready()
183+ yield self
184+ finally:
185+ self._keyboard.dismiss()
186+
187+ def press(self, keys, delay=0.2):
188+ raise NotImplementedError(
189+ "OSK Backend does not support the press method"
190+ )
191+
192+ def release(self, keys, delay=0.2):
193+ raise NotImplementedError(
194+ "OSK Backend does not support the release method"
195+ )
196+
197+ def press_and_release(self, key, delay=0.2):
198+ """Press and release the key *key*.
199+
200+ The 'key' argument must be a string of the single key you want pressed
201+ and released.
202+
203+ For example::
204+
205+ press_and_release('A')
206+
207+ presses then releases the 'A' key.
208+
209+ :raises: *UnsupportedKey* if the provided key is not supported by the
210+ OSK Backend (or the current OSK langauge layout.)
211+ :raises: *ValueError* if there is more than a single key supplied in
212+ the *key* argument.
213+
214+ """
215+
216+ if len(self._sanitise_keys(key)) != 1:
217+ raise ValueError("Only a single key can be passed in.")
218+
219+ try:
220+ self._keyboard.press_key(key)
221+ sleep(delay)
222+ except UnsupportedKey as e:
223+ e.args += ("OSK Backend is unable to type the key '%s" % key,)
224+ raise
225+
226+ def type(self, string, delay=0.1):
227+ """Simulate a user typing a string of text.
228+
229+ Only 'normal' keys can be typed with this method. There is no such
230+ thing as Alt or Ctrl on the Onscreen Keyboard.
231+
232+ The OSK class back end will take care of ensuring that capitalized
233+ keys are in fact capitalized.
234+
235+ :raises: *UnsupportedKey* if there is a key within the string that is
236+ not supported by the OSK Backend (or the current OSK langauge layout.)
237+
238+ """
239+ if not isinstance(string, basestring):
240+ raise TypeError("'string' argument must be a string.")
241+ logger.debug("Typing text: %s", string)
242+ self._keyboard.type(string, delay)
243+
244+ @classmethod
245+ def on_test_end(cls, test_instance):
246+ """Dismiss (swipe hide) the keyboard.
247+
248+ """
249+ logger.debug("Dismissing the OSK with a swipe.")
250+ cls._keyboard.dismiss()
251+
252+ def _sanitise_keys(self, keys):
253+ if keys == '+':
254+ return [keys]
255+ else:
256+ return keys.split('+')
257
258=== modified file 'autopilot/tests/functional/test_input_stack.py'
259--- autopilot/tests/functional/test_input_stack.py 2013-07-24 08:26:10 +0000
260+++ autopilot/tests/functional/test_input_stack.py 2013-08-23 05:01:40 +0000
261@@ -21,11 +21,13 @@
262 import json
263 import os
264 from tempfile import mktemp
265-from testtools import TestCase
266+from testtools import TestCase, skipIf
267 from testtools.matchers import IsInstance, Equals, raises
268+from textwrap import dedent
269 from unittest import SkipTest
270 from mock import patch
271
272+from autopilot import platform
273 from autopilot.testcase import AutopilotTestCase, multiply_scenarios
274 from autopilot.input import Keyboard, Mouse, Pointer, Touch
275 from autopilot.input._common import get_center_point
276@@ -106,6 +108,24 @@
277 "app shows: " + text_edit.plainText
278 )
279
280+ def test_typing_with_contextmanager(self):
281+ """Typing text must produce the correct characters in the target
282+ app.
283+
284+ """
285+ app_proxy = self.start_mock_app()
286+ text_edit = app_proxy.select_single('QTextEdit')
287+
288+ keyboard = Keyboard.create(self.backend)
289+ with keyboard.focused_type(text_edit) as kb:
290+ kb.type(self.input, 0.01)
291+
292+ self.assertThat(
293+ text_edit.plainText,
294+ Eventually(Equals(self.input)),
295+ "app shows: " + text_edit.plainText
296+ )
297+
298 def test_keyboard_keys_are_released(self):
299 """Typing characters must not leave keys pressed."""
300 app_proxy = self.start_mock_app()
301@@ -137,6 +157,115 @@
302 )
303
304
305+@skipIf(platform.model() == 'Desktop', "Only on device")
306+class OSKBackendTests(AutopilotTestCase):
307+ """Testing the Onscreen Keyboard (Ubuntu Keyboard) backend specifically.
308+
309+ There are limitations (i.e. on device only, window-mocker doesn't work on
310+ the device, can't type all the characters that X11/UInput can.) that
311+ necessitate this split into it's own test class.
312+
313+ """
314+
315+ scenarios = [
316+ ('lower_alpha', dict(input='abcdefghijklmnopqrstuvwxyz')),
317+ ('upper_alpha', dict(input='ABCDEFGHIJKLMNOPQRSTUVWXYZ')),
318+ ('numeric', dict(input='0123456789')),
319+ ('punctuation', dict(input='`~!@#$%^&*()_-+={}[]|\\:;"\'<>,.?/')),
320+ ]
321+
322+ def launch_test_input_area(self):
323+ self.app = self._launch_simple_input()
324+ text_area = self.app.select_single("QQuickTextInput")
325+
326+ return text_area
327+
328+ def _start_qml_script(self, script_contents):
329+ """Launch a qml script."""
330+ qml_path = mktemp(suffix='.qml')
331+ open(qml_path, 'w').write(script_contents)
332+ self.addCleanup(os.remove, qml_path)
333+
334+ return self.launch_test_application(
335+ "qmlscene",
336+ qml_path,
337+ app_type='qt',
338+ )
339+
340+ def _launch_simple_input(self):
341+ simple_script = dedent("""
342+ import QtQuick 2.0
343+ import Ubuntu.Components 0.1
344+
345+ Rectangle {
346+ id: window
347+ objectName: "windowRectangle"
348+ color: "lightgrey"
349+
350+ Text {
351+ id: inputLabel
352+ text: "OSK Tests"
353+ font.pixelSize: units.gu(3)
354+ anchors {
355+ left: input.left
356+ top: parent.top
357+ topMargin: 25
358+ bottomMargin: 25
359+ }
360+ }
361+
362+ TextField {
363+ id: input;
364+ objectName: "input"
365+ anchors {
366+ top: inputLabel.bottom
367+ horizontalCenter: parent.horizontalCenter
368+ topMargin: 10
369+ }
370+ inputMethodHints: Qt.ImhNoPredictiveText
371+ }
372+ }
373+
374+ """)
375+
376+ return self._start_qml_script(simple_script)
377+
378+ def test_can_type_string(self):
379+ """Typing text must produce the expected characters in the input
380+ field.
381+
382+ """
383+
384+ text_area = self.launch_test_input_area()
385+ keyboard = Keyboard.create('OSK')
386+ pointer = Pointer(Touch.create())
387+ pointer.click_object(text_area)
388+ keyboard._keyboard.wait_for_keyboard_ready()
389+
390+ keyboard.type(self.input)
391+
392+ self.assertThat(text_area.text, Eventually(Equals(self.input)))
393+
394+ def test_focused_typing_contextmanager(self):
395+ """Typing text using the 'focused_typing' context manager must not only
396+ produce the expected characters in the input field but also cleanup the
397+ OSK afterwards too.
398+
399+ """
400+ text_area = self.launch_test_input_area()
401+ keyboard = Keyboard.create('OSK')
402+ with keyboard.focused_type(text_area) as kb:
403+ kb.type(self.input)
404+ self.assertThat(
405+ text_area.text,
406+ Eventually(Equals(self.input))
407+ )
408+ self.assertThat(
409+ keyboard._keyboard.is_available,
410+ Eventually(Equals(False))
411+ )
412+
413+
414 class MouseTestCase(AutopilotTestCase):
415
416 def test_move_to_nonint_point(self):
417
418=== modified file 'docs/faq/faq.rst'
419--- docs/faq/faq.rst 2013-07-24 08:38:59 +0000
420+++ docs/faq/faq.rst 2013-08-23 05:01:40 +0000
421@@ -20,7 +20,7 @@
422
423 **I am running the latest development image!**
424
425-In that case you can install autopilot directly - either by installing the ``autopilot-desktop`` or ``autopilot-touch`` packages, depending on whether you are installing to a desktop or phablet device.
426+In that case you can install autopilot directly - either by installing the ``autopilot-desktop`` or ``autopilot-touch`` packages, depending on whether you are installing to a desktop or an Ubuntu Touch device.
427
428 **I am running the Ubuntu release previous to the development release!**
429
430@@ -108,8 +108,8 @@
431
432 In general, autopilot tests are more relaxed about the 'one assertion per test' rule. However, care should still be taken to produce tests that are as small and understandable as possible.
433
434-How do I write a test that uses either a Mouse or a Touch device interchangeably?
435-==============================================================================================
436+Q. How do I write a test that uses either a Mouse or a Touch device interchangeably?
437+====================================================================================
438
439 The :class:`autopilot.input.Pointer` class is a simple wrapper that unifies some of the differences between the :class:`~autopilot.input.Touch` and :class:`~autopilot.input.Mouse` classes. To use it, pass in the device you want to use under the hood, like so::
440
441@@ -136,6 +136,33 @@
442
443 If you only want to use the mouse on certain platforms, use the :mod:`autopilot.platform` module to determine the current platform at runtime.
444
445+Q. How do I use the Onscreen Keyboard (OSK) to input text in my test?
446+=====================================================================
447+
448+The OSK is an backend option for the :meth:`autopilot.input.Keyboard.create`
449+method (see this :ref:`Advanced Autopilot<adv_picking_backend>` section for
450+details regarding backend selection.)
451+
452+Unlike the other backends (X11, UInput) the OSK has a GUI presence and thus can
453+be displayed on the screen.
454+
455+The :class:`autopilot.input.Keyboard` class provides a context manager that
456+handles any cleanup required when dealing with the input backends.
457+
458+For example in the instance when the backend is the OSK, when leaving the scope
459+of the context manager the OSK will be dismissed with a swipe::
460+
461+ from autopilot.input import Keyboard
462+
463+ text_area = self._launch_test_input_area()
464+ keyboard = Keyboard.create('OSK')
465+ with keyboard.focused_type(text_area) as kb:
466+ kb.type("Hello World.")
467+ self.assertThat(text_area.text, Equals("Hello World"))
468+ # At this point now the OSK has been swiped away.
469+ self.assertThat()
470+
471+
472 Autopilot Qt & Gtk Support
473 ++++++++++++++++++++++++++
474
475
476=== modified file 'docs/tutorial/advanced_autopilot.rst'
477--- docs/tutorial/advanced_autopilot.rst 2013-07-24 08:38:59 +0000
478+++ docs/tutorial/advanced_autopilot.rst 2013-08-23 05:01:40 +0000
479@@ -199,10 +199,12 @@
480
481 The code snippet above will create an instance of the Keyboard class that uses X11 on Desktop systems, and UInput on other systems. On the rare occaison when test authors need to construct these objects themselves, we expect that the default creation pattern to be used.
482
483+.. _adv_picking_backend:
484+
485 Picking a Backend
486 +++++++++++++++++
487
488-Test authors may sometimes want to pick a specific backend. The possible backends are documented in the API documentation for each class. For example, the documentation for the :meth:`autopilot.input.Keyboard.create` method says there are two backends available: the ``X11`` backend, and the ``UInput`` backend. These backends can be specified in the create method. For example, to specify that you want a Keyboard that uses X11 to generate it's input events::
489+Test authors may sometimes want to pick a specific backend. The possible backends are documented in the API documentation for each class. For example, the documentation for the :meth:`autopilot.input.Keyboard.create` method says there are three backends available: the ``X11`` backend, the ``UInput`` backend, and the ``OSK`` backend. These backends can be specified in the create method. For example, to specify that you want a Keyboard that uses X11 to generate it's input events::
490
491 >>> from autopilot.input import Keyboard
492 >>> kbd = Keyboard.create("X11")
493@@ -212,8 +214,15 @@
494 >>> from autopilot.input import Keyboard
495 >>> kbd = Keyboard.create("UInput")
496
497+Finally, for the Onscreen Keyboard::
498+
499+ >>> from autopilot.input import Keyboard
500+ >>> kbd = Keyboard.create("OSK")
501+
502 .. warning:: Care must be taken when specifying specific backends. There is no guarantee that the backend you ask for is going to be available across all platforms. For that reason, using the default creation method is encouraged.
503
504+.. warning:: The **OSK** backend has some known implementation limitations, please see :meth:`autopilot.input.Keyboard.create` method documenation for further details.
505+
506 Possible Errors when Creating Backends
507 ++++++++++++++++++++++++++++++++++++++
508
509@@ -249,6 +258,92 @@
510 'UInputError(\'"/dev/uinput" cannot be opened for writing\',)'
511 'BackendException(\'Error while initialising backend. Original exception was: "/dev/uinput" cannot be opened for writing\',)'
512
513+Keyboard Backends
514+=================
515+
516+A quick introduction to the Keyboard backends
517++++++++++++++++++++++++++++++++++++++++++++++
518+
519+Each backend has a different method of operating behind the scenes to provide
520+the Keyboard interface.
521+
522+Here is a quick overview of how each backend works.
523+
524+.. list-table::
525+ :widths: 15, 85
526+ :header-rows: 1
527+
528+ * - Backend
529+ - Description
530+ * - X11
531+ - The X11 backend generates X11 events using a mock input device which it
532+ then syncs with X to actually action the input.
533+ * - Uinput
534+ - The UInput backend injects events directly in to the kernel using the
535+ UInput device driver to produce input.
536+ * - OSK
537+ - The Onscreen Keyboard backend uses the GUI pop-up keyboard to enter
538+ input. Using a pointer object it taps on the required keys to get the
539+ expected output.
540+
541+.. _keyboard_backend_limitations:
542+
543+Limitations of the different Keyboard backends
544+++++++++++++++++++++++++++++++++++++++++++++++
545+
546+While every effort has been made so that the Keyboard devices act the same
547+regardless of which backend or platform is in use, the simple fact is that
548+there can be some technical limitations for some backends.
549+
550+Some of these limitations are hidden when using the "create" method and won't
551+cause any concern (i.e. X11 backend on desktop, UInput on an Ubuntu Touch device.)
552+while others will raise exceptions (that are fully documented in the API docs).
553+
554+Here is a list of known limitations:
555+
556+**X11**
557+
558+* Only available on desktop platforms
559+
560+ - X11 isn't available on Ubuntu Touch devices
561+
562+**UInput**
563+
564+* Requires correct device access permissions
565+
566+ - The user (or group) that are running the autopilot tests need read/write
567+ access to the UInput device (usually /dev/uinput).
568+
569+* Specific kernel support is required
570+
571+ - The kernel on the system running the tests must be running a kernel that
572+ includes UInput support (as well as have the module loaded.
573+
574+**OSK**
575+
576+* Currently only available on Ubuntu Touch devices
577+
578+ - At the time of writing this the OSK/Ubuntu Keyboard is only
579+ supported/available on the Ubuntu Touch devices. It is possible that it
580+ will be available on the desktop in the near future.
581+
582+* Unable to type 'special' keys i.e. Alt
583+
584+ - This shouldn't be an issue as applications running on Ubuntu Touch devices
585+ will be using the expected patterns of use on these platforms.
586+
587+* The following methods have limitations or are not implemented:
588+
589+ - :meth:`autopilot.input.Keyboard.press`: Raises NotImplementedError if
590+ called.
591+
592+ - :meth:`autopilot.input.Keyboard.release`: Raises NotImplementedError if
593+ called.
594+
595+ - :meth:`autopilot.input.Keyboard.press_and_release`: can can only handle
596+ single keys/characters. Raises either ValueError if passed more than a
597+ single character key or UnsupportedKey if passed a key that is not
598+ supported by the OSK backend (or the current language layout).
599
600
601 Process Control

Subscribers

People subscribed via source and target branches