@@ -1,42 +1,34 @@
11# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2−# Copyright 2013 Canonical
32#
4−# This program is free software: you can redistribute it and/or modify it
5−# under the terms of the GNU General Public License version 3, as published
6−# by the Free Software Foundation.
7−
8−
9−# This file contains general purpose test cases for Unity.
10−# Each test written in this file will be executed for a variety of
11−# configurations, such as Phone, Tablet or Desktop form factors.
3+# Unity Autopilot Test Suite
4+# Copyright (C) 2012-2013 Canonical
125#
13−# Sometimes there is the need to disable a certain test for a particular
14−# configuration. To do so, add this in a new line directly below your test:
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.
1510#
16−# test_testname.blacklist = (FormFactors.Tablet, FormFactors.Desktop,)
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/>.
1718#
18−# Available form factors are:
19−# FormFactors.Phone
20−# FormFactors.Tablet
21−# FormFactors.Desktop
2219
23−"""Tests for the Shell"""
20+"""Tests for Notifications"""
2421
2522from __future__ import absolute_import
2623
27−from unity8.shell import with_lightdm_mock
28−from unity8.shell.emulators.notification_helper import Notifications
2924from unity8.shell.tests import UnityTestCase, _get_device_emulation_scenarios
3025
31−from testtools.matchers import Equals, NotEquals, GreaterThan, MismatchError
32−from testtools import skip
26+from testtools.matchers import Equals, NotEquals
3327from autopilot.matchers import Eventually
34−from autopilot.platform import model
3528
36−from gi.repository import GLib, Notify
29+from gi.repository import Notify
3730import time
3831import os
39−from os import path
4032import logging
4133import signal
4234import subprocess
@@ -44,73 +36,83 @@ import subprocess
4436logger = logging.getLogger(__name__)
4537
4638
47−class TestNotifications(UnityTestCase):
39+class NotificationsBase(UnityTestCase):
40+ """Base class for all notification tests that provides helper methods."""
4841
4942 scenarios = _get_device_emulation_scenarios()
50− action_interactive_triggered = False
51− action_accept_triggered = False
52− action_send_message_triggered = False
53− loop = None
5443
55− def setUp(self):
56− p = subprocess.Popen(['dbus-launch'], stdout=subprocess.PIPE)
57− output = p.communicate()
58− results = output[0].split("\n")
59− self.dbus_pid = int(results[1].split("=")[1])
60− self.dbus_address = results[0].split("=", 1)[1]
44+ def _get_icon_path(self, icon_name):
45+ """Given an icons file name returns the full path (either system or
46+ source tree.
6147
62− logger.info("Using DBUS_SESSION_BUS_ADDRESS: %s", self.dbus_address)
63− logger.info("Using DBUS PID: %d", self.dbus_pid)
48+ Consider the graphics directory as root so for example (runnign tests
49+ from installed unity8-autopilot package):
50+ >>> self.get_icon_path('clock@18.png')
51+ /usr/share/unity8/graphics/clock@18.png
6452
65− self.patch_environment("DBUS_SESSION_BUS_ADDRESS", self.dbus_address)
53+ >>> self.get_icon_path('applicationIcons/facebook@18.png')
54+ /usr/share/unity8/graphics/applicationIcons/facebook@18.png
6655
67− kill_dbus = lambda pid: os.killpg(pid, signal.SIGTERM)
68− self.addCleanup(kill_dbus, self.dbus_pid)
56+ """
57+ if os.path.abspath(__file__).startswith('/usr/'):
58+ return '/usr/share/unity8/graphics/' + icon_name
59+ else:
60+ return os.path.abspath(
61+ os.getcwd() + "/../../graphics/" + icon_name
62+ )
6963
70− super(TestNotifications, self).setUp()
71− self._notify_proc = None
64+ def _get_notifications_list(self):
65+ main_view = self.main_window.get_qml_view()
66+ return main_view.select_single(
67+ "QQuickListView",
68+ objectName='notificationList'
69+ )
7270
73− def tearDown(self):
74− super(TestNotifications, self).tearDown()
75− if self._notify_proc is not None and self._notify_proc.poll() is None:
76− logger.error("Notification process wasn't killed.")
77− os.killpg(self._notify_proc.pid, signal.SIGTERM)
71+ def _assert_notification(
72+ self,
73+ notification,
74+ summary=None,
75+ body=None,
76+ icon=True,
77+ secondary_icon=False,
78+ opacity=None
79+ ):
80+ """Assert that the expected qualities of a notification are as
81+ expected.
7882
79− @with_lightdm_mock("single")
80− def test_icon_summary_body(self):
81− """Notification must display the expected summary and body text."""
82− self.launch_unity(dbus_bus=self.dbus_address)
83− greeter = self.main_window.get_greeter()
84− greeter.unlock()
83+ """
8584
86− notify_list = self._get_notifications_list()
85+ if summary is not None:
86+ self.assertThat(notification.summary, Equals(summary))
8787
88− summary = "Icon-Summary-Body"
89− body = "Hey pal, what's up with the party next weekend? Will you join me and Anna?"
90− icon_path = self._get_icon_path('avatars/anna_olsson@12.png')
91− hints = [
92− (
93− "x-canonical-secondary-icon",
94− self._get_icon_path('applicationIcons/phone-app@18.png')
95− )
96− ]
88+ if body is not None:
89+ self.assertThat(notification.body, Equals(body))
9790
98− self._create_notification(
99− summary,
100− body,
101− icon_path,
102− "NORMAL",
103− hints
104− )
91+ if icon:
92+ self.assertThat(notification.iconSource, NotEquals(""))
93+ else:
94+ self.assertThat(notification.iconSource, Equals(""))
10595
106− get_notification = lambda: notify_list.select_single('Notification')
107− self.assertThat(get_notification, Eventually(NotEquals(None)))
108− notification = get_notification()
109− self._assert_notification(notification, summary, body, True, True, 1.0)
96+ if secondary_icon:
97+ self.assertThat(notification.secondaryIconSource, NotEquals(""))
98+ else:
99+ self.assertThat(notification.secondaryIconSource, Equals(""))
100+
101+ if opacity is not None:
102+ self.assertThat(notification.opacity, Eventually(Equals(opacity)))
103+
104+
105+class InteractiveNotificationBase(NotificationsBase):
106+ """Collection of test for Interactive tests including snap decisions."""
107+
108+ def setUp(self):
109+ super(InteractiveNotificationBase, self).setUp()
110+ # Need to keep track when we launch the notification script.
111+ self._notify_proc = None
110112
111− @with_lightdm_mock("single")
112113 def test_interactive(self):
113− self.launch_unity(dbus_bus=self.dbus_address)
114+ """Interactive notification must react upon click on itself."""
115+ self.launch_unity()
114116 greeter = self.main_window.get_greeter()
115117 greeter.unlock()
116118
@@ -119,6 +121,7 @@ class TestNotifications(UnityTestCase):
119121 summary = "Summary"
120122 body = "Body"
121123 icon_path = self._get_icon_path('avatars/anna_olsson@12.png')
124+ actions = [("action_id", "dummy")]
122125 hints = [
123126 ("x-canonical-switch-to-application", "true"),
124127 (
@@ -132,9 +135,8 @@ class TestNotifications(UnityTestCase):
132135 body,
133136 icon_path,
134137 "NORMAL",
135− "action_id",
136− "dummy",
137− hints
138+ actions,
139+ hints,
138140 )
139141
140142 get_notification = lambda: notify_list.select_single('Notification')
@@ -147,7 +149,6 @@ class TestNotifications(UnityTestCase):
147149
148150 self.assert_notification_action_id_was_called('action_id')
149151
150− @with_lightdm_mock("single")
151152 def test_sd_incoming_call(self):
152153 """Snap-decision simulating incoming call."""
153154 self.launch_unity()
@@ -163,7 +164,8 @@ class TestNotifications(UnityTestCase):
163164 (
164165 "x-canonical-secondary-icon",
165166 self._get_icon_path('applicationIcons/phone-app@18.png')
166− )
167+ ),
168+ ("x-canonical-snap-decisions", "true"),
167169 ]
168170
169171 actions = [
@@ -174,7 +176,7 @@ class TestNotifications(UnityTestCase):
174176 ('action_decline_4', 'Send custom message...'),
175177 ]
176178
177− self._create_sd_notification(
179+ self._create_interactive_notification(
178180 summary,
179181 body,
180182 icon_path,
@@ -188,14 +190,178 @@ class TestNotifications(UnityTestCase):
188190 notification = get_notification()
189191 #self._assert_notification(notification, None, None, True, True, 1.0)
190192 self.touch.tap_object(notification.select_single(objectName="button1"))
191− #self.assertThat(notification.select_single(objectName="buttonRow").expanded, Eventually(Equals(True)))
193+ # Veebers: this needs a better check as it's happening to quick.
194+ self.assertThat(
195+ notification.select_single(objectName="buttonRow").expanded,
196+ Eventually(Equals(True))
197+ )
198+ time.sleep(2)
192199 self.touch.tap_object(notification.select_single(objectName="button4"))
193− #self.assertThat(self.action_send_message_triggered, Equals(True))
194200 self.assert_notification_action_id_was_called("action_decline_4")
195201
196− @with_lightdm_mock("single")
202+ def _create_interactive_notification(
203+ self,
204+ summary="",
205+ body="",
206+ icon=None,
207+ urgency="NORMAL",
208+ actions=[],
209+ hints=[]
210+ ):
211+ """Create a interactive notification command.
212+
213+ :param summary: Summary text for the notification
214+ :param body: Body text to display in the notification
215+ :param icon: Path string to the icon to use
216+ :param urgency: Urgency string for the noticiation, either: 'LOW',
217+ 'NORMAL', 'CRITICAL'
218+ :param actions: List of tuples containing the 'id' and 'label' for all
219+ the actions to add
220+ :param hint_strings: List of tuples containing the 'name' and value for
221+ setting the hint strings for the notification
222+
223+ """
224+
225+ logger.info(
226+ "Creating snap-decision notification with summary(%s), body(%s) "
227+ "and urgency(%r)",
228+ summary,
229+ body,
230+ urgency
231+ )
232+
233+ script_args = [
234+ '--summary', summary,
235+ '--body', body,
236+ '--urgency', urgency
237+ ]
238+
239+ if icon is not None:
240+ script_args.extend(['--icon', icon])
241+
242+ for hint in hints:
243+ key, value = hint
244+ script_args.extend(['--hint', "%s,%s" % (key, value)])
245+
246+ for action in actions:
247+ action_id, action_label = action
248+ action_string = "%s,%s" % (action_id, action_label)
249+ script_args.extend(['--action', action_string])
250+
251+ command = [self._get_notify_script()] + script_args
252+ logger.info("Launching snap-decision notification as: %s", command)
253+ self._notify_proc = subprocess.Popen(
254+ command,
255+ stdin=subprocess.PIPE,
256+ stdout=subprocess.PIPE,
257+ stderr=subprocess.PIPE,
258+ close_fds=True,
259+ preexec_fn=os.setsid
260+ )
261+
262+ self.addCleanup(self._tidy_up_script_process)
263+
264+ poll_result = self._notify_proc.poll()
265+ if poll_result is not None and self._notify_proc.returncode != 0:
266+ error_output = self._notify_proc.communicate()[1]
267+ raise RuntimeError("Call to script failed with: %s" % error_output)
268+
269+ def _get_notify_script(self):
270+ """Returns the path to the interactive notification creation script."""
271+ file_path = "../../emulators/create_interactive_notification.py"
272+
273+ the_path = os.path.abspath(
274+ os.path.join(__file__, file_path))
275+
276+ return the_path
277+
278+ def _tidy_up_script_process(self):
279+ if self._notify_proc is not None and self._notify_proc.poll() is None:
280+ logger.error("Notification process wasn't killed, killing now.")
281+ os.killpg(self._notify_proc.pid, signal.SIGTERM)
282+
283+ def assert_notification_action_id_was_called(self, action_id, timeout=10):
284+ """Assert that the interactive notification callback of id *action_id*
285+ was called.
286+
287+ :raises AssertionError: If no interactive notification has actually
288+ been created.
289+ :raises AssertionError: When *action_id* does not match the actual
290+ returned.
291+ :raises AssertionError: If no callback was called at all.
292+ """
293+
294+ if self._notify_proc is None:
295+ raise AssertionError("No interactive notification was created.")
296+
297+ for i in xrange(timeout):
298+ self._notify_proc.poll()
299+ if self._notify_proc.returncode is not None:
300+ output = self._notify_proc.communicate()
301+ actual_action_id = output[0].strip("\n")
302+ if actual_action_id != action_id:
303+ raise AssertionError(
304+ "action id '%s' does not match actual returned '%s'"
305+ % (action_id, actual_action_id)
306+ )
307+ else:
308+ return
309+ time.sleep(1)
310+
311+ os.killpg(self._notify_proc.pid, signal.SIGTERM)
312+ self._notify_proc = None
313+ raise AssertionError(
314+ "No callback was called, killing interactivenotification script"
315+ )
316+
317+
318+class EphemeralNotificationsTests(NotificationsBase):
319+ """Collection of tests for Emphemeral notifications (non-interactive.)"""
320+
321+ def setUp(self):
322+ super(EphemeralNotificationsTests, self).setUp()
323+ # Because we are using the Notify library we need to init and un-init
324+ # otherwise we get crashes.
325+ Notify.init("Autopilot Ephemeral Notification Tests")
326+ self.addCleanup(Notify.uninit)
327+
328+ def test_icon_summary_body(self):
329+ """Notification must display the expected summary and body text."""
330+ self.launch_unity()
331+ greeter = self.main_window.get_greeter()
332+ greeter.unlock()
333+
334+ notify_list = self._get_notifications_list()
335+
336+ summary = "Icon-Summary-Body"
337+ body = "Hey pal, what's up with the party next weekend? Will you " \
338+ "join me and Anna?"
339+ icon_path = self._get_icon_path('avatars/anna_olsson@12.png')
340+ hints = [
341+ (
342+ "x-canonical-secondary-icon",
343+ self._get_icon_path('applicationIcons/phone-app@18.png')
344+ )
345+ ]
346+
347+ notification = self._create_ephemeral_notification(
348+ summary,
349+ body,
350+ icon_path,
351+ hints,
352+ "NORMAL",
353+ )
354+
355+ notification.show()
356+
357+ get_notification = lambda: notify_list.select_single('Notification')
358+ self.assertThat(get_notification, Eventually(NotEquals(None)))
359+ notification = get_notification()
360+ self._assert_notification(notification, summary, body, True, True, 1.0)
361+
197362 def test_icon_summary(self):
198− self.launch_unity(dbus_bus=self.dbus_address)
363+ """Notification must display the expected summary and secondary icon."""
364+ self.launch_unity()
199365 greeter = self.main_window.get_greeter()
200366 greeter.unlock()
201367
@@ -209,482 +375,353 @@ class TestNotifications(UnityTestCase):
209375 )
210376 ]
211377
212− self._create_notification(
378+ notification = self._create_ephemeral_notification(
213379 summary,
214380 None,
215381 None,
382+ hints,
216383 "NORMAL",
217− hints
218384 )
219385
386+ notification.show()
387+
220388 get_notification = lambda: notify_list.select_single('Notification')
221389 self.assertThat(get_notification, Eventually(NotEquals(None)))
222390 notification = get_notification()
223− self._assert_notification(notification, summary, None, False, True, 1.0)
391+ self._assert_notification(
392+ notification,
393+ summary,
394+ None,
395+ False,
396+ True,
397+ 1.0
398+ )
224399
225− @with_lightdm_mock("single")
226400 def test_urgency_order(self):
401+ """Notifications must be displayed in order according to their urgency."""
227402 self.launch_unity()
228403 greeter = self.main_window.get_greeter()
229404 greeter.unlock()
230− Notify.init("Autopilot Notification Tests")
405+
231406 notify_list = self._get_notifications_list()
232407
233408 summary_low = 'Low Urgency'
234409 body_low = "No, I'd rather see paint dry, pal *yawn*"
235410 icon_path_low = self._get_icon_path('avatars/amanda@12.png')
411+
236412 summary_normal = 'Normal Urgency'
237− body_normal = "Hey pal, what's up with the party next weekend? Will you join me and Anna?"
413+ body_normal = "Hey pal, what's up with the party next weekend? Will " \
414+ "you join me and Anna?"
238415 icon_path_normal = self._get_icon_path('avatars/funky@12.png')
416+
239417 summary_critical = 'Critical Urgency'
240418 body_critical = 'Dude, this is so urgent you have no idea :)'
241419 icon_path_critical = self._get_icon_path('avatars/anna_olsson@12.png')
242420
243− notification_normal = self._create_ephemeral(summary_normal, body_normal, icon_path_normal, None, "NORMAL")
421+ notification_normal = self._create_ephemeral_notification(
422+ summary_normal,
423+ body_normal,
424+ icon_path_normal,
425+ urgency="NORMAL"
426+ )
244427 notification_normal.show()
245− notification_low = self._create_ephemeral(summary_low, body_low, icon_path_low, None, "LOW")
428+
429+ notification_low = self._create_ephemeral_notification(
430+ summary_low,
431+ body_low,
432+ icon_path_low,
433+ urgency="LOW"
434+ )
246435 notification_low.show()
247− notification_critical = self._create_ephemeral(summary_critical, body_critical, icon_path_critical, None, "CRITICAL")
436+
437+ notification_critical = self._create_ephemeral_notification(
438+ summary_critical,
439+ body_critical,
440+ icon_path_critical,
441+ urgency="CRITICAL"
442+ )
248443 notification_critical.show()
249444
250− time.sleep(4)
251− get_notification = lambda: notify_list.select_single('Notification')
445+ get_notification = lambda: notify_list.select_single(
446+ 'Notification',
447+ summary=summary_critical
448+ )
252449 self.assertThat(get_notification, Eventually(NotEquals(None)))
253− notification = get_notification()
254− self._assert_notification(notification, summary_critical, body_critical, True, False, 1.0)
255450
256− time.sleep(4)
257451 notification = get_notification()
258− self._assert_notification(notification, summary_normal, body_normal, True, False, 1.0)
452+ self._assert_notification(
453+ notification,
454+ summary_critical,
455+ body_critical,
456+ True,
457+ False,
458+ 1.0
459+ )
259460
260− time.sleep(4)
261− notification = get_notification()
262− self._assert_notification(notification, summary_low, body_low, True, False, 1.0)
263− Notify.uninit()
461+ get_normal_notification = lambda: notify_list.select_single(
462+ 'Notification',
463+ summary=summary_normal
464+ )
465+ self.assertThat(get_normal_notification, Eventually(NotEquals(None)))
466+ notification = get_normal_notification()
467+ self._assert_notification(
468+ notification,
469+ summary_normal,
470+ body_normal,
471+ True,
472+ False,
473+ 1.0
474+ )
264475
265− @with_lightdm_mock("single")
266− def test_summary_body(self):
476+ get_low_notification = lambda: notify_list.select_single(
477+ 'Notification',
478+ summary=summary_low
479+ )
480+ self.assertThat(get_low_notification, Eventually(NotEquals(None)))
481+ notification = get_low_notification()
482+ self._assert_notification(
483+ notification,
484+ summary_low,
485+ body_low,
486+ True,
487+ False,
488+ 1.0
489+ )
490+
491+ def test_summary_and_body(self):
492+ """Notification must display the expected summary- and body-text."""
267493 self.launch_unity()
268494 greeter = self.main_window.get_greeter()
269495 greeter.unlock()
270− Notify.init("Autopilot Notification Tests")
496+
271497 notify_list = self._get_notifications_list()
272498
273499 summary = 'Summary-Body'
274500 body = 'This is a superfluous notification'
275501
276− notification = self._create_ephemeral(summary, body)
502+ notification = self._create_ephemeral_notification(summary, body)
277503 notification.show()
278504
279505 get_notification = lambda: notify_list.select_single('Notification')
280506 self.assertThat(get_notification, Eventually(NotEquals(None)))
281507 notification = get_notification()
282− self._assert_notification(notification, summary, body, False, False, 1.0)
283− Notify.uninit()
508+ self._assert_notification(
509+ notification,
510+ summary,
511+ body,
512+ False,
513+ False,
514+ 1.0
515+ )
284516
285− @with_lightdm_mock("single")
286517 def test_summary_only(self):
518+ """Notification must display only the expected summary-text."""
287519 self.launch_unity()
288520 greeter = self.main_window.get_greeter()
289521 greeter.unlock()
290− Notify.init("Autopilot Notification Tests")
522+
291523 notify_list = self._get_notifications_list()
292524
293525 summary = 'Summary-Only'
294526
295− notification = self._create_ephemeral(summary)
527+ notification = self._create_ephemeral_notification(summary)
296528 notification.show()
297529
298530 get_notification = lambda: notify_list.select_single('Notification')
299531 self.assertThat(get_notification, Eventually(NotEquals(None)))
300532 notification = get_notification()
533+
301534 self._assert_notification(notification, summary, '', False, False, 1.0)
302− Notify.uninit()
303535
304− @with_lightdm_mock("single")
305− def test_update_notification(self):
536+ def test_update_notification_same_layout(self):
537+ """Notification must allow updating its contents while being displayed."""
306538 self.launch_unity()
307539 greeter = self.main_window.get_greeter()
308540 greeter.unlock()
309− Notify.init("Autopilot Notification Tests")
541+
310542 notify_list = self._get_notifications_list()
311543
312− summary = 'Inital notification (1. notification)'
544+ summary = 'Inital notification'
313545 body = 'This is the original content of this notification-bubble.'
314546 icon_path = self._get_icon_path('avatars/funky@12.png')
315− notification = self._create_ephemeral(summary, body, icon_path)
547+
548+ notification = self._create_ephemeral_notification(
549+ summary,
550+ body,
551+ icon_path
552+ )
316553 notification.show()
554+
317555 get_notification = lambda: notify_list.select_single('Notification')
318556 self.assertThat(get_notification, Eventually(NotEquals(None)))
319− self._assert_notification(get_notification(), summary, body, True, False, 1.0)
557+ self._assert_notification(
558+ get_notification(),
559+ summary,
560+ body,
561+ True,
562+ False,
563+ 1.0
564+ )
320565
321566 time.sleep(3)
322− summary = 'Updated notification (1. notification)'
323− body = 'Here the same bubble with new title- and body-text, even the icon can be changed on the update.'
567+ summary = 'Updated notification'
568+ body = 'Here the same bubble with new title- and body-text, even ' \
569+ 'the icon can be changed on the update.'
324570 icon_path = self._get_icon_path('avatars/amanda@12.png')
325571 notification.update(summary, body, icon_path)
326572 notification.show()
327573 self.assertThat(get_notification, Eventually(NotEquals(None)))
328574 self._assert_notification(get_notification(), summary, body)
329575
330− time.sleep(6)
331− summary = 'Initial layout (2. notification)'
332− body = 'This bubble uses the icon-title-body layout with a secondary icon.'
576+ def test_update_notification_layout_change(self):
577+ """Notification must allow updating its contents and layout while being displayed."""
578+ self.launch_unity()
579+ greeter = self.main_window.get_greeter()
580+ greeter.unlock()
581+
582+ notify_list = self._get_notifications_list()
583+
584+ summary = 'Initial layout'
585+ body = 'This bubble uses the icon-title-body layout with a ' \
586+ 'secondary icon.'
333587 icon_path = self._get_icon_path('avatars/anna_olsson@12.png')
334588 hint_icon = self._get_icon_path('applicationIcons/phone-app@18.png')
335− notification = self._create_ephemeral(summary, body, icon_path)
336− notification.set_hint('x-canonical-secondary-icon', GLib.Variant.new_string(hint_icon))
589+
590+ notification = self._create_ephemeral_notification(
591+ summary,
592+ body,
593+ icon_path
594+ )
595+ notification.set_hint_string(
596+ 'x-canonical-secondary-icon',
597+ hint_icon
598+ )
337599 notification.show()
600+
601+ get_notification = lambda: notify_list.select_single('Notification')
338602 self.assertThat(get_notification, Eventually(NotEquals(None)))
339− self._assert_notification(get_notification(), summary, body, True, True, 1.0)
603+ self._assert_notification(
604+ get_notification(),
605+ summary,
606+ body,
607+ True,
608+ True,
609+ 1.0
610+ )
340611
341612 time.sleep(3)
342613 notification.clear_hints()
343− summary = 'Updated layout (2. notification)'
344− body = 'After the update we now have a bubble using the title-body layout.'
614+ summary = 'Updated layout'
615+ body = 'After the update we now have a bubble using the title-body ' \
616+ 'layout.'
345617 notification.update(summary, body, None)
346618 notification.show()
619+
347620 self.assertThat(get_notification, Eventually(NotEquals(None)))
348621 self._assert_notification(get_notification(), summary, body, False)
349− Notify.uninit()
350622
351− @with_lightdm_mock("single")
352623 def test_append_hint(self):
624+ """Notification has to accumulate body-text using append-hint."""
353625 self.launch_unity()
354626 greeter = self.main_window.get_greeter()
355627 greeter.unlock()
356− Notify.init("Autopilot Notification Tests")
628+
357629 notify_list = self._get_notifications_list()
358630
359631 summary = 'Cole Raby'
360632 body = 'Hey Bro Coly!'
361633 icon_path = self._get_icon_path('avatars/amanda@12.png')
362634 body_sum = body
363− notification = self._create_ephemeral(summary, body, icon_path)
364− notification.set_hint('x-canonical-append', GLib.Variant.new_string('true'))
635+ notification = self._create_ephemeral_notification(
636+ summary,
637+ body,
638+ icon_path,
639+ hints=[('x-canonical-append', 'true')]
640+ )
641+
365642 notification.show()
643+
366644 get_notification = lambda: notify_list.select_single('Notification')
367645 self.assertThat(get_notification, Eventually(NotEquals(None)))
368646 notification = get_notification()
369− self._assert_notification(notification, summary, body_sum, True, False, 1.0)
647+ self._assert_notification(
648+ notification,
649+ summary,
650+ body_sum,
651+ True,
652+ False,
653+ 1.0
654+ )
370655
371− bodies = ['What\'s up dude?',
372− 'Did you watch the air-race in Oshkosh last week?',
373− 'Phil owned the place like no one before him!',
374− 'Did really everything in the race work according to regulations?',
375− 'Somehow I think to remember Burt Williams did cut corners and was not punished for this.',
376− 'Hopefully the referees will watch the videos of the race.',
377− 'Burt could get fined with US$ 50000 for that rule-violation :)']
656+ bodies = [
657+ 'What\'s up dude?',
658+ 'Did you watch the air-race in Oshkosh last week?',
659+ 'Phil owned the place like no one before him!',
660+ 'Did really everything in the race work according to regulations?',
661+ 'Somehow I think to remember Burt Williams did cut corners and was not punished for this.',
662+ 'Hopefully the referees will watch the videos of the race.',
663+ 'Burt could get fined with US$ 50000 for that rule-violation :)'
664+ ]
378665
379666 for new_body in bodies:
380667 time.sleep(1)
381668 body = new_body
382669 body_sum += '\n' + body
383− notification = self._create_ephemeral(summary, body, icon_path)
384− notification.set_hint('x-canonical-append', GLib.Variant.new_string('true'))
670+ notification = self._create_ephemeral_notification(
671+ summary,
672+ body,
673+ icon_path,
674+ hints=[('x-canonical-append', 'true')]
675+ )
385676 notification.show()
386− get_notification = lambda: notify_list.select_single('Notification')
677+ get_notification = lambda: notify_list.select_single(
678+ 'Notification'
679+ )
387680 self.assertThat(get_notification, Eventually(NotEquals(None)))
388681 notification = get_notification()
389− self._assert_notification(notification, summary, body_sum, True, False, 1.0)
390−
391− Notify.uninit()
392−
393− def _run_loop_with_killswitch(self, loop):
394− logger.info("Running loop with timeout")
395− print "Running loop with timeout"
396−
397− def killer(loop):
398− logger.info("Killing Loop!")
399− print "Killing Loop!"
400− loop.quit()
401−
402− GLib.timeout_add_seconds(10, killer, loop)
403− loop.run()
404−
405− def _create_notification(
406− self,
407− summary="",
408− body="",
409− icon=None,
410− urgency="NORMAL",
411− hints=[]
412− ):
413− """Create a notification command.
414−
415− :param summary: Summary text for the notification
416− :param body: Body text to display in the notification
417− :param icon: Path string to the icon to use
418− :param urgency: Urgency string for the noticiation, either: 'LOW',
419− 'NORMAL', 'CRITICAL'
420− :param hint_strings: List of tuples containing the 'name' and value for
421− setting the hint strings for the notification
422−
423− """
424− logger.info(
425− "Creating notification with summary(%s), body(%s) "
426− "and urgency(%r)",
427− summary,
428− body,
429− urgency
430− )
431−
432− script_args = [
433− '--summary', summary,
434− '--body', body,
435− '--urgency', urgency
436− ]
437−
438− if icon is not None:
439− script_args.extend(['--icon', icon])
440−
441− for hint in hints:
442− key, value = hint
443− script_args.extend(['--hint', "%s,%s" % (key, value)])
444−
445− command = [self._get_notify_script()] + script_args
446− logger.info("Launching interactive notification as: %s", command)
447− self._notify_proc = subprocess.Popen(
448− command,
449− stdin=subprocess.PIPE,
450− stdout=subprocess.PIPE,
451− stderr=subprocess.PIPE,
452− close_fds=True,
453− preexec_fn=os.setsid
454− )
455−
456− def _create_interactive_notification(
457− self,
458− summary="",
459− body="",
460− icon=None,
461− urgency="NORMAL",
462− action_id="action_id",
463− action_label="action_label",
464− hints=[]
465− ):
466− """Create an Interactive notification command.
467−
468− :param summary: Summary text for the notification
469− :param body: Body text to display in the notification
470− :param icon: Path string to the icon to use
471− :param urgency: Urgency string for the noticiation, either: 'LOW',
472− 'NORMAL', 'CRITICAL'
473− :param action_id: String containing id to store for the callback
474− :param action_label: String to display on the notification
475− :param hint_strings: List of tuples containing the 'name' and value for
476− setting the hint strings for the notification
477−
478− """
479− logger.info(
480− "Creating interactive notification with summary(%s), body(%s) "
481− "and urgency(%r)",
482− summary,
483− body,
484− urgency
485− )
486−
487− script_args = [
488− '--summary', summary,
489− '--body', body,
490− '--urgency', urgency,
491− '--action', "%s,%s" % (action_id, action_label)
492− ]
493−
494− if icon is not None:
495− script_args.extend(['--icon', icon])
496−
497− for hint in hints:
498− key, value = hint
499− script_args.extend(['--hint', "%s,%s" % (key, value)])
500−
501− command = [self._get_notify_script("Interactive")] + script_args
502− logger.info("Launching interactive notification as: %s", command)
503− self._notify_proc = subprocess.Popen(
504− command,
505− stdin=subprocess.PIPE,
506− stdout=subprocess.PIPE,
507− stderr=subprocess.PIPE,
508− close_fds=True,
509− preexec_fn=os.setsid
510− )
682+ self._assert_notification(
683+ notification,
684+ summary,
685+ body_sum,
686+ True,
687+ False,
688+ 1.0
689+ )
511690
512− def _create_sd_notification(
691+ def _create_ephemeral_notification(
513692 self,
514693 summary="",
515694 body="",
516695 icon=None,
517− urgency="NORMAL",
518− actions=[],
519− hints=[]
696+ hints=[],
697+ urgency="NORMAL"
520698 ):
521− """Create a SnapDecision notification command.
699+ """Create an ephemeral (non-interactive) notification
522700
523− :param summary: Summary text for the notification
524− :param body: Body text to display in the notification
525− :param icon: Path string to the icon to use
526− :param urgency: Urgency for the noticiation, either: Notifications.LOW,
527− Notifications.NORMAL, Notifications.CRITICAL
528− :param actions: List of tuples containing the 'id' and 'label' for all
529− the actions to add
530− :param hint_strings: List of tuples containing the 'name' and value for
531− setting the hint strings for the notification
701+ :param summary: Summary text for the notification
702+ :param body: Body text to display in the notification
703+ :param icon: Path string to the icon to use
704+ :param hint_strings: List of tuples containing the 'name' and value
705+ for setting the hint strings for the notification
706+ :param urgency: Urgency string for the noticiation, either: 'LOW',
707+ 'NORMAL', 'CRITICAL'
532708
533709 """
534−
535710 logger.info(
536− "Creating snap-decision notification with summary(%s), body(%s) "
537− "and urgency(%r)",
711+ "Creating ephemeral: summary(%s), body(%s), urgency(%r) "
712+ "and Icon(%s)",
538713 summary,
539714 body,
540− urgency
715+ urgency,
716+ icon
541717 )
542718
543− script_args = [
544− '--summary', summary,
545− '--body', body,
546− '--urgency', urgency
547− ]
548−
549− if icon is not None:
550− script_args.extend(['--icon', icon])
719+ n = Notify.Notification.new(summary, body, icon)
551720
552721 for hint in hints:
553722 key, value = hint
554− script_args.extend(['--hint', "%s,%s" % (key, value)])
555−
556− for action in actions:
557− action_id, action_label = action
558− script_args.extend(['--action', "%s,%s" % (action_id, action_label)])
559−
560− command = [self._get_notify_script("SnapDecision")] + script_args
561− logger.info("Launching snap-decision notification as: %s", command)
562− self._notify_proc = subprocess.Popen(
563− command,
564− stdin=subprocess.PIPE,
565− stdout=subprocess.PIPE,
566− stderr=subprocess.PIPE,
567− close_fds=True,
568− preexec_fn=os.setsid
569− )
570−
571− def _get_notify_script(self, type):
572− """Returns the path to the interactive notification creation script."""
573− if type == "Interactive":
574− file = "../../emulators/create_interactive_notification.py"
575− elif type == "SnapDecision":
576− file = "../../emulators/create_sd_notification.py"
577− elif type == "Ephemeral":
578− file = "../../emulators/create_ephemeral_notification.py"
579−
580− the_path = os.path.abspath(
581− os.path.join(__file__, file))
582−
583− return the_path
584−
585− def assert_notification_action_id_was_called(self, action_id, timeout=10):
586− """Assert that the interactive notification callback of id *action_id*
587− was called.
588−
589− :raises AssertionError: If no interactive notification has actually been
590− created.
591− :raises AssertionError: When *action_id* does not match the actual
592− returned.
593− :raises AssertionError: If no callback was called at all.
594− """
595−
596− if self._notify_proc is None:
597− raise AssertionError("No interactive notification was created.")
598−
599− for i in xrange(timeout):
600− self._notify_proc.poll()
601− if self._notify_proc.returncode is not None:
602− output = self._notify_proc.communicate()
603− actual_action_id = output[0].strip("\n")
604− if actual_action_id != action_id:
605− raise AssertionError(
606− "action id '%s' does not match actual returned '%s'"
607− % (action_id, actual_action_id)
608− )
609− else:
610− return
611− time.sleep(1)
612−
613− os.killpg(self._notify_proc.pid, signal.SIGTERM)
614− self._notify_proc = None
615− raise AssertionError(
616− "No callback was called, killing interactivenotification script"
617− )
618−
619− def _create_ephemeral(self, summary="", body="", icon=None, secondary_icon=None, urgency="NORMAL"):
620− logger.info("Creating ephemeral notification with summary(%s), body(%s) and urgency(%r)", summary, body, urgency)
621− if icon is not None:
622− logger.info("Using icon(%s)", icon)
623− if secondary_icon is not None:
624− logger.info("Using secondary-icon(%s)", secondary_icon)
625−
626− n = Notify.Notification.new(summary, body, icon)
627− if secondary_icon is not None:
628− n.set_hint('x-canonical-secondary-icon', GLib.Variant.new_string(secondary_icon))
629− n.set_urgency(self._get_urgency(urgency))
630−
631− return n
632−
633− def _create_interactive(self, summary="", body="", icon=None, secondary_icon=None, urgency="NORMAL", action_id="action_id", action_label="action_label", action_cb=None):
634− logger.info("Creating interactive notification with summary(%s), body(%s) and urgency(%r)", summary, body, urgency)
635− if icon is not None:
636− logger.info("Using icon(%s)", icon)
637− if secondary_icon is not None:
638− logger.info("Using secondary-icon(%s)", secondary_icon)
639−
640− n = Notify.Notification.new(summary, body, icon)
641− if secondary_icon is not None:
642− n.set_hint('x-canonical-secondary-icon', GLib.Variant.new_string(secondary_icon))
643− n.set_hint('x-canonical-switch-to-application', GLib.Variant.new_string('true'))
644− n.add_action(action_id, action_label, action_cb, None, None)
645− n.connect('closed', self._close_cb)
646− n.set_urgency(self._get_urgency(urgency))
647−
648− return n
649−
650− def _create_snap_decision(self, summary="", body="", icon=None, secondary_icon=None, urgency="NORMAL", action_ids=None, action_labels=None, action_cbs=None):
651− logger.info("Creating snap-decision notification with summary(%s), body(%s) and urgency(%r)", summary, body, urgency)
652− if icon is not None:
653− logger.info("Using icon(%s)", icon)
654− if secondary_icon is not None:
655− logger.info("Using secondary-icon(%s)", secondary_icon)
656−
657− size_ids = len(action_ids)
658− size_labels = len(action_labels)
659− size_cbs = len(action_cbs)
660− if size_ids != size_labels and size_ids != size_cbs:
661− logger.info("Array-sizes of action-ids (%d), action-labels (%d) and action-callbacks (%d) do not match!", size_ids, size_labels, size_cbs)
662− return None
663−
664− n = Notify.Notification.new(summary, body, icon)
665− if secondary_icon is not None:
666− n.set_hint('x-canonical-secondary-icon', GLib.Variant.new_string(secondary_icon))
667− n.set_hint('x-canonical-snap-decisions', GLib.Variant.new_string('true'))
668− for i in range(size_ids):
669− n.add_action(action_ids[i], action_labels[i], action_cbs[i], None, None)
670− n.connect('closed', self._close_cb)
671− n.set_urgency(self._get_urgency(urgency))
672−
673− return n
674−
675− def _create_confirmation(self, summary="", body="", icon=None, secondary_icon=None, urgency="NORMAL"):
676− logger.info("Creating confirmation notification with summary(%s), body(%s) and urgency(%r)", summary, body, urgency)
677− if icon is not None:
678− logger.info("Using icon(%s)", icon)
679− if secondary_icon is not None:
680− logger.info("Using secondary-icon(%s)", secondary_icon)
681−
682− n = Notify.Notification.new(summary, body, icon)
683− if secondary_icon is not None:
684− n.set_hint('x-canonical-secondary-icon', GLib.Variant.new_string(secondary_icon))
685− n.set_hint('x-canonical-private-synchronous', GLib.Variant.new_string('true'))
686− if summary == "" and body == "":
687− n.set_hint('x-canonical-private-icon-only', GLib.Variant.new_string('true'))
723+ n.set_hint_string(key, value)
724+ logger.info("Adding hint to notification: (%s, %s)", key, value)
688725 n.set_urgency(self._get_urgency(urgency))
689726
690727 return n
@@ -695,92 +732,3 @@ class TestNotifications(UnityTestCase):
695732 'NORMAL': Notify.Urgency.NORMAL,
696733 'CRITICAL': Notify.Urgency.CRITICAL}
697734 return _urgency_enums.get(urgency.upper())
698−
699− def _get_icon_path(self, icon_name):
700− """Given an icons file name returns the full path (either system or
701− source tree.
702−
703− Consider the graphics directory as root so for example (runnign tests
704− from installed unity8-autopilot package):
705− >>> self.get_icon_path('clock@18.png')
706− /usr/share/unity8/graphics/clock@18.png
707−
708− >>> self.get_icon_path('applicationIcons/facebook@18.png')
709− /usr/share/unity8/graphics/applicationIcons/facebook@18.png
710−
711− """
712− if os.path.abspath(__file__).startswith('/usr/'):
713− return '/usr/share/unity8/graphics/' + icon_name
714− else:
715− return os.path.abspath(os.getcwd() + "/../../graphics/" + icon_name)
716−
717− def _get_notifications_list(self):
718− main_view = self.main_window.get_qml_view()
719− return main_view.select_single("QQuickListView", objectName='notificationList')
720−
721− def _assert_notification(self, notification, summary=None, body=None, icon=True, secondary_icon=False, opacity=None):
722− if summary is not None:
723− self.assertThat(notification.summary, Equals(summary))
724−
725− if body is not None:
726− self.assertThat(notification.body, Equals(body))
727−
728− if icon:
729− self.assertThat(notification.iconSource, NotEquals(""))
730− else:
731− self.assertThat(notification.iconSource, Equals(""))
732−
733− if secondary_icon:
734− self.assertThat(notification.secondaryIconSource, NotEquals(""))
735− else:
736− self.assertThat(notification.secondaryIconSource, Equals(""))
737−
738− if opacity is not None:
739− self.assertThat(notification.opacity, Eventually(Equals(opacity)))
740−
741− def _action_interactive_cb(self, notification, action, data):
742− if action == "action_id":
743− logger.info("--- Triggering action ---")
744− print "--- Triggering action ---"
745− self.action_interactive_triggered = True
746− else:
747− logger.info("--- That should not have happened (action_id)! ---")
748− print "--- That should not have happened (action_id)! ---"
749− self.action_interactive_triggered = False
750−
751− def _action_sd_decline1_cb(self, notification, action, data):
752− if action == "action_decline_1":
753− print "Decline"
754− else:
755− print "That should not have happened (action_decline_1)!"
756−
757− def _action_sd_decline2_cb(self, notification, action, data):
758− if action == "action_decline_2":
759− print "\"Can't talk now, what's up?\""
760− else:
761− print "That should not have happened (action_decline_2)!"
762−
763− def _action_sd_decline3_cb(self, notification, action, data):
764− if action == "action_decline_3":
765− print "\"I call you back.\""
766− else:
767− print "That should not have happened (action_decline_3)!"
768−
769− def _action_sd_decline4_cb(self, notification, action, data):
770− if action == "action_decline_4":
771− print "Send custom message..."
772− self.action_send_message_triggered = True
773− else:
774− print "That should not have happened (action_decline_4)!"
775− self.action_send_message_triggered = False
776−
777− def _action_sd_accept_cb(self, notification, action, data):
778− if action == "action_accept":
779− print "Accepting call"
780− self.action_accept_triggered = True
781− else:
782− print "That should not have happened (action_accept)!"
783− self.action_accept_triggered = False
784−
785− def _close_cb(self, notification):
786− self.loop.quit()