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