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/dialer-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/dialer-app.png')
176  ),
177  ("x-canonical-snap-decisions", "true"),
178  ]
179 
180  actions = [
181  ('action_accept', 'Hold + Answer'),
182  ('action_decline_1', 'End + Answer'),
183  ('action_decline_2', 'Decline'),
184  ('action_decline_3', 'messages:I missed your call - can you call me now?'),
185  ('action_decline_4', 'messages:I\'m running late. I\'m on my way.'),
186  ('action_decline_5', 'messages:I\'m busy at the moment. I\'ll call later.'),
187  ('action_decline_6', 'edit:Custom'),
188  ]
189 
191  summary,
192  body,
193  icon_path,
194  "NORMAL",
195  actions,
196  hints
197  )
198 
199  notify_list = self._get_notifications_list()
200  get_notification = lambda: notify_list.wait_select_single(
201  'Notification', objectName='notification1')
202  notification = get_notification()
203  self._assert_notification(notification, summary, body, True, True, 1.0)
204  initial_height = notification.height
205  self.touch.tap_object(notification.select_single(objectName="combobutton_dropdown"))
206  self.assertThat(notification.select_single(objectName="button2").expanded, Eventually(Equals(True)))
207  time.sleep(2)
208  self.touch.tap_object(notification.select_single(objectName="button4"))
209  self.assert_notification_action_id_was_called("action_decline_4")
210 
212  """Snap-decision should block input to shell without greeter."""
213  unity_proxy = self.launch_unity()
214  unlock_unity(unity_proxy)
215 
216  summary = "Incoming file"
217  body = "Frank would like to send you the file: essay.pdf"
218  icon_path = "sync-idle"
219  hints = [
220  ("x-canonical-snap-decisions", "true"),
221  ("x-canonical-non-shaped-icon", "true"),
222  ]
223 
224  actions = [
225  ('action_accept', 'Accept'),
226  ('action_decline_1', 'Decline'),
227  ]
228 
230  summary,
231  body,
232  icon_path,
233  "NORMAL",
234  actions,
235  hints
236  )
237 
238  # verify that we cannot reveal the launcher (no longer interact with
239  # the shell)
240  time.sleep(1)
241  self.main_window.show_dash_swiping()
242  launcher = self.main_window.get_launcher()
243  self.assertThat(launcher.shown, Eventually(Equals(False)))
244 
245  # verify and interact with the triggered snap-decision notification
246  notify_list = self._get_notifications_list()
247  get_notification = lambda: notify_list.wait_select_single(
248  'Notification', objectName='notification1')
249  notification = get_notification()
251  notification, summary, body, True, False, 1.0)
252  self.touch.tap_object(notification.select_single(objectName="button0"))
253  self.assert_notification_action_id_was_called("action_accept")
254 
256  """A snap-decision should not block input to the greeter beneath it."""
257  self.launch_unity()
258 
259  summary = "Incoming file"
260  body = "Frank would like to send you the file: essay.pdf"
261  icon_path = "sync-idle"
262  hints = [
263  ("x-canonical-snap-decisions", "true"),
264  ("x-canonical-non-shaped-icon", "true"),
265  ]
266 
267  actions = [
268  ('action_accept', 'Accept'),
269  ('action_decline_1', 'Decline'),
270  ]
271 
273  summary,
274  body,
275  icon_path,
276  "NORMAL",
277  actions,
278  hints
279  )
280 
281  # verify that we can swipe away the greeter (interact with the "shell")
282  time.sleep(1)
283  self.main_window.show_dash_swiping()
284  greeter = self.main_window.get_greeter()
285  self.assertThat(greeter.shown, Eventually(Equals(False)))
286 
287  # verify and interact with the triggered snap-decision notification
288  notify_list = self._get_notifications_list()
289  get_notification = lambda: notify_list.wait_select_single(
290  'Notification', objectName='notification1')
291  notification = get_notification()
293  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/dialer-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()
667  get_notification(), summary, body, True, False, 1.0)
668 
670  """Notification must allow updating its contents and layout while
671  being displayed."""
672  unity_proxy = self.launch_unity()
673  unlock_unity(unity_proxy)
674 
675  notify_list = self._get_notifications_list()
676 
677  summary = 'Initial layout'
678  body = 'This bubble uses the icon-title-body layout with a ' \
679  'secondary icon.'
680  icon_path = self._get_icon_path('avatars/anna_olsson.png')
681  hint_icon = self._get_icon_path('applicationIcons/dialer-app.png')
682 
683  notification = shell.create_ephemeral_notification(
684  summary,
685  body,
686  icon_path
687  )
688  notification.set_hint_string(
689  'x-canonical-secondary-icon',
690  hint_icon
691  )
692  notification.show()
693 
694  get_notification = lambda: notify_list.wait_select_single(
695  'Notification', objectName='notification1')
696 
698  get_notification(),
699  summary,
700  body,
701  True,
702  True,
703  1.0
704  )
705 
706  notification.clear_hints()
707  summary = 'Updated layout'
708  body = 'After the update we now have a bubble using the title-body ' \
709  'layout.'
710  notification.update(summary, body, None)
711  notification.show()
712 
713  self.assertThat(get_notification, Eventually(NotEquals(None)))
715  get_notification(), summary, body, False, False, 1.0)
716 
717  def test_append_hint(self):
718  """Notification has to accumulate body-text using append-hint."""
719  unity_proxy = self.launch_unity()
720  unlock_unity(unity_proxy)
721 
722  notify_list = self._get_notifications_list()
723 
724  summary = 'Cole Raby'
725  body = 'Hey Bro Coly!'
726  icon_path = self._get_icon_path('avatars/amanda.png')
727  body_sum = body
728  notification = shell.create_ephemeral_notification(
729  summary,
730  body,
731  icon_path,
732  hints=[('x-canonical-append', 'true')]
733  )
734 
735  notification.show()
736 
737  get_notification = lambda: notify_list.wait_select_single(
738  'Notification', objectName='notification1')
739 
740  notification = get_notification()
742  notification,
743  summary,
744  body_sum,
745  True,
746  False,
747  1.0
748  )
749 
750  bodies = [
751  'What\'s up dude?',
752  'Did you watch the air-race in Oshkosh last week?',
753  'Phil owned the place like no one before him!',
754  'Did really everything in the race work according to regulations?',
755  'Somehow I think to remember Burt Williams did cut corners and '
756  'was not punished for this.',
757  'Hopefully the referees will watch the videos of the race.',
758  'Burt could get fined with US$ 50000 for that rule-violation :)'
759  ]
760 
761  for new_body in bodies:
762  body = new_body
763  body_sum += '\n' + body
764  notification = shell.create_ephemeral_notification(
765  summary,
766  body,
767  icon_path,
768  hints=[('x-canonical-append', 'true')]
769  )
770  notification.show()
771 
772  get_notification = lambda: notify_list.wait_select_single(
773  'Notification',
774  objectName='notification1'
775  )
776  notification = get_notification()
778  notification,
779  summary,
780  body_sum,
781  True,
782  False,
783  1.0
784  )