File indexing completed on 2021-12-21 13:19:38

0001 /* This file is part of the dbusmenu-qt library
0002    Copyright 2009 Canonical
0003    Author: Aurelien Gateau <aurelien.gateau@canonical.com>
0004 
0005    This library is free software; you can redistribute it and/or
0006    modify it under the terms of the GNU Library General Public
0007    License (LGPL) as published by the Free Software Foundation;
0008    either version 2 of the License, or (at your option) any later
0009    version.
0010 
0011    This library is distributed in the hope that it will be useful,
0012    but WITHOUT ANY WARRANTY; without even the implied warranty of
0013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0014    Library General Public License for more details.
0015 
0016    You should have received a copy of the GNU Library General Public License
0017    along with this library; see the file COPYING.LIB.  If not, write to
0018    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0019    Boston, MA 02110-1301, USA.
0020 */
0021 // Self
0022 #include "dbusmenuexportertest.h"
0023 
0024 // Qt
0025 #include <QDBusConnection>
0026 #include <QDBusInterface>
0027 #include <QDBusReply>
0028 #include <QIcon>
0029 #include <QMenu>
0030 #include <QtTest>
0031 
0032 // DBusMenuQt
0033 #include <dbusmenuexporter.h>
0034 #include <dbusmenutypes_p.h>
0035 #include <dbusmenushortcut_p.h>
0036 #include <debug_p.h>
0037 
0038 // Local
0039 #include "testutils.h"
0040 
0041 QTEST_MAIN(DBusMenuExporterTest)
0042 
0043 static const char *TEST_SERVICE = "org.kde.dbusmenu-qt-test";
0044 static const char *TEST_OBJECT_PATH = "/TestMenuBar";
0045 
0046 Q_DECLARE_METATYPE(QList<int>)
0047 
0048 static DBusMenuLayoutItemList getChildren(QDBusAbstractInterface* iface, int parentId, const QStringList &propertyNames)
0049 {
0050     QDBusPendingReply<uint, DBusMenuLayoutItem> reply = iface->call("GetLayout", parentId, /*recursionDepth=*/ 1, propertyNames);
0051     reply.waitForFinished();
0052     if (!reply.isValid()) {
0053         qFatal("%s", qPrintable(reply.error().message()));
0054         return DBusMenuLayoutItemList();
0055     }
0056 
0057     DBusMenuLayoutItem rootItem = reply.argumentAt<1>();
0058     return rootItem.children;
0059 }
0060 
0061 void DBusMenuExporterTest::init()
0062 {
0063     QVERIFY(QDBusConnection::sessionBus().registerService(TEST_SERVICE));
0064     QCoreApplication::setAttribute(Qt::AA_DontShowIconsInMenus, false);
0065 }
0066 
0067 void DBusMenuExporterTest::cleanup()
0068 {
0069     QVERIFY(QDBusConnection::sessionBus().unregisterService(TEST_SERVICE));
0070 }
0071 
0072 void DBusMenuExporterTest::testGetSomeProperties_data()
0073 {
0074     QTest::addColumn<QString>("label");
0075     QTest::addColumn<QString>("iconName");
0076     QTest::addColumn<bool>("enabled");
0077 
0078     QTest::newRow("label only")           << "label" << QString()   << true;
0079     QTest::newRow("disabled, label only") << "label" << QString()   << false;
0080     QTest::newRow("icon name")            << "label" << "edit-undo" << true;
0081 }
0082 
0083 void DBusMenuExporterTest::testGetSomeProperties()
0084 {
0085     QFETCH(QString, label);
0086     QFETCH(QString, iconName);
0087     QFETCH(bool, enabled);
0088 
0089     // Create an exporter for a menu with one action, defined by the test data
0090     QMenu inputMenu;
0091     DBusMenuExporter exporter(TEST_OBJECT_PATH, &inputMenu);
0092 
0093     QAction *action = new QAction(label, &inputMenu);
0094     if (!iconName.isEmpty()) {
0095         QIcon icon = QIcon::fromTheme(iconName);
0096         QVERIFY(!icon.isNull());
0097         action->setIcon(icon);
0098     }
0099     action->setEnabled(enabled);
0100     inputMenu.addAction(action);
0101 
0102     // Check out exporter is on DBus
0103     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0104     QVERIFY2(iface.isValid(), qPrintable(iface.lastError().message()));
0105 
0106     // Get exported menu info
0107     QStringList propertyNames = QStringList() << "type" << "enabled" << "label" << "icon-name";
0108     DBusMenuLayoutItemList list = getChildren(&iface, /*parentId=*/0, propertyNames);
0109     DBusMenuLayoutItem item = list.first();
0110     QVERIFY(item.id != 0);
0111     QVERIFY(item.children.isEmpty());
0112     QVERIFY(!item.properties.contains("type"));
0113     QCOMPARE(item.properties.value("label").toString(), label);
0114     if (enabled) {
0115         QVERIFY(!item.properties.contains("enabled"));
0116     } else {
0117         QCOMPARE(item.properties.value("enabled").toBool(), false);
0118     }
0119     if (iconName.isEmpty()) {
0120         QVERIFY(!item.properties.contains("icon-name"));
0121     } else {
0122         QCOMPARE(item.properties.value("icon-name").toString(), iconName);
0123     }
0124 }
0125 
0126 void DBusMenuExporterTest::testGetAllProperties()
0127 {
0128     // set of properties which must be returned because their values are not
0129     // the default values
0130     const QSet<QString> a1Properties = QSet<QString>()
0131         << "label"
0132         ;
0133 
0134     const QSet<QString> separatorProperties = QSet<QString>()
0135         << "type";
0136 
0137     const QSet<QString> a2Properties = QSet<QString>()
0138         << "label"
0139         << "enabled"
0140         << "icon-name"
0141         << "icon-data" // Icon data is always provided if the icon is valid.
0142         << "visible"
0143         ;
0144 
0145     // Create the menu items
0146     QMenu inputMenu;
0147     DBusMenuExporter exporter(TEST_OBJECT_PATH, &inputMenu);
0148 
0149     inputMenu.addAction("a1");
0150 
0151     inputMenu.addSeparator();
0152 
0153     QAction *a2 = new QAction("a2", &inputMenu);
0154     a2->setEnabled(false);
0155     QIcon icon = QIcon::fromTheme("edit-undo");
0156     QVERIFY(!icon.isNull());
0157     a2->setIcon(icon);
0158     a2->setVisible(false);
0159     inputMenu.addAction(a2);
0160 
0161     // Export them
0162     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0163     QVERIFY2(iface.isValid(), qPrintable(iface.lastError().message()));
0164 
0165     // Get children
0166     DBusMenuLayoutItemList list = getChildren(&iface, 0, QStringList());
0167     QCOMPARE(list.count(), 3);
0168 
0169     // Check we get the right properties
0170     DBusMenuLayoutItem item = list.takeFirst();
0171     QCOMPARE(QSet<QString>::fromList(item.properties.keys()), a1Properties);
0172 
0173     item = list.takeFirst();
0174     QCOMPARE(QSet<QString>::fromList(item.properties.keys()), separatorProperties);
0175 
0176     item = list.takeFirst();
0177     QCOMPARE(QSet<QString>::fromList(item.properties.keys()), a2Properties);
0178 }
0179 
0180 void DBusMenuExporterTest::testGetNonExistentProperty()
0181 {
0182     const char* NON_EXISTENT_KEY = "i-do-not-exist";
0183 
0184     QMenu inputMenu;
0185     inputMenu.addAction("a1");
0186     DBusMenuExporter exporter(TEST_OBJECT_PATH, &inputMenu);
0187 
0188     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0189     DBusMenuLayoutItemList list = getChildren(&iface, 0, QStringList() << NON_EXISTENT_KEY);
0190     QCOMPARE(list.count(), 1);
0191 
0192     DBusMenuLayoutItem item = list.takeFirst();
0193     QVERIFY(!item.properties.contains(NON_EXISTENT_KEY));
0194 }
0195 
0196 void DBusMenuExporterTest::testClickedEvent()
0197 {
0198     QMenu inputMenu;
0199     QAction *action = inputMenu.addAction("a1");
0200     QSignalSpy spy(action, SIGNAL(triggered()));
0201     DBusMenuExporter exporter(TEST_OBJECT_PATH, &inputMenu);
0202 
0203     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0204     DBusMenuLayoutItemList list = getChildren(&iface, 0, QStringList());
0205     QCOMPARE(list.count(), 1);
0206     int id = list.first().id;
0207 
0208     QVariant empty = QVariant::fromValue(QDBusVariant(QString()));
0209     uint timestamp = QDateTime::currentDateTime().toTime_t();
0210     iface.call("Event", id, "clicked", empty, timestamp);
0211     QTest::qWait(500);
0212 
0213     QCOMPARE(spy.count(), 1);
0214 }
0215 
0216 void DBusMenuExporterTest::testSubMenu()
0217 {
0218     QMenu inputMenu;
0219     QMenu *subMenu = inputMenu.addMenu("menu");
0220     QAction *a1 = subMenu->addAction("a1");
0221     QAction *a2 = subMenu->addAction("a2");
0222     DBusMenuExporter exporter(TEST_OBJECT_PATH, &inputMenu);
0223 
0224     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0225     DBusMenuLayoutItemList list = getChildren(&iface, 0, QStringList());
0226     QCOMPARE(list.count(), 1);
0227     int id = list.first().id;
0228 
0229     list = getChildren(&iface, id, QStringList());
0230     QCOMPARE(list.count(), 2);
0231 
0232     DBusMenuLayoutItem item = list.takeFirst();
0233     QVERIFY(item.id != 0);
0234     QCOMPARE(item.properties.value("label").toString(), a1->text());
0235 
0236     item = list.takeFirst();
0237     QCOMPARE(item.properties.value("label").toString(), a2->text());
0238 }
0239 
0240 void DBusMenuExporterTest::testDynamicSubMenu()
0241 {
0242     // Track LayoutUpdated() signal: we don't want this signal to be emitted
0243     // too often because it causes refreshes
0244     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0245     ManualSignalSpy layoutUpdatedSpy;
0246     QDBusConnection::sessionBus().connect(TEST_SERVICE, TEST_OBJECT_PATH, "com.canonical.dbusmenu", "LayoutUpdated", "ui", &layoutUpdatedSpy, SLOT(receiveCall(uint, int)));
0247 
0248     // Create our test menu
0249     QMenu inputMenu;
0250     DBusMenuExporter exporter(TEST_OBJECT_PATH, &inputMenu);
0251     QAction *action = inputMenu.addAction("menu");
0252     QMenu *subMenu = new QMenu(&inputMenu);
0253     action->setMenu(subMenu);
0254     MenuFiller filler(subMenu);
0255     filler.addAction(new QAction("a1", subMenu));
0256     filler.addAction(new QAction("a2", subMenu));
0257 
0258     // Get id of submenu
0259     DBusMenuLayoutItemList list = getChildren(&iface, 0, QStringList());
0260     QCOMPARE(list.count(), 1);
0261     int id = list.first().id;
0262 
0263     // Nothing for now
0264     QCOMPARE(subMenu->actions().count(), 0);
0265 
0266     // LayoutUpdated should be emitted once because inputMenu is filled
0267     QTest::qWait(500);
0268     QCOMPARE(layoutUpdatedSpy.count(), 1);
0269     QCOMPARE(layoutUpdatedSpy.takeFirst().at(1).toInt(), 0);
0270 
0271     // Pretend we show the menu
0272     QDBusReply<bool> aboutToShowReply = iface.call("AboutToShow", id);
0273     QVERIFY2(aboutToShowReply.isValid(), qPrintable(aboutToShowReply.error().message()));
0274     QVERIFY(aboutToShowReply.value());
0275     QTest::qWait(500);
0276     QCOMPARE(layoutUpdatedSpy.count(), 1);
0277     QCOMPARE(layoutUpdatedSpy.takeFirst().at(1).toInt(), id);
0278 
0279     // Get submenu items
0280     list = getChildren(&iface, id, QStringList());
0281     QVERIFY(subMenu->actions().count() > 0);
0282     QCOMPARE(list.count(), subMenu->actions().count());
0283 
0284     for (int pos=0; pos< list.count(); ++pos) {
0285         DBusMenuLayoutItem item = list.at(pos);
0286         QVERIFY(item.id != 0);
0287         QAction *action = subMenu->actions().at(pos);
0288         QVERIFY(action);
0289         QCOMPARE(item.properties.value("label").toString(), action->text());
0290     }
0291 }
0292 
0293 void DBusMenuExporterTest::testRadioItems()
0294 {
0295     DBusMenuLayoutItem item;
0296     DBusMenuLayoutItemList list;
0297     QMenu inputMenu;
0298     QVERIFY(QDBusConnection::sessionBus().registerService(TEST_SERVICE));
0299     DBusMenuExporter exporter(TEST_OBJECT_PATH, &inputMenu);
0300 
0301     // Create 2 radio items, check first one
0302     QAction *a1 = inputMenu.addAction("a1");
0303     a1->setCheckable(true);
0304     QAction *a2 = inputMenu.addAction("a1");
0305     a2->setCheckable(true);
0306 
0307     QActionGroup group(0);
0308     group.addAction(a1);
0309     group.addAction(a2);
0310     a1->setChecked(true);
0311 
0312     QVERIFY(!a2->isChecked());
0313 
0314     // Get item ids
0315     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0316     list = getChildren(&iface, 0, QStringList());
0317     QCOMPARE(list.count(), 2);
0318 
0319     // Check items are radios and correctly toggled
0320     item = list.takeFirst();
0321     QCOMPARE(item.properties.value("toggle-type").toString(), QString("radio"));
0322     QCOMPARE(item.properties.value("toggle-state").toInt(), 1);
0323     int a1Id = item.id;
0324     item = list.takeFirst();
0325     QCOMPARE(item.properties.value("toggle-type").toString(), QString("radio"));
0326     QCOMPARE(item.properties.value("toggle-state").toInt(), 0);
0327     int a2Id = item.id;
0328 
0329     // Click a2
0330     ManualSignalSpy spy;
0331     QDBusConnection::sessionBus().connect(TEST_SERVICE, TEST_OBJECT_PATH, "com.canonical.dbusmenu", "ItemsPropertiesUpdated", "a(ia{sv})a(ias)",
0332         &spy, SLOT(receiveCall(DBusMenuItemList, DBusMenuItemKeysList)));
0333     QVariant empty = QVariant::fromValue(QDBusVariant(QString()));
0334     uint timestamp = QDateTime::currentDateTime().toTime_t();
0335     iface.call("Event", a2Id, "clicked", empty, timestamp);
0336     QTest::qWait(500);
0337 
0338     // Check a1 is not checked, but a2 is
0339     list = getChildren(&iface, 0, QStringList());
0340     QCOMPARE(list.count(), 2);
0341 
0342     item = list.takeFirst();
0343     QCOMPARE(item.properties.value("toggle-state").toInt(), 0);
0344 
0345     item = list.takeFirst();
0346     QCOMPARE(item.properties.value("toggle-state").toInt(), 1);
0347 
0348     // Did we get notified?
0349     QCOMPARE(spy.count(), 1);
0350     QSet<int> updatedIds;
0351     {
0352         QVariantList lst = spy.takeFirst().at(0).toList();
0353         Q_FOREACH(QVariant variant, lst) {
0354             updatedIds << variant.toInt();
0355         }
0356     }
0357 
0358     QSet<int> expectedIds;
0359     expectedIds << a1Id << a2Id;
0360 
0361     QCOMPARE(updatedIds, expectedIds);
0362 }
0363 
0364 void DBusMenuExporterTest::testNonExclusiveActionGroup()
0365 {
0366     DBusMenuLayoutItem item;
0367     DBusMenuLayoutItemList list;
0368     QMenu inputMenu;
0369     QVERIFY(QDBusConnection::sessionBus().registerService(TEST_SERVICE));
0370     DBusMenuExporter exporter(TEST_OBJECT_PATH, &inputMenu);
0371 
0372     // Create 2 checkable items
0373     QAction *a1 = inputMenu.addAction("a1");
0374     a1->setCheckable(true);
0375     QAction *a2 = inputMenu.addAction("a1");
0376     a2->setCheckable(true);
0377 
0378     // Put them into a non exclusive group
0379     QActionGroup group(0);
0380     group.addAction(a1);
0381     group.addAction(a2);
0382     group.setExclusive(false);
0383 
0384     // Get item ids
0385     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0386     list = getChildren(&iface, 0, QStringList());
0387     QCOMPARE(list.count(), 2);
0388 
0389     // Check items are checkmark, not radio
0390     item = list.takeFirst();
0391     QCOMPARE(item.properties.value("toggle-type").toString(), QString("checkmark"));
0392     int a1Id = item.id;
0393     item = list.takeFirst();
0394     QCOMPARE(item.properties.value("toggle-type").toString(), QString("checkmark"));
0395     int a2Id = item.id;
0396 }
0397 
0398 void DBusMenuExporterTest::testClickDeletedAction()
0399 {
0400     QMenu inputMenu;
0401     QVERIFY(QDBusConnection::sessionBus().registerService(TEST_SERVICE));
0402     DBusMenuExporter exporter(TEST_OBJECT_PATH, &inputMenu);
0403 
0404     QAction *a1 = inputMenu.addAction("a1");
0405 
0406     // Get id
0407     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0408     DBusMenuLayoutItemList list = getChildren(&iface, 0, QStringList());
0409     QCOMPARE(list.count(), 1);
0410     int id = list.takeFirst().id;
0411 
0412     // Delete a1, it should not cause a crash when trying to trigger it
0413     delete a1;
0414 
0415     // Send a click to deleted a1
0416     QVariant empty = QVariant::fromValue(QDBusVariant(QString()));
0417     uint timestamp = QDateTime::currentDateTime().toTime_t();
0418     iface.call("Event", id, "clicked", empty, timestamp);
0419     QTest::qWait(500);
0420 }
0421 
0422 // Reproduce LP BUG 521011
0423 // https://bugs.launchpad.net/bugs/521011
0424 void DBusMenuExporterTest::testDeleteExporterBeforeMenu()
0425 {
0426     QMenu inputMenu;
0427     QVERIFY(QDBusConnection::sessionBus().registerService(TEST_SERVICE));
0428     DBusMenuExporter *exporter = new DBusMenuExporter(TEST_OBJECT_PATH, &inputMenu);
0429 
0430     QAction *a1 = inputMenu.addAction("a1");
0431     delete exporter;
0432     inputMenu.removeAction(a1);
0433 }
0434 
0435 void DBusMenuExporterTest::testUpdateAndDeleteSubMenu()
0436 {
0437     // Create a menu with a submenu
0438     QMenu inputMenu;
0439     QMenu *subMenu = inputMenu.addMenu("menu");
0440     QAction *a1 = subMenu->addAction("a1");
0441 
0442     // Export it
0443     QVERIFY(QDBusConnection::sessionBus().registerService(TEST_SERVICE));
0444     DBusMenuExporter exporter(TEST_OBJECT_PATH, &inputMenu);
0445 
0446     // Update a1 (which is in subMenu) and delete subMenu right after that. If
0447     // DBusMenuExporter is not careful it will crash in the qWait() because it
0448     // tries to send itemUpdated() for a1.
0449     a1->setText("Not a menu anymore");
0450     delete subMenu;
0451     QTest::qWait(500);
0452 }
0453 
0454 void DBusMenuExporterTest::testMenuShortcut()
0455 {
0456     // Create a menu containing an action with a shortcut
0457     QMenu inputMenu;
0458     QVERIFY(QDBusConnection::sessionBus().registerService(TEST_SERVICE));
0459     DBusMenuExporter *exporter = new DBusMenuExporter(TEST_OBJECT_PATH, &inputMenu);
0460 
0461     QAction *a1 = inputMenu.addAction("a1");
0462     a1->setShortcut(Qt::CTRL | Qt::Key_A);
0463 
0464     QAction *a2 = inputMenu.addAction("a2");
0465     a2->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_A, Qt::ALT | Qt::Key_B));
0466 
0467     // No shortcut, to test the property is not added in this case
0468     QAction *a3 = inputMenu.addAction("a3");
0469 
0470     QList<QAction*> actionList;
0471     actionList << a1 << a2 << a3;
0472 
0473     // Check out exporter is on DBus
0474     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0475     QVERIFY2(iface.isValid(), qPrintable(iface.lastError().message()));
0476 
0477     // Get exported menu info
0478     QStringList propertyNames = QStringList() << "label" << "shortcut";
0479     DBusMenuLayoutItemList list = getChildren(&iface, 0, propertyNames);
0480     QCOMPARE(list.count(), actionList.count());
0481 
0482     Q_FOREACH(const QAction* action, actionList) {
0483         DBusMenuLayoutItem item = list.takeFirst();
0484         if (action->shortcut().isEmpty()) {
0485             QVERIFY(!item.properties.contains("shortcut"));
0486         } else {
0487             QVERIFY(item.properties.contains("shortcut"));
0488             QDBusArgument arg = item.properties.value("shortcut").value<QDBusArgument>();
0489             DBusMenuShortcut shortcut;
0490             arg >> shortcut;
0491             QCOMPARE(shortcut.toKeySequence(), action->shortcut());
0492         }
0493     }
0494 }
0495 
0496 void DBusMenuExporterTest::testGetGroupProperties()
0497 {
0498     // Create a menu containing two actions
0499     QMenu inputMenu;
0500     QVERIFY(QDBusConnection::sessionBus().registerService(TEST_SERVICE));
0501     DBusMenuExporter *exporter = new DBusMenuExporter(TEST_OBJECT_PATH, &inputMenu);
0502 
0503     QAction *a1 = inputMenu.addAction("a1");
0504     QAction *a2 = inputMenu.addAction("a2");
0505 
0506     // Check exporter is on DBus
0507     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0508     QVERIFY2(iface.isValid(), qPrintable(iface.lastError().message()));
0509 
0510     // Get item ids
0511     DBusMenuLayoutItemList list = getChildren(&iface, 0, QStringList());
0512     QCOMPARE(list.count(), inputMenu.actions().count());
0513 
0514     int id1 = list.at(0).id;
0515     int id2 = list.at(1).id;
0516 
0517     // Get group properties
0518     QList<int> ids = QList<int>() << id1 << id2;
0519     QDBusReply<DBusMenuItemList> reply = iface.call("GetGroupProperties", QVariant::fromValue(ids), QStringList());
0520     QVERIFY2(reply.isValid(), qPrintable(reply.error().message()));
0521     DBusMenuItemList groupPropertiesList = reply.value();
0522 
0523     // Check the info we received
0524     QCOMPARE(groupPropertiesList.count(), inputMenu.actions().count());
0525 
0526     Q_FOREACH(const QAction* action, inputMenu.actions()) {
0527         DBusMenuItem item = groupPropertiesList.takeFirst();
0528         QCOMPARE(item.properties.value("label").toString(), action->text());
0529     }
0530 }
0531 
0532 void DBusMenuExporterTest::testActivateAction()
0533 {
0534     // Create a menu containing two actions
0535     QMenu inputMenu;
0536     QVERIFY(QDBusConnection::sessionBus().registerService(TEST_SERVICE));
0537     DBusMenuExporter *exporter = new DBusMenuExporter(TEST_OBJECT_PATH, &inputMenu);
0538 
0539     QAction *a1 = inputMenu.addAction("a1");
0540     QAction *a2 = inputMenu.addAction("a2");
0541 
0542     // Check exporter is on DBus
0543     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0544     QVERIFY2(iface.isValid(), qPrintable(iface.lastError().message()));
0545 
0546     ManualSignalSpy spy;
0547     QDBusConnection::sessionBus().connect(TEST_SERVICE, TEST_OBJECT_PATH, "com.canonical.dbusmenu", "ItemActivationRequested", "iu", &spy, SLOT(receiveCall(int, uint)));
0548 
0549     // Get item ids
0550     DBusMenuLayoutItemList list = getChildren(&iface, 0, QStringList());
0551     QCOMPARE(list.count(), inputMenu.actions().count());
0552 
0553     int id1 = list.at(0).id;
0554     int id2 = list.at(1).id;
0555 
0556     // Trigger actions
0557     exporter->activateAction(a1);
0558     exporter->activateAction(a2);
0559 
0560     // Check we received the signals in the correct order
0561     QTest::qWait(500);
0562     QCOMPARE(spy.count(), 2);
0563     QCOMPARE(spy.takeFirst().at(0).toInt(), id1);
0564     QCOMPARE(spy.takeFirst().at(0).toInt(), id2);
0565 }
0566 
0567 static int trackCount(QMenu* menu)
0568 {
0569     QList<QObject*> lst = menu->findChildren<QObject*>();
0570     int count = 0;
0571     Q_FOREACH(QObject* child, lst) {
0572         if (qstrcmp(child->metaObject()->className(), "DBusMenu") == 0) {
0573             ++count;
0574         }
0575     }
0576     return count;
0577 }
0578 
0579 // Check we do not create more than one DBusMenu object for each menu
0580 // See KDE bug 254066
0581 void DBusMenuExporterTest::testTrackActionsOnlyOnce()
0582 {
0583     // Create a menu with a submenu, unplug the submenu and plug it back. The
0584     // submenu should not have more than one DBusMenu child object.
0585     QMenu mainMenu;
0586     QVERIFY(QDBusConnection::sessionBus().registerService(TEST_SERVICE));
0587     DBusMenuExporter *exporter = new DBusMenuExporter(TEST_OBJECT_PATH, &mainMenu);
0588 
0589     QMenu* subMenu = new QMenu("File");
0590     subMenu->addAction("a1");
0591     mainMenu.addAction(subMenu->menuAction());
0592 
0593     QTest::qWait(500);
0594     QCOMPARE(trackCount(subMenu), 1);
0595 
0596     mainMenu.removeAction(subMenu->menuAction());
0597 
0598     mainMenu.addAction(subMenu->menuAction());
0599 
0600     QTest::qWait(500);
0601     QCOMPARE(trackCount(subMenu), 1);
0602 }
0603 
0604 // If desktop does not want icon in menus, check we do not export them
0605 void DBusMenuExporterTest::testHonorDontShowIconsInMenusAttribute()
0606 {
0607     QCoreApplication::setAttribute(Qt::AA_DontShowIconsInMenus, true);
0608     QMenu inputMenu;
0609     DBusMenuExporter exporter(TEST_OBJECT_PATH, &inputMenu);
0610 
0611     QAction *action = new QAction("Undo", &inputMenu);
0612     QIcon icon = QIcon::fromTheme("edit-undo");
0613     QVERIFY(!icon.isNull());
0614     action->setIcon(icon);
0615     inputMenu.addAction(action);
0616 
0617     // Check out exporter is on DBus
0618     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0619     QVERIFY2(iface.isValid(), qPrintable(iface.lastError().message()));
0620 
0621     // Get exported menu info
0622     QStringList propertyNames = QStringList() << "icon-name";
0623     DBusMenuLayoutItemList list = getChildren(&iface, /*parentId=*/0, propertyNames);
0624     DBusMenuLayoutItem item = list.first();
0625     QVERIFY(item.id != 0);
0626     QVERIFY(!item.properties.contains("icon-name"));
0627 }
0628 
0629 static bool hasInternalDBusMenuObject(QMenu* menu)
0630 {
0631     Q_FOREACH(QObject* obj, menu->children()) {
0632         if (obj->inherits("DBusMenu")) {
0633             return true;
0634         }
0635     }
0636     return false;
0637 }
0638 
0639 // DBusMenuExporter adds an instance of an internal class named "DBusMenu" to
0640 // any QMenu it tracks. Check they go away when the exporter is deleted.
0641 void DBusMenuExporterTest::testDBusMenuObjectIsDeletedWhenExporterIsDeleted()
0642 {
0643     QMenu inputMenu;
0644     QVERIFY(QDBusConnection::sessionBus().registerService(TEST_SERVICE));
0645     DBusMenuExporter *exporter = new DBusMenuExporter(TEST_OBJECT_PATH, &inputMenu);
0646 
0647     QAction *a1 = inputMenu.addAction("a1");
0648     QVERIFY2(hasInternalDBusMenuObject(&inputMenu), "Test setup failed");
0649     delete exporter;
0650     QVERIFY(!hasInternalDBusMenuObject(&inputMenu));
0651 }
0652 
0653 void DBusMenuExporterTest::testSeparatorCollapsing_data()
0654 {
0655     QTest::addColumn<QString>("input");
0656     QTest::addColumn<QString>("expected");
0657 
0658     QTest::newRow("one-separator")         << "a-b"           << "a-b";
0659     QTest::newRow("two-separators")        << "a-b-c"         << "a-b-c";
0660     QTest::newRow("middle-separators")     << "a--b"          << "a-b";
0661     QTest::newRow("separators-at-begin")   << "--a-b"         << "a-b";
0662     QTest::newRow("separators-at-end")     << "a-b--"         << "a-b";
0663     QTest::newRow("separators-everywhere") << "--a---bc--d--" << "a-bc-d";
0664     QTest::newRow("empty-menu")            << ""              << "";
0665     QTest::newRow("separators-only")       << "---"           << "";
0666 }
0667 
0668 void DBusMenuExporterTest::testSeparatorCollapsing()
0669 {
0670     QFETCH(QString, input);
0671     QFETCH(QString, expected);
0672 
0673     // Create menu from menu string
0674     QMenu inputMenu;
0675 
0676     QVERIFY(QDBusConnection::sessionBus().registerService(TEST_SERVICE));
0677     DBusMenuExporter *exporter = new DBusMenuExporter(TEST_OBJECT_PATH, &inputMenu);
0678 
0679     if (input.isEmpty()) {
0680         // Pretend there was an action so that doEmitLayoutUpdated() is called
0681         // even if the new menu is empty. If we don't do this we don't test
0682         // DBusMenuExporterPrivate::collapseSeparators() for empty menus.
0683         delete inputMenu.addAction("dummy");
0684     }
0685 
0686     Q_FOREACH(QChar ch, input) {
0687         if (ch == '-') {
0688             inputMenu.addSeparator();
0689         } else {
0690             inputMenu.addAction(ch);
0691         }
0692     }
0693 
0694     QTest::qWait(500);
0695 
0696     // Check out exporter is on DBus
0697     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0698     QVERIFY2(iface.isValid(), qPrintable(iface.lastError().message()));
0699 
0700     // Get exported menu info
0701     QStringList propertyNames = QStringList();
0702     DBusMenuLayoutItemList list = getChildren(&iface, /*parentId=*/0, propertyNames);
0703 
0704     // Recreate a menu string from the item list
0705     QString output;
0706     Q_FOREACH(const DBusMenuLayoutItem& item, list) {
0707         QVariantMap properties = item.properties;
0708         if (properties.contains("visible") && !properties.value("visible").toBool()) {
0709             continue;
0710         }
0711         QString type = properties.value("type").toString();
0712         if (type == "separator") {
0713             output += '-';
0714         } else {
0715             output += properties.value("label").toString();
0716         }
0717     }
0718 
0719     // Check it matches
0720     QCOMPARE(output, expected);
0721 }
0722 
0723 static void checkPropertiesChangedArgs(const QVariantList& args, const QString& name, const QVariant& value)
0724 {
0725     QCOMPARE(args[0].toString(), QString("com.canonical.dbusmenu"));
0726     QVariantMap map;
0727     map.insert(name, value);
0728     QCOMPARE(args[1].toMap(), map);
0729     QCOMPARE(args[2].toStringList(), QStringList());
0730 }
0731 
0732 void DBusMenuExporterTest::testSetStatus()
0733 {
0734     QMenu inputMenu;
0735     QVERIFY(QDBusConnection::sessionBus().registerService(TEST_SERVICE));
0736     DBusMenuExporter *exporter = new DBusMenuExporter(TEST_OBJECT_PATH, &inputMenu);
0737     ManualSignalSpy spy;
0738     QDBusConnection::sessionBus().connect(TEST_SERVICE, TEST_OBJECT_PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged", "sa{sv}as", &spy, SLOT(receiveCall(QString, QVariantMap, QStringList)));
0739 
0740     QTest::qWait(500);
0741 
0742     // Check our exporter is on DBus
0743     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0744     QVERIFY2(iface.isValid(), qPrintable(iface.lastError().message()));
0745 
0746     QCOMPARE(exporter->status(), QString("normal"));
0747 
0748     // Change status, a DBus signal should be emitted
0749     exporter->setStatus("notice");
0750     QCOMPARE(exporter->status(), QString("notice"));
0751     QTest::qWait(500);
0752     QCOMPARE(spy.count(), 1);
0753     checkPropertiesChangedArgs(spy.takeFirst(), "Status", "notice");
0754 
0755     // Same status => no signal
0756     exporter->setStatus("notice");
0757     QTest::qWait(500);
0758     QCOMPARE(spy.count(), 0);
0759 
0760     // Change status, a DBus signal should be emitted
0761     exporter->setStatus("normal");
0762     QTest::qWait(500);
0763     QCOMPARE(spy.count(), 1);
0764     checkPropertiesChangedArgs(spy.takeFirst(), "Status", "normal");
0765 }
0766 
0767 void DBusMenuExporterTest::testGetIconDataProperty()
0768 {
0769     // Create an icon
0770     QImage img(16, 16, QImage::Format_ARGB32);
0771     {
0772         QPainter painter(&img);
0773         painter.setCompositionMode(QPainter::CompositionMode_Source);
0774         QRect rect = img.rect();
0775         painter.fillRect(rect, Qt::transparent);
0776         rect.adjust(2, 2, -2, -2);
0777         painter.fillRect(rect, Qt::red);
0778         rect.adjust(2, 2, -2, -2);
0779         painter.fillRect(rect, Qt::green);
0780     }
0781 
0782     QIcon icon(QPixmap::fromImage(img));
0783 
0784     // Create a menu with the icon and export it
0785     QMenu inputMenu;
0786     QAction* a1 = inputMenu.addAction("a1");
0787     a1->setIcon(icon);
0788     DBusMenuExporter exporter(TEST_OBJECT_PATH, &inputMenu);
0789 
0790     // Get properties
0791     QDBusInterface iface(TEST_SERVICE, TEST_OBJECT_PATH);
0792     DBusMenuLayoutItemList layoutItemlist = getChildren(&iface, 0, QStringList());
0793     QCOMPARE(layoutItemlist.count(), 1);
0794 
0795     QList<int> ids = QList<int>() << layoutItemlist[0].id;
0796 
0797     QDBusReply<DBusMenuItemList> reply = iface.call("GetGroupProperties", QVariant::fromValue(ids), QStringList());
0798 
0799     DBusMenuItemList itemlist = reply.value();
0800     QCOMPARE(itemlist.count(), 1);
0801 
0802     // Check we have the right property
0803     DBusMenuItem item = itemlist.takeFirst();
0804     QVERIFY(!item.properties.contains("icon-name"));
0805     QVERIFY(item.properties.contains("icon-data"));
0806 
0807     // Check saved image is the same
0808     QByteArray data = item.properties.value("icon-data").toByteArray();
0809     QVERIFY(!data.isEmpty());
0810     QImage result;
0811     QVERIFY(result.loadFromData(data, "PNG"));
0812     QCOMPARE(result, img);
0813 }
0814 
0815 #include "dbusmenuexportertest.moc"