aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile.am4
-rw-r--r--configure.ac7
-rw-r--r--data/com.canonical.indicator.messages5
-rw-r--r--debian/changelog53
-rw-r--r--debian/control2
-rw-r--r--libmessaging-menu/messaging-menu-app.c4
-rw-r--r--libmessaging-menu/messaging-menu-message.c7
-rw-r--r--po/POTFILES.in1
-rw-r--r--src/Makefile.am2
-rw-r--r--src/im-accounts-service.c223
-rw-r--r--src/im-accounts-service.h53
-rw-r--r--src/im-application-list.c169
-rw-r--r--src/im-desktop-menu.c9
-rw-r--r--src/im-menu.c33
-rw-r--r--src/im-menu.h2
-rw-r--r--src/im-phone-menu.c13
-rw-r--r--src/im-phone-menu.h3
-rw-r--r--src/messages-service.c4
-rw-r--r--test/applications/test.desktop2
-rw-r--r--tests/Makefile.am (renamed from test/Makefile.am)58
-rw-r--r--tests/accounts-service-mock.h134
-rw-r--r--tests/applications/test.desktop5
-rw-r--r--tests/applications/test2.desktop5
-rw-r--r--tests/indicator-fixture.h688
-rw-r--r--tests/indicator-messages-service-activate.build.sh (renamed from test/indicator-messages-service-activate.build.sh)0
-rw-r--r--tests/indicator-messages-service-activate.c (renamed from test/indicator-messages-service-activate.c)0
-rw-r--r--tests/indicator-test.cpp209
-rw-r--r--tests/manual47
-rwxr-xr-xtests/test-client.py (renamed from test/test-client.py)0
-rw-r--r--tests/test-gactionmuxer.cpp (renamed from test/test-gactionmuxer.cpp)0
30 files changed, 1689 insertions, 53 deletions
diff --git a/Makefile.am b/Makefile.am
index f8141a8..a48804e 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -12,10 +12,10 @@ endif
if BUILD_TESTS
SUBDIRS += \
- test
+ tests
# build src first
-test: src
+tests: src libmessaging-menu
endif
diff --git a/configure.ac b/configure.ac
index e27f41d..b9c8800 100644
--- a/configure.ac
+++ b/configure.ac
@@ -43,10 +43,13 @@ GLIB_REQUIRED_VERSION=2.35.4
INTROSPECTION_REQUIRED_VERSION=1.32.0
PKG_CHECK_MODULES(APPLET, gio-unix-2.0 >= $GIO_UNIX_REQUIRED_VERSION
- glib-2.0 >= $GLIB_REQUIRED_VERSION)
+ glib-2.0 >= $GLIB_REQUIRED_VERSION
+ accountsservice)
PKG_CHECK_MODULES(GIO, gio-unix-2.0 >= $GIO_UNIX_REQUIRED_VERSION)
+PKG_CHECK_MODULES(DBUSTEST, dbustest-1)
+
AC_SUBST(APPLET_CFLAGS)
AC_SUBST(APPLET_LIBS)
@@ -171,7 +174,7 @@ data/icons/scalable/status/Makefile
data/icons/scalable/categories/Makefile
data/upstart/Makefile
po/Makefile.in
-test/Makefile
+tests/Makefile
libmessaging-menu/Makefile
libmessaging-menu/messaging-menu.pc
doc/Makefile
diff --git a/data/com.canonical.indicator.messages b/data/com.canonical.indicator.messages
index 2210b6b..572a4a7 100644
--- a/data/com.canonical.indicator.messages
+++ b/data/com.canonical.indicator.messages
@@ -6,11 +6,14 @@ Position=50
[desktop]
ObjectPath=/com/canonical/indicator/messages/desktop
+[desktop_greeter]
+ObjectPath=/com/canonical/indicator/messages/desktop_greeter
+
[phone]
ObjectPath=/com/canonical/indicator/messages/phone
Position=100
[phone_greeter]
-ObjectPath=/com/canonical/indicator/messages/phone
+ObjectPath=/com/canonical/indicator/messages/phone_greeter
Position=100
diff --git a/debian/changelog b/debian/changelog
index 230a105..e1820d8 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,56 @@
+indicator-messages (13.10.1+15.04.20150311-0ubuntu1) vivid; urgency=medium
+
+ [ Ted Gould ]
+ * Sanity tests to test basic features of the messaging menu
+
+ -- CI Train Bot <ci-train-bot@canonical.com> Wed, 11 Mar 2015 16:21:18 +0000
+
+indicator-messages (13.10.1+15.04.20150304-0ubuntu1) vivid; urgency=medium
+
+ [ Ted Gould ]
+ * Make the message actions have the message ID in their names like the
+ muxer
+
+ -- CI Train Bot <ci-train-bot@canonical.com> Wed, 04 Mar 2015 23:59:37 +0000
+
+indicator-messages (13.10.1+15.04.20150219-0ubuntu1) vivid; urgency=medium
+
+ [ Ted Gould ]
+ * Provide functions that allow for action names directly (LP:
+ #1385331)
+
+ -- CI Train Bot <ci-train-bot@canonical.com> Thu, 19 Feb 2015 15:55:54 +0000
+
+indicator-messages (13.10.1+15.04.20150126-0ubuntu1) vivid; urgency=low
+
+ [ Lars Uebernickel ]
+ * unescape_action_name: fix logic error (& instead of |) (LP:
+ #1414025)
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Mon, 26 Jan 2015 22:07:44 +0000
+
+indicator-messages (13.10.1+15.04.20150112-0ubuntu1) vivid; urgency=low
+
+ [ Lars Uebernickel ]
+ * desktop menu: don't warn when no default handler for a mime type is
+ found (LP: #1389725)
+ * Escape message and source ids when using them as action names (LP:
+ #1386584)
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Mon, 12 Jan 2015 18:39:08 +0000
+
+indicator-messages (13.10.1+15.04.20141103-0ubuntu1) vivid; urgency=low
+
+ [ Ted Gould ]
+ * Add filtering for lock screen. (LP: #1358340)
+
+ [ Lars Uebernickel ]
+ * libmessaging-menu: allow numbers in object paths (LP: #1384811)
+ * libmessaging-menu: fix section link in the documentation and add
+ descriptions (LP: #1313561)
+
+ -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Mon, 03 Nov 2014 20:56:30 +0000
+
indicator-messages (13.10.1+14.10.20141009-0ubuntu1) utopic; urgency=low
[ Ted Gould ]
diff --git a/debian/control b/debian/control
index b2c899d..eccb874 100644
--- a/debian/control
+++ b/debian/control
@@ -10,6 +10,8 @@ Build-Depends: debhelper (>= 9),
gobject-introspection (>= 0.9.12-4~),
gtk-doc-tools,
intltool,
+ libaccountsservice-dev,
+ libdbustest1-dev,
libgirepository1.0-dev (>= 0.9.12),
libgtest-dev,
python3-dbusmock,
diff --git a/libmessaging-menu/messaging-menu-app.c b/libmessaging-menu/messaging-menu-app.c
index 689a388..83dfb41 100644
--- a/libmessaging-menu/messaging-menu-app.c
+++ b/libmessaging-menu/messaging-menu-app.c
@@ -25,7 +25,7 @@
#include <string.h>
/**
- * SECTION:messaging-menu
+ * SECTION:messaging-menu-app
* @title: MessagingMenuApp
* @short_description: An application section in the messaging menu
* @include: messaging-menu.h
@@ -207,7 +207,7 @@ messaging_menu_app_get_dbus_object_path (MessagingMenuApp *app)
g_app_info_get_id (G_APP_INFO (app->appinfo)),
NULL);
- g_strcanon (path, "/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", '_');
+ g_strcanon (path, "/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", '_');
return path;
}
diff --git a/libmessaging-menu/messaging-menu-message.c b/libmessaging-menu/messaging-menu-message.c
index 02b329d..4bf8b7c 100644
--- a/libmessaging-menu/messaging-menu-message.c
+++ b/libmessaging-menu/messaging-menu-message.c
@@ -19,6 +19,13 @@
#include "messaging-menu-message.h"
+/**
+ * SECTION:messaging-menu-message
+ * @title: MessagingMenuMessage
+ * @short_description: A single message in the messaging menu
+ * @include: messaging-menu.h
+ */
+
typedef GObjectClass MessagingMenuMessageClass;
struct _MessagingMenuMessage
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 6cb028f..4cb6565 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -1,5 +1,4 @@
[encoding: UTF-8]
-test/indicator-messages-service-activate.c
src/im-phone-menu.c
src/messages-service.c
src/im-desktop-menu.c
diff --git a/src/Makefile.am b/src/Makefile.am
index bc674c2..36e6a93 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -10,6 +10,8 @@ indicator_messages_service_SOURCES = \
gactionmuxer.h \
gsettingsstrv.c \
gsettingsstrv.h \
+ im-accounts-service.c \
+ im-accounts-service.h \
im-menu.c \
im-menu.h \
im-phone-menu.c \
diff --git a/src/im-accounts-service.c b/src/im-accounts-service.c
new file mode 100644
index 0000000..b7ab15d
--- /dev/null
+++ b/src/im-accounts-service.c
@@ -0,0 +1,223 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3, as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranties of
+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Ted Gould <ted@canonical.com>
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <act/act.h>
+
+#include "im-accounts-service.h"
+
+typedef struct _ImAccountsServicePrivate ImAccountsServicePrivate;
+
+struct _ImAccountsServicePrivate {
+ ActUserManager * user_manager;
+ GDBusProxy * touch_settings;
+ GCancellable * cancel;
+};
+
+#define IM_ACCOUNTS_SERVICE_GET_PRIVATE(o) \
+(G_TYPE_INSTANCE_GET_PRIVATE ((o), IM_ACCOUNTS_SERVICE_TYPE, ImAccountsServicePrivate))
+
+static void im_accounts_service_class_init (ImAccountsServiceClass *klass);
+static void im_accounts_service_init (ImAccountsService *self);
+static void im_accounts_service_dispose (GObject *object);
+static void im_accounts_service_finalize (GObject *object);
+static void user_changed (ActUserManager * manager, ActUser * user, gpointer user_data);
+static void on_user_manager_loaded (ActUserManager * manager, GParamSpec * pspect, gpointer user_data);
+static void security_privacy_ready (GObject * obj, GAsyncResult * res, gpointer user_data);
+
+G_DEFINE_TYPE (ImAccountsService, im_accounts_service, G_TYPE_OBJECT);
+
+static void
+im_accounts_service_class_init (ImAccountsServiceClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ g_type_class_add_private (klass, sizeof (ImAccountsServicePrivate));
+
+ object_class->dispose = im_accounts_service_dispose;
+ object_class->finalize = im_accounts_service_finalize;
+}
+
+static void
+im_accounts_service_init (ImAccountsService *self)
+{
+ ImAccountsServicePrivate * priv = IM_ACCOUNTS_SERVICE_GET_PRIVATE(self);
+
+ priv->cancel = g_cancellable_new();
+
+ priv->user_manager = act_user_manager_get_default();
+ g_signal_connect(priv->user_manager, "user-changed", G_CALLBACK(user_changed), self);
+ g_signal_connect(priv->user_manager, "notify::is-loaded", G_CALLBACK(on_user_manager_loaded), self);
+
+ gboolean isLoaded = FALSE;
+ g_object_get(G_OBJECT(priv->user_manager), "is-loaded", &isLoaded, NULL);
+ if (isLoaded) {
+ on_user_manager_loaded(priv->user_manager, NULL, NULL);
+ }
+}
+
+static void
+im_accounts_service_dispose (GObject *object)
+{
+ ImAccountsServicePrivate * priv = IM_ACCOUNTS_SERVICE_GET_PRIVATE(object);
+
+ if (priv->cancel != NULL) {
+ g_cancellable_cancel(priv->cancel);
+ g_clear_object(&priv->cancel);
+ }
+
+ g_clear_object(&priv->user_manager);
+
+ G_OBJECT_CLASS (im_accounts_service_parent_class)->dispose (object);
+}
+
+static void
+im_accounts_service_finalize (GObject *object)
+{
+ G_OBJECT_CLASS (im_accounts_service_parent_class)->finalize (object);
+}
+
+/* Handles a User getting updated */
+static void
+user_changed (ActUserManager * manager, ActUser * user, gpointer user_data)
+{
+ if (g_strcmp0(act_user_get_user_name(user), g_get_user_name()) != 0) {
+ return;
+ }
+
+ ImAccountsServicePrivate * priv = IM_ACCOUNTS_SERVICE_GET_PRIVATE(user_data);
+ g_debug("User Updated");
+
+ /* Clear old proxies */
+ g_clear_object(&priv->touch_settings);
+
+ /* Start getting a new proxy */
+ g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM,
+ G_DBUS_PROXY_FLAGS_NONE,
+ NULL,
+ "org.freedesktop.Accounts",
+ act_user_get_object_path(user),
+ "com.ubuntu.touch.AccountsService.SecurityPrivacy",
+ priv->cancel,
+ security_privacy_ready,
+ user_data);
+}
+
+/* Respond to the async of setting up the proxy. Mostly we get it or we error. */
+static void
+security_privacy_ready (GObject * obj, GAsyncResult * res, gpointer user_data)
+{
+ GError * error = NULL;
+ GDBusProxy * proxy = g_dbus_proxy_new_for_bus_finish(res, &error);
+
+ if (error != NULL) {
+ g_warning("Unable to get a proxy on accounts service for touch settings: %s", error->message);
+ g_error_free(error);
+ return;
+ }
+
+ ImAccountsServicePrivate * priv = IM_ACCOUNTS_SERVICE_GET_PRIVATE(user_data);
+ /* Ensure we didn't get a proxy while we weren't looking */
+ g_clear_object(&priv->touch_settings);
+ priv->touch_settings = proxy;
+}
+
+/* When the user manager is loaded see if we have a user already loaded
+ along with. */
+static void
+on_user_manager_loaded (ActUserManager * manager, GParamSpec * pspect, gpointer user_data)
+{
+ ImAccountsServicePrivate * priv = IM_ACCOUNTS_SERVICE_GET_PRIVATE(user_data);
+ ActUser * user = NULL;
+
+ g_debug("Accounts Manager Loaded");
+
+ user = act_user_manager_get_user(priv->user_manager, g_get_user_name());
+ if (user != NULL) {
+ user_changed(priv->user_manager, user, user_data);
+ g_object_unref(user);
+ }
+}
+
+/* Not the most testable way to do this but, it is a less invasive one, and we'll
+ probably restructure this codebase soonish */
+/* Gets an account service wrapper reference, so then it can be free'd */
+ImAccountsService *
+im_accounts_service_ref_default (void)
+{
+ static ImAccountsService * as = NULL;
+ if (as == NULL) {
+ as = IM_ACCOUNTS_SERVICE(g_object_new(IM_ACCOUNTS_SERVICE_TYPE, NULL));
+ g_object_add_weak_pointer(G_OBJECT(as), (gpointer *)&as);
+ return as;
+ }
+
+ return g_object_ref(as);
+}
+
+/* The draws attention setting is very legacy right now, we've patched and not changed
+ things much. We're gonna do better in the future, this function abstracts out the ugly */
+void
+im_accounts_service_set_draws_attention (ImAccountsService * service, gboolean draws_attention)
+{
+ g_return_if_fail(IM_IS_ACCOUNTS_SERVICE(service));
+ ImAccountsServicePrivate * priv = IM_ACCOUNTS_SERVICE_GET_PRIVATE(service);
+
+ if (priv->touch_settings == NULL) {
+ return;
+ }
+
+ g_dbus_connection_call(g_dbus_proxy_get_connection(priv->touch_settings),
+ g_dbus_proxy_get_name(priv->touch_settings),
+ g_dbus_proxy_get_object_path(priv->touch_settings),
+ "org.freedesktop.Accounts.User",
+ "SetXHasMessages",
+ g_variant_new("(b)", draws_attention),
+ NULL, /* reply */
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, /* timeout */
+ priv->cancel, /* cancellable */
+ NULL, NULL); /* cb */
+}
+
+/* Looks at the property that is set by settings. We default to off in any case
+ that we can or we don't know what the state is. */
+gboolean
+im_accounts_service_get_show_on_greeter (ImAccountsService * service)
+{
+ g_return_val_if_fail(IM_IS_ACCOUNTS_SERVICE(service), FALSE);
+
+ ImAccountsServicePrivate * priv = IM_ACCOUNTS_SERVICE_GET_PRIVATE(service);
+
+ if (priv->touch_settings == NULL) {
+ return FALSE;
+ }
+
+ GVariant * val = g_dbus_proxy_get_cached_property(priv->touch_settings, "MessagesWelcomeScreen");
+ if (val == NULL) {
+ return FALSE;
+ }
+
+ gboolean retval = g_variant_get_boolean(val);
+ g_variant_unref(val);
+ return retval;
+}
diff --git a/src/im-accounts-service.h b/src/im-accounts-service.h
new file mode 100644
index 0000000..d7611d8
--- /dev/null
+++ b/src/im-accounts-service.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3, as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranties of
+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Ted Gould <ted@canonical.com>
+ */
+
+#ifndef __IM_ACCOUNTS_SERVICE_H__
+#define __IM_ACCOUNTS_SERVICE_H__
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define IM_ACCOUNTS_SERVICE_TYPE (im_accounts_service_get_type ())
+#define IM_ACCOUNTS_SERVICE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), IM_ACCOUNTS_SERVICE_TYPE, ImAccountsService))
+#define IM_ACCOUNTS_SERVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), IM_ACCOUNTS_SERVICE_TYPE, ImAccountsServiceClass))
+#define IM_IS_ACCOUNTS_SERVICE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), IM_ACCOUNTS_SERVICE_TYPE))
+#define IM_IS_ACCOUNTS_SERVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), IM_ACCOUNTS_SERVICE_TYPE))
+#define IM_ACCOUNTS_SERVICE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), IM_ACCOUNTS_SERVICE_TYPE, ImAccountsServiceClass))
+
+typedef struct _ImAccountsService ImAccountsService;
+typedef struct _ImAccountsServiceClass ImAccountsServiceClass;
+
+struct _ImAccountsServiceClass {
+ GObjectClass parent_class;
+};
+
+struct _ImAccountsService {
+ GObject parent;
+};
+
+GType im_accounts_service_get_type (void);
+ImAccountsService * im_accounts_service_ref_default (void);
+void im_accounts_service_set_draws_attention (ImAccountsService * service, gboolean draws_attention);
+gboolean im_accounts_service_get_show_on_greeter (ImAccountsService * service);
+
+G_END_DECLS
+
+#endif
diff --git a/src/im-application-list.c b/src/im-application-list.c
index 931e9ad..36b2ff3 100644
--- a/src/im-application-list.c
+++ b/src/im-application-list.c
@@ -22,6 +22,7 @@
#include "indicator-messages-application.h"
#include "gactionmuxer.h"
#include "indicator-desktop-shortcuts.h"
+#include "im-accounts-service.h"
#include <gio/gdesktopappinfo.h>
#include <string.h>
@@ -41,6 +42,8 @@ struct _ImApplicationList
GSimpleAction * statusaction;
GHashTable *app_status;
+
+ ImAccountsService * as;
};
G_DEFINE_TYPE (ImApplicationList, im_application_list, G_TYPE_OBJECT);
@@ -131,6 +134,62 @@ _g_action_group_has_actions (GActionGroup * ag)
return retval;
}
+static gchar *
+escape_action_name (const gchar *name)
+{
+ static const gchar *xdigits = "0123456789abcdef";
+ GString *escaped;
+ gchar c;
+
+ g_return_val_if_fail (name != NULL, NULL);
+
+ escaped = g_string_new (NULL);
+ while ((c = *name++))
+ {
+ if (g_ascii_isalnum (c) || c == '.')
+ {
+ g_string_append_c (escaped, c);
+ }
+ else
+ {
+ g_string_append_c (escaped, '-');
+ g_string_append_c (escaped, xdigits[c >> 4]);
+ g_string_append_c (escaped, xdigits[c & 0xf]);
+ }
+ }
+
+ return g_string_free (escaped, FALSE);
+}
+
+static gchar *
+unescape_action_name (const gchar *name)
+{
+ GString *unescaped;
+ gint i;
+
+ g_return_val_if_fail (name != NULL, NULL);
+
+ unescaped = g_string_new (NULL);
+ for (i = 0; name[i]; i++)
+ {
+ gint one, two;
+
+ if (name[i] == '-' &&
+ (one = g_ascii_xdigit_value (name[i + 1])) >= 0 &&
+ (two = g_ascii_xdigit_value (name[i + 2])) >= 0)
+ {
+ g_string_append_c (unescaped, (one << 4) | two);
+ i += 2;
+ }
+ else
+ {
+ g_string_append_c (unescaped, name[i]);
+ }
+ }
+
+ return g_string_free (unescaped, FALSE);
+}
+
/* Check to see if either of our action groups has any actions, if
so return TRUE so we get chosen! */
static gboolean
@@ -170,9 +229,11 @@ im_application_list_update_root_action (ImApplicationList *list)
if (g_hash_table_find (list->applications, application_draws_attention, NULL)) {
base_icon_name = "indicator-messages-new-%s";
accessible_name = _("New Messages");
+ im_accounts_service_set_draws_attention(list->as, TRUE);
} else {
base_icon_name = "indicator-messages-%s";
accessible_name = _("Messages");
+ im_accounts_service_set_draws_attention(list->as, FALSE);
}
/* Include the IM state in the icon */
@@ -284,18 +345,30 @@ application_update_draws_attention (Application * app)
return was_drawing_attention != app->draws_attention;
}
+static void
+im_application_list_source_removed_action (Application *app,
+ const gchar *action_name)
+{
+ g_action_map_remove_action (G_ACTION_MAP(app->source_actions), action_name);
+ g_signal_emit (app->list, signals[SOURCE_REMOVED], 0, app->id, action_name);
+
+ if (application_update_draws_attention(app))
+ im_application_list_update_root_action (app->list);
+}
+
/* Remove a source from an application, signal up and update the status
of the draws attention flag. */
static void
im_application_list_source_removed (Application *app,
const gchar *id)
{
- g_action_map_remove_action (G_ACTION_MAP(app->source_actions), id);
+ gchar *action_name;
- g_signal_emit (app->list, signals[SOURCE_REMOVED], 0, app->id, id);
+ action_name = escape_action_name (id);
- if (application_update_draws_attention(app))
- im_application_list_update_root_action (app->list);
+ im_application_list_source_removed_action (app, action_name);
+
+ g_free (action_name);
}
static void
@@ -304,9 +377,11 @@ im_application_list_source_activated (GSimpleAction *action,
gpointer user_data)
{
Application *app = user_data;
- const gchar *source_id;
+ const gchar *action_name;
+ gchar *source_id;
- source_id = g_action_get_name (G_ACTION (action));
+ action_name = g_action_get_name (G_ACTION (action));
+ source_id = unescape_action_name (action_name);
if (g_variant_get_boolean (parameter))
{
@@ -323,20 +398,35 @@ im_application_list_source_activated (GSimpleAction *action,
app->cancellable, NULL, NULL);
}
- im_application_list_source_removed (app, source_id);
+ im_application_list_source_removed_action (app, action_name);
+
+ g_free (source_id);
}
static void
-im_application_list_message_removed (Application *app,
- const gchar *id)
+im_application_list_message_removed_action (Application *app,
+ const gchar *action_name)
{
- g_action_map_remove_action (G_ACTION_MAP(app->message_actions), id);
- g_action_muxer_remove (app->message_sub_actions, id);
+ g_action_map_remove_action (G_ACTION_MAP(app->message_actions), action_name);
+ g_action_muxer_remove (app->message_sub_actions, action_name);
if (application_update_draws_attention(app))
im_application_list_update_root_action (app->list);
- g_signal_emit (app->list, signals[MESSAGE_REMOVED], 0, app->id, id);
+ g_signal_emit (app->list, signals[MESSAGE_REMOVED], 0, app->id, action_name);
+}
+
+static void
+im_application_list_message_removed (Application *app,
+ const gchar *id)
+{
+ gchar *action_name;
+
+ action_name = escape_action_name (id);
+
+ im_application_list_message_removed_action(app, action_name);
+
+ g_free (action_name);
}
static void
@@ -345,9 +435,11 @@ im_application_list_message_activated (GSimpleAction *action,
gpointer user_data)
{
Application *app = user_data;
- const gchar *message_id;
+ const gchar *action_name;
+ gchar *message_id;
- message_id = g_action_get_name (G_ACTION (action));
+ action_name = g_action_get_name (G_ACTION (action));
+ message_id = unescape_action_name (action_name);
if (g_variant_get_boolean (parameter))
{
@@ -366,7 +458,9 @@ im_application_list_message_activated (GSimpleAction *action,
app->cancellable, NULL, NULL);
}
- im_application_list_message_removed (app, message_id);
+ im_application_list_message_removed_action (app, action_name);
+
+ g_free (message_id);
}
static void
@@ -376,11 +470,11 @@ im_application_list_sub_message_activated (GSimpleAction *action,
{
Application *app = user_data;
const gchar *message_id;
- const gchar *action_id;
+ gchar *action_id;
GVariantBuilder builder;
message_id = g_object_get_data (G_OBJECT (action), "message");
- action_id = g_action_get_name (G_ACTION (action));
+ action_id = unescape_action_name (g_action_get_name (G_ACTION (action)));
g_variant_builder_init (&builder, G_VARIANT_TYPE ("av"));
if (parameter)
@@ -394,6 +488,8 @@ im_application_list_sub_message_activated (GSimpleAction *action,
NULL, NULL);
im_application_list_message_removed (app, message_id);
+
+ g_free (action_id);
}
static void
@@ -418,11 +514,11 @@ im_application_list_remove_all (GSimpleAction *action,
source_actions = g_action_group_list_actions (G_ACTION_GROUP (app->source_actions));
for (it = source_actions; *it; it++)
- im_application_list_source_removed (app, *it);
+ im_application_list_source_removed_action (app, *it);
message_actions = g_action_group_list_actions (G_ACTION_GROUP (app->message_actions));
for (it = message_actions; *it; it++)
- im_application_list_message_removed (app, *it);
+ im_application_list_message_removed_action (app, *it);
if (app->proxy != NULL) /* If it is remote, we tell the app we've cleared */
{
@@ -451,6 +547,8 @@ im_application_list_dispose (GObject *object)
g_clear_pointer (&list->applications, g_hash_table_unref);
g_clear_object (&list->muxer);
+ g_clear_object (&list->as);
+
G_OBJECT_CLASS (im_application_list_parent_class)->dispose (object);
}
@@ -602,6 +700,8 @@ im_application_list_init (ImApplicationList *list)
list->muxer = g_action_muxer_new ();
g_action_muxer_insert (list->muxer, NULL, G_ACTION_GROUP (list->globalactions));
+ list->as = im_accounts_service_ref_default();
+
im_application_list_update_root_action (list);
}
@@ -792,6 +892,7 @@ im_application_list_source_added (Application *app,
GVariant *serialized_icon = NULL;
GVariant *state;
GSimpleAction *action;
+ gchar *action_name;
g_variant_get (source, "(&s&s@avux&sb)",
&id, &label, &maybe_serialized_icon, &count, &time, &string, &draws_attention);
@@ -802,12 +903,13 @@ im_application_list_source_added (Application *app,
visible = count > 0 || time != 0 || (string != NULL && string[0] != '\0');
state = g_variant_new ("(uxsb)", count, time, string, draws_attention);
- action = g_simple_action_new_stateful (id, G_VARIANT_TYPE_BOOLEAN, state);
+ action_name = escape_action_name (id);
+ action = g_simple_action_new_stateful (action_name, G_VARIANT_TYPE_BOOLEAN, state);
g_signal_connect (action, "activate", G_CALLBACK (im_application_list_source_activated), app);
g_action_map_add_action (G_ACTION_MAP(app->source_actions), G_ACTION (action));
- g_signal_emit (app->list, signals[SOURCE_ADDED], 0, app->id, id, label, serialized_icon, visible);
+ g_signal_emit (app->list, signals[SOURCE_ADDED], 0, app->id, action_name, label, serialized_icon, visible);
if (visible && draws_attention && app->draws_attention == FALSE)
{
@@ -815,6 +917,7 @@ im_application_list_source_added (Application *app,
im_application_list_update_root_action (app->list);
}
+ g_free (action_name);
g_object_unref (action);
if (serialized_icon)
g_variant_unref (serialized_icon);
@@ -834,6 +937,7 @@ im_application_list_source_changed (Application *app,
gboolean draws_attention;
GVariant *serialized_icon = NULL;
gboolean visible;
+ gchar *action_name;
g_variant_get (source, "(&s&s@avux&sb)",
&id, &label, &maybe_serialized_icon, &count, &time, &string, &draws_attention);
@@ -841,12 +945,14 @@ im_application_list_source_changed (Application *app,
if (g_variant_n_children (maybe_serialized_icon) == 1)
g_variant_get_child (maybe_serialized_icon, 0, "v", &serialized_icon);
- g_action_group_change_action_state (G_ACTION_GROUP (app->source_actions), id,
+ action_name = escape_action_name (id);
+
+ g_action_group_change_action_state (G_ACTION_GROUP (app->source_actions), action_name,
g_variant_new ("(uxsb)", count, time, string, draws_attention));
visible = count > 0 || time != 0 || (string != NULL && string[0] != '\0');
- g_signal_emit (app->list, signals[SOURCE_CHANGED], 0, app->id, id, label, serialized_icon, visible);
+ g_signal_emit (app->list, signals[SOURCE_CHANGED], 0, app->id, action_name, label, serialized_icon, visible);
if (application_update_draws_attention (app))
im_application_list_update_root_action (app->list);
@@ -854,6 +960,7 @@ im_application_list_source_changed (Application *app,
if (serialized_icon)
g_variant_unref (serialized_icon);
g_variant_unref (maybe_serialized_icon);
+ g_free (action_name);
}
static void
@@ -958,6 +1065,7 @@ im_application_list_message_added (Application *app,
GSimpleAction *action;
GIcon *app_icon;
GVariant *actions = NULL;
+ gchar *action_name;
g_variant_get (message, "(&s@av&s&s&sxaa{sv}b)",
&id, &maybe_serialized_icon, &title, &subtitle, &body, &time, &action_iter, &draws_attention);
@@ -965,7 +1073,8 @@ im_application_list_message_added (Application *app,
if (g_variant_n_children (maybe_serialized_icon) == 1)
g_variant_get_child (maybe_serialized_icon, 0, "v", &serialized_icon);
- action = g_simple_action_new (id, G_VARIANT_TYPE_BOOLEAN);
+ action_name = escape_action_name (id);
+ action = g_simple_action_new (action_name, G_VARIANT_TYPE_BOOLEAN);
g_object_set_qdata(G_OBJECT(action), message_action_draws_attention_quark(), GINT_TO_POINTER(draws_attention));
g_signal_connect (action, "activate", G_CALLBACK (im_application_list_message_activated), app);
g_action_map_add_action (G_ACTION_MAP(app->message_actions), G_ACTION (action));
@@ -987,6 +1096,7 @@ im_application_list_message_added (Application *app,
GVariant *hint;
GVariantBuilder dict_builder;
gchar *prefixed_name;
+ gchar *escaped_name;
if (!g_variant_lookup (entry, "name", "&s", &name))
{
@@ -998,14 +1108,15 @@ im_application_list_message_added (Application *app,
g_variant_lookup (entry, "parameter-type", "&g", &type);
hint = g_variant_lookup_value (entry, "parameter-hint", NULL);
- action = g_simple_action_new (name, type ? G_VARIANT_TYPE (type) : NULL);
+ escaped_name = escape_action_name (name);
+ action = g_simple_action_new (escaped_name, type ? G_VARIANT_TYPE (type) : NULL);
g_object_set_data_full (G_OBJECT (action), "message", g_strdup (id), g_free);
g_signal_connect (action, "activate", G_CALLBACK (im_application_list_sub_message_activated), app);
g_action_map_add_action (G_ACTION_MAP(action_group), G_ACTION (action));
g_variant_builder_init (&dict_builder, G_VARIANT_TYPE ("a{sv}"));
- prefixed_name = g_strjoin (".", app->id, "msg-actions", id, name, NULL);
+ prefixed_name = g_strjoin (".", app->id, "msg-actions", action_name, escaped_name, NULL);
g_variant_builder_add (&dict_builder, "{sv}", "name", g_variant_new_string (prefixed_name));
if (label)
@@ -1028,9 +1139,10 @@ im_application_list_message_added (Application *app,
g_object_unref (action);
g_variant_unref (entry);
g_free (prefixed_name);
+ g_free (escaped_name);
}
- g_action_muxer_insert (app->message_sub_actions, id, G_ACTION_GROUP (action_group));
+ g_action_muxer_insert (app->message_sub_actions, action_name, G_ACTION_GROUP (action_group));
actions = g_variant_builder_end (&actions_builder);
g_object_unref (action_group);
@@ -1045,9 +1157,10 @@ im_application_list_message_added (Application *app,
app_icon = get_symbolic_app_icon (app->info);
g_signal_emit (app->list, signals[MESSAGE_ADDED], 0,
- app->id, app_icon, id, serialized_icon, title,
+ app->id, app_icon, action_name, serialized_icon, title,
subtitle, body, actions, time, draws_attention);
+ g_free (action_name);
g_variant_iter_free (action_iter);
g_object_unref (action);
if (serialized_icon)
diff --git a/src/im-desktop-menu.c b/src/im-desktop-menu.c
index 137e222..c762735 100644
--- a/src/im-desktop-menu.c
+++ b/src/im-desktop-menu.c
@@ -81,12 +81,15 @@ g_app_info_is_default_for_uri_scheme (GAppInfo *info,
const gchar *uri_scheme)
{
GAppInfo *default_info;
- gboolean is_default;
+ gboolean is_default = FALSE;
default_info = g_app_info_get_default_for_uri_scheme (uri_scheme);
- is_default = g_app_info_equal (info, default_info);
+ if (default_info)
+ {
+ is_default = g_app_info_equal (info, default_info);
+ g_object_unref (default_info);
+ }
- g_object_unref (default_info);
return is_default;
}
diff --git a/src/im-menu.c b/src/im-menu.c
index 55d4685..0c39b97 100644
--- a/src/im-menu.c
+++ b/src/im-menu.c
@@ -18,12 +18,15 @@
*/
#include "im-menu.h"
+#include "im-accounts-service.h"
struct _ImMenuPrivate
{
GMenu *toplevel_menu;
GMenu *menu;
ImApplicationList *applist;
+ gboolean on_greeter;
+ ImAccountsService *as;
};
G_DEFINE_TYPE_WITH_PRIVATE (ImMenu, im_menu, G_TYPE_OBJECT)
@@ -32,6 +35,7 @@ enum
{
PROP_0,
PROP_APPLICATION_LIST,
+ PROP_ON_GREETER,
NUM_PROPERTIES
};
@@ -43,6 +47,7 @@ im_menu_finalize (GObject *object)
g_object_unref (priv->toplevel_menu);
g_object_unref (priv->menu);
g_object_unref (priv->applist);
+ g_object_unref (priv->as);
G_OBJECT_CLASS (im_menu_parent_class)->finalize (object);
}
@@ -60,6 +65,9 @@ im_menu_get_property (GObject *object,
case PROP_APPLICATION_LIST:
g_value_set_object (value, priv->applist);
break;
+ case PROP_ON_GREETER:
+ g_value_set_boolean (value, priv->on_greeter);
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -79,6 +87,9 @@ im_menu_set_property (GObject *object,
case PROP_APPLICATION_LIST: /* construct only */
priv->applist = g_value_dup_object (value);
break;
+ case PROP_ON_GREETER:
+ priv->on_greeter = g_value_get_boolean (value);
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -100,6 +111,12 @@ im_menu_class_init (ImMenuClass *class)
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
+ g_object_class_install_property (object_class, PROP_ON_GREETER,
+ g_param_spec_boolean ("on-greeter", "", "",
+ FALSE,
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
}
static void
@@ -110,6 +127,8 @@ im_menu_init (ImMenu *menu)
priv->toplevel_menu = g_menu_new ();
priv->menu = g_menu_new ();
+ priv->on_greeter = FALSE;
+ priv->as = im_accounts_service_ref_default();
root = g_menu_item_new (NULL, "indicator.messages");
g_menu_item_set_attribute (root, "x-canonical-type", "s", "com.canonical.indicator.root");
@@ -225,3 +244,17 @@ im_menu_insert_item_sorted (ImMenu *menu,
g_menu_insert_item (priv->menu, position, item);
}
+
+/* Whether the menu should show extra data on it. Depends on the greeter
+ status and user settings */
+gboolean
+im_menu_show_data (ImMenu *menu)
+{
+ g_return_val_if_fail (IM_IS_MENU (menu), FALSE);
+ ImMenuPrivate *priv = im_menu_get_instance_private (IM_MENU (menu));
+
+ if (!priv->on_greeter)
+ return TRUE;
+
+ return im_accounts_service_get_show_on_greeter(priv->as);
+}
diff --git a/src/im-menu.h b/src/im-menu.h
index f67abc8..b76d616 100644
--- a/src/im-menu.h
+++ b/src/im-menu.h
@@ -64,4 +64,6 @@ void im_menu_insert_item_sorted (ImMenu
gint first,
gint last);
+gboolean im_menu_show_data (ImMenu *menu);
+
#endif
diff --git a/src/im-phone-menu.c b/src/im-phone-menu.c
index 754fc2b..58a23ff 100644
--- a/src/im-phone-menu.c
+++ b/src/im-phone-menu.c
@@ -142,12 +142,13 @@ im_phone_menu_init (ImPhoneMenu *menu)
}
ImPhoneMenu *
-im_phone_menu_new (ImApplicationList *applist)
+im_phone_menu_new (ImApplicationList *applist, gboolean greeter)
{
g_return_val_if_fail (IM_IS_APPLICATION_LIST (applist), NULL);
return g_object_new (IM_TYPE_PHONE_MENU,
"application-list", applist,
+ "on-greeter", greeter,
NULL);
}
@@ -179,10 +180,12 @@ im_phone_menu_add_message (ImPhoneMenu *menu,
gint n_messages;
gint pos;
GVariant *serialized_app_icon;
+ gboolean show_data;
g_return_if_fail (IM_IS_PHONE_MENU (menu));
g_return_if_fail (app_id);
+ show_data = im_menu_show_data(IM_MENU (menu));
action_name = g_strconcat (app_id, ".msg.", id, NULL);
item = g_menu_item_new (title, NULL);
@@ -190,8 +193,10 @@ im_phone_menu_add_message (ImPhoneMenu *menu,
g_menu_item_set_attribute (item, "x-canonical-type", "s", "com.canonical.indicator.messages.messageitem");
g_menu_item_set_attribute (item, "x-canonical-message-id", "s", id);
- g_menu_item_set_attribute (item, "x-canonical-subtitle", "s", subtitle);
- g_menu_item_set_attribute (item, "x-canonical-text", "s", body);
+ if (show_data)
+ g_menu_item_set_attribute (item, "x-canonical-subtitle", "s", subtitle);
+ if (show_data)
+ g_menu_item_set_attribute (item, "x-canonical-text", "s", body);
g_menu_item_set_attribute (item, "x-canonical-time", "x", time);
if (serialized_icon)
@@ -203,7 +208,7 @@ im_phone_menu_add_message (ImPhoneMenu *menu,
g_variant_unref (serialized_app_icon);
}
- if (actions)
+ if (actions && show_data)
g_menu_item_set_attribute (item, "x-canonical-message-actions", "v", actions);
n_messages = g_menu_model_get_n_items (G_MENU_MODEL (menu->message_section));
diff --git a/src/im-phone-menu.h b/src/im-phone-menu.h
index 4f96c8c..813634d 100644
--- a/src/im-phone-menu.h
+++ b/src/im-phone-menu.h
@@ -33,7 +33,8 @@ typedef struct _ImPhoneMenu ImPhoneMenu;
GType im_phone_menu_get_type (void);
-ImPhoneMenu * im_phone_menu_new (ImApplicationList *applist);
+ImPhoneMenu * im_phone_menu_new (ImApplicationList *applist,
+ gboolean greeter);
void im_phone_menu_add_message (ImPhoneMenu *menu,
const gchar *app_id,
diff --git a/src/messages-service.c b/src/messages-service.c
index d1ccbbc..d2c7e92 100644
--- a/src/messages-service.c
+++ b/src/messages-service.c
@@ -273,8 +273,10 @@ main (int argc, char ** argv)
}
menus = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_object_unref);
- g_hash_table_insert (menus, "phone", im_phone_menu_new (applications));
+ g_hash_table_insert (menus, "phone", im_phone_menu_new (applications, FALSE));
+ g_hash_table_insert (menus, "phone_greeter", im_phone_menu_new (applications, TRUE));
g_hash_table_insert (menus, "desktop", im_desktop_menu_new (applications));
+ g_hash_table_insert (menus, "desktop_greeter", im_desktop_menu_new (applications));
g_unix_signal_add(SIGTERM, sig_term_handler, mainloop);
diff --git a/test/applications/test.desktop b/test/applications/test.desktop
deleted file mode 100644
index c2332b9..0000000
--- a/test/applications/test.desktop
+++ /dev/null
@@ -1,2 +0,0 @@
-[Desktop Entry]
-Type=Application
diff --git a/test/Makefile.am b/tests/Makefile.am
index b679615..bd559fd 100644
--- a/test/Makefile.am
+++ b/tests/Makefile.am
@@ -1,5 +1,6 @@
-check_LIBRARIES = libgtest.a
+CLEANFILES=
+check_LTLIBRARIES = libgtest.la
check_PROGRAMS = test-gactionmuxer
TESTS = $(check_PROGRAMS)
@@ -7,15 +8,22 @@ TESTS = $(check_PROGRAMS)
AM_CPPFLAGS = $(GTEST_CPPFLAGS) \
-I${top_srcdir}/src
-nodist_libgtest_a_SOURCES = \
+######################################
+# Google Test
+######################################
+
+nodist_libgtest_la_SOURCES = \
$(GTEST_SOURCE)/src/gtest-all.cc \
$(GTEST_SOURCE)/src/gtest_main.cc
-libgtest_a_CPPFLAGS = \
+libgtest_la_CPPFLAGS = \
$(GTEST_CPPFLAGS) -w \
$(AM_CPPFLAGS)
-libgtest_a_CXXFLAGS = \
+libgtest_la_CXXFLAGS = \
$(AM_CXXFLAGS)
+######################################
+# GActionMixer
+######################################
test_gactionmuxer_SOURCES = \
test-gactionmuxer.cpp
@@ -27,8 +35,46 @@ test_gactionmuxer_CPPFLAGS = \
test_gactionmuxer_LDADD = \
$(APPLET_LIBS) \
libindicator-messages-service.la \
- libgtest.a
+ libgtest.la
+
+######################################
+# Indicator Test
+######################################
+
+SCHEMA_COMPILED_DIR="$(abs_builddir)/gsettings-schemas-compiled/"
+
+indicator_test_SOURCES = \
+ indicator-test.cpp
+
+indicator_test_CPPFLAGS = \
+ -DINDICATOR_MESSAGES_SERVICE_BINARY="\"$(abs_top_builddir)/src/indicator-messages-service\"" \
+ -DSCHEMA_DIR="\"$(SCHEMA_COMPILED_DIR)\"" \
+ -DXDG_DATA_DIRS="\"$(abs_srcdir)/\"" \
+ -I$(top_srcdir)/libmessaging-menu \
+ -std=c++11 \
+ $(APPLET_CFLAGS) \
+ $(DBUSTEST_CFLAGS) \
+ $(AM_CPPFLAGS)
+
+indicator_test_LDADD = \
+ $(APPLET_LIBS) \
+ $(DBUSTEST_LIBS) \
+ $(top_builddir)/libmessaging-menu/libmessaging-menu.la \
+ libgtest.la \
+ -lc -lpthread
+
+indicator-test.cpp: schemas-compiled.stamp
+
+schemas-compiled.stamp: $(top_srcdir)/data/*gschema.xml
+ @rm -rf $(SCHEMA_COMPILED_DIR)
+ @mkdir -p $(SCHEMA_COMPILED_DIR)
+ @cp -f $(top_srcdir)/data/*gschema.xml $(SCHEMA_COMPILED_DIR)
+ @`pkg-config gio-2.0 --variable glib_compile_schemas` $(SCHEMA_COMPILED_DIR)
+ @touch schemas-compiled.stamp
+CLEANFILES += schemas-compiled.stamp
+TESTS += indicator-test
+check_PROGRAMS += indicator-test
######################################
# Lib containing code under test
@@ -41,7 +87,7 @@ libindicator_messages_service_la_SOURCES = \
$(top_builddir)/common/indicator-messages-service.c \
$(top_builddir)/common/indicator-messages-service.h \
$(top_srcdir)/src/gactionmuxer.c \
- $(top_srcdir)/src/gactionmuxer.h
+ $(top_srcdir)/src/gactionmuxer.h \
$(top_srcdir)/src/dbus-data.h
libindicator_messages_service_ladir = \
diff --git a/tests/accounts-service-mock.h b/tests/accounts-service-mock.h
new file mode 100644
index 0000000..301fcd4
--- /dev/null
+++ b/tests/accounts-service-mock.h
@@ -0,0 +1,134 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Ted Gould <ted@canonical.com>
+ */
+
+#include <memory>
+#include <libdbustest/dbus-test.h>
+
+class AccountsServiceMock
+{
+ DbusTestDbusMock * mock = nullptr;
+ DbusTestDbusMockObject * soundobj = nullptr;
+ DbusTestDbusMockObject * userobj = nullptr;
+ DbusTestDbusMockObject * syssoundobj = nullptr;
+ DbusTestDbusMockObject * privacyobj = nullptr;
+
+ public:
+ AccountsServiceMock () {
+ mock = dbus_test_dbus_mock_new("org.freedesktop.Accounts");
+
+ dbus_test_task_set_bus(DBUS_TEST_TASK(mock), DBUS_TEST_SERVICE_BUS_SYSTEM);
+
+ DbusTestDbusMockObject * baseobj = dbus_test_dbus_mock_get_object(mock, "/org/freedesktop/Accounts", "org.freedesktop.Accounts", NULL);
+
+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
+ "CacheUser", G_VARIANT_TYPE_STRING, G_VARIANT_TYPE_OBJECT_PATH,
+ "ret = dbus.ObjectPath('/user')\n", NULL);
+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
+ "FindUserById", G_VARIANT_TYPE_INT64, G_VARIANT_TYPE_OBJECT_PATH,
+ "ret = dbus.ObjectPath('/user')\n", NULL);
+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
+ "FindUserByName", G_VARIANT_TYPE_STRING, G_VARIANT_TYPE_OBJECT_PATH,
+ "ret = dbus.ObjectPath('/user')\n", NULL);
+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
+ "ListCachedUsers", NULL, G_VARIANT_TYPE_OBJECT_PATH_ARRAY,
+ "ret = [ dbus.ObjectPath('/user') ]\n", NULL);
+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
+ "UncacheUser", G_VARIANT_TYPE_STRING, NULL,
+ "", NULL);
+
+ userobj = dbus_test_dbus_mock_get_object(mock, "/user", "org.freedesktop.Accounts.User", NULL);
+ dbus_test_dbus_mock_object_add_property(mock, userobj,
+ "UserName", G_VARIANT_TYPE_STRING,
+ g_variant_new_string(g_get_user_name()), NULL);
+ dbus_test_dbus_mock_object_add_method(mock, baseobj,
+ "SetXHasMessages", G_VARIANT_TYPE_BOOLEAN, nullptr,
+ "", NULL);
+
+ soundobj = dbus_test_dbus_mock_get_object(mock, "/user", "com.canonical.indicator.sound.AccountsService", NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "Timestamp", G_VARIANT_TYPE_UINT64,
+ g_variant_new_uint64(0), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "PlayerName", G_VARIANT_TYPE_STRING,
+ g_variant_new_string(""), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "PlayerIcon", G_VARIANT_TYPE_VARIANT,
+ g_variant_new_variant(g_variant_new_string("")), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "Running", G_VARIANT_TYPE_BOOLEAN,
+ g_variant_new_boolean(FALSE), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "State", G_VARIANT_TYPE_STRING,
+ g_variant_new_string(""), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "Title", G_VARIANT_TYPE_STRING,
+ g_variant_new_string(""), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "Artist", G_VARIANT_TYPE_STRING,
+ g_variant_new_string(""), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "Album", G_VARIANT_TYPE_STRING,
+ g_variant_new_string(""), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, soundobj,
+ "ArtUrl", G_VARIANT_TYPE_STRING,
+ g_variant_new_string(""), NULL);
+
+ syssoundobj = dbus_test_dbus_mock_get_object(mock, "/user", "com.ubuntu.touch.AccountsService.Sound", NULL);
+ dbus_test_dbus_mock_object_add_property(mock, syssoundobj,
+ "SilentMode", G_VARIANT_TYPE_BOOLEAN,
+ g_variant_new_boolean(FALSE), NULL);
+
+ privacyobj = dbus_test_dbus_mock_get_object(mock, "/user", "com.ubuntu.touch.AccountsService.SecurityPrivacy", NULL);
+ dbus_test_dbus_mock_object_add_property(mock, privacyobj,
+ "MessagesWelcomeScreen", G_VARIANT_TYPE_BOOLEAN,
+ g_variant_new_boolean(true), NULL);
+ dbus_test_dbus_mock_object_add_property(mock, privacyobj,
+ "StatsWelcomeScreen", G_VARIANT_TYPE_BOOLEAN,
+ g_variant_new_boolean(true), NULL);
+ }
+
+ ~AccountsServiceMock () {
+ g_debug("Destroying the Accounts Service Mock");
+ g_clear_object(&mock);
+ }
+
+ void setSilentMode (bool modeValue) {
+ dbus_test_dbus_mock_object_update_property(mock, syssoundobj,
+ "SilentMode", g_variant_new_boolean(modeValue ? TRUE : FALSE),
+ NULL);
+ }
+
+ operator std::shared_ptr<DbusTestTask> () {
+ return std::shared_ptr<DbusTestTask>(
+ DBUS_TEST_TASK(g_object_ref(mock)),
+ [](DbusTestTask * task) { g_clear_object(&task); });
+ }
+
+ operator DbusTestTask* () {
+ return DBUS_TEST_TASK(mock);
+ }
+
+ operator DbusTestDbusMock* () {
+ return mock;
+ }
+
+ DbusTestDbusMockObject * get_sound () {
+ return soundobj;
+ }
+};
diff --git a/tests/applications/test.desktop b/tests/applications/test.desktop
new file mode 100644
index 0000000..63ccb6b
--- /dev/null
+++ b/tests/applications/test.desktop
@@ -0,0 +1,5 @@
+[Desktop Entry]
+Type=Application
+Name=Test
+Exec=test
+Icon=test-app
diff --git a/tests/applications/test2.desktop b/tests/applications/test2.desktop
new file mode 100644
index 0000000..63ccb6b
--- /dev/null
+++ b/tests/applications/test2.desktop
@@ -0,0 +1,5 @@
+[Desktop Entry]
+Type=Application
+Name=Test
+Exec=test
+Icon=test-app
diff --git a/tests/indicator-fixture.h b/tests/indicator-fixture.h
new file mode 100644
index 0000000..e2c4316
--- /dev/null
+++ b/tests/indicator-fixture.h
@@ -0,0 +1,688 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Ted Gould <ted@canonical.com>
+ */
+
+#include <memory>
+#include <algorithm>
+#include <string>
+#include <functional>
+#include <future>
+
+#include <gtest/gtest.h>
+#include <gio/gio.h>
+#include <libdbustest/dbus-test.h>
+
+class IndicatorFixture : public ::testing::Test
+{
+ private:
+ std::string _indicatorPath;
+ std::string _indicatorAddress;
+ std::vector<std::shared_ptr<DbusTestTask>> _mocks;
+ protected:
+ std::chrono::milliseconds _eventuallyTime;
+
+ private:
+ class PerRunData {
+ public:
+ /* We're private in the fixture but other than that we don't care,
+ we don't leak out. This object's purpose isn't to hide data it is
+ to make the lifecycle of the items more clear. */
+ std::shared_ptr<GMenuModel> _menu;
+ std::shared_ptr<GActionGroup> _actions;
+ DbusTestService * _session_service;
+ DbusTestService * _system_service;
+ DbusTestTask * _test_indicator;
+ DbusTestTask * _test_dummy;
+ GDBusConnection * _session;
+ GDBusConnection * _system;
+
+ PerRunData (const std::string& indicatorPath, const std::string& indicatorAddress, std::vector<std::shared_ptr<DbusTestTask>>& mocks)
+ : _menu(nullptr)
+ , _session(nullptr)
+ {
+ _session_service = dbus_test_service_new(nullptr);
+ dbus_test_service_set_bus(_session_service, DBUS_TEST_SERVICE_BUS_SESSION);
+
+ _system_service = dbus_test_service_new(nullptr);
+ dbus_test_service_set_bus(_system_service, DBUS_TEST_SERVICE_BUS_SYSTEM);
+
+ _test_indicator = DBUS_TEST_TASK(dbus_test_process_new(indicatorPath.c_str()));
+ dbus_test_task_set_name(_test_indicator, "Indicator");
+ dbus_test_service_add_task(_session_service, _test_indicator);
+
+ _test_dummy = dbus_test_task_new();
+ dbus_test_task_set_wait_for(_test_dummy, indicatorAddress.c_str());
+ dbus_test_task_set_name(_test_dummy, "Dummy");
+ dbus_test_service_add_task(_session_service, _test_dummy);
+
+ for(auto task : mocks) {
+ if (dbus_test_task_get_bus(task.get()) == DBUS_TEST_SERVICE_BUS_SYSTEM) {
+ dbus_test_service_add_task(_system_service, task.get());
+ } else {
+ dbus_test_service_add_task(_session_service, task.get());
+ }
+ }
+
+ g_debug("Starting System Bus");
+ dbus_test_service_start_tasks(_system_service);
+ _system = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, nullptr);
+ g_dbus_connection_set_exit_on_close(_system, FALSE);
+
+ g_debug("Starting Session Bus");
+ dbus_test_service_start_tasks(_session_service);
+ _session = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr);
+ g_dbus_connection_set_exit_on_close(_session, FALSE);
+ }
+
+ virtual ~PerRunData (void) {
+ _menu.reset();
+ _actions.reset();
+
+ /* D-Bus Test Stuff */
+ g_clear_object(&_test_dummy);
+ g_clear_object(&_test_indicator);
+ g_clear_object(&_session_service);
+ g_clear_object(&_system_service);
+
+ /* Wait for D-Bus session bus to go */
+ if (!g_dbus_connection_is_closed(_session)) {
+ g_dbus_connection_close_sync(_session, nullptr, nullptr);
+ }
+ g_clear_object(&_session);
+
+ if (!g_dbus_connection_is_closed(_system)) {
+ g_dbus_connection_close_sync(_system, nullptr, nullptr);
+ }
+ g_clear_object(&_system);
+ }
+ };
+
+ std::shared_ptr<PerRunData> run;
+
+ public:
+ virtual ~IndicatorFixture() = default;
+
+ IndicatorFixture (const std::string& path,
+ const std::string& addr)
+ : _indicatorPath(path)
+ , _indicatorAddress(addr)
+ , _eventuallyTime(std::chrono::seconds(5))
+ {
+ };
+
+
+ protected:
+ virtual void SetUp() override
+ {
+ run = std::make_shared<PerRunData>(_indicatorPath, _indicatorAddress, _mocks);
+
+ _mocks.clear();
+ }
+
+ virtual void TearDown() override
+ {
+ run.reset();
+ }
+
+ void addMock (std::shared_ptr<DbusTestTask> mock)
+ {
+ _mocks.push_back(mock);
+ }
+
+ std::shared_ptr<DbusTestTask> buildBustleMock (const std::string& filename, DbusTestServiceBus bus = DBUS_TEST_SERVICE_BUS_BOTH)
+ {
+ return std::shared_ptr<DbusTestTask>([filename, bus]() {
+ DbusTestTask * bustle = DBUS_TEST_TASK(dbus_test_bustle_new(filename.c_str()));
+ dbus_test_task_set_name(bustle, "Bustle");
+ dbus_test_task_set_bus(bustle, bus);
+ return bustle;
+ }(), [](DbusTestTask * bustle) {
+ g_clear_object(&bustle);
+ });
+ }
+
+ private:
+ void waitForCore (GObject * obj, const gchar * signalname) {
+ auto loop = g_main_loop_new(nullptr, FALSE);
+
+ /* Our two exit criteria */
+ gulong signal = g_signal_connect_swapped(obj, signalname, G_CALLBACK(g_main_loop_quit), loop);
+ guint timer = g_timeout_add_seconds(5, [](gpointer user_data) -> gboolean {
+ g_warning("Menu Timeout");
+ g_main_loop_quit((GMainLoop *)user_data);
+ return G_SOURCE_CONTINUE;
+ }, loop);
+
+ /* Wait for sync */
+ g_main_loop_run(loop);
+
+ /* Clean up */
+ g_source_remove(timer);
+ g_signal_handler_disconnect(obj, signal);
+
+ g_main_loop_unref(loop);
+ }
+
+ void menuWaitForItems (const std::shared_ptr<GMenuModel>& menu) {
+ auto count = g_menu_model_get_n_items(menu.get());
+
+ if (count != 0)
+ return;
+
+ waitForCore(G_OBJECT(menu.get()), "items-changed");
+ }
+
+ void agWaitForActions (const std::shared_ptr<GActionGroup>& group) {
+ auto list = std::shared_ptr<gchar *>(
+ g_action_group_list_actions(group.get()),
+ [](gchar ** list) {
+ g_strfreev(list);
+ });
+
+ if (g_strv_length(list.get()) != 0) {
+ return;
+ }
+
+ waitForCore(G_OBJECT(group.get()), "action-added");
+ }
+
+ testing::AssertionResult expectEventually (std::function<testing::AssertionResult(void)> &testfunc) {
+ auto loop = std::shared_ptr<GMainLoop>(g_main_loop_new(nullptr, FALSE), [](GMainLoop * loop) { if (loop != nullptr) g_main_loop_unref(loop); });
+
+ std::promise<testing::AssertionResult> retpromise;
+ auto retfuture = retpromise.get_future();
+ auto start = std::chrono::steady_clock::now();
+
+ /* The core of the idle function as an object so we can use the C++-isms
+ of attaching the variables and make this code reasonably readable */
+ std::function<void(void)> idlefunc = [&loop, &retpromise, &testfunc, &start, this]() -> void {
+ auto result = testfunc();
+
+ if (result == false && _eventuallyTime > (std::chrono::steady_clock::now() - start)) {
+ return;
+ }
+
+ retpromise.set_value(result);
+ g_main_loop_quit(loop.get());
+ };
+
+ auto idlesrc = g_idle_add([](gpointer data) -> gboolean {
+ auto func = reinterpret_cast<std::function<void(void)> *>(data);
+ (*func)();
+ return G_SOURCE_CONTINUE;
+ }, &idlefunc);
+
+ g_main_loop_run(loop.get());
+ g_source_remove(idlesrc);
+
+ return retfuture.get();
+ }
+
+ protected:
+ void setMenu (const std::string& path) {
+ run->_menu.reset();
+
+ g_debug("Getting Menu: %s:%s", _indicatorAddress.c_str(), path.c_str());
+ run->_menu = std::shared_ptr<GMenuModel>(G_MENU_MODEL(g_dbus_menu_model_get(run->_session, _indicatorAddress.c_str(), path.c_str())), [](GMenuModel * modelptr) {
+ g_clear_object(&modelptr);
+ });
+
+ menuWaitForItems(run->_menu);
+ }
+
+ void setActions (const std::string& path) {
+ run->_actions.reset();
+
+ run->_actions = std::shared_ptr<GActionGroup>(G_ACTION_GROUP(g_dbus_action_group_get(run->_session, _indicatorAddress.c_str(), path.c_str())), [](GActionGroup * groupptr) {
+ g_clear_object(&groupptr);
+ });
+
+ agWaitForActions(run->_actions);
+ }
+
+ void activateAction (const std::string &name, std::shared_ptr<GVariant> &parameter) {
+ g_action_group_activate_action(run->_actions.get(), name.c_str(), parameter.get());
+ }
+
+ void activateAction (const std::string &name, GVariant * parameter = nullptr) {
+ std::shared_ptr<GVariant> param;
+
+ if (parameter != nullptr)
+ param = std::shared_ptr<GVariant>(g_variant_ref_sink(parameter), [](GVariant * var) {
+ g_variant_unref(var);
+ });
+
+ return activateAction(name, param);
+ }
+
+ testing::AssertionResult expectActionExists (const gchar * nameStr, const std::string& name) {
+ bool hasit = g_action_group_has_action(run->_actions.get(), name.c_str());
+
+ if (!hasit) {
+ auto result = testing::AssertionFailure();
+ result <<
+ " Action: " << nameStr << std::endl <<
+ " Expected: " << "Exists" << std::endl <<
+ " Actual: " << "No action found" << std::endl;
+
+ return result;
+ }
+
+ auto result = testing::AssertionSuccess();
+ return result;
+ }
+
+ template <typename... Args> testing::AssertionResult expectEventuallyActionExists (Args&& ... args) {
+ std::function<testing::AssertionResult(void)> func = [&]() {
+ return expectActionExists(std::forward<Args>(args)...);
+ };
+ return expectEventually(func);
+ }
+
+ testing::AssertionResult expectActionDoesNotExist (const gchar * nameStr, const std::string& name) {
+ bool hasit = g_action_group_has_action(run->_actions.get(), name.c_str());
+
+ if (hasit) {
+ auto result = testing::AssertionFailure();
+ result <<
+ " Action: " << nameStr << std::endl <<
+ " Expected: " << "No action found" << std::endl <<
+ " Actual: " << "Exists" << std::endl;
+
+ return result;
+ }
+
+ auto result = testing::AssertionSuccess();
+ return result;
+ }
+
+ template <typename... Args> testing::AssertionResult expectEventuallyActionDoesNotExist (Args&& ... args) {
+ std::function<testing::AssertionResult(void)> func = [&]() {
+ return expectActionDoesNotExist(std::forward<Args>(args)...);
+ };
+ return expectEventually(func);
+ }
+
+ testing::AssertionResult expectActionEnabled (const char * nameStr, const char * typeStr, const std::string& name, bool enabled) {
+ auto aenabled = g_action_group_get_action_enabled(run->_actions.get(), name.c_str());
+
+ if (enabled != aenabled) {
+ auto result = testing::AssertionFailure();
+ result <<
+ " Action: " << nameStr << std::endl <<
+ " Expected: " << enabled << std::endl <<
+ " Actual: " << aenabled << std::endl;
+
+ return result;
+ }
+
+ auto result = testing::AssertionSuccess();
+ return result;
+ }
+
+ template <typename... Args> testing::AssertionResult expectEventuallyActionEnabled (Args&& ... args) {
+ std::function<testing::AssertionResult(void)> func = [&]() {
+ return expectActionEnabled(std::forward<Args>(args)...);
+ };
+ return expectEventually(func);
+ }
+
+ testing::AssertionResult expectActionStateType (const char * nameStr, const char * typeStr, const std::string& name, const GVariantType * type) {
+ auto atype = g_action_group_get_action_state_type(run->_actions.get(), name.c_str());
+ bool same = false;
+
+ if (atype != nullptr) {
+ same = g_variant_type_equal(atype, type);
+ }
+
+ if (!same) {
+ auto result = testing::AssertionFailure();
+ result <<
+ " Action: " << nameStr << std::endl <<
+ " Expected: " << typeStr << std::endl <<
+ " Actual: " << (atype == nullptr ? "(null)" : g_variant_type_peek_string(atype)) << std::endl;
+
+ return result;
+ }
+
+ auto result = testing::AssertionSuccess();
+ return result;
+ }
+
+ template <typename... Args> testing::AssertionResult expectEventuallyActionStateType (Args&& ... args) {
+ std::function<testing::AssertionResult(void)> func = [&]() {
+ return expectActionStateType(std::forward<Args>(args)...);
+ };
+ return expectEventually(func);
+ }
+
+ testing::AssertionResult expectActionActivationType (const char * nameStr, const char * typeStr, const std::string& name, const GVariantType * type) {
+ auto atype = g_action_group_get_action_parameter_type(run->_actions.get(), name.c_str());
+ bool same = false;
+
+ if (atype != nullptr) {
+ same = g_variant_type_equal(atype, type);
+ }
+
+ if (!same) {
+ auto result = testing::AssertionFailure();
+ result <<
+ " Action: " << nameStr << std::endl <<
+ " Expected: " << typeStr << std::endl <<
+ " Actual: " << (atype == nullptr ? "(null)" : g_variant_type_peek_string(atype)) << std::endl;
+
+ return result;
+ }
+
+ auto result = testing::AssertionSuccess();
+ return result;
+ }
+
+ template <typename... Args> testing::AssertionResult expectEventuallyActionActivationType (Args&& ... args) {
+ std::function<testing::AssertionResult(void)> func = [&]() {
+ return expectActionActivationType(std::forward<Args>(args)...);
+ };
+ return expectEventually(func);
+ }
+
+ testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, std::shared_ptr<GVariant> varref) {
+ auto aval = std::shared_ptr<GVariant>(g_action_group_get_action_state(run->_actions.get(), name.c_str()), [] (GVariant * varptr) {
+ if (varptr != nullptr)
+ g_variant_unref(varptr);
+ });
+ bool match = false;
+
+ if (aval != nullptr) {
+ match = g_variant_equal(aval.get(), varref.get());
+ }
+
+ if (!match) {
+ gchar * attstr = nullptr;
+
+ if (aval != nullptr) {
+ attstr = g_variant_print(aval.get(), TRUE);
+ } else {
+ attstr = g_strdup("nullptr");
+ }
+
+ auto result = testing::AssertionFailure();
+ result <<
+ " Action: " << nameStr << std::endl <<
+ " Expected: " << valueStr << std::endl <<
+ " Actual: " << attstr << std::endl;
+
+ g_free(attstr);
+
+ return result;
+ } else {
+ auto result = testing::AssertionSuccess();
+ return result;
+ }
+ }
+
+ testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, GVariant * value) {
+ auto varref = std::shared_ptr<GVariant>(g_variant_ref_sink(value), [](GVariant * varptr) {
+ if (varptr != nullptr)
+ g_variant_unref(varptr);
+ });
+ return expectActionStateIs(nameStr, valueStr, name, varref);
+ }
+
+ testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, bool value) {
+ GVariant * var = g_variant_new_boolean(value);
+ return expectActionStateIs(nameStr, valueStr, name, var);
+ }
+
+ testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, std::string value) {
+ GVariant * var = g_variant_new_string(value.c_str());
+ return expectActionStateIs(nameStr, valueStr, name, var);
+ }
+
+ testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, const char * value) {
+ GVariant * var = g_variant_new_string(value);
+ return expectActionStateIs(nameStr, valueStr, name, var);
+ }
+
+ testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, double value) {
+ GVariant * var = g_variant_new_double(value);
+ return expectActionStateIs(nameStr, valueStr, name, var);
+ }
+
+ testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, float value) {
+ GVariant * var = g_variant_new_double(value);
+ return expectActionStateIs(nameStr, valueStr, name, var);
+ }
+
+ template <typename... Args> testing::AssertionResult expectEventuallyActionStateIs (Args&& ... args) {
+ std::function<testing::AssertionResult(void)> func = [&]() {
+ return expectActionStateIs(std::forward<Args>(args)...);
+ };
+ return expectEventually(func);
+ }
+
+
+ private:
+ std::shared_ptr<GVariant> getMenuAttributeVal (int location, std::shared_ptr<GMenuModel>& menu, const std::string& attribute, std::shared_ptr<GVariant>& value) {
+ if (!(location < g_menu_model_get_n_items(menu.get()))) {
+ return nullptr;
+ }
+
+ if (location >= g_menu_model_get_n_items(menu.get()))
+ return nullptr;
+
+ auto menuval = std::shared_ptr<GVariant>(g_menu_model_get_item_attribute_value(menu.get(), location, attribute.c_str(), g_variant_get_type(value.get())), [](GVariant * varptr) {
+ if (varptr != nullptr)
+ g_variant_unref(varptr);
+ });
+
+ return menuval;
+ }
+
+ std::shared_ptr<GVariant> getMenuAttributeRecurse (std::vector<int>::const_iterator menuLocation, std::vector<int>::const_iterator menuEnd, const std::string& attribute, std::shared_ptr<GVariant>& value, std::shared_ptr<GMenuModel>& menu) {
+ if (menuLocation == menuEnd)
+ return nullptr;
+
+ if (menuLocation + 1 == menuEnd)
+ return getMenuAttributeVal(*menuLocation, menu, attribute, value);
+
+ auto clearfunc = [](GMenuModel * modelptr) {
+ g_clear_object(&modelptr);
+ };
+
+ auto submenu = std::shared_ptr<GMenuModel>(g_menu_model_get_item_link(menu.get(), *menuLocation, G_MENU_LINK_SUBMENU), clearfunc);
+
+ if (submenu == nullptr)
+ submenu = std::shared_ptr<GMenuModel>(g_menu_model_get_item_link(menu.get(), *menuLocation, G_MENU_LINK_SECTION), clearfunc);
+
+ if (submenu == nullptr)
+ return nullptr;
+
+ menuWaitForItems(submenu);
+
+ return getMenuAttributeRecurse(menuLocation + 1, menuEnd, attribute, value, submenu);
+ }
+
+ protected:
+ testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const char * attributeStr, const char * valueStr, const std::vector<int> menuLocation, const std::string& attribute, GVariant * value) {
+ auto varref = std::shared_ptr<GVariant>(g_variant_ref_sink(value), [](GVariant * varptr) {
+ if (varptr != nullptr)
+ g_variant_unref(varptr);
+ });
+
+ auto attrib = getMenuAttributeRecurse(menuLocation.cbegin(), menuLocation.cend(), attribute, varref, run->_menu);
+ bool same = false;
+
+ if (attrib != nullptr && varref != nullptr) {
+ same = g_variant_equal(attrib.get(), varref.get());
+ }
+
+ if (!same) {
+ gchar * attstr = nullptr;
+
+ if (attrib != nullptr) {
+ attstr = g_variant_print(attrib.get(), TRUE);
+ } else {
+ attstr = g_strdup("nullptr");
+ }
+
+ auto result = testing::AssertionFailure();
+ result <<
+ " Menu: " << menuLocationStr << std::endl <<
+ " Attribute: " << attributeStr << std::endl <<
+ " Expected: " << valueStr << std::endl <<
+ " Actual: " << attstr << std::endl;
+
+ g_free(attstr);
+
+ return result;
+ } else {
+ auto result = testing::AssertionSuccess();
+ return result;
+ }
+ }
+
+ testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const char * attributeStr, const char * valueStr, const std::vector<int> menuLocation, const std::string& attribute, bool value) {
+ GVariant * var = g_variant_new_boolean(value);
+ return expectMenuAttribute(menuLocationStr, attributeStr, valueStr, menuLocation, attribute, var);
+ }
+
+ testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const char * attributeStr, const char * valueStr, const std::vector<int> menuLocation, const std::string& attribute, std::string value) {
+ GVariant * var = g_variant_new_string(value.c_str());
+ return expectMenuAttribute(menuLocationStr, attributeStr, valueStr, menuLocation, attribute, var);
+ }
+
+ testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const char * attributeStr, const char * valueStr, const std::vector<int> menuLocation, const std::string& attribute, const char * value) {
+ GVariant * var = g_variant_new_string(value);
+ return expectMenuAttribute(menuLocationStr, attributeStr, valueStr, menuLocation, attribute, var);
+ }
+
+ template <typename... Args> testing::AssertionResult expectEventuallyMenuAttribute (Args&& ... args) {
+ std::function<testing::AssertionResult(void)> func = [&]() {
+ return expectMenuAttribute(std::forward<Args>(args)...);
+ };
+ return expectEventually(func);
+ }
+
+ /* Eventually Helpers */
+ #define _EVENTUALLY_HELPER(oper) \
+ template <typename... Args> testing::AssertionResult expectEventually##oper (Args&& ... args) { \
+ std::function<testing::AssertionResult(void)> func = [&]() { \
+ return testing::internal::CmpHelper##oper(std::forward<Args>(args)...); \
+ }; \
+ return expectEventually(func); \
+ }
+
+ _EVENTUALLY_HELPER(EQ);
+ _EVENTUALLY_HELPER(NE);
+ _EVENTUALLY_HELPER(LT);
+ _EVENTUALLY_HELPER(GT);
+ _EVENTUALLY_HELPER(STREQ);
+ _EVENTUALLY_HELPER(STRNE);
+
+ #undef _EVENTUALLY_HELPER
+};
+
+/* Menu Attrib */
+#define ASSERT_MENU_ATTRIB(menu, attrib, value) \
+ ASSERT_PRED_FORMAT3(IndicatorFixture::expectMenuAttribute, menu, attrib, value)
+
+#define EXPECT_MENU_ATTRIB(menu, attrib, value) \
+ EXPECT_PRED_FORMAT3(IndicatorFixture::expectMenuAttribute, menu, attrib, value)
+
+#define EXPECT_EVENTUALLY_MENU_ATTRIB(menu, attrib, value) \
+ EXPECT_PRED_FORMAT3(IndicatorFixture::expectEventuallyMenuAttribute, menu, attrib, value)
+
+/* Action Exists */
+#define ASSERT_ACTION_EXISTS(action) \
+ ASSERT_PRED_FORMAT1(IndicatorFixture::expectActionExists, action)
+
+#define EXPECT_ACTION_EXISTS(action) \
+ EXPECT_PRED_FORMAT1(IndicatorFixture::expectActionExists, action)
+
+#define EXPECT_EVENTUALLY_ACTION_EXISTS(action) \
+ EXPECT_PRED_FORMAT1(IndicatorFixture::expectEventuallyActionExists, action)
+
+/* Action Does Not Exist */
+#define ASSERT_ACTION_DOES_NOT_EXIST(action) \
+ ASSERT_PRED_FORMAT1(IndicatorFixture::expectActionDoesNotExist, action)
+
+#define EXPECT_ACTION_DOES_NOT_EXIST(action) \
+ EXPECT_PRED_FORMAT1(IndicatorFixture::expectActionDoesNotExist, action)
+
+#define EXPECT_EVENTUALLY_ACTION_DOES_NOT_EXIST(action) \
+ EXPECT_PRED_FORMAT1(IndicatorFixture::expectEventuallyActionDoesNotExist, action)
+
+/* Action Enabled */
+#define ASSERT_ACTION_ENABLED(action, state) \
+ ASSERT_PRED_FORMAT2(IndicatorFixture::expectActionEnabled, action, state)
+
+#define EXPECT_ACTION_ENABLED(action, state) \
+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectActionEnabled, action, state)
+
+#define EXPECT_EVENTUALLY_ACTION_ENABLED(action, state) \
+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyActionEnabled, action, state)
+
+/* Action State */
+#define ASSERT_ACTION_STATE(action, value) \
+ ASSERT_PRED_FORMAT2(IndicatorFixture::expectActionStateIs, action, value)
+
+#define EXPECT_ACTION_STATE(action, value) \
+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectActionStateIs, action, value)
+
+#define EXPECT_EVENTUALLY_ACTION_STATE(action, value) \
+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyActionStateIs, action, value)
+
+/* Action State Type */
+#define ASSERT_ACTION_STATE_TYPE(action, type) \
+ ASSERT_PRED_FORMAT2(IndicatorFixture::expectActionStateType, action, type)
+
+#define EXPECT_ACTION_STATE_TYPE(action, type) \
+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectActionStateType, action, type)
+
+#define EXPECT_EVENTUALLY_ACTION_STATE_TYPE(action, type) \
+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyActionStateType, action, type)
+
+/* Action Activation Type */
+#define ASSERT_ACTION_ACTIVATION_TYPE(action, type) \
+ ASSERT_PRED_FORMAT2(IndicatorFixture::expectActionActivationType, action, type)
+
+#define EXPECT_ACTION_ACTIVATION_TYPE(action, type) \
+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectActionActivationType, action, type)
+
+#define EXPECT_EVENTUALLY_ACTION_ACTIVATION_TYPE(action, type) \
+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyActionActivationType, action, type)
+
+/* Helpers */
+
+#define EXPECT_EVENTUALLY_EQ(expected, actual) \
+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyEQ, expected, actual)
+
+#define EXPECT_EVENTUALLY_NE(expected, actual) \
+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyNE, expected, actual)
+
+#define EXPECT_EVENTUALLY_LT(expected, actual) \
+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyLT, expected, actual)
+
+#define EXPECT_EVENTUALLY_GT(expected, actual) \
+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyGT, expected, actual)
+
+#define EXPECT_EVENTUALLY_STREQ(expected, actual) \
+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallySTREQ, expected, actual)
+
+#define EXPECT_EVENTUALLY_STRNE(expected, actual) \
+ EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallySTRNE, expected, actual)
diff --git a/test/indicator-messages-service-activate.build.sh b/tests/indicator-messages-service-activate.build.sh
index 87a0316..87a0316 100644
--- a/test/indicator-messages-service-activate.build.sh
+++ b/tests/indicator-messages-service-activate.build.sh
diff --git a/test/indicator-messages-service-activate.c b/tests/indicator-messages-service-activate.c
index f5a26b0..f5a26b0 100644
--- a/test/indicator-messages-service-activate.c
+++ b/tests/indicator-messages-service-activate.c
diff --git a/tests/indicator-test.cpp b/tests/indicator-test.cpp
new file mode 100644
index 0000000..0991db5
--- /dev/null
+++ b/tests/indicator-test.cpp
@@ -0,0 +1,209 @@
+/*
+ * Copyright © 2015 Canonical Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Ted Gould <ted@canonical.com>
+ */
+
+#include <gtest/gtest.h>
+#include <gio/gio.h>
+
+#include "indicator-fixture.h"
+#include "accounts-service-mock.h"
+
+#include "messaging-menu-app.h"
+#include "messaging-menu-message.h"
+
+class IndicatorTest : public IndicatorFixture
+{
+protected:
+ IndicatorTest (void) :
+ IndicatorFixture(INDICATOR_MESSAGES_SERVICE_BINARY, "com.canonical.indicator.messages")
+ {
+ }
+
+ std::shared_ptr<AccountsServiceMock> as;
+
+ virtual void SetUp() override
+ {
+ g_setenv("GSETTINGS_SCHEMA_DIR", SCHEMA_DIR, TRUE);
+ g_setenv("GSETTINGS_BACKEND", "memory", TRUE);
+
+ g_setenv("XDG_DATA_DIRS", XDG_DATA_DIRS, TRUE);
+
+ as = std::make_shared<AccountsServiceMock>();
+ addMock(*as);
+
+ IndicatorFixture::SetUp();
+ }
+
+ virtual void TearDown() override
+ {
+ as.reset();
+
+ IndicatorFixture::TearDown();
+ }
+
+};
+
+
+TEST_F(IndicatorTest, RootAction) {
+ setActions("/com/canonical/indicator/messages");
+
+ EXPECT_EVENTUALLY_ACTION_EXISTS("messages");
+ EXPECT_ACTION_STATE_TYPE("messages", G_VARIANT_TYPE("a{sv}"));
+ EXPECT_ACTION_STATE("messages", g_variant_new_parsed("{'icon': <('themed', <['indicator-messages-offline', 'indicator-messages', 'indicator']>)>, 'title': <'Notifications'>, 'accessible-desc': <'Messages'>, 'visible': <false>}"));
+}
+
+TEST_F(IndicatorTest, SingleMessage) {
+ setActions("/com/canonical/indicator/messages");
+
+ auto app = std::shared_ptr<MessagingMenuApp>(messaging_menu_app_new("test.desktop"), [](MessagingMenuApp * app) { g_clear_object(&app); });
+ ASSERT_NE(nullptr, app);
+ messaging_menu_app_register(app.get());
+
+ EXPECT_EVENTUALLY_ACTION_EXISTS("test.launch");
+
+ auto msg = std::shared_ptr<MessagingMenuMessage>(messaging_menu_message_new(
+ "testid",
+ nullptr, /* no icon */
+ "Test Title",
+ "A subtitle too",
+ "You only like me for my body",
+ 0), [](MessagingMenuMessage * msg) { g_clear_object(&msg); });
+ messaging_menu_app_append_message(app.get(), msg.get(), nullptr, FALSE);
+
+ EXPECT_EVENTUALLY_ACTION_EXISTS("test.msg.testid");
+
+ setMenu("/com/canonical/indicator/messages/phone");
+
+ EXPECT_EVENTUALLY_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "x-canonical-type", "com.canonical.indicator.messages.messageitem");
+ EXPECT_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "label", "Test Title");
+ EXPECT_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "x-canonical-message-id", "testid");
+ EXPECT_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "x-canonical-subtitle", "A subtitle too");
+ EXPECT_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "x-canonical-text", "You only like me for my body");
+}
+
+static void
+messageReplyActivate (GObject * obj, gchar * name, GVariant * value, gpointer user_data) {
+ auto res = reinterpret_cast<std::string *>(user_data);
+ *res = g_variant_get_string(value, nullptr);
+}
+
+TEST_F(IndicatorTest, MessageReply) {
+ setActions("/com/canonical/indicator/messages");
+
+ auto app = std::shared_ptr<MessagingMenuApp>(messaging_menu_app_new("test.desktop"), [](MessagingMenuApp * app) { g_clear_object(&app); });
+ ASSERT_NE(nullptr, app);
+ messaging_menu_app_register(app.get());
+
+ EXPECT_EVENTUALLY_ACTION_EXISTS("test.launch");
+
+ auto msg = std::shared_ptr<MessagingMenuMessage>(messaging_menu_message_new(
+ "messageid",
+ nullptr, /* no icon */
+ "Reply Message",
+ "A message to reply to",
+ "In-app replies are for wimps, reply here to save yourself time and be cool.",
+ 0), [](MessagingMenuMessage * msg) { g_clear_object(&msg); });
+ messaging_menu_message_add_action(msg.get(),
+ "replyid",
+ "Reply",
+ G_VARIANT_TYPE_STRING,
+ nullptr);
+ messaging_menu_app_append_message(app.get(), msg.get(), nullptr, FALSE);
+
+ EXPECT_EVENTUALLY_ACTION_EXISTS("test.msg.messageid");
+ EXPECT_EVENTUALLY_ACTION_EXISTS("test.msg-actions.messageid.replyid");
+ EXPECT_ACTION_ACTIVATION_TYPE("test.msg-actions.messageid.replyid", G_VARIANT_TYPE_STRING);
+
+ EXPECT_ACTION_ENABLED("remove-all", true);
+
+ setMenu("/com/canonical/indicator/messages/phone");
+
+ EXPECT_EVENTUALLY_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "x-canonical-type", "com.canonical.indicator.messages.messageitem");
+
+ std::string activateResponse;
+ g_signal_connect(msg.get(), "activate", G_CALLBACK(messageReplyActivate), &activateResponse);
+
+ activateAction("test.msg-actions.messageid.replyid", g_variant_new_string("Reply to me"));
+
+ EXPECT_EVENTUALLY_EQ("Reply to me", activateResponse);
+
+ EXPECT_EVENTUALLY_ACTION_ENABLED("remove-all", false);
+}
+
+TEST_F(IndicatorTest, IconNotification) {
+ auto normalicon = std::shared_ptr<GVariant>(g_variant_ref_sink(g_variant_new_parsed("{'icon': <('themed', <['indicator-messages-offline', 'indicator-messages', 'indicator']>)>, 'title': <'Notifications'>, 'accessible-desc': <'Messages'>, 'visible': <true>}")), [](GVariant *var) {if (var != nullptr) g_variant_unref(var); });
+ auto blueicon = std::shared_ptr<GVariant>(g_variant_ref_sink(g_variant_new_parsed("{'icon': <('themed', <['indicator-messages-new-offline', 'indicator-messages-new', 'indicator-messages', 'indicator']>)>, 'title': <'Notifications'>, 'accessible-desc': <'New Messages'>, 'visible': <true>}")), [](GVariant *var) {if (var != nullptr) g_variant_unref(var); });
+
+ setActions("/com/canonical/indicator/messages");
+
+ auto app = std::shared_ptr<MessagingMenuApp>(messaging_menu_app_new("test.desktop"), [](MessagingMenuApp * app) { g_clear_object(&app); });
+ ASSERT_NE(nullptr, app);
+ messaging_menu_app_register(app.get());
+
+ EXPECT_EVENTUALLY_ACTION_EXISTS("test.launch");
+
+ EXPECT_ACTION_STATE("messages", normalicon);
+
+ auto app2 = std::shared_ptr<MessagingMenuApp>(messaging_menu_app_new("test2.desktop"), [](MessagingMenuApp * app) { g_clear_object(&app); });
+ ASSERT_NE(nullptr, app2);
+ messaging_menu_app_register(app2.get());
+
+ EXPECT_EVENTUALLY_ACTION_EXISTS("test2.launch");
+
+ messaging_menu_app_append_source_with_count(app2.get(),
+ "countsource",
+ nullptr,
+ "Count Source",
+ 500);
+ messaging_menu_app_draw_attention(app2.get(), "countsource");
+
+ EXPECT_EVENTUALLY_ACTION_STATE("messages", blueicon);
+
+ auto msg = std::shared_ptr<MessagingMenuMessage>(messaging_menu_message_new(
+ "messageid",
+ nullptr, /* no icon */
+ "Message",
+ "A secret message",
+ "asdfa;lkweraoweprijas;dvlknasvdoiewur;aslkd",
+ 0), [](MessagingMenuMessage * msg) { g_clear_object(&msg); });
+ messaging_menu_message_set_draws_attention(msg.get(), true);
+ messaging_menu_app_append_message(app.get(), msg.get(), nullptr, FALSE);
+
+ EXPECT_EVENTUALLY_ACTION_EXISTS("test.msg.messageid");
+ EXPECT_ACTION_STATE("messages", blueicon);
+
+ messaging_menu_app_unregister(app2.get());
+ app2.reset();
+
+ EXPECT_EVENTUALLY_ACTION_DOES_NOT_EXIST("test2.msg.countsource");
+ EXPECT_ACTION_STATE("messages", blueicon);
+
+ messaging_menu_app_remove_message(app.get(), msg.get());
+
+ EXPECT_EVENTUALLY_ACTION_STATE("messages", normalicon);
+ EXPECT_ACTION_ENABLED("remove-all", false);
+
+ messaging_menu_app_append_message(app.get(), msg.get(), nullptr, FALSE);
+
+ EXPECT_EVENTUALLY_ACTION_STATE("messages", blueicon);
+ EXPECT_ACTION_ENABLED("remove-all", true);
+
+ activateAction("remove-all");
+
+ EXPECT_EVENTUALLY_ACTION_STATE("messages", normalicon);
+}
diff --git a/tests/manual b/tests/manual
index ed930a9..d638875 100644
--- a/tests/manual
+++ b/tests/manual
@@ -31,3 +31,50 @@ Test-case indicator-messages/unity8-phone-symbolic-icon
<dt>Verify the application icon in the menu item is monochromatic</dt>
<dd>On the right side of the item the application icon should have no color</dd>
</dl>
+
+Test-case indicator-messages/unity8-embedded-greeter
+<dl>
+ <dt>NOTE: Only works with embedded greeter, split greeter will require modifications to this test</dt>
+ <dt>NOTE: Only works on a device that can receive SMS messages</dt>
+ <dt>Ensure System Settings is set to "Show Messages on Greeter"</dt>
+ <dt>Send an SMS to the device</dt>
+ <dd>The notification icon should change color</dd>
+ <dd>There should be an entry in the messaging menu with the SMS message</dd>
+ <dd>The item should include the sender and the start of the message</dd>
+ <dt>Go to the greeter. This can be done by hitting the lock button twice.</dt>
+ <dt>Ensure the messaging menu has the message</dt>
+ <dd>The notification icon should have color</dd>
+ <dd>There should be an entry in the messaging menu with the SMS message</dd>
+ <dd>The item should include the sender and the start of the message</dd>
+ <dt>Clear the message in the greeter</dt>
+ <dd>The message should no longer be in the messaging menu</dd>
+ <dt>Disable System Settings value "Show Messages on Greeter"</dt>
+ <dt>Send an SMS to the device</dt>
+ <dd>The notification icon should change color</dd>
+ <dd>There should be an entry in the messaging menu with the SMS message</dd>
+ <dd>The item should include the sender and the start of the message</dd>
+ <dt>Go to the greeter. This can be done by hitting the lock button twice.</dt>
+ <dt>Ensure the messaging menu has the message, but it does not include the start of the message</dt>
+ <dd>The notification icon should have color</dd>
+ <dd>There should be an entry in the messaging menu with the SMS message</dd>
+ <dd>The item should include the sender but NOT the start of the message</dd>
+ <dt>Clear the message in the greeter</dt>
+ <dd>The message should no longer be in the messaging menu</dd>
+</dl>
+
+Test-case indicator-messages/push-message-twitter
+<dl>
+ <dt>From a shell prompt send a simultated Twitter push notification</dt>
+ <dd>gdbus call --session --dest com.ubuntu.Postal --object-path /com/ubuntu/Postal/com_2eubuntu_2edeveloper_2ewebapps_2ewebapp_2dtwitter --method com.ubuntu.Postal.Post com.ubuntu.developer.webapps.webapp-twitter_webapp-twitter '"{\"message\": \"foobar\", \"notification\":{\"card\": {\"summary\": \"yes\", \"body\": \"hello\", \"popup\": true, \"persist\": true}}}"'</dd>
+ <dd>The messaging envelope on the panel should change to highlight a message</dd>
+ <dt>Open the messaging menu</dt>
+ <dd>The menu should contain an entry with the Twitter icon for the application</dd>
+ <dd>The title of the message should be 'yes'</dd>
+ <dd>The body of the message should be 'hello'</dd>
+ <dd>At the bottom of them menu there should be a 'Clear All' menu item</dd>
+ <dt>Clear the message using the 'Clear All' command</dt>
+ <dd>The Twitter message should disappear</dd>
+ <dd>The 'Clear All' item should disappear</dd>
+ <dd>The icon in the panel should return to its original state</dd>
+</dl>
+
diff --git a/test/test-client.py b/tests/test-client.py
index 0dbf868..0dbf868 100755
--- a/test/test-client.py
+++ b/tests/test-client.py
diff --git a/test/test-gactionmuxer.cpp b/tests/test-gactionmuxer.cpp
index 5c98c90..5c98c90 100644
--- a/test/test-gactionmuxer.cpp
+++ b/tests/test-gactionmuxer.cpp