File indexing completed on 2024-11-10 05:02:44

0001 /*
0002     SPDX-FileCopyrightText: 2022 Fushan Wen <qydwhotmail@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include <QImage>
0008 #include <QQmlEngine>
0009 #include <QQmlExpression>
0010 #include <QQuickItem>
0011 #include <QQuickItemGrabResult>
0012 #include <QQuickView>
0013 #include <QSignalSpy>
0014 #include <QStandardPaths>
0015 #include <QTest>
0016 
0017 #include <KPackage/PackageLoader>
0018 
0019 #include "../utils/mediaproxy.h"
0020 #include "commontestdata.h"
0021 
0022 namespace
0023 {
0024 class MockWallpaperInterface : public QObject
0025 {
0026     Q_OBJECT
0027 
0028     Q_PROPERTY(bool loading MEMBER m_loading NOTIFY isLoadingChanged)
0029     Q_PROPERTY(QColor accentColor MEMBER m_accentColor NOTIFY accentColorChanged)
0030 
0031 public:
0032     explicit MockWallpaperInterface(QObject *parent = nullptr)
0033         : QObject(parent)
0034     {
0035         connect(this, &MockWallpaperInterface::accentColorChanged, this, [this] {
0036             m_repainted = true;
0037         });
0038     }
0039 
0040     bool m_repainted = false; // To be set to true by QQC2.StackView.onActivated
0041     bool m_loading = true; // To be set to false by replaceWhenLoaded
0042     QColor m_accentColor = Qt::transparent;
0043 
0044 Q_SIGNALS:
0045     void isLoadingChanged();
0046     void accentColorChanged();
0047 };
0048 
0049 template<typename T>
0050 static T evaluate(QObject *scope, const char *expression)
0051 {
0052     QQmlExpression expr(qmlContext(scope), scope, QString::fromLatin1(expression));
0053     QVariant result = expr.evaluate();
0054     if (expr.hasError()) {
0055         qWarning() << expr.error().toString();
0056     }
0057     return result.value<T>();
0058 }
0059 
0060 template<>
0061 void evaluate<void>(QObject *scope, const char *expression)
0062 {
0063     QQmlExpression expr(qmlContext(scope), scope, QString::fromLatin1(expression));
0064     expr.evaluate();
0065     if (expr.hasError()) {
0066         qWarning() << expr.error().toString();
0067     }
0068 }
0069 
0070 bool initView(QQuickView *view, const QUrl &url, QByteArray *errorMessage)
0071 {
0072     view->setResizeMode(QQuickView::SizeViewToRootObject);
0073     view->setSource(url);
0074 
0075     while (view->status() == QQuickView::Loading) {
0076         QTest::qWait(10);
0077     }
0078     if (view->status() != QQuickView::Ready) {
0079         const auto errors = view->errors();
0080         for (const QQmlError &e : errors)
0081             errorMessage->append(e.toString().toLocal8Bit() + '\n');
0082         return false;
0083     }
0084 
0085     return true;
0086 }
0087 
0088 }
0089 
0090 class ImageFrontendTest : public QObject
0091 {
0092     Q_OBJECT
0093 
0094 private Q_SLOTS:
0095     void initTestCase();
0096     void init();
0097     void cleanup();
0098 
0099     void testLoadWallpaper_data();
0100     void testLoadWallpaper();
0101     void testReloadWallpaperOnScreenSizeChanged();
0102 
0103     void testCustomAccentColorFromWallpaperMetaData();
0104 
0105 private:
0106     QPointer<QQuickView> m_view;
0107     QPointer<MockWallpaperInterface> m_wallpaperInterface;
0108 
0109     QDir m_dataDir;
0110 };
0111 
0112 void ImageFrontendTest::initTestCase()
0113 {
0114     m_dataDir = QDir(QFINDTESTDATA("testdata/default"));
0115     QVERIFY(!m_dataDir.isEmpty());
0116 
0117     QStandardPaths::setTestModeEnabled(true);
0118 }
0119 
0120 void ImageFrontendTest::init()
0121 {
0122     Q_ASSERT(!m_view && !m_wallpaperInterface);
0123     m_view = new QQuickView();
0124     m_view->engine()->setBaseUrl(QUrl::fromLocalFile(QFINDTESTDATA("../../imagepackage/contents/ui/")));
0125     m_wallpaperInterface = new MockWallpaperInterface(m_view);
0126 }
0127 
0128 void ImageFrontendTest::cleanup()
0129 {
0130     Q_ASSERT(m_view);
0131     delete m_view;
0132 }
0133 
0134 void ImageFrontendTest::testLoadWallpaper_data()
0135 {
0136     QTest::addColumn<int>("fillMode");
0137     QTest::addColumn<QString>("configColor");
0138     QTest::addColumn<bool>("blur");
0139     QTest::addColumn<QString>("source");
0140     QTest::addColumn<QString>("modelImage");
0141     QTest::addColumn<QSize>("sourceSize");
0142     QTest::addColumn<QColor>("expectedColorAtTopLeft");
0143 
0144     const QString defaultImage = m_dataDir.absoluteFilePath(ImageBackendTestData::defaultImageFileName1);
0145     const QImage image(defaultImage);
0146     const QSize imageSize = image.size();
0147 
0148     // Use different fill modes and colors
0149     // Test the background color is covered by the image
0150     Q_ASSERT(image.pixelColor(0, 0) != Qt::blue); // Make sure background color and image color are different
0151     QTest::newRow("Default") << 2 << QStringLiteral("blue") << false << defaultImage << QUrl::fromLocalFile(defaultImage).toString()
0152                              << QSize(imageSize.width() + 100, imageSize.height()) << image.pixelColor(0, 0);
0153     // Test blurred background
0154     QTest::newRow("Blur enabled") << 1 /* PreserveAspectFit */ << QStringLiteral("blue") << true << defaultImage << QUrl::fromLocalFile(defaultImage).toString()
0155                                   << QSize(imageSize.width() + 100, imageSize.height()) << image.pixelColor(0, 0);
0156     // Test background color
0157     QTest::newRow("Background color") << 6 /* Pad */ << QStringLiteral("blue") << false << defaultImage << QUrl::fromLocalFile(defaultImage).toString()
0158                                       << imageSize * 2 << QColor(Qt::blue);
0159 }
0160 
0161 void ImageFrontendTest::testLoadWallpaper()
0162 {
0163     QFETCH(int, fillMode);
0164     QFETCH(QString, configColor);
0165     QFETCH(bool, blur);
0166     QFETCH(QString, source);
0167     QFETCH(QString, modelImage);
0168     QFETCH(QSize, sourceSize);
0169     QFETCH(QColor, expectedColorAtTopLeft);
0170 
0171     // Set required properties and window size
0172     QVariantMap initialProperties;
0173     initialProperties.insert(QStringLiteral("fillMode"), fillMode);
0174     initialProperties.insert(QStringLiteral("configColor"), configColor);
0175     initialProperties.insert(QStringLiteral("blur"), blur);
0176     initialProperties.insert(QStringLiteral("source"), source);
0177     initialProperties.insert(QStringLiteral("sourceSize"), sourceSize);
0178     initialProperties.insert(QStringLiteral("width"), sourceSize.width());
0179     initialProperties.insert(QStringLiteral("height"), sourceSize.height());
0180     initialProperties.insert(QStringLiteral("wallpaperInterface"), QVariant::fromValue(m_wallpaperInterface.data()));
0181     m_view->setInitialProperties(initialProperties);
0182 
0183     QByteArray errorMessage;
0184     QVERIFY2(initView(m_view.data(), QUrl::fromLocalFile(QFINDTESTDATA("../../imagepackage/contents/ui/ImageStackView.qml")), &errorMessage),
0185              errorMessage.constData());
0186 
0187     m_view->show();
0188     QVERIFY(QTest::qWaitForWindowExposed(m_view));
0189     QQuickItem *rootObject = m_view->rootObject();
0190     QVERIFY(rootObject);
0191 
0192     // Wait until the transition animation has finished.
0193     QSignalSpy repaintSpy(m_wallpaperInterface, &MockWallpaperInterface::accentColorChanged);
0194     QVERIFY(m_wallpaperInterface->m_repainted || repaintSpy.wait());
0195     auto currentItem = evaluate<QQuickItem *>(rootObject, "currentItem");
0196     QVERIFY(currentItem);
0197 
0198     // Check required properties are set correctly
0199     QCOMPARE(evaluate<int>(rootObject, "fillMode"), fillMode);
0200     QCOMPARE(evaluate<int>(currentItem, "fillMode"), fillMode);
0201     QCOMPARE(evaluate<QString>(rootObject, "configColor"), configColor);
0202     QCOMPARE(evaluate<QColor>(currentItem, "color"), QColor(configColor));
0203     QCOMPARE(evaluate<bool>(rootObject, "blur"), blur);
0204     QCOMPARE(evaluate<bool>(currentItem, "blur"), blur);
0205     QCOMPARE(evaluate<QString>(rootObject, "source"), QUrl::fromUserInput(source).toString());
0206     QCOMPARE(evaluate<QSize>(rootObject, "sourceSize"), sourceSize);
0207     QCOMPARE(evaluate<QSize>(currentItem, "sourceSize"), sourceSize);
0208 
0209     // Check modelImage path
0210     QCOMPARE(evaluate<QString>(rootObject, "modelImage"), modelImage);
0211     QCOMPARE(evaluate<QUrl>(currentItem, "source").toString(), modelImage);
0212 
0213     // Check color
0214     QEventLoop loop;
0215     auto grabResult = rootObject->grabToImage();
0216     QVERIFY(grabResult);
0217     loop.connect(grabResult.data(), &QQuickItemGrabResult::ready, &loop, &QEventLoop::quit);
0218     loop.exec();
0219     const QImage grabResultImage = grabResult->image();
0220     QVERIFY(!grabResultImage.isNull());
0221     QCOMPARE(grabResultImage.size(), sourceSize);
0222     QCOMPARE(grabResultImage.pixelColor(0, 0), expectedColorAtTopLeft);
0223 
0224     // Other checks
0225     // Check wallpaper interface
0226     QCOMPARE(rootObject->property("wallpaperInterface").value<MockWallpaperInterface *>(), m_wallpaperInterface.data());
0227     // Check item type
0228     QCOMPARE(evaluate<bool>(rootObject, "this instanceof QQC2.StackView"), true);
0229     QCOMPARE(evaluate<bool>(currentItem, "this instanceof Rectangle"), true);
0230     // Check item size
0231     QCOMPARE(m_view->rootObject()->size().toSize(), sourceSize);
0232 }
0233 
0234 void ImageFrontendTest::testReloadWallpaperOnScreenSizeChanged()
0235 {
0236     // Set required properties and window size
0237     QVariantMap initialProperties;
0238     initialProperties.insert(QStringLiteral("fillMode"), 1 /* PreserveAspectFit */);
0239     initialProperties.insert(QStringLiteral("configColor"), QStringLiteral("black"));
0240     initialProperties.insert(QStringLiteral("blur"), false);
0241     // Set a package that contains different resolutions
0242     initialProperties.insert(QStringLiteral("source"), m_dataDir.absoluteFilePath(ImageBackendTestData::defaultPackageFolderName2));
0243     QSize sourceSize(1024, 768);
0244     initialProperties.insert(QStringLiteral("sourceSize"), sourceSize);
0245     initialProperties.insert(QStringLiteral("width"), sourceSize.width());
0246     initialProperties.insert(QStringLiteral("height"), sourceSize.height());
0247     initialProperties.insert(QStringLiteral("wallpaperInterface"), QVariant::fromValue(m_wallpaperInterface.data()));
0248     m_view->setInitialProperties(initialProperties);
0249 
0250     QByteArray errorMessage;
0251     QVERIFY2(initView(m_view.data(), QUrl::fromLocalFile(QFINDTESTDATA("../../imagepackage/contents/ui/ImageStackView.qml")), &errorMessage),
0252              errorMessage.constData());
0253 
0254     m_view->show();
0255     QVERIFY(QTest::qWaitForWindowExposed(m_view));
0256     QQuickItem *rootObject = m_view->rootObject();
0257     QVERIFY(rootObject);
0258 
0259     // Wait until the transition animation has finished.
0260     QSignalSpy repaintSpy(m_wallpaperInterface, &MockWallpaperInterface::accentColorChanged);
0261     QVERIFY(m_wallpaperInterface->m_repainted || repaintSpy.wait());
0262     auto firstItem = evaluate<QQuickItem *>(rootObject, "currentItem");
0263     QVERIFY(firstItem);
0264     QSignalSpy firstItemDestroySpy(firstItem, &QObject::destroyed);
0265 
0266     // Case 1: 1024x768
0267     QUrl source = evaluate<QUrl>(firstItem, "source");
0268     QVERIFY(source.toString().contains(QLatin1String("targetWidth=1024&targetHeight=768")));
0269     QCOMPARE(evaluate<QSizeF>(firstItem, "sourceSize"), QSizeF(1024, 768));
0270 
0271     // Change sourceSize
0272     evaluate<void>(rootObject, "sourceSize = Qt.size(1920, 1080);");
0273     // Qt.callLater
0274     QVERIFY(repaintSpy.wait());
0275     // The first item should be destroyed, otherwise there will be a memory leak
0276     QVERIFY(firstItemDestroySpy.wait());
0277     auto secondItem = evaluate<QQuickItem *>(rootObject, "currentItem");
0278     QVERIFY(secondItem);
0279     QSignalSpy secondItemDestroySpy(secondItem, &QObject::destroyed);
0280     // Case 2: 1920x1080
0281     source = evaluate<QUrl>(secondItem, "source");
0282     QVERIFY(source.toString().contains(QLatin1String("targetWidth=1920&targetHeight=1080")));
0283     QCOMPARE(evaluate<QSizeF>(secondItem, "sourceSize"), QSizeF(1920, 1080));
0284 
0285     // Now set a single image to test if the frontend will still reload the wallpaper in different sizes
0286     const QString singleImagePath = m_dataDir.absoluteFilePath(ImageBackendTestData::defaultImageFileName1);
0287     QVERIFY(QFileInfo::exists(singleImagePath));
0288     rootObject->setProperty("source", singleImagePath);
0289     rootObject->setProperty("sourceSize", QSizeF(320, 240));
0290     // Qt.callLater
0291     QVERIFY(repaintSpy.wait());
0292     // The second item should also be destroyed, otherwise there will be a memory leak
0293     QVERIFY(secondItemDestroySpy.wait());
0294     auto thirdItem = evaluate<QQuickItem *>(rootObject, "currentItem");
0295     QVERIFY(thirdItem);
0296     QSignalSpy thirdItemDestroySpy(thirdItem, &QObject::destroyed);
0297     source = evaluate<QUrl>(thirdItem, "source");
0298     QCOMPARE(source, QUrl::fromLocalFile(singleImagePath));
0299     QCOMPARE(evaluate<QSizeF>(thirdItem, "sourceSize"), QSizeF(320, 240));
0300 
0301     // Now change to 1920x1080
0302     evaluate<void>(rootObject, "sourceSize = Qt.size(1920, 1080);");
0303     // Qt.callLater
0304     QVERIFY(repaintSpy.wait());
0305     // The third item should also be destroyed, otherwise there will be a memory leak
0306     QVERIFY(thirdItemDestroySpy.wait());
0307     auto fourthItem = evaluate<QQuickItem *>(rootObject, "currentItem");
0308     QVERIFY(fourthItem);
0309     source = evaluate<QUrl>(fourthItem, "source");
0310     QCOMPARE(source, QUrl::fromLocalFile(singleImagePath));
0311     QCOMPARE(evaluate<QSizeF>(fourthItem, "sourceSize"), QSizeF(1920, 1080));
0312 }
0313 
0314 void ImageFrontendTest::testCustomAccentColorFromWallpaperMetaData()
0315 {
0316     // Case 1: value is a dict
0317     auto package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Wallpaper/Images"));
0318     package.setPath(QFINDTESTDATA(ImageBackendTestData::customAccentColorPackage1));
0319     QVERIFY(package.isValid());
0320 
0321     QPalette palette;
0322     // Light variant
0323     palette.setColor(QPalette::Normal, QPalette::Window, Qt::white);
0324     qGuiApp->setPalette(palette);
0325     QColor customColor = MediaProxy::getAccentColorFromMetaData(package);
0326     QCOMPARE(customColor, Qt::red);
0327 
0328     // Dark variant
0329     palette.setColor(QPalette::Normal, QPalette::Window, Qt::black);
0330     qGuiApp->setPalette(palette);
0331     customColor = MediaProxy::getAccentColorFromMetaData(package);
0332     QCOMPARE(customColor, Qt::cyan);
0333 
0334     // Case 2: value is a string
0335     package.setPath(QFINDTESTDATA(ImageBackendTestData::customAccentColorPackage2));
0336     QVERIFY(package.isValid());
0337     customColor = MediaProxy::getAccentColorFromMetaData(package);
0338     QCOMPARE(customColor, QColor("green")); // Qt::green is not QColor("green")
0339 
0340     // Real-life test
0341     palette.setColor(QPalette::Normal, QPalette::Window, Qt::white);
0342     qGuiApp->setPalette(palette);
0343     QVariantMap initialProperties;
0344     initialProperties.insert(QStringLiteral("fillMode"), 1 /* PreserveAspectFit */);
0345     initialProperties.insert(QStringLiteral("configColor"), QStringLiteral("black"));
0346     initialProperties.insert(QStringLiteral("blur"), false);
0347     initialProperties.insert(QStringLiteral("source"), QFINDTESTDATA(ImageBackendTestData::customAccentColorPackage1));
0348     QSize sourceSize(320, 240);
0349     initialProperties.insert(QStringLiteral("sourceSize"), sourceSize);
0350     initialProperties.insert(QStringLiteral("width"), sourceSize.width());
0351     initialProperties.insert(QStringLiteral("height"), sourceSize.height());
0352     initialProperties.insert(QStringLiteral("wallpaperInterface"), QVariant::fromValue(m_wallpaperInterface.data()));
0353     m_view->setInitialProperties(initialProperties);
0354 
0355     QSignalSpy repaintSpy(m_wallpaperInterface, &MockWallpaperInterface::accentColorChanged);
0356 
0357     QByteArray errorMessage;
0358     QVERIFY2(initView(m_view.data(), QUrl::fromLocalFile(QFINDTESTDATA("../../imagepackage/contents/ui/ImageStackView.qml")), &errorMessage),
0359              errorMessage.constData());
0360 
0361     m_view->show();
0362     QVERIFY(repaintSpy.wait());
0363     QCOMPARE(m_wallpaperInterface->m_accentColor, Qt::red);
0364 
0365     palette.setColor(QPalette::Normal, QPalette::Window, Qt::black);
0366     qGuiApp->setPalette(palette);
0367     QVERIFY(repaintSpy.wait());
0368     QCOMPARE(m_wallpaperInterface->m_accentColor, Qt::cyan);
0369 
0370     // Switch to a wallpaper package that does not contain accent color information
0371     m_view->rootObject()->setProperty("source", m_dataDir.absoluteFilePath(ImageBackendTestData::defaultImageFileName1));
0372     QVERIFY(repaintSpy.wait());
0373     QCOMPARE(m_wallpaperInterface->m_accentColor, Qt::transparent);
0374 }
0375 
0376 QTEST_MAIN(ImageFrontendTest)
0377 
0378 #include "tst_imagefrontend.moc"