Merge lp:~bfiller/ubuntu-keyboard/adding_autopilot_tests_and_emulators into lp:ubuntu-keyboard

Proposed by Bill Filler
Status: Merged
Approved by: Bill Filler
Approved revision: 35
Merged at revision: 23
Proposed branch: lp:~bfiller/ubuntu-keyboard/adding_autopilot_tests_and_emulators
Merge into: lp:ubuntu-keyboard
Diff against target: 1124 lines (+859/-4)
16 files modified
debian/control (+11/-0)
debian/rules (+9/-3)
debian/ubuntu-keyboard-autopilot.install (+1/-0)
qml/Keyboard.qml (+10/-0)
qml/WordRibbon.qml (+3/-0)
src/lib/logic/layouthelper.h (+2/-0)
src/lib/logic/layoutupdater.cpp (+11/-0)
src/lib/models/layout.cpp (+18/-0)
src/lib/models/layout.h (+17/-1)
src/plugin/inputmethod.cpp (+3/-0)
tests/autopilot/setup.py (+32/-0)
tests/autopilot/ubuntu_keyboard/__init__.py (+18/-0)
tests/autopilot/ubuntu_keyboard/emulators/__init__.py (+18/-0)
tests/autopilot/ubuntu_keyboard/emulators/keyboard.py (+346/-0)
tests/autopilot/ubuntu_keyboard/tests/__init__.py (+18/-0)
tests/autopilot/ubuntu_keyboard/tests/test_keyboard.py (+342/-0)
To merge this branch: bzr merge lp:~bfiller/ubuntu-keyboard/adding_autopilot_tests_and_emulators
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Approve
Bill Filler (community) Approve
Thomi Richards Pending
Review via email: mp+181920@code.launchpad.net

This proposal supersedes a proposal from 2013-08-16.

Commit message

adding autopilot test, and re-merge from trunk

Description of the change

Adding autopilot emulators and tests to ubuntu-keyboard.

The tests cover expectations in the design docs as well as other general tests.

The Emulators will allow us to add a backend to autopilot so that we can use the OSK during tests to enter input.

To post a comment you must log in.
Revision history for this message
Michael Terry (mterry) wrote : Posted in a previous version of this proposal

Looking at the debian/* changes, the only change I would make is a small pet peeve of mine, but can you keep the the Depends lines sorted? python* should be after pkg-config, and the lines for the new package too.

As an aside, I'm bummed that autopilot is python2, instead of python3. :-/

Revision history for this message
Thomi Richards (thomir-deactivatedaccount) wrote : Posted in a previous version of this proposal

> As an aside, I'm bummed that autopilot is python2, instead of python3. :-/

man, me too!

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
Thomi Richards (thomir-deactivatedaccount) wrote : Posted in a previous version of this proposal

Python side LGTM

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
Bill Filler (bfiller) wrote : Posted in a previous version of this proposal

approve, there are known failures of the majority of tests due to reported bugs, but those bugs will get fixed as soon as possible. daily image testing of ubuntu-keyboard should not be enabled until that point.

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Bill Filler (bfiller) wrote :

approve, again: there are known failures of the majority of tests due to reported bugs, but those bugs will get fixed as soon as possible. daily image testing of ubuntu-keyboard should not be enabled until that point.

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/control'
2--- debian/control 2013-08-22 02:03:04 +0000
3+++ debian/control 2013-08-23 20:24:27 +0000
4@@ -14,6 +14,8 @@
5 libubuntu-platform-api1-dev,
6 maliit-framework-dev (>= 0.99.0+git20130615+97e8335-0ubuntu3),
7 pkg-config,
8+ python:any (>= 2.7),
9+ python-setuptools,
10 qt5-default,
11 qtbase5-dev,
12 qtdeclarative5-dev,
13@@ -55,3 +57,12 @@
14 ${shlibs:Depends},
15 Description: Ubuntu on-screen keyboard tests
16 Tests for the Ubuntu virtual keyboard
17+
18+Package: ubuntu-keyboard-autopilot
19+Architecture: all
20+Depends: libautopilot-qt,
21+ ${misc:Depends},
22+ ${python:Depends},
23+ ubuntu-keyboard (>= ${source:Version}),
24+Description: Tests and emulators package for ubuntu-keyboard
25+ Autopilot tests for the ubuntu-keyboard package
26\ No newline at end of file
27
28=== modified file 'debian/rules'
29--- debian/rules 2013-08-15 21:07:50 +0000
30+++ debian/rules 2013-08-23 20:24:27 +0000
31@@ -4,7 +4,7 @@
32 export DPKG_GENSYMBOLS_CHECK_LEVEL=4
33
34 %:
35- dh $@ --fail-missing
36+ dh $@ --fail-missing --with python2
37
38 override_dh_auto_configure:
39 dh_auto_configure -- -recursive \
40@@ -19,10 +19,16 @@
41 CONFIG+=enable-qt-mobility
42
43 override_dh_install:
44- # Don't install the tests
45+ # install autopilot tests
46+ cd tests/autopilot; \
47+ set -ex; for python in $(shell pyversions -r); do \
48+ $$python setup.py install --root=$(CURDIR)/debian/tmp --install-layout=deb; \
49+ done; \
50+ cd $(CURDIR)
51+ # Don't install the other tests
52 rm debian/tmp/usr/bin/language-layout-loading
53 rm debian/tmp/usr/bin/language-layout-switching
54 rm debian/tmp/usr/bin/repeat-backspace
55 rm debian/tmp/usr/bin/ut_editor
56 rm debian/tmp/usr/bin/word-candidates
57- dh_install
58+ dh_install -X'*.pyc' --fail-missing
59
60=== added file 'debian/ubuntu-keyboard-autopilot.install'
61--- debian/ubuntu-keyboard-autopilot.install 1970-01-01 00:00:00 +0000
62+++ debian/ubuntu-keyboard-autopilot.install 2013-08-23 20:24:27 +0000
63@@ -0,0 +1,1 @@
64+usr/lib/python*/*/ubuntu_keyboard*
65
66=== modified file 'qml/Keyboard.qml'
67--- qml/Keyboard.qml 2013-08-20 21:43:23 +0000
68+++ qml/Keyboard.qml 2013-08-23 20:24:27 +0000
69@@ -33,6 +33,7 @@
70
71 Item {
72 id: canvas
73+ objectName: "ubuntuKeyboard" // Allow us to specify a specific keyboard within autopilot.
74 property alias layout: keyRepeater.model
75 property variant event_handler
76 property bool area_enabled // MouseArea has no id property so we cannot alias its enabled property.
77@@ -40,6 +41,10 @@
78
79 visible: layout.visible
80
81+ // Expose details for use with Autopilot.
82+ readonly property var layoutState: layout.keyboard_state
83+ readonly property string activeView: layout.activeView
84+
85 property int contentOrientation: Qt.PrimaryOrientation
86
87 property bool shown: false;
88@@ -77,6 +82,7 @@
89
90 WordRibbon {
91 id: wordRibbon
92+ objectName: "wordRibbon"
93
94 anchors.bottom: keypadMouseArea.top
95 width: parent.width;
96@@ -125,6 +131,7 @@
97 }
98
99 Item {
100+ objectName: "keyboardKeypad"
101 id: keyPad
102
103 anchors.top: borderTop.bottom
104@@ -160,6 +167,9 @@
105 Text {
106 id: key_text_item
107
108+ // Expose detail for use within Autopilot
109+ property var action_type: key_action_type
110+
111 anchors.fill: parent
112 text: key_text
113 font.family: key_font
114
115=== modified file 'qml/WordRibbon.qml'
116--- qml/WordRibbon.qml 2013-07-19 12:05:07 +0000
117+++ qml/WordRibbon.qml 2013-08-23 20:24:27 +0000
118@@ -20,6 +20,7 @@
119 Rectangle {
120
121 id: wordRibbonCanvas
122+ objectName: "wordRibbenCanvas"
123 state: "NORMAL"
124
125 Rectangle {
126@@ -29,6 +30,7 @@
127
128 ListView {
129 id: listView
130+ objectName: "wordListView"
131 anchors.fill: parent;
132
133 model: maliit_wordribbon
134@@ -44,6 +46,7 @@
135 id: wordCandidateItem
136 width: wordItem.width + units.gu(2);
137 height: wordRibbonCanvas.height
138+ property alias word_text: wordItem // For testing in Autopilot
139
140 Item {
141 anchors.fill: parent
142
143=== modified file 'src/lib/logic/layouthelper.h'
144--- src/lib/logic/layouthelper.h 2013-07-19 12:05:07 +0000
145+++ src/lib/logic/layouthelper.h 2013-08-23 20:24:27 +0000
146@@ -35,6 +35,7 @@
147 #include "models/key.h"
148 #include "models/keyarea.h"
149 #include "models/wordribbon.h"
150+#include "models/layout.h"
151
152 #include <QtCore>
153
154@@ -143,6 +144,7 @@
155 Q_SLOT void onKeysOverriden(const Logic::KeyOverrides &overriden_keys,
156 bool update);
157
158+ Q_SIGNAL void stateChanged(Model::Layout::State state);
159 private:
160 const QScopedPointer<LayoutHelperPrivate> d_ptr;
161 };
162
163=== modified file 'src/lib/logic/layoutupdater.cpp'
164--- src/lib/logic/layoutupdater.cpp 2013-08-09 15:22:29 +0000
165+++ src/lib/logic/layoutupdater.cpp 2013-08-23 20:24:27 +0000
166@@ -733,6 +733,13 @@
167 converter.setLayoutOrientation(orientation);
168 d->layout->setCenterPanel(d->inShiftedState() ? converter.shiftedKeyArea()
169 : converter.keyArea());
170+
171+ if (d->inShiftedState())
172+ Q_EMIT d->layout->stateChanged(Model::Layout::ShiftedState);
173+ else if (d->inDeadkeyState())
174+ Q_EMIT d->layout->stateChanged(Model::Layout::DeadkeyState);
175+ else
176+ Q_EMIT d->layout->stateChanged(Model::Layout::DefaultState);
177 }
178
179 void LayoutUpdater::switchToPrimarySymView()
180@@ -750,6 +757,8 @@
181
182 // Reset shift state machine, also see switchToMainView.
183 d->shift_machine.restart();
184+
185+ Q_EMIT d->layout->stateChanged(Model::Layout::PrimarySymbolState);
186 }
187
188 void LayoutUpdater::switchToSecondarySymView()
189@@ -764,6 +773,8 @@
190 KeyAreaConverter converter(d->style->attributes(), &d->loader);
191 converter.setLayoutOrientation(orientation);
192 d->layout->setCenterPanel(converter.symbolsKeyArea(1));
193+
194+ Q_EMIT d->layout->stateChanged(Model::Layout::SecondarySymbolState);
195 }
196
197 void LayoutUpdater::switchToAccentedView()
198
199=== modified file 'src/lib/models/layout.cpp'
200--- src/lib/models/layout.cpp 2013-08-09 15:22:29 +0000
201+++ src/lib/models/layout.cpp 2013-08-23 20:24:27 +0000
202@@ -63,6 +63,7 @@
203 KeyArea key_area;
204 QString image_directory;
205 QHash<int, QByteArray> roles;
206+ Layout::State state;
207 QString activeViewId;
208
209 explicit LayoutPrivate();
210@@ -74,6 +75,7 @@
211 , key_area()
212 , image_directory()
213 , roles()
214+ , state(Layout::DefaultState)
215 {
216 // Model roles are used as variables in QML, hence the under_score naming
217 // convention:
218@@ -88,6 +90,7 @@
219 roles[Layout::RoleKeyFontStretch] = "key_font_stretch";
220 roles[Layout::RoleKeyIcon] = "key_icon";
221 roles[Layout::RoleKeyActionInsert] = "key_action_insert";
222+ roles[Layout::RoleKeyAction] = "key_action_type";
223 }
224
225
226@@ -230,6 +233,18 @@
227 qGuiApp->primaryScreen()->orientation()) );
228 }
229
230+Layout::State Layout::state() const
231+{
232+ Q_D(const Layout);
233+ return d->state;
234+}
235+
236+void Layout::setState(Model::Layout::State state)
237+{
238+ Q_D(Layout);
239+ d->state = state;
240+ Q_EMIT stateChanged(state);
241+}
242
243 QString Layout::activeView() const
244 {
245@@ -321,6 +336,9 @@
246
247 case RoleKeyActionInsert:
248 return QVariant(key.action() == Key::ActionInsert);
249+
250+ case RoleKeyAction:
251+ return QVariant(key.action());
252 }
253
254 qWarning() << __PRETTY_FUNCTION__
255
256=== modified file 'src/lib/models/layout.h'
257--- src/lib/models/layout.h 2013-08-09 15:22:29 +0000
258+++ src/lib/models/layout.h 2013-08-23 20:24:27 +0000
259@@ -78,11 +78,22 @@
260 Q_PROPERTY(int invisible_toucharea_height READ invisibleTouchAreaHeight
261 NOTIFY invisibleTouchAreaHeightChanged)
262
263+ Q_PROPERTY(State keyboard_state READ state WRITE setState
264+ NOTIFY stateChanged)
265 Q_PROPERTY(QString activeView READ activeView WRITE setActiveView
266 NOTIFY activeViewChanged)
267
268+ Q_ENUMS(State)
269
270 public:
271+ enum State {
272+ DefaultState,
273+ ShiftedState,
274+ PrimarySymbolState,
275+ SecondarySymbolState,
276+ DeadkeyState
277+ };
278+
279 enum Roles {
280 RoleKeyRectangle = Qt::UserRole + 1,
281 RoleKeyReactiveArea,
282@@ -94,7 +105,8 @@
283 RoleKeyFontSize,
284 RoleKeyFontStretch,
285 RoleKeyIcon,
286- RoleKeyActionInsert
287+ RoleKeyActionInsert,
288+ RoleKeyAction // Extra introspection detail for testing.
289 };
290
291 explicit Layout(QObject *parent = 0);
292@@ -137,6 +149,10 @@
293 Q_SLOT int invisibleTouchAreaHeight() const;
294 Q_SIGNAL void invisibleTouchAreaHeightChanged(int &changed);
295
296+ Q_SLOT State state() const;
297+ Q_SLOT void setState(Model::Layout::State state);
298+ Q_SIGNAL void stateChanged(Model::Layout::State state);
299+
300 Q_SLOT QString activeView() const;
301 Q_SLOT void setActiveView(const QString& activeViewId);
302 Q_SIGNAL void activeViewChanged(const QString &activeViewId);
303
304=== modified file 'src/plugin/inputmethod.cpp'
305--- src/plugin/inputmethod.cpp 2013-08-23 08:39:24 +0000
306+++ src/plugin/inputmethod.cpp 2013-08-23 20:24:27 +0000
307@@ -246,6 +246,9 @@
308 QObject::connect(&layout.updater, SIGNAL(languageChanged(QString)),
309 &editor, SLOT(onLanguageChanged(const QString&)));
310
311+ QObject::connect(&layout.helper, SIGNAL(stateChanged(Model::Layout::State)),
312+ &layout.model, SLOT(setState(Model::Layout::State)));
313+
314 #ifdef EXTENDED_SURFACE_TEMP_DISABLED
315 QObject::connect(&layout.event_handler, SIGNAL(extendedKeysShown(Key)),
316 &extended_layout.event_handler, SLOT(onExtendedKeysShown(Key)));
317
318=== added directory 'tests/autopilot'
319=== added file 'tests/autopilot/setup.py'
320--- tests/autopilot/setup.py 1970-01-01 00:00:00 +0000
321+++ tests/autopilot/setup.py 2013-08-23 20:24:27 +0000
322@@ -0,0 +1,32 @@
323+#!/usr/bin/python
324+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
325+#
326+# Ubuntu Keyboard Autopilot Test Suite
327+# Copyright (C) 2013 Canonical
328+#
329+# This program is free software: you can redistribute it and/or modify
330+# it under the terms of the GNU General Public License as published by
331+# the Free Software Foundation, either version 3 of the License, or
332+# (at your option) any later version.
333+#
334+# This program is distributed in the hope that it will be useful,
335+# but WITHOUT ANY WARRANTY; without even the implied warranty of
336+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
337+# GNU General Public License for more details.
338+#
339+# You should have received a copy of the GNU General Public License
340+# along with this program. If not, see <http://www.gnu.org/licenses/>.
341+#
342+
343+
344+from distutils.core import setup
345+from setuptools import find_packages
346+
347+setup(
348+ name='ubuntu_keyboard',
349+ version='1.0',
350+ description='Ubuntu Keyboard autopilot tests and emulators.',
351+ url='https://launchpad.net/ubuntu-keyboard',
352+ license='GPLv3',
353+ packages=find_packages(),
354+)
355
356=== added directory 'tests/autopilot/ubuntu_keyboard'
357=== added file 'tests/autopilot/ubuntu_keyboard/__init__.py'
358--- tests/autopilot/ubuntu_keyboard/__init__.py 1970-01-01 00:00:00 +0000
359+++ tests/autopilot/ubuntu_keyboard/__init__.py 2013-08-23 20:24:27 +0000
360@@ -0,0 +1,18 @@
361+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
362+#
363+# Ubuntu Keyboard Test Suite
364+# Copyright (C) 2013 Canonical
365+#
366+# This program is free software: you can redistribute it and/or modify
367+# it under the terms of the GNU General Public License as published by
368+# the Free Software Foundation, either version 3 of the License, or
369+# (at your option) any later version.
370+#
371+# This program is distributed in the hope that it will be useful,
372+# but WITHOUT ANY WARRANTY; without even the implied warranty of
373+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
374+# GNU General Public License for more details.
375+#
376+# You should have received a copy of the GNU General Public License
377+# along with this program. If not, see <http://www.gnu.org/licenses/>.
378+#
379
380=== added directory 'tests/autopilot/ubuntu_keyboard/emulators'
381=== added file 'tests/autopilot/ubuntu_keyboard/emulators/__init__.py'
382--- tests/autopilot/ubuntu_keyboard/emulators/__init__.py 1970-01-01 00:00:00 +0000
383+++ tests/autopilot/ubuntu_keyboard/emulators/__init__.py 2013-08-23 20:24:27 +0000
384@@ -0,0 +1,18 @@
385+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
386+#
387+# Ubuntu Keyboard Test Suite
388+# Copyright (C) 2013 Canonical
389+#
390+# This program is free software: you can redistribute it and/or modify
391+# it under the terms of the GNU General Public License as published by
392+# the Free Software Foundation, either version 3 of the License, or
393+# (at your option) any later version.
394+#
395+# This program is distributed in the hope that it will be useful,
396+# but WITHOUT ANY WARRANTY; without even the implied warranty of
397+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
398+# GNU General Public License for more details.
399+#
400+# You should have received a copy of the GNU General Public License
401+# along with this program. If not, see <http://www.gnu.org/licenses/>.
402+#
403
404=== added file 'tests/autopilot/ubuntu_keyboard/emulators/keyboard.py'
405--- tests/autopilot/ubuntu_keyboard/emulators/keyboard.py 1970-01-01 00:00:00 +0000
406+++ tests/autopilot/ubuntu_keyboard/emulators/keyboard.py 2013-08-23 20:24:27 +0000
407@@ -0,0 +1,346 @@
408+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
409+#
410+# Ubuntu Keyboard Test Suite
411+# Copyright (C) 2013 Canonical
412+#
413+# This program is free software: you can redistribute it and/or modify
414+# it under the terms of the GNU General Public License as published by
415+# the Free Software Foundation, either version 3 of the License, or
416+# (at your option) any later version.
417+#
418+# This program is distributed in the hope that it will be useful,
419+# but WITHOUT ANY WARRANTY; without even the implied warranty of
420+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
421+# GNU General Public License for more details.
422+#
423+# You should have received a copy of the GNU General Public License
424+# along with this program. If not, see <http://www.gnu.org/licenses/>.
425+#
426+
427+from collections import defaultdict, namedtuple
428+from time import sleep
429+import logging
430+
431+from autopilot.input import Pointer, Touch
432+from autopilot.introspection import (
433+ get_proxy_object_for_existing_process,
434+ ProcessSearchError
435+)
436+
437+
438+logger = logging.getLogger(__name__)
439+
440+
441+# Definitions of enums used within the cpp source code.
442+class KeyboardState:
443+ DEFAULT = 0
444+ SHIFTED = 1
445+ SYMBOL_1 = 2
446+ SYMBOL_2 = 3
447+
448+
449+class KeyAction:
450+ INSERT = 0
451+ SHIFT = 1
452+ BACKSPACE = 2
453+ SPACE = 3
454+ SYM = 6
455+ RETURN = 7
456+ SWITCH = 11
457+
458+
459+class UnsupportedKey(RuntimeError):
460+ pass
461+
462+
463+class Keyboard(object):
464+
465+ KeyPos = namedtuple("KeyPos", ['x', 'y', 'h', 'w'])
466+
467+ # Note (veebers 19-aug-13): this hardcoded right now, but will be reading
468+ # data from the keyboard itself in the very near future. Moved '/' to
469+ # primary symbol, default layout can have a .com instead.
470+ default_keys = "qwertyuiopasdfghjklzxcvbnm."
471+ shifted_keys = "QWERTYUIOPASDFGHJKLZXCVBNM."
472+ primary_symbol = "1234567890*#+-=()!?@~/\\';:,."
473+ secondary_symbol = u"$%<>[]`^|_{}\"&,.\u20ac\xa3\xa5\u20b9\xa7\xa1\xbf" \
474+ u"\xab\xbb\u201c\u201d\u201e"
475+
476+ # The ability to name the non-text keys.
477+ _action_id_to_text = {
478+ KeyAction.SHIFT: 'SHIFT',
479+ KeyAction.BACKSPACE: '\b',
480+ KeyAction.SPACE: ' ',
481+ KeyAction.RETURN: '\n'
482+ }
483+
484+ def __init__(self, pointer=None):
485+ try:
486+ maliit = get_proxy_object_for_existing_process(
487+ connection_name='org.maliit.server'
488+ )
489+ except ProcessSearchError as e:
490+ e.args += (
491+ "Unable to find maliit-server dbus object. Has it been "
492+ "started with introspection enabled?",
493+ )
494+ raise
495+
496+ try:
497+ self.keyboard = maliit.select_single(
498+ "Keyboard",
499+ objectName="ubuntuKeyboard"
500+ )
501+ if self.keyboard is None:
502+ raise RuntimeError(
503+ "Unable to find the Ubuntu Keyboard object within the "
504+ "maliit server"
505+ )
506+ except ValueError as e:
507+ e.args += (
508+ "There was more than one Keyboard object found, aborting.",
509+ )
510+ raise
511+
512+ try:
513+ self.keypad = self.keyboard.select_single(
514+ "QQuickItem",
515+ objectName="keyboardKeypad"
516+ )
517+
518+ if self.keypad is None:
519+ raise RuntimeError(
520+ "Unable to find the keypad object within the "
521+ "maliit server"
522+ )
523+ except ValueError as e:
524+ e.args += (
525+ "There was more than one keyboard keypad object found, "
526+ "aborting.",
527+ )
528+ raise
529+
530+ # Contains instructions on how to move the keyboard into a specific
531+ # state/layout so that we can successfully press the required key.
532+ self._state_lookup_table = self._generate_state_lookup_table()
533+ # Cache the position of the keys
534+ self._key_pos_table = defaultdict(dict)
535+
536+ if pointer is None:
537+ self.pointer = Pointer(Touch.create())
538+ else:
539+ self.pointer = pointer
540+
541+ def dismiss(self):
542+ """Swipe the keyboard down to hide it.
543+
544+ :raises: *AssertionError* if the state.wait_for fails meaning that the
545+ keyboard failed to hide.
546+
547+ """
548+ if self.is_available():
549+ x, y, h, w = self.keyboard.globalRect
550+ x_pos = int(w / 2)
551+ # start_y: just inside the keyboard, must be a better way than +1px
552+ start_y = y + 1
553+ end_y = y + int(h / 2)
554+ self.pointer.drag(x_pos, start_y, x_pos, end_y)
555+
556+ self.keyboard.state.wait_for("HIDDEN")
557+
558+ def is_available(self):
559+ """Returns true if the keyboard is shown and ready to use."""
560+ return (
561+ self.keyboard.state == "SHOWN"
562+ and not self.keyboard.hideAnimationFinished
563+ )
564+
565+ @property
566+ def current_state(self):
567+ return self.keyboard.layoutState
568+
569+ # Much like is_available, but attempts to wait for the keyboard to be
570+ # ready.
571+ def wait_for_keyboard_ready(self, timeout=10):
572+ """Waits for *timeout* for the keyboard to be ready and returns
573+ true. Returns False if the keyboard fails to be considered ready within
574+ the alloted time.
575+
576+ """
577+ try:
578+ self.keyboard.state.wait_for("SHOWN", timeout=timeout)
579+ self.keyboard.hideAnimationFinished.wait_for(
580+ False,
581+ timeout=timeout
582+ )
583+ return True
584+ except RuntimeError:
585+ return False
586+
587+ def get_key_position(self, key):
588+ """Returns the global rect of the given key.
589+
590+ It may need to do a lookup to update the table of positions.
591+
592+ """
593+ current_state = self.keyboard.layoutState
594+ if self._key_pos_table.get(current_state) is None:
595+ self._update_pos_table_for_current_state()
596+
597+ return self._key_pos_table[current_state][key]
598+
599+ def _update_pos_table_for_current_state(self):
600+ all_keys = self.keypad.select_many('QQuickText')
601+ current_state = self.keyboard.layoutState
602+ labeled_keys = (KeyAction.INSERT, KeyAction.SWITCH, KeyAction.SYM)
603+ for key in all_keys:
604+ with key.no_automatic_refreshing():
605+ key_pos = Keyboard.KeyPos(*key.globalRect)
606+ if key.action_type in labeled_keys:
607+ self._key_pos_table[current_state][key.text] = key_pos
608+ else:
609+ key_text = Keyboard._action_id_to_text[key.action_type]
610+ self._key_pos_table[current_state][key_text] = key_pos
611+
612+ def press_key(self, key):
613+ """Tap on the key with the internal pointer
614+
615+ :params key: String containing the text of the key to tap.
616+
617+ :raises: *RuntimeError* if the keyboard is not available and thus not
618+ ready to be used.
619+ :raises: *UnsupportedKey* if the supplied key cannot be found on any of
620+ the the current keyboards layouts.
621+ """
622+ if not self.is_available():
623+ raise RuntimeError("Keyboard is not on screen")
624+
625+ if not self._is_special_key(key):
626+ required_state_for_key = self._get_keys_required_state(key)
627+ self._switch_keyboard_to_state(required_state_for_key)
628+
629+ key_rect = self.get_key_position(key)
630+ self.pointer.click_object(key_rect)
631+
632+ def type(self, string, delay=0.1):
633+ """Type the string *string* with a delay of *delay* between each key
634+ press
635+
636+ .. note:: The delay provides a minimum delay, it may take longer
637+ between each press as the keyboard shifts between states etc.
638+
639+ Only 'normal' or single characters can be typed this way.
640+
641+ :raises: *UnsupportedKey* if one of the the supplied keys cannot be
642+ found on any of the the current keyboards layouts.
643+
644+ """
645+ for char in string:
646+ self.press_key(char)
647+ sleep(delay)
648+
649+ def _get_keys_required_state(self, char):
650+ """Given a character determine which state the keyboard needs to be in
651+ so that it is visible and can be clicked.
652+
653+ """
654+
655+ if char in Keyboard.default_keys:
656+ return KeyboardState.DEFAULT
657+ elif char in Keyboard.shifted_keys:
658+ return KeyboardState.SHIFTED
659+ elif char in Keyboard.primary_symbol:
660+ return KeyboardState.SYMBOL_1
661+ elif char in Keyboard.secondary_symbol:
662+ return KeyboardState.SYMBOL_2
663+ else:
664+ raise UnsupportedKey(
665+ "Don't know which state key '%s' requires" % char
666+ )
667+
668+ def _switch_keyboard_to_state(self, target_state):
669+ """Given a target_state, presses the required keys to bring the
670+ keyboard into the correct state.
671+
672+ :raises: *RuntimeError* if unable to change the keyboard into the
673+ expected state.
674+
675+ """
676+ current_state = self.keyboard.layoutState
677+
678+ if target_state == current_state:
679+ return
680+
681+ instructions = self._state_lookup_table[target_state].get(
682+ current_state,
683+ None
684+ )
685+ if instructions is None:
686+ raise RuntimeError(
687+ "Don't know how to get to state %d from current state (%d)"
688+ % (target_state, current_state)
689+ )
690+
691+ for step in instructions:
692+ key, expected_state = step
693+ self.press_key(key)
694+ self.keyboard.layoutState.wait_for(expected_state)
695+
696+ def _is_special_key(self, key):
697+ return key in ["\n", "\b", " ", "SHIFT", "?123", "ABC", "1/2", "2/2"]
698+
699+ # Give the state that you want and the current state, get instructions on
700+ # how to move to that state.
701+ # lookup_table[REQUESTED_STATE][CURRENT_STATE] -> Instructions(Key to
702+ # press, Expected state after key press.)
703+ def _generate_state_lookup_table(self):
704+ return {
705+ KeyboardState.DEFAULT: {
706+ KeyboardState.SHIFTED: [
707+ ("SHIFT", KeyboardState.DEFAULT)
708+ ],
709+ KeyboardState.SYMBOL_1: [
710+ ("ABC", KeyboardState.DEFAULT)
711+ ],
712+ KeyboardState.SYMBOL_2: [
713+ ("ABC", KeyboardState.DEFAULT)
714+ ],
715+ },
716+ KeyboardState.SHIFTED: {
717+ KeyboardState.DEFAULT: [
718+ ("SHIFT", KeyboardState.SHIFTED)
719+ ],
720+ KeyboardState.SYMBOL_1: [
721+ ("ABC", KeyboardState.DEFAULT),
722+ ("SHIFT", KeyboardState.SHIFTED)
723+ ],
724+ KeyboardState.SYMBOL_2: [
725+ ("ABC", KeyboardState.DEFAULT),
726+ ("SHIFT", KeyboardState.SHIFTED)
727+ ],
728+ },
729+ KeyboardState.SYMBOL_1: {
730+ KeyboardState.DEFAULT: [
731+ ("?123", KeyboardState.SYMBOL_1)
732+ ],
733+ KeyboardState.SHIFTED: [
734+ ("?123", KeyboardState.SYMBOL_1)
735+ ],
736+ KeyboardState.SYMBOL_2: [
737+ ("2/2", KeyboardState.SYMBOL_1)
738+ ],
739+ },
740+ KeyboardState.SYMBOL_2: {
741+ KeyboardState.DEFAULT: [
742+ ("?123", KeyboardState.SYMBOL_1),
743+ ("1/2", KeyboardState.SYMBOL_2)
744+ ],
745+ KeyboardState.SHIFTED: [
746+ ("?123", KeyboardState.SYMBOL_1),
747+ ("1/2", KeyboardState.SYMBOL_2)
748+ ],
749+ KeyboardState.SYMBOL_1: [
750+ ("1/2", KeyboardState.SYMBOL_2)
751+ ],
752+ },
753+ }
754
755=== added directory 'tests/autopilot/ubuntu_keyboard/tests'
756=== added file 'tests/autopilot/ubuntu_keyboard/tests/__init__.py'
757--- tests/autopilot/ubuntu_keyboard/tests/__init__.py 1970-01-01 00:00:00 +0000
758+++ tests/autopilot/ubuntu_keyboard/tests/__init__.py 2013-08-23 20:24:27 +0000
759@@ -0,0 +1,18 @@
760+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
761+#
762+# Ubuntu Keyboard Test Suite
763+# Copyright (C) 2013 Canonical
764+#
765+# This program is free software: you can redistribute it and/or modify
766+# it under the terms of the GNU General Public License as published by
767+# the Free Software Foundation, either version 3 of the License, or
768+# (at your option) any later version.
769+#
770+# This program is distributed in the hope that it will be useful,
771+# but WITHOUT ANY WARRANTY; without even the implied warranty of
772+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
773+# GNU General Public License for more details.
774+#
775+# You should have received a copy of the GNU General Public License
776+# along with this program. If not, see <http://www.gnu.org/licenses/>.
777+#
778
779=== added file 'tests/autopilot/ubuntu_keyboard/tests/test_keyboard.py'
780--- tests/autopilot/ubuntu_keyboard/tests/test_keyboard.py 1970-01-01 00:00:00 +0000
781+++ tests/autopilot/ubuntu_keyboard/tests/test_keyboard.py 2013-08-23 20:24:27 +0000
782@@ -0,0 +1,342 @@
783+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
784+#
785+# Ubuntu Keyboard Test Suite
786+# Copyright (C) 2013 Canonical
787+#
788+# This program is free software: you can redistribute it and/or modify
789+# it under the terms of the GNU General Public License as published by
790+# the Free Software Foundation, either version 3 of the License, or
791+# (at your option) any later version.
792+#
793+# This program is distributed in the hope that it will be useful,
794+# but WITHOUT ANY WARRANTY; without even the implied warranty of
795+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
796+# GNU General Public License for more details.
797+#
798+# You should have received a copy of the GNU General Public License
799+# along with this program. If not, see <http://www.gnu.org/licenses/>.
800+#
801+
802+import os
803+
804+from testtools.matchers import Equals
805+from tempfile import mktemp
806+from textwrap import dedent
807+
808+from autopilot.testcase import AutopilotTestCase
809+from autopilot.input import Pointer, Touch
810+from autopilot.matchers import Eventually
811+
812+from ubuntu_keyboard.emulators.keyboard import Keyboard, KeyboardState
813+
814+
815+class UbuntuKeyboardTests(AutopilotTestCase):
816+ def setUp(self):
817+ super(UbuntuKeyboardTests, self).setUp()
818+ self.pointer = Pointer(Touch.create())
819+
820+ def launch_test_input_area(self, label="", input_hints=None):
821+ self.app = self._launch_simple_input(label, input_hints)
822+ text_area = self.app.select_single("QQuickTextInput")
823+
824+ return text_area
825+
826+ def ensure_focus_on_input(self, input_area):
827+ self.pointer.click_object(input_area)
828+ keyboard = Keyboard()
829+ self.addCleanup(keyboard.dismiss)
830+ self.assertThat(keyboard.is_available, Eventually(Equals(True)))
831+
832+ def _start_qml_script(self, script_contents):
833+ """Launch a qml script."""
834+ qml_path = mktemp(suffix='.qml')
835+ open(qml_path, 'w').write(script_contents)
836+ self.addCleanup(os.remove, qml_path)
837+
838+ return self.launch_test_application(
839+ "qmlscene",
840+ qml_path,
841+ app_type='qt',
842+ )
843+
844+ def _launch_simple_input(self, label="", input_hints=None):
845+ if input_hints is None:
846+ extra_script = "Qt.ImhNoPredictiveText"
847+ else:
848+ extra_script = "|".join(input_hints)
849+
850+ simple_script = dedent("""
851+ import QtQuick 2.0
852+ import Ubuntu.Components 0.1
853+
854+ Rectangle {
855+ id: window
856+ objectName: "windowRectangle"
857+ color: "lightgrey"
858+
859+ Text {
860+ id: inputLabel
861+ text: "%(label)s"
862+ font.pixelSize: units.gu(3)
863+ anchors {
864+ left: input.left
865+ top: parent.top
866+ topMargin: 25
867+ bottomMargin: 25
868+ }
869+ }
870+
871+ TextField {
872+ id: input;
873+ objectName: "input"
874+ anchors {
875+ top: inputLabel.bottom
876+ horizontalCenter: parent.horizontalCenter
877+ topMargin: 10
878+ }
879+ inputMethodHints: %(input_method)s
880+ }
881+ }
882+
883+ """ % {'label': label, 'input_method': extra_script})
884+
885+ return self._start_qml_script(simple_script)
886+
887+
888+class UbuntuKeyboardTestsAccess(UbuntuKeyboardTests):
889+
890+ def test_keyboard_is_available(self):
891+ keyboard = Keyboard()
892+ self.addCleanup(keyboard.dismiss)
893+ app = self._launch_simple_input()
894+ text_rectangle = app.select_single("QQuickTextInput")
895+
896+ self.pointer.click_object(text_rectangle)
897+
898+ self.assertThat(keyboard.is_available, Eventually(Equals(True)))
899+
900+
901+class UbuntuKeyboardTypingTests(UbuntuKeyboardTests):
902+
903+ scenarios = [
904+ (
905+ 'lower_alpha',
906+ dict(
907+ label="Lowercase",
908+ input='abcdefghijklmnopqrstuvwxyz'
909+ )
910+ ),
911+ (
912+ 'upper_alpha',
913+ dict(
914+ label="Uppercase",
915+ input='ABCDEFGHIJKLMNOPQRSTUVWXYZ'
916+ )
917+ ),
918+ (
919+ 'numeric',
920+ dict(
921+ label="Numeric",
922+ input='0123456789'
923+ )
924+ ),
925+ (
926+ 'punctuation',
927+ dict(
928+ label="Puncuation",
929+ input='`~!@#$%^&*()_-+={}[]|\\:;"\'<>,.?/'
930+ )
931+ )
932+ ]
933+
934+ def test_can_type_string(self):
935+ text_area = self.launch_test_input_area(label=self.label)
936+ self.ensure_focus_on_input(text_area)
937+ keyboard = Keyboard()
938+ self.addCleanup(keyboard.dismiss)
939+
940+ keyboard.type(self.input)
941+
942+ self.assertThat(text_area.text, Eventually(Equals(self.input)))
943+
944+
945+class UbuntuKeyboardStateChanges(UbuntuKeyboardTests):
946+
947+ # Note: this is a failing test due to bug lp:1214695
948+ # Note: based on UX design doc
949+ def test_keyboard_layout_starts_shifted(self):
950+ """When first launched the keyboard state must be
951+ shifted/capitalised.
952+
953+ """
954+ text_area = self.launch_test_input_area()
955+ self.ensure_focus_on_input(text_area)
956+ keyboard = Keyboard()
957+ self.addCleanup(keyboard.dismiss)
958+
959+ self.assertThat(
960+ keyboard.keyboard.layoutState,
961+ Eventually(Equals(KeyboardState.SHIFTED))
962+ )
963+
964+ def test_shift_latch(self):
965+ """Double tap of the shift key must lock it 'On' until the shift key
966+ tapped again.
967+
968+ Normally hitting shift then a letter reverts from the shifted state
969+ back to the default. If double clicked it should stay in the shifted
970+ until the shift key is clicked again.
971+
972+ """
973+ text_area = self.launch_test_input_area()
974+ self.ensure_focus_on_input(text_area)
975+ keyboard = Keyboard()
976+ self.addCleanup(keyboard.dismiss)
977+
978+ keyboard.type('abc')
979+ keyboard.press_key('SHIFT')
980+ keyboard.press_key('SHIFT')
981+ keyboard.type('S')
982+
983+ self.assertThat(
984+ keyboard.keyboard.layoutState,
985+ Eventually(Equals(KeyboardState.SHIFTED))
986+ )
987+ self.assertThat(text_area.text, Eventually(Equals('abcS')))
988+
989+ # Note: based on UX design doc
990+ def test_shift_state_returns_to_default_after_letter_typed(self):
991+ """Pushing shift and then typing an uppercase letter must automatically
992+ shift the keyboard back into the default state.
993+
994+ """
995+ text_area = self.launch_test_input_area()
996+ self.ensure_focus_on_input(text_area)
997+ keyboard = Keyboard()
998+ self.addCleanup(keyboard.dismiss)
999+
1000+ # Normally, type and (press_key) take care of shifting into the correct
1001+ # state, we do it manually here as that's what we're testing.
1002+ keyboard.type('abc')
1003+ keyboard.press_key('SHIFT')
1004+ keyboard.type('A')
1005+
1006+ # Once the capital letter has been typed, we must be able to access the
1007+ # lowercase letters, otherwise it's not in the correct state.
1008+ self.assertThat(
1009+ keyboard.keyboard.layoutState,
1010+ Eventually(Equals(KeyboardState.DEFAULT))
1011+ )
1012+
1013+ self.assertThat(text_area.text, Eventually(Equals('abcA')))
1014+
1015+ # Note: this is a failing test due to bug lp:1214695
1016+ # Note: Based on UX design doc.
1017+ def test_shift_state_entered_after_fullstop(self):
1018+ """After typing a fullstop the keyboard state must automatically
1019+ enter the shifted state.
1020+
1021+ """
1022+ text_area = self.launch_test_input_area()
1023+ self.ensure_focus_on_input(text_area)
1024+ keyboard = Keyboard()
1025+ self.addCleanup(keyboard.dismiss)
1026+
1027+ keyboard.type("abc.")
1028+
1029+ self.assertThat(
1030+ text_area.text,
1031+ Eventually(Equals("abc."))
1032+ )
1033+
1034+ self.assertThat(
1035+ keyboard.keyboard.layoutState,
1036+ Eventually(Equals(KeyboardState.SHIFTED))
1037+ )
1038+
1039+ def test_switching_between_states(self):
1040+ """The user must be able to type many different characters including
1041+ spaces and backspaces.
1042+
1043+ """
1044+ text_area = self.launch_test_input_area()
1045+ self.ensure_focus_on_input(text_area)
1046+ keyboard = Keyboard()
1047+ self.addCleanup(keyboard.dismiss)
1048+
1049+ keyboard.type(
1050+ 'abc gone\b\b & \bABC (123)'
1051+ )
1052+
1053+ expected = "abc go & ABC (123)"
1054+ self.assertThat(
1055+ text_area.text,
1056+ Eventually(Equals(expected))
1057+ )
1058+
1059+
1060+class UbuntuKeyboardInputTypeStateChange(UbuntuKeyboardTests):
1061+ """Note: these tests are currently failing due to bug lp:1214694 (the
1062+ activeView detail isn't exposed correctly nor is it updated as expected
1063+ (i.e. when the view changes.))
1064+
1065+ """
1066+
1067+ scenarios = [
1068+ (
1069+ "Url",
1070+ dict(
1071+ label="Url",
1072+ hints=['Qt.ImhUrlCharactersOnly'],
1073+ expected_activeview="url"
1074+ )
1075+ ),
1076+ (
1077+ "Password",
1078+ dict(
1079+ label="Password",
1080+ hints=['Qt.ImhHiddenText', 'Qt.ImhSensitiveData'],
1081+ expected_activeview="password"
1082+ )
1083+ ),
1084+ (
1085+ "Email",
1086+ dict(
1087+ label="Email",
1088+ hints=['Qt.ImhEmailCharactersOnly'],
1089+ expected_activeview="email"
1090+ )
1091+ ),
1092+ (
1093+ "Number",
1094+ dict(
1095+ label="Number",
1096+ hints=['Qt.ImhFormattedNumbersOnly'],
1097+ expected_activeview="number"
1098+ )
1099+ ),
1100+ (
1101+ "Telephone",
1102+ dict(
1103+ label="Telephone",
1104+ hints=['Qt.ImhDigitsOnly'],
1105+ expected_activeview="phonenumber"
1106+ )
1107+ ),
1108+ ]
1109+
1110+ # Note: based on UX design doc
1111+ def test_keyboard_layout(self):
1112+ """The Keyboard must respond to the input type and change to be the
1113+ correct state.
1114+
1115+ """
1116+ text_area = self.launch_test_input_area(self.label, self.hints)
1117+ self.ensure_focus_on_input(text_area)
1118+ keyboard = Keyboard()
1119+ self.addCleanup(keyboard.dismiss)
1120+
1121+ self.assertThat(
1122+ keyboard.keyboard.activeView,
1123+ Eventually(Equals(self.expected_activeview))
1124+ )

Subscribers

People subscribed via source and target branches