Unity 8
 All Classes Functions Properties
test_notifications.py
1 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2 #
3 # Unity Autopilot Test Suite
4 # Copyright (C) 2012, 2013, 2014 Canonical
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 #
19 
20 """Tests for Notifications"""
21 
22 from __future__ import absolute_import
23 
24 from unity8 import shell
25 from unity8.process_helpers import unlock_unity
26 from unity8.shell.tests import UnityTestCase, _get_device_emulation_scenarios
27 
28 from testtools.matchers import Equals, NotEquals
29 from autopilot.matchers import Eventually
30 
31 from gi.repository import Notify
32 import time
33 import os
34 import logging
35 import signal
36 import subprocess
37 import sys
38 
39 logger = logging.getLogger(__name__)
40 
41 # from __future__ import range
42 # (python3's range, is same as python2's xrange)
43 if sys.version_info < (3,):
44  range = xrange
45 
46 
48  """Base class for all notification tests that provides helper methods."""
49 
50  scenarios = _get_device_emulation_scenarios('Nexus4')
51 
52  def _get_icon_path(self, icon_name):
53  """Given an icons file name returns the full path (either system or
54  source tree.
55 
56  Consider the graphics directory as root so for example (running tests
57  from installed unity8-autopilot package):
58  >>> self.get_icon_path('clock.png')
59  /usr/share/unity8/graphics/clock.png
60 
61  >>> self.get_icon_path('applicationIcons/facebook.png')
62  /usr/share/unity8/graphics/applicationIcons/facebook.png
63 
64  """
65  if os.path.abspath(__file__).startswith('/usr/'):
66  return '/usr/share/unity8/graphics/' + icon_name
67  else:
68  return os.path.dirname(__file__) + (
69  "/../../../../../qml/graphics/" + icon_name)
70 
71  def _get_notifications_list(self):
72  return self.main_window.select_single(
73  "Notifications",
74  objectName='notificationList'
75  )
76 
77  def _assert_notification(
78  self,
79  notification,
80  summary=None,
81  body=None,
82  icon=True,
83  secondary_icon=False,
84  opacity=None
85  ):
86  """Assert that the expected qualities of a notification are as
87  expected.
88 
89  """
90 
91  if summary is not None:
92  self.assertThat(notification.summary, Eventually(Equals(summary)))
93 
94  if body is not None:
95  self.assertThat(notification.body, Eventually(Equals(body)))
96 
97  if icon:
98  self.assertThat(notification.iconSource, Eventually(NotEquals("")))
99  else:
100  self.assertThat(notification.iconSource, Eventually(Equals("")))
101 
102  if secondary_icon:
103  self.assertThat(
104  notification.secondaryIconSource,
105  Eventually(NotEquals(""))
106  )
107  else:
108  self.assertThat(
109  notification.secondaryIconSource,
110  Eventually(Equals(""))
111  )
112 
113  if opacity is not None:
114  self.assertThat(notification.opacity, Eventually(Equals(opacity)))
115 
116 
118  """Collection of test for Interactive tests including snap decisions."""
119 
120  def setUp(self):
121  super(InteractiveNotificationBase, self).setUp()
122  # Need to keep track when we launch the notification script.
123  self._notify_proc = None
124 
125  def test_interactive(self):
126  """Interactive notification must react upon click on itself."""
127  unity_proxy = self.launch_unity()
128  unlock_unity(unity_proxy)
129 
130  notify_list = self._get_notifications_list()
131 
132  summary = "Interactive notification"
133  body = "This notification can be clicked on to trigger an action."
134  icon_path = self._get_icon_path('avatars/anna_olsson.png')
135  actions = [("action_id", "dummy")]
136  hints = [
137  ("x-canonical-switch-to-application", "true"),
138  (
139  "x-canonical-secondary-icon",
140  self._get_icon_path('applicationIcons/phone-app.png')
141  )
142  ]
143 
145  summary,
146  body,
147  icon_path,
148  "NORMAL",
149  actions,
150  hints,
151  )
152 
153  get_notification = lambda: notify_list.wait_select_single(
154  'Notification', objectName='notification1')
155  notification = get_notification()
156 
157  self.touch.tap_object(
158  notification.select_single(objectName="interactiveArea")
159  )
160 
162 
164  """Rejecting a call should make notification expand and
165  offer more options."""
166  unity_proxy = self.launch_unity()
167  unlock_unity(unity_proxy)
168 
169  summary = "Incoming call"
170  body = "Frank Zappa\n+44 (0)7736 027340"
171  icon_path = self._get_icon_path('avatars/anna_olsson.png')
172  hints = [
173  (
174  "x-canonical-secondary-icon",
175  self._get_icon_path('applicationIcons/phone-app.png')
176  ),
177  ("x-canonical-snap-decisions", "true"),
178  ]
179 
180  actions = [
181  ('action_accept', 'Accept'),
182  ('action_decline_1', 'Decline'),
183  ('action_decline_2', '"Can\'t talk now, what\'s up?"'),
184  ('action_decline_3', '"I call you back."'),
185  ('action_decline_4', 'Send custom message...'),
186  ]
187 
189  summary,
190  body,
191  icon_path,
192  "NORMAL",
193  actions,
194  hints
195  )
196 
197  notify_list = self._get_notifications_list()
198  get_notification = lambda: notify_list.wait_select_single(
199  'Notification', objectName='notification1')
200  notification = get_notification()
201  self._assert_notification(notification, summary, body, True, True, 1.0)
202  initial_height = notification.height
203  self.touch.tap_object(notification.select_single(objectName="button1"))
204  self.assertThat(
205  notification.height,
206  Eventually(Equals(initial_height +
207  3 * notification.select_single(
208  objectName="buttonColumn").spacing +
209  3 * notification.select_single(
210  objectName="button4").height)))
211  self.touch.tap_object(notification.select_single(objectName="button4"))
212  self.assert_notification_action_id_was_called("action_decline_4")
213 
215  """A snap-decision on a phone should block input to shell beneath it when there's no greeter."""
216  unity_proxy = self.launch_unity()
217  unlock_unity(unity_proxy)
218 
219  summary = "Incoming file"
220  body = "Frank would like to send you the file: essay.pdf"
221  icon_path = "sync-idle"
222  hints = [
223  ("x-canonical-snap-decisions", "true"),
224  ("x-canonical-non-shaped-icon", "true"),
225  ]
226 
227  actions = [
228  ('action_accept', 'Accept'),
229  ('action_decline_1', 'Decline'),
230  ]
231 
233  summary,
234  body,
235  icon_path,
236  "NORMAL",
237  actions,
238  hints
239  )
240 
241  # verify that we cannot reveal the launcher (no longer interact with the shell)
242  time.sleep(1)
243  self.main_window.show_dash_swiping()
244  launcher = self.main_window.get_launcher()
245  self.assertThat(launcher.shown, Eventually(Equals(False)))
246 
247  # verify and interact with the triggered snap-decision notification
248  notify_list = self._get_notifications_list()
249  get_notification = lambda: notify_list.wait_select_single(
250  'Notification', objectName='notification1')
251  notification = get_notification()
252  self._assert_notification(notification, summary, body, True, False, 1.0)
253  self.touch.tap_object(notification.select_single(objectName="button0"))
254  self.assert_notification_action_id_was_called("action_accept")
255 
257  """A snap-decision on a phone should not block input to the greeter beneath it."""
258  unity_proxy = self.launch_unity()
259 
260  summary = "Incoming file"
261  body = "Frank would like to send you the file: essay.pdf"
262  icon_path = "sync-idle"
263  hints = [
264  ("x-canonical-snap-decisions", "true"),
265  ("x-canonical-non-shaped-icon", "true"),
266  ]
267 
268  actions = [
269  ('action_accept', 'Accept'),
270  ('action_decline_1', 'Decline'),
271  ]
272 
274  summary,
275  body,
276  icon_path,
277  "NORMAL",
278  actions,
279  hints
280  )
281 
282  # verify that we can swipe away the greeter (interact with the "shell")
283  time.sleep(1)
284  self.main_window.show_dash_swiping()
285  greeter = self.main_window.get_greeter()
286  self.assertThat(greeter.shown, Eventually(Equals(False)))
287 
288  # verify and interact with the triggered snap-decision notification
289  notify_list = self._get_notifications_list()
290  get_notification = lambda: notify_list.wait_select_single(
291  'Notification', objectName='notification1')
292  notification = get_notification()
293  self._assert_notification(notification, summary, body, True, False, 1.0)
294  self.touch.tap_object(notification.select_single(objectName="button0"))
295  self.assert_notification_action_id_was_called("action_accept")
296 
297  def _create_interactive_notification(
298  self,
299  summary="",
300  body="",
301  icon=None,
302  urgency="NORMAL",
303  actions=[],
304  hints=[]
305  ):
306  """Create a interactive notification command.
307 
308  :param summary: Summary text for the notification
309  :param body: Body text to display in the notification
310  :param icon: Path string to the icon to use
311  :param urgency: Urgency string for the noticiation, either: 'LOW',
312  'NORMAL', 'CRITICAL'
313  :param actions: List of tuples containing the 'id' and 'label' for all
314  the actions to add
315  :param hint_strings: List of tuples containing the 'name' and value for
316  setting the hint strings for the notification
317 
318  """
319 
320  logger.info(
321  "Creating snap-decision notification with summary(%s), body(%s) "
322  "and urgency(%r)",
323  summary,
324  body,
325  urgency
326  )
327 
328  script_args = [
329  '--summary', summary,
330  '--body', body,
331  '--urgency', urgency
332  ]
333 
334  if icon is not None:
335  script_args.extend(['--icon', icon])
336 
337  for hint in hints:
338  key, value = hint
339  script_args.extend(['--hint', "%s,%s" % (key, value)])
340 
341  for action in actions:
342  action_id, action_label = action
343  action_string = "%s,%s" % (action_id, action_label)
344  script_args.extend(['--action', action_string])
345 
346  python_bin = subprocess.check_output(['which', 'python']).strip()
347  command = [python_bin, self._get_notify_script()] + script_args
348  logger.info("Launching snap-decision notification as: %s", command)
349  self._notify_proc = subprocess.Popen(
350  command,
351  stdin=subprocess.PIPE,
352  stdout=subprocess.PIPE,
353  stderr=subprocess.PIPE,
354  close_fds=True,
355  universal_newlines=True,
356  )
357 
358  self.addCleanup(self._tidy_up_script_process)
359 
360  poll_result = self._notify_proc.poll()
361  if poll_result is not None and self._notify_proc.returncode != 0:
362  error_output = self._notify_proc.communicate()[1]
363  raise RuntimeError("Call to script failed with: %s" % error_output)
364 
365  def _get_notify_script(self):
366  """Returns the path to the interactive notification creation script."""
367  file_path = "../../emulators/create_interactive_notification.py"
368 
369  the_path = os.path.abspath(
370  os.path.join(__file__, file_path))
371 
372  return the_path
373 
374  def _tidy_up_script_process(self):
375  if self._notify_proc is not None and self._notify_proc.poll() is None:
376  logger.error("Notification process wasn't killed, killing now.")
377  os.killpg(self._notify_proc.pid, signal.SIGTERM)
378 
379  def assert_notification_action_id_was_called(self, action_id, timeout=10):
380  """Assert that the interactive notification callback of id *action_id*
381  was called.
382 
383  :raises AssertionError: If no interactive notification has actually
384  been created.
385  :raises AssertionError: When *action_id* does not match the actual
386  returned.
387  :raises AssertionError: If no callback was called at all.
388  """
389 
390  if self._notify_proc is None:
391  raise AssertionError("No interactive notification was created.")
392 
393  for i in range(timeout):
394  self._notify_proc.poll()
395  if self._notify_proc.returncode is not None:
396  output = self._notify_proc.communicate()
397  actual_action_id = output[0].strip("\n")
398  if actual_action_id != action_id:
399  raise AssertionError(
400  "action id '%s' does not match actual returned '%s'"
401  % (action_id, actual_action_id)
402  )
403  else:
404  return
405  time.sleep(1)
406 
407  os.killpg(self._notify_proc.pid, signal.SIGTERM)
408  self._notify_proc = None
409  raise AssertionError(
410  "No callback was called, killing interactivenotification script"
411  )
412 
413 
415  """Collection of tests for Emphemeral notifications (non-interactive.)"""
416 
417  def setUp(self):
418  super(EphemeralNotificationsTests, self).setUp()
419  # Because we are using the Notify library we need to init and un-init
420  # otherwise we get crashes.
421  Notify.init("Autopilot Ephemeral Notification Tests")
422  self.addCleanup(Notify.uninit)
423 
425  """Notification must display the expected summary and body text."""
426  unity_proxy = self.launch_unity()
427  unlock_unity(unity_proxy)
428 
429  notify_list = self._get_notifications_list()
430 
431  summary = "Icon-Summary-Body"
432  body = "Hey pal, what's up with the party next weekend? Will you " \
433  "join me and Anna?"
434  icon_path = self._get_icon_path('avatars/anna_olsson.png')
435  hints = [
436  (
437  "x-canonical-secondary-icon",
438  self._get_icon_path('applicationIcons/phone-app.png')
439  )
440  ]
441 
442  notification = shell.create_ephemeral_notification(
443  summary,
444  body,
445  icon_path,
446  hints,
447  "NORMAL",
448  )
449 
450  notification.show()
451 
452  notification = lambda: notify_list.wait_select_single(
453  'Notification', objectName='notification1')
455  notification(),
456  summary,
457  body,
458  True,
459  True,
460  1.0,
461  )
462 
463  def test_icon_summary(self):
464  """Notification must display the expected summary and secondary
465  icon."""
466  unity_proxy = self.launch_unity()
467  unlock_unity(unity_proxy)
468 
469  notify_list = self._get_notifications_list()
470 
471  summary = "Upload of image completed"
472  hints = [
473  (
474  "x-canonical-secondary-icon",
475  self._get_icon_path('applicationIcons/facebook.png')
476  )
477  ]
478 
479  notification = shell.create_ephemeral_notification(
480  summary,
481  None,
482  None,
483  hints,
484  "NORMAL",
485  )
486 
487  notification.show()
488 
489  notification = lambda: notify_list.wait_select_single(
490  'Notification', objectName='notification1')
492  notification(),
493  summary,
494  None,
495  False,
496  True,
497  1.0
498  )
499 
501  """Notifications must be displayed in order according to their
502  urgency."""
503  unity_proxy = self.launch_unity()
504  unlock_unity(unity_proxy)
505 
506  notify_list = self._get_notifications_list()
507 
508  summary_low = 'Low Urgency'
509  body_low = "No, I'd rather see paint dry, pal *yawn*"
510  icon_path_low = self._get_icon_path('avatars/amanda.png')
511 
512  summary_normal = 'Normal Urgency'
513  body_normal = "Hey pal, what's up with the party next weekend? Will " \
514  "you join me and Anna?"
515  icon_path_normal = self._get_icon_path('avatars/funky.png')
516 
517  summary_critical = 'Critical Urgency'
518  body_critical = 'Dude, this is so urgent you have no idea :)'
519  icon_path_critical = self._get_icon_path('avatars/anna_olsson.png')
520 
521  notification_normal = shell.create_ephemeral_notification(
522  summary_normal,
523  body_normal,
524  icon_path_normal,
525  urgency="NORMAL"
526  )
527  notification_normal.show()
528 
529  notification_low = shell.create_ephemeral_notification(
530  summary_low,
531  body_low,
532  icon_path_low,
533  urgency="LOW"
534  )
535  notification_low.show()
536 
537  notification_critical = shell.create_ephemeral_notification(
538  summary_critical,
539  body_critical,
540  icon_path_critical,
541  urgency="CRITICAL"
542  )
543  notification_critical.show()
544 
545  get_notification = lambda: notify_list.wait_select_single(
546  'Notification',
547  summary=summary_critical
548  )
549 
550  notification = get_notification()
552  notification,
553  summary_critical,
554  body_critical,
555  True,
556  False,
557  1.0
558  )
559 
560  get_normal_notification = lambda: notify_list.wait_select_single(
561  'Notification',
562  summary=summary_normal
563  )
564  notification = get_normal_notification()
566  notification,
567  summary_normal,
568  body_normal,
569  True,
570  False,
571  1.0
572  )
573 
574  get_low_notification = lambda: notify_list.wait_select_single(
575  'Notification',
576  summary=summary_low
577  )
578  notification = get_low_notification()
580  notification,
581  summary_low,
582  body_low,
583  True,
584  False,
585  1.0
586  )
587 
589  """Notification must display the expected summary- and body-text."""
590  unity_proxy = self.launch_unity()
591  unlock_unity(unity_proxy)
592 
593  notify_list = self._get_notifications_list()
594 
595  summary = 'Summary-Body'
596  body = 'This is a superfluous notification'
597 
598  notification = shell.create_ephemeral_notification(summary, body)
599  notification.show()
600 
601  notification = notify_list.wait_select_single(
602  'Notification', objectName='notification1')
603 
605  notification,
606  summary,
607  body,
608  False,
609  False,
610  1.0
611  )
612 
613  def test_summary_only(self):
614  """Notification must display only the expected summary-text."""
615  unity_proxy = self.launch_unity()
616  unlock_unity(unity_proxy)
617 
618  notify_list = self._get_notifications_list()
619 
620  summary = 'Summary-Only'
621 
622  notification = shell.create_ephemeral_notification(summary)
623  notification.show()
624 
625  notification = notify_list.wait_select_single(
626  'Notification', objectName='notification1')
627 
628  self._assert_notification(notification, summary, '', False, False, 1.0)
629 
631  """Notification must allow updating its contents while being
632  displayed."""
633  unity_proxy = self.launch_unity()
634  unlock_unity(unity_proxy)
635 
636  notify_list = self._get_notifications_list()
637 
638  summary = 'Initial notification'
639  body = 'This is the original content of this notification-bubble.'
640  icon_path = self._get_icon_path('avatars/funky.png')
641 
642  notification = shell.create_ephemeral_notification(
643  summary,
644  body,
645  icon_path
646  )
647  notification.show()
648 
649  get_notification = lambda: notify_list.wait_select_single(
650  'Notification', summary=summary)
652  get_notification(),
653  summary,
654  body,
655  True,
656  False,
657  1.0
658  )
659 
660  summary = 'Updated notification'
661  body = 'Here the same bubble with new title- and body-text, even ' \
662  'the icon can be changed on the update.'
663  icon_path = self._get_icon_path('avatars/amanda.png')
664  notification.update(summary, body, icon_path)
665  notification.show()
666  self._assert_notification(get_notification(), summary, body, True, False, 1.0)
667 
669  """Notification must allow updating its contents and layout while
670  being displayed."""
671  unity_proxy = self.launch_unity()
672  unlock_unity(unity_proxy)
673 
674  notify_list = self._get_notifications_list()
675 
676  summary = 'Initial layout'
677  body = 'This bubble uses the icon-title-body layout with a ' \
678  'secondary icon.'
679  icon_path = self._get_icon_path('avatars/anna_olsson.png')
680  hint_icon = self._get_icon_path('applicationIcons/phone-app.png')
681 
682  notification = shell.create_ephemeral_notification(
683  summary,
684  body,
685  icon_path
686  )
687  notification.set_hint_string(
688  'x-canonical-secondary-icon',
689  hint_icon
690  )
691  notification.show()
692 
693  get_notification = lambda: notify_list.wait_select_single(
694  'Notification', objectName='notification1')
695 
697  get_notification(),
698  summary,
699  body,
700  True,
701  True,
702  1.0
703  )
704 
705  notification.clear_hints()
706  summary = 'Updated layout'
707  body = 'After the update we now have a bubble using the title-body ' \
708  'layout.'
709  notification.update(summary, body, None)
710  notification.show()
711 
712  self.assertThat(get_notification, Eventually(NotEquals(None)))
713  self._assert_notification(get_notification(), summary, body, False, False, 1.0)
714 
715  def test_append_hint(self):
716  """Notification has to accumulate body-text using append-hint."""
717  unity_proxy = self.launch_unity()
718  unlock_unity(unity_proxy)
719 
720  notify_list = self._get_notifications_list()
721 
722  summary = 'Cole Raby'
723  body = 'Hey Bro Coly!'
724  icon_path = self._get_icon_path('avatars/amanda.png')
725  body_sum = body
726  notification = shell.create_ephemeral_notification(
727  summary,
728  body,
729  icon_path,
730  hints=[('x-canonical-append', 'true')]
731  )
732 
733  notification.show()
734 
735  get_notification = lambda: notify_list.wait_select_single(
736  'Notification', objectName='notification1')
737 
738  notification = get_notification()
740  notification,
741  summary,
742  body_sum,
743  True,
744  False,
745  1.0
746  )
747 
748  bodies = [
749  'What\'s up dude?',
750  'Did you watch the air-race in Oshkosh last week?',
751  'Phil owned the place like no one before him!',
752  'Did really everything in the race work according to regulations?',
753  'Somehow I think to remember Burt Williams did cut corners and '
754  'was not punished for this.',
755  'Hopefully the referees will watch the videos of the race.',
756  'Burt could get fined with US$ 50000 for that rule-violation :)'
757  ]
758 
759  for new_body in bodies:
760  body = new_body
761  body_sum += '\n' + body
762  notification = shell.create_ephemeral_notification(
763  summary,
764  body,
765  icon_path,
766  hints=[('x-canonical-append', 'true')]
767  )
768  notification.show()
769 
770  get_notification = lambda: notify_list.wait_select_single(
771  'Notification',
772  objectName='notification1'
773  )
774  notification = get_notification()
776  notification,
777  summary,
778  body_sum,
779  True,
780  False,
781  1.0
782  )