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