| 1 | /**************************************************************************** |
| 2 | ** |
| 3 | ** Copyright (C) 2016 Research In Motion. |
| 4 | ** Contact: https://www.qt.io/licensing/ |
| 5 | ** |
| 6 | ** This file is part of the test suite of the Qt Toolkit. |
| 7 | ** |
| 8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| 9 | ** Commercial License Usage |
| 10 | ** Licensees holding valid commercial Qt licenses may use this file in |
| 11 | ** accordance with the commercial license agreement provided with the |
| 12 | ** Software or, alternatively, in accordance with the terms contained in |
| 13 | ** a written agreement between you and The Qt Company. For licensing terms |
| 14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
| 15 | ** information use the contact form at https://www.qt.io/contact-us. |
| 16 | ** |
| 17 | ** GNU General Public License Usage |
| 18 | ** Alternatively, this file may be used under the terms of the GNU |
| 19 | ** General Public License version 3 as published by the Free Software |
| 20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| 21 | ** included in the packaging of this file. Please review the following |
| 22 | ** information to ensure the GNU General Public License requirements will |
| 23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| 24 | ** |
| 25 | ** $QT_END_LICENSE$ |
| 26 | ** |
| 27 | ****************************************************************************/ |
| 28 | |
| 29 | #include "../../shared/util.h" |
| 30 | #include <QQmlApplicationEngine> |
| 31 | #include <QScopedPointer> |
| 32 | #include <QSignalSpy> |
| 33 | #include <QRegularExpression> |
| 34 | #if QT_CONFIG(process) |
| 35 | #include <QProcess> |
| 36 | #endif |
| 37 | #include <QDebug> |
| 38 | |
| 39 | class tst_qqmlapplicationengine : public QQmlDataTest |
| 40 | { |
| 41 | Q_OBJECT |
| 42 | public: |
| 43 | tst_qqmlapplicationengine() {} |
| 44 | |
| 45 | |
| 46 | private slots: |
| 47 | void initTestCase(); |
| 48 | void basicLoading(); |
| 49 | void testNonResolvedPath(); |
| 50 | void application_data(); |
| 51 | void application(); |
| 52 | void applicationProperties(); |
| 53 | void removeObjectsWhenDestroyed(); |
| 54 | void loadTranslation_data(); |
| 55 | void loadTranslation(); |
| 56 | void translationChange(); |
| 57 | void setInitialProperties(); |
| 58 | void failureToLoadTriggersWarningSignal(); |
| 59 | void errorWhileCreating(); |
| 60 | |
| 61 | private: |
| 62 | QString buildDir; |
| 63 | QString srcDir; |
| 64 | }; |
| 65 | |
| 66 | void tst_qqmlapplicationengine::initTestCase() |
| 67 | { |
| 68 | qputenv(varName: "QT_MESSAGE_PATTERN" , value: "" ); // don't let it modify the debug output from testapp |
| 69 | buildDir = QDir::currentPath(); |
| 70 | QQmlDataTest::initTestCase(); //Changes current path to src dir |
| 71 | srcDir = QDir::currentPath(); |
| 72 | } |
| 73 | |
| 74 | void tst_qqmlapplicationengine::basicLoading() |
| 75 | { |
| 76 | int size = 0; |
| 77 | |
| 78 | QQmlApplicationEngine *test = new QQmlApplicationEngine(testFileUrl(fileName: "basicTest.qml" )); |
| 79 | QCOMPARE(test->rootObjects().size(), ++size); |
| 80 | QVERIFY(test->rootObjects()[size -1]); |
| 81 | QVERIFY(test->rootObjects()[size -1]->property("success" ).toBool()); |
| 82 | |
| 83 | QSignalSpy objectCreated(test, SIGNAL(objectCreated(QObject*,QUrl))); |
| 84 | test->load(url: testFileUrl(fileName: "basicTest.qml" )); |
| 85 | QCOMPARE(objectCreated.count(), size);//one less than rootObjects().size() because we missed the first one |
| 86 | QCOMPARE(test->rootObjects().size(), ++size); |
| 87 | QVERIFY(test->rootObjects()[size -1]); |
| 88 | QVERIFY(test->rootObjects()[size -1]->property("success" ).toBool()); |
| 89 | |
| 90 | QByteArray testQml("import QtQml 2.0; QtObject{property bool success: true; property TestItem t: TestItem{}}" ); |
| 91 | test->loadData(data: testQml, url: testFileUrl(fileName: "dynamicTest.qml" )); |
| 92 | QCOMPARE(objectCreated.count(), size); |
| 93 | QCOMPARE(test->rootObjects().size(), ++size); |
| 94 | QVERIFY(test->rootObjects()[size -1]); |
| 95 | QVERIFY(test->rootObjects()[size -1]->property("success" ).toBool()); |
| 96 | |
| 97 | delete test; |
| 98 | } |
| 99 | |
| 100 | // make sure we resolve a relative URL to an absolute one, otherwise things |
| 101 | // will break. |
| 102 | void tst_qqmlapplicationengine::testNonResolvedPath() |
| 103 | { |
| 104 | #ifdef Q_OS_ANDROID |
| 105 | QSKIP("Android stores QML files in resources, and the path to a resource cannot be relative in this case" ); |
| 106 | #endif |
| 107 | { |
| 108 | // NOTE NOTE NOTE! Missing testFileUrl is *WANTED* here! We want a |
| 109 | // non-resolved URL. |
| 110 | QQmlApplicationEngine test("data/nonResolvedLocal.qml" ); |
| 111 | QCOMPARE(test.rootObjects().size(), 1); |
| 112 | QVERIFY(test.rootObjects()[0]); |
| 113 | QVERIFY(test.rootObjects()[0]->property("success" ).toBool()); |
| 114 | } |
| 115 | { |
| 116 | QQmlApplicationEngine test; |
| 117 | // NOTE NOTE NOTE! Missing testFileUrl is *WANTED* here! We want a |
| 118 | // non-resolved URL. |
| 119 | test.load(filePath: "data/nonResolvedLocal.qml" ); |
| 120 | QCOMPARE(test.rootObjects().size(), 1); |
| 121 | QVERIFY(test.rootObjects()[0]); |
| 122 | QVERIFY(test.rootObjects()[0]->property("success" ).toBool()); |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | void tst_qqmlapplicationengine::application_data() |
| 127 | { |
| 128 | #ifdef Q_OS_ANDROID |
| 129 | QSKIP("Cannot launch external process on Android" ); |
| 130 | #endif |
| 131 | QTest::addColumn<QByteArray>(name: "qmlFile" ); |
| 132 | QTest::addColumn<QByteArray>(name: "expectedStdErr" ); |
| 133 | |
| 134 | QTest::newRow(dataTag: "delayed quit" ) << QByteArray("delayedQuit.qml" ) |
| 135 | << QByteArray("qml: Start: delayedQuit.qml\nqml: End\n" ); |
| 136 | QTest::newRow(dataTag: "delayed exit" ) << QByteArray("delayedExit.qml" ) |
| 137 | << QByteArray("qml: Start: delayedExit.qml\nqml: End\n" ); |
| 138 | QTest::newRow(dataTag: "immediate quit" ) << QByteArray("immediateQuit.qml" ) |
| 139 | << QByteArray("qml: End: immediateQuit.qml\n" ); |
| 140 | QTest::newRow(dataTag: "immediate exit" ) << QByteArray("immediateExit.qml" ) |
| 141 | << QByteArray("qml: End: immediateExit.qml\n" ); |
| 142 | } |
| 143 | |
| 144 | void tst_qqmlapplicationengine::application() |
| 145 | { |
| 146 | /* This test batches together some tests about running an external application |
| 147 | written with QQmlApplicationEngine. The application tests the following functionality |
| 148 | which is easier to do by watching a separate process: |
| 149 | - Loads relative paths from the working directory |
| 150 | - Quits when quit is called |
| 151 | - Exits when exit is called |
| 152 | - Emits aboutToQuit after quit is called |
| 153 | - Has access to application command line arguments |
| 154 | |
| 155 | Note that checking the output means that on builds with extra debugging, this might fail with a false positive. |
| 156 | Also the testapp is automatically built and installed in shadow builds, so it does NOT use testData |
| 157 | */ |
| 158 | |
| 159 | QFETCH(QByteArray, qmlFile); |
| 160 | QFETCH(QByteArray, expectedStdErr); |
| 161 | |
| 162 | #if QT_CONFIG(process) |
| 163 | QDir::setCurrent(buildDir); |
| 164 | QProcess *testProcess = new QProcess(this); |
| 165 | QStringList args; |
| 166 | args << qmlFile; // QML file passed as an argument is going to be run by testapp. |
| 167 | testProcess->start(program: QLatin1String("testapp/testapp" ), arguments: args); |
| 168 | QVERIFY(testProcess->waitForFinished(5000)); |
| 169 | QCOMPARE(testProcess->exitCode(), 0); |
| 170 | QByteArray testStdOut = testProcess->readAllStandardOutput(); |
| 171 | QByteArray testStdErr = testProcess->readAllStandardError(); |
| 172 | #ifdef Q_OS_WIN |
| 173 | expectedStdErr.replace('\n', QByteArray("\r\n" )); |
| 174 | #endif |
| 175 | QCOMPARE(testStdOut, QByteArray("" )); |
| 176 | QVERIFY2(QString(testStdErr).endsWith(QString(expectedStdErr)), |
| 177 | QByteArray("\nExpected ending:\n" ) + expectedStdErr |
| 178 | + QByteArray("\nActual output:\n" ) + testStdErr); |
| 179 | delete testProcess; |
| 180 | QDir::setCurrent(srcDir); |
| 181 | #else // process |
| 182 | QSKIP("No process support" ); |
| 183 | #endif // process |
| 184 | } |
| 185 | |
| 186 | void tst_qqmlapplicationengine::applicationProperties() |
| 187 | { |
| 188 | const QString originalName = QCoreApplication::applicationName(); |
| 189 | const QString originalVersion = QCoreApplication::applicationVersion(); |
| 190 | const QString originalOrganization = QCoreApplication::organizationName(); |
| 191 | const QString originalDomain = QCoreApplication::organizationDomain(); |
| 192 | QString firstName = QLatin1String("Test A" ); |
| 193 | QString firstVersion = QLatin1String("0.0A" ); |
| 194 | QString firstOrganization = QLatin1String("Org A" ); |
| 195 | QString firstDomain = QLatin1String("a.org" ); |
| 196 | QString secondName = QLatin1String("Test B" ); |
| 197 | QString secondVersion = QLatin1String("0.0B" ); |
| 198 | QString secondOrganization = QLatin1String("Org B" ); |
| 199 | QString secondDomain = QLatin1String("b.org" ); |
| 200 | |
| 201 | QCoreApplication::setApplicationName(firstName); |
| 202 | QCoreApplication::setApplicationVersion(firstVersion); |
| 203 | QCoreApplication::setOrganizationName(firstOrganization); |
| 204 | QCoreApplication::setOrganizationDomain(firstDomain); |
| 205 | |
| 206 | QQmlApplicationEngine *test = new QQmlApplicationEngine(testFileUrl(fileName: "applicationTest.qml" )); |
| 207 | QObject* root = test->rootObjects().at(i: 0); |
| 208 | QVERIFY(root); |
| 209 | QCOMPARE(root->property("originalName" ).toString(), firstName); |
| 210 | QCOMPARE(root->property("originalVersion" ).toString(), firstVersion); |
| 211 | QCOMPARE(root->property("originalOrganization" ).toString(), firstOrganization); |
| 212 | QCOMPARE(root->property("originalDomain" ).toString(), firstDomain); |
| 213 | QCOMPARE(root->property("currentName" ).toString(), secondName); |
| 214 | QCOMPARE(root->property("currentVersion" ).toString(), secondVersion); |
| 215 | QCOMPARE(root->property("currentOrganization" ).toString(), secondOrganization); |
| 216 | QCOMPARE(root->property("currentDomain" ).toString(), secondDomain); |
| 217 | QCOMPARE(QCoreApplication::applicationName(), secondName); |
| 218 | QCOMPARE(QCoreApplication::applicationVersion(), secondVersion); |
| 219 | QCOMPARE(QCoreApplication::organizationName(), secondOrganization); |
| 220 | QCOMPARE(QCoreApplication::organizationDomain(), secondDomain); |
| 221 | |
| 222 | QObject* application = root->property(name: "applicationInstance" ).value<QObject*>(); |
| 223 | QVERIFY(application); |
| 224 | QSignalSpy nameChanged(application, SIGNAL(nameChanged())); |
| 225 | QSignalSpy versionChanged(application, SIGNAL(versionChanged())); |
| 226 | QSignalSpy organizationChanged(application, SIGNAL(organizationChanged())); |
| 227 | QSignalSpy domainChanged(application, SIGNAL(domainChanged())); |
| 228 | |
| 229 | QCoreApplication::setApplicationName(originalName); |
| 230 | QCoreApplication::setApplicationVersion(originalVersion); |
| 231 | QCoreApplication::setOrganizationName(originalOrganization); |
| 232 | QCoreApplication::setOrganizationDomain(originalDomain); |
| 233 | |
| 234 | QCOMPARE(nameChanged.count(), 1); |
| 235 | QCOMPARE(versionChanged.count(), 1); |
| 236 | QCOMPARE(organizationChanged.count(), 1); |
| 237 | QCOMPARE(domainChanged.count(), 1); |
| 238 | |
| 239 | delete test; |
| 240 | } |
| 241 | |
| 242 | void tst_qqmlapplicationengine::removeObjectsWhenDestroyed() |
| 243 | { |
| 244 | QScopedPointer<QQmlApplicationEngine> test(new QQmlApplicationEngine); |
| 245 | QVERIFY(test->rootObjects().isEmpty()); |
| 246 | |
| 247 | QSignalSpy objectCreated(test.data(), SIGNAL(objectCreated(QObject*,QUrl))); |
| 248 | test->load(url: testFileUrl(fileName: "basicTest.qml" )); |
| 249 | QCOMPARE(objectCreated.count(), 1); |
| 250 | |
| 251 | QSignalSpy objectDestroyed(test->rootObjects().first(), SIGNAL(destroyed())); |
| 252 | test->rootObjects().first()->deleteLater(); |
| 253 | objectDestroyed.wait(); |
| 254 | QCOMPARE(objectDestroyed.count(), 1); |
| 255 | QCOMPARE(test->rootObjects().size(), 0); |
| 256 | } |
| 257 | |
| 258 | void tst_qqmlapplicationengine::loadTranslation_data() |
| 259 | { |
| 260 | QTest::addColumn<QUrl>(name: "qmlUrl" ); |
| 261 | QTest::addColumn<QString>(name: "translation" ); |
| 262 | |
| 263 | QTest::newRow(dataTag: "local file" ) << testFileUrl(fileName: "loadTranslation.qml" ) |
| 264 | << QStringLiteral("translated" ); |
| 265 | QTest::newRow(dataTag: "qrc" ) << QUrl(QLatin1String("qrc:///data/loadTranslation.qml" )) |
| 266 | << QStringLiteral("translated" ); |
| 267 | } |
| 268 | |
| 269 | void tst_qqmlapplicationengine::loadTranslation() |
| 270 | { |
| 271 | QFETCH(QUrl, qmlUrl); |
| 272 | QFETCH(QString, translation); |
| 273 | |
| 274 | QQmlApplicationEngine test(qmlUrl); |
| 275 | QVERIFY(!test.rootObjects().isEmpty()); |
| 276 | |
| 277 | QObject *rootObject = test.rootObjects().first(); |
| 278 | QVERIFY(rootObject); |
| 279 | |
| 280 | QCOMPARE(rootObject->property("translation" ).toString(), translation); |
| 281 | } |
| 282 | |
| 283 | void tst_qqmlapplicationengine::translationChange() |
| 284 | { |
| 285 | if (QLocale().language() == QLocale::SwissGerman) { |
| 286 | QSKIP("Skipping this when running under the Swiss locale as we would always load translation." ); |
| 287 | } |
| 288 | |
| 289 | QQmlApplicationEngine engine(testFileUrl(fileName: "loadTranslation.qml" )); |
| 290 | |
| 291 | QCOMPARE(engine.uiLanguage(), QLocale().bcp47Name()); |
| 292 | |
| 293 | QObject *rootObject = engine.rootObjects().first(); |
| 294 | QVERIFY(rootObject); |
| 295 | |
| 296 | QCOMPARE(rootObject->property("translation" ).toString(), "translated" ); |
| 297 | |
| 298 | engine.setUiLanguage("de_CH" ); |
| 299 | QCOMPARE(rootObject->property("translation" ).toString(), QString::fromUtf8("Gr\u00FCezi" )); |
| 300 | |
| 301 | engine.setUiLanguage(QString()); |
| 302 | QCOMPARE(rootObject->property("translation" ).toString(), "translate it" ); |
| 303 | } |
| 304 | |
| 305 | void tst_qqmlapplicationengine::setInitialProperties() |
| 306 | { |
| 307 | QQmlApplicationEngine test {}; |
| 308 | { |
| 309 | test.setInitialProperties(QVariantMap{{"success" , false}}); |
| 310 | test.load(url: testFileUrl(fileName: "basicTest.qml" )); |
| 311 | QVERIFY(!test.rootObjects().empty()); |
| 312 | QCOMPARE(test.rootObjects().first()->property("success" ).toBool(), false); |
| 313 | } |
| 314 | { |
| 315 | test.setInitialProperties({{"success" , true}}); |
| 316 | test.load(url: testFileUrl(fileName: "basicTest.qml" )); |
| 317 | QCOMPARE(test.rootObjects().size(), 2); |
| 318 | QCOMPARE(test.rootObjects().at(1)->property("success" ).toBool(), true); |
| 319 | } |
| 320 | } |
| 321 | |
| 322 | Q_DECLARE_METATYPE(QList<QQmlError>) // for signalspy below |
| 323 | |
| 324 | void tst_qqmlapplicationengine::failureToLoadTriggersWarningSignal() |
| 325 | { |
| 326 | auto url = testFileUrl(fileName: "invalid.qml" ); |
| 327 | qRegisterMetaType<QList<QQmlError>>(); |
| 328 | QTest::ignoreMessage(type: QtMsgType::QtWarningMsg, message: "QQmlApplicationEngine failed to load component" ); |
| 329 | QTest::ignoreMessage(type: QtMsgType::QtWarningMsg, |
| 330 | messagePattern: QRegularExpression(QRegularExpression::escape(str: url.toString()) + QLatin1Char('*'))); |
| 331 | QQmlApplicationEngine test; |
| 332 | QSignalSpy warningObserver(&test, &QQmlApplicationEngine::warnings); |
| 333 | test.load(url); |
| 334 | QTRY_COMPARE(warningObserver.count(), 1); |
| 335 | } |
| 336 | |
| 337 | void tst_qqmlapplicationengine::errorWhileCreating() |
| 338 | { |
| 339 | auto url = testFileUrl(fileName: "requiredViolation.qml" ); |
| 340 | QQmlApplicationEngine test; |
| 341 | QSignalSpy observer(&test, &QQmlApplicationEngine::objectCreated); |
| 342 | |
| 343 | QTest::ignoreMessage(type: QtMsgType::QtWarningMsg, message: "QQmlApplicationEngine failed to create component" ); |
| 344 | QTest::ignoreMessage(type: QtMsgType::QtWarningMsg, qPrintable(QStringLiteral("%1:5:5: Required property foo was not initialized" ).arg(testFileUrl("Required.qml" ).toString()))); |
| 345 | |
| 346 | test.load(url); |
| 347 | |
| 348 | QTRY_COMPARE(observer.count(), 1); |
| 349 | QList<QVariant> args = observer.takeFirst(); |
| 350 | QVERIFY(args.at(0).isNull()); |
| 351 | QCOMPARE(args.at(1).toUrl(), url); |
| 352 | } |
| 353 | |
| 354 | QTEST_MAIN(tst_qqmlapplicationengine) |
| 355 | |
| 356 | #include "tst_qqmlapplicationengine.moc" |
| 357 | |