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