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"