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