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 | |