File indexing completed on 2024-05-19 16:37:10

0001 /*
0002     SPDX-FileCopyrightText: 2016 David Rosca <nowrep@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "iconitemtest.h"
0008 
0009 #include <QIcon>
0010 #include <QQmlComponent>
0011 #include <QQmlContext>
0012 #include <QQmlEngine>
0013 #include <QQuickItemGrabResult>
0014 #include <QSignalSpy>
0015 
0016 #include <KConfigGroup>
0017 #include <KIconEngine>
0018 #include <KIconLoader>
0019 #include <KIconTheme>
0020 
0021 #include "plasma/svg.h"
0022 #include "plasma/theme.h"
0023 
0024 #include "utils.h"
0025 
0026 static bool imageIsEmpty(const QImage &img)
0027 {
0028     for (int i = 0; i < img.width(); ++i) {
0029         for (int j = 0; j < img.height(); ++j) {
0030             if (img.pixel(i, j) != 0) {
0031                 return false;
0032             }
0033         }
0034     }
0035     return true;
0036 }
0037 
0038 void IconItemTest::initTestCase()
0039 {
0040     Plasma::TestUtils::installPlasmaTheme("breeze");
0041     Plasma::TestUtils::installPlasmaTheme("breeze-light");
0042     Plasma::TestUtils::installPlasmaTheme("breeze-dark");
0043 
0044     qputenv("XDG_DATA_DIRS", QByteArray(qgetenv("XDG_DATA_DIRS") + ":" + QFINDTESTDATA("data").toLocal8Bit()));
0045 
0046     QString configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation);
0047 
0048     if (!QDir(configPath).mkpath(QStringLiteral("."))) {
0049         qFatal("Failed to create test configuration directory.");
0050     }
0051 
0052     QFile::remove(configPath);
0053 
0054     QIcon::setThemeSearchPaths({QFINDTESTDATA("data/icons")});
0055     QIcon::setThemeName("test-theme");
0056 
0057     KIconTheme::forceThemeForTests("test-theme");
0058     KIconTheme::reconfigure();
0059     KIconLoader::global()->reconfigure(QString());
0060 
0061     m_view = new QQuickView();
0062     m_view->setSource(QUrl::fromLocalFile(QFINDTESTDATA("data/view.qml")));
0063     m_view->show();
0064     QVERIFY(QTest::qWaitForWindowExposed(m_view));
0065 
0066     if (!m_view->rootObject() || !m_view->rootObject()->grabToImage()) {
0067         QSKIP("Cannot grab item to image.");
0068     }
0069 }
0070 
0071 void IconItemTest::cleanupTestCase()
0072 {
0073     delete m_view;
0074 }
0075 
0076 void IconItemTest::init()
0077 {
0078     Plasma::Theme().setThemeName(QStringLiteral("default"));
0079 }
0080 
0081 void IconItemTest::cleanup()
0082 {
0083     qDeleteAll(m_view->rootObject()->childItems());
0084 }
0085 
0086 QQuickItem *IconItemTest::createIconItem()
0087 {
0088     QByteArray iconQml =
0089         "import QtQuick 2.0;"
0090         "import org.kde.plasma.core 2.0 as PlasmaCore;"
0091         "PlasmaCore.IconItem {"
0092         "    id: root;"
0093         "}";
0094 
0095     QQmlComponent component(m_view->engine());
0096 
0097     QSignalSpy spy(&component, SIGNAL(statusChanged(QQmlComponent::Status)));
0098     component.setData(iconQml, QUrl("test://iconTest"));
0099     if (component.status() != QQmlComponent::Ready) {
0100         spy.wait();
0101     }
0102 
0103     QQuickItem *item = qobject_cast<QQuickItem *>(component.create(m_view->engine()->rootContext()));
0104     Q_ASSERT(item && qstrcmp(item->metaObject()->className(), "IconItem") == 0);
0105     item->setParentItem(m_view->rootObject());
0106     return item;
0107 }
0108 
0109 QImage IconItemTest::grabImage(QQuickItem *item)
0110 {
0111     QSharedPointer<QQuickItemGrabResult> grab = item->grabToImage();
0112     QSignalSpy spy(grab.data(), SIGNAL(ready()));
0113     spy.wait();
0114     return grab->image();
0115 }
0116 
0117 QImage IconItemTest::waitAndGrabImage(QQuickItem *item, int delay)
0118 {
0119     auto window = item->window();
0120 
0121     // Ensure the window is exposed, as otherwise rendering does not happen.
0122     Q_ASSERT(QTest::qWaitForWindowExposed(window));
0123 
0124     // Wait for the provided time. This ensures we can delay so animations have
0125     // time to run.
0126     QTest::qWait(delay);
0127 
0128     // Ensure the item is rendered at least once before continuing. Otherwise
0129     // we run the risk of nothing having changed when calling this in quick
0130     // succession.
0131     QSignalSpy frameSwappedSpy(window, &QQuickWindow::frameSwapped);
0132     frameSwappedSpy.wait(100);
0133 
0134     auto result = grabImage(item);
0135 
0136     // In rare cases, we can trigger a use-after free if we don't explicitly
0137     // disconnect from QQuickWindow::frameSwapped.
0138     frameSwappedSpy.disconnect(window);
0139 
0140     return result;
0141 }
0142 
0143 Plasma::Svg *IconItemTest::findPlasmaSvg(QQuickItem *item)
0144 {
0145     return item->findChild<Plasma::Svg *>();
0146 }
0147 
0148 void IconItemTest::changeTheme(Plasma::Theme *theme, const QString &themeName)
0149 {
0150     if (theme->themeName() != themeName) {
0151         QSignalSpy spy(theme, SIGNAL(themeChanged()));
0152         theme->setThemeName(themeName);
0153         spy.wait();
0154     }
0155 }
0156 
0157 // ------ Tests
0158 
0159 void IconItemTest::loadPixmap()
0160 {
0161     std::unique_ptr<QQuickItem> item(createIconItem());
0162     QPixmap sourcePixmap(QFINDTESTDATA("data/test_image.png"));
0163 
0164     item->setSize(sourcePixmap.size());
0165     item->setProperty("source", sourcePixmap);
0166     QVERIFY(item->property("valid").toBool());
0167 
0168     QImage capture = grabImage(item.get());
0169     QCOMPARE(capture, sourcePixmap.toImage().convertToFormat(capture.format()));
0170     QCOMPARE(sourcePixmap, item->property("source").value<QPixmap>());
0171 }
0172 
0173 // tests setting icon from a QImage
0174 void IconItemTest::loadImage()
0175 {
0176     std::unique_ptr<QQuickItem> item(createIconItem());
0177     QImage sourceImage(QFINDTESTDATA("data/test_image.png"));
0178 
0179     item->setSize(sourceImage.size());
0180     item->setProperty("source", sourceImage);
0181     QVERIFY(item->property("valid").toBool());
0182 
0183     QImage capture = grabImage(item.get());
0184     QCOMPARE(capture, sourceImage.convertToFormat(capture.format()));
0185     QCOMPARE(sourceImage, item->property("source").value<QImage>());
0186 }
0187 
0188 void IconItemTest::invalidIcon()
0189 {
0190     QString name("tst-plasma-framework-invalid-icon-name");
0191     KIconLoader iconLoader("tst_plasma-framework");
0192     if (iconLoader.hasIcon(name)) {
0193         QSKIP("Current icon theme has 'tst-plasma-framework-invalid-icon-name' icon.");
0194     }
0195 
0196     QQuickItem *item = createIconItem();
0197     item->setProperty("source", name);
0198     QVERIFY(!item->property("valid").toBool());
0199     QVERIFY(imageIsEmpty(grabImage(item)));
0200 }
0201 
0202 void IconItemTest::usesPlasmaTheme()
0203 {
0204     // usesPlasmaTheme = true (default)
0205     QQuickItem *item1 = createIconItem();
0206     item1->setProperty("source", "konversation");
0207     QVERIFY(item1->property("valid").toBool());
0208     QCOMPARE(QStringLiteral("konversation"), item1->property("source").toString());
0209 
0210     Plasma::Svg svg;
0211     svg.setContainsMultipleImages(true);
0212     svg.setImagePath("icons/konversation");
0213 
0214     QImage img1 = grabImage(item1);
0215     QImage img2 = svg.image(QSize(item1->width(), item1->height()), "konversation").convertToFormat(img1.format());
0216     QVERIFY(!imageIsEmpty(img1));
0217     QVERIFY(!imageIsEmpty(img2));
0218     QCOMPARE(img1, img2);
0219 
0220     // usesPlasmaTheme = false
0221     QQuickItem *item2 = createIconItem();
0222     item2->setProperty("usesPlasmaTheme", false);
0223     item2->setProperty("source", "konversation");
0224 
0225     img1 = grabImage(item2);
0226     // This depends on konversation icon being different in Plasma Breeze theme
0227     // and our test icon theme
0228     QVERIFY(img1 != img2);
0229 }
0230 
0231 void IconItemTest::animation()
0232 {
0233     // animated = true (default)
0234     QQuickItem *item1 = createIconItem();
0235     item1->setProperty("source", "user-away");
0236     // first icon is not animated
0237     QImage userAwayImg = grabImage(item1);
0238 
0239     item1->setProperty("source", "user-busy");
0240     grabImage(item1);
0241     item1->setProperty("source", "user-away");
0242     // animation from user-busy -> user-away
0243     QVERIFY(userAwayImg != waitAndGrabImage(item1));
0244 
0245     // animated = false
0246     QQuickItem *item2 = createIconItem();
0247     item2->setProperty("animated", false);
0248     item2->setProperty("source", "user-busy");
0249     QImage userBusyImg = grabImage(item2);
0250 
0251     item2->setProperty("source", "user-away");
0252     QCOMPARE(userAwayImg, grabImage(item2));
0253 
0254     item2->setProperty("source", "user-busy");
0255     QCOMPARE(userBusyImg, grabImage(item2));
0256 }
0257 
0258 void IconItemTest::animationAfterHide()
0259 {
0260     QQuickItem *item1 = createIconItem();
0261     QQuickItem *item2 = createIconItem();
0262     item1->setProperty("source", "user-away");
0263     item2->setProperty("source", "user-busy");
0264     // first icon is not animated
0265     QImage userAwayImg = grabImage(item1);
0266     QImage userBusyImg = grabImage(item2);
0267 
0268     item1->setProperty("source", "user-busy");
0269     grabImage(item1);
0270     item1->setProperty("visible", "false");
0271     item1->setProperty("visible", "true");
0272     item1->setProperty("source", "user-away");
0273     // icon was hidden, no animation
0274     QCOMPARE(userAwayImg, grabImage(item1));
0275 
0276     item1->setProperty("source", "user-busy");
0277     QVERIFY(userBusyImg != waitAndGrabImage(item1));
0278 }
0279 
0280 void IconItemTest::bug_359388()
0281 {
0282     if (!KIconTheme::list().contains("hicolor")) {
0283         // This test depends on hicolor icon theme to resolve the icon.
0284         QSKIP("hicolor icon theme not available");
0285     }
0286 
0287     QString name("bug359388");
0288     KIconLoader iconLoader("tst_plasma-framework");
0289     QIcon customThemeIcon(new KIconEngine(name, &iconLoader));
0290     if (iconLoader.hasIcon(name)) {
0291         QSKIP("Current icon theme has 'bug359388' icon.");
0292     }
0293 
0294     iconLoader.addAppDir("tst_plasma-framework", QFINDTESTDATA("data/bug359388"));
0295 
0296     QQuickItem *item1 = createIconItem();
0297     item1->setProperty("source", customThemeIcon);
0298     QVERIFY(item1->property("valid").toBool());
0299     QCOMPARE(customThemeIcon, item1->property("source").value<QIcon>());
0300 
0301     QQuickItem *item2 = createIconItem();
0302     item2->setProperty("source", QIcon(QFINDTESTDATA("data/bug359388/hicolor/22x22/apps/" + name + ".svg")));
0303     QVERIFY(item2->property("valid").toBool());
0304 
0305     QCOMPARE(grabImage(item1), grabImage(item2));
0306 }
0307 
0308 void IconItemTest::loadSvg()
0309 {
0310     QString name("tst-plasma-framework-test-icon");
0311 
0312     QQuickItem *item = createIconItem();
0313     item->setProperty("animated", false);
0314     item->setSize(QSize(22, 22));
0315     item->setProperty("source", name);
0316     QVERIFY(item->property("valid").toBool());
0317 
0318     Plasma::Svg *svg;
0319     svg = findPlasmaSvg(item);
0320     Q_ASSERT(svg);
0321     QCOMPARE(svg->imagePath(), QFINDTESTDATA("data/icons/test-theme/apps/22/" + name + ".svg"));
0322 
0323     // we only have 32x32 and 22x22 version in the theme, thus 32x32 is a better match.
0324     item->setSize(QSize(64, 64));
0325     // just to update the icon
0326     grabImage(item);
0327     svg = findPlasmaSvg(item);
0328     Q_ASSERT(svg);
0329     QCOMPARE(svg->imagePath(), QFINDTESTDATA("data/icons/test-theme/apps/32/" + name + ".svg"));
0330 }
0331 
0332 void IconItemTest::themeChange()
0333 {
0334     // Icon from Plasma theme
0335     QQuickItem *item1 = createIconItem();
0336     item1->setProperty("animated", false);
0337     item1->setProperty("source", "zoom-fit-height");
0338     Plasma::Svg *svg1 = item1->findChild<Plasma::Svg *>();
0339     changeTheme(svg1->theme(), "breeze-light");
0340     QImage img1 = grabImage(item1);
0341     changeTheme(svg1->theme(), "breeze-dark");
0342     QImage img2 = grabImage(item1);
0343     QVERIFY(img1 != img2);
0344 
0345     // Icon from icon theme
0346     QQuickItem *item2 = createIconItem();
0347     item2->setProperty("animated", false);
0348     item2->setProperty("width", 22);
0349     item2->setProperty("height", 22);
0350     item2->setProperty("source", "tst-plasma-framework-test-icon");
0351     Plasma::Svg *svg2 = item2->findChild<Plasma::Svg *>();
0352     changeTheme(svg2->theme(), "breeze-light");
0353     img1 = grabImage(item2);
0354     changeTheme(svg2->theme(), "breeze-dark");
0355     img2 = grabImage(item2);
0356     QVERIFY(img1 != img2);
0357 }
0358 
0359 void IconItemTest::qiconFromTheme()
0360 {
0361     // Icon from Plasma theme
0362     QQuickItem *item1 = createIconItem();
0363     QIcon icon1 = QIcon::fromTheme("konversation");
0364     item1->setProperty("source", icon1);
0365     QVERIFY(item1->findChild<Plasma::Svg *>());
0366     QVERIFY(!imageIsEmpty(grabImage(item1)));
0367     QCOMPARE(icon1, item1->property("source").value<QIcon>());
0368 
0369     // Icon from icon theme
0370     QQuickItem *item2 = createIconItem();
0371     QIcon icon2 = QIcon::fromTheme("tst-plasma-framework-test-icon");
0372     item2->setProperty("source", icon2);
0373     QVERIFY(item2->findChild<Plasma::Svg *>());
0374     QVERIFY(!imageIsEmpty(grabImage(item2)));
0375     QCOMPARE(icon2, item2->property("source").value<QIcon>());
0376 }
0377 
0378 void IconItemTest::changeColorGroup()
0379 {
0380     // Icon from Plasma theme
0381     QQuickItem *item = createIconItem();
0382     item->setProperty("animated", false);
0383     item->setProperty("source", "zoom-fit-height");
0384     Plasma::Svg *svg = item->findChild<Plasma::Svg *>();
0385     // not using "breeze" theme as that one follows system color scheme
0386     // and that one might not have a complementary group or a broken one
0387     changeTheme(svg->theme(), "breeze-light");
0388     QSignalSpy spy(svg, SIGNAL(repaintNeeded()));
0389     QVERIFY(spy.isValid());
0390     QImage img1 = grabImage(item);
0391     item->setProperty("colorGroup", Plasma::Theme::ComplementaryColorGroup);
0392     QTRY_VERIFY(spy.count() == 1);
0393     QImage img2 = grabImage(item);
0394     QVERIFY(img1 != img2);
0395 }
0396 
0397 void IconItemTest::animatingActiveChange()
0398 {
0399     QQuickItem *item1 = createIconItem();
0400     item1->setProperty("animated", false);
0401     item1->setProperty("source", "tst-plasma-framework-test-icon");
0402     QImage img1 = grabImage(item1);
0403 
0404     QQuickItem *item2 = createIconItem();
0405     item2->setProperty("animated", false);
0406     item2->setProperty("active", true);
0407     item2->setProperty("source", "tst-plasma-framework-test-icon");
0408     QImage img2 = grabImage(item2);
0409     QVERIFY(img1 != img2);
0410 
0411     item1->setProperty("active", true);
0412     img1 = grabImage(item1);
0413     QVERIFY(img1 != img2); // animation is running
0414 }
0415 
0416 void IconItemTest::animatingEnabledChange()
0417 {
0418     QQuickItem *item1 = createIconItem();
0419     item1->setProperty("animated", false);
0420     item1->setProperty("source", "tst-plasma-framework-test-icon");
0421     QImage img1 = grabImage(item1);
0422 
0423     QQuickItem *item2 = createIconItem();
0424     item2->setProperty("animated", false);
0425     item2->setProperty("enabled", false);
0426     item2->setProperty("source", "tst-plasma-framework-test-icon");
0427     QImage img2 = grabImage(item2);
0428     QVERIFY(img1 != img2);
0429 
0430     item1->setProperty("enabled", false);
0431     img1 = grabImage(item1);
0432     QVERIFY(img1 != img2); // animation is running
0433 }
0434 
0435 void IconItemTest::windowChanged()
0436 {
0437     QQuickItem *item = createIconItem();
0438     item->setProperty("animated", false);
0439     item->setProperty("source", "tst-plasma-framework-test-icon");
0440     QImage img = grabImage(item);
0441 
0442     QQuickView newView;
0443     newView.setSource(QUrl::fromLocalFile(QFINDTESTDATA("data/view.qml")));
0444     newView.show();
0445     QVERIFY(QTest::qWaitForWindowExposed(&newView));
0446 
0447     item->setProperty("visible", false);
0448     item->setParentItem(newView.rootObject());
0449     item->setProperty("visible", true);
0450     QCOMPARE(grabImage(item), img);
0451 }
0452 
0453 void IconItemTest::paintedSize()
0454 {
0455     QQuickItem *item = createIconItem();
0456 
0457     QCOMPARE(item->property("paintedWidth").toInt(), item->property("implicitWidth").toInt());
0458     QCOMPARE(item->property("paintedHeight").toInt(), item->property("implicitHeight").toInt());
0459 
0460     item->setWidth(40);
0461     item->setHeight(40);
0462 
0463     QCOMPARE(item->property("paintedWidth").toInt(), 32);
0464     QCOMPARE(item->property("paintedHeight").toInt(), 32);
0465 
0466     QIcon landscapeIcon(QPixmap(40, 35));
0467     item->setProperty("source", landscapeIcon);
0468     grabImage(item); // basically just to force loading the pixmap
0469 
0470     // expanded to fit IconItem size whilst keeping aspect ratio
0471     // width should be rounded to icon size, ie. 32 is next smallest
0472     QCOMPARE(item->property("paintedWidth").toInt(), 32);
0473     // height should still match aspect ratio, so *not* 24!
0474     QCOMPARE(item->property("paintedHeight").toInt(), 28);
0475 
0476     QIcon portraitIcon(QPixmap(15, 40));
0477     item->setProperty("source", portraitIcon);
0478     grabImage(item);
0479 
0480     QCOMPARE(item->property("paintedWidth").toInt(), 12);
0481     QCOMPARE(item->property("paintedHeight").toInt(), 32);
0482 
0483     item->setWidth(400);
0484     item->setHeight(400);
0485 
0486     grabImage(item);
0487 
0488     QCOMPARE(item->property("paintedWidth").toInt(), 150);
0489     QCOMPARE(item->property("paintedHeight").toInt(), 400);
0490 }
0491 
0492 void IconItemTest::implicitSize()
0493 {
0494     KConfigGroup cg(KSharedConfig::openConfig(), "DialogIcons");
0495     cg.writeEntry("Size", 22);
0496     cg.sync();
0497     KIconLoader::global()->reconfigure(QString());
0498 
0499     QQuickItem *item = createIconItem();
0500 
0501     // qreal cast needed as QTest::qCompare<double, int> fails to link
0502     QCOMPARE(item->implicitWidth(), qreal(22));
0503     QCOMPARE(item->implicitHeight(), qreal(22));
0504 
0505     QSignalSpy widthSpy(item, &QQuickItem::implicitWidthChanged);
0506     QVERIFY(widthSpy.isValid());
0507     QSignalSpy heightSpy(item, &QQuickItem::implicitHeightChanged);
0508     QVERIFY(heightSpy.isValid());
0509 
0510     cg.writeEntry("Size", 64);
0511     cg.sync();
0512     KIconLoader::global()->reconfigure(QString());
0513     // merely changing the setting and calling reconfigure won't emit this signal,
0514     // the KCM uses a method "newIconLoader" method which does that but it's deprecated
0515     Q_EMIT KIconLoader::global()->iconLoaderSettingsChanged();
0516 
0517     QCOMPARE(widthSpy.count(), 1);
0518     QCOMPARE(heightSpy.count(), 1);
0519 
0520     QCOMPARE(item->implicitWidth(), qreal(64));
0521     QCOMPARE(item->implicitHeight(), qreal(64));
0522 }
0523 
0524 void IconItemTest::nonSquareImplicitSize()
0525 {
0526     QQuickItem *item1 = createIconItem();
0527 
0528     // Both file:///foo and /foo must behave the same
0529     item1->setProperty("source", QFINDTESTDATA("data/test_nonsquare.png"));
0530 
0531     QCOMPARE(item1->implicitWidth(), qreal(150));
0532     QCOMPARE(item1->implicitHeight(), qreal(50));
0533 
0534     QQuickItem *item2 = createIconItem();
0535 
0536     item2->setProperty("source", QUrl::fromLocalFile(QFINDTESTDATA("data/test_nonsquare.png")));
0537 
0538     QCOMPARE(item2->implicitWidth(), item1->implicitWidth());
0539     QCOMPARE(item2->implicitHeight(), item1->implicitHeight());
0540 }
0541 
0542 void IconItemTest::roundToIconSize()
0543 {
0544     QQuickItem *item = createIconItem();
0545 
0546     item->setWidth(25);
0547     item->setHeight(25);
0548     QVERIFY(item->property("paintedWidth").toInt() != 25);
0549     QVERIFY(item->property("paintedHeight").toInt() != 25);
0550 
0551     QSignalSpy paintedSizeSpy(item, SIGNAL(paintedSizeChanged()));
0552     QSignalSpy roundToIconSizeSpy(item, SIGNAL(roundToIconSizeChanged()));
0553 
0554     item->setProperty("roundToIconSize", false);
0555 
0556     QTRY_COMPARE(paintedSizeSpy.count(), 1);
0557     QTRY_COMPARE(roundToIconSizeSpy.count(), 1);
0558     QVERIFY(item->property("paintedWidth").toInt() == 25);
0559     QVERIFY(item->property("paintedHeight").toInt() == 25);
0560 }
0561 
0562 QTEST_MAIN(IconItemTest)
0563 
0564 #include "moc_iconitemtest.cpp"