Unity 8
 All Classes Functions
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  ("x-canonical-secondary-icon","dialer")
139  ]
140 
142  summary,
143  body,
144  icon_path,
145  "NORMAL",
146  actions,
147  hints,
148  )
149 
150  get_notification = lambda: notify_list.wait_select_single(
151  'Notification', objectName='notification1')
152  notification = get_notification()
153 
154  notification.pointing_device.click_object(
155  notification.select_single(objectName="interactiveArea")
156  )
157 
159 
161  """Rejecting a call should make notification expand and
162  offer more options."""
163  unity_proxy = self.launch_unity()
164  unlock_unity(unity_proxy)
165 
166  summary = "Incoming call"
167  body = "Frank Zappa\n+44 (0)7736 027340"
168  icon_path = self._get_icon_path('avatars/anna_olsson.png')
169  hints = [
170  ("x-canonical-secondary-icon", "incoming-call"),
171  ("x-canonical-snap-decisions", "true"),
172  ("x-canonical-private-affirmative-tint", "true"),
173  ("x-canonical-private-rejection-tint", "true"),
174  ]
175 
176  actions = [
177  ('action_accept', 'Hold + Answer'),
178  ('action_decline_1', 'End + Answer'),
179  ('action_decline_2', 'Decline'),
180  ('action_decline_3', 'message:I missed your call - can you call me now?'),
181  ('action_decline_4', 'message:I\'m running late. I\'m on my way.'),
182  ('action_decline_5', 'message:I\'m busy at the moment. I\'ll call later.'),
183  ('action_decline_6', 'edit:Custom'),
184  ]
185 
187  summary,
188  body,
189  icon_path,
190  "NORMAL",
191  actions,
192  hints
193  )
194 
195  notify_list = self._get_notifications_list()
196  get_notification = lambda: notify_list.wait_select_single(
197  'Notification', objectName='notification1')
198  notification = get_notification()
199  self._assert_notification(notification, summary, body, True, True, 1.0)
200  notification.pointing_device.click_object(
201  notification.select_single(objectName="combobutton_dropdown"))
202  self.assertThat(
203  notification.select_single(objectName="notify_button2").expanded,
204  Eventually(Equals(True)))
205  time.sleep(2)
206  notification.pointing_device.click_object(
207  notification.select_single(objectName="notify_button4"))
208  self.assert_notification_action_id_was_called("action_decline_4")
209 
210 
212  """Snap-decision with three actions should use one-over two button layout."""
213  unity_proxy = self.launch_unity()
214  unlock_unity(unity_proxy)
215 
216  summary = "Theatre at Ferria Stadium"
217  body = "at Ferria Stadium in Bilbao, Spain\n07578545317"
218  hints = [
219  ("x-canonical-snap-decisions", "true"),
220  ("x-canonical-non-shaped-icon", "true"),
221  ("x-canonical-private-affirmative-tint", "true")
222  ]
223 
224  actions = [
225  ('action_accept', 'Ok'),
226  ('action_decline_1', 'Snooze'),
227  ('action_decline_2', 'View'),
228  ]
229 
231  summary,
232  body,
233  None,
234  "NORMAL",
235  actions,
236  hints
237  )
238 
239  # verify and interact with the triggered snap-decision notification
240  notify_list = self._get_notifications_list()
241  get_notification = lambda: notify_list.wait_select_single(
242  'Notification', objectName='notification1')
243  notification = get_notification()
245  notification, summary, body, False, False, 1.0)
246  notification.pointing_device.click_object(
247  notification.select_single(objectName="notify_oot_button0"))
248  self.assert_notification_action_id_was_called("action_accept")
249 
251  """Snap-decision should block input to shell without greeter."""
252  unity_proxy = self.launch_unity()
253  unlock_unity(unity_proxy)
254 
255  summary = "Incoming file"
256  body = "Frank would like to send you the file: essay.pdf"
257  icon_path = "sync-idle"
258  hints = [
259  ("x-canonical-snap-decisions", "true"),
260  ("x-canonical-non-shaped-icon", "true"),
261  ("x-canonical-private-affirmative-tint", "true"),
262  ("x-canonical-private-rejection-tint", "true"),
263  ]
264 
265  actions = [
266  ('action_accept', 'Accept'),
267  ('action_decline_1', 'Decline'),
268  ]
269 
271  summary,
272  body,
273  icon_path,
274  "NORMAL",
275  actions,
276  hints
277  )
278 
279  # verify that we cannot reveal the launcher (no longer interact with
280  # the shell)
281  time.sleep(1)
282  self.main_window.show_dash_swiping()
283  self.assertThat(
284  self.main_window.is_launcher_open, Eventually(Equals(False)))
285 
286  # verify and interact with the triggered snap-decision notification
287  notify_list = self._get_notifications_list()
288  get_notification = lambda: notify_list.wait_select_single(
289  'Notification', objectName='notification1')
290  notification = get_notification()
292  notification, summary, body, True, False, 1.0)
293  notification.pointing_device.click_object(
294  notification.select_single(objectName="notify_button0"))
295  self.assert_notification_action_id_was_called("action_accept")
296 
298  """A snap-decision should not block input to the greeter beneath it."""
299  self.launch_unity()
300 
301  summary = "Incoming file"
302  body = "Frank would like to send you the file: essay.pdf"
303  icon_path = "sync-idle"
304  hints = [
305  ("x-canonical-snap-decisions", "true"),
306  ("x-canonical-non-shaped-icon", "true"),
307  ("x-canonical-private-affirmative-tint", "true"),
308  ("x-canonical-private-rejection-tint", "true"),
309  ]
310 
311  actions = [
312  ('action_accept', 'Accept'),
313  ('action_decline_1', 'Decline'),
314  ]
315 
317  summary,
318  body,
319  icon_path,
320  "NORMAL",
321  actions,
322  hints
323  )
324 
325  # verify that we can swipe away the greeter (interact with the "shell")
326  time.sleep(1)
327  self.main_window.show_dash_swiping()
328  greeter = self.main_window.get_greeter()
329  self.assertThat(greeter.shown, Eventually(Equals(False)))
330 
331  # verify and interact with the triggered snap-decision notification
332  notify_list = self._get_notifications_list()
333  get_notification = lambda: notify_list.wait_select_single(
334  'Notification', objectName='notification1')
335  notification = get_notification()
337  notification, summary, body, True, False, 1.0)
338  notification.pointing_device.click_object(
339  notification.select_single(objectName="notify_button0"))
340  self.assert_notification_action_id_was_called("action_accept")
341 
342  def _create_interactive_notification(
343  self,
344  summary="",
345  body="",
346  icon=None,
347  urgency="NORMAL",
348  actions=[],
349  hints=[]
350  ):
351  """Create a interactive notification command.
352 
353  :param summary: Summary text for the notification
354  :param body: Body text to display in the notification
355  :param icon: Path string to the icon to use
356  :param urgency: Urgency string for the noticiation, either: 'LOW',
357  'NORMAL', 'CRITICAL'
358  :param actions: List of tuples containing the 'id' and 'label' for all
359  the actions to add
360  :param hint_strings: List of tuples containing the 'name' and value for
361  setting the hint strings for the notification
362 
363  """
364 
365  logger.info(
366  "Creating snap-decision notification with summary(%s), body(%s) "
367  "and urgency(%r)",
368  summary,
369  body,
370  urgency
371  )
372 
373  script_args = [
374  '--summary', summary,
375  '--body', body,
376  '--urgency', urgency
377  ]
378 
379  if icon is not None:
380  script_args.extend(['--icon', icon])
381 
382  for hint in hints:
383  key, value = hint
384  script_args.extend(['--hint', "%s,%s" % (key, value)])
385 
386  for action in actions:
387  action_id, action_label = action
388  action_string = "%s,%s" % (action_id, action_label)
389  script_args.extend(['--action', action_string])
390 
391  python_bin = subprocess.check_output(['which', 'python']).strip()
392  command = [python_bin, self._get_notify_script()] + script_args
393  logger.info("Launching snap-decision notification as: %s", command)
394  self._notify_proc = subprocess.Popen(
395  command,
396  stdin=subprocess.PIPE,
397  stdout=subprocess.PIPE,
398  stderr=subprocess.PIPE,
399  close_fds=True,
400  universal_newlines=True,
401  )
402 
403  self.addCleanup(self._tidy_up_script_process)
404 
405  poll_result = self._notify_proc.poll()
406  if poll_result is not None and self._notify_proc.returncode != 0:
407  error_output = self._notify_proc.communicate()[1]
408  raise RuntimeError("Call to script failed with: %s" % error_output)
409 
410  def _get_notify_script(self):
411  """Returns the path to the interactive notification creation script."""
412  file_path = "../../emulators/create_interactive_notification.py"
413 
414  the_path = os.path.abspath(
415  os.path.join(__file__, file_path))
416 
417  return the_path
418 
419  def _tidy_up_script_process(self):
420  if self._notify_proc is not None and self._notify_proc.poll() is None:
421  logger.error("Notification process wasn't killed, killing now.")
422  os.killpg(self._notify_proc.pid, signal.SIGTERM)
423 
424  def assert_notification_action_id_was_called(self, action_id, timeout=10):
425  """Assert that the interactive notification callback of id *action_id*
426  was called.
427 
428  :raises AssertionError: If no interactive notification has actually
429  been created.
430  :raises AssertionError: When *action_id* does not match the actual
431  returned.
432  :raises AssertionError: If no callback was called at all.
433  """
434 
435  if self._notify_proc is None:
436  raise AssertionError("No interactive notification was created.")
437 
438  for i in range(timeout):
439  self._notify_proc.poll()
440  if self._notify_proc.returncode is not None:
441  output = self._notify_proc.communicate()
442  actual_action_id = output[0].strip("\n")
443  if actual_action_id != action_id:
444  raise AssertionError(
445  "action id '%s' does not match actual returned '%s'"
446  % (action_id, actual_action_id)
447  )
448  else:
449  return
450  time.sleep(1)
451 
452  os.killpg(self._notify_proc.pid, signal.SIGTERM)
453  self._notify_proc = None
454  raise AssertionError(
455  "No callback was called, killing interactivenotification script"
456  )
457 
458 
460  """Collection of tests for Emphemeral notifications (non-interactive.)"""
461 
462  def setUp(self):
463  super(EphemeralNotificationsTests, self).setUp()
464  # Because we are using the Notify library we need to init and un-init
465  # otherwise we get crashes.
466  Notify.init("Autopilot Ephemeral Notification Tests")
467  self.addCleanup(Notify.uninit)
468 
470  """Notification must display the expected summary and body text."""
471  unity_proxy = self.launch_unity()
472  unlock_unity(unity_proxy)
473 
474  notify_list = self._get_notifications_list()
475 
476  summary = "Icon-Summary-Body"
477  body = "Hey pal, what's up with the party next weekend? Will you " \
478  "join me and Anna?"
479  icon_path = self._get_icon_path('avatars/anna_olsson.png')
480  hints = [
481  ("x-canonical-secondary-icon", "message")
482  ]
483 
484  notification = shell.create_ephemeral_notification(
485  summary,
486  body,
487  icon_path,
488  hints,
489  "NORMAL",
490  )
491 
492  notification.show()
493 
494  notification = lambda: notify_list.wait_select_single(
495  'Notification', objectName='notification1')
497  notification(),
498  summary,
499  body,
500  True,
501  True,
502  1.0,
503  )
504 
505  def test_icon_summary(self):
506  """Notification must display the expected summary and secondary
507  icon."""
508  unity_proxy = self.launch_unity()
509  unlock_unity(unity_proxy)
510 
511  notify_list = self._get_notifications_list()
512 
513  summary = "Upload of image completed"
514  icon_path = self._get_icon_path('applicationIcons/facebook.png')
515  hints=[]
516 
517  notification = shell.create_ephemeral_notification(
518  summary,
519  None,
520  icon_path,
521  hints,
522  "NORMAL",
523  )
524 
525  notification.show()
526 
527  notification = lambda: notify_list.wait_select_single(
528  'Notification', objectName='notification1')
530  notification(),
531  summary,
532  None,
533  True,
534  False,
535  1.0
536  )
537 
539  """Notifications must be displayed in order according to their
540  urgency."""
541  unity_proxy = self.launch_unity()
542  unlock_unity(unity_proxy)
543 
544  notify_list = self._get_notifications_list()
545 
546  summary_low = 'Low Urgency'
547  body_low = "No, I'd rather see paint dry, pal *yawn*"
548  icon_path_low = self._get_icon_path('avatars/amanda.png')
549 
550  summary_normal = 'Normal Urgency'
551  body_normal = "Hey pal, what's up with the party next weekend? Will " \
552  "you join me and Anna?"
553  icon_path_normal = self._get_icon_path('avatars/funky.png')
554 
555  summary_critical = 'Critical Urgency'
556  body_critical = 'Dude, this is so urgent you have no idea :)'
557  icon_path_critical = self._get_icon_path('avatars/anna_olsson.png')
558 
559  notification_normal = shell.create_ephemeral_notification(
560  summary_normal,
561  body_normal,
562  icon_path_normal,
563  urgency="NORMAL"
564  )
565  notification_normal.show()
566 
567  notification_low = shell.create_ephemeral_notification(
568  summary_low,
569  body_low,
570  icon_path_low,
571  urgency="LOW"
572  )
573  notification_low.show()
574 
575  notification_critical = shell.create_ephemeral_notification(
576  summary_critical,
577  body_critical,
578  icon_path_critical,
579  urgency="CRITICAL"
580  )
581  notification_critical.show()
582 
583  get_notification = lambda: notify_list.wait_select_single(
584  'Notification',
585  summary=summary_critical
586  )
587 
588  notification = get_notification()
590  notification,
591  summary_critical,
592  body_critical,
593  True,
594  False,
595  1.0
596  )
597 
598  get_normal_notification = lambda: notify_list.wait_select_single(
599  'Notification',
600  summary=summary_normal
601  )
602  notification = get_normal_notification()
604  notification,
605  summary_normal,
606  body_normal,
607  True,
608  False,
609  1.0
610  )
611 
612  get_low_notification = lambda: notify_list.wait_select_single(
613  'Notification',
614  summary=summary_low
615  )
616  notification = get_low_notification()
618  notification,
619  summary_low,
620  body_low,
621  True,
622  False,
623  1.0
624  )
625 
627  """Notification must display the expected summary- and body-text."""
628  unity_proxy = self.launch_unity()
629  unlock_unity(unity_proxy)
630 
631  notify_list = self._get_notifications_list()
632 
633  summary = 'Summary-Body'
634  body = 'This is a superfluous notification'
635 
636  notification = shell.create_ephemeral_notification(summary, body)
637  notification.show()
638 
639  notification = notify_list.wait_select_single(
640  'Notification', objectName='notification1')
641 
643  notification,
644  summary,
645  body,
646  False,
647  False,
648  1.0
649  )
650 
651  def test_summary_only(self):
652  """Notification must display only the expected summary-text."""
653  unity_proxy = self.launch_unity()
654  unlock_unity(unity_proxy)
655 
656  notify_list = self._get_notifications_list()
657 
658  summary = 'Summary-Only'
659 
660  notification = shell.create_ephemeral_notification(summary)
661  notification.show()
662 
663  notification = notify_list.wait_select_single(
664  'Notification', objectName='notification1')
665 
666  self._assert_notification(notification, summary, '', False, False, 1.0)
667 
669  """Notification must allow updating its contents while being
670  displayed."""
671  unity_proxy = self.launch_unity()
672  unlock_unity(unity_proxy)
673 
674  notify_list = self._get_notifications_list()
675 
676  summary = 'Initial notification'
677  body = 'This is the original content of this notification-bubble.'
678  icon_path = self._get_icon_path('avatars/funky.png')
679 
680  notification = shell.create_ephemeral_notification(
681  summary,
682  body,
683  icon_path
684  )
685  notification.show()
686 
687  get_notification = lambda: notify_list.wait_select_single(
688  'Notification', summary=summary)
690  get_notification(),
691  summary,
692  body,
693  True,
694  False,
695  1.0
696  )
697 
698  summary = 'Updated notification'
699  body = 'Here the same bubble with new title- and body-text, even ' \
700  'the icon can be changed on the update.'
701  icon_path = self._get_icon_path('avatars/amanda.png')
702  notification.update(summary, body, icon_path)
703  notification.show()
705  get_notification(), summary, body, True, False, 1.0)
706 
708  """Notification must allow updating its contents and layout while
709  being displayed."""
710  unity_proxy = self.launch_unity()
711  unlock_unity(unity_proxy)
712 
713  notify_list = self._get_notifications_list()
714 
715  summary = 'Initial layout'
716  body = 'This bubble uses the icon-title-body layout with a ' \
717  'secondary icon.'
718  icon_path = self._get_icon_path('avatars/anna_olsson.png')
719  hint_icon = 'dialer'
720 
721  notification = shell.create_ephemeral_notification(
722  summary,
723  body,
724  icon_path
725  )
726  notification.set_hint_string(
727  'x-canonical-secondary-icon',
728  hint_icon
729  )
730  notification.show()
731 
732  get_notification = lambda: notify_list.wait_select_single(
733  'Notification', objectName='notification1')
734 
736  get_notification(),
737  summary,
738  body,
739  True,
740  True,
741  1.0
742  )
743 
744  notification.clear_hints()
745  summary = 'Updated layout'
746  body = 'After the update we now have a bubble using the title-body ' \
747  'layout.'
748  notification.update(summary, body, None)
749  notification.show()
750 
751  self.assertThat(get_notification, Eventually(NotEquals(None)))
753  get_notification(), summary, body, False, False, 1.0)