Merge lp:~elopio/ubuntu-ui-toolkit/datepicker-autopilot_helper into lp:ubuntu-ui-toolkit/staging
- datepicker-autopilot_helper
- Merge into staging
Status: | Superseded |
---|---|
Proposed branch: | lp:~elopio/ubuntu-ui-toolkit/datepicker-autopilot_helper |
Merge into: | lp:ubuntu-ui-toolkit/staging |
Diff against target: |
985 lines (+688/-41) 12 files modified
modules/Ubuntu/Components/Pickers/DatePicker.qml (+3/-5) modules/Ubuntu/Components/Pickers/PickerRow.qml (+1/-1) tests/autopilot/ubuntuuitoolkit/__init__.py (+4/-2) tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/__init__.py (+4/-2) tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/_common.py (+45/-1) tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/_flickable.py (+50/-16) tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/_qquicklistview.py (+28/-11) tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/pickers.py (+206/-0) tests/autopilot/ubuntuuitoolkit/emulators.py (+2/-2) tests/autopilot/ubuntuuitoolkit/tests/custom_proxy_objects/test_date_picker.py (+147/-0) tests/autopilot/ubuntuuitoolkit/tests/custom_proxy_objects/test_flickable.py (+197/-0) tests/autopilot/ubuntuuitoolkit/tests/test_emulators.py (+1/-1) |
To merge this branch: | bzr merge lp:~elopio/ubuntu-ui-toolkit/datepicker-autopilot_helper |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot | continuous-integration | Needs Fixing | |
Allan LeSage (community) | Approve | ||
Ubuntu SDK team | Pending | ||
Review via email: mp+218588@code.launchpad.net |
This proposal has been superseded by a proposal from 2014-05-09.
Commit message
Added autopilot helpers to pick a date from a date picker.
Description of the change
Next branch will add support for picking a time.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1041
http://
Executed test runs:
SUCCESS: http://
FAILURE: http://
FAILURE: http://
SUCCESS: http://
deb: http://
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
Leo Arias (elopio) wrote : | # |
> Ok I think understand this change, appears thoroughly tested, nice work :) .
> I'm assuming that the comment on l.535 doesn't apply to the present merge
> proposal.
It does. I was just trying to test it the wrong way. Now I added a couple of tests for that.
Unmerged revisions
Preview Diff
1 | === modified file 'modules/Ubuntu/Components/Pickers/DatePicker.qml' |
2 | --- modules/Ubuntu/Components/Pickers/DatePicker.qml 2014-04-23 08:50:20 +0000 |
3 | +++ modules/Ubuntu/Components/Pickers/DatePicker.qml 2014-05-09 04:12:47 +0000 |
4 | @@ -42,7 +42,7 @@ |
5 | Column { |
6 | Label { |
7 | text: "Selected date: W" + datePicker.week + " - " + |
8 | - Qt.formatDate(datePicker.date, "dddd, dd-mmmm-yyyy") |
9 | + Qt.formatDate(datePicker.date, "dddd, dd-MMMM-yyyy") |
10 | } |
11 | DatePicker { |
12 | id: datePicker |
13 | @@ -61,7 +61,7 @@ |
14 | |
15 | Column { |
16 | Label { |
17 | - text: "Selected month: " + Qt.formatDate(datePicker.date, "mmmm-yyyy") |
18 | + text: "Selected month: " + Qt.formatDate(datePicker.date, "MMMM-yyyy") |
19 | } |
20 | DatePicker { |
21 | id: datePicker |
22 | @@ -117,7 +117,7 @@ |
23 | |
24 | Column { |
25 | Label { |
26 | - text: "Selected date: " + Qt.formatDate(datePicker.date, "dddd, dd-mmmm-yyyy") |
27 | + text: "Selected date: " + Qt.formatDate(datePicker.date, "dddd, dd-MMMM-yyyy") |
28 | } |
29 | DatePicker { |
30 | id: datePicker |
31 | @@ -667,5 +667,3 @@ |
32 | } |
33 | } |
34 | } |
35 | - |
36 | - |
37 | |
38 | === modified file 'modules/Ubuntu/Components/Pickers/PickerRow.qml' |
39 | --- modules/Ubuntu/Components/Pickers/PickerRow.qml 2014-04-23 08:50:20 +0000 |
40 | +++ modules/Ubuntu/Components/Pickers/PickerRow.qml 2014-05-09 04:12:47 +0000 |
41 | @@ -96,7 +96,7 @@ |
42 | } |
43 | delegate: PickerDelegate { |
44 | Label { |
45 | - objectName: "PickerRow_PickerLabel" |
46 | + objectName: "PickerRow_PickerLabel" + (pickerModel ? modelData : "") |
47 | text: pickerModel ? pickerModel.text(modelData) : "" |
48 | anchors.fill: parent |
49 | verticalAlignment: Text.AlignVCenter |
50 | |
51 | === modified file 'tests/autopilot/ubuntuuitoolkit/__init__.py' |
52 | --- tests/autopilot/ubuntuuitoolkit/__init__.py 2014-04-25 18:39:51 +0000 |
53 | +++ tests/autopilot/ubuntuuitoolkit/__init__.py 2014-05-09 04:12:47 +0000 |
54 | @@ -24,14 +24,15 @@ |
55 | 'environment', |
56 | 'emulators', |
57 | 'fixture_setup', |
58 | - 'Flickable', |
59 | 'get_keyboard', |
60 | 'get_pointing_device', |
61 | 'Header', |
62 | 'listitems', |
63 | 'MainView', |
64 | 'OptionSelector', |
65 | + 'pickers', |
66 | 'popups', |
67 | + 'QQuickFlickable', |
68 | 'QQuickListView', |
69 | 'TabBar', |
70 | 'Tabs', |
71 | @@ -53,14 +54,15 @@ |
72 | from ubuntuuitoolkit._custom_proxy_objects import ( |
73 | check_autopilot_version, |
74 | CheckBox, |
75 | - Flickable, |
76 | get_keyboard, |
77 | get_pointing_device, |
78 | Header, |
79 | listitems, |
80 | MainView, |
81 | OptionSelector, |
82 | + pickers, |
83 | popups, |
84 | + QQuickFlickable, |
85 | QQuickListView, |
86 | TabBar, |
87 | Tabs, |
88 | |
89 | === modified file 'tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/__init__.py' |
90 | --- tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/__init__.py 2014-04-28 15:39:24 +0000 |
91 | +++ tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/__init__.py 2014-05-09 04:12:47 +0000 |
92 | @@ -20,14 +20,15 @@ |
93 | __all__ = [ |
94 | 'check_autopilot_version', |
95 | 'CheckBox', |
96 | - 'Flickable', |
97 | 'get_keyboard', |
98 | 'get_pointing_device', |
99 | 'Header', |
100 | 'listitems', |
101 | 'MainView', |
102 | 'OptionSelector', |
103 | + 'pickers', |
104 | 'popups', |
105 | + 'QQuickFlickable', |
106 | 'QQuickListView', |
107 | 'TabBar', |
108 | 'Tabs', |
109 | @@ -45,13 +46,14 @@ |
110 | ToolkitException, |
111 | UbuntuUIToolkitCustomProxyObjectBase, |
112 | ) |
113 | -from ubuntuuitoolkit._custom_proxy_objects._flickable import Flickable |
114 | +from ubuntuuitoolkit._custom_proxy_objects._flickable import QQuickFlickable |
115 | from ubuntuuitoolkit._custom_proxy_objects._header import Header |
116 | from ubuntuuitoolkit._custom_proxy_objects import listitems |
117 | from ubuntuuitoolkit._custom_proxy_objects._mainview import MainView |
118 | from ubuntuuitoolkit._custom_proxy_objects._optionselector import ( |
119 | OptionSelector |
120 | ) |
121 | +from ubuntuuitoolkit._custom_proxy_objects import pickers |
122 | from ubuntuuitoolkit._custom_proxy_objects import popups |
123 | from ubuntuuitoolkit._custom_proxy_objects._qquicklistview import ( |
124 | QQuickListView |
125 | |
126 | === modified file 'tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/_common.py' |
127 | --- tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/_common.py 2014-04-16 21:13:39 +0000 |
128 | +++ tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/_common.py 2014-05-09 04:12:47 +0000 |
129 | @@ -16,13 +16,21 @@ |
130 | |
131 | """Common helpers for Ubuntu UI Toolkit Autopilot custom proxy objects.""" |
132 | |
133 | +import logging |
134 | from distutils import version |
135 | |
136 | import autopilot |
137 | -from autopilot import platform, input |
138 | +from autopilot import ( |
139 | + input, |
140 | + logging as autopilot_logging, |
141 | + platform |
142 | +) |
143 | from autopilot.introspection import dbus |
144 | |
145 | |
146 | +logger = logging.getLogger(__name__) |
147 | + |
148 | + |
149 | class ToolkitException(Exception): |
150 | """Exception raised when there is an error with the emulator.""" |
151 | |
152 | @@ -67,3 +75,39 @@ |
153 | check_autopilot_version() |
154 | super(UbuntuUIToolkitCustomProxyObjectBase, self).__init__(*args) |
155 | self.pointing_device = get_pointing_device() |
156 | + |
157 | + def is_flickable(self): |
158 | + """Check if the object is flickable. |
159 | + |
160 | + If the object has a flicking attribute, we consider it as a flickable. |
161 | + |
162 | + :return: True if the object is flickable. False otherwise. |
163 | + |
164 | + """ |
165 | + try: |
166 | + self.flicking |
167 | + return True |
168 | + except AttributeError: |
169 | + return False |
170 | + |
171 | + @autopilot_logging.log_action(logger.info) |
172 | + def swipe_into_view(self): |
173 | + """Make the object visible. |
174 | + |
175 | + Currently it works only when the object needs to be swiped vertically. |
176 | + TODO implement horizontal swiping. --elopio - 2014-03-21 |
177 | + |
178 | + """ |
179 | + flickable_parent = self._get_flickable_parent() |
180 | + flickable_parent.swipe_child_into_view(self) |
181 | + |
182 | + def _get_flickable_parent(self): |
183 | + parent = self.get_parent() |
184 | + root = self.get_root_instance() |
185 | + while parent.id != root.id: |
186 | + if parent.is_flickable(): |
187 | + return parent |
188 | + parent = parent.get_parent() |
189 | + raise ToolkitException( |
190 | + "The element is not contained in a Flickable so it can't be " |
191 | + "swiped into view.") |
192 | |
193 | === modified file 'tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/_flickable.py' |
194 | --- tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/_flickable.py 2014-04-16 21:13:39 +0000 |
195 | +++ tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/_flickable.py 2014-05-09 04:12:47 +0000 |
196 | @@ -38,7 +38,7 @@ |
197 | return min(containers_bottom) |
198 | |
199 | |
200 | -class Flickable(_common.UbuntuUIToolkitCustomProxyObjectBase): |
201 | +class Scrollable(_common.UbuntuUIToolkitCustomProxyObjectBase): |
202 | |
203 | @autopilot_logging.log_action(logger.info) |
204 | def swipe_child_into_view(self, child): |
205 | @@ -68,15 +68,19 @@ |
206 | def _get_top_container(self): |
207 | """Return the top-most container with a globalRect.""" |
208 | root = self.get_root_instance() |
209 | - containers = [root] |
210 | - while len(containers) == 1: |
211 | - try: |
212 | - containers[0].globalRect |
213 | - return containers[0] |
214 | - except AttributeError: |
215 | - containers = containers[0].get_children() |
216 | - |
217 | - raise _common.ToolkitException("Couldn't find the top-most container.") |
218 | + parent = self.get_parent() |
219 | + top_container = None |
220 | + while parent.id != root.id: |
221 | + if hasattr(parent, 'globalRect'): |
222 | + top_container = parent |
223 | + |
224 | + parent = parent.get_parent() |
225 | + |
226 | + if top_container is None: |
227 | + raise _common.ToolkitException( |
228 | + "Couldn't find the top-most container.") |
229 | + else: |
230 | + return top_container |
231 | |
232 | def _is_child_visible(self, child, containers): |
233 | """Check if the center of the child is visible. |
234 | @@ -90,8 +94,35 @@ |
235 | return (object_center >= visible_top and |
236 | object_center <= visible_bottom) |
237 | |
238 | + def _slow_drag(self, start_x, stop_x, start_y, stop_y): |
239 | + # If we drag too fast, we end up scrolling more than what we |
240 | + # should, sometimes missing the element we are looking for. |
241 | + # I found that when the flickDeceleration is 1500, the rate should be |
242 | + # 5 and that when it's 100, the rate should be 1. With those two points |
243 | + # we can get that the following equation. |
244 | + rate = (self.flickDeceleration + 250) / 350 |
245 | + self.pointing_device.drag(start_x, start_y, stop_x, stop_y, rate=rate) |
246 | + |
247 | + |
248 | +class QQuickFlickable(Scrollable): |
249 | + |
250 | + @autopilot_logging.log_action(logger.info) |
251 | + def swipe_child_into_view(self, child): |
252 | + """Make the child visible. |
253 | + |
254 | + Currently it works only when the object needs to be swiped vertically. |
255 | + TODO implement horizontal swiping. --elopio - 2014-03-21 |
256 | + |
257 | + """ |
258 | + containers = self._get_containers() |
259 | + if not self._is_child_visible(child, containers): |
260 | + self._swipe_non_visible_child_into_view(child, containers) |
261 | + else: |
262 | + logger.debug('The element is already visible.') |
263 | + |
264 | @autopilot_logging.log_action(logger.info) |
265 | def _swipe_non_visible_child_into_view(self, child, containers): |
266 | + original_content_y = self.contentY |
267 | while not self._is_child_visible(child, containers): |
268 | # Check the direction of the swipe based on the position of the |
269 | # child relative to the immediate flickable container. |
270 | @@ -100,6 +131,10 @@ |
271 | else: |
272 | self._swipe_to_show_more_below(containers) |
273 | |
274 | + if self.contentY == original_content_y: |
275 | + raise _common.ToolkitException( |
276 | + "Couldn't swipe in the flickable.") |
277 | + |
278 | @autopilot_logging.log_action(logger.info) |
279 | def _swipe_to_show_more_above(self, containers): |
280 | if self.atYBeginning: |
281 | @@ -124,8 +159,12 @@ |
282 | # bottom. |
283 | top = _get_visible_container_top(containers) + 5 |
284 | bottom = _get_visible_container_bottom(containers) - 5 |
285 | + |
286 | if direction == 'below': |
287 | - start_y = bottom |
288 | + # Take into account that swiping from below can open the toolbar or |
289 | + # trigger the bottom edge gesture. |
290 | + # XXX Do this only if we are close to the bottom edge. |
291 | + start_y = bottom - 20 |
292 | stop_y = top |
293 | elif direction == 'above': |
294 | start_y = top |
295 | @@ -137,11 +176,6 @@ |
296 | self.dragging.wait_for(False) |
297 | self.moving.wait_for(False) |
298 | |
299 | - def _slow_drag(self, start_x, stop_x, start_y, stop_y): |
300 | - # If we drag too fast, we end up scrolling more than what we |
301 | - # should, sometimes missing the element we are looking for. |
302 | - self.pointing_device.drag(start_x, start_y, stop_x, stop_y, rate=5) |
303 | - |
304 | @autopilot_logging.log_action(logger.info) |
305 | def _scroll_to_top(self): |
306 | if not self.atYBeginning: |
307 | |
308 | === modified file 'tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/_qquicklistview.py' |
309 | --- tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/_qquicklistview.py 2014-04-16 21:13:39 +0000 |
310 | +++ tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/_qquicklistview.py 2014-05-09 04:12:47 +0000 |
311 | @@ -19,22 +19,24 @@ |
312 | from autopilot import logging as autopilot_logging |
313 | from autopilot.introspection import dbus |
314 | |
315 | -from ubuntuuitoolkit._custom_proxy_objects import _flickable |
316 | -from ubuntuuitoolkit._custom_proxy_objects import _common |
317 | - |
318 | +from ubuntuuitoolkit._custom_proxy_objects import _common, _flickable |
319 | |
320 | logger = logging.getLogger(__name__) |
321 | |
322 | |
323 | -class QQuickListView(_flickable.Flickable): |
324 | +class QQuickListView(_flickable.QQuickFlickable): |
325 | |
326 | @autopilot_logging.log_action(logger.info) |
327 | - def click_element(self, object_name): |
328 | + def click_element(self, object_name, direction=None): |
329 | """Click an element from the list. |
330 | |
331 | It swipes the element into view if it's center is not visible. |
332 | |
333 | :parameter objectName: The objectName property of the element to click. |
334 | + :parameter direction: The direction where the element is, it can be |
335 | + either 'above' or 'below'. Default value is None, which means we |
336 | + don't know where the object is and we will need to search the full |
337 | + list. |
338 | |
339 | """ |
340 | try: |
341 | @@ -42,16 +44,31 @@ |
342 | except dbus.StateNotFoundError: |
343 | # The element might be on a part of the list that hasn't been |
344 | # created yet. We have to search for it scrolling the entire list. |
345 | - element = self._find_element(object_name) |
346 | + element = self._find_element(object_name, direction) |
347 | self.swipe_child_into_view(element) |
348 | self.pointing_device.click_object(element) |
349 | |
350 | @autopilot_logging.log_action(logger.info) |
351 | - def _find_element(self, object_name): |
352 | - self._scroll_to_top() |
353 | - while not self.atYEnd: |
354 | - containers = self._get_containers() |
355 | - self._swipe_to_show_more_below(containers) |
356 | + def _find_element(self, object_name, direction=None): |
357 | + if direction is None: |
358 | + # We don't know where the object is so we start looking for it from |
359 | + # the top. |
360 | + self._scroll_to_top() |
361 | + direction = 'below' |
362 | + |
363 | + if direction == 'below': |
364 | + fail_condition = lambda: self.atYEnd |
365 | + swipe_method = self._swipe_to_show_more_below |
366 | + elif direction == 'above': |
367 | + fail_condition = lambda: self.atYBeginning |
368 | + swipe_method = self._swipe_to_show_more_above |
369 | + else: |
370 | + raise _common.ToolkitException( |
371 | + 'Invalid direction: {}'.format(direction)) |
372 | + |
373 | + containers = self._get_containers() |
374 | + while not fail_condition(): |
375 | + swipe_method(containers) |
376 | try: |
377 | return self.select_single(objectName=object_name) |
378 | except dbus.StateNotFoundError: |
379 | |
380 | === added file 'tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/pickers.py' |
381 | --- tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/pickers.py 1970-01-01 00:00:00 +0000 |
382 | +++ tests/autopilot/ubuntuuitoolkit/_custom_proxy_objects/pickers.py 2014-05-09 04:12:47 +0000 |
383 | @@ -0,0 +1,206 @@ |
384 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
385 | +# |
386 | +# Copyright (C) 2014 Canonical Ltd. |
387 | +# |
388 | +# This program is free software; you can redistribute it and/or modify |
389 | +# it under the terms of the GNU Lesser General Public License as published by |
390 | +# the Free Software Foundation; version 3. |
391 | +# |
392 | +# This program is distributed in the hope that it will be useful, |
393 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
394 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
395 | +# GNU Lesser General Public License for more details. |
396 | +# |
397 | +# You should have received a copy of the GNU Lesser General Public License |
398 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
399 | + |
400 | +import datetime |
401 | +import logging |
402 | + |
403 | +from autopilot import logging as autopilot_logging |
404 | +from autopilot.introspection import dbus |
405 | + |
406 | +from ubuntuuitoolkit._custom_proxy_objects import ( |
407 | + _common, |
408 | + _flickable, |
409 | + _qquicklistview |
410 | +) |
411 | + |
412 | + |
413 | +logger = logging.getLogger(__name__) |
414 | + |
415 | + |
416 | +class DatePicker(_common.UbuntuUIToolkitCustomProxyObjectBase): |
417 | + """Autopilot helper for the DatePicker component.""" |
418 | + |
419 | + @autopilot_logging.log_action(logger.info) |
420 | + def pick_date(self, date): |
421 | + """Pick a date from the date picker. |
422 | + |
423 | + :parameter date: The date to pick. |
424 | + :type date: An object with year, month and day attributes, like |
425 | + python's datetime.date. |
426 | + :raises ubuntuuitoolkit.ToolkitException if the mode of the picker |
427 | + doesn't let select a date. |
428 | + |
429 | + """ |
430 | + if not self._is_date_picker(): |
431 | + raise _common.ToolkitException( |
432 | + "Can't pick date. The picker mode is: {!r}.".format(self.mode)) |
433 | + if 'Years' in self.mode: |
434 | + self._pick_year(date.year) |
435 | + self.year.wait_for(date.year) |
436 | + if 'Month' in self.mode: |
437 | + # Python's date object starts at one. The model in the date picker |
438 | + # at 0. |
439 | + self._pick_month(date.month - 1) |
440 | + self.month.wait_for(date.month - 1) |
441 | + if 'Day' in self.mode: |
442 | + self._pick_day(date.day) |
443 | + self.day.wait_for(date.day) |
444 | + |
445 | + def _is_date_picker(self): |
446 | + mode = self.mode |
447 | + if 'Years' in mode or 'Months' in mode or 'Days' in mode: |
448 | + return True |
449 | + else: |
450 | + return False |
451 | + |
452 | + @autopilot_logging.log_action(logger.info) |
453 | + def _pick_year(self, year): |
454 | + picker = self.select_single( |
455 | + 'Picker', objectName='PickerRow_YearPicker') |
456 | + list_view = picker.select_single( |
457 | + _qquicklistview.QQuickListView, objectName='Picker_Linear') |
458 | + self._pick_date_value(self.year, year, list_view) |
459 | + |
460 | + @autopilot_logging.log_action(logger.info) |
461 | + def _pick_month(self, month): |
462 | + picker = self.select_single( |
463 | + 'Picker', objectName='PickerRow_MonthPicker') |
464 | + path_view = picker.select_single( |
465 | + QQuickPathView, objectName='Picker_WrapAround') |
466 | + self._pick_date_value(self.month, month, path_view) |
467 | + |
468 | + @autopilot_logging.log_action(logger.info) |
469 | + def _pick_day(self, day): |
470 | + picker = self.select_single( |
471 | + 'Picker', objectName='PickerRow_DayPicker') |
472 | + path_view = picker.select_single( |
473 | + QQuickPathView, objectName='Picker_WrapAround') |
474 | + # Python's date object starts at one. The model in the date picker |
475 | + # at 0. |
476 | + self._pick_date_value(self.get_date().day - 1, day - 1, path_view) |
477 | + |
478 | + def _pick_date_value(self, current_value, new_value, scrollable): |
479 | + if new_value > current_value: |
480 | + direction = 'below' |
481 | + elif new_value < current_value: |
482 | + direction = 'above' |
483 | + else: |
484 | + logger.debug('The value is already selected.') |
485 | + return |
486 | + scrollable.click_element( |
487 | + object_name='PickerRow_PickerLabel{}'.format(new_value), |
488 | + direction=direction) |
489 | + |
490 | + def get_date(self): |
491 | + """Return the currently selected date. |
492 | + |
493 | + :return: a python datetime.date object with the selected date. |
494 | + |
495 | + """ |
496 | + # Python's date object starts at one. The model in the date picker |
497 | + # at 0. |
498 | + return datetime.date( |
499 | + self.year, self.month + 1, self.day) |
500 | + |
501 | + |
502 | +class QQuickPathView(_flickable.Scrollable): |
503 | + |
504 | + # TODO make it more general and move it to its own module. |
505 | + # --elopio - 2014-05-06 |
506 | + |
507 | + @autopilot_logging.log_action(logger.info) |
508 | + def click_element(self, object_name, direction='below'): |
509 | + try: |
510 | + element = self.select_single(objectName=object_name) |
511 | + except dbus.StateNotFoundError: |
512 | + # The element might be on a part of the list that hasn't been |
513 | + # created yet. We have to search for it scrolling. |
514 | + element = self._find_element(object_name, direction) |
515 | + self.swipe_child_into_view(element) |
516 | + self.pointing_device.click_object(element) |
517 | + |
518 | + @autopilot_logging.log_action(logger.info) |
519 | + def _find_element(self, object_name, direction): |
520 | + containers = self._get_containers() |
521 | + for index in range(self.count): |
522 | + if direction == 'below': |
523 | + swipe_method = self._swipe_to_show_one_more_below |
524 | + elif direction == 'above': |
525 | + swipe_method = self._swipe_to_show_one_more_above |
526 | + else: |
527 | + raise _common.ToolkitException( |
528 | + 'Invalid direction: {}'.format(direction)) |
529 | + |
530 | + swipe_method(containers) |
531 | + |
532 | + try: |
533 | + return self.select_single(objectName=object_name) |
534 | + except dbus.StateNotFoundError: |
535 | + pass |
536 | + raise _common.ToolkitException( |
537 | + 'List element with objectName "{}" not found.'.format(object_name)) |
538 | + |
539 | + @autopilot_logging.log_action(logger.info) |
540 | + def _swipe_to_show_one_more_above(self, containers): |
541 | + self._swipe_to_show_one_more('above', containers) |
542 | + |
543 | + @autopilot_logging.log_action(logger.info) |
544 | + def _swipe_to_show_one_more_below(self, containers): |
545 | + self._swipe_to_show_one_more('below', containers) |
546 | + |
547 | + def _swipe_to_show_one_more(self, direction, containers): |
548 | + start_x = stop_x = self.globalRect.x + (self.globalRect.width // 2) |
549 | + center_y = self.globalRect.y + (self.globalRect.height // 2) |
550 | + # XXX This assumes all the children are of the same height |
551 | + child = self.get_children_by_type('PickerDelegate')[0] |
552 | + top = center_y - (child.globalRect.height // 2) |
553 | + bottom = center_y + (child.globalRect.height // 2) |
554 | + if direction == 'below': |
555 | + start_y = bottom |
556 | + stop_y = top |
557 | + elif direction == 'above': |
558 | + start_y = top |
559 | + stop_y = bottom |
560 | + else: |
561 | + raise _common.ToolkitException( |
562 | + 'Invalid direction {}.'.format(direction)) |
563 | + self._slow_drag(start_x, stop_x, start_y, stop_y) |
564 | + self.dragging.wait_for(False) |
565 | + self.moving.wait_for(False) |
566 | + |
567 | + @autopilot_logging.log_action(logger.info) |
568 | + def swipe_child_into_view(self, child): |
569 | + """Make the child visible. |
570 | + |
571 | + Currently it works only when the object needs to be swiped vertically. |
572 | + TODO implement horizontal swiping. --elopio - 2014-03-21 |
573 | + |
574 | + """ |
575 | + containers = self._get_containers() |
576 | + if not self._is_child_visible(child, containers): |
577 | + self._swipe_non_visible_child_into_view(child, containers) |
578 | + else: |
579 | + logger.debug('The element is already visible.') |
580 | + |
581 | + @autopilot_logging.log_action(logger.info) |
582 | + def _swipe_non_visible_child_into_view(self, child, containers): |
583 | + while not self._is_child_visible(child, containers): |
584 | + # Check the direction of the swipe based on the position of the |
585 | + # child relative to the immediate flickable container. |
586 | + if child.globalRect.y < self.globalRect.y: |
587 | + self._swipe_to_show_one_more_above(containers) |
588 | + else: |
589 | + self._swipe_to_show_one_more_below(containers) |
590 | |
591 | === modified file 'tests/autopilot/ubuntuuitoolkit/emulators.py' |
592 | --- tests/autopilot/ubuntuuitoolkit/emulators.py 2014-04-25 18:39:51 +0000 |
593 | +++ tests/autopilot/ubuntuuitoolkit/emulators.py 2014-05-09 04:12:47 +0000 |
594 | @@ -36,12 +36,12 @@ |
595 | 'CheckBox', |
596 | 'ComposerSheet', |
597 | 'Empty', |
598 | - 'Flickable', |
599 | 'Header', |
600 | 'ItemSelector', |
601 | 'MainView', |
602 | 'MultiValue', |
603 | 'OptionSelector', |
604 | + 'QQuickFlickable', |
605 | 'QQuickListView', |
606 | 'SingleControl', |
607 | 'SingleValue', |
608 | @@ -61,10 +61,10 @@ |
609 | get_keyboard, |
610 | get_pointing_device, |
611 | CheckBox, |
612 | - Flickable, |
613 | Header, |
614 | MainView, |
615 | OptionSelector, |
616 | + QQuickFlickable, |
617 | QQuickListView, |
618 | TabBar, |
619 | Tabs, |
620 | |
621 | === added file 'tests/autopilot/ubuntuuitoolkit/tests/custom_proxy_objects/test_date_picker.py' |
622 | --- tests/autopilot/ubuntuuitoolkit/tests/custom_proxy_objects/test_date_picker.py 1970-01-01 00:00:00 +0000 |
623 | +++ tests/autopilot/ubuntuuitoolkit/tests/custom_proxy_objects/test_date_picker.py 2014-05-09 04:12:47 +0000 |
624 | @@ -0,0 +1,147 @@ |
625 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
626 | +# |
627 | +# Copyright (C) 2014 Canonical Ltd. |
628 | +# |
629 | +# This program is free software; you can redistribute it and/or modify |
630 | +# it under the terms of the GNU Lesser General Public License as published by |
631 | +# the Free Software Foundation; version 3. |
632 | +# |
633 | +# This program is distributed in the hope that it will be useful, |
634 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
635 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
636 | +# GNU Lesser General Public License for more details. |
637 | +# |
638 | +# You should have received a copy of the GNU Lesser General Public License |
639 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
640 | + |
641 | +import datetime |
642 | + |
643 | +import ubuntuuitoolkit |
644 | +from ubuntuuitoolkit import pickers, tests |
645 | + |
646 | + |
647 | +class DatePickerBaseTestCase(tests.QMLStringAppTestCase): |
648 | + |
649 | + test_qml = (""" |
650 | +import QtQuick 2.0 |
651 | +import Ubuntu.Components 1.1 |
652 | +import Ubuntu.Components.Pickers 1.0 |
653 | + |
654 | +MainView { |
655 | + width: units.gu(48) |
656 | + height: units.gu(60) |
657 | + |
658 | + Column { |
659 | + DatePicker { |
660 | + id: datePicker |
661 | + objectName: 'datePicker' |
662 | + mode: 'Years|Months|Days' |
663 | + date: { |
664 | + var d = new Date() |
665 | + // Make sure that the picker will have higher and lower values |
666 | + // to select. |
667 | + d.setFullYear(d.getFullYear() + 25) |
668 | + d.setMonth('5') |
669 | + d.setDate('15') |
670 | + return d |
671 | + } |
672 | + } |
673 | + DatePicker { |
674 | + id: timePicker |
675 | + objectName: 'timePicker' |
676 | + mode: 'Hours|Minutes|Seconds' |
677 | + } |
678 | + } |
679 | +} |
680 | +""") |
681 | + |
682 | + def setUp(self): |
683 | + super(DatePickerBaseTestCase, self).setUp() |
684 | + self.date_picker = self.main_view.select_single( |
685 | + pickers.DatePicker, objectName='datePicker') |
686 | + |
687 | + |
688 | +class DatePickerTestCase(DatePickerBaseTestCase): |
689 | + |
690 | + def test_select_date_picker_must_return_custom_proxy_object(self): |
691 | + self.assertIsInstance( |
692 | + self.date_picker, pickers.DatePicker) |
693 | + |
694 | + def test_pick_date_on_time_picker_must_raise_exception(self): |
695 | + time_picker = self.main_view.select_single( |
696 | + pickers.DatePicker, objectName='timePicker') |
697 | + error = self.assertRaises( |
698 | + ubuntuuitoolkit.ToolkitException, time_picker.pick_date, 'dummy') |
699 | + self.assertEqual( |
700 | + str(error), |
701 | + "Can't pick date. The picker mode is: {!r}.".format( |
702 | + time_picker.mode)) |
703 | + |
704 | + def test_swipe_to_show_one_more_below_must_select_next_index(self): |
705 | + """Test that we don't end up swiping more than needed. |
706 | + |
707 | + This would cause us to miss the element we are looking for, and to have |
708 | + to swipe many times in order to finally click it. |
709 | + |
710 | + """ |
711 | + picker = self.main_view.select_single( |
712 | + 'Picker', objectName='PickerRow_DayPicker') |
713 | + path_view = picker.select_single( |
714 | + pickers.QQuickPathView, objectName='Picker_WrapAround') |
715 | + current_index = path_view.currentIndex |
716 | + |
717 | + path_view._swipe_to_show_one_more_below(path_view._get_containers()) |
718 | + |
719 | + self.assertEqual(path_view.currentIndex, current_index + 1) |
720 | + |
721 | + def test_swipe_to_show_one_more_above_must_select_previous_index(self): |
722 | + """Test that we don't end up swiping more than needed. |
723 | + |
724 | + This would cause us to miss the element we are looking for, and to have |
725 | + to swipe many times in order to finally click it. |
726 | + |
727 | + """ |
728 | + picker = self.main_view.select_single( |
729 | + 'Picker', objectName='PickerRow_DayPicker') |
730 | + path_view = picker.select_single( |
731 | + pickers.QQuickPathView, objectName='Picker_WrapAround') |
732 | + current_index = path_view.currentIndex |
733 | + |
734 | + path_view._swipe_to_show_one_more_above(path_view._get_containers()) |
735 | + |
736 | + self.assertEqual(path_view.currentIndex, current_index - 1) |
737 | + |
738 | + |
739 | +class PickDateFromDatePickerTestCase(DatePickerBaseTestCase): |
740 | + |
741 | + SELECTED_YEAR = datetime.date.today().year + 25 |
742 | + SELECTED_MONTH = 6 # June |
743 | + SELECTED_DAY = 15 |
744 | + |
745 | + scenarios = [ |
746 | + ('higher year', { |
747 | + 'date_to_pick': datetime.date( |
748 | + SELECTED_YEAR + 10, SELECTED_MONTH, SELECTED_DAY)}), |
749 | + ('lower year', { |
750 | + 'date_to_pick': datetime.date( |
751 | + SELECTED_YEAR - 10, SELECTED_MONTH, SELECTED_DAY)}), |
752 | + ('higher month', { |
753 | + 'date_to_pick': datetime.date( |
754 | + SELECTED_YEAR, SELECTED_MONTH + 4, SELECTED_DAY)}), |
755 | + ('lower month', { |
756 | + 'date_to_pick': datetime.date( |
757 | + SELECTED_YEAR, SELECTED_MONTH - 4, SELECTED_DAY)}), |
758 | + ('higher day', { |
759 | + 'date_to_pick': datetime.date( |
760 | + SELECTED_YEAR, SELECTED_MONTH, SELECTED_DAY + 10)}), |
761 | + ('lower day', { |
762 | + 'date_to_pick': datetime.date( |
763 | + SELECTED_YEAR, SELECTED_MONTH, SELECTED_DAY - 10)}), |
764 | + ('change all value', { |
765 | + 'date_to_pick': datetime.date( |
766 | + SELECTED_YEAR - 10, SELECTED_MONTH + 4, SELECTED_DAY - 10)}), |
767 | + ] |
768 | + |
769 | + def test_pick_date(self): |
770 | + self.date_picker.pick_date(self.date_to_pick) |
771 | + self.assertEqual(self.date_picker.get_date(), self.date_to_pick) |
772 | |
773 | === added file 'tests/autopilot/ubuntuuitoolkit/tests/custom_proxy_objects/test_flickable.py' |
774 | --- tests/autopilot/ubuntuuitoolkit/tests/custom_proxy_objects/test_flickable.py 1970-01-01 00:00:00 +0000 |
775 | +++ tests/autopilot/ubuntuuitoolkit/tests/custom_proxy_objects/test_flickable.py 2014-05-09 04:12:47 +0000 |
776 | @@ -0,0 +1,197 @@ |
777 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
778 | +# |
779 | +# Copyright (C) 2014 Canonical Ltd. |
780 | +# |
781 | +# This program is free software; you can redistribute it and/or modify |
782 | +# it under the terms of the GNU Lesser General Public License as published by |
783 | +# the Free Software Foundation; version 3. |
784 | +# |
785 | +# This program is distributed in the hope that it will be useful, |
786 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
787 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
788 | +# GNU Lesser General Public License for more details. |
789 | +# |
790 | +# You should have received a copy of the GNU Lesser General Public License |
791 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
792 | + |
793 | + |
794 | +import testtools |
795 | + |
796 | +import ubuntuuitoolkit |
797 | +from ubuntuuitoolkit import tests |
798 | +from ubuntuuitoolkit._custom_proxy_objects import _common |
799 | + |
800 | + |
801 | +class FlickableTestCase(testtools.TestCase): |
802 | + |
803 | + def test_get_unity_top_container(self): |
804 | + """Test that we can get the top cointainer in Unity.""" |
805 | + # This tests bug http://pad.lv/1314390 |
806 | + # On Unity, the top container is not the first child as it is in all |
807 | + # the apps that have a MainView. This makes the first implementation of |
808 | + # _get_top_container fail. Instead of going from the top looking for |
809 | + # a container, we should start from the flickable until we find the |
810 | + # top-most container. |
811 | + RootClass = type('obj', (object,), {'id': 'root'}) |
812 | + mock_root_instance = RootClass() |
813 | + # We consider a container is an object with a globalRect. |
814 | + MockNonContainerClass = type('obj', (object,), {}) |
815 | + mock_non_container = MockNonContainerClass() |
816 | + MockContainerClass = type( |
817 | + 'obj', (object,), {'id': 'container', 'globalRect': 'dummy'}) |
818 | + mock_container = MockContainerClass() |
819 | + mock_container.get_parent = lambda: mock_root_instance |
820 | + |
821 | + # The root instance has two children. This exposes the bug. |
822 | + mock_root_instance.get_children = lambda: [ |
823 | + mock_non_container, mock_container] |
824 | + |
825 | + dummy_state = {'id': '10'} |
826 | + flickable = ubuntuuitoolkit.QQuickFlickable( |
827 | + dummy_state, 'dummy', 'dummy') |
828 | + |
829 | + flickable.get_root_instance = lambda: mock_root_instance |
830 | + # The top container of the flickable is its immediate parent. |
831 | + flickable.get_parent = lambda: mock_container |
832 | + |
833 | + top_container = flickable._get_top_container() |
834 | + self.assertEqual(top_container, mock_container) |
835 | + |
836 | + def test_is_flickable_with_flicking_property_must_return_true(self): |
837 | + """is_flickable returns True if flickable property exists.""" |
838 | + dummy_id = (0, 0) |
839 | + dummy_flicking = (0, 'dummy') |
840 | + state_with_flicking = {'id': dummy_id, 'flicking': dummy_flicking} |
841 | + element = _common.UbuntuUIToolkitCustomProxyObjectBase( |
842 | + state_with_flicking, 'dummy', 'dummy') |
843 | + with element.no_automatic_refreshing(): |
844 | + self.assertTrue(element.is_flickable()) |
845 | + |
846 | + def test_is_flickable_without_flicking_property_must_return_false(self): |
847 | + """is_flickable returns False if flickable property doesn't exist.""" |
848 | + dummy_id = (0, 0) |
849 | + state_without_flicking = {'id': dummy_id} |
850 | + element = _common.UbuntuUIToolkitCustomProxyObjectBase( |
851 | + state_without_flicking, 'dummy', 'dummy') |
852 | + with element.no_automatic_refreshing(): |
853 | + self.assertFalse(element.is_flickable()) |
854 | + |
855 | + |
856 | +class IsFlickableTestCase(tests.QMLStringAppTestCase): |
857 | + """Functional test to check that is_flickable returns the right value. |
858 | + |
859 | + We already have tests for is_flickable with mocks, so here we just check |
860 | + with some real elements. |
861 | + |
862 | + """ |
863 | + |
864 | + test_qml = (""" |
865 | +import QtQuick 2.0 |
866 | +import Ubuntu.Components 0.1 |
867 | +import Ubuntu.Components.ListItems 0.1 as ListItem |
868 | + |
869 | +MainView { |
870 | + objectName: 'mainView' |
871 | + width: units.gu(48) |
872 | + height: units.gu(60) |
873 | + |
874 | + Flickable { |
875 | + objectName: 'flickable' |
876 | + } |
877 | + ListView { |
878 | + objectName: 'listView' |
879 | + } |
880 | + Label { |
881 | + objectName: 'label' |
882 | + } |
883 | +} |
884 | +""") |
885 | + |
886 | + scenarios = [ |
887 | + ('main view', dict(object_name='mainView', is_flickable=False)), |
888 | + ('flickable', dict(object_name='flickable', is_flickable=True)), |
889 | + ('list view', dict(object_name='listView', is_flickable=True)), |
890 | + ('label', dict(object_name='label', is_flickable=False)) |
891 | + ] |
892 | + |
893 | + def test_is_flickable(self): |
894 | + """Test that is_flickable identifies the elements correctly.""" |
895 | + element = self.app.select_single(objectName=self.object_name) |
896 | + self.assertEqual(element.is_flickable(), self.is_flickable) |
897 | + |
898 | + |
899 | +class SwipeIntoViewTestCase(tests.QMLStringAppTestCase): |
900 | + |
901 | + test_qml = (""" |
902 | +import QtQuick 2.0 |
903 | +import Ubuntu.Components 0.1 |
904 | + |
905 | +MainView { |
906 | + width: units.gu(48) |
907 | + height: units.gu(60) |
908 | + |
909 | + Label { |
910 | + id: clickedLabel |
911 | + objectName: "clickedLabel" |
912 | + text: "No element clicked." |
913 | + } |
914 | + |
915 | + Flickable { |
916 | + anchors { |
917 | + fill: parent |
918 | + topMargin: clickedLabel.height |
919 | + // It can't be at the bottom, or the toolbar will be opened |
920 | + // when we try to click it. |
921 | + bottomMargin: units.gu(10) |
922 | + } |
923 | + objectName: 'flickable' |
924 | + height: units.gu(60) |
925 | + contentHeight: bottomButton.y + bottomButton.height |
926 | + |
927 | + Button { |
928 | + id: topButton |
929 | + objectName: 'topButton' |
930 | + text: 'Top button' |
931 | + onClicked: clickedLabel.text = objectName |
932 | + } |
933 | + Rectangle { |
934 | + id: emptyRectangle |
935 | + height: units.gu(80) |
936 | + anchors.top: topButton.bottom |
937 | + } |
938 | + Button { |
939 | + id: bottomButton |
940 | + objectName: 'bottomButton' |
941 | + text: 'Bottom button' |
942 | + onClicked: clickedLabel.text = objectName |
943 | + anchors.top: emptyRectangle.bottom |
944 | + } |
945 | + } |
946 | +} |
947 | +""") |
948 | + |
949 | + def setUp(self): |
950 | + super(SwipeIntoViewTestCase, self).setUp() |
951 | + self.label = self.main_view.select_single( |
952 | + 'Label', objectName='clickedLabel') |
953 | + self.assertEqual(self.label.text, 'No element clicked.') |
954 | + |
955 | + def test_swipe_to_bottom(self): |
956 | + self.main_view.close_toolbar() |
957 | + |
958 | + button = self.main_view.select_single(objectName='bottomButton') |
959 | + button.swipe_into_view() |
960 | + |
961 | + self.pointing_device.click_object(button) |
962 | + self.assertEqual(self.label.text, 'bottomButton') |
963 | + |
964 | + def test_swipe_to_top(self): |
965 | + self.main_view.close_toolbar() |
966 | + bottomButton = self.main_view.select_single(objectName='bottomButton') |
967 | + bottomButton.swipe_into_view() |
968 | + |
969 | + topButton = self.main_view.select_single(objectName='topButton') |
970 | + topButton.swipe_into_view() |
971 | + |
972 | + self.pointing_device.click_object(topButton) |
973 | + self.assertEqual(self.label.text, 'topButton') |
974 | |
975 | === modified file 'tests/autopilot/ubuntuuitoolkit/tests/test_emulators.py' |
976 | --- tests/autopilot/ubuntuuitoolkit/tests/test_emulators.py 2014-04-28 15:39:24 +0000 |
977 | +++ tests/autopilot/ubuntuuitoolkit/tests/test_emulators.py 2014-05-09 04:12:47 +0000 |
978 | @@ -32,7 +32,7 @@ |
979 | |
980 | symbols_retaining_name = [ |
981 | 'check_autopilot_version', 'get_keyboard', 'get_pointing_device', |
982 | - 'CheckBox', 'Flickable', 'Header', 'MainView', 'OptionSelector', |
983 | + 'CheckBox', 'Header', 'MainView', 'OptionSelector', 'QQuickFlickable', |
984 | 'QQuickListView', 'TabBar', 'Tabs', 'TextField', 'Toolbar', |
985 | ] |
986 |
Ok I think understand this change, appears thoroughly tested, nice work :) . I'm assuming that the comment on l.535 doesn't apply to the present merge proposal.