diff options
Diffstat (limited to 'tests/integration')
-rw-r--r-- | tests/integration/CMakeLists.txt | 132 | ||||
-rw-r--r-- | tests/integration/Copy of test-sound.wav | bin | 0 -> 191989 bytes | |||
-rw-r--r-- | tests/integration/indicator-sound-test-base.cpp | 889 | ||||
-rw-r--r-- | tests/integration/indicator-sound-test-base.h | 166 | ||||
-rw-r--r-- | tests/integration/main.cpp | 58 | ||||
-rw-r--r-- | tests/integration/test-indicator.cpp | 989 | ||||
-rw-r--r-- | tests/integration/test-sound.wav | bin | 0 -> 7665640 bytes | |||
-rwxr-xr-x | tests/integration/touch-stream-restore.table | 4 | ||||
-rw-r--r-- | tests/integration/utils/dbus-pulse-volume.cpp | 232 | ||||
-rw-r--r-- | tests/integration/utils/dbus-pulse-volume.h | 57 | ||||
-rw-r--r-- | tests/integration/utils/get-volume.cpp | 33 | ||||
-rw-r--r-- | tests/integration/utils/set-volume.cpp | 36 |
12 files changed, 2596 insertions, 0 deletions
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 Binary files differnew file mode 100644 index 0000000..709c6eb --- /dev/null +++ b/tests/integration/Copy of test-sound.wav diff --git a/tests/integration/indicator-sound-test-base.cpp b/tests/integration/indicator-sound-test-base.cpp new file mode 100644 index 0000000..3b9c58d --- /dev/null +++ b/tests/integration/indicator-sound-test-base.cpp @@ -0,0 +1,889 @@ +/* + * 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 <chrono> +#include <thread> + +#include <QSignalSpy> +#include "utils/dbus-pulse-volume.h" + +using namespace QtDBusTest; +using namespace QtDBusMock; +using namespace std; +using namespace testing; +namespace mh = unity::gmenuharness; + +namespace +{ + const int MAX_TIME_WAITING_FOR_NOTIFICATIONS = 2000; +} + +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(); + + int waitedTime = 0; + while (!dbusTestRunner.sessionConnection().interface()->isServiceRegistered("org.freedesktop.Notifications") && waitedTime < MAX_TIME_WAITING_FOR_NOTIFICATIONS) + { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + waitedTime += 10; + } +} + +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 != "") + { + main_accounts_interface_.reset(new AccountsInterface("org.freedesktop.Accounts", + "/org/freedesktop/Accounts", + dbusTestRunner.systemConnection(), 0)); + + QDBusReply<QDBusObjectPath> userResp = main_accounts_interface_->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 = 0.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(¬ificationsMockInterface(), + 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(INITIAL_VOLUME, speakerString, false, notificationsSpy.at(2)); + notificationsSpy.clear(); + } + + 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(INITIAL_VOLUME, headphonesString, false, notificationsSpy.at(2)); + notificationsSpy.clear(); + } + else + { + WAIT_FOR_SIGNALS(notificationsSpy, 2); + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(0)); + checkVolumeNotification(INITIAL_VOLUME, headphonesString, false, notificationsSpy.at(1)); + 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(INITIAL_VOLUME, headphonesStringMenu)) + ) + ).match()); + + // deactivate the headphones + EXPECT_TRUE(activateHeadphones(false)); + + WAIT_FOR_SIGNALS(notificationsSpy, 2); + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(0)); + checkVolumeNotification(INITIAL_VOLUME, speakerString, false, notificationsSpy.at(1)); + 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(INITIAL_VOLUME, speakerStringMenu)) + ) + ).match()); +} + +bool IndicatorSoundTestBase::setVolumeUntilAccountsIsConnected(double volume) +{ + int RETRY_TIME = 5000; + + setActionValue("volume", QVariant::fromValue(volume)); + while(!signal_spy_volume_changed_->wait(10) && RETRY_TIME) + { + RETRY_TIME -= 10; + setActionValue("volume", QVariant::fromValue(volume)); + } + return (signal_spy_volume_changed_->count() != 0); +} + +QVariantList IndicatorSoundTestBase::getActionValue(QString const &action) +{ + if (!menu_interface_) + { + menu_interface_.reset(new MenusInterface("com.canonical.indicator.sound", + "/com/canonical/indicator/sound", + dbusTestRunner.sessionConnection(), 0)); + } + if (menu_interface_) + { + QDBusReply<DBusActionResult> resp = menu_interface_->call(QLatin1String("Describe"), + action); + if (!resp.isValid()) + { + qWarning() << "IndicatorSoundTestBase::getActionValue(): D-Bus error: " << resp.error().message(); + return QVariantList(); + } + else + { + return resp.value().getValue(); + } + } + + return QVariantList(); +} + +qlonglong IndicatorSoundTestBase::getVolumeSyncValue(bool *isValid) +{ + qlonglong result = 0; + + QVariantList varList = getActionValue("volume-sync"); + if (varList.size() == 1) + { + result = varList.at(0).toULongLong(isValid); + } + else + { + if (isValid) + { + *isValid = false; + } + } + + return result; +} + +float IndicatorSoundTestBase::getVolumeValue(bool *isValid) +{ + float result = 0.0; + + QVariantList varList = getActionValue("volume"); + if (varList.size() == 1) + { + result = varList.at(0).toFloat(isValid); + } + else + { + if (isValid) + { + *isValid = false; + } + } + + return result; +} diff --git a/tests/integration/indicator-sound-test-base.h b/tests/integration/indicator-sound-test-base.h new file mode 100644 index 0000000..67f347f --- /dev/null +++ b/tests/integration/indicator-sound-test-base.h @@ -0,0 +1,166 @@ +/* + * 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 AccountsInterface; +class QSignalSpy; + +#define WAIT_FOR_SIGNALS(signalSpy, signalsExpected)\ +{\ + while (signalSpy.size() < signalsExpected)\ + {\ + ASSERT_TRUE(signalSpy.wait());\ + }\ + ASSERT_EQ(signalsExpected, signalSpy.size());\ +} + +#define WAIT_AT_LEAST_SIGNALS(signalSpy, signalsExpected)\ +{\ + while (signalSpy.size() < signalsExpected)\ + {\ + ASSERT_TRUE(signalSpy.wait());\ + }\ + ASSERT_TRUE(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); + + bool setVolumeUntilAccountsIsConnected(double volume); + + QVariantList getActionValue(QString const &action); + + qlonglong getVolumeSyncValue(bool *isValid = nullptr); + + float getVolumeValue(bool *isValid = nullptr); + + 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<AccountsInterface> main_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..f36a764 --- /dev/null +++ b/tests/integration/test-indicator.cpp @@ -0,0 +1,989 @@ +/* + * 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; + + QSignalSpy &userAccountsSpy = *signal_spy_volume_changed_; + // set an initial volume to the alert role + userAccountsSpy.clear(); + EXPECT_TRUE(setVolumeUntilAccountsIsConnected(1.0)); + userAccountsSpy.clear(); + // play a test sound, it should change the role in the indicator + EXPECT_TRUE(startTestSound("multimedia")); + + // this time we only expect 1 signal as it's only the indicator + // updating the value + WAIT_FOR_SIGNALS(userAccountsSpy, 1); + //EXPECT_TRUE(waitVolumeChangedInIndicator()); + + userAccountsSpy.clear(); + // set the random volume to the multimedia role + setActionValue("volume", QVariant::fromValue(randomVolume)); + if (randomVolume != INITIAL_VOLUME) + { + WAIT_FOR_SIGNALS(userAccountsSpy, 1); + } + + // 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()); + + // initialize the signal spy + EXPECT_TRUE(initializeMenuChangedSignal()); + userAccountsSpy.clear(); + // stop the test sound, the role should change again to alert + stopTestSound(); + if (randomVolume != 1.0) + { + // wait for the menu change + EXPECT_TRUE(waitMenuChange()); + } + + // 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(¬ificationsMockInterface(), + 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, 2) + + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(0)); + checkVolumeNotification(0.0, "Speakers", false, notificationsSpy.at(1)); + + notificationsSpy.clear(); + setActionValue("volume", QVariant::fromValue(0.5)); + + WAIT_FOR_SIGNALS(notificationsSpy, 2) + + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(0)); + checkVolumeNotification(0.5, "Speakers", false, notificationsSpy.at(1)); +} + +TEST_F(TestIndicator, PhoneNotificationWarningVolume) +{ + double INITIAL_VOLUME = 0.0; + + QSignalSpy notificationsSpy(¬ificationsMockInterface(), + 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()); + + 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(0.0, "Headphones", false, notificationsSpy.at(2)); + notificationsSpy.clear(); + + // change volume to 0.3... no warning should be emitted + setActionValue("volume", QVariant::fromValue(0.3)); + EXPECT_TRUE(waitVolumeChangedInIndicator()); + + WAIT_FOR_SIGNALS(notificationsSpy, 2); + + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(0)); + checkVolumeNotification(0.3, "Headphones", false, notificationsSpy.at(1)); + notificationsSpy.clear(); + + // change volume to 0.5... no warning should be emitted + setActionValue("volume", QVariant::fromValue(0.5)); + EXPECT_TRUE(waitVolumeChangedInIndicator()); + + WAIT_FOR_SIGNALS(notificationsSpy, 2); + + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(0)); + checkVolumeNotification(0.5, "Headphones", false, notificationsSpy.at(1)); + notificationsSpy.clear(); + + // change volume to 1.0... warning should be emitted + setActionValue("volume", QVariant::fromValue(1.0)); + EXPECT_TRUE(waitVolumeChangedInIndicator()); + + WAIT_FOR_SIGNALS(notificationsSpy, 8); + + // the notification is sent twice (TODO check why) + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(0)); + checkCloseNotification(1, notificationsSpy.at(1)); + checkHighVolumeNotification(notificationsSpy.at(2)); + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(3)); + checkCloseNotification(1, notificationsSpy.at(4)); + checkHighVolumeNotification(notificationsSpy.at(5)); + + // get the last notification ID + int idNotification = getNotificationID(notificationsSpy.at(5)); + ASSERT_NE(-1, idNotification); + + // check the sync value before cancelling the dialog + bool isValid; + qlonglong syncValueBeforeCancel = getVolumeSyncValue(&isValid); + EXPECT_TRUE(isValid); + + // 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()); + + // verify that the sync value is increased + qlonglong syncValueAfterCancel = getVolumeSyncValue(&isValid); + EXPECT_TRUE(isValid); + EXPECT_NE(syncValueBeforeCancel, syncValueAfterCancel); + + // try again... + notificationsSpy.clear(); + + qWarning() << "-----------------------------------------------------------"; + // change volume to 1.0... warning should be emitted + setActionValue("volume", QVariant::fromValue(1.0)); + EXPECT_TRUE(waitVolumeChangedInIndicator()); + + WAIT_FOR_SIGNALS(notificationsSpy, 6); + + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(0)); + checkHighVolumeNotification(notificationsSpy.at(1)); + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(2)); + checkHighVolumeNotification(notificationsSpy.at(3)); + + // get the last notification ID + idNotification = getNotificationID(notificationsSpy.at(1)); + ASSERT_NE(-1, 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)); + EXPECT_TRUE(waitVolumeChangedInIndicator()); + + WAIT_FOR_SIGNALS(notificationsSpy, 6); + + // check the notification TODO check why the sound indicator sends it twice + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(0)); + checkCloseNotification(idNotification, notificationsSpy.at(1)); + checkVolumeNotification(0.5, "Headphones", false, notificationsSpy.at(2)); + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(3)); + checkCloseNotification(idNotification, notificationsSpy.at(4)); + checkVolumeNotification(0.5, "Headphones", false, notificationsSpy.at(5)); + + // 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)); + EXPECT_TRUE(waitVolumeChangedInIndicator()); + + WAIT_FOR_SIGNALS(notificationsSpy, 4); + + // check the notification TODO check why the sound indicator sends it twice + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(0)); + checkVolumeNotification(1.0, "Headphones", true, notificationsSpy.at(1)); + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(2)); + checkVolumeNotification(1.0, "Headphones", true, notificationsSpy.at(3)); +} + +TEST_F(TestIndicator, PhoneNotificationWarningVolumeAlertMode) +{ + double INITIAL_VOLUME = 0.0; + + QSignalSpy notificationsSpy(¬ificationsMockInterface(), + 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, 5); + + // 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)); + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(3)); + checkVolumeNotification(0.0, "Headphones", false, notificationsSpy.at(4)); + notificationsSpy.clear(); + + // change volume to 0.5... no warning should be emitted + setActionValue("volume", QVariant::fromValue(0.5)); + + WAIT_FOR_SIGNALS(notificationsSpy, 2); + + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(0)); + checkVolumeNotification(0.5, "Headphones", false, notificationsSpy.at(1)); + 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, 2); + + checkNotificationWithNoArgs("GetCapabilities", notificationsSpy.at(0)); + checkVolumeNotification(1.0, "Headphones", false, notificationsSpy.at(1)); + 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 Binary files differnew file mode 100644 index 0000000..f696657 --- /dev/null +++ b/tests/integration/test-sound.wav 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; +} |