aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXavi Garcia Mena <xavi.garcia.mena@canonical.com>2015-10-15 12:08:07 +0200
committerXavi Garcia Mena <xavi.garcia.mena@canonical.com>2015-10-15 12:08:07 +0200
commite5a1645266514e38c49281a7f8da296a0e81687d (patch)
tree633da2d705450f8a1eae26c4a0ae372a2e20131f
parentd3802b66c55e50ff1b9e2103cd446cb586d8acb3 (diff)
parent828fae630464f2eae96145cffcf97e5791497264 (diff)
downloadayatana-indicator-sound-e5a1645266514e38c49281a7f8da296a0e81687d.tar.gz
ayatana-indicator-sound-e5a1645266514e38c49281a7f8da296a0e81687d.tar.bz2
ayatana-indicator-sound-e5a1645266514e38c49281a7f8da296a0e81687d.zip
Added integragrion tests to the devices labels and icons
-rw-r--r--debian/control11
-rw-r--r--include/CMakeLists.txt1
-rw-r--r--include/unity/CMakeLists.txt1
-rw-r--r--include/unity/gmenuharness/MatchResult.h66
-rw-r--r--include/unity/gmenuharness/MatchUtils.h42
-rw-r--r--include/unity/gmenuharness/MenuItemMatcher.h143
-rw-r--r--include/unity/gmenuharness/MenuMatcher.h95
-rw-r--r--src/CMakeLists.txt19
-rw-r--r--src/gmenuharness/CMakeLists.txt17
-rw-r--r--src/gmenuharness/MatchResult.cpp187
-rw-r--r--src/gmenuharness/MatchUtils.cpp77
-rw-r--r--src/gmenuharness/MenuItemMatcher.cpp1008
-rw-r--r--src/gmenuharness/MenuMatcher.cpp208
-rw-r--r--src/media-player-mpris.vala22
-rw-r--r--src/media-player.vala4
-rw-r--r--src/mpris2-interfaces.vala5
-rw-r--r--src/sound-menu.vala84
-rw-r--r--src/volume-control-pulse.vala70
-rw-r--r--tests/CMakeLists.txt202
-rw-r--r--tests/dbus-types/CMakeLists.txt53
-rw-r--r--tests/dbus-types/com.ubuntu.AccountsService.Sound.xml9
-rw-r--r--tests/dbus-types/dbus-types.h48
-rw-r--r--tests/dbus-types/org.PulseAudio.Ext.StreamRestore1.xml7
-rw-r--r--tests/dbus-types/org.freedesktop.Accounts.xml8
-rw-r--r--tests/dbus-types/org.freedesktop.DBus.Properties.xml22
-rw-r--r--tests/dbus-types/org.freedesktop.Notifications.xml47
-rw-r--r--tests/dbus-types/org.gtk.Actions.xml13
-rw-r--r--tests/dbus-types/pulseaudio-volume.cpp156
-rw-r--r--tests/dbus-types/pulseaudio-volume.h69
-rw-r--r--tests/integration/CMakeLists.txt132
-rw-r--r--tests/integration/Copy of test-sound.wavbin0 -> 191989 bytes
-rw-r--r--tests/integration/indicator-sound-test-base.cpp793
-rw-r--r--tests/integration/indicator-sound-test-base.h146
-rw-r--r--tests/integration/main.cpp58
-rw-r--r--tests/integration/test-indicator.cpp948
-rw-r--r--tests/integration/test-sound.wavbin0 -> 7665640 bytes
-rwxr-xr-xtests/integration/touch-stream-restore.table4
-rw-r--r--tests/integration/utils/dbus-pulse-volume.cpp232
-rw-r--r--tests/integration/utils/dbus-pulse-volume.h57
-rw-r--r--tests/integration/utils/get-volume.cpp33
-rw-r--r--tests/integration/utils/set-volume.cpp36
-rw-r--r--tests/media-player-mock.vala6
-rw-r--r--tests/service-mocks/CMakeLists.txt2
-rw-r--r--tests/service-mocks/DBusPropertiesNotifier.cpp41
-rw-r--r--tests/service-mocks/DBusPropertiesNotifier.h48
-rw-r--r--tests/service-mocks/accounts-mock/AccountsDefs.h37
-rw-r--r--tests/service-mocks/accounts-mock/AccountsMock.cpp40
-rw-r--r--tests/service-mocks/accounts-mock/AccountsMock.h50
-rw-r--r--tests/service-mocks/accounts-mock/AccountsServiceSoundMock.cpp48
-rw-r--r--tests/service-mocks/accounts-mock/AccountsServiceSoundMock.h58
-rw-r--r--tests/service-mocks/accounts-mock/CMakeLists.txt42
-rw-r--r--tests/service-mocks/accounts-mock/com.ubuntu.AccountsService.Sound.Mock.xml6
-rw-r--r--tests/service-mocks/accounts-mock/main.cpp63
-rw-r--r--tests/service-mocks/accounts-mock/org.freedesktop.Accounts.Mock.xml13
-rw-r--r--tests/service-mocks/media-player-mpris-mock/CMakeLists.txt63
-rw-r--r--tests/service-mocks/media-player-mpris-mock/MediaPlayerMprisDefs.h37
-rw-r--r--tests/service-mocks/media-player-mpris-mock/MediaPlayerMprisMock.cpp103
-rw-r--r--tests/service-mocks/media-player-mpris-mock/MediaPlayerMprisMock.h77
-rw-r--r--tests/service-mocks/media-player-mpris-mock/applications/testplayer1.desktop21
-rw-r--r--tests/service-mocks/media-player-mpris-mock/main.cpp64
-rw-r--r--tests/service-mocks/media-player-mpris-mock/org.mpris.MediaPlayer2.Player.xml24
-rw-r--r--tests/service-mocks/media-player-mpris-mock/org.mpris.MediaPlayer2.xml6
-rw-r--r--tests/service-mocks/media-player-mpris-mock/player-update.cpp93
-rw-r--r--tests/sound-menu.cc190
64 files changed, 6057 insertions, 208 deletions
diff --git a/debian/control b/debian/control
index 3585713..f170fcf 100644
--- a/debian/control
+++ b/debian/control
@@ -5,11 +5,13 @@ Maintainer: Ubuntu Desktop Team <ubuntu-desktop@lists.ubuntu.com>
XSBC-Original-Maintainer: Conor Curran <conor.curran@canonical.com>
Build-Depends: debhelper (>= 9.0),
cmake,
+ cmake-extras (>= 0.4),
dbus,
dbus-test-runner (>> 14.04.0+14.04.20150120.1),
dh-translations,
gir1.2-accountsservice-1.0,
gnome-common,
+ google-mock (>= 1.6.0+svn437),
gsettings-ubuntu-schemas,
autotools-dev,
valac (>= 0.20),
@@ -18,13 +20,22 @@ Build-Depends: debhelper (>= 9.0),
libgirepository1.0-dev,
libglib2.0-dev (>= 2.22.3),
libgtest-dev,
+ libqtdbusmock1-dev (>= 0.3),
+ libqtdbustest1-dev,
+ libunity-api-dev,
liburl-dispatcher1-dev,
libpulse-dev (>= 1:4.0-0ubuntu21),
libpulse-mainloop-glib0 (>= 0.9.18),
libnotify-dev,
libgee-dev,
libxml2-dev,
+ pulseaudio,
python3-dbusmock,
+ qt5-default,
+ qtbase5-dev,
+ qtbase5-dev-tools,
+ qtdeclarative5-dev,
+ qtdeclarative5-dev-tools,
Standards-Version: 3.9.4
Homepage: https://launchpad.net/indicator-sound
# If you aren't a member of ~indicator-applet-developers but need to upload
diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt
new file mode 100644
index 0000000..9d74e15
--- /dev/null
+++ b/include/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(unity)
diff --git a/include/unity/CMakeLists.txt b/include/unity/CMakeLists.txt
new file mode 100644
index 0000000..ef57b9e
--- /dev/null
+++ b/include/unity/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(gmenuharness)
diff --git a/include/unity/gmenuharness/MatchResult.h b/include/unity/gmenuharness/MatchResult.h
new file mode 100644
index 0000000..8503a98
--- /dev/null
+++ b/include/unity/gmenuharness/MatchResult.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser 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 warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by: Pete Woods <pete.woods@canonical.com>
+ */
+
+#pragma once
+
+#include <vector>
+#include <memory>
+#include <string>
+
+namespace unity
+{
+
+namespace gmenuharness
+{
+
+class MatchResult
+{
+public:
+ MatchResult();
+
+ MatchResult(MatchResult&& other);
+
+ MatchResult(const MatchResult& other);
+
+ MatchResult& operator=(const MatchResult& other);
+
+ MatchResult& operator=(MatchResult&& other);
+
+ ~MatchResult() = default;
+
+ MatchResult createChild() const;
+
+ void failure(const std::vector<unsigned int>& location, const std::string& message);
+
+ void merge(const MatchResult& other);
+
+ bool success() const;
+
+ bool hasTimedOut() const;
+
+ std::string concat_failures() const;
+
+protected:
+ struct Priv;
+
+ std::shared_ptr<Priv> p;
+};
+
+} // namespace gmenuharness
+
+} // namespace unity
diff --git a/include/unity/gmenuharness/MatchUtils.h b/include/unity/gmenuharness/MatchUtils.h
new file mode 100644
index 0000000..899e506
--- /dev/null
+++ b/include/unity/gmenuharness/MatchUtils.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser 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 warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by: Pete Woods <pete.woods@canonical.com>
+ */
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include <gio/gio.h>
+
+namespace unity
+{
+
+namespace gmenuharness
+{
+
+void waitForCore(GObject* obj, const std::string& signalName, unsigned int timeout = 10);
+
+void menuWaitForItems(const std::shared_ptr<GMenuModel>& menu, unsigned int timeout = 10);
+
+void g_object_deleter(gpointer object);
+
+void gvariant_deleter(GVariant* varptr);
+
+} //namespace gmenuharness
+
+} // namespace unity
diff --git a/include/unity/gmenuharness/MenuItemMatcher.h b/include/unity/gmenuharness/MenuItemMatcher.h
new file mode 100644
index 0000000..38a5187
--- /dev/null
+++ b/include/unity/gmenuharness/MenuItemMatcher.h
@@ -0,0 +1,143 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser 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 warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by: Pete Woods <pete.woods@canonical.com>
+ */
+
+#pragma once
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include <gio/gio.h>
+
+namespace unity
+{
+
+namespace gmenuharness
+{
+
+class MatchResult;
+
+class MenuItemMatcher
+{
+public:
+ enum class Mode
+ {
+ all,
+ starts_with,
+ ends_with
+ };
+
+ enum class Type
+ {
+ plain,
+ checkbox,
+ radio
+ };
+
+ static MenuItemMatcher checkbox();
+
+ static MenuItemMatcher radio();
+
+ MenuItemMatcher();
+
+ ~MenuItemMatcher();
+
+ MenuItemMatcher(const MenuItemMatcher& other);
+
+ MenuItemMatcher(MenuItemMatcher&& other);
+
+ MenuItemMatcher& operator=(const MenuItemMatcher& other);
+
+ MenuItemMatcher& operator=(MenuItemMatcher&& other);
+
+ MenuItemMatcher& type(Type type);
+
+ MenuItemMatcher& label(const std::string& label);
+
+ MenuItemMatcher& action(const std::string& action);
+
+ MenuItemMatcher& state_icons(const std::vector<std::string>& state);
+
+ MenuItemMatcher& icon(const std::string& icon);
+
+ MenuItemMatcher& themed_icon(const std::string& iconName, const std::vector<std::string>& icons);
+
+ MenuItemMatcher& widget(const std::string& widget);
+
+ MenuItemMatcher& pass_through_attribute(const std::string& actionName, const std::shared_ptr<GVariant>& value);
+
+ MenuItemMatcher& pass_through_boolean_attribute(const std::string& actionName, bool value);
+
+ MenuItemMatcher& pass_through_string_attribute(const std::string& actionName, const std::string& value);
+
+ MenuItemMatcher& pass_through_double_attribute(const std::string& actionName, double value);
+
+ MenuItemMatcher& round_doubles(double maxDifference);
+
+ MenuItemMatcher& attribute(const std::string& name, const std::shared_ptr<GVariant>& value);
+
+ MenuItemMatcher& boolean_attribute(const std::string& name, bool value);
+
+ MenuItemMatcher& string_attribute(const std::string& name, const std::string& value);
+
+ MenuItemMatcher& int32_attribute(const std::string& name, int value);
+
+ MenuItemMatcher& int64_attribute(const std::string& name, int value);
+
+ MenuItemMatcher& double_attribute(const std::string& name, double value);
+
+ MenuItemMatcher& attribute_not_set(const std::string& name);
+
+ MenuItemMatcher& toggled(bool toggled);
+
+ MenuItemMatcher& mode(Mode mode);
+
+ MenuItemMatcher& submenu();
+
+ MenuItemMatcher& section();
+
+ MenuItemMatcher& is_empty();
+
+ MenuItemMatcher& has_exactly(std::size_t children);
+
+ MenuItemMatcher& item(const MenuItemMatcher& item);
+
+ MenuItemMatcher& item(MenuItemMatcher&& item);
+
+ MenuItemMatcher& pass_through_activate(const std::string& action, const std::shared_ptr<GVariant>& parameter = nullptr);
+
+ MenuItemMatcher& activate(const std::shared_ptr<GVariant>& parameter = nullptr);
+
+ MenuItemMatcher& set_pass_through_action_state(const std::string& action, const std::shared_ptr<GVariant>& state);
+
+ MenuItemMatcher& set_action_state(const std::shared_ptr<GVariant>& state);
+
+ void match(MatchResult& matchResult, const std::vector<unsigned int>& location,
+ const std::shared_ptr<GMenuModel>& menu,
+ std::map<std::string, std::shared_ptr<GActionGroup>>& actions,
+ int index) const;
+
+protected:
+ struct Priv;
+
+ std::shared_ptr<Priv> p;
+};
+
+} // namespace gmenuharness
+
+} // namespace unity
diff --git a/include/unity/gmenuharness/MenuMatcher.h b/include/unity/gmenuharness/MenuMatcher.h
new file mode 100644
index 0000000..c2eb65e
--- /dev/null
+++ b/include/unity/gmenuharness/MenuMatcher.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser 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 warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by: Pete Woods <pete.woods@canonical.com>
+ */
+
+#pragma once
+
+#define EXPECT_MATCHRESULT(statement) \
+do {\
+ auto result = (statement);\
+ GTEST_TEST_BOOLEAN_(result.success(), #statement, false, true, \
+ GTEST_NONFATAL_FAILURE_) << result.concat_failures().c_str(); \
+} while (0)
+
+#include <unity/gmenuharness/MatchResult.h>
+#include <unity/gmenuharness/MenuItemMatcher.h>
+
+#include <memory>
+#include <vector>
+
+namespace unity
+{
+
+namespace gmenuharness
+{
+
+class MenuMatcher
+{
+public:
+ class Parameters
+ {
+ public:
+ Parameters(
+ const std::string& busName,
+ const std::vector<std::pair<std::string, std::string>>& actions,
+ const std::string& menuObjectPath);
+
+ ~Parameters();
+
+ Parameters(const Parameters& other);
+
+ Parameters(Parameters&& other);
+
+ Parameters& operator=(const Parameters& other);
+
+ Parameters& operator=(Parameters&& other);
+
+ protected:
+ friend MenuMatcher;
+
+ struct Priv;
+
+ std::shared_ptr<Priv> p;
+ };
+
+ MenuMatcher(const Parameters& parameters);
+
+ ~MenuMatcher();
+
+ MenuMatcher(const MenuMatcher& other) = delete;
+
+ MenuMatcher(MenuMatcher&& other) = delete;
+
+ MenuMatcher& operator=(const MenuMatcher& other) = delete;
+
+ MenuMatcher& operator=(MenuMatcher&& other) = delete;
+
+ MenuMatcher& item(const MenuItemMatcher& item);
+
+ MatchResult match() const;
+
+ void match(MatchResult& matchResult) const;
+
+protected:
+ struct Priv;
+
+ std::shared_ptr<Priv> p;
+};
+
+} // gmenuharness
+
+} // unity
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a0f458d..73a270c 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -8,12 +8,12 @@ set(SYMBOLS_PATH "${CMAKE_CURRENT_BINARY_DIR}/indicator-sound-service.def")
set(VAPI_PATH "${CMAKE_CURRENT_BINARY_DIR}/indicator-sound-service.vapi")
vapi_gen(accounts-service
- LIBRARY
- accounts-service
- PACKAGES
- gio-2.0
- INPUT
- /usr/share/gir-1.0/AccountsService-1.0.gir
+ LIBRARY
+ accounts-service
+ PACKAGES
+ gio-2.0
+ INPUT
+ /usr/share/gir-1.0/AccountsService-1.0.gir
)
vala_init(indicator-sound-service
@@ -70,7 +70,7 @@ vala_add(indicator-sound-service
media-player-user.vala
DEPENDS
media-player
- accounts-service-sound-settings
+ accounts-service-sound-settings
greeter-broadcast
)
vala_add(indicator-sound-service
@@ -165,8 +165,8 @@ add_definitions(
)
add_library(
- indicator-sound-service-lib STATIC
- ${INDICATOR_SOUND_SOURCES}
+ indicator-sound-service-lib STATIC
+ ${INDICATOR_SOUND_SOURCES}
)
target_link_libraries(
@@ -207,3 +207,4 @@ install(
RUNTIME DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}/indicator-sound/
)
+add_subdirectory(gmenuharness)
diff --git a/src/gmenuharness/CMakeLists.txt b/src/gmenuharness/CMakeLists.txt
new file mode 100644
index 0000000..c9e613a
--- /dev/null
+++ b/src/gmenuharness/CMakeLists.txt
@@ -0,0 +1,17 @@
+pkg_check_modules(UNITY_API libunity-api>=0.1.3 REQUIRED)
+include_directories(${UNITY_API_INCLUDE_DIRS})
+
+include_directories("${CMAKE_SOURCE_DIR}/include")
+
+add_library(
+ gmenuharness-shared SHARED
+ MatchResult.cpp
+ MatchUtils.cpp
+ MenuItemMatcher.cpp
+ MenuMatcher.cpp
+)
+
+target_link_libraries(
+ gmenuharness-shared
+ ${GLIB_LDFLAGS}
+)
diff --git a/src/gmenuharness/MatchResult.cpp b/src/gmenuharness/MatchResult.cpp
new file mode 100644
index 0000000..40629aa
--- /dev/null
+++ b/src/gmenuharness/MatchResult.cpp
@@ -0,0 +1,187 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser 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 warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by: Pete Woods <pete.woods@canonical.com>
+ */
+
+#include <unity/gmenuharness/MatchResult.h>
+
+#include <chrono>
+#include <map>
+#include <sstream>
+#include <iostream>
+
+using namespace std;
+
+namespace unity
+{
+
+namespace gmenuharness
+{
+
+namespace
+{
+
+
+static void printLocation(ostream& ss, const vector<unsigned int>& location, bool first)
+{
+ for (int i : location)
+ {
+ ss << " ";
+ if (first)
+ {
+ ss << i;
+ }
+ else
+ {
+ ss << " ";
+ }
+ }
+ ss << " ";
+}
+
+struct compare_vector
+{
+ bool operator()(const vector<unsigned int>& a,
+ const vector<unsigned int>& b) const
+ {
+ auto p1 = a.begin();
+ auto p2 = b.begin();
+
+ while (p1 != a.end())
+ {
+ if (p2 == b.end())
+ {
+ return false;
+ }
+ if (*p2 > *p1)
+ {
+ return true;
+ }
+ if (*p1 > *p2)
+ {
+ return false;
+ }
+
+ ++p1;
+ ++p2;
+ }
+
+ if (p2 != b.end())
+ {
+ return true;
+ }
+
+ return false;
+ }
+};
+}
+
+struct MatchResult::Priv
+{
+ bool m_success = true;
+
+ map<vector<unsigned int>, vector<string>, compare_vector> m_failures;
+
+ chrono::time_point<chrono::system_clock> m_timeout = chrono::system_clock::now() + chrono::seconds(10);
+};
+
+MatchResult::MatchResult() :
+ p(new Priv)
+{
+}
+
+MatchResult::MatchResult(MatchResult&& other)
+{
+ *this = move(other);
+}
+
+MatchResult::MatchResult(const MatchResult& other) :
+ p(new Priv)
+{
+ *this = other;
+}
+
+MatchResult& MatchResult::operator=(const MatchResult& other)
+{
+ p->m_success = other.p->m_success;
+ p->m_failures= other.p->m_failures;
+ return *this;
+}
+
+MatchResult& MatchResult::operator=(MatchResult&& other)
+{
+ p = move(other.p);
+ return *this;
+}
+
+MatchResult MatchResult::createChild() const
+{
+ MatchResult child;
+ child.p->m_timeout = p->m_timeout;
+ return child;
+}
+
+void MatchResult::failure(const vector<unsigned int>& location, const string& message)
+{
+ p->m_success = false;
+ auto it = p->m_failures.find(location);
+ if (it == p->m_failures.end())
+ {
+ it = p->m_failures.insert(make_pair(location, vector<string>())).first;
+ }
+ it->second.emplace_back(message);
+}
+
+void MatchResult::merge(const MatchResult& other)
+{
+ p->m_success &= other.p->m_success;
+ for (const auto& e : other.p->m_failures)
+ {
+ p->m_failures.insert(make_pair(e.first, e.second));
+ }
+}
+
+bool MatchResult::success() const
+{
+ return p->m_success;
+}
+
+bool MatchResult::hasTimedOut() const
+{
+ auto now = chrono::system_clock::now();
+ return (now >= p->m_timeout);
+}
+
+string MatchResult::concat_failures() const
+{
+ stringstream ss;
+ ss << "Failed expectations:" << endl;
+ for (const auto& failure : p->m_failures)
+ {
+ bool first = true;
+ for (const string& s: failure.second)
+ {
+ printLocation(ss, failure.first, first);
+ first = false;
+ ss << s << endl;
+ }
+ }
+ return ss.str();
+}
+
+} // namespace gmenuharness
+
+} // namespace unity
diff --git a/src/gmenuharness/MatchUtils.cpp b/src/gmenuharness/MatchUtils.cpp
new file mode 100644
index 0000000..7b87a25
--- /dev/null
+++ b/src/gmenuharness/MatchUtils.cpp
@@ -0,0 +1,77 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser 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 warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by: Pete Woods <pete.woods@canonical.com>
+ */
+
+#include <unity/gmenuharness/MatchUtils.h>
+
+#include <unity/util/ResourcePtr.h>
+
+using namespace std;
+namespace util = unity::util;
+
+namespace unity
+{
+
+namespace gmenuharness
+{
+
+void waitForCore (GObject * obj, const string& signalName, unsigned int timeout) {
+ shared_ptr<GMainLoop> loop(g_main_loop_new(nullptr, false), &g_main_loop_unref);
+
+ /* Our two exit criteria */
+ util::ResourcePtr<gulong, function<void(gulong)>> signal(
+ g_signal_connect_swapped(obj, signalName.c_str(),
+ G_CALLBACK(g_main_loop_quit), loop.get()),
+ [obj](gulong s)
+ {
+ g_signal_handler_disconnect(obj, s);
+ });
+
+ util::ResourcePtr<guint, function<void(guint)>> timer(g_timeout_add(timeout,
+ [](gpointer user_data) -> gboolean
+ {
+ g_main_loop_quit((GMainLoop *)user_data);
+ return G_SOURCE_CONTINUE;
+ },
+ loop.get()),
+ &g_source_remove);
+
+ /* Wait for sync */
+ g_main_loop_run(loop.get());
+}
+
+void menuWaitForItems(const shared_ptr<GMenuModel>& menu, unsigned int timeout)
+{
+ waitForCore(G_OBJECT(menu.get()), "items-changed", timeout);
+}
+
+void g_object_deleter(gpointer object)
+{
+ g_clear_object(&object);
+}
+
+void gvariant_deleter(GVariant* varptr)
+{
+ if (varptr != nullptr)
+ {
+ g_variant_unref(varptr);
+ }
+}
+
+} // namespace gmenuharness
+
+} // namespace unity
diff --git a/src/gmenuharness/MenuItemMatcher.cpp b/src/gmenuharness/MenuItemMatcher.cpp
new file mode 100644
index 0000000..f39acef
--- /dev/null
+++ b/src/gmenuharness/MenuItemMatcher.cpp
@@ -0,0 +1,1008 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser 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 warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by: Pete Woods <pete.woods@canonical.com>
+ */
+
+#include <unity/gmenuharness/MatchResult.h>
+#include <unity/gmenuharness/MatchUtils.h>
+#include <unity/gmenuharness/MenuItemMatcher.h>
+
+#include <iostream>
+#include <vector>
+#include <map>
+
+using namespace std;
+
+namespace unity
+{
+
+namespace gmenuharness
+{
+
+namespace
+{
+
+enum class LinkType
+{
+ any,
+ section,
+ submenu
+};
+
+static string bool_to_string(bool value)
+{
+ return value? "true" : "false";
+}
+
+static shared_ptr<GVariant> get_action_group_attribute(const shared_ptr<GActionGroup>& actionGroup, const gchar* attribute)
+{
+ shared_ptr<GVariant> value(
+ g_action_group_get_action_state(actionGroup.get(), attribute),
+ &gvariant_deleter);
+ return value;
+}
+
+static shared_ptr<GVariant> get_attribute(const shared_ptr<GMenuItem> menuItem, const gchar* attribute)
+{
+ shared_ptr<GVariant> value(
+ g_menu_item_get_attribute_value(menuItem.get(), attribute, nullptr),
+ &gvariant_deleter);
+ return value;
+}
+
+static string get_string_attribute(const shared_ptr<GMenuItem> menuItem, const gchar* attribute)
+{
+ string result;
+ char* temp = nullptr;
+ if (g_menu_item_get_attribute(menuItem.get(), attribute, "s", &temp))
+ {
+ result = temp;
+ g_free(temp);
+ }
+ return result;
+}
+
+static pair<string, string> split_action(const string& action)
+{
+ auto index = action.find('.');
+
+ if (index == string::npos)
+ {
+ return make_pair(string(), action);
+ }
+
+ return make_pair(action.substr(0, index), action.substr(index + 1, action.size()));
+}
+
+static string type_to_string(MenuItemMatcher::Type type)
+{
+ switch(type)
+ {
+ case MenuItemMatcher::Type::plain:
+ return "plain";
+ case MenuItemMatcher::Type::checkbox:
+ return "checkbox";
+ case MenuItemMatcher::Type::radio:
+ return "radio";
+ }
+
+ return string();
+}
+}
+
+struct MenuItemMatcher::Priv
+{
+ void all(MatchResult& matchResult, const vector<unsigned int>& location,
+ const shared_ptr<GMenuModel>& menu,
+ map<string, shared_ptr<GActionGroup>>& actions)
+ {
+ int count = g_menu_model_get_n_items(menu.get());
+
+ if (m_items.size() != (unsigned int) count)
+ {
+ matchResult.failure(
+ location,
+ "Expected " + to_string(m_items.size())
+ + " children, but found " + to_string(count));
+ return;
+ }
+
+ for (size_t i = 0; i < m_items.size(); ++i)
+ {
+ const auto& matcher = m_items.at(i);
+ matcher.match(matchResult, location, menu, actions, i);
+ }
+ }
+
+ void startsWith(MatchResult& matchResult, const vector<unsigned int>& location,
+ const shared_ptr<GMenuModel>& menu,
+ map<string, shared_ptr<GActionGroup>>& actions)
+ {
+ int count = g_menu_model_get_n_items(menu.get());
+ if (m_items.size() > (unsigned int) count)
+ {
+ matchResult.failure(
+ location,
+ "Expected at least " + to_string(m_items.size())
+ + " children, but found " + to_string(count));
+ return;
+ }
+
+ for (size_t i = 0; i < m_items.size(); ++i)
+ {
+ const auto& matcher = m_items.at(i);
+ matcher.match(matchResult, location, menu, actions, i);
+ }
+ }
+
+ void endsWith(MatchResult& matchResult, const vector<unsigned int>& location,
+ const shared_ptr<GMenuModel>& menu,
+ map<string, shared_ptr<GActionGroup>>& actions)
+ {
+ int count = g_menu_model_get_n_items(menu.get());
+ if (m_items.size() > (unsigned int) count)
+ {
+ matchResult.failure(
+ location,
+ "Expected at least " + to_string(m_items.size())
+ + " children, but found " + to_string(count));
+ return;
+ }
+
+ // match the last N items
+ size_t j;
+ for (size_t i = count - m_items.size(), j = 0; i < count && j < m_items.size(); ++i, ++j)
+ {
+ const auto& matcher = m_items.at(j);
+ matcher.match(matchResult, location, menu, actions, i);
+ }
+ }
+
+ Type m_type = Type::plain;
+
+ Mode m_mode = Mode::all;
+
+ LinkType m_linkType = LinkType::any;
+
+ shared_ptr<size_t> m_expectedSize;
+
+ shared_ptr<string> m_label;
+
+ shared_ptr<string> m_icon;
+
+ map<shared_ptr<string>, vector<std::string>> m_themed_icons;
+
+ shared_ptr<string> m_action;
+
+ vector<std::string> m_state_icons;
+
+ vector<pair<string, shared_ptr<GVariant>>> m_attributes;
+
+ vector<string> m_not_exist_attributes;
+
+ vector<pair<string, shared_ptr<GVariant>>> m_pass_through_attributes;
+
+ shared_ptr<bool> m_isToggled;
+
+ vector<MenuItemMatcher> m_items;
+
+ vector<pair<string, shared_ptr<GVariant>>> m_activations;
+
+ vector<pair<string, shared_ptr<GVariant>>> m_setActionStates;
+
+ double m_maxDifference = 0.0;
+};
+
+MenuItemMatcher MenuItemMatcher::checkbox()
+{
+ MenuItemMatcher matcher;
+ matcher.type(Type::checkbox);
+ return matcher;
+}
+
+MenuItemMatcher MenuItemMatcher::radio()
+{
+ MenuItemMatcher matcher;
+ matcher.type(Type::radio);
+ return matcher;
+}
+
+MenuItemMatcher::MenuItemMatcher() :
+ p(new Priv)
+{
+}
+
+MenuItemMatcher::~MenuItemMatcher()
+{
+}
+
+MenuItemMatcher::MenuItemMatcher(const MenuItemMatcher& other) :
+ p(new Priv)
+{
+ *this = other;
+}
+
+MenuItemMatcher::MenuItemMatcher(MenuItemMatcher&& other)
+{
+ *this = move(other);
+}
+
+MenuItemMatcher& MenuItemMatcher::operator=(const MenuItemMatcher& other)
+{
+ p->m_type = other.p->m_type;
+ p->m_mode = other.p->m_mode;
+ p->m_expectedSize = other.p->m_expectedSize;
+ p->m_label = other.p->m_label;
+ p->m_icon = other.p->m_icon;
+ p->m_themed_icons = other.p->m_themed_icons;
+ p->m_action = other.p->m_action;
+ p->m_state_icons = other.p->m_state_icons;
+ p->m_attributes = other.p->m_attributes;
+ p->m_not_exist_attributes = other.p->m_not_exist_attributes;
+ p->m_pass_through_attributes = other.p->m_pass_through_attributes;
+ p->m_isToggled = other.p->m_isToggled;
+ p->m_linkType = other.p->m_linkType;
+ p->m_items = other.p->m_items;
+ p->m_activations = other.p->m_activations;
+ p->m_setActionStates = other.p->m_setActionStates;
+ p->m_maxDifference = other.p->m_maxDifference;
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::operator=(MenuItemMatcher&& other)
+{
+ p = move(other.p);
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::type(Type type)
+{
+ p->m_type = type;
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::label(const string& label)
+{
+ p->m_label = make_shared<string>(label);
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::action(const string& action)
+{
+ p->m_action = make_shared<string>(action);
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::state_icons(const std::vector<std::string>& state_icons)
+{
+ p->m_state_icons = state_icons;
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::icon(const string& icon)
+{
+ p->m_icon = make_shared<string>(icon);
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::themed_icon(const std::string& iconName, const std::vector<std::string>& icons)
+{
+ p->m_themed_icons[make_shared<string>(iconName)] = icons;
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::widget(const string& widget)
+{
+ return string_attribute("x-canonical-type", widget);
+}
+
+MenuItemMatcher& MenuItemMatcher::pass_through_attribute(const string& actionName, const shared_ptr<GVariant>& value)
+{
+ p->m_pass_through_attributes.emplace_back(actionName, value);
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::pass_through_boolean_attribute(const string& actionName, bool value)
+{
+ return pass_through_attribute(
+ actionName,
+ shared_ptr<GVariant>(g_variant_new_boolean(value),
+ &gvariant_deleter));
+}
+
+MenuItemMatcher& MenuItemMatcher::pass_through_string_attribute(const string& actionName, const string& value)
+{
+ return pass_through_attribute(
+ actionName,
+ shared_ptr<GVariant>(g_variant_new_string(value.c_str()),
+ &gvariant_deleter));
+}
+
+MenuItemMatcher& MenuItemMatcher::pass_through_double_attribute(const std::string& actionName, double value)
+{
+ return pass_through_attribute(
+ actionName,
+ shared_ptr<GVariant>(g_variant_new_double(value),
+ &gvariant_deleter));
+}
+
+MenuItemMatcher& MenuItemMatcher::round_doubles(double maxDifference)
+{
+ p->m_maxDifference = maxDifference;
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::attribute(const string& name, const shared_ptr<GVariant>& value)
+{
+ p->m_attributes.emplace_back(name, value);
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::boolean_attribute(const string& name, bool value)
+{
+ return attribute(
+ name,
+ shared_ptr<GVariant>(g_variant_new_boolean(value),
+ &gvariant_deleter));
+}
+
+MenuItemMatcher& MenuItemMatcher::string_attribute(const string& name, const string& value)
+{
+ return attribute(
+ name,
+ shared_ptr<GVariant>(g_variant_new_string(value.c_str()),
+ &gvariant_deleter));
+}
+
+MenuItemMatcher& MenuItemMatcher::int32_attribute(const std::string& name, int value)
+{
+ return attribute(
+ name,
+ shared_ptr<GVariant>(g_variant_new_int32 (value),
+ &gvariant_deleter));
+}
+
+MenuItemMatcher& MenuItemMatcher::int64_attribute(const std::string& name, int value)
+{
+ return attribute(
+ name,
+ shared_ptr<GVariant>(g_variant_new_int64 (value),
+ &gvariant_deleter));
+}
+
+MenuItemMatcher& MenuItemMatcher::double_attribute(const std::string& name, double value)
+{
+ return attribute(
+ name,
+ shared_ptr<GVariant>(g_variant_new_double (value),
+ &gvariant_deleter));
+}
+
+MenuItemMatcher& MenuItemMatcher::attribute_not_set(const std::string& name)
+{
+ p->m_not_exist_attributes.emplace_back (name);
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::toggled(bool isToggled)
+{
+ p->m_isToggled = make_shared<bool>(isToggled);
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::mode(Mode mode)
+{
+ p->m_mode = mode;
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::submenu()
+{
+ p->m_linkType = LinkType::submenu;
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::section()
+{
+ p->m_linkType = LinkType::section;
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::is_empty()
+{
+ return has_exactly(0);
+}
+
+MenuItemMatcher& MenuItemMatcher::has_exactly(size_t children)
+{
+ p->m_expectedSize = make_shared<size_t>(children);
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::item(const MenuItemMatcher& item)
+{
+ p->m_items.emplace_back(item);
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::item(MenuItemMatcher&& item)
+{
+ p->m_items.emplace_back(item);
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::pass_through_activate(std::string const& action, const shared_ptr<GVariant>& parameter)
+{
+ p->m_activations.emplace_back(action, parameter);
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::activate(const shared_ptr<GVariant>& parameter)
+{
+ p->m_activations.emplace_back(string(), parameter);
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::set_pass_through_action_state(const std::string& action, const std::shared_ptr<GVariant>& state)
+{
+ p->m_setActionStates.emplace_back(action, state);
+ return *this;
+}
+
+MenuItemMatcher& MenuItemMatcher::set_action_state(const std::shared_ptr<GVariant>& state)
+{
+ p->m_setActionStates.emplace_back("", state);
+ return *this;
+}
+
+void MenuItemMatcher::match(
+ MatchResult& matchResult,
+ const vector<unsigned int>& parentLocation,
+ const shared_ptr<GMenuModel>& menu,
+ map<string, shared_ptr<GActionGroup>>& actions,
+ int index) const
+{
+ shared_ptr<GMenuItem> menuItem(g_menu_item_new_from_model(menu.get(), index), &g_object_deleter);
+
+ vector<unsigned int> location(parentLocation);
+ location.emplace_back(index);
+
+ string action = get_string_attribute(menuItem, G_MENU_ATTRIBUTE_ACTION);
+
+ bool isCheckbox = false;
+ bool isRadio = false;
+ bool isToggled = false;
+
+ pair<string, string> idPair;
+ shared_ptr<GActionGroup> actionGroup;
+ shared_ptr<GVariant> state;
+
+ if (!action.empty())
+ {
+ idPair = split_action(action);
+ actionGroup = actions[idPair.first];
+ state = shared_ptr<GVariant>(g_action_group_get_action_state(actionGroup.get(),
+ idPair.second.c_str()),
+ &gvariant_deleter);
+ auto attributeTarget = get_attribute(menuItem, G_MENU_ATTRIBUTE_TARGET);
+
+ if (attributeTarget && state)
+ {
+ isToggled = g_variant_equal(state.get(), attributeTarget.get());
+ isRadio = true;
+ }
+ else if (state
+ && g_variant_is_of_type(state.get(), G_VARIANT_TYPE_BOOLEAN))
+ {
+ isToggled = g_variant_get_boolean(state.get());
+ isCheckbox = true;
+ }
+ }
+
+ Type actualType = Type::plain;
+ if (isCheckbox)
+ {
+ actualType = Type::checkbox;
+ }
+ else if (isRadio)
+ {
+ actualType = Type::radio;
+ }
+
+ if (actualType != p->m_type)
+ {
+ matchResult.failure(
+ location,
+ "Expected " + type_to_string(p->m_type) + ", found "
+ + type_to_string(actualType));
+ }
+
+ // check themed icons
+ map<shared_ptr<string>, vector<string>>::iterator iter;
+ for (iter = p->m_themed_icons.begin(); iter != p->m_themed_icons.end(); ++iter)
+ {
+ auto icon_val = g_menu_item_get_attribute_value(menuItem.get(), (*iter).first->c_str(), nullptr);
+ if (!icon_val)
+ {
+ matchResult.failure(
+ location,
+ "Expected themed icon " + (*(*iter).first) + " was not found");
+ }
+
+ auto gicon = g_icon_deserialize(icon_val);
+ if (!gicon || !G_IS_THEMED_ICON(gicon))
+ {
+ matchResult.failure(
+ location,
+ "Expected attribute " + (*(*iter).first) + " is not a themed icon");
+ }
+ auto iconNames = g_themed_icon_get_names(G_THEMED_ICON(gicon));
+ int nb_icons = 0;
+ while(iconNames[nb_icons])
+ {
+ ++nb_icons;
+ }
+
+ if (nb_icons != (*iter).second.size())
+ {
+ matchResult.failure(
+ location,
+ "Expected " + to_string((*iter).second.size()) +
+ " icons for themed icon [" + (*(*iter).first) +
+ "], but " + to_string(nb_icons) + " were found.");
+ }
+ else
+ {
+ // now compare all the icons
+ for (int i = 0; i < nb_icons; ++i)
+ {
+ if (string(iconNames[i]) != (*iter).second[i])
+ {
+ matchResult.failure(
+ location,
+ "Icon at position " + to_string(i) +
+ " for themed icon [" + (*(*iter).first) +
+ "], mismatchs. Expected: " + iconNames[i] + " but found " + (*iter).second[i]);
+ }
+ }
+ }
+ g_object_unref(gicon);
+ }
+
+ string label = get_string_attribute(menuItem, G_MENU_ATTRIBUTE_LABEL);
+ if (p->m_label && (*p->m_label) != label)
+ {
+ matchResult.failure(
+ location,
+ "Expected label '" + *p->m_label + "', but found '" + label
+ + "'");
+ }
+
+ string icon = get_string_attribute(menuItem, G_MENU_ATTRIBUTE_ICON);
+ if (p->m_icon && (*p->m_icon) != icon)
+ {
+ matchResult.failure(
+ location,
+ "Expected icon '" + *p->m_icon + "', but found '" + icon + "'");
+ }
+
+ if (p->m_action && (*p->m_action) != action)
+ {
+ matchResult.failure(
+ location,
+ "Expected action '" + *p->m_action + "', but found '" + action
+ + "'");
+ }
+
+ if (!p->m_state_icons.empty() && !state)
+ {
+ matchResult.failure(
+ location,
+ "Expected state icons but no state was found");
+ }
+ else if (!p->m_state_icons.empty() && state &&
+ !g_variant_is_of_type(state.get(), G_VARIANT_TYPE_VARDICT))
+ {
+ matchResult.failure(
+ location,
+ "Expected state icons vardict, found "
+ + type_to_string(actualType));
+ }
+ else if (!p->m_state_icons.empty() && state &&
+ g_variant_is_of_type(state.get(), G_VARIANT_TYPE_VARDICT))
+ {
+ std::vector<std::string> actual_state_icons;
+ GVariantIter it;
+ gchar* key;
+ GVariant* value;
+
+ g_variant_iter_init(&it, state.get());
+ while (g_variant_iter_loop(&it, "{sv}", &key, &value))
+ {
+ if (std::string(key) == "icon") {
+ auto gicon = g_icon_deserialize(value);
+ if (gicon && G_IS_THEMED_ICON(gicon))
+ {
+ auto iconNames = g_themed_icon_get_names(G_THEMED_ICON(gicon));
+ // Just take the first icon in the list (there is only ever one)
+ actual_state_icons.push_back(iconNames[0]);
+ g_object_unref(gicon);
+ }
+ }
+ else if (std::string(key) == "icons" && g_variant_is_of_type(value, G_VARIANT_TYPE("av")))
+ {
+ // If we find "icons" in the map, clear any icons we may have found in "icon",
+ // then break from the loop as we have found all icons now.
+ actual_state_icons.clear();
+ GVariantIter icon_it;
+ GVariant* icon_value;
+
+ g_variant_iter_init(&icon_it, value);
+ while (g_variant_iter_loop(&icon_it, "v", &icon_value))
+ {
+ auto gicon = g_icon_deserialize(icon_value);
+ if (gicon && G_IS_THEMED_ICON(gicon))
+ {
+ auto iconNames = g_themed_icon_get_names(G_THEMED_ICON(gicon));
+ // Just take the first icon in the list (there is only ever one)
+ actual_state_icons.push_back(iconNames[0]);
+ g_object_unref(gicon);
+ }
+ }
+ // We're breaking out of g_variant_iter_loop here so clean up
+ g_variant_unref(value);
+ g_free(key);
+ break;
+ }
+ }
+
+ if (p->m_state_icons != actual_state_icons)
+ {
+ std::string expected_icons;
+ for (unsigned int i = 0; i < p->m_state_icons.size(); ++i)
+ {
+ expected_icons += i == 0 ? p->m_state_icons[i] : ", " + p->m_state_icons[i];
+ }
+ std::string actual_icons;
+ for (unsigned int i = 0; i < actual_state_icons.size(); ++i)
+ {
+ actual_icons += i == 0 ? actual_state_icons[i] : ", " + actual_state_icons[i];
+ }
+ matchResult.failure(
+ location,
+ "Expected state_icons == {" + expected_icons
+ + "} but found {" + actual_icons + "}");
+ }
+ }
+
+ for (const auto& e: p->m_pass_through_attributes)
+ {
+ string actionName = get_string_attribute(menuItem, e.first.c_str());
+ if (actionName.empty())
+ {
+ matchResult.failure(
+ location,
+ "Could not find action name '" + e.first + "'");
+ }
+ else
+ {
+ auto passThroughIdPair = split_action(actionName);
+ auto actionGroup = actions[passThroughIdPair.first];
+ if (actionGroup)
+ {
+ auto value = get_action_group_attribute(
+ actionGroup, passThroughIdPair.second.c_str());
+ if (!value)
+ {
+ matchResult.failure(
+ location,
+ "Expected pass-through attribute '" + e.first
+ + "' was not present");
+ }
+ else if (!g_variant_is_of_type(e.second.get(), g_variant_get_type(value.get())))
+ {
+ std::string expectedType = g_variant_get_type_string(e.second.get());
+ std::string actualType = g_variant_get_type_string(value.get());
+ matchResult.failure(
+ location,
+ "Expected pass-through attribute type '" + expectedType
+ + "' but found '" + actualType + "'");
+ }
+ else if (g_variant_compare(e.second.get(), value.get()))
+ {
+ bool reportMismatch = true;
+ if (g_strcmp0(g_variant_get_type_string(value.get()),"d") == 0 && p->m_maxDifference)
+ {
+ auto actualDouble = g_variant_get_double(value.get());
+ auto expectedDouble = g_variant_get_double(e.second.get());
+ auto difference = actualDouble-expectedDouble;
+ if (difference < 0) difference = difference * -1.0;
+ if (difference <= p->m_maxDifference)
+ {
+ reportMismatch = false;
+ }
+ }
+ if (reportMismatch)
+ {
+ gchar* expectedString = g_variant_print(e.second.get(), true);
+ gchar* actualString = g_variant_print(value.get(), true);
+ matchResult.failure(
+ location,
+ "Expected pass-through attribute '" + e.first
+ + "' == " + expectedString + " but found "
+ + actualString);
+
+ g_free(expectedString);
+ g_free(actualString);
+ }
+ }
+ }
+ else
+ {
+ matchResult.failure(location, "Could not find action group for ID '" + passThroughIdPair.first + "'");
+ }
+ }
+ }
+
+ for (const auto& e: p->m_attributes)
+ {
+ auto value = get_attribute(menuItem, e.first.c_str());
+ if (!value)
+ {
+ matchResult.failure(location,
+ "Expected attribute '" + e.first
+ + "' could not be found");
+ }
+ else if (!g_variant_is_of_type(e.second.get(), g_variant_get_type(value.get())))
+ {
+ std::string expectedType = g_variant_get_type_string(e.second.get());
+ std::string actualType = g_variant_get_type_string(value.get());
+ matchResult.failure(
+ location,
+ "Expected attribute type '" + expectedType
+ + "' but found '" + actualType + "'");
+ }
+ else if (g_variant_compare(e.second.get(), value.get()))
+ {
+ gchar* expectedString = g_variant_print(e.second.get(), true);
+ gchar* actualString = g_variant_print(value.get(), true);
+ matchResult.failure(
+ location,
+ "Expected attribute '" + e.first + "' == " + expectedString
+ + ", but found " + actualString);
+ g_free(expectedString);
+ g_free(actualString);
+ }
+ }
+
+ for (const auto& e: p->m_not_exist_attributes)
+ {
+ auto value = get_attribute(menuItem, e.c_str());
+ if (value)
+ {
+ matchResult.failure(location,
+ "Not expected attribute '" + e
+ + "' was found");
+ }
+ }
+
+ if (p->m_isToggled && (*p->m_isToggled) != isToggled)
+ {
+ matchResult.failure(
+ location,
+ "Expected toggled = " + bool_to_string(*p->m_isToggled)
+ + ", but found " + bool_to_string(isToggled));
+ }
+
+ if (!matchResult.success())
+ {
+ return;
+ }
+
+ if (!p->m_items.empty() || p->m_expectedSize)
+ {
+ shared_ptr<GMenuModel> link;
+
+ switch (p->m_linkType)
+ {
+ case LinkType::any:
+ {
+ link.reset(g_menu_model_get_item_link(menu.get(), (int) index, G_MENU_LINK_SUBMENU), &g_object_deleter);
+ if (!link)
+ {
+ link.reset(g_menu_model_get_item_link(menu.get(), (int) index, G_MENU_LINK_SECTION), &g_object_deleter);
+ }
+ break;
+ }
+ case LinkType::submenu:
+ {
+ link.reset(g_menu_model_get_item_link(menu.get(), (int) index, G_MENU_LINK_SUBMENU), &g_object_deleter);
+ break;
+ }
+ case LinkType::section:
+ {
+ link.reset(g_menu_model_get_item_link(menu.get(), (int) index, G_MENU_LINK_SECTION), &g_object_deleter);
+ break;
+ }
+ }
+
+
+ if (!link)
+ {
+ if (p->m_expectedSize)
+ {
+ matchResult.failure(
+ location,
+ "Expected " + to_string(*p->m_expectedSize)
+ + " children, but found none");
+ }
+ else
+ {
+ matchResult.failure(
+ location,
+ "Expected " + to_string(p->m_items.size())
+ + " children, but found none");
+ }
+ return;
+ }
+ else
+ {
+ while (true)
+ {
+ MatchResult childMatchResult(matchResult.createChild());
+
+ if (p->m_expectedSize
+ && *p->m_expectedSize
+ != (unsigned int) g_menu_model_get_n_items(
+ link.get()))
+ {
+ childMatchResult.failure(
+ location,
+ "Expected " + to_string(*p->m_expectedSize)
+ + " child items, but found "
+ + to_string(
+ g_menu_model_get_n_items(
+ link.get())));
+ }
+ else if (!p->m_items.empty())
+ {
+ switch (p->m_mode)
+ {
+ case Mode::all:
+ p->all(childMatchResult, location, link, actions);
+ break;
+ case Mode::starts_with:
+ p->startsWith(childMatchResult, location, link, actions);
+ break;
+ case Mode::ends_with:
+ p->endsWith(childMatchResult, location, link, actions);
+ break;
+ }
+ }
+
+ if (childMatchResult.success())
+ {
+ matchResult.merge(childMatchResult);
+ break;
+ }
+ else
+ {
+ if (matchResult.hasTimedOut())
+ {
+ matchResult.merge(childMatchResult);
+ break;
+ }
+ menuWaitForItems(link);
+ }
+ }
+ }
+ }
+
+
+ for (const auto& a: p->m_setActionStates)
+ {
+ auto stateAction = action;
+ auto stateIdPair = idPair;
+ auto stateActionGroup = actionGroup;
+ if (!a.first.empty())
+ {
+ stateAction = get_string_attribute(menuItem, a.first.c_str());;
+ stateIdPair = split_action(stateAction);
+ stateActionGroup = actions[stateIdPair.first];
+ }
+
+ if (stateAction.empty())
+ {
+ matchResult.failure(
+ location,
+ "Tried to set action state, but no action was found");
+ }
+ else if(!stateActionGroup)
+ {
+ matchResult.failure(
+ location,
+ "Tried to set action state for action group '" + stateIdPair.first
+ + "', but action group wasn't found");
+ }
+ else if (!g_action_group_has_action(stateActionGroup.get(), stateIdPair.second.c_str()))
+ {
+ matchResult.failure(
+ location,
+ "Tried to set action state for action '" + stateAction
+ + "', but action was not found");
+ }
+ else
+ {
+ g_action_group_change_action_state(stateActionGroup.get(), stateIdPair.second.c_str(),
+ g_variant_ref(a.second.get()));
+ }
+
+ // FIXME this is a dodgy way to ensure the action state change gets dispatched
+ menuWaitForItems(menu, 100);
+ }
+
+ for (const auto& a: p->m_activations)
+ {
+ string tmpAction = action;
+ auto tmpIdPair = idPair;
+ auto tmpActionGroup = actionGroup;
+ if (!a.first.empty())
+ {
+ tmpAction = get_string_attribute(menuItem, a.first.c_str());
+ tmpIdPair = split_action(tmpAction);
+ tmpActionGroup = actions[tmpIdPair.first];
+ }
+
+ if (tmpAction.empty())
+ {
+ matchResult.failure(
+ location,
+ "Tried to activate action, but no action was found");
+ }
+ else if(!tmpActionGroup)
+ {
+ matchResult.failure(
+ location,
+ "Tried to activate action group '" + tmpIdPair.first
+ + "', but action group wasn't found");
+ }
+ else if (!g_action_group_has_action(tmpActionGroup.get(), tmpIdPair.second.c_str()))
+ {
+ matchResult.failure(
+ location,
+ "Tried to activate action '" + tmpAction + "', but action was not found");
+ }
+ else
+ {
+ if (a.second)
+ {
+ g_action_group_activate_action(tmpActionGroup.get(), tmpIdPair.second.c_str(),
+ g_variant_ref(a.second.get()));
+ }
+ else
+ {
+ g_action_group_activate_action(tmpActionGroup.get(), tmpIdPair.second.c_str(), nullptr);
+ }
+
+ // FIXME this is a dodgy way to ensure the activation gets dispatched
+ menuWaitForItems(menu, 100);
+ }
+ }
+}
+
+} // namepsace gmenuharness
+
+} // namespace unity
diff --git a/src/gmenuharness/MenuMatcher.cpp b/src/gmenuharness/MenuMatcher.cpp
new file mode 100644
index 0000000..5bb4fbd
--- /dev/null
+++ b/src/gmenuharness/MenuMatcher.cpp
@@ -0,0 +1,208 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser 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 warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authored by: Pete Woods <pete.woods@canonical.com>
+ */
+
+#include <unity/gmenuharness/MenuMatcher.h>
+#include <unity/gmenuharness/MatchUtils.h>
+
+#include <iostream>
+
+#include <gio/gio.h>
+
+using namespace std;
+
+namespace unity
+{
+
+namespace gmenuharness
+{
+
+namespace
+{
+
+static void gdbus_connection_deleter(GDBusConnection* connection)
+{
+// if (!g_dbus_connection_is_closed(connection))
+// {
+// g_dbus_connection_close_sync(connection, nullptr, nullptr);
+// }
+ g_clear_object(&connection);
+}
+}
+
+struct MenuMatcher::Parameters::Priv
+{
+ string m_busName;
+
+ vector<pair<string, string>> m_actions;
+
+ string m_menuObjectPath;
+};
+
+MenuMatcher::Parameters::Parameters(const string& busName,
+ const vector<pair<string, string>>& actions,
+ const string& menuObjectPath) :
+ p(new Priv)
+{
+ p->m_busName = busName;
+ p->m_actions = actions;
+ p->m_menuObjectPath = menuObjectPath;
+}
+
+MenuMatcher::Parameters::~Parameters()
+{
+}
+
+MenuMatcher::Parameters::Parameters(const Parameters& other) :
+ p(new Priv)
+{
+ *this = other;
+}
+
+MenuMatcher::Parameters::Parameters(Parameters&& other)
+{
+ *this = move(other);
+}
+
+MenuMatcher::Parameters& MenuMatcher::Parameters::operator=(const Parameters& other)
+{
+ p->m_busName = other.p->m_busName;
+ p->m_actions = other.p->m_actions;
+ p->m_menuObjectPath = other.p->m_menuObjectPath;
+ return *this;
+}
+
+MenuMatcher::Parameters& MenuMatcher::Parameters::operator=(Parameters&& other)
+{
+ p = move(other.p);
+ return *this;
+}
+
+struct MenuMatcher::Priv
+{
+ Priv(const Parameters& parameters) :
+ m_parameters(parameters)
+ {
+ }
+
+ Parameters m_parameters;
+
+ vector<MenuItemMatcher> m_items;
+
+ shared_ptr<GDBusConnection> m_system;
+
+ shared_ptr<GDBusConnection> m_session;
+
+ shared_ptr<GMenuModel> m_menu;
+
+ map<string, shared_ptr<GActionGroup>> m_actions;
+};
+
+MenuMatcher::MenuMatcher(const Parameters& parameters) :
+ p(new Priv(parameters))
+{
+ p->m_system.reset(g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, nullptr),
+ &gdbus_connection_deleter);
+ g_dbus_connection_set_exit_on_close(p->m_system.get(), false);
+
+ p->m_session.reset(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr),
+ &gdbus_connection_deleter);
+ g_dbus_connection_set_exit_on_close(p->m_session.get(), false);
+
+ p->m_menu.reset(
+ G_MENU_MODEL(
+ g_dbus_menu_model_get(
+ p->m_session.get(),
+ p->m_parameters.p->m_busName.c_str(),
+ p->m_parameters.p->m_menuObjectPath.c_str())),
+ &g_object_deleter);
+
+ for (const auto& action : p->m_parameters.p->m_actions)
+ {
+ shared_ptr<GActionGroup> actionGroup(
+ G_ACTION_GROUP(
+ g_dbus_action_group_get(
+ p->m_session.get(),
+ p->m_parameters.p->m_busName.c_str(),
+ action.second.c_str())),
+ &g_object_deleter);
+ p->m_actions[action.first] = actionGroup;
+ }
+}
+
+MenuMatcher::~MenuMatcher()
+{
+}
+
+MenuMatcher& MenuMatcher::item(const MenuItemMatcher& item)
+{
+ p->m_items.emplace_back(item);
+ return *this;
+}
+
+void MenuMatcher::match(MatchResult& matchResult) const
+{
+ vector<unsigned int> location;
+
+ while (true)
+ {
+ MatchResult childMatchResult(matchResult.createChild());
+
+ int menuSize = g_menu_model_get_n_items(p->m_menu.get());
+ if (p->m_items.size() > (unsigned int) menuSize)
+ {
+ childMatchResult.failure(
+ location,
+ "Row count mismatch, expected " + to_string(p->m_items.size())
+ + " but found " + to_string(menuSize));
+ }
+ else
+ {
+ for (size_t i = 0; i < p->m_items.size(); ++i)
+ {
+ const auto& matcher = p->m_items.at(i);
+ matcher.match(childMatchResult, location, p->m_menu, p->m_actions, i);
+ }
+ }
+
+ if (childMatchResult.success())
+ {
+ matchResult.merge(childMatchResult);
+ break;
+ }
+ else
+ {
+ if (matchResult.hasTimedOut())
+ {
+ matchResult.merge(childMatchResult);
+ break;
+ }
+ menuWaitForItems(p->m_menu);
+ }
+ }
+}
+
+MatchResult MenuMatcher::match() const
+{
+ MatchResult matchResult;
+ match(matchResult);
+ return matchResult;
+}
+
+} // namespace gmenuharness
+
+} // namespace unity
diff --git a/src/media-player-mpris.vala b/src/media-player-mpris.vala
index 741d887..780d724 100644
--- a/src/media-player-mpris.vala
+++ b/src/media-player-mpris.vala
@@ -79,6 +79,24 @@ public class MediaPlayerMpris: MediaPlayer {
}
}
+ public override bool can_do_play {
+ get {
+ return this.proxy.CanPlay;
+ }
+ }
+
+ public override bool can_do_prev {
+ get {
+ return this.proxy.CanGoPrevious;
+ }
+ }
+
+ public override bool can_do_next {
+ get {
+ return this.proxy.CanGoNext;
+ }
+ }
+
/**
* Attach this object to a process of the associated media player. The player must own @dbus_name and
* implement the org.mpris.MediaPlayer2.Player interface.
@@ -272,6 +290,10 @@ public class MediaPlayerMpris: MediaPlayer {
if (changed_properties.lookup ("PlaybackStatus", "s", null)) {
this.state = this.proxy.PlaybackStatus != null ? this.proxy.PlaybackStatus : "Unknown";
}
+ if (changed_properties.lookup ("CanGoNext", "b", null) || changed_properties.lookup ("CanGoPrevious", "b", null) ||
+ changed_properties.lookup ("CanPlay", "b", null) || changed_properties.lookup ("CanPause", "b", null) ) {
+ this.playbackstatus_changed ();
+ }
var metadata = changed_properties.lookup_value ("Metadata", new VariantType ("a{sv}"));
if (metadata != null)
diff --git a/src/media-player.vala b/src/media-player.vala
index 4d4aef3..04d1426 100644
--- a/src/media-player.vala
+++ b/src/media-player.vala
@@ -26,6 +26,9 @@ public abstract class MediaPlayer : Object {
public virtual bool is_running { get { not_implemented(); return false; } }
public virtual bool can_raise { get { not_implemented(); return false; } }
+ public virtual bool can_do_next { get { not_implemented(); return false; } }
+ public virtual bool can_do_prev { get { not_implemented(); return false; } }
+ public virtual bool can_do_play { get { not_implemented(); return false; } }
public class Track : Object {
public string artist { get; construct; }
@@ -44,6 +47,7 @@ public abstract class MediaPlayer : Object {
}
public signal void playlists_changed ();
+ public signal void playbackstatus_changed ();
public abstract void activate ();
public abstract void play_pause ();
diff --git a/src/mpris2-interfaces.vala b/src/mpris2-interfaces.vala
index a472d5c..0ed8719 100644
--- a/src/mpris2-interfaces.vala
+++ b/src/mpris2-interfaces.vala
@@ -37,7 +37,10 @@ public interface MprisPlayer : Object {
// properties
public abstract HashTable<string, Variant?> Metadata{owned get; set;}
public abstract int32 Position{owned get; set;}
- public abstract string? PlaybackStatus{owned get; set;}
+ public abstract string? PlaybackStatus{owned get; set;}
+ public abstract bool CanPlay{owned get; set;}
+ public abstract bool CanGoNext{owned get; set;}
+ public abstract bool CanGoPrevious{owned get; set;}
// methods
public abstract async void PlayPause() throws IOError;
public abstract async void Next() throws IOError;
diff --git a/src/sound-menu.vala b/src/sound-menu.vala
index b4e3e2a..b612264 100644
--- a/src/sound-menu.vala
+++ b/src/sound-menu.vala
@@ -158,18 +158,26 @@ public class SoundMenu: Object
this.update_playlists (player);
var handler_id = player.notify["is-running"].connect ( () => {
- if (player.is_running)
- if (this.find_player_section(player) == -1)
+ if (player.is_running) {
+ int index = this.find_player_section(player);
+ if (index == -1) {
this.insert_player_section (player);
- else
+ }
+ else {
+ update_player_section (player, index);
+ }
+ }
+ else {
if (this.hide_inactive)
this.remove_player_section (player);
+ }
this.update_playlists (player);
});
this.notify_handlers.insert (player, handler_id);
player.playlists_changed.connect (this.update_playlists);
+ player.playbackstatus_changed.connect (this.update_playbackstatus);
}
public void remove_player (MediaPlayer player) {
@@ -197,9 +205,24 @@ public class SoundMenu: Object
case VolumeControl.ActiveOutput.HEADPHONES:
label = "Volume (Headphones)";
break;
- case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES:
+ case VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER:
label = "Volume (Bluetooth)";
break;
+ case VolumeControl.ActiveOutput.USB_SPEAKER:
+ label = "Volume (Usb)";
+ break;
+ case VolumeControl.ActiveOutput.HDMI_SPEAKER:
+ label = "Volume (HDMI)";
+ break;
+ case VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES:
+ label = "Volume (Bluetooth headphones)";
+ break;
+ case VolumeControl.ActiveOutput.USB_HEADPHONES:
+ label = "Volume (Usb headphones)";
+ break;
+ case VolumeControl.ActiveOutput.HDMI_HEADPHONES:
+ label = "Volume (HDMI headphones)";
+ break;
}
this.volume_section.remove (index);
this.volume_section.insert_item (index, this.create_slider_menu_item (_(label), "indicator.volume(0)", 0.0, 1.0, 0.01,
@@ -238,6 +261,23 @@ public class SoundMenu: Object
return -1;
}
+ MenuItem create_playback_menu_item (MediaPlayer player) {
+ var playback_item = new MenuItem (null, null);
+ playback_item.set_attribute ("x-canonical-type", "s", "com.canonical.unity.playback-item");
+ if (player.is_running) {
+ if (player.can_do_play) {
+ playback_item.set_attribute ("x-canonical-play-action", "s", "indicator.play." + player.id);
+ }
+ if (player.can_do_next) {
+ playback_item.set_attribute ("x-canonical-next-action", "s", "indicator.next." + player.id);
+ }
+ if (player.can_do_prev) {
+ playback_item.set_attribute ("x-canonical-previous-action", "s", "indicator.previous." + player.id);
+ }
+ }
+ return playback_item;
+ }
+
void insert_player_section (MediaPlayer player) {
if (this.hide_players)
return;
@@ -263,9 +303,21 @@ public class SoundMenu: Object
var playback_item = new MenuItem (null, null);
playback_item.set_attribute ("x-canonical-type", "s", "com.canonical.unity.playback-item");
- playback_item.set_attribute ("x-canonical-play-action", "s", "indicator.play." + player.id);
- playback_item.set_attribute ("x-canonical-next-action", "s", "indicator.next." + player.id);
- playback_item.set_attribute ("x-canonical-previous-action", "s", "indicator.previous." + player.id);
+ playback_item.set_attribute ("x-canonical-play-action", "s", "indicator.play." + player.id + ".disabled");
+ playback_item.set_attribute ("x-canonical-next-action", "s", "indicator.next." + player.id + ".disabled");
+ playback_item.set_attribute ("x-canonical-previous-action", "s", "indicator.previous." + player.id + ".disabled");
+
+ if (player.is_running) {
+ if (player.can_do_play) {
+ playback_item.set_attribute ("x-canonical-play-action", "s", "indicator.play." + player.id);
+ }
+ if (player.can_do_next) {
+ playback_item.set_attribute ("x-canonical-next-action", "s", "indicator.next." + player.id);
+ }
+ if (player.can_do_prev) {
+ playback_item.set_attribute ("x-canonical-previous-action", "s", "indicator.previous." + player.id);
+ }
+ }
section.append_item (playback_item);
/* Add new players to the end of the player sections, just before the settings */
@@ -285,6 +337,17 @@ public class SoundMenu: Object
this.menu.remove (index);
}
+ void update_player_section (MediaPlayer player, int index) {
+ var player_section = this.menu.get_item_link(index, Menu.LINK_SECTION) as Menu;
+ if (player_section.get_n_items () == 2) {
+ // we have 2 items, the second one is the playback item
+ // remove it first
+ player_section.remove (1);
+ MenuItem playback_item = create_playback_menu_item (player);
+ player_section.append_item (playback_item);
+ }
+ }
+
void update_playlists (MediaPlayer player) {
int index = find_player_section (player);
if (index < 0)
@@ -315,6 +378,13 @@ public class SoundMenu: Object
submenu.append_section (null, playlists_section);
player_section.append_submenu (_("Choose Playlist"), submenu);
}
+
+ void update_playbackstatus (MediaPlayer player) {
+ int index = find_player_section (player);
+ if (index != -1) {
+ update_player_section (player, index);
+ }
+ }
MenuItem create_slider_menu_item (string label, string action, double min, double max, double step, string min_icon_name, string max_icon_name) {
var min_icon = new ThemedIcon.with_default_fallbacks (min_icon_name);
diff --git a/src/volume-control-pulse.vala b/src/volume-control-pulse.vala
index a1f743b..8122f26 100644
--- a/src/volume-control-pulse.vala
+++ b/src/volume-control-pulse.vala
@@ -144,41 +144,38 @@ public class VolumeControlPulse : VolumeControl
* checking for the port name. On touch (with the pulseaudio droid element)
* the headset/headphone port is called 'output-headset' and 'output-headphone'.
* On the desktop this is usually called 'analog-output-headphones' */
- if (sink.active_port != null) {
- // look if it's a headset/headphones
- if (sink.active_port.name.contains("headset") ||
- sink.active_port.name.contains("headphone")) {
- _active_port_headphone = true;
- // check if it's a bluetooth device
- var device_bus = sink.proplist.gets ("device.bus");
- if (device_bus != null && device_bus == "bluetooth") {
- ret_output = VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES;
- } else if (device_bus != null && device_bus == "usb") {
- ret_output = VolumeControl.ActiveOutput.USB_HEADPHONES;
- } else if (device_bus != null && device_bus == "hdmi") {
- ret_output = VolumeControl.ActiveOutput.HDMI_HEADPHONES;
- } else {
- ret_output = VolumeControl.ActiveOutput.HEADPHONES;
- }
+ // look if it's a headset/headphones
+ if (sink.name == "indicator_sound_test_headphones" ||
+ (sink.active_port != null &&
+ (sink.active_port.name.contains("headset") ||
+ sink.active_port.name.contains("headphone")))) {
+ _active_port_headphone = true;
+ // check if it's a bluetooth device
+ var device_bus = sink.proplist.gets ("device.bus");
+ if (device_bus != null && device_bus == "bluetooth") {
+ ret_output = VolumeControl.ActiveOutput.BLUETOOTH_HEADPHONES;
+ } else if (device_bus != null && device_bus == "usb") {
+ ret_output = VolumeControl.ActiveOutput.USB_HEADPHONES;
+ } else if (device_bus != null && device_bus == "hdmi") {
+ ret_output = VolumeControl.ActiveOutput.HDMI_HEADPHONES;
} else {
- // speaker
- _active_port_headphone = false;
- var device_bus = sink.proplist.gets ("device.bus");
- if (device_bus != null && device_bus == "bluetooth") {
- ret_output = VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER;
- } else if (device_bus != null && device_bus == "usb") {
- ret_output = VolumeControl.ActiveOutput.USB_SPEAKER;
- } else if (device_bus != null && device_bus == "hdmi") {
- ret_output = VolumeControl.ActiveOutput.HDMI_SPEAKER;
- } else {
- ret_output = VolumeControl.ActiveOutput.SPEAKERS;
- }
- }
- } else {
- _active_port_headphone = false;
- ret_output = VolumeControl.ActiveOutput.SPEAKERS;
- }
-
+ ret_output = VolumeControl.ActiveOutput.HEADPHONES;
+ }
+ } else {
+ // speaker
+ _active_port_headphone = false;
+ var device_bus = sink.proplist.gets ("device.bus");
+ if (device_bus != null && device_bus == "bluetooth") {
+ ret_output = VolumeControl.ActiveOutput.BLUETOOTH_SPEAKER;
+ } else if (device_bus != null && device_bus == "usb") {
+ ret_output = VolumeControl.ActiveOutput.USB_SPEAKER;
+ } else if (device_bus != null && device_bus == "hdmi") {
+ ret_output = VolumeControl.ActiveOutput.HDMI_SPEAKER;
+ } else {
+ ret_output = VolumeControl.ActiveOutput.SPEAKERS;
+ }
+ }
+
return ret_output;
}
@@ -527,7 +524,8 @@ public class VolumeControlPulse : VolumeControl
this.context = new PulseAudio.Context (loop.get_api(), null, props);
this.context.set_state_callback (context_state_callback);
- if (context.connect(null, Context.Flags.NOFAIL, null) < 0)
+ var server_string = Environment.get_variable("PULSE_SERVER");
+ if (context.connect(server_string, Context.Flags.NOFAIL, null) < 0)
warning( "pa_context_connect() failed: %s\n", PulseAudio.strerror(context.errno()));
}
@@ -767,7 +765,7 @@ public class VolumeControlPulse : VolumeControl
private bool calculate_high_volume_from_volume(double volume) {
return _active_port_headphone
&& _warning_volume_enabled
- && volume >= _warning_volume_norms
+ && volume > _warning_volume_norms
&& (stream == "multimedia");
}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 2bbd8c5..adc08de 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -5,10 +5,10 @@
include_directories(${GTEST_INCLUDE_DIR})
-add_library (gtest STATIC
+add_library (gtest-static STATIC
${GTEST_SOURCE_DIR}/gtest-all.cc
${GTEST_SOURCE_DIR}/gtest_main.cc)
-target_link_libraries(gtest ${GTEST_LIBS})
+target_link_libraries(gtest-static ${GTEST_LIBS})
###########################
# GSettings Schema
@@ -22,7 +22,8 @@ set_source_files_properties (gschemas.compiled GENERATED)
# GSettings:
# compile the indicator-sound schema into a gschemas.compiled file in this directory,
# and help the tests to find that file by setting -DSCHEMA_DIR
-set (SCHEMA_DIR "${CMAKE_CURRENT_BINARY_DIR}/gsettings-schemas")
+set (XDG_DATA_DIRS "${CMAKE_CURRENT_BINARY_DIR}/gsettings-schemas")
+set (SCHEMA_DIR "${XDG_DATA_DIRS}/glib-2.0/schemas")
add_definitions(-DSCHEMA_DIR="${SCHEMA_DIR}")
execute_process (COMMAND ${PKG_CONFIG_EXECUTABLE} gio-2.0 --variable glib_compile_schemas
OUTPUT_VARIABLE COMPILE_SCHEMA_EXECUTABLE
@@ -31,6 +32,7 @@ add_custom_command (OUTPUT gschemas.compiled
DEPENDS ${CMAKE_SOURCE_DIR}/data/com.canonical.indicator.sound.gschema.xml
COMMAND mkdir -p ${SCHEMA_DIR}
COMMAND cp -f ${CMAKE_SOURCE_DIR}/data/*gschema.xml ${SCHEMA_DIR}
+ COMMAND cp -f /usr/share/glib-2.0/schemas/com.ubuntu.sound.gschema.xml ${SCHEMA_DIR}
COMMAND ${COMPILE_SCHEMA_EXECUTABLE} ${SCHEMA_DIR})
###########################
@@ -41,74 +43,74 @@ set(VALA_MOCKS_HEADER_PATH "${CMAKE_CURRENT_BINARY_DIR}/vala-mocks.h")
set(VALA_MOCKS_SYMBOLS_PATH "${CMAKE_CURRENT_BINARY_DIR}/vala-mocks.def")
vala_init(vala-mocks
- DEPENDS
- indicator-sound-service-lib
- PACKAGES
- config
- gio-2.0
- gio-unix-2.0
- libxml-2.0
- libpulse
- libpulse-mainloop-glib
- libnotify
- accounts-service
- indicator-sound-service
- OPTIONS
- --ccode
- --thread
- --vapidir=${CMAKE_BINARY_DIR}/src/
- --vapidir=${CMAKE_SOURCE_DIR}/vapi/
- --vapidir=.
+ DEPENDS
+ indicator-sound-service-lib
+ PACKAGES
+ config
+ gio-2.0
+ gio-unix-2.0
+ libxml-2.0
+ libpulse
+ libpulse-mainloop-glib
+ libnotify
+ accounts-service
+ indicator-sound-service
+ OPTIONS
+ --ccode
+ --thread
+ --vapidir=${CMAKE_BINARY_DIR}/src/
+ --vapidir=${CMAKE_SOURCE_DIR}/vapi/
+ --vapidir=.
)
vala_add(vala-mocks
- media-player-mock.vala
+ media-player-mock.vala
)
vala_add(vala-mocks
- media-player-list-mock.vala
+ media-player-list-mock.vala
)
vala_add(vala-mocks
- volume-control-mock.vala
+ volume-control-mock.vala
)
vala_finish(vala-mocks
- SOURCES
- vala_mocks_VALA_SOURCES
- OUTPUTS
- vala_mocks_VALA_C
- GENERATE_HEADER
- ${VALA_MOCKS_HEADER_PATH}
- GENERATE_SYMBOLS
- ${VALA_MOCKS_SYMBOLS_PATH}
+ SOURCES
+ vala_mocks_VALA_SOURCES
+ OUTPUTS
+ vala_mocks_VALA_C
+ GENERATE_HEADER
+ ${VALA_MOCKS_HEADER_PATH}
+ GENERATE_SYMBOLS
+ ${VALA_MOCKS_SYMBOLS_PATH}
)
set_source_files_properties(
- ${vala_mocks_VALA_SOURCES}
- PROPERTIES
- HEADER_FILE_ONLY TRUE
+ ${vala_mocks_VALA_SOURCES}
+ PROPERTIES
+ HEADER_FILE_ONLY TRUE
)
set(
- VALA_MOCKS_SOURCES
- ${vala_mocks_VALA_SOURCES}
- ${vala_mocks_VALA_C}
- ${VALA_MOCKS_SYMBOLS_PATH}
+ VALA_MOCKS_SOURCES
+ ${vala_mocks_VALA_SOURCES}
+ ${vala_mocks_VALA_C}
+ ${VALA_MOCKS_SYMBOLS_PATH}
)
add_definitions(
- -Wno-unused-but-set-variable
+ -Wno-unused-but-set-variable
)
add_library(
- vala-mocks-lib STATIC
- ${VALA_MOCKS_SOURCES}
+ vala-mocks-lib STATIC
+ ${VALA_MOCKS_SOURCES}
)
target_link_libraries(
- vala-mocks-lib
- indicator-sound-service-lib
+ vala-mocks-lib
+ indicator-sound-service-lib
)
include_directories(${CMAKE_CURRENT_BINARY_DIR})
@@ -118,9 +120,9 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR})
###########################
add_library(
- pulse-mock
- SHARED
- pa-mock.cpp
+ pulse-mock
+ SHARED
+ pa-mock.cpp
)
target_link_libraries (pulse-mock ${PULSEAUDIO_LIBRARIES})
@@ -131,7 +133,7 @@ target_link_libraries (pulse-mock ${PULSEAUDIO_LIBRARIES})
include_directories(${CMAKE_SOURCE_DIR}/src)
add_executable (name-watch-test name-watch-test.cc ${CMAKE_SOURCE_DIR}/src/bus-watch-namespace.c)
-target_link_libraries (name-watch-test gtest ${SOUNDSERVICE_LIBRARIES})
+target_link_libraries (name-watch-test gtest-static ${SOUNDSERVICE_LIBRARIES})
add_test(name-watch-test name-watch-test)
###########################
@@ -141,21 +143,21 @@ add_test(name-watch-test name-watch-test)
include_directories(${CMAKE_SOURCE_DIR}/src)
add_executable (accounts-service-user-test accounts-service-user.cc)
target_link_libraries (
- accounts-service-user-test
- indicator-sound-service-lib
- vala-mocks-lib
- gtest
- ${SOUNDSERVICE_LIBRARIES}
- ${TEST_LIBRARIES}
+ accounts-service-user-test
+ indicator-sound-service-lib
+ vala-mocks-lib
+ gtest-static
+ ${SOUNDSERVICE_LIBRARIES}
+ ${TEST_LIBRARIES}
)
# Split tests to work around libaccountservice sucking
add_test(accounts-service-user-test-basic
- accounts-service-user-test --gtest_filter=AccountsServiceUserTest.BasicObject
+ accounts-service-user-test --gtest_filter=AccountsServiceUserTest.BasicObject
)
add_test(accounts-service-user-test-player
- accounts-service-user-test --gtest_filter=AccountsServiceUserTest.SetMediaPlayer
+ accounts-service-user-test --gtest_filter=AccountsServiceUserTest.SetMediaPlayer
)
###########################
@@ -165,11 +167,11 @@ add_test(accounts-service-user-test-player
include_directories(${CMAKE_SOURCE_DIR}/src)
add_executable (volume-control-test volume-control-test.cc gschemas.compiled)
target_link_libraries (
- volume-control-test
- indicator-sound-service-lib
- pulse-mock
- gtest
- ${TEST_LIBRARIES}
+ volume-control-test
+ indicator-sound-service-lib
+ pulse-mock
+ gtest-static
+ ${TEST_LIBRARIES}
)
add_test(volume-control-test volume-control-test)
@@ -181,12 +183,12 @@ add_test(volume-control-test volume-control-test)
include_directories(${CMAKE_SOURCE_DIR}/src)
add_executable (sound-menu-test sound-menu.cc)
target_link_libraries (
- sound-menu-test
- indicator-sound-service-lib
- vala-mocks-lib
- gtest
- ${SOUNDSERVICE_LIBRARIES}
- ${TEST_LIBRARIES}
+ sound-menu-test
+ indicator-sound-service-lib
+ vala-mocks-lib
+ gtest-static
+ ${SOUNDSERVICE_LIBRARIES}
+ ${TEST_LIBRARIES}
)
add_test(sound-menu-test sound-menu-test)
@@ -198,13 +200,13 @@ add_test(sound-menu-test sound-menu-test)
include_directories(${CMAKE_SOURCE_DIR}/src)
add_executable (notifications-test notifications-test.cc)
target_link_libraries (
- notifications-test
- indicator-sound-service-lib
- vala-mocks-lib
- pulse-mock
- gtest
- ${SOUNDSERVICE_LIBRARIES}
- ${TEST_LIBRARIES}
+ notifications-test
+ indicator-sound-service-lib
+ vala-mocks-lib
+ pulse-mock
+ gtest-static
+ ${SOUNDSERVICE_LIBRARIES}
+ ${TEST_LIBRARIES}
)
add_test(notifications-test notifications-test)
@@ -216,23 +218,23 @@ add_test(notifications-test notifications-test)
include_directories(${CMAKE_SOURCE_DIR}/src)
add_executable (media-player-user-test media-player-user.cc)
target_link_libraries (
- media-player-user-test
- indicator-sound-service-lib
- vala-mocks-lib
- gtest
- ${SOUNDSERVICE_LIBRARIES}
- ${TEST_LIBRARIES}
+ media-player-user-test
+ indicator-sound-service-lib
+ vala-mocks-lib
+ gtest-static
+ ${SOUNDSERVICE_LIBRARIES}
+ ${TEST_LIBRARIES}
)
# Split tests to work around libaccountservice sucking
add_test(media-player-user-test-basic
- media-player-user-test --gtest_filter=MediaPlayerUserTest.BasicObject
+ media-player-user-test --gtest_filter=MediaPlayerUserTest.BasicObject
)
add_test(media-player-user-test-dataset
- media-player-user-test --gtest_filter=MediaPlayerUserTest.DataSet
+ media-player-user-test --gtest_filter=MediaPlayerUserTest.DataSet
)
add_test(media-player-user-test-timeout
- media-player-user-test --gtest_filter=MediaPlayerUserTest.TimeoutTest
+ media-player-user-test --gtest_filter=MediaPlayerUserTest.TimeoutTest
)
###########################
@@ -242,20 +244,20 @@ add_test(media-player-user-test-timeout
include_directories(${CMAKE_SOURCE_DIR}/src)
add_executable (greeter-list-test greeter-list.cc)
target_link_libraries (
- greeter-list-test
- indicator-sound-service-lib
- vala-mocks-lib
- gtest
- ${SOUNDSERVICE_LIBRARIES}
- ${TEST_LIBRARIES}
+ greeter-list-test
+ indicator-sound-service-lib
+ vala-mocks-lib
+ gtest-static
+ ${SOUNDSERVICE_LIBRARIES}
+ ${TEST_LIBRARIES}
)
# Split tests to work around libaccountservice sucking
add_test(greeter-list-test-basic
- greeter-list-test --gtest_filter=GreeterListTest.BasicObject
+ greeter-list-test --gtest_filter=GreeterListTest.BasicObject
)
add_test(greeter-list-test-iterator
- greeter-list-test --gtest_filter=GreeterListTest.BasicIterator
+ greeter-list-test --gtest_filter=GreeterListTest.BasicIterator
)
###########################
@@ -263,18 +265,22 @@ add_test(greeter-list-test-iterator
###########################
add_definitions(
- -DINDICATOR_SOUND_SERVICE_BINARY="${CMAKE_BINARY_DIR}/src/indicator-sound-service"
- -DPA_MOCK_LIB="${CMAKE_CURRENT_BINARY_DIR}/libpulse-mock.so"
+ -DINDICATOR_SOUND_SERVICE_BINARY="${CMAKE_BINARY_DIR}/src/indicator-sound-service"
+ -DPA_MOCK_LIB="${CMAKE_CURRENT_BINARY_DIR}/libpulse-mock.so"
)
add_executable (indicator-test indicator-test.cc gschemas.compiled)
target_link_libraries (
- indicator-test
- gtest
- ${SOUNDSERVICE_LIBRARIES}
- ${TEST_LIBRARIES}
+ indicator-test
+ gtest-static
+ ${SOUNDSERVICE_LIBRARIES}
+ ${TEST_LIBRARIES}
)
# Split tests to work around libaccountservice sucking
add_test(indcator-test
- indicator-test
+ indicator-test
)
+
+add_subdirectory(integration)
+add_subdirectory(dbus-types)
+add_subdirectory(service-mocks) \ No newline at end of file
diff --git a/tests/dbus-types/CMakeLists.txt b/tests/dbus-types/CMakeLists.txt
new file mode 100644
index 0000000..cb7f512
--- /dev/null
+++ b/tests/dbus-types/CMakeLists.txt
@@ -0,0 +1,53 @@
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+find_package(Qt5DBus REQUIRED)
+include_directories(${Qt5DBus_INCLUDE_DIRS})
+
+add_definitions(-DQT_NO_KEYWORDS=1)
+
+set(dbusinterface_streamrestore_xml "org.PulseAudio.Ext.StreamRestore1.xml")
+set_source_files_properties(${dbusinterface_streamrestore_xml} PROPERTIES
+ CLASSNAME StreamRestoreInterface)
+
+set(dbusinterface_accounts_xml "org.freedesktop.Accounts.xml")
+set_source_files_properties(${dbusinterface_accounts_xml} PROPERTIES
+ CLASSNAME AccountsInterface)
+
+set(dbusinterface_accountssound_xml "com.ubuntu.AccountsService.Sound.xml")
+set_source_files_properties(${dbusinterface_accountssound_xml} PROPERTIES
+ CLASSNAME AccountsSoundInterface)
+
+set(dbusinterface_properties_xml "org.freedesktop.DBus.Properties.xml")
+set_source_files_properties(${dbusinterface_properties_xml} PROPERTIES
+ CLASSNAME DBusPropertiesInterface
+ NO_NAMESPACE YES
+ INCLUDE "dbus-types.h")
+
+set(dbusinterface_actions_xml "org.gtk.Actions.xml")
+set_source_files_properties(${dbusinterface_actions_xml} PROPERTIES
+ CLASSNAME MenusInterface)
+
+set(dbusinterface_notifications_xml "org.freedesktop.Notifications.xml")
+set_source_files_properties(${dbusinterface_notifications_xml} PROPERTIES
+ CLASSNAME NotificationsInterface)
+
+qt5_add_dbus_interface(interface_files ${dbusinterface_streamrestore_xml} stream_restore_interface)
+qt5_add_dbus_interface(interface_files ${dbusinterface_properties_xml} dbus_properties_interface)
+qt5_add_dbus_interface(interface_files ${dbusinterface_accounts_xml} dbus_accounts_interface)
+qt5_add_dbus_interface(interface_files ${dbusinterface_accountssound_xml} dbus_accountssound_interface)
+qt5_add_dbus_interface(interface_files ${dbusinterface_actions_xml} dbus_menus_interface)
+qt5_add_dbus_interface(interface_files ${dbusinterface_notifications_xml} dbus_notifications_interface)
+
+add_library(
+ sound-indicator-dbus-interfaces
+ STATIC
+ ${interface_files}
+ pulseaudio-volume.cpp
+)
+
+qt5_use_modules(
+ sound-indicator-dbus-interfaces
+ Core
+ DBus
+)
diff --git a/tests/dbus-types/com.ubuntu.AccountsService.Sound.xml b/tests/dbus-types/com.ubuntu.AccountsService.Sound.xml
new file mode 100644
index 0000000..27c915a
--- /dev/null
+++ b/tests/dbus-types/com.ubuntu.AccountsService.Sound.xml
@@ -0,0 +1,9 @@
+<node>
+ <interface name="com.ubuntu.AccountsService.Sound">
+ <method name="Set">
+ <arg direction="in" type="s" name="interface" />
+ <arg direction="in" type="s" name="property" />
+ <arg direction="out" type="o" name="path" />
+ </method>
+ </interface>
+</node>
diff --git a/tests/dbus-types/dbus-types.h b/tests/dbus-types/dbus-types.h
new file mode 100644
index 0000000..b75acf0
--- /dev/null
+++ b/tests/dbus-types/dbus-types.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#pragma once
+
+#include <QDBusMetaType>
+#include "pulseaudio-volume.h"
+
+namespace DBusTypes
+{
+ inline void registerMetaTypes()
+ {
+ PulseaudioVolume::registerMetaType();
+ PulseaudioVolumeArray::registerMetaType();
+ }
+
+ static constexpr char const* DBUS_NAME = "com.canonical.indicator.sound";
+
+ static constexpr char const* DBUS_PULSE = "org.PulseAudio1";
+
+ static constexpr char const* ACCOUNTS_SERVICE = "org.freedesktop.Accounts";
+
+ static constexpr char const* STREAM_RESTORE_NAME = "org.PulseAudio.Ext.StreamRestore1";
+
+ static constexpr char const* STREAM_RESTORE_PATH = "/org/pulseaudio/stream_restore1";
+
+ static constexpr char const* STREAM_RESTORE_ENTRY_NAME = "org.PulseAudio.Ext.StreamRestore1.RestoreEntry";
+
+ static constexpr char const* MAIN_SERVICE_PATH = "/com/canonical/indicator/sound";
+
+ static constexpr char const* ACTIONS_INTERFACE = "org.gtk.Actions";
+
+} // namespace DBusTypes
+
diff --git a/tests/dbus-types/org.PulseAudio.Ext.StreamRestore1.xml b/tests/dbus-types/org.PulseAudio.Ext.StreamRestore1.xml
new file mode 100644
index 0000000..bf9af76
--- /dev/null
+++ b/tests/dbus-types/org.PulseAudio.Ext.StreamRestore1.xml
@@ -0,0 +1,7 @@
+<node>
+ <interface name="org.PulseAudio.Ext.StreamRestore1">
+ <method name="GetEntryByName">
+ <arg direction="in" type="s" name="entry" />
+ <arg direction="out" type="o" name="value" />
+ </interface>
+</node>
diff --git a/tests/dbus-types/org.freedesktop.Accounts.xml b/tests/dbus-types/org.freedesktop.Accounts.xml
new file mode 100644
index 0000000..d9c6b2a
--- /dev/null
+++ b/tests/dbus-types/org.freedesktop.Accounts.xml
@@ -0,0 +1,8 @@
+<node>
+ <interface name="org.freedesktop.Accounts">
+ <method name="FindUserByName">
+ <arg direction="in" type="s" name="user" />
+ <arg direction="out" type="o" name="path" />
+ </method>
+ </interface>
+</node>
diff --git a/tests/dbus-types/org.freedesktop.DBus.Properties.xml b/tests/dbus-types/org.freedesktop.DBus.Properties.xml
new file mode 100644
index 0000000..bdf6cad
--- /dev/null
+++ b/tests/dbus-types/org.freedesktop.DBus.Properties.xml
@@ -0,0 +1,22 @@
+<node>
+ <interface name="org.freedesktop.DBus.Properties">
+ <method name="Set">
+ <arg direction="in" type="s" name="entry" />
+ <arg direction="in" type="s" name="property" />
+ <arg direction="in" type="v" name="value" />
+ </method>
+
+ <method name="Get">
+ <arg direction="in" type="s" name="entry" />
+ <arg direction="in" type="s" name="property" />
+ <arg direction="out" type="v" name="value" />
+ </method>
+
+ <signal name="PropertiesChanged">
+ <arg type="s" name="interface_name"/>
+ <arg type="a{sv}" name="changed_properties"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap"/>
+ <arg type="as" name="invalidated_properties"/>
+ </signal>
+ </interface>
+</node>
diff --git a/tests/dbus-types/org.freedesktop.Notifications.xml b/tests/dbus-types/org.freedesktop.Notifications.xml
new file mode 100644
index 0000000..f7d923e
--- /dev/null
+++ b/tests/dbus-types/org.freedesktop.Notifications.xml
@@ -0,0 +1,47 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+ <interface name="org.freedesktop.Notifications">
+ <signal name="NotificationClosed">
+ <arg name="id" type="u" direction="out"/>
+ <arg name="reason" type="u" direction="out"/>
+ </signal>
+ <signal name="ActionInvoked">
+ <arg name="id" type="u" direction="out"/>
+ <arg name="action_key" type="s" direction="out"/>
+ </signal>
+ <signal name="dataChanged">
+ <arg name="id" type="u" direction="out"/>
+ </signal>
+ <method name="CloseNotification">
+ <arg name="id" type="u" direction="in"/>
+ </method>
+ <method name="GetServerInformation">
+ <arg name="name" type="s" direction="out"/>
+ <arg name="vendor" type="s" direction="out"/>
+ <arg name="version" type="s" direction="out"/>
+ <arg name="specVersion" type="s" direction="out"/>
+ </method>
+ <method name="GetCapabilities">
+ <arg type="as" direction="out"/>
+ </method>
+ <method name="Notify">
+ <arg type="u" direction="out"/>
+ <arg name="app_name" type="s" direction="in"/>
+ <arg name="replaces_id" type="u" direction="in"/>
+ <arg name="app_icon" type="s" direction="in"/>
+ <arg name="summary" type="s" direction="in"/>
+ <arg name="body" type="s" direction="in"/>
+ <arg name="actions" type="as" direction="in"/>
+ <arg name="hints" type="a{sv}" direction="in"/>
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In6" value="QVariantMap"/>
+ <arg name="expire_timeout" type="i" direction="in"/>
+ </method>
+ <method name="onDataChanged">
+ <arg name="id" type="u" direction="in"/>
+ </method>
+ <method name="onCompleted">
+ <arg name="id" type="u" direction="in"/>
+ </method>
+ </interface>
+</node>
diff --git a/tests/dbus-types/org.gtk.Actions.xml b/tests/dbus-types/org.gtk.Actions.xml
new file mode 100644
index 0000000..b691f1f
--- /dev/null
+++ b/tests/dbus-types/org.gtk.Actions.xml
@@ -0,0 +1,13 @@
+<node>
+ <interface name="org.gtk.Actions">
+ <signal name="Changed">
+ <arg name="removedActions" type="as" direction="in" />
+ <arg name="actionsFlags" type="a{sb}" direction="in" />
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="QVariantMap"/>
+ <arg name="actionsChanged" type="a{sv}" direction="in" />
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QVariantMap"/>
+ <arg name="actionsAdded" type="a{s(bgav)}" direction="in" />
+ <annotation name="org.qtproject.QtDBus.QtTypeName.In3" value="QVariantMap"/>
+ </signal>
+ </interface>
+</node>
diff --git a/tests/dbus-types/pulseaudio-volume.cpp b/tests/dbus-types/pulseaudio-volume.cpp
new file mode 100644
index 0000000..8ee305f
--- /dev/null
+++ b/tests/dbus-types/pulseaudio-volume.cpp
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#include "dbus-types.h"
+
+PulseaudioVolume::PulseaudioVolume() :
+ type_(0),
+ volume_(10)
+{
+}
+
+PulseaudioVolume::PulseaudioVolume(unsigned int type, unsigned int volume) :
+ type_(type)
+ , volume_(volume)
+{
+}
+
+PulseaudioVolume::PulseaudioVolume(const PulseaudioVolume &other) :
+ type_(other.type_),
+ volume_(other.volume_)
+{
+}
+
+PulseaudioVolume& PulseaudioVolume::operator=(const PulseaudioVolume &other)
+{
+ type_ = other.type_;
+ volume_ = other.volume_;
+
+ return *this;
+}
+
+PulseaudioVolume::~PulseaudioVolume()
+{
+}
+
+unsigned int PulseaudioVolume::getType() const
+{
+ return type_;
+}
+
+unsigned int PulseaudioVolume::getVolume() const
+{
+ return volume_;
+}
+
+void PulseaudioVolume::registerMetaType()
+{
+ qRegisterMetaType<PulseaudioVolume>("PulseaudioVolume");
+
+ qDBusRegisterMetaType<PulseaudioVolume>();
+}
+
+QDBusArgument &operator<<(QDBusArgument &argument, const PulseaudioVolume& volume)
+{
+ argument.beginStructure();
+ argument << volume.type_;
+ argument << volume.volume_;
+ argument.endStructure();
+
+ return argument;
+}
+
+const QDBusArgument &operator>>(const QDBusArgument &argument, PulseaudioVolume &volume)
+{
+ argument.beginStructure();
+ argument >> volume.type_;
+ argument >> volume.volume_;
+ argument.endStructure();
+
+ return argument;
+}
+
+PulseaudioVolumeArray::PulseaudioVolumeArray()
+{
+}
+
+PulseaudioVolumeArray::PulseaudioVolumeArray(const PulseaudioVolumeArray &other) :
+ volume_array_(other.volume_array_)
+{
+}
+
+PulseaudioVolumeArray& PulseaudioVolumeArray::operator=(const PulseaudioVolumeArray &other)
+{
+ volume_array_ = other.volume_array_;
+
+ return *this;
+}
+
+PulseaudioVolumeArray::~PulseaudioVolumeArray()
+{
+}
+
+int PulseaudioVolumeArray::getNumItems() const
+{
+ return volume_array_.size();
+}
+
+PulseaudioVolume PulseaudioVolumeArray::getItem(int i) const
+{
+ if (i < volume_array_.size())
+ {
+ return volume_array_[i];
+ }
+ return PulseaudioVolume();
+}
+
+void PulseaudioVolumeArray::addItem(PulseaudioVolume const &item)
+{
+ volume_array_.push_back(item);
+}
+
+void PulseaudioVolumeArray::registerMetaType()
+{
+ qRegisterMetaType<PulseaudioVolumeArray>("PulseaudioVolumeArray");
+
+ qDBusRegisterMetaType<PulseaudioVolumeArray>();
+}
+
+QDBusArgument &operator<<(QDBusArgument &argument, const PulseaudioVolumeArray& volume)
+{
+ argument.beginArray( qMetaTypeId<PulseaudioVolume>() );
+ for (int i = 0; i < volume.volume_array_.size(); ++ i)
+ {
+ PulseaudioVolume item = volume.getItem(i);
+ argument << item;
+ }
+ argument.endArray();
+ return argument;
+}
+
+const QDBusArgument &operator>>(const QDBusArgument &argument, PulseaudioVolumeArray &volume)
+{
+ argument.beginArray();
+ while ( !argument.atEnd() ) {
+ PulseaudioVolume item;
+ argument >> item;
+ volume.volume_array_.push_back(item);
+ }
+ argument.endArray();
+
+ return argument;
+}
diff --git a/tests/dbus-types/pulseaudio-volume.h b/tests/dbus-types/pulseaudio-volume.h
new file mode 100644
index 0000000..6d5a222
--- /dev/null
+++ b/tests/dbus-types/pulseaudio-volume.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#pragma once
+
+#include <QtDBus>
+
+class PulseaudioVolume
+{
+public:
+ PulseaudioVolume();
+ PulseaudioVolume(unsigned int type, unsigned int volume);
+ PulseaudioVolume(const PulseaudioVolume &other);
+ PulseaudioVolume& operator=(const PulseaudioVolume &other);
+ ~PulseaudioVolume();
+
+ friend QDBusArgument &operator<<(QDBusArgument &argument, PulseaudioVolume const & volume);
+ friend const QDBusArgument &operator>>(QDBusArgument const & argument, PulseaudioVolume &volume);
+
+ unsigned int getType() const;
+ unsigned int getVolume() const;
+
+ //register Message with the Qt type system
+ static void registerMetaType();
+
+private:
+ unsigned int type_;
+ unsigned int volume_;
+};
+
+class PulseaudioVolumeArray
+{
+public:
+ PulseaudioVolumeArray();
+ PulseaudioVolumeArray(QString const &interface, QString const &property, QDBusVariant const& value);
+ PulseaudioVolumeArray(const PulseaudioVolumeArray &other);
+ PulseaudioVolumeArray& operator=(const PulseaudioVolumeArray &other);
+ ~PulseaudioVolumeArray();
+
+ friend QDBusArgument &operator<<(QDBusArgument &argument, PulseaudioVolumeArray const & volume);
+ friend const QDBusArgument &operator>>(QDBusArgument const & argument, PulseaudioVolumeArray &volume);
+
+ int getNumItems() const;
+ PulseaudioVolume getItem(int i) const;
+ void addItem(PulseaudioVolume const &item);
+
+ //register Message with the Qt type system
+ static void registerMetaType();
+
+private:
+ QVector<PulseaudioVolume> volume_array_;
+};
+
+Q_DECLARE_METATYPE(PulseaudioVolume)
+Q_DECLARE_METATYPE(PulseaudioVolumeArray)
diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt
new file mode 100644
index 0000000..939e329
--- /dev/null
+++ b/tests/integration/CMakeLists.txt
@@ -0,0 +1,132 @@
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+include(FindGMock)
+
+#pkg_check_modules(GMENUHARNESS REQUIRED libgmenuharness REQUIRED)
+#include_directories(${GMENUHARNESS_INCLUDE_DIRS})
+include_directories("${CMAKE_SOURCE_DIR}/include")
+
+pkg_check_modules(QTDBUSTEST REQUIRED libqtdbustest-1 REQUIRED)
+include_directories(${QTDBUSTEST_INCLUDE_DIRS})
+
+pkg_check_modules(QTDBUSMOCK REQUIRED libqtdbusmock-1 REQUIRED)
+include_directories(${QTDBUSMOCK_INCLUDE_DIRS})
+
+find_package(Qt5Test REQUIRED)
+include_directories(${Qt5Test_INCLUDE_DIRS})
+
+find_package(Qt5DBus REQUIRED)
+include_directories(${Qt5DBus_INCLUDE_DIRS})
+
+include_directories(${CMAKE_CURRENT_SOURCE_DIR})
+include_directories(${GMOCK_INCLUDE_DIRS})
+include_directories(${GTEST_INCLUDE_DIRS})
+
+include_directories("${CMAKE_SOURCE_DIR}/tests/dbus-types")
+include_directories("${CMAKE_BINARY_DIR}/tests/dbus-types")
+
+add_definitions(-DSOUND_SERVICE_BIN="${CMAKE_BINARY_DIR}/src/indicator-sound-service"
+ -DSTREAM_RESTORE_TABLE="${CMAKE_SOURCE_DIR}/tests/integration/touch-stream-restore.table"
+ -DVOLUME_SET_BIN="${CMAKE_BINARY_DIR}/tests/integration/set-volume"
+ -DACCOUNTS_SERVICE_BIN="${CMAKE_BINARY_DIR}/tests/service-mocks/accounts-mock/accounts-service-sound"
+ -DMEDIA_PLAYER_MPRIS_BIN="${CMAKE_BINARY_DIR}/tests/service-mocks/media-player-mpris-mock/media-player-mpris-mock"
+ -DMEDIA_PLAYER_MPRIS_UPDATE_BIN="${CMAKE_BINARY_DIR}/tests/service-mocks/media-player-mpris-mock/media-player-mpris-mock-update"
+ -DTEST_SOUND="${CMAKE_SOURCE_DIR}/tests/integration/test-sound.wav"
+ -DQT_NO_KEYWORDS=1
+ -DXDG_DATA_DIRS="${XDG_DATA_DIRS}"
+)
+
+set(GLIB_REQUIRED_VERSION 2.26)
+
+pkg_check_modules(
+ GLIB REQUIRED
+ glib-2.0>=${GLIB_REQUIRED_VERSION}
+ gio-2.0>=${GLIB_REQUIRED_VERSION}
+)
+include_directories(${GLIB_INCLUDE_DIRS})
+
+set(
+ INTEGRATION_TESTS_SRC
+ indicator-sound-test-base.cpp
+ test-indicator.cpp
+ utils/dbus-pulse-volume.cpp
+ main.cpp
+)
+
+add_executable(
+ integration-tests
+ ${INTEGRATION_TESTS_SRC}
+)
+
+qt5_use_modules(
+ integration-tests
+ Core
+ DBus
+ Test
+)
+
+target_link_libraries(
+ integration-tests
+ sound-indicator-dbus-interfaces
+ ${QTDBUSMOCK_LDFLAGS}
+ ${QTDBUSTEST_LDFLAGS}
+ ${GTEST_LIBRARIES}
+ ${GMOCK_LIBRARIES}
+# ${GMENUHARNESS_LDFLAGS}
+ ${GLIB_LDFLAGS}
+ gmenuharness-shared
+)
+
+add_test(
+ integration-tests
+ integration-tests
+)
+
+set(
+ SET-VOLUME-SRC
+ utils/dbus-pulse-volume.cpp
+ utils/set-volume.cpp
+)
+
+set(
+ GET-VOLUME-SRC
+ utils/dbus-pulse-volume.cpp
+ utils/get-volume.cpp
+)
+
+add_executable(
+ set-volume
+ ${SET-VOLUME-SRC}
+)
+
+add_executable(
+ get-volume
+ ${GET-VOLUME-SRC}
+)
+
+qt5_use_modules(
+ set-volume
+ Core
+ DBus
+ Test
+)
+
+qt5_use_modules(
+ get-volume
+ Core
+ DBus
+ Test
+)
+
+target_link_libraries(
+ get-volume
+ sound-indicator-dbus-interfaces
+)
+
+target_link_libraries(
+ set-volume
+ sound-indicator-dbus-interfaces
+)
+
+#add_subdirectory(utils) \ No newline at end of file
diff --git a/tests/integration/Copy of test-sound.wav b/tests/integration/Copy of test-sound.wav
new file mode 100644
index 0000000..709c6eb
--- /dev/null
+++ b/tests/integration/Copy of test-sound.wav
Binary files differ
diff --git a/tests/integration/indicator-sound-test-base.cpp b/tests/integration/indicator-sound-test-base.cpp
new file mode 100644
index 0000000..c261fcf
--- /dev/null
+++ b/tests/integration/indicator-sound-test-base.cpp
@@ -0,0 +1,793 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+
+#include "indicator-sound-test-base.h"
+
+#include "dbus_menus_interface.h"
+#include "dbus_properties_interface.h"
+#include "dbus_accounts_interface.h"
+#include "dbus_accountssound_interface.h"
+#include "dbus_notifications_interface.h"
+#include "dbus-types.h"
+
+#include <gio/gio.h>
+
+#include <QSignalSpy>
+#include "utils/dbus-pulse-volume.h"
+
+using namespace QtDBusTest;
+using namespace QtDBusMock;
+using namespace std;
+using namespace testing;
+namespace mh = unity::gmenuharness;
+
+IndicatorSoundTestBase::IndicatorSoundTestBase() :
+ dbusMock(dbusTestRunner)
+{
+}
+
+IndicatorSoundTestBase::~IndicatorSoundTestBase()
+{
+}
+
+void IndicatorSoundTestBase::SetUp()
+{
+ setenv("XDG_DATA_DIRS", XDG_DATA_DIRS, true);
+ setenv("DBUS_SYSTEM_BUS_ADDRESS", dbusTestRunner.systemBus().toStdString().c_str(), true);
+ setenv("DBUS_SESSION_BUS_ADDRESS", dbusTestRunner.sessionBus().toStdString().c_str(), true);
+ dbusMock.registerNotificationDaemon();
+
+ dbusTestRunner.startServices();
+
+ auto& notifications = notificationsMockInterface();
+ notifications.AddMethod("org.freedesktop.Notifications",
+ "GetCapabilities",
+ "",
+ "as",
+ "ret = ['actions', 'body', 'body-markup', 'icon-static', 'image/svg+xml', 'x-canonical-private-synchronous', 'x-canonical-append', 'x-canonical-private-icon-only', 'x-canonical-truncation', 'private-synchronous', 'append', 'private-icon-only', 'truncation']"
+ ).waitForFinished();
+}
+
+void IndicatorSoundTestBase::TearDown()
+{
+ unsetenv("XDG_DATA_DIRS");
+ unsetenv("PULSE_SERVER");
+ unsetenv("DBUS_SYSTEM_BUS_ADDRESS");
+}
+
+void gvariant_deleter(GVariant* varptr)
+{
+ if (varptr != nullptr)
+ {
+ g_variant_unref(varptr);
+ }
+}
+
+std::shared_ptr<GVariant> IndicatorSoundTestBase::volume_variant(double volume)
+{
+ GVariantBuilder builder;
+
+ g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
+ g_variant_builder_add(&builder,
+ "{sv}",
+ "title",
+ g_variant_new_string("_Sound"));
+
+ g_variant_builder_add(&builder,
+ "{sv}",
+ "accessible-desc",
+ g_variant_new_string("_Sound"));
+
+ auto icon = g_themed_icon_new("icon");
+ g_variant_builder_add(&builder,
+ "{sv}",
+ "icon",
+ g_icon_serialize(icon));
+
+ g_variant_builder_add(&builder,
+ "{sv}",
+ "visible",
+ g_variant_new_boolean(true));
+ return shared_ptr<GVariant>(g_variant_builder_end(&builder), &gvariant_deleter);
+}
+
+bool IndicatorSoundTestBase::setStreamRestoreVolume(QString const &role, double volume)
+{
+ QProcess setVolume;
+ setVolume.start(VOLUME_SET_BIN, QStringList()
+ << role
+ << QString("%1").arg(volume));
+ if (!setVolume.waitForStarted())
+ return false;
+
+ if (!setVolume.waitForFinished())
+ return false;
+
+ return setVolume.exitCode() == 0;
+}
+
+bool IndicatorSoundTestBase::setSinkVolume(double volume)
+{
+ QString volume_percentage = QString("%1\%").arg(volume*100);
+ QProcess setVolume;
+ setVolume.start("pactl", QStringList()
+ << "-s"
+ << "127.0.0.1"
+ << "set-sink-volume"
+ << "0"
+ << volume_percentage);
+ if (!setVolume.waitForStarted())
+ return false;
+
+ if (!setVolume.waitForFinished())
+ return false;
+
+ return setVolume.exitCode() == 0;
+}
+
+bool IndicatorSoundTestBase::clearGSettingsPlayers()
+{
+ QProcess clearPlayers;
+
+ clearPlayers.start("gsettings", QStringList()
+ << "set"
+ << "com.canonical.indicator.sound"
+ << "interested-media-players"
+ << "[]");
+ if (!clearPlayers.waitForStarted())
+ return false;
+
+ if (!clearPlayers.waitForFinished())
+ return false;
+
+ return clearPlayers.exitCode() == 0;
+}
+
+bool IndicatorSoundTestBase::startTestMprisPlayer(QString const& playerName)
+{
+ testPlayer1.terminate();
+ testPlayer1.start(MEDIA_PLAYER_MPRIS_BIN, QStringList()
+ << playerName);
+ if (!testPlayer1.waitForStarted())
+ return false;
+
+
+ return true;
+}
+
+bool IndicatorSoundTestBase::setTestMprisPlayerProperty(QString const &testPlayer, QString const &property, bool value)
+{
+ QProcess setProperty;
+ QString strValue;
+ strValue = value ? "true" : "false";
+
+ setProperty.start(MEDIA_PLAYER_MPRIS_UPDATE_BIN, QStringList()
+ << testPlayer
+ << property
+ << strValue);
+ if (!setProperty.waitForStarted())
+ return false;
+
+ if (!setProperty.waitForFinished())
+ return false;
+
+ return setProperty.exitCode() == 0;
+}
+
+bool IndicatorSoundTestBase::startTestSound(QString const &role)
+{
+ testSoundProcess.terminate();
+ testSoundProcess.start("paplay", QStringList()
+ << "-s"
+ << "127.0.0.1"
+ << TEST_SOUND
+ << QString("--property=media.role=%1").arg(role));
+
+ if (!testSoundProcess.waitForStarted())
+ return false;
+
+ return true;
+}
+
+void IndicatorSoundTestBase::stopTestSound()
+{
+ testSoundProcess.terminate();
+}
+
+void IndicatorSoundTestBase::startPulseDesktop(DevicePortType speakerPort, DevicePortType headphonesPort)
+{
+ try
+ {
+ pulseaudio.reset(
+ new QProcessDBusService(DBusTypes::DBUS_PULSE,
+ QDBusConnection::SessionBus,
+ "pulseaudio",
+ QStringList() << "--start"
+ << "-vvvv"
+ << "--disable-shm=true"
+ << "--daemonize=false"
+ << "--use-pid-file=false"
+ << "--system=false"
+ << "--exit-idle-time=-1"
+ << "-n"
+ << QString("--load=module-null-sink sink_name=indicator_sound_test_speaker sink_properties=device.bus=%1").arg(getDevicePortString(speakerPort))
+ << QString("--load=module-null-sink sink_name=indicator_sound_test_headphones sink_properties=device.bus=%1").arg(getDevicePortString(headphonesPort))
+ << "--log-target=file:/tmp/pulse-daemon.log"
+ << "--load=module-dbus-protocol"
+ << "--load=module-native-protocol-tcp auth-ip-acl=127.0.0.1"
+ ));
+ pulseaudio->start(dbusTestRunner.sessionConnection());
+ }
+ catch (exception const& e)
+ {
+ cout << "pulseaudio(): " << e.what() << endl;
+ throw;
+ }
+}
+
+void IndicatorSoundTestBase::startPulsePhone(DevicePortType speakerPort, DevicePortType headphonesPort)
+{
+ try
+ {
+ pulseaudio.reset(
+ new QProcessDBusService(DBusTypes::DBUS_PULSE,
+ QDBusConnection::SessionBus,
+ "pulseaudio",
+ QStringList() << "--start"
+ << "-vvvv"
+ << "--disable-shm=true"
+ << "--daemonize=false"
+ << "--use-pid-file=false"
+ << "--system=false"
+ << "--exit-idle-time=-1"
+ << "-n"
+ << QString("--load=module-null-sink sink_name=indicator_sound_test_speaker sink_properties=device.bus=%1").arg(getDevicePortString(speakerPort))
+ << QString("--load=module-null-sink sink_name=indicator_sound_test_headphones sink_properties=device.bus=%1").arg(getDevicePortString(headphonesPort))
+ << "--log-target=file:/tmp/pulse-daemon.log"
+ << QString("--load=module-stream-restore restore_device=false restore_muted=false fallback_table=%1").arg(STREAM_RESTORE_TABLE)
+ << "--load=module-dbus-protocol"
+ << "--load=module-native-protocol-tcp auth-ip-acl=127.0.0.1"
+ ));
+ pulseaudio->start(dbusTestRunner.sessionConnection());
+ }
+ catch (exception const& e)
+ {
+ cout << "pulseaudio(): " << e.what() << endl;
+ throw;
+ }
+}
+
+void IndicatorSoundTestBase::startAccountsService()
+{
+ try
+ {
+ accountsService.reset(
+ new QProcessDBusService(DBusTypes::ACCOUNTS_SERVICE,
+ QDBusConnection::SystemBus,
+ ACCOUNTS_SERVICE_BIN,
+ QStringList()));
+ accountsService->start(dbusTestRunner.systemConnection());
+
+ initializeAccountsInterface();
+ }
+ catch (exception const& e)
+ {
+ cout << "accountsService(): " << e.what() << endl;
+ throw;
+ }
+}
+
+void IndicatorSoundTestBase::startIndicator()
+{
+ try
+ {
+ setenv("PULSE_SERVER", "127.0.0.1", true);
+ indicator.reset(
+ new QProcessDBusService(DBusTypes::DBUS_NAME,
+ QDBusConnection::SessionBus,
+ SOUND_SERVICE_BIN,
+ QStringList()));
+ indicator->start(dbusTestRunner.sessionConnection());
+ }
+ catch (exception const& e)
+ {
+ cout << "startIndicator(): " << e.what() << endl;
+ throw;
+ }
+}
+
+mh::MenuMatcher::Parameters IndicatorSoundTestBase::desktopParameters()
+{
+ return mh::MenuMatcher::Parameters(
+ "com.canonical.indicator.sound",
+ { { "indicator", "/com/canonical/indicator/sound" } },
+ "/com/canonical/indicator/sound/desktop");
+}
+
+mh::MenuMatcher::Parameters IndicatorSoundTestBase::phoneParameters()
+{
+ return mh::MenuMatcher::Parameters(
+ "com.canonical.indicator.sound",
+ { { "indicator", "/com/canonical/indicator/sound" } },
+ "/com/canonical/indicator/sound/phone");
+}
+
+unity::gmenuharness::MenuItemMatcher IndicatorSoundTestBase::volumeSlider(double volume, QString const &label)
+{
+ return mh::MenuItemMatcher().radio()
+ .label(label.toStdString())
+ .round_doubles(0.1)
+ .int32_attribute("target", 0)
+ .double_attribute("min-value", 0.0)
+ .double_attribute("max-value", 1.0)
+ .double_attribute("step", 0.01)
+ .string_attribute("x-canonical-type", "com.canonical.unity.slider")
+ .themed_icon("max-icon", {"audio-volume-high-panel", "audio-volume-high", "audio-volume", "audio"})
+ .themed_icon("min-icon", {"audio-volume-low-zero-panel", "audio-volume-low-zero", "audio-volume-low", "audio-volume", "audio"})
+ .pass_through_double_attribute("action", volume);
+}
+
+unity::gmenuharness::MenuItemMatcher IndicatorSoundTestBase::silentModeSwitch(bool toggled)
+{
+ return mh::MenuItemMatcher::checkbox()
+ .label("Silent Mode")
+ .action("indicator.silent-mode")
+ .toggled(toggled);
+}
+
+bool IndicatorSoundTestBase::waitMenuChange()
+{
+ if (signal_spy_menu_changed_)
+ {
+ return signal_spy_menu_changed_->wait();
+ }
+ return false;
+}
+
+bool IndicatorSoundTestBase::initializeMenuChangedSignal()
+{
+ if (!menu_interface_)
+ {
+ menu_interface_.reset(new MenusInterface("com.canonical.indicator.sound",
+ "/com/canonical/indicator/sound",
+ dbusTestRunner.sessionConnection(), 0));
+ }
+ if (menu_interface_)
+ {
+ qDebug() << "Waiting for signal";
+ signal_spy_menu_changed_.reset(new QSignalSpy(menu_interface_.get(), &MenusInterface::Changed));
+ }
+ if (!menu_interface_ || !signal_spy_menu_changed_)
+ {
+ return false;
+ }
+ return true;
+}
+
+bool IndicatorSoundTestBase::waitVolumeChangedInIndicator()
+{
+ if (signal_spy_volume_changed_)
+ {
+ return signal_spy_volume_changed_->wait();
+ }
+ return false;
+}
+
+void IndicatorSoundTestBase::initializeAccountsInterface()
+{
+ auto username = qgetenv("USER");
+ if (username != "")
+ {
+ std::unique_ptr<AccountsInterface> accountsInterface(new AccountsInterface("org.freedesktop.Accounts",
+ "/org/freedesktop/Accounts",
+ dbusTestRunner.systemConnection(), 0));
+
+ QDBusReply<QDBusObjectPath> userResp = accountsInterface->call(QLatin1String("FindUserByName"),
+ QLatin1String(username));
+
+ if (!userResp.isValid())
+ {
+ qWarning() << "SetVolume::initializeAccountsInterface(): D-Bus error: " << userResp.error().message();
+ }
+
+ auto userPath = userResp.value().path();
+ if (userPath != "")
+ {
+ std::unique_ptr<AccountsSoundInterface> soundInterface(new AccountsSoundInterface("org.freedesktop.Accounts",
+ userPath,
+ dbusTestRunner.systemConnection(), 0));
+
+ accounts_interface_.reset(new DBusPropertiesInterface("org.freedesktop.Accounts",
+ userPath,
+ dbusTestRunner.systemConnection(), 0));
+ if (!accounts_interface_->isValid())
+ {
+ qWarning() << "SetVolume::initializeAccountsInterface(): D-Bus error: " << accounts_interface_->lastError().message();
+ }
+ signal_spy_volume_changed_.reset(new QSignalSpy(accounts_interface_.get(),&DBusPropertiesInterface::PropertiesChanged));
+ }
+ }
+}
+
+OrgFreedesktopDBusMockInterface& IndicatorSoundTestBase::notificationsMockInterface()
+{
+ return dbusMock.mockInterface("org.freedesktop.Notifications",
+ "/org/freedesktop/Notifications",
+ "org.freedesktop.Notifications",
+ QDBusConnection::SessionBus);
+}
+
+bool IndicatorSoundTestBase::setActionValue(const QString & action, QVariant value)
+{
+ QDBusInterface actionsInterface(DBusTypes::DBUS_NAME,
+ DBusTypes::MAIN_SERVICE_PATH,
+ DBusTypes::ACTIONS_INTERFACE,
+ dbusTestRunner.sessionConnection());
+
+ QDBusVariant dbusVar(value);
+ auto resp = actionsInterface.call("SetState",
+ action,
+ QVariant::fromValue(dbusVar),
+ QVariant::fromValue(QVariantMap()));
+
+ if (resp.type() == QDBusMessage::ErrorMessage)
+ {
+ qCritical() << "IndicatorSoundTestBase::setActionValue(): Failed to set value for action "
+ << action
+ << " "
+ << resp.errorMessage();
+ return false;
+ }
+ else
+ {
+ return true;
+ }
+}
+
+bool IndicatorSoundTestBase::pressNotificationButton(int id, const QString & button)
+{
+ OrgFreedesktopDBusMockInterface actionsInterface("org.freedesktop.Notifications",
+ "/org/freedesktop/Notifications",
+ dbusTestRunner.sessionConnection());
+
+ actionsInterface.EmitSignal(
+ "org.freedesktop.Notifications",
+ "ActionInvoked", "us", QVariantList() << id << button);
+
+ return true;
+}
+
+bool IndicatorSoundTestBase::qDBusArgumentToMap(QVariant const& variant, QVariantMap& map)
+{
+ if (variant.canConvert<QDBusArgument>())
+ {
+ QDBusArgument value(variant.value<QDBusArgument>());
+ if (value.currentType() == QDBusArgument::MapType)
+ {
+ value >> map;
+ return true;
+ }
+ }
+ return false;
+}
+
+void IndicatorSoundTestBase::checkVolumeNotification(double volume, QString const& label, bool isLoud, QVariantList call)
+{
+ QString icon;
+ if (volume <= 0.0)
+ {
+ icon = "audio-volume-muted";
+ }
+ else if (volume <= 0.3)
+ {
+ icon = "audio-volume-low";
+ }
+ else if (volume <= 0.7)
+ {
+ icon = "audio-volume-medium";
+ }
+ else
+ {
+ icon = "audio-volume-high";
+ }
+
+ ASSERT_NE(call.size(), 0);
+ EXPECT_EQ("Notify", call.at(0));
+
+ QVariantList const& args(call.at(1).toList());
+ ASSERT_EQ(8, args.size());
+ EXPECT_EQ("indicator-sound", args.at(0));
+ EXPECT_EQ(icon, args.at(2));
+ EXPECT_EQ("Volume", args.at(3));
+ EXPECT_EQ(label, args.at(4));
+ EXPECT_EQ(QStringList(), args.at(5));
+
+ QVariantMap hints;
+ ASSERT_TRUE(qDBusArgumentToMap(args.at(6), hints));
+ ASSERT_TRUE(hints.contains("value"));
+ ASSERT_TRUE(hints.contains("x-canonical-non-shaped-icon"));
+ ASSERT_TRUE(hints.contains("x-canonical-value-bar-tint"));
+ ASSERT_TRUE(hints.contains("x-canonical-private-synchronous"));
+
+ EXPECT_EQ(volume*100, hints["value"]);
+ EXPECT_EQ(true, hints["x-canonical-non-shaped-icon"]);
+ EXPECT_EQ(isLoud, hints["x-canonical-value-bar-tint"]);
+ EXPECT_EQ(true, hints["x-canonical-private-synchronous"]);
+}
+
+void IndicatorSoundTestBase::checkHighVolumeNotification(QVariantList call)
+{
+ ASSERT_NE(call.size(), 0);
+ EXPECT_EQ("Notify", call.at(0));
+
+ QVariantList const& args(call.at(1).toList());
+ ASSERT_EQ(8, args.size());
+ EXPECT_EQ("indicator-sound", args.at(0));
+ EXPECT_EQ("Volume", args.at(3));
+}
+
+void IndicatorSoundTestBase::checkCloseNotification(int id, QVariantList call)
+{
+ EXPECT_EQ("CloseNotification", call.at(0));
+ QVariantList const& args(call.at(1).toList());
+ ASSERT_EQ(1, args.size());
+}
+
+void IndicatorSoundTestBase::checkNotificationWithNoArgs(QString const& method, QVariantList call)
+{
+ EXPECT_EQ(method, call.at(0));
+ QVariantList const& args(call.at(1).toList());
+ ASSERT_EQ(0, args.size());
+}
+
+int IndicatorSoundTestBase::getNotificationID(QVariantList call)
+{
+ if (call.size() == 0)
+ {
+ return -1;
+ }
+ QVariantList const& args(call.at(1).toList());
+ if (args.size() != 8)
+ {
+ return -1;
+ }
+ if (args.at(0) != "indicator-sound")
+ {
+ return -1;
+ }
+
+ bool isInt;
+ int id = args.at(1).toInt(&isInt);
+ if (!isInt)
+ {
+ return -1;
+ }
+ return id;
+}
+
+bool IndicatorSoundTestBase::activateHeadphones(bool headphonesActive)
+{
+ QProcess pacltProcess;
+
+ QString defaultSinkName = "indicator_sound_test_speaker";
+ QString suspendedSinkName = "indicator_sound_test_headphones";
+ if (headphonesActive)
+ {
+ defaultSinkName = "indicator_sound_test_headphones";
+ suspendedSinkName = "indicator_sound_test_speaker";
+ }
+
+ pacltProcess.start("pactl", QStringList() << "-s"
+ << "127.0.0.1"
+ << "set-default-sink"
+ << defaultSinkName);
+ if (!pacltProcess.waitForStarted())
+ return false;
+
+ if (!pacltProcess.waitForFinished())
+ return false;
+
+ pacltProcess.start("pactl", QStringList() << "-s"
+ << "127.0.0.1"
+ << "suspend-sink"
+ << defaultSinkName
+ << "0");
+ if (!pacltProcess.waitForStarted())
+ return false;
+
+ if (!pacltProcess.waitForFinished())
+ return false;
+
+ pacltProcess.start("pactl", QStringList() << "-s"
+ << "127.0.0.1"
+ << "suspend-sink"
+ << suspendedSinkName
+ << "1");
+ if (!pacltProcess.waitForStarted())
+ return false;
+
+ if (!pacltProcess.waitForFinished())
+ return false;
+
+ return pacltProcess.exitCode() == 0;
+}
+
+QString IndicatorSoundTestBase::getDevicePortString(DevicePortType port)
+{
+ QString portString;
+
+ switch (port)
+ {
+ case WIRED:
+ portString = "wired";
+ break;
+ case BLUETOOTH:
+ portString = "bluetooth";
+ break;
+ case USB:
+ portString = "usb";
+ break;
+ case HDMI:
+ portString = "hdmi";
+ break;
+ default:
+ portString = "not_defined";
+ break;
+ }
+
+ return portString;
+}
+
+void IndicatorSoundTestBase::checkPortDevicesLabels(DevicePortType speakerPort, DevicePortType headphonesPort)
+{
+ double INITIAL_VOLUME = 1.0;
+
+ QString speakerString;
+ QString speakerStringMenu;
+ switch(speakerPort)
+ {
+ case WIRED:
+ speakerString = "Speakers";
+ speakerStringMenu = "Volume";
+ break;
+ case BLUETOOTH:
+ speakerString = "Bluetooth speaker";
+ speakerStringMenu = "Volume (Bluetooth)";
+ break;
+ case USB:
+ speakerString = "Usb speaker";
+ speakerStringMenu = "Volume (Usb)";
+ break;
+ case HDMI:
+ speakerString = "HDMI speaker";
+ speakerStringMenu = "Volume (HDMI)";
+ break;
+ }
+
+ QString headphonesString;
+ QString headphonesStringMenu;
+ switch(headphonesPort)
+ {
+ case WIRED:
+ headphonesString = "Headphones";
+ headphonesStringMenu = "Volume (Headphones)";
+ break;
+ case BLUETOOTH:
+ headphonesString = "Bluetooth headphones";
+ headphonesStringMenu = "Volume (Bluetooth headphones)";
+ break;
+ case USB:
+ headphonesString = "Usb headphones";
+ headphonesStringMenu = "Volume (Usb headphones)";
+ break;
+ case HDMI:
+ headphonesString = "HDMI headphones";
+ headphonesStringMenu = "Volume (HDMI headphones)";
+ break;
+ }
+
+ QSignalSpy notificationsSpy(&notificationsMockInterface(),
+ SIGNAL(MethodCalled(const QString &, const QVariantList &)));
+
+ ASSERT_NO_THROW(startAccountsService());
+ ASSERT_NO_THROW(startPulsePhone(speakerPort, headphonesPort));
+
+ // initialize volumes in pulseaudio
+ EXPECT_TRUE(setStreamRestoreVolume("alert", INITIAL_VOLUME));
+ EXPECT_TRUE(setStreamRestoreVolume("multimedia", INITIAL_VOLUME));
+
+ // start now the indicator, so it picks the new volumes
+ ASSERT_NO_THROW(startIndicator());
+
+ // if the speaker is the normal one it does not emit any notification, as that's
+ // the default one.
+ // for the rest it notifies the output
+ if (speakerPort != WIRED)
+ {
+ WAIT_FOR_SIGNALS(notificationsSpy, 3);
+
+ // the first time we also have the calls to
+ // GetServerInformation and GetCapabilities
+ checkNotificationWithNoArgs("GetServerInformation", notificationsSpy.at(0));
+ checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(1));
+ checkVolumeNotification(1.0, speakerString, false, notificationsSpy.at(2));
+ notificationsSpy.clear();
+ }
+
+ // activate the headphones
+ EXPECT_TRUE(activateHeadphones(true));
+
+ if (speakerPort == WIRED)
+ {
+ WAIT_FOR_SIGNALS(notificationsSpy, 3);
+
+ // the first time we also have the calls to
+ // GetServerInformation and GetCapabilities
+ checkNotificationWithNoArgs("GetServerInformation", notificationsSpy.at(0));
+ checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(1));
+ checkVolumeNotification(1.0, headphonesString, false, notificationsSpy.at(2));
+ notificationsSpy.clear();
+ }
+ else
+ {
+ WAIT_FOR_SIGNALS(notificationsSpy, 1);
+ checkVolumeNotification(1.0, headphonesString, false, notificationsSpy.at(0));
+ notificationsSpy.clear();
+ }
+
+ // check the label in the menu
+ EXPECT_MATCHRESULT(mh::MenuMatcher(phoneParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-scroll-action", "indicator.scroll")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .string_attribute("submenu-action", "indicator.indicator-shown")
+ .mode(mh::MenuItemMatcher::Mode::starts_with)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(silentModeSwitch(false))
+ .item(volumeSlider(1.0, headphonesStringMenu))
+ )
+ ).match());
+
+ // deactivate the headphones
+ EXPECT_TRUE(activateHeadphones(false));
+
+ WAIT_FOR_SIGNALS(notificationsSpy, 1);
+ checkVolumeNotification(1.0, speakerString, false, notificationsSpy.at(0));
+ notificationsSpy.clear();
+
+ // check the label in the menu
+ EXPECT_MATCHRESULT(mh::MenuMatcher(phoneParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-scroll-action", "indicator.scroll")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .string_attribute("submenu-action", "indicator.indicator-shown")
+ .mode(mh::MenuItemMatcher::Mode::starts_with)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(silentModeSwitch(false))
+ .item(volumeSlider(1.0, speakerStringMenu))
+ )
+ ).match());
+}
diff --git a/tests/integration/indicator-sound-test-base.h b/tests/integration/indicator-sound-test-base.h
new file mode 100644
index 0000000..6879bcb
--- /dev/null
+++ b/tests/integration/indicator-sound-test-base.h
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+
+#pragma once
+
+#include <libqtdbustest/DBusTestRunner.h>
+#include <libqtdbustest/QProcessDBusService.h>
+#include <libqtdbusmock/DBusMock.h>
+
+#include <unity/gmenuharness/MatchUtils.h>
+#include <unity/gmenuharness/MenuMatcher.h>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+class MenusInterface;
+class DBusPulseVolume;
+class DBusPropertiesInterface;
+class QSignalSpy;
+
+#define WAIT_FOR_SIGNALS(signalSpy, signalsExpected)\
+{\
+ while (signalSpy.size() < signalsExpected)\
+ {\
+ ASSERT_TRUE(signalSpy.wait());\
+ }\
+ ASSERT_EQ(signalsExpected, signalSpy.size());\
+}
+
+class IndicatorSoundTestBase: public testing::Test
+{
+public:
+ IndicatorSoundTestBase();
+
+ ~IndicatorSoundTestBase();
+
+ enum DevicePortType
+ {
+ WIRED,
+ BLUETOOTH,
+ USB,
+ HDMI
+ };
+
+protected:
+ void SetUp() override;
+ void TearDown() override;
+
+ void startIndicator();
+ void startPulseDesktop(DevicePortType speakerPort=WIRED, DevicePortType headphonesPort=WIRED);
+ void startPulsePhone(DevicePortType speakerPort=WIRED, DevicePortType headphonesPort=WIRED);
+ void startAccountsService();
+
+ bool clearGSettingsPlayers();
+
+ bool startTestMprisPlayer(QString const& playerName);
+
+ bool setTestMprisPlayerProperty(QString const &testPlayer, QString const &property, bool value);
+
+ bool setStreamRestoreVolume(QString const &role, double volume);
+
+ bool setSinkVolume(double volume);
+
+ bool startTestSound(QString const &role);
+
+ void stopTestSound();
+
+ static std::shared_ptr<GVariant> volume_variant(double volume);
+
+ static unity::gmenuharness::MenuMatcher::Parameters desktopParameters();
+
+ static unity::gmenuharness::MenuMatcher::Parameters phoneParameters();
+
+ static unity::gmenuharness::MenuItemMatcher volumeSlider(double volume, QString const &label);
+
+ static unity::gmenuharness::MenuItemMatcher silentModeSwitch(bool toggled);
+
+ bool waitMenuChange();
+
+ bool initializeMenuChangedSignal();
+
+ bool waitVolumeChangedInIndicator();
+
+ void initializeAccountsInterface();
+
+ OrgFreedesktopDBusMockInterface& notificationsMockInterface();
+
+ bool setActionValue(const QString & action, QVariant value);
+
+ bool pressNotificationButton(int id, const QString & button);
+
+ bool qDBusArgumentToMap(QVariant const& variant, QVariantMap& map);
+
+ void checkVolumeNotification(double volume, QString const& label, bool isLoud, QVariantList call);
+
+ void checkHighVolumeNotification(QVariantList call);
+
+ void checkCloseNotification(int id, QVariantList call);
+
+ void checkNotificationWithNoArgs(QString const& method, QVariantList call);
+
+ int getNotificationID(QVariantList call);
+
+ bool activateHeadphones(bool headphonesActive);
+
+ QString getDevicePortString(DevicePortType port);
+
+ void checkPortDevicesLabels(DevicePortType speakerPort, DevicePortType headphonesPort);
+
+ QtDBusTest::DBusTestRunner dbusTestRunner;
+
+ QtDBusMock::DBusMock dbusMock;
+
+ QtDBusTest::DBusServicePtr indicator;
+
+ QtDBusTest::DBusServicePtr pulseaudio;
+
+ QtDBusTest::DBusServicePtr accountsService;
+
+ QProcess testSoundProcess;
+
+ QProcess testPlayer1;
+
+ std::unique_ptr<MenusInterface> menu_interface_;
+
+ std::unique_ptr<DBusPropertiesInterface> accounts_interface_;
+
+ std::unique_ptr<QSignalSpy> signal_spy_volume_changed_;
+
+ std::unique_ptr<QSignalSpy> signal_spy_menu_changed_;
+};
diff --git a/tests/integration/main.cpp b/tests/integration/main.cpp
new file mode 100644
index 0000000..a29b3b6
--- /dev/null
+++ b/tests/integration/main.cpp
@@ -0,0 +1,58 @@
+/*
+ * Copyright © 2014 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser 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 warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Pete Woods <pete.woods@canonical.com>
+ */
+
+//#include <config.h>
+
+#include <QCoreApplication>
+#include <QTimer>
+#include <gtest/gtest.h>
+
+#include <libqtdbusmock/DBusMock.h>
+
+#include "dbus-types.h"
+
+using namespace QtDBusMock;
+
+class Runner: public QObject
+{
+ Q_OBJECT
+public Q_SLOTS:
+ void run()
+ {
+ QCoreApplication::exit(RUN_ALL_TESTS());
+ }
+};
+
+int main(int argc, char **argv)
+{
+ qputenv("LANG", "C.UTF-8");
+ unsetenv("LC_ALL");
+
+ QCoreApplication application(argc, argv);
+ DBusMock::registerMetaTypes();
+ DBusTypes::registerMetaTypes();
+ ::testing::InitGoogleTest(&argc, argv);
+
+ Runner runner;
+ QTimer::singleShot(0, &runner, SLOT(run()));
+
+ return application.exec();
+}
+
+#include "main.moc"
diff --git a/tests/integration/test-indicator.cpp b/tests/integration/test-indicator.cpp
new file mode 100644
index 0000000..fccaf84
--- /dev/null
+++ b/tests/integration/test-indicator.cpp
@@ -0,0 +1,948 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+
+#include <indicator-sound-test-base.h>
+
+#include <QDebug>
+#include <QTestEventLoop>
+#include <QSignalSpy>
+
+using namespace std;
+using namespace testing;
+namespace mh = unity::gmenuharness;
+namespace
+{
+
+class TestIndicator: public IndicatorSoundTestBase
+{
+};
+
+TEST_F(TestIndicator, PhoneChangeRoleVolume)
+{
+ double INITIAL_VOLUME = 0.0;
+
+ ASSERT_NO_THROW(startAccountsService());
+ ASSERT_NO_THROW(startPulsePhone());
+
+ // initialize volumes in pulseaudio
+ EXPECT_TRUE(setStreamRestoreVolume("alert", INITIAL_VOLUME));
+ EXPECT_TRUE(setStreamRestoreVolume("multimedia", INITIAL_VOLUME));
+
+ // start now the indicator, so it picks the new volumes
+ ASSERT_NO_THROW(startIndicator());
+
+ // Generate a random volume
+ QTime now = QTime::currentTime();
+ qsrand(now.msec());
+ int randInt = qrand() % 100;
+ double randomVolume = randInt / 100.0;
+
+ // set an initial volume to the alert role
+ setStreamRestoreVolume("alert", 1.0);
+ EXPECT_TRUE(waitVolumeChangedInIndicator());
+
+ // play a test sound, it should change the role in the indicator
+ EXPECT_TRUE(startTestSound("multimedia"));
+ EXPECT_TRUE(waitVolumeChangedInIndicator());
+
+ // set the random volume to the multimedia role
+ EXPECT_TRUE(setStreamRestoreVolume("multimedia", randomVolume));
+ if (randomVolume != INITIAL_VOLUME)
+ {
+ EXPECT_TRUE(waitVolumeChangedInIndicator());
+ }
+
+ // check the indicator
+ EXPECT_MATCHRESULT(mh::MenuMatcher(phoneParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-scroll-action", "indicator.scroll")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .string_attribute("submenu-action", "indicator.indicator-shown")
+ .mode(mh::MenuItemMatcher::Mode::starts_with)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(silentModeSwitch(false))
+ .item(volumeSlider(randomVolume, "Volume"))
+ )
+ ).match());
+
+ // check that the last item is Sound Settings
+ EXPECT_MATCHRESULT(mh::MenuMatcher(phoneParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .mode(mh::MenuItemMatcher::Mode::ends_with)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .label("Sound Settings…")
+ .action("indicator.phone-settings")
+ )
+ ).match());
+
+ // stop the test sound, the role should change again to alert
+ stopTestSound();
+ if (randomVolume != 1.0)
+ {
+ // we only wait if the volume in the alert and the
+ // one set in the multimedia roles differ
+ EXPECT_TRUE(waitVolumeChangedInIndicator());
+ }
+
+ // check the initial volume for the alert role
+ EXPECT_MATCHRESULT(mh::MenuMatcher(phoneParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-scroll-action", "indicator.scroll")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .string_attribute("submenu-action", "indicator.indicator-shown")
+ .mode(mh::MenuItemMatcher::Mode::starts_with)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(silentModeSwitch(false))
+ .item(volumeSlider(1.0, "Volume"))
+ )
+ ).match());
+
+ // check that the last item is Sound Settings
+ EXPECT_MATCHRESULT(mh::MenuMatcher(phoneParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .mode(mh::MenuItemMatcher::Mode::ends_with)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .label("Sound Settings…")
+ .action("indicator.phone-settings")
+ )
+ ).match());
+}
+
+TEST_F(TestIndicator, PhoneBasicInitialVolume)
+{
+ double INITIAL_VOLUME = 0.0;
+
+ ASSERT_NO_THROW(startAccountsService());
+ EXPECT_TRUE(clearGSettingsPlayers());
+ ASSERT_NO_THROW(startPulsePhone());
+
+ // initialize volumes in pulseaudio
+ EXPECT_TRUE(setStreamRestoreVolume("alert", INITIAL_VOLUME));
+
+ // start now the indicator, so it picks the new volumes
+ ASSERT_NO_THROW(startIndicator());
+
+ EXPECT_MATCHRESULT(mh::MenuMatcher(phoneParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-scroll-action", "indicator.scroll")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .string_attribute("submenu-action", "indicator.indicator-shown")
+ .mode(mh::MenuItemMatcher::Mode::all)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(silentModeSwitch(false))
+ .item(volumeSlider(INITIAL_VOLUME, "Volume"))
+ )
+ .item(mh::MenuItemMatcher()
+ .label("Sound Settings…")
+ .action("indicator.phone-settings")
+ )
+ ).match());
+}
+
+TEST_F(TestIndicator, PhoneAddMprisPlayer)
+{
+ double INITIAL_VOLUME = 0.0;
+
+ ASSERT_NO_THROW(startAccountsService());
+ EXPECT_TRUE(clearGSettingsPlayers());
+ ASSERT_NO_THROW(startPulsePhone());
+
+ // initialize volumes in pulseaudio
+ EXPECT_TRUE(setStreamRestoreVolume("alert", INITIAL_VOLUME));
+
+ // start now the indicator, so it picks the new volumes
+ ASSERT_NO_THROW(startIndicator());
+
+ EXPECT_MATCHRESULT(mh::MenuMatcher(phoneParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-scroll-action", "indicator.scroll")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .string_attribute("submenu-action", "indicator.indicator-shown")
+ .mode(mh::MenuItemMatcher::Mode::all)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(silentModeSwitch(false))
+ .item(volumeSlider(INITIAL_VOLUME, "Volume"))
+ )
+ .item(mh::MenuItemMatcher()
+ .label("Sound Settings…")
+ .action("indicator.phone-settings")
+ )
+ ).match());
+
+ // initialize the signal spy
+ EXPECT_TRUE(initializeMenuChangedSignal());
+
+ // start the test player
+ EXPECT_TRUE(startTestMprisPlayer("testplayer1"));
+
+ // wait for the menu change
+ EXPECT_TRUE(waitMenuChange());
+
+ // finally verify that the player is added
+ EXPECT_MATCHRESULT(mh::MenuMatcher(phoneParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-scroll-action", "indicator.scroll")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .string_attribute("submenu-action", "indicator.indicator-shown")
+ .mode(mh::MenuItemMatcher::Mode::all)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(silentModeSwitch(false))
+ .item(volumeSlider(INITIAL_VOLUME, "Volume"))
+ )
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(mh::MenuItemMatcher()
+ .action("indicator.testplayer1.desktop")
+ .label("TestPlayer1")
+ .themed_icon("icon", {"testplayer"})
+ .string_attribute("x-canonical-type", "com.canonical.unity.media-player")
+ )
+ .item(mh::MenuItemMatcher()
+ .string_attribute("x-canonical-previous-action","indicator.previous.testplayer1.desktop")
+ .string_attribute("x-canonical-play-action","indicator.play.testplayer1.desktop")
+ .string_attribute("x-canonical-next-action","indicator.next.testplayer1.desktop")
+ .string_attribute("x-canonical-type","com.canonical.unity.playback-item")
+ )
+ )
+ .item(mh::MenuItemMatcher()
+ .label("Sound Settings…")
+ .action("indicator.phone-settings")
+ )
+ ).match());
+}
+
+TEST_F(TestIndicator, DesktopBasicInitialVolume)
+{
+ double INITIAL_VOLUME = 0.0;
+
+ ASSERT_NO_THROW(startAccountsService());
+ EXPECT_TRUE(clearGSettingsPlayers());
+ ASSERT_NO_THROW(startPulseDesktop());
+
+ // initialize volumes in pulseaudio
+ EXPECT_FALSE(setStreamRestoreVolume("alert", INITIAL_VOLUME));
+ EXPECT_TRUE(setSinkVolume(INITIAL_VOLUME));
+
+ // start the test player
+ EXPECT_TRUE(startTestMprisPlayer("testplayer1"));
+
+ // start now the indicator, so it picks the new volumes
+ ASSERT_NO_THROW(startIndicator());
+
+ EXPECT_MATCHRESULT(mh::MenuMatcher(desktopParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .mode(mh::MenuItemMatcher::Mode::all)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(mh::MenuItemMatcher().checkbox()
+ .label("Mute")
+ )
+ .item(volumeSlider(INITIAL_VOLUME, "Volume"))
+ )
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(mh::MenuItemMatcher()
+ .action("indicator.testplayer1.desktop")
+ .label("TestPlayer1")
+ .themed_icon("icon", {"testplayer"})
+ .string_attribute("x-canonical-type", "com.canonical.unity.media-player")
+ )
+ .item(mh::MenuItemMatcher()
+ .string_attribute("x-canonical-previous-action","indicator.previous.testplayer1.desktop")
+ .string_attribute("x-canonical-play-action","indicator.play.testplayer1.desktop")
+ .string_attribute("x-canonical-next-action","indicator.next.testplayer1.desktop")
+ .string_attribute("x-canonical-type","com.canonical.unity.playback-item")
+ )
+ )
+ .item(mh::MenuItemMatcher()
+ .label("Sound Settings…")
+ )
+ ).match());
+}
+
+TEST_F(TestIndicator, DesktopAddMprisPlayer)
+{
+ double INITIAL_VOLUME = 0.0;
+
+ ASSERT_NO_THROW(startAccountsService());
+ EXPECT_TRUE(clearGSettingsPlayers());
+ ASSERT_NO_THROW(startPulseDesktop());
+
+ // initialize volumes in pulseaudio
+ EXPECT_FALSE(setStreamRestoreVolume("alert", INITIAL_VOLUME));
+ EXPECT_TRUE(setSinkVolume(INITIAL_VOLUME));
+
+ // start the test player
+ EXPECT_TRUE(startTestMprisPlayer("testplayer1"));
+
+ // start now the indicator, so it picks the new volumes
+ ASSERT_NO_THROW(startIndicator());
+
+ // check that the player is added
+ EXPECT_MATCHRESULT(mh::MenuMatcher(desktopParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .mode(mh::MenuItemMatcher::Mode::all)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(mh::MenuItemMatcher().checkbox()
+ .label("Mute")
+ )
+ .item(volumeSlider(INITIAL_VOLUME, "Volume"))
+ )
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(mh::MenuItemMatcher()
+ .action("indicator.testplayer1.desktop")
+ .label("TestPlayer1")
+ .themed_icon("icon", {"testplayer"})
+ .string_attribute("x-canonical-type", "com.canonical.unity.media-player")
+ )
+ .item(mh::MenuItemMatcher()
+ .string_attribute("x-canonical-previous-action","indicator.previous.testplayer1.desktop")
+ .string_attribute("x-canonical-play-action","indicator.play.testplayer1.desktop")
+ .string_attribute("x-canonical-next-action","indicator.next.testplayer1.desktop")
+ .string_attribute("x-canonical-type","com.canonical.unity.playback-item")
+ )
+ )
+ .item(mh::MenuItemMatcher()
+ .label("Sound Settings…")
+ )
+ ).match());
+}
+
+TEST_F(TestIndicator, DesktopMprisPlayerButtonsState)
+{
+ double INITIAL_VOLUME = 0.0;
+
+ ASSERT_NO_THROW(startAccountsService());
+ EXPECT_TRUE(clearGSettingsPlayers());
+ ASSERT_NO_THROW(startPulseDesktop());
+
+ // initialize volumes in pulseaudio
+ EXPECT_FALSE(setStreamRestoreVolume("alert", INITIAL_VOLUME));
+ EXPECT_TRUE(setSinkVolume(INITIAL_VOLUME));
+
+ // start the test player
+ EXPECT_TRUE(startTestMprisPlayer("testplayer1"));
+
+ // start now the indicator, so it picks the new volumes
+ ASSERT_NO_THROW(startIndicator());
+
+ // check that the player is added
+ EXPECT_MATCHRESULT(mh::MenuMatcher(desktopParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .mode(mh::MenuItemMatcher::Mode::all)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(mh::MenuItemMatcher().checkbox()
+ .label("Mute")
+ )
+ .item(volumeSlider(INITIAL_VOLUME, "Volume"))
+ )
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(mh::MenuItemMatcher()
+ .action("indicator.testplayer1.desktop")
+ .label("TestPlayer1")
+ .themed_icon("icon", {"testplayer"})
+ .string_attribute("x-canonical-type", "com.canonical.unity.media-player")
+ )
+ .item(mh::MenuItemMatcher()
+ .string_attribute("x-canonical-previous-action","indicator.previous.testplayer1.desktop")
+ .string_attribute("x-canonical-play-action","indicator.play.testplayer1.desktop")
+ .string_attribute("x-canonical-next-action","indicator.next.testplayer1.desktop")
+ .string_attribute("x-canonical-type","com.canonical.unity.playback-item")
+ )
+ )
+ .item(mh::MenuItemMatcher()
+ .label("Sound Settings…")
+ )
+ ).match());
+
+ // change the state of CanGoNext
+ EXPECT_TRUE(setTestMprisPlayerProperty("testplayer1", "CanGoNext", false));
+
+ // verify that the action changes
+ EXPECT_MATCHRESULT(mh::MenuMatcher(desktopParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .mode(mh::MenuItemMatcher::Mode::all)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(mh::MenuItemMatcher().checkbox()
+ .label("Mute")
+ )
+ .item(volumeSlider(INITIAL_VOLUME, "Volume"))
+ )
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(mh::MenuItemMatcher()
+ .action("indicator.testplayer1.desktop")
+ .label("TestPlayer1")
+ .themed_icon("icon", {"testplayer"})
+ .string_attribute("x-canonical-type", "com.canonical.unity.media-player")
+ )
+ .item(mh::MenuItemMatcher()
+ .string_attribute("x-canonical-previous-action","indicator.previous.testplayer1.desktop")
+ .string_attribute("x-canonical-play-action","indicator.play.testplayer1.desktop")
+ .attribute_not_set("x-canonical-next-action")
+ .string_attribute("x-canonical-type","com.canonical.unity.playback-item")
+ )
+ )
+ .item(mh::MenuItemMatcher()
+ .label("Sound Settings…")
+ )
+ ).match());
+
+
+ // change the state of CanGoPrevious
+ EXPECT_TRUE(setTestMprisPlayerProperty("testplayer1", "CanGoPrevious", false));
+
+ // verify that the action changes
+ EXPECT_MATCHRESULT(mh::MenuMatcher(desktopParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .mode(mh::MenuItemMatcher::Mode::all)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(mh::MenuItemMatcher().checkbox()
+ .label("Mute")
+ )
+ .item(volumeSlider(INITIAL_VOLUME, "Volume"))
+ )
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(mh::MenuItemMatcher()
+ .action("indicator.testplayer1.desktop")
+ .label("TestPlayer1")
+ .themed_icon("icon", {"testplayer"})
+ .string_attribute("x-canonical-type", "com.canonical.unity.media-player")
+ )
+ .item(mh::MenuItemMatcher()
+ .attribute_not_set("x-canonical-previous-action")
+ .string_attribute("x-canonical-play-action","indicator.play.testplayer1.desktop")
+ .attribute_not_set("x-canonical-next-action")
+ .string_attribute("x-canonical-type","com.canonical.unity.playback-item")
+ )
+ )
+ .item(mh::MenuItemMatcher()
+ .label("Sound Settings…")
+ )
+ ).match());
+
+ // set back both to true
+ EXPECT_TRUE(setTestMprisPlayerProperty("testplayer1", "CanGoNext", true));
+ EXPECT_TRUE(setTestMprisPlayerProperty("testplayer1", "CanGoPrevious", true));
+
+ EXPECT_MATCHRESULT(mh::MenuMatcher(desktopParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .mode(mh::MenuItemMatcher::Mode::all)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(mh::MenuItemMatcher().checkbox()
+ .label("Mute")
+ )
+ .item(volumeSlider(INITIAL_VOLUME, "Volume"))
+ )
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(mh::MenuItemMatcher()
+ .action("indicator.testplayer1.desktop")
+ .label("TestPlayer1")
+ .themed_icon("icon", {"testplayer"})
+ .string_attribute("x-canonical-type", "com.canonical.unity.media-player")
+ )
+ .item(mh::MenuItemMatcher()
+ .string_attribute("x-canonical-previous-action","indicator.previous.testplayer1.desktop")
+ .string_attribute("x-canonical-play-action","indicator.play.testplayer1.desktop")
+ .string_attribute("x-canonical-next-action","indicator.next.testplayer1.desktop")
+ .string_attribute("x-canonical-type","com.canonical.unity.playback-item")
+ )
+ )
+ .item(mh::MenuItemMatcher()
+ .label("Sound Settings…")
+ )
+ ).match());
+}
+
+TEST_F(TestIndicator, DesktopChangeRoleVolume)
+{
+ double INITIAL_VOLUME = 0.0;
+
+ ASSERT_NO_THROW(startAccountsService());
+ ASSERT_NO_THROW(startPulseDesktop());
+
+ // initialize volumes in pulseaudio
+ // expect false for stream restore, because the module
+ // is not loaded in the desktop pulseaudio instance
+ EXPECT_FALSE(setStreamRestoreVolume("mutimedia", INITIAL_VOLUME));
+ EXPECT_FALSE(setStreamRestoreVolume("alert", INITIAL_VOLUME));
+
+ EXPECT_TRUE(setSinkVolume(INITIAL_VOLUME));
+
+ // start now the indicator, so it picks the new volumes
+ ASSERT_NO_THROW(startIndicator());
+
+ // Generate a random volume
+ QTime now = QTime::currentTime();
+ qsrand(now.msec());
+ int randInt = qrand() % 100;
+ double randomVolume = randInt / 100.0;
+
+ // play a test sound, it should NOT change the role in the indicator
+ EXPECT_TRUE(startTestSound("multimedia"));
+ EXPECT_FALSE(waitVolumeChangedInIndicator());
+
+ // set the random volume
+ EXPECT_TRUE(setSinkVolume(randomVolume));
+ if (randomVolume != INITIAL_VOLUME)
+ {
+ EXPECT_TRUE(waitVolumeChangedInIndicator());
+ }
+
+ // check the indicator
+ EXPECT_MATCHRESULT(mh::MenuMatcher(desktopParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .mode(mh::MenuItemMatcher::Mode::starts_with)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(mh::MenuItemMatcher().checkbox()
+ .label("Mute")
+ )
+ .item(volumeSlider(randomVolume, "Volume"))
+ )
+ ).match());
+
+ // check that the last item is Sound Settings
+ EXPECT_MATCHRESULT(mh::MenuMatcher(desktopParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .mode(mh::MenuItemMatcher::Mode::ends_with)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .label("Sound Settings…")
+ )
+ ).match());
+
+ // stop the test sound, the role should change again to alert
+ stopTestSound();
+
+ // although we were playing something in the multimedia role
+ // the server does not have the streamrestore module, so
+ // the volume does not change (the role does not change)
+ EXPECT_FALSE(waitVolumeChangedInIndicator());
+
+ // check the initial volume for the alert role
+ EXPECT_MATCHRESULT(mh::MenuMatcher(phoneParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-scroll-action", "indicator.scroll")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .string_attribute("submenu-action", "indicator.indicator-shown")
+ .mode(mh::MenuItemMatcher::Mode::starts_with)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(silentModeSwitch(false))
+ .item(volumeSlider(randomVolume, "Volume"))
+ )
+ ).match());
+
+ // check that the last item is Sound Settings
+ EXPECT_MATCHRESULT(mh::MenuMatcher(phoneParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .mode(mh::MenuItemMatcher::Mode::ends_with)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .label("Sound Settings…")
+ .action("indicator.phone-settings")
+ )
+ ).match());
+}
+
+TEST_F(TestIndicator, PhoneNotificationVolume)
+{
+ double INITIAL_VOLUME = 0.0;
+
+ QSignalSpy notificationsSpy(&notificationsMockInterface(),
+ SIGNAL(MethodCalled(const QString &, const QVariantList &)));
+
+ ASSERT_NO_THROW(startAccountsService());
+ EXPECT_TRUE(clearGSettingsPlayers());
+ ASSERT_NO_THROW(startPulsePhone());
+
+ // initialize volumes in pulseaudio
+ EXPECT_TRUE(setStreamRestoreVolume("alert", INITIAL_VOLUME));
+
+ // start now the indicator, so it picks the new volumes
+ ASSERT_NO_THROW(startIndicator());
+
+ // check the initial state
+ EXPECT_MATCHRESULT(mh::MenuMatcher(phoneParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-scroll-action", "indicator.scroll")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .string_attribute("submenu-action", "indicator.indicator-shown")
+ .mode(mh::MenuItemMatcher::Mode::all)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(silentModeSwitch(false))
+ .item(volumeSlider(INITIAL_VOLUME, "Volume"))
+ )
+ .item(mh::MenuItemMatcher()
+ .label("Sound Settings…")
+ .action("indicator.phone-settings")
+ )
+ ).match());
+
+ // change volume to 1.0
+ setActionValue("volume", QVariant::fromValue(1.0));
+
+ WAIT_FOR_SIGNALS(notificationsSpy, 3);
+
+ // the first time we also have the calls to
+ // GetServerInformation and GetCapabilities
+ checkNotificationWithNoArgs("GetServerInformation", notificationsSpy.at(0));
+ checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(1));
+ checkVolumeNotification(1.0, "Speakers", false, notificationsSpy.at(2));
+
+ notificationsSpy.clear();
+ setActionValue("volume", QVariant::fromValue(0.0));
+
+ WAIT_FOR_SIGNALS(notificationsSpy, 1)
+
+ checkVolumeNotification(0.0, "Speakers", false, notificationsSpy.at(0));
+
+ notificationsSpy.clear();
+ setActionValue("volume", QVariant::fromValue(0.5));
+
+ WAIT_FOR_SIGNALS(notificationsSpy, 1)
+
+ checkVolumeNotification(0.5, "Speakers", false, notificationsSpy.at(0));
+}
+
+TEST_F(TestIndicator, PhoneNotificationWarningVolume)
+{
+ double INITIAL_VOLUME = 0.0;
+
+ QSignalSpy notificationsSpy(&notificationsMockInterface(),
+ SIGNAL(MethodCalled(const QString &, const QVariantList &)));
+
+ ASSERT_NO_THROW(startAccountsService());
+ ASSERT_NO_THROW(startPulsePhone());
+
+ // initialize volumes in pulseaudio
+ EXPECT_TRUE(setStreamRestoreVolume("alert", INITIAL_VOLUME));
+ EXPECT_TRUE(setStreamRestoreVolume("multimedia", INITIAL_VOLUME));
+
+ // start now the indicator, so it picks the new volumes
+ ASSERT_NO_THROW(startIndicator());
+
+ // activate the headphones
+ EXPECT_TRUE(activateHeadphones(true));
+
+ // set an initial volume to the alert role
+ setStreamRestoreVolume("alert", 1.0);
+ EXPECT_TRUE(waitVolumeChangedInIndicator());
+
+ // play a test sound, it should change the role in the indicator
+ EXPECT_TRUE(startTestSound("multimedia"));
+ EXPECT_TRUE(waitVolumeChangedInIndicator());
+
+ // change volume to 0.0... no warning should be emitted
+ setActionValue("volume", QVariant::fromValue(0.0));
+
+ WAIT_FOR_SIGNALS(notificationsSpy, 4);
+
+ // the first time we also have the calls to
+ // GetServerInformation and GetCapabilities
+ checkNotificationWithNoArgs("GetServerInformation", notificationsSpy.at(0));
+ checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(1));
+ checkVolumeNotification(0.0, "Headphones", false, notificationsSpy.at(2));
+ checkVolumeNotification(0.0, "Headphones", false, notificationsSpy.at(3));
+ notificationsSpy.clear();
+
+ // change volume to 0.5... no warning should be emitted
+ setActionValue("volume", QVariant::fromValue(0.5));
+
+ WAIT_FOR_SIGNALS(notificationsSpy, 1);
+
+ checkVolumeNotification(0.5, "Headphones", false, notificationsSpy.at(0));
+ notificationsSpy.clear();
+
+ // change volume to 1.0... warning should be emitted
+ setActionValue("volume", QVariant::fromValue(1.0));
+
+ WAIT_FOR_SIGNALS(notificationsSpy, 4);
+
+ // the notification is sent twice (TODO check why)
+ checkCloseNotification(1, notificationsSpy.at(0));
+ checkHighVolumeNotification(notificationsSpy.at(1));
+ checkCloseNotification(1, notificationsSpy.at(2));
+ checkHighVolumeNotification(notificationsSpy.at(3));
+
+ // get the last notification ID
+ int idNotification = getNotificationID(notificationsSpy.at(3));
+ ASSERT_NE(-1, idNotification);
+
+ qWarning() << "XGM: id Notification: " << idNotification;
+
+ // cancel the dialog
+ pressNotificationButton(idNotification, "cancel");
+
+ // check that the volume was clamped
+ EXPECT_MATCHRESULT(mh::MenuMatcher(phoneParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-scroll-action", "indicator.scroll")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .string_attribute("submenu-action", "indicator.indicator-shown")
+ .mode(mh::MenuItemMatcher::Mode::starts_with)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(silentModeSwitch(false))
+ .item(volumeSlider(0.74, "Volume (Headphones)"))
+ )
+ ).match());
+
+ // try again...
+ notificationsSpy.clear();
+ // change volume to 1.0... warning should be emitted
+ setActionValue("volume", QVariant::fromValue(1.0));
+
+ WAIT_FOR_SIGNALS(notificationsSpy, 4);
+
+ checkCloseNotification(1, notificationsSpy.at(0));
+ checkHighVolumeNotification(notificationsSpy.at(1));
+ checkCloseNotification(1, notificationsSpy.at(2));
+ checkHighVolumeNotification(notificationsSpy.at(3));
+
+ // get the last notification ID
+ idNotification = getNotificationID(notificationsSpy.at(3));
+ ASSERT_NE(-1, idNotification);
+
+ qWarning() << "XGM: id Notification: " << idNotification;
+
+ // this time we approve
+ pressNotificationButton(idNotification, "ok");
+
+ // check that the volume was applied
+ EXPECT_MATCHRESULT(mh::MenuMatcher(phoneParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-scroll-action", "indicator.scroll")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .string_attribute("submenu-action", "indicator.indicator-shown")
+ .mode(mh::MenuItemMatcher::Mode::starts_with)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(silentModeSwitch(false))
+ .item(volumeSlider(1.0, "Volume (Headphones)"))
+ .item(mh::MenuItemMatcher()
+ .action("indicator.high-volume-warning-item")
+ .label("High volume can damage your hearing.")
+ )
+ )
+ ).match());
+
+ // after the warning was approved we should be able to modify the volume
+ // and don't get the warning
+ notificationsSpy.clear();
+
+ // change volume to 0.5... no warning should be emitted
+ setActionValue("volume", QVariant::fromValue(0.5));
+
+ WAIT_FOR_SIGNALS(notificationsSpy, 2);
+
+ // check the notification TODO check why the sound indicator sends it twice
+ checkVolumeNotification(0.5, "Headphones", false, notificationsSpy.at(0));
+ checkVolumeNotification(0.5, "Headphones", false, notificationsSpy.at(1));
+
+ // check that the volume was applied
+ // and that we don't have the warning item
+ EXPECT_MATCHRESULT(mh::MenuMatcher(phoneParameters())
+ .item(mh::MenuItemMatcher()
+ .action("indicator.root")
+ .string_attribute("x-canonical-type", "com.canonical.indicator.root")
+ .string_attribute("x-canonical-scroll-action", "indicator.scroll")
+ .string_attribute("x-canonical-secondary-action", "indicator.mute")
+ .string_attribute("submenu-action", "indicator.indicator-shown")
+ .mode(mh::MenuItemMatcher::Mode::starts_with)
+ .submenu()
+ .item(mh::MenuItemMatcher()
+ .section()
+ .item(silentModeSwitch(false))
+ .item(volumeSlider(0.5, "Volume (Headphones)"))
+ )
+ ).match());
+
+ // now set high volume again, we should not get the warning dialog
+ // as we already approved it
+ notificationsSpy.clear();
+
+ setActionValue("volume", QVariant::fromValue(1.0));
+
+ WAIT_FOR_SIGNALS(notificationsSpy, 2);
+
+ // check the notification TODO check why the sound indicator sends it twice
+ checkVolumeNotification(1.0, "High volume can damage your hearing.", true, notificationsSpy.at(0));
+ checkVolumeNotification(1.0, "High volume can damage your hearing.", true, notificationsSpy.at(1));
+}
+
+TEST_F(TestIndicator, PhoneNotificationWarningVolumeAlertMode)
+{
+ double INITIAL_VOLUME = 0.0;
+
+ QSignalSpy notificationsSpy(&notificationsMockInterface(),
+ SIGNAL(MethodCalled(const QString &, const QVariantList &)));
+
+ ASSERT_NO_THROW(startAccountsService());
+ ASSERT_NO_THROW(startPulsePhone());
+
+ // initialize volumes in pulseaudio
+ EXPECT_TRUE(setStreamRestoreVolume("alert", INITIAL_VOLUME));
+ EXPECT_TRUE(setStreamRestoreVolume("multimedia", INITIAL_VOLUME));
+
+ // start now the indicator, so it picks the new volumes
+ ASSERT_NO_THROW(startIndicator());
+
+ // activate the headphones
+ EXPECT_TRUE(activateHeadphones(true));
+
+ // set an initial volume to the alert role
+ setStreamRestoreVolume("alert", 1.0);
+ EXPECT_TRUE(waitVolumeChangedInIndicator());
+
+ // change volume to 0.0... no warning should be emitted
+ setActionValue("volume", QVariant::fromValue(0.0));
+
+ WAIT_FOR_SIGNALS(notificationsSpy, 4);
+
+ // the first time we also have the calls to
+ // GetServerInformation and GetCapabilities
+ checkNotificationWithNoArgs("GetServerInformation", notificationsSpy.at(0));
+ checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(1));
+ checkVolumeNotification(0.0, "Headphones", false, notificationsSpy.at(2));
+ checkVolumeNotification(0.0, "Headphones", false, notificationsSpy.at(3));
+ notificationsSpy.clear();
+
+ // change volume to 0.5... no warning should be emitted
+ setActionValue("volume", QVariant::fromValue(0.5));
+
+ WAIT_FOR_SIGNALS(notificationsSpy, 1);
+
+ checkVolumeNotification(0.5, "Headphones", false, notificationsSpy.at(0));
+ notificationsSpy.clear();
+
+ // change volume to 1.0... no warning should be emitted, we are in alert mode
+ setActionValue("volume", QVariant::fromValue(1.0));
+
+ WAIT_FOR_SIGNALS(notificationsSpy, 1);
+
+ checkVolumeNotification(1.0, "Headphones", false, notificationsSpy.at(0));
+ notificationsSpy.clear();
+}
+
+TEST_F(TestIndicator, PhoneNotificationHeadphoneSpeakerWiredLabels)
+{
+ checkPortDevicesLabels(WIRED, WIRED);
+}
+
+TEST_F(TestIndicator, PhoneNotificationHeadphoneSpeakerBluetoothLabels)
+{
+ checkPortDevicesLabels(BLUETOOTH, BLUETOOTH);
+}
+
+TEST_F(TestIndicator, PhoneNotificationHeadphoneSpeakerUSBLabels)
+{
+ checkPortDevicesLabels(USB, USB);
+}
+
+TEST_F(TestIndicator, PhoneNotificationHeadphoneSpeakerHDMILabels)
+{
+ checkPortDevicesLabels(HDMI, HDMI);
+}
+
+} // namespace
diff --git a/tests/integration/test-sound.wav b/tests/integration/test-sound.wav
new file mode 100644
index 0000000..f696657
--- /dev/null
+++ b/tests/integration/test-sound.wav
Binary files differ
diff --git a/tests/integration/touch-stream-restore.table b/tests/integration/touch-stream-restore.table
new file mode 100755
index 0000000..146b02e
--- /dev/null
+++ b/tests/integration/touch-stream-restore.table
@@ -0,0 +1,4 @@
+sink-input-by-media-role:multimedia -8
+sink-input-by-media-role:alert -8
+sink-input-by-media-role:alarm -8
+sink-input-by-media-role:phone -15
diff --git a/tests/integration/utils/dbus-pulse-volume.cpp b/tests/integration/utils/dbus-pulse-volume.cpp
new file mode 100644
index 0000000..c8b6ae6
--- /dev/null
+++ b/tests/integration/utils/dbus-pulse-volume.cpp
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+
+#include "dbus-pulse-volume.h"
+
+#include "dbus_properties_interface.h"
+#include "dbus_accounts_interface.h"
+#include "dbus_accountssound_interface.h"
+#include "stream_restore_interface.h"
+
+#include <pulse/volume.h>
+
+#include <QSignalSpy>
+
+unsigned int volumeDoubleToUint(double volume)
+{
+ double tmp = (double)(PA_VOLUME_NORM - PA_VOLUME_MUTED) * volume;
+ return (unsigned int)tmp + PA_VOLUME_MUTED;
+}
+
+double volumeUIntToDoulbe(uint volume)
+{
+ double tmp = (double)(volume - PA_VOLUME_MUTED);
+ return tmp / (double)(PA_VOLUME_NORM - PA_VOLUME_MUTED);
+}
+
+DBusPulseVolume::DBusPulseVolume() :
+ QObject()
+{
+ std::unique_ptr<DBusPropertiesInterface> basicConnectionInterface(new DBusPropertiesInterface("org.PulseAudio1",
+ "/org/pulseaudio/server_lookup1",
+ QDBusConnection::sessionBus(), 0));
+
+ QDBusReply<QVariant> connection_string = basicConnectionInterface->call(QLatin1String("Get"),
+ QLatin1String("org.PulseAudio.ServerLookup1"),
+ QLatin1String("Address"));
+
+ if (!connection_string.isValid())
+ {
+ qWarning() << "DBusPulseVolume::DBusPulseVolume(): D-Bus error: " << connection_string.error().message();
+ }
+
+ connection_.reset(new QDBusConnection(QDBusConnection::connectToPeer(connection_string.value().toString(), "set-volume")));
+
+ if (connection_->isConnected())
+ {
+ interface_paths_.reset(new StreamRestoreInterface("org.PulseAudio.Ext.StreamRestore1",
+ "/org/pulseaudio/stream_restore1",
+ *(connection_.get()), 0));
+ if (interface_paths_)
+ {
+ // get the role paths
+ fillRolePath("multimedia");
+ fillRolePath("alert");
+ fillRolePath("alarm");
+ fillRolePath("phone");
+ }
+
+ initializeAccountsInterface();
+ }
+ else
+ {
+ qWarning() << "DBusPulseVolume::DBusPulseVolume(): Error connecting: " << connection_->lastError().message();
+ }
+}
+
+DBusPulseVolume::~DBusPulseVolume()
+{
+}
+
+QString DBusPulseVolume::fillRolePath(QString const &role)
+{
+ QString role_complete_name = QString("sink-input-by-media-role:") + role;
+ // get the role paths
+ QDBusReply<QDBusObjectPath> objectPath = interface_paths_->call(QLatin1String("GetEntryByName"), role_complete_name);
+ if (!objectPath.isValid())
+ {
+ qWarning() << "SetVolume::fillRolePath(): D-Bus error: " << objectPath.error().message();
+ return "";
+ }
+
+ auto role_info = std::make_shared<RoleInformation>();
+ role_info->interface_.reset(new DBusPropertiesInterface("org.PulseAudio.Ext.StreamRestore1.RestoreEntry",
+ objectPath.value().path(),
+ *(connection_.get()), 0));
+ if (!role_info->interface_)
+ {
+ qWarning() << "SetVolume::fillRolePath() - Error obtaining properties interface";
+ return "";
+ }
+ role_info->path_ = objectPath.value().path();
+ roles_map_[role] = role_info;
+ return role_info->path_;
+}
+
+bool DBusPulseVolume::setVolume(QString const & role, double volume)
+{
+ if (!interface_paths_)
+ {
+ qWarning() << "SetVolume::setVolume(): error: Volume interfaces are not initialized";
+ return false;
+ }
+
+ RolesMap::const_iterator iter = roles_map_.find(role);
+ if (iter != roles_map_.end())
+ {
+ QVariant var;
+ PulseaudioVolumeArray t;
+ PulseaudioVolume vv(0, volumeDoubleToUint(volume));
+ t.addItem(vv);
+ var.setValue(t);
+ QDBusVariant dbusVar(var);
+ QDBusReply<void> set_vol = (*iter).second->interface_->call(QLatin1String("Set"),
+ QVariant::fromValue(QString("org.PulseAudio.Ext.StreamRestore1.RestoreEntry")),
+ QVariant::fromValue(QString("Volume")),
+ QVariant::fromValue(dbusVar));
+
+ if (!set_vol.isValid())
+ {
+ qWarning() << "SetVolume::setVolume(): D-Bus error: " << set_vol.error().message();
+ return false;
+ }
+
+ if (accounts_interface_)
+ {
+ QDBusVariant dbusVar(QVariant::fromValue(volume));
+ QDBusReply<void> set_vol = accounts_interface_->call(QLatin1String("Set"),
+ QVariant::fromValue(QString("com.ubuntu.AccountsService.Sound")),
+ QVariant::fromValue(QString("Volume")),
+ QVariant::fromValue(dbusVar));
+ if (!set_vol.isValid())
+ {
+ qWarning() << "SetVolume::setVolume(): D-Bus error: " << set_vol.error().message();
+ return false;
+ }
+ }
+ }
+ else
+ {
+ qWarning() << "SetVolume::setVolume(): role " << role << " was not found.";
+ return false;
+ }
+ return true;
+}
+
+double DBusPulseVolume::getVolume(QString const & role)
+{
+ if (interface_paths_)
+ {
+ RolesMap::const_iterator iter = roles_map_.find(role);
+ if (iter != roles_map_.end())
+ {
+ QDBusReply<QVariant> prev_vol = (*iter).second->interface_->call(QLatin1String("Get"),
+ QLatin1String("org.PulseAudio.Ext.StreamRestore1.RestoreEntry"),
+ QLatin1String("Volume"));
+
+ if (!prev_vol.isValid())
+ {
+ qWarning() << "SetVolume::setVolume(): D-Bus error: " << prev_vol.error().message();
+ }
+ QDBusArgument arg = prev_vol.value().value<QDBusArgument>();
+ PulseaudioVolumeArray element;
+ arg >> element;
+ return volumeUIntToDoulbe(element.getItem(0).getVolume());
+ }
+ }
+ return -1.0;
+}
+
+void DBusPulseVolume::initializeAccountsInterface()
+{
+ auto username = qgetenv("USER");
+ if (username != "")
+ {
+ qDebug() << "Setting Accounts interface for user: " << username;
+ std::unique_ptr<AccountsInterface> setInterface(new AccountsInterface("org.freedesktop.Accounts",
+ "/org/freedesktop/Accounts",
+ QDBusConnection::systemBus(), 0));
+
+ QDBusReply<QDBusObjectPath> userResp = setInterface->call(QLatin1String("FindUserByName"),
+ QLatin1String(username));
+
+ if (!userResp.isValid())
+ {
+ qWarning() << "SetVolume::initializeAccountsInterface(): D-Bus error: " << userResp.error().message();
+ }
+ auto userPath = userResp.value().path();
+ if (userPath != "")
+ {
+ std::unique_ptr<AccountsSoundInterface> soundInterface(new AccountsSoundInterface("org.freedesktop.Accounts",
+ userPath,
+ QDBusConnection::systemBus(), 0));
+
+ accounts_interface_.reset(new DBusPropertiesInterface("org.freedesktop.Accounts",
+ userPath,
+ soundInterface->connection(), 0));
+ if (!accounts_interface_->isValid())
+ {
+ qWarning() << "SetVolume::initializeAccountsInterface(): D-Bus error: " << accounts_interface_->lastError().message();
+ }
+ signal_spy_volume_changed_.reset(new QSignalSpy(accounts_interface_.get(),&DBusPropertiesInterface::PropertiesChanged));
+ }
+ }
+}
+
+bool DBusPulseVolume::waitForVolumeChangedInAccountsService()
+{
+ if (signal_spy_volume_changed_)
+ {
+ return signal_spy_volume_changed_->wait();
+ }
+ else
+ {
+ qWarning() << "DBusPulseVolume::waitForVolumeChangedInAccountsService(): signal was not instantiated";
+ }
+ return false;
+}
diff --git a/tests/integration/utils/dbus-pulse-volume.h b/tests/integration/utils/dbus-pulse-volume.h
new file mode 100644
index 0000000..2d55e89
--- /dev/null
+++ b/tests/integration/utils/dbus-pulse-volume.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#pragma once
+
+#include <QObject>
+#include <QDBusConnection>
+
+#include <map>
+#include <memory>
+
+class StreamRestoreInterface;
+class DBusPropertiesInterface;
+class AccountsInterface;
+class AccountsSoundInterface;
+class QSignalSpy;
+
+struct RoleInformation
+{
+ std::unique_ptr<DBusPropertiesInterface> interface_;
+ QString path_;
+};
+
+class DBusPulseVolume : public QObject
+{
+public:
+ DBusPulseVolume();
+ ~DBusPulseVolume();
+
+ bool setVolume(QString const & role, double volume);
+ double getVolume(QString const & role);
+ bool waitForVolumeChangedInAccountsService();
+
+protected:
+ QString fillRolePath(QString const &role);
+ void initializeAccountsInterface();
+ std::unique_ptr<QDBusConnection> connection_;
+ std::unique_ptr<StreamRestoreInterface> interface_paths_;
+ typedef std::map<QString, std::shared_ptr<RoleInformation>> RolesMap;
+ RolesMap roles_map_;
+ std::unique_ptr<DBusPropertiesInterface> accounts_interface_;
+ std::unique_ptr<QSignalSpy> signal_spy_volume_changed_;
+};
diff --git a/tests/integration/utils/get-volume.cpp b/tests/integration/utils/get-volume.cpp
new file mode 100644
index 0000000..309da36
--- /dev/null
+++ b/tests/integration/utils/get-volume.cpp
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+
+#include "dbus-types.h"
+
+#include <string>
+#include "dbus-pulse-volume.h"
+
+int main(int argc, char **argv)
+{
+ DBusTypes::registerMetaTypes();
+ if (argc == 2)
+ {
+ DBusPulseVolume volume;
+ volume.getVolume(argv[1]);
+ }
+ return 0;
+}
diff --git a/tests/integration/utils/set-volume.cpp b/tests/integration/utils/set-volume.cpp
new file mode 100644
index 0000000..d29e5ef
--- /dev/null
+++ b/tests/integration/utils/set-volume.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+
+#include "dbus-types.h"
+
+#include <string>
+#include "dbus-pulse-volume.h"
+
+int main(int argc, char **argv)
+{
+ DBusTypes::registerMetaTypes();
+ if (argc == 3)
+ {
+ DBusPulseVolume volume;
+ if(!volume.setVolume(argv[1], std::stod(argv[2])))
+ {
+ return 1;
+ }
+ }
+ return 0;
+}
diff --git a/tests/media-player-mock.vala b/tests/media-player-mock.vala
index 14028f5..0c4ae80 100644
--- a/tests/media-player-mock.vala
+++ b/tests/media-player-mock.vala
@@ -28,6 +28,9 @@ public class MediaPlayerMock: MediaPlayer {
public override bool is_running { get { return mock_is_running; } }
public override bool can_raise { get { return mock_can_raise; } }
+ public override bool can_do_next { get { return mock_can_do_next; } }
+ public override bool can_do_prev { get { return mock_can_do_prev; } }
+ public override bool can_do_play { get { return mock_can_do_play; } }
public override MediaPlayer.Track? current_track { get { return mock_current_track; } set { this.mock_current_track = value; } }
@@ -40,6 +43,9 @@ public class MediaPlayerMock: MediaPlayer {
public bool mock_is_running { get; set; }
public bool mock_can_raise { get; set; }
+ public bool mock_can_do_next { get; set; }
+ public bool mock_can_do_prev { get; set; }
+ public bool mock_can_do_play { get; set; }
public MediaPlayer.Track? mock_current_track { get; set; }
diff --git a/tests/service-mocks/CMakeLists.txt b/tests/service-mocks/CMakeLists.txt
new file mode 100644
index 0000000..9cd8acb
--- /dev/null
+++ b/tests/service-mocks/CMakeLists.txt
@@ -0,0 +1,2 @@
+add_subdirectory(accounts-mock)
+add_subdirectory(media-player-mpris-mock)
diff --git a/tests/service-mocks/DBusPropertiesNotifier.cpp b/tests/service-mocks/DBusPropertiesNotifier.cpp
new file mode 100644
index 0000000..686e4e9
--- /dev/null
+++ b/tests/service-mocks/DBusPropertiesNotifier.cpp
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+
+#include "DBusPropertiesNotifier.h"
+
+#include <QDBusMessage>
+
+using namespace ubuntu::indicators::testing;
+
+void DBusPropertiesNotifier::notifyPropertyChanged(QDBusConnection const & connection,
+ QString const & interface,
+ QString const & path,
+ QString const & propertyName,
+ QVariant const & propertyValue)
+{
+ QDBusMessage signal = QDBusMessage::createSignal(
+ path,
+ "org.freedesktop.DBus.Properties",
+ "PropertiesChanged");
+ signal << interface;
+ QVariantMap changedProps;
+ changedProps.insert(propertyName, propertyValue);
+ signal << changedProps;
+ signal << QStringList();
+ connection.send(signal);
+}
diff --git a/tests/service-mocks/DBusPropertiesNotifier.h b/tests/service-mocks/DBusPropertiesNotifier.h
new file mode 100644
index 0000000..9fa013b
--- /dev/null
+++ b/tests/service-mocks/DBusPropertiesNotifier.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#pragma once
+
+#include <QDBusConnection>
+
+namespace ubuntu
+{
+
+namespace indicators
+{
+
+namespace testing
+{
+
+class DBusPropertiesNotifier
+{
+public:
+ DBusPropertiesNotifier() = default;
+ ~DBusPropertiesNotifier() = default;
+
+ void notifyPropertyChanged(QDBusConnection const & connection,
+ QString const & interface,
+ QString const & path,
+ QString const & propertyName,
+ QVariant const & propertyValue);
+};
+
+} // namespace testing
+
+} // namespace indicators
+
+} // namespace ubuntu
diff --git a/tests/service-mocks/accounts-mock/AccountsDefs.h b/tests/service-mocks/accounts-mock/AccountsDefs.h
new file mode 100644
index 0000000..0e4f270
--- /dev/null
+++ b/tests/service-mocks/accounts-mock/AccountsDefs.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#pragma once
+
+namespace ubuntu
+{
+
+namespace indicators
+{
+
+namespace testing
+{
+ constexpr const char ACCOUNTS_SERVICE[] = "org.freedesktop.Accounts";
+ constexpr const char USER_PATH[] = "/org/freedesktop/Accounts/UserTest";
+ constexpr const char ACCOUNTS_PATH[] = "/org/freedesktop/Accounts";
+ constexpr const char ACCOUNTS_SOUND_INTERFACE[] = "com.ubuntu.AccountsService.Sound";
+} // namespace testing
+
+} // namespace indicators
+
+} // namespace ubuntu
+
diff --git a/tests/service-mocks/accounts-mock/AccountsMock.cpp b/tests/service-mocks/accounts-mock/AccountsMock.cpp
new file mode 100644
index 0000000..5c92dc5
--- /dev/null
+++ b/tests/service-mocks/accounts-mock/AccountsMock.cpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#include <QDebug>
+
+#include "AccountsMock.h"
+#include "AccountsDefs.h"
+
+using namespace ubuntu::indicators::testing;
+
+AccountsMock::AccountsMock(QObject* parent)
+ : QObject(parent)
+{
+}
+
+AccountsMock::~AccountsMock() = default;
+
+QDBusObjectPath AccountsMock::FindUserByName(QString const & username) const
+{
+ return QDBusObjectPath(USER_PATH);
+}
+
+QDBusObjectPath AccountsMock::FindUserById(int64_t uid) const
+{
+ return QDBusObjectPath(USER_PATH);
+}
diff --git a/tests/service-mocks/accounts-mock/AccountsMock.h b/tests/service-mocks/accounts-mock/AccountsMock.h
new file mode 100644
index 0000000..72372e0
--- /dev/null
+++ b/tests/service-mocks/accounts-mock/AccountsMock.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#pragma once
+
+#include <QDBusContext>
+#include <QDBusObjectPath>
+#include <QObject>
+
+namespace ubuntu
+{
+
+namespace indicators
+{
+
+namespace testing
+{
+
+class AccountsMock : public QObject, protected QDBusContext
+{
+ Q_OBJECT
+
+public Q_SLOTS:
+ QDBusObjectPath FindUserByName(QString const & username) const;
+ QDBusObjectPath FindUserById(int64_t uid) const;
+
+public:
+ AccountsMock(QObject* parent = 0);
+ virtual ~AccountsMock();
+};
+
+} // namespace testing
+
+} // namespace indicators
+
+} // namespace ubuntu
diff --git a/tests/service-mocks/accounts-mock/AccountsServiceSoundMock.cpp b/tests/service-mocks/accounts-mock/AccountsServiceSoundMock.cpp
new file mode 100644
index 0000000..37de377
--- /dev/null
+++ b/tests/service-mocks/accounts-mock/AccountsServiceSoundMock.cpp
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#include <QDebug>
+#include <QDBusMessage>
+#include <QDBusConnection>
+
+#include "AccountsServiceSoundMock.h"
+#include "AccountsDefs.h"
+
+using namespace ubuntu::indicators::testing;
+
+AccountsServiceSoundMock::AccountsServiceSoundMock(QObject* parent)
+ : QObject(parent)
+ , volume_(0.0)
+{
+}
+
+AccountsServiceSoundMock::~AccountsServiceSoundMock() = default;
+
+double AccountsServiceSoundMock::volume() const
+{
+ return volume_;
+}
+
+void AccountsServiceSoundMock::setVolume(double volume)
+{
+ volume_ = volume;
+ notifier_.notifyPropertyChanged(QDBusConnection::systemBus(),
+ ACCOUNTS_SOUND_INTERFACE,
+ USER_PATH,
+ "Volume",
+ property("Volume"));
+}
diff --git a/tests/service-mocks/accounts-mock/AccountsServiceSoundMock.h b/tests/service-mocks/accounts-mock/AccountsServiceSoundMock.h
new file mode 100644
index 0000000..bb3dbe8
--- /dev/null
+++ b/tests/service-mocks/accounts-mock/AccountsServiceSoundMock.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#pragma once
+
+#include <QDBusContext>
+#include <QObject>
+
+#include "DBusPropertiesNotifier.h"
+
+namespace ubuntu
+{
+
+namespace indicators
+{
+
+namespace testing
+{
+
+class DBusPropertiesNotifier;
+
+class AccountsServiceSoundMock : public QObject, protected QDBusContext
+{
+ Q_OBJECT
+ Q_PROPERTY(double Volume READ volume WRITE setVolume)
+
+public Q_SLOTS:
+ double volume() const;
+ void setVolume(double volume);
+
+public:
+ AccountsServiceSoundMock(QObject* parent = 0);
+ virtual ~AccountsServiceSoundMock();
+
+private:
+ double volume_;
+ DBusPropertiesNotifier notifier_;
+};
+
+} // namespace testing
+
+} // namespace indicators
+
+} // namespace ubuntu
diff --git a/tests/service-mocks/accounts-mock/CMakeLists.txt b/tests/service-mocks/accounts-mock/CMakeLists.txt
new file mode 100644
index 0000000..bc71a8b
--- /dev/null
+++ b/tests/service-mocks/accounts-mock/CMakeLists.txt
@@ -0,0 +1,42 @@
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+find_package(Qt5DBus REQUIRED)
+include_directories(${Qt5DBus_INCLUDE_DIRS}
+ "${CMAKE_SOURCE_DIR}/tests/service-mocks")
+
+add_definitions(-DQT_NO_KEYWORDS=1)
+
+set(dbusinterface_sound_xml "com.ubuntu.AccountsService.Sound.Mock.xml")
+set_source_files_properties(${dbusinterface_sound_xml} PROPERTIES
+ CLASSNAME AccountsServiceSoundMockInterface)
+
+qt5_add_dbus_interface(interface_files ${dbusinterface_sound_xml} accountsservice_sound_interface)
+
+qt5_add_dbus_adaptor(adaptor_files
+ com.ubuntu.AccountsService.Sound.Mock.xml
+ AccountsServiceSoundMock.h
+ ubuntu::indicators::testing::AccountsServiceSoundMock
+ AccountsServiceSoundMockAdaptor)
+
+qt5_add_dbus_adaptor(adaptor_files
+ org.freedesktop.Accounts.Mock.xml
+ AccountsMock.h
+ ubuntu::indicators::testing::AccountsMock
+ AccountsMockAdaptor)
+
+add_executable(
+ accounts-service-sound
+ ${interface_files}
+ ${adaptor_files}
+ AccountsServiceSoundMock.cpp
+ AccountsMock.cpp
+ ${CMAKE_SOURCE_DIR}/tests/service-mocks/DBusPropertiesNotifier.cpp
+ main.cpp
+)
+
+qt5_use_modules(
+ accounts-service-sound
+ Core
+ DBus
+)
diff --git a/tests/service-mocks/accounts-mock/com.ubuntu.AccountsService.Sound.Mock.xml b/tests/service-mocks/accounts-mock/com.ubuntu.AccountsService.Sound.Mock.xml
new file mode 100644
index 0000000..859cd46
--- /dev/null
+++ b/tests/service-mocks/accounts-mock/com.ubuntu.AccountsService.Sound.Mock.xml
@@ -0,0 +1,6 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+ <interface name="com.ubuntu.AccountsService.Sound">
+ <property name="Volume" type="d" access="readwrite"/>
+ </interface>
+</node> \ No newline at end of file
diff --git a/tests/service-mocks/accounts-mock/main.cpp b/tests/service-mocks/accounts-mock/main.cpp
new file mode 100644
index 0000000..d6cd1d3
--- /dev/null
+++ b/tests/service-mocks/accounts-mock/main.cpp
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#include <QtDBus/QDBusConnection>
+#include <QtDBus/QDBusConnectionInterface>
+
+#include <string>
+
+#include "AccountsDefs.h"
+#include "AccountsServiceSoundMock.h"
+#include "AccountsServiceSoundMockAdaptor.h"
+#include "AccountsMock.h"
+#include "AccountsMockAdaptor.h"
+
+using namespace ubuntu::indicators::testing;
+
+int main(int argc, char *argv[])
+{
+ QCoreApplication app(argc, argv);
+ QDBusConnection connection = QDBusConnection::systemBus();
+ if (!connection.interface()->isServiceRegistered(ACCOUNTS_SERVICE))
+ {
+ auto service = new AccountsServiceSoundMock(&app);
+ new SoundAdaptor(service);
+
+ auto accounts_service = new AccountsMock(&app);
+ new AccountsAdaptor(accounts_service);
+
+ if (!connection.registerService(ACCOUNTS_SERVICE))
+ {
+ qFatal("Could not register AccountsService Volume service.");
+ }
+
+ if (!connection.registerObject(USER_PATH, service))
+ {
+ qFatal("Could not register AccountsService Volume object.");
+ }
+
+ if (!connection.registerObject(ACCOUNTS_PATH, accounts_service))
+ {
+ qFatal("Could not register Accounts object.");
+ }
+ }
+ else
+ {
+ qDebug() << "Service is already registered!.";
+ }
+ return app.exec();
+}
diff --git a/tests/service-mocks/accounts-mock/org.freedesktop.Accounts.Mock.xml b/tests/service-mocks/accounts-mock/org.freedesktop.Accounts.Mock.xml
new file mode 100644
index 0000000..f284d54
--- /dev/null
+++ b/tests/service-mocks/accounts-mock/org.freedesktop.Accounts.Mock.xml
@@ -0,0 +1,13 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+ <interface name="org.freedesktop.Accounts">
+ <method name="FindUserByName">
+ <arg direction="in" type="s" name="user" />
+ <arg direction="out" type="o" name="path" />
+ </method>
+ <method name="FindUserById">
+ <arg direction="in" type="x" name="uid" />
+ <arg direction="out" type="o" name="path" />
+ </method>
+ </interface>
+</node>
diff --git a/tests/service-mocks/media-player-mpris-mock/CMakeLists.txt b/tests/service-mocks/media-player-mpris-mock/CMakeLists.txt
new file mode 100644
index 0000000..b5f5861
--- /dev/null
+++ b/tests/service-mocks/media-player-mpris-mock/CMakeLists.txt
@@ -0,0 +1,63 @@
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -fPIC -pthread")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -fno-strict-aliasing -Wextra -fPIC -pthread")
+
+find_package(Qt5DBus REQUIRED)
+include_directories(${Qt5DBus_INCLUDE_DIRS}
+ "${CMAKE_SOURCE_DIR}/tests/service-mocks")
+
+add_definitions(-DQT_NO_KEYWORDS=1)
+
+set(dbusinterface_mediaplayermpris_xml "org.mpris.MediaPlayer2.Player.xml")
+set_source_files_properties(${dbusinterface_mediaplayermpris_xml} PROPERTIES
+ CLASSNAME MediaPlayerMprisMockInterface)
+
+qt5_add_dbus_interface(interface_files ${dbusinterface_mediaplayermpris_xml} MediaPlayerMprisMockInterface)
+
+qt5_add_dbus_adaptor(adaptor_files
+ org.mpris.MediaPlayer2.Player.xml
+ MediaPlayerMprisMock.h
+ ubuntu::indicators::testing::MediaPlayerMprisMock
+ MediaPlayerMprisMockAdaptor)
+
+qt5_add_dbus_adaptor(adaptor_files
+ org.mpris.MediaPlayer2.xml
+ MediaPlayerMprisMock.h
+ ubuntu::indicators::testing::MediaPlayerMprisMock
+ MediaPlayer2MockAdaptor)
+
+add_executable(
+ media-player-mpris-mock
+ ${adaptor_files}
+ MediaPlayerMprisMock.cpp
+ ${CMAKE_SOURCE_DIR}/tests/service-mocks/DBusPropertiesNotifier.cpp
+ main.cpp
+ testplayers
+)
+
+add_executable(
+ media-player-mpris-mock-update
+ ${interface_files}
+ player-update.cpp
+ testplayers
+)
+
+qt5_use_modules(
+ media-player-mpris-mock
+ Core
+ DBus
+)
+
+qt5_use_modules(
+ media-player-mpris-mock-update
+ Core
+ DBus
+)
+
+# test players desktop files
+add_custom_command (OUTPUT testplayers
+ DEPENDS ${CMAKE_SOURCE_DIR}/tests/service-mocks/media-player-mpris-mock/applications
+ COMMAND mkdir -p ${XDG_DATA_DIRS}
+ COMMAND cp -r ${CMAKE_SOURCE_DIR}/tests/service-mocks/media-player-mpris-mock/applications ${XDG_DATA_DIRS})
diff --git a/tests/service-mocks/media-player-mpris-mock/MediaPlayerMprisDefs.h b/tests/service-mocks/media-player-mpris-mock/MediaPlayerMprisDefs.h
new file mode 100644
index 0000000..4d28b38
--- /dev/null
+++ b/tests/service-mocks/media-player-mpris-mock/MediaPlayerMprisDefs.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#pragma once
+
+namespace ubuntu
+{
+
+namespace indicators
+{
+
+namespace testing
+{
+ constexpr const char MEDIA_PLAYER_MPRIS_SERVICE[] = "org.freedesktop.Accounts";
+ constexpr const char USER_PATH[] = "/org/freedesktop/Accounts/UserTest";
+ constexpr const char ACCOUNTS_PATH[] = "/org/freedesktop/Accounts";
+ constexpr const char ACCOUNTS_SOUND_INTERFACE[] = "com.ubuntu.AccountsService.Sound";
+} // namespace testing
+
+} // namespace indicators
+
+} // namespace ubuntu
+
diff --git a/tests/service-mocks/media-player-mpris-mock/MediaPlayerMprisMock.cpp b/tests/service-mocks/media-player-mpris-mock/MediaPlayerMprisMock.cpp
new file mode 100644
index 0000000..25fe0b7
--- /dev/null
+++ b/tests/service-mocks/media-player-mpris-mock/MediaPlayerMprisMock.cpp
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#include <QDebug>
+
+#include "MediaPlayerMprisMock.h"
+
+using namespace ubuntu::indicators::testing;
+
+MediaPlayerMprisMock::MediaPlayerMprisMock(QString const &playerName, QObject* parent)
+ : QObject(parent)
+ , can_play_(true)
+ , can_pause_(true)
+ , can_gonext_(true)
+ , can_goprevious_(true)
+ , player_name_(playerName)
+{
+}
+
+MediaPlayerMprisMock::~MediaPlayerMprisMock() = default;
+
+bool MediaPlayerMprisMock::canPlay() const
+{
+ return can_play_;
+}
+
+void MediaPlayerMprisMock::setCanPlay(bool canPlay)
+{
+ can_play_ = canPlay;
+ notifier_.notifyPropertyChanged(QDBusConnection::sessionBus(),
+ "org.mpris.MediaPlayer2.Player",
+ "/org/mpris/MediaPlayer2",
+ "CanPlay",
+ property("CanPlay"));
+}
+
+bool MediaPlayerMprisMock::canPause() const
+{
+ return can_pause_;
+}
+
+void MediaPlayerMprisMock::setCanPause(bool canPause)
+{
+ can_pause_ = canPause;
+ notifier_.notifyPropertyChanged(QDBusConnection::sessionBus(),
+ "org.mpris.MediaPlayer2.Player",
+ "/org/mpris/MediaPlayer2",
+ "CanPause",
+ property("CanPause"));
+}
+
+bool MediaPlayerMprisMock::canGoNext() const
+{
+ return can_gonext_;
+}
+
+void MediaPlayerMprisMock::setCanGoNext(bool canGoNext)
+{
+ can_gonext_ = canGoNext;
+ notifier_.notifyPropertyChanged(QDBusConnection::sessionBus(),
+ "org.mpris.MediaPlayer2.Player",
+ "/org/mpris/MediaPlayer2",
+ "CanGoNext",
+ property("CanGoNext"));
+}
+
+bool MediaPlayerMprisMock::canGoPrevious() const
+{
+ return can_goprevious_;
+}
+
+void MediaPlayerMprisMock::setCanGoPrevious(bool canGoPrevious)
+{
+ can_goprevious_ = canGoPrevious;
+ notifier_.notifyPropertyChanged(QDBusConnection::sessionBus(),
+ "org.mpris.MediaPlayer2.Player",
+ "/org/mpris/MediaPlayer2",
+ "CanGoPrevious",
+ property("CanGoPrevious"));
+}
+
+QString MediaPlayerMprisMock::desktopEntry() const
+{
+ return player_name_;
+}
+
+void MediaPlayerMprisMock::setDesktopEntry(QString const &)
+{
+}
diff --git a/tests/service-mocks/media-player-mpris-mock/MediaPlayerMprisMock.h b/tests/service-mocks/media-player-mpris-mock/MediaPlayerMprisMock.h
new file mode 100644
index 0000000..58dce8d
--- /dev/null
+++ b/tests/service-mocks/media-player-mpris-mock/MediaPlayerMprisMock.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#pragma once
+
+#include <QDBusContext>
+#include <QDBusObjectPath>
+#include <QObject>
+
+#include "DBusPropertiesNotifier.h"
+
+namespace ubuntu
+{
+
+namespace indicators
+{
+
+namespace testing
+{
+
+class MediaPlayerMprisMock : public QObject, protected QDBusContext
+{
+ Q_OBJECT
+ Q_PROPERTY(bool CanPlay READ canPlay WRITE setCanPlay)
+ Q_PROPERTY(bool CanPause READ canPause WRITE setCanPause)
+ Q_PROPERTY(bool CanGoNext READ canGoNext WRITE setCanGoNext)
+ Q_PROPERTY(bool CanGoPrevious READ canGoPrevious WRITE setCanGoPrevious)
+ Q_PROPERTY(QString DesktopEntry READ desktopEntry WRITE setDesktopEntry)
+
+public Q_SLOTS:
+ bool canPlay() const;
+ void setCanPlay(bool canPlay);
+
+ bool canPause() const;
+ void setCanPause(bool canPause);
+
+ bool canGoNext() const;
+ void setCanGoNext(bool canGoNext);
+
+ bool canGoPrevious() const;
+ void setCanGoPrevious(bool canGoPrevious);
+
+ QString desktopEntry() const;
+ void setDesktopEntry(QString const &destopEntry);
+
+public:
+ MediaPlayerMprisMock(QString const &playerName, QObject* parent = 0);
+ virtual ~MediaPlayerMprisMock();
+
+private:
+ bool can_play_;
+ bool can_pause_;
+ bool can_gonext_;
+ bool can_goprevious_;
+ DBusPropertiesNotifier notifier_;
+ QString player_name_;
+};
+
+} // namespace testing
+
+} // namespace indicators
+
+} // namespace ubuntu
diff --git a/tests/service-mocks/media-player-mpris-mock/applications/testplayer1.desktop b/tests/service-mocks/media-player-mpris-mock/applications/testplayer1.desktop
new file mode 100644
index 0000000..2ed5008
--- /dev/null
+++ b/tests/service-mocks/media-player-mpris-mock/applications/testplayer1.desktop
@@ -0,0 +1,21 @@
+[Desktop Entry]
+Name=TestPlayer1
+GenericName=Test Player 1
+X-GNOME-FullName=Test Player 1
+Comment=Play and organize your music collection
+Keywords=Audio;Song;MP3;CD;Podcast;MTP;iPod;Playlist;Last.fm;UPnP;DLNA;Radio;
+Exec=echo %U
+Terminal=false
+Type=Application
+Icon=testplayer
+X-GNOME-DocPath=testplayer/testplayer.xml
+Categories=GNOME;GTK;AudioVideo;Audio;Player;
+MimeType=application/x-ogg;application/ogg;audio/x-vorbis+ogg;audio/x-scpls;audio/x-mp3;audio/x-mpeg;audio/mpeg;audio/x-mpegurl;audio/x-flac;audio/mp4;x-scheme-handler/itms;x-scheme-handler/itmss;
+StartupNotify=true
+X-GNOME-Bugzilla-Bugzilla=GNOME
+X-GNOME-Bugzilla-Product=testplayer
+X-GNOME-Bugzilla-Component=general
+X-GNOME-Bugzilla-OtherBinaries=rhythmbox-client;rhythmbox-metadata;
+X-GNOME-Bugzilla-Version=3.1
+X-GNOME-UsesNotifications=true
+
diff --git a/tests/service-mocks/media-player-mpris-mock/main.cpp b/tests/service-mocks/media-player-mpris-mock/main.cpp
new file mode 100644
index 0000000..8945673
--- /dev/null
+++ b/tests/service-mocks/media-player-mpris-mock/main.cpp
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#include <QtDBus/QDBusConnection>
+#include <QtDBus/QDBusConnectionInterface>
+
+#include <string>
+
+#include "MediaPlayerMprisDefs.h"
+#include "MediaPlayerMprisMock.h"
+#include "MediaPlayerMprisMockAdaptor.h"
+#include "MediaPlayer2MockAdaptor.h"
+
+using namespace ubuntu::indicators::testing;
+
+int main(int argc, char *argv[])
+{
+ if (argc != 2)
+ {
+ qWarning() << "usage: " << argv[0] << "TEST_PLAYER_NAME";
+ return 1;
+ }
+ QString playerName = QString(argv[1]);
+
+ QCoreApplication app(argc, argv);
+ QString playerService = QString("org.mpris.MediaPlayer2.") + playerName;
+ QDBusConnection connection = QDBusConnection::sessionBus();
+ if (!connection.interface()->isServiceRegistered(playerService))
+ {
+ auto service = new MediaPlayerMprisMock(playerName, &app);
+ new PlayerAdaptor(service);
+ new MediaPlayer2Adaptor(service);
+
+ if (!connection.registerService(playerService))
+ {
+ qDebug() << connection.lastError();
+ qFatal("Could not register MediaPlayerMprisMock service.");
+ }
+
+ if (!connection.registerObject("/org/mpris/MediaPlayer2", service))
+ {
+ qFatal("Could not register MediaPlayerMprisMock object.");
+ }
+ }
+ else
+ {
+ qDebug() << "Service is already registered!.";
+ }
+ return app.exec();
+}
diff --git a/tests/service-mocks/media-player-mpris-mock/org.mpris.MediaPlayer2.Player.xml b/tests/service-mocks/media-player-mpris-mock/org.mpris.MediaPlayer2.Player.xml
new file mode 100644
index 0000000..3efd002
--- /dev/null
+++ b/tests/service-mocks/media-player-mpris-mock/org.mpris.MediaPlayer2.Player.xml
@@ -0,0 +1,24 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+ <interface name="org.mpris.MediaPlayer2.Player">
+ <property name="CanPlay" type="b" access="read"/>
+ <method name="setCanPlay">
+ <arg direction="in" type="b" name="canPlay" />
+ </method>
+
+ <property name="CanPause" type="b" access="read"/>
+ <method name="setCanPause">
+ <arg direction="in" type="b" name="canPause" />
+ </method>
+
+ <property name="CanGoNext" type="b" access="read"/>
+ <method name="setCanGoNext">
+ <arg direction="in" type="b" name="canGoNext" />
+ </method>
+
+ <property name="CanGoPrevious" type="b" access="read"/>
+ <method name="setCanGoPrevious">
+ <arg direction="in" type="b" name="canGoPrevious" />
+ </method>
+ </interface>
+</node>
diff --git a/tests/service-mocks/media-player-mpris-mock/org.mpris.MediaPlayer2.xml b/tests/service-mocks/media-player-mpris-mock/org.mpris.MediaPlayer2.xml
new file mode 100644
index 0000000..489c68a
--- /dev/null
+++ b/tests/service-mocks/media-player-mpris-mock/org.mpris.MediaPlayer2.xml
@@ -0,0 +1,6 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+ <interface name="org.mpris.MediaPlayer2">
+ <property name="DesktopEntry" type="s" access="read"/>
+ </interface>
+</node>
diff --git a/tests/service-mocks/media-player-mpris-mock/player-update.cpp b/tests/service-mocks/media-player-mpris-mock/player-update.cpp
new file mode 100644
index 0000000..5768372
--- /dev/null
+++ b/tests/service-mocks/media-player-mpris-mock/player-update.cpp
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 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 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/>.
+ *
+ * Author: Xavi Garcia <xavi.garcia.mena@canonical.com>
+ */
+#include "MediaPlayerMprisMockInterface.h"
+
+#include <memory>
+
+QMap<QString, QString> getPropertiesMap()
+{
+ QMap<QString, QString> retMap;
+
+ retMap["CANPLAY"] = "setCanPlay";
+ retMap["CANPAUSE"] = "setCanPause";
+ retMap["CANGONEXT"] = "setCanGoNext";
+ retMap["CANGOPREVIOUS"] = "setCanGoPrevious";
+
+ return retMap;
+}
+
+bool getBoolValue(QString const & str)
+{
+ if (str == "TRUE")
+ {
+ return true;
+ }
+ return false;
+}
+
+QTextStream& qStdErr()
+{
+ static QTextStream ts( stderr );
+ return ts;
+}
+
+int main(int argc, char **argv)
+{
+ QCoreApplication app(argc, argv);
+
+ if (argc != 4)
+ {
+ qStdErr() << "usage: " << argv[0] << "TEST_PLAYER_NAME PropertyName true|false\n";
+ return 1;
+ }
+
+ QString state = QString(argv[3]).toUpper();
+ QString property = QString(argv[2]).toUpper();
+ QString playerName = QString(argv[1]);
+
+ auto propertiesMap = getPropertiesMap();
+
+ std::shared_ptr<MediaPlayerMprisMockInterface> iface(
+ new MediaPlayerMprisMockInterface("org.mpris.MediaPlayer2." + playerName,
+ "/org/mpris/MediaPlayer2",
+ QDBusConnection::sessionBus()));
+ if (!iface)
+ {
+ qWarning() << argv[0] << ": error creating interface";
+ return 1;
+ }
+
+ QMap<QString, QString>::iterator iter = propertiesMap.find(property);
+ if (iter == propertiesMap.end())
+ {
+ qWarning() << argv[0] << ": property " << property << " was not found.";
+ return 1;
+ }
+
+ QDBusReply<void> set_prop = iface->call(QLatin1String((*iter).toStdString().c_str()),
+ QVariant::fromValue(getBoolValue(state)));
+
+ if (!set_prop.isValid())
+ {
+ qWarning() << argv[0] << ": D-Bus error: " << set_prop.error().message();
+ return 1;
+ }
+
+ return 0;
+}
+
diff --git a/tests/sound-menu.cc b/tests/sound-menu.cc
index 10c0cb9..75a661b 100644
--- a/tests/sound-menu.cc
+++ b/tests/sound-menu.cc
@@ -27,87 +27,163 @@ extern "C" {
class SoundMenuTest : public ::testing::Test
{
- protected:
- GTestDBus * bus = nullptr;
+ protected:
+ GTestDBus * bus = nullptr;
- virtual void SetUp() {
- bus = g_test_dbus_new(G_TEST_DBUS_NONE);
- g_test_dbus_up(bus);
- }
+ virtual void SetUp() {
+ bus = g_test_dbus_new(G_TEST_DBUS_NONE);
+ g_test_dbus_up(bus);
+ }
- virtual void TearDown() {
- g_test_dbus_down(bus);
- g_clear_object(&bus);
- }
+ virtual void TearDown() {
+ g_test_dbus_down(bus);
+ g_clear_object(&bus);
+ }
- void verify_item_attribute (GMenuModel * mm, guint index, const gchar * name, GVariant * value) {
- g_variant_ref_sink(value);
+ void verify_item_attribute (GMenuModel * mm, guint index, const gchar * name, GVariant * value) {
+ g_variant_ref_sink(value);
- gchar * variantstr = g_variant_print(value, TRUE);
- g_debug("Expecting item %d to have a '%s' attribute: %s", index, name, variantstr);
+ gchar * variantstr = g_variant_print(value, TRUE);
+ g_debug("Expecting item %d to have a '%s' attribute: %s", index, name, variantstr);
- const GVariantType * type = g_variant_get_type(value);
- GVariant * itemval = g_menu_model_get_item_attribute_value(mm, index, name, type);
+ const GVariantType * type = g_variant_get_type(value);
+ GVariant * itemval = g_menu_model_get_item_attribute_value(mm, index, name, type);
- ASSERT_NE(nullptr, itemval);
- EXPECT_TRUE(g_variant_equal(itemval, value));
+ ASSERT_NE(nullptr, itemval);
+ EXPECT_TRUE(g_variant_equal(itemval, value));
- g_variant_unref(value);
- }
+ g_variant_unref(value);
+ }
+
+ void verify_item_attribute_is_not_set(GMenuModel * mm, guint index, const gchar * name, const GVariantType * type) {
+ GVariant * itemval = g_menu_model_get_item_attribute_value(mm, index, name, type);
+ EXPECT_EQ(itemval, nullptr);
+ }
+
+ void check_player_control_buttons(bool canPlay, bool canNext, bool canPrev)
+ {
+ SoundMenu * menu = sound_menu_new (nullptr, SOUND_MENU_DISPLAY_FLAGS_NONE);
+
+ MediaPlayerTrack * track = media_player_track_new("Artist", "Title", "Album", "http://art.url");
+
+ MediaPlayerMock * media = MEDIA_PLAYER_MOCK(
+ g_object_new(TYPE_MEDIA_PLAYER_MOCK,
+ "mock-id", "player-id",
+ "mock-name", "Test Player",
+ "mock-state", "Playing",
+ "mock-is-running", TRUE,
+ "mock-can-raise", FALSE,
+ "mock-current-track", track,
+ "mock-can-do-play", canPlay,
+ "mock-can-do-next", canNext,
+ "mock-can-do-prev", canPrev,
+ NULL)
+ );
+ g_clear_object(&track);
+
+ sound_menu_add_player(menu, MEDIA_PLAYER(media));
+
+ ASSERT_NE(nullptr, menu->menu);
+ EXPECT_EQ(2, g_menu_model_get_n_items(G_MENU_MODEL(menu->menu)));
+
+ GMenuModel * section = g_menu_model_get_item_link(G_MENU_MODEL(menu->menu), 1, G_MENU_LINK_SECTION);
+ ASSERT_NE(nullptr, section);
+ EXPECT_EQ(2, g_menu_model_get_n_items(section)); /* No playlists, so two items */
+
+ /* Player display */
+ verify_item_attribute(section, 0, "action", g_variant_new_string("indicator.player-id"));
+ verify_item_attribute(section, 0, "x-canonical-type", g_variant_new_string("com.canonical.unity.media-player"));
+
+ /* Player control */
+ verify_item_attribute(section, 1, "x-canonical-type", g_variant_new_string("com.canonical.unity.playback-item"));
+ verify_item_attribute(section, 1, "x-canonical-play-action", g_variant_new_string(canPlay ? "indicator.play.player-id" : "indicator.play.player-id.disabled"));
+ verify_item_attribute(section, 1, "x-canonical-next-action", g_variant_new_string(canNext ? "indicator.next.player-id" : "indicator.next.player-id.disabled"));
+ verify_item_attribute(section, 1, "x-canonical-previous-action", g_variant_new_string(canPrev ? "indicator.previous.player-id" : "indicator.previous.player-id.disabled"));
+
+ g_clear_object(&section);
+
+ sound_menu_remove_player(menu, MEDIA_PLAYER(media));
+
+ EXPECT_EQ(1, g_menu_model_get_n_items(G_MENU_MODEL(menu->menu)));
+
+ g_clear_object(&media);
+ g_clear_object(&menu);
+ }
};
TEST_F(SoundMenuTest, BasicObject) {
- SoundMenu * menu = sound_menu_new (nullptr, SOUND_MENU_DISPLAY_FLAGS_NONE);
+ SoundMenu * menu = sound_menu_new (nullptr, SOUND_MENU_DISPLAY_FLAGS_NONE);
- ASSERT_NE(nullptr, menu);
+ ASSERT_NE(nullptr, menu);
- g_clear_object(&menu);
- return;
+ g_clear_object(&menu);
+ return;
}
TEST_F(SoundMenuTest, AddRemovePlayer) {
- SoundMenu * menu = sound_menu_new (nullptr, SOUND_MENU_DISPLAY_FLAGS_NONE);
+ SoundMenu * menu = sound_menu_new (nullptr, SOUND_MENU_DISPLAY_FLAGS_NONE);
+
+ MediaPlayerTrack * track = media_player_track_new("Artist", "Title", "Album", "http://art.url");
- MediaPlayerTrack * track = media_player_track_new("Artist", "Title", "Album", "http://art.url");
+ MediaPlayerMock * media = MEDIA_PLAYER_MOCK(
+ g_object_new(TYPE_MEDIA_PLAYER_MOCK,
+ "mock-id", "player-id",
+ "mock-name", "Test Player",
+ "mock-state", "Playing",
+ "mock-is-running", TRUE,
+ "mock-can-raise", FALSE,
+ "mock-current-track", track,
+ "mock-can-do-play", TRUE,
+ "mock-can-do-next", TRUE,
+ "mock-can-do-prev", TRUE,
+ NULL)
+ );
+ g_clear_object(&track);
- MediaPlayerMock * media = MEDIA_PLAYER_MOCK(
- g_object_new(TYPE_MEDIA_PLAYER_MOCK,
- "mock-id", "player-id",
- "mock-name", "Test Player",
- "mock-state", "Playing",
- "mock-is-running", TRUE,
- "mock-can-raise", FALSE,
- "mock-current-track", track,
- NULL)
- );
- g_clear_object(&track);
+ sound_menu_add_player(menu, MEDIA_PLAYER(media));
- sound_menu_add_player(menu, MEDIA_PLAYER(media));
+ ASSERT_NE(nullptr, menu->menu);
+ EXPECT_EQ(2, g_menu_model_get_n_items(G_MENU_MODEL(menu->menu)));
- ASSERT_NE(nullptr, menu->menu);
- EXPECT_EQ(2, g_menu_model_get_n_items(G_MENU_MODEL(menu->menu)));
+ GMenuModel * section = g_menu_model_get_item_link(G_MENU_MODEL(menu->menu), 1, G_MENU_LINK_SECTION);
+ ASSERT_NE(nullptr, section);
+ EXPECT_EQ(2, g_menu_model_get_n_items(section)); /* No playlists, so two items */
- GMenuModel * section = g_menu_model_get_item_link(G_MENU_MODEL(menu->menu), 1, G_MENU_LINK_SECTION);
- ASSERT_NE(nullptr, section);
- EXPECT_EQ(2, g_menu_model_get_n_items(section)); /* No playlists, so two items */
+ /* Player display */
+ verify_item_attribute(section, 0, "action", g_variant_new_string("indicator.player-id"));
+ verify_item_attribute(section, 0, "x-canonical-type", g_variant_new_string("com.canonical.unity.media-player"));
- /* Player display */
- verify_item_attribute(section, 0, "action", g_variant_new_string("indicator.player-id"));
- verify_item_attribute(section, 0, "x-canonical-type", g_variant_new_string("com.canonical.unity.media-player"));
+ /* Player control */
+ verify_item_attribute(section, 1, "x-canonical-type", g_variant_new_string("com.canonical.unity.playback-item"));
+ verify_item_attribute(section, 1, "x-canonical-play-action", g_variant_new_string("indicator.play.player-id"));
+ verify_item_attribute(section, 1, "x-canonical-next-action", g_variant_new_string("indicator.next.player-id"));
+ verify_item_attribute(section, 1, "x-canonical-previous-action", g_variant_new_string("indicator.previous.player-id"));
- /* Player control */
- verify_item_attribute(section, 1, "x-canonical-type", g_variant_new_string("com.canonical.unity.playback-item"));
- verify_item_attribute(section, 1, "x-canonical-play-action", g_variant_new_string("indicator.play.player-id"));
- verify_item_attribute(section, 1, "x-canonical-next-action", g_variant_new_string("indicator.next.player-id"));
- verify_item_attribute(section, 1, "x-canonical-previous-action", g_variant_new_string("indicator.previous.player-id"));
+ g_clear_object(&section);
- g_clear_object(&section);
+ sound_menu_remove_player(menu, MEDIA_PLAYER(media));
- sound_menu_remove_player(menu, MEDIA_PLAYER(media));
+ EXPECT_EQ(1, g_menu_model_get_n_items(G_MENU_MODEL(menu->menu)));
- EXPECT_EQ(1, g_menu_model_get_n_items(G_MENU_MODEL(menu->menu)));
+ g_clear_object(&media);
+ g_clear_object(&menu);
+ return;
+}
+
+TEST_F(SoundMenuTest, AddRemovePlayerNoPlayNextPrev) {
+ check_player_control_buttons(false, false, false);
+}
- g_clear_object(&media);
- g_clear_object(&menu);
- return;
+TEST_F(SoundMenuTest, AddRemovePlayerNoNext) {
+ check_player_control_buttons(true, false, true);
}
+
+TEST_F(SoundMenuTest, AddRemovePlayerNoPrev) {
+ check_player_control_buttons(true, true, false);
+}
+
+TEST_F(SoundMenuTest, AddRemovePlayerNoPlay) {
+ check_player_control_buttons(false, true, true);
+}
+
+//