@@ -0,0 +1,737 @@
1+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
3+# Unity Autopilot Test Suite
4+# Copyright (C) 2012-2013 Canonical
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.
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.
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/>.
20+"""Tests for Notifications"""
22+from __future__ import absolute_import
24+from unity8.shell.tests import UnityTestCase, _get_device_emulation_scenarios
26+from testtools.matchers import Equals, NotEquals
27+from autopilot.matchers import Eventually
29+from gi.repository import Notify
36+logger = logging.getLogger(__name__)
39+class NotificationsBase(UnityTestCase):
40+ """Base class for all notification tests that provides helper methods."""
42+ scenarios = _get_device_emulation_scenarios('Nexus4')
44+ def _get_icon_path(self, icon_name):
45+ """Given an icons file name returns the full path (either system or
48+ Consider the graphics directory as root so for example (runnign tests
49+ from installed unity8-autopilot package):
50+ >>> self.get_icon_path('clock.png')
51+ /usr/share/unity8/graphics/clock.png
53+ >>> self.get_icon_path('applicationIcons/facebook.png')
54+ /usr/share/unity8/graphics/applicationIcons/facebook.png
57+ if os.path.abspath(__file__).startswith('/usr/'):
58+ return '/usr/share/unity8/graphics/' + icon_name
60+ return os.path.abspath(
61+ os.getcwd() + "/../../graphics/" + icon_name
64+ def _get_notifications_list(self):
65+ main_view = self.main_window.get_qml_view()
66+ return main_view.select_single(
68+ objectName='notificationList'
71+ def _assert_notification(
77+ secondary_icon=False,
80+ """Assert that the expected qualities of a notification are as
85+ if summary is not None:
86+ self.assertThat(notification.summary, Eventually(Equals(summary)))
89+ self.assertThat(notification.body, Eventually(Equals(body)))
92+ self.assertThat(notification.iconSource, Eventually(NotEquals("")))
94+ self.assertThat(notification.iconSource, Eventually(Equals("")))
98+ notification.secondaryIconSource,
99+ Eventually(NotEquals(""))
103+ notification.secondaryIconSource,
104+ Eventually(Equals(""))
107+ if opacity is not None:
108+ self.assertThat(notification.opacity, Eventually(Equals(opacity)))
111+class InteractiveNotificationBase(NotificationsBase):
112+ """Collection of test for Interactive tests including snap decisions."""
115+ super(InteractiveNotificationBase, self).setUp()
116+ # Need to keep track when we launch the notification script.
117+ self._notify_proc = None
119+ def test_interactive(self):
120+ """Interactive notification must react upon click on itself."""
122+ greeter = self.main_window.get_greeter()
125+ notify_list = self._get_notifications_list()
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")]
132+ ("x-canonical-switch-to-application", "true"),
134+ "x-canonical-secondary-icon",
135+ self._get_icon_path('applicationIcons/phone-app.png')
139+ self._create_interactive_notification(
148+ get_notification = lambda: notify_list.select_single('Notification')
149+ self.assertThat(get_notification, Eventually(NotEquals(None)))
150+ notification = get_notification()
152+ self.touch.tap_object(
153+ notification.select_single(objectName="interactiveArea")
156+ self.assert_notification_action_id_was_called('action_id')
158+ def test_sd_incoming_call(self):
159+ """Rejecting a call should make notification expand and offer more options."""
161+ greeter = self.main_window.get_greeter()
164+ notify_list = self._get_notifications_list()
166+ summary = "Incoming call"
167+ body = "Frank Zappa\n+44 (0)7736 027340"
168+ icon_path = self._get_icon_path('avatars/anna_olsson.png')
171+ "x-canonical-secondary-icon",
172+ self._get_icon_path('applicationIcons/phone-app.png')
174+ ("x-canonical-snap-decisions", "true"),
178+ ('action_accept', 'Accept'),
179+ ('action_decline_1', 'Decline'),
180+ ('action_decline_2', '"Can\'t talk now, what\'s up?"'),
181+ ('action_decline_3', '"I call you back."'),
182+ ('action_decline_4', 'Send custom message...'),
185+ self._create_interactive_notification(
194+ get_notification = lambda: notify_list.select_single('Notification')
195+ self.assertThat(get_notification, Eventually(NotEquals(None)))
196+ notification = get_notification()
197+ self._assert_notification(notification, None, None, True, True, 1.0)
198+ initial_height = notification.height
199+ self.touch.tap_object(notification.select_single(objectName="button1"))
201+ notification.height,
202+ Eventually(Equals(initial_height +
203+ 3 * notification.select_single(objectName="buttonColumn").spacing +
204+ 3 * notification.select_single(objectName="button4").height)))
205+ self.touch.tap_object(notification.select_single(objectName="button4"))
206+ self.assert_notification_action_id_was_called("action_decline_4")
208+ def _create_interactive_notification(
217+ """Create a interactive notification command.
219+ :param summary: Summary text for the notification
220+ :param body: Body text to display in the notification
221+ :param icon: Path string to the icon to use
222+ :param urgency: Urgency string for the noticiation, either: 'LOW',
223+ 'NORMAL', 'CRITICAL'
224+ :param actions: List of tuples containing the 'id' and 'label' for all
226+ :param hint_strings: List of tuples containing the 'name' and value for
227+ setting the hint strings for the notification
232+ "Creating snap-decision notification with summary(%s), body(%s) "
240+ '--summary', summary,
242+ '--urgency', urgency
245+ if icon is not None:
246+ script_args.extend(['--icon', icon])
250+ script_args.extend(['--hint', "%s,%s" % (key, value)])
252+ for action in actions:
253+ action_id, action_label = action
254+ action_string = "%s,%s" % (action_id, action_label)
255+ script_args.extend(['--action', action_string])
257+ python_bin = subprocess.check_output(['which', 'python']).strip()
258+ command = [python_bin, self._get_notify_script()] + script_args
259+ logger.info("Launching snap-decision notification as: %s", command)
260+ self._notify_proc = subprocess.Popen(
262+ stdin=subprocess.PIPE,
263+ stdout=subprocess.PIPE,
264+ stderr=subprocess.PIPE,
268+ self.addCleanup(self._tidy_up_script_process)
270+ poll_result = self._notify_proc.poll()
271+ if poll_result is not None and self._notify_proc.returncode != 0:
272+ error_output = self._notify_proc.communicate()[1]
273+ raise RuntimeError("Call to script failed with: %s" % error_output)
275+ def _get_notify_script(self):
276+ """Returns the path to the interactive notification creation script."""
277+ file_path = "../../emulators/create_interactive_notification.py"
279+ the_path = os.path.abspath(
280+ os.path.join(__file__, file_path))
284+ def _tidy_up_script_process(self):
285+ if self._notify_proc is not None and self._notify_proc.poll() is None:
286+ logger.error("Notification process wasn't killed, killing now.")
287+ os.killpg(self._notify_proc.pid, signal.SIGTERM)
289+ def assert_notification_action_id_was_called(self, action_id, timeout=10):
290+ """Assert that the interactive notification callback of id *action_id*
293+ :raises AssertionError: If no interactive notification has actually
295+ :raises AssertionError: When *action_id* does not match the actual
297+ :raises AssertionError: If no callback was called at all.
300+ if self._notify_proc is None:
301+ raise AssertionError("No interactive notification was created.")
303+ for i in xrange(timeout):
304+ self._notify_proc.poll()
305+ if self._notify_proc.returncode is not None:
306+ output = self._notify_proc.communicate()
307+ actual_action_id = output[0].strip("\n")
308+ if actual_action_id != action_id:
309+ raise AssertionError(
310+ "action id '%s' does not match actual returned '%s'"
311+ % (action_id, actual_action_id)
317+ os.killpg(self._notify_proc.pid, signal.SIGTERM)
318+ self._notify_proc = None
319+ raise AssertionError(
320+ "No callback was called, killing interactivenotification script"
324+class EphemeralNotificationsTests(NotificationsBase):
325+ """Collection of tests for Emphemeral notifications (non-interactive.)"""
328+ super(EphemeralNotificationsTests, self).setUp()
329+ # Because we are using the Notify library we need to init and un-init
330+ # otherwise we get crashes.
331+ Notify.init("Autopilot Ephemeral Notification Tests")
332+ self.addCleanup(Notify.uninit)
334+ def test_icon_summary_body(self):
335+ """Notification must display the expected summary and body text."""
337+ greeter = self.main_window.get_greeter()
340+ notify_list = self._get_notifications_list()
342+ summary = "Icon-Summary-Body"
343+ body = "Hey pal, what's up with the party next weekend? Will you " \
345+ icon_path = self._get_icon_path('avatars/anna_olsson.png')
348+ "x-canonical-secondary-icon",
349+ self._get_icon_path('applicationIcons/phone-app.png')
353+ notification = self._create_ephemeral_notification(
363+ get_notification = lambda: notify_list.select_single('Notification')
364+ self.assertThat(get_notification, Eventually(NotEquals(None)))
365+ notification = get_notification()
366+ self._assert_notification(notification, summary, body, True, True, 1.0)
368+ def test_icon_summary(self):
369+ """Notification must display the expected summary and secondary icon."""
371+ greeter = self.main_window.get_greeter()
374+ notify_list = self._get_notifications_list()
376+ summary = "Upload of image completed"
379+ "x-canonical-secondary-icon",
380+ self._get_icon_path('applicationIcons/facebook.png')
384+ notification = self._create_ephemeral_notification(
394+ get_notification = lambda: notify_list.select_single('Notification')
395+ self.assertThat(get_notification, Eventually(NotEquals(None)))
396+ notification = get_notification()
397+ self._assert_notification(
406+ def test_urgency_order(self):
407+ """Notifications must be displayed in order according to their urgency."""
409+ greeter = self.main_window.get_greeter()
412+ notify_list = self._get_notifications_list()
414+ summary_low = 'Low Urgency'
415+ body_low = "No, I'd rather see paint dry, pal *yawn*"
416+ icon_path_low = self._get_icon_path('avatars/amanda.png')
418+ summary_normal = 'Normal Urgency'
419+ body_normal = "Hey pal, what's up with the party next weekend? Will " \
420+ "you join me and Anna?"
421+ icon_path_normal = self._get_icon_path('avatars/funky.png')
423+ summary_critical = 'Critical Urgency'
424+ body_critical = 'Dude, this is so urgent you have no idea :)'
425+ icon_path_critical = self._get_icon_path('avatars/anna_olsson.png')
427+ notification_normal = self._create_ephemeral_notification(
433+ notification_normal.show()
435+ notification_low = self._create_ephemeral_notification(
441+ notification_low.show()
443+ notification_critical = self._create_ephemeral_notification(
449+ notification_critical.show()
451+ get_notification = lambda: notify_list.select_single(
453+ summary=summary_critical
455+ self.assertThat(get_notification, Eventually(NotEquals(None)))
457+ notification = get_notification()
458+ self._assert_notification(
467+ get_normal_notification = lambda: notify_list.select_single(
469+ summary=summary_normal
471+ self.assertThat(get_normal_notification, Eventually(NotEquals(None)))
472+ notification = get_normal_notification()
473+ self._assert_notification(
482+ get_low_notification = lambda: notify_list.select_single(
486+ self.assertThat(get_low_notification, Eventually(NotEquals(None)))
487+ notification = get_low_notification()
488+ self._assert_notification(
497+ def test_summary_and_body(self):
498+ """Notification must display the expected summary- and body-text."""
500+ greeter = self.main_window.get_greeter()
503+ notify_list = self._get_notifications_list()
505+ summary = 'Summary-Body'
506+ body = 'This is a superfluous notification'
508+ notification = self._create_ephemeral_notification(summary, body)
511+ get_notification = lambda: notify_list.select_single('Notification')
512+ self.assertThat(get_notification, Eventually(NotEquals(None)))
513+ notification = get_notification()
514+ self._assert_notification(
523+ def test_summary_only(self):
524+ """Notification must display only the expected summary-text."""
526+ greeter = self.main_window.get_greeter()
529+ notify_list = self._get_notifications_list()
531+ summary = 'Summary-Only'
533+ notification = self._create_ephemeral_notification(summary)
536+ get_notification = lambda: notify_list.select_single('Notification')
537+ self.assertThat(get_notification, Eventually(NotEquals(None)))
538+ notification = get_notification()
540+ self._assert_notification(notification, summary, '', False, False, 1.0)
542+ def test_update_notification_same_layout(self):
543+ """Notification must allow updating its contents while being displayed."""
545+ greeter = self.main_window.get_greeter()
548+ notify_list = self._get_notifications_list()
550+ summary = 'Initial notification'
551+ body = 'This is the original content of this notification-bubble.'
552+ icon_path = self._get_icon_path('avatars/funky.png')
554+ notification = self._create_ephemeral_notification(
561+ get_notification = lambda: notify_list.select_single('Notification', summary=summary)
562+ self.assertThat(get_notification, Eventually(NotEquals(None)))
563+ self._assert_notification(
572+ summary = 'Updated notification'
573+ body = 'Here the same bubble with new title- and body-text, even ' \
574+ 'the icon can be changed on the update.'
575+ icon_path = self._get_icon_path('avatars/amanda.png')
576+ notification.update(summary, body, icon_path)
578+ self.assertThat(get_notification, Eventually(NotEquals(None)))
579+ self._assert_notification(get_notification(), summary, body)
581+ def test_update_notification_layout_change(self):
582+ """Notification must allow updating its contents and layout while being displayed."""
584+ greeter = self.main_window.get_greeter()
587+ notify_list = self._get_notifications_list()
589+ summary = 'Initial layout'
590+ body = 'This bubble uses the icon-title-body layout with a ' \
592+ icon_path = self._get_icon_path('avatars/anna_olsson.png')
593+ hint_icon = self._get_icon_path('applicationIcons/phone-app.png')
595+ notification = self._create_ephemeral_notification(
600+ notification.set_hint_string(
601+ 'x-canonical-secondary-icon',
606+ get_notification = lambda: notify_list.select_single('Notification')
607+ self.assertThat(get_notification, Eventually(NotEquals(None)))
608+ self._assert_notification(
617+ notification.clear_hints()
618+ summary = 'Updated layout'
619+ body = 'After the update we now have a bubble using the title-body ' \
621+ notification.update(summary, body, None)
624+ self.assertThat(get_notification, Eventually(NotEquals(None)))
625+ self._assert_notification(get_notification(), summary, body, False)
627+ def test_append_hint(self):
628+ """Notification has to accumulate body-text using append-hint."""
630+ greeter = self.main_window.get_greeter()
633+ notify_list = self._get_notifications_list()
635+ summary = 'Cole Raby'
636+ body = 'Hey Bro Coly!'
637+ icon_path = self._get_icon_path('avatars/amanda.png')
639+ notification = self._create_ephemeral_notification(
643+ hints=[('x-canonical-append', 'true')]
648+ get_notification = lambda: notify_list.select_single('Notification')
649+ self.assertThat(get_notification, Eventually(NotEquals(None)))
650+ notification = get_notification()
651+ self._assert_notification(
662+ 'Did you watch the air-race in Oshkosh last week?',
663+ 'Phil owned the place like no one before him!',
664+ 'Did really everything in the race work according to regulations?',
665+ 'Somehow I think to remember Burt Williams did cut corners and was not punished for this.',
666+ 'Hopefully the referees will watch the videos of the race.',
667+ 'Burt could get fined with US$ 50000 for that rule-violation :)'
670+ for new_body in bodies:
672+ body_sum += '\n' + body
673+ notification = self._create_ephemeral_notification(
677+ hints=[('x-canonical-append', 'true')]
680+ get_notification = lambda: notify_list.select_single(
683+ self.assertThat(get_notification, Eventually(NotEquals(None)))
684+ notification = get_notification()
685+ self._assert_notification(
694+ def _create_ephemeral_notification(
702+ """Create an ephemeral (non-interactive) notification
704+ :param summary: Summary text for the notification
705+ :param body: Body text to display in the notification
706+ :param icon: Path string to the icon to use
707+ :param hint_strings: List of tuples containing the 'name' and value
708+ for setting the hint strings for the notification
709+ :param urgency: Urgency string for the noticiation, either: 'LOW',
710+ 'NORMAL', 'CRITICAL'
714+ "Creating ephemeral: summary(%s), body(%s), urgency(%r) "
722+ n = Notify.Notification.new(summary, body, icon)
726+ n.set_hint_string(key, value)
727+ logger.info("Adding hint to notification: (%s, %s)", key, value)
728+ n.set_urgency(self._get_urgency(urgency))
732+ def _get_urgency(self, urgency):
733+ """Translates urgency string to enum."""
734+ _urgency_enums = {'LOW': Notify.Urgency.LOW,
735+ 'NORMAL': Notify.Urgency.NORMAL,
736+ 'CRITICAL': Notify.Urgency.CRITICAL}
737+ return _urgency_enums.get(urgency.upper())