1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
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 | #include <qtest.h> |
29 | #include <QtQml/qqmlengine.h> |
30 | #include <QtQml/qqmlcomponent.h> |
31 | #include <private/qqmlconnections_p.h> |
32 | #include <private/qquickitem_p.h> |
33 | #include "../../shared/util.h" |
34 | #include <QtQml/qqmlscriptstring.h> |
35 | |
36 | class tst_qqmlconnections : public QQmlDataTest |
37 | { |
38 | Q_OBJECT |
39 | public: |
40 | tst_qqmlconnections(); |
41 | |
42 | private slots: |
43 | void defaultValues(); |
44 | void properties(); |
45 | |
46 | void connection_data() { prefixes(); } |
47 | void connection(); |
48 | |
49 | void trimming_data() { prefixes(); } |
50 | void trimming(); |
51 | |
52 | void targetChanged_data() { prefixes(); }; |
53 | void targetChanged(); |
54 | |
55 | void unknownSignals_data(); |
56 | void unknownSignals(); |
57 | |
58 | void errors_data(); |
59 | void errors(); |
60 | |
61 | void rewriteErrors_data() { prefixes(); } |
62 | void rewriteErrors(); |
63 | |
64 | void singletonTypeTarget_data() { prefixes(); } |
65 | void singletonTypeTarget(); |
66 | |
67 | void enableDisable_QTBUG_36350_data() { prefixes(); } |
68 | void enableDisable_QTBUG_36350(); |
69 | |
70 | void disabledAtStart_data() { prefixes(); } |
71 | void disabledAtStart(); |
72 | |
73 | void clearImplicitTarget_data() { prefixes(); } |
74 | void clearImplicitTarget(); |
75 | void onWithoutASignal(); |
76 | |
77 | void noAcceleratedGlobalLookup_data() { prefixes(); } |
78 | void noAcceleratedGlobalLookup(); |
79 | |
80 | void bindToPropertyWithUnderscoreChangeHandler(); |
81 | |
82 | private: |
83 | QQmlEngine engine; |
84 | void prefixes(); |
85 | }; |
86 | |
87 | tst_qqmlconnections::tst_qqmlconnections() |
88 | { |
89 | } |
90 | |
91 | void tst_qqmlconnections::prefixes() |
92 | { |
93 | QTest::addColumn<QString>(name: "prefix" ); |
94 | QTest::newRow(dataTag: "functions" ) << QString("functions" ); |
95 | QTest::newRow(dataTag: "bindings" ) << QString("bindings" ); |
96 | } |
97 | |
98 | void tst_qqmlconnections::defaultValues() |
99 | { |
100 | QQmlEngine engine; |
101 | QQmlComponent c(&engine, testFileUrl(fileName: "test-connection3.qml" )); |
102 | QQmlConnections *item = qobject_cast<QQmlConnections*>(object: c.create()); |
103 | |
104 | QVERIFY(item != nullptr); |
105 | QVERIFY(!item->target()); |
106 | |
107 | delete item; |
108 | } |
109 | |
110 | void tst_qqmlconnections::properties() |
111 | { |
112 | QQmlEngine engine; |
113 | QQmlComponent c(&engine, testFileUrl(fileName: "test-connection2.qml" )); |
114 | QQmlConnections *item = qobject_cast<QQmlConnections*>(object: c.create()); |
115 | |
116 | QVERIFY(item != nullptr); |
117 | |
118 | QVERIFY(item != nullptr); |
119 | QCOMPARE(item->target(), item); |
120 | |
121 | delete item; |
122 | } |
123 | |
124 | void tst_qqmlconnections::connection() |
125 | { |
126 | QFETCH(QString, prefix); |
127 | QQmlEngine engine; |
128 | QQmlComponent c(&engine, testFileUrl(fileName: prefix + "/test-connection.qml" )); |
129 | QQuickItem *item = qobject_cast<QQuickItem*>(object: c.create()); |
130 | |
131 | QVERIFY(item != nullptr); |
132 | |
133 | QCOMPARE(item->property("tested" ).toBool(), false); |
134 | QCOMPARE(item->width(), 50.); |
135 | emit item->setWidth(100.); |
136 | QCOMPARE(item->width(), 100.); |
137 | QCOMPARE(item->property("tested" ).toBool(), true); |
138 | |
139 | delete item; |
140 | } |
141 | |
142 | void tst_qqmlconnections::trimming() |
143 | { |
144 | QFETCH(QString, prefix); |
145 | QQmlEngine engine; |
146 | QQmlComponent c(&engine, testFileUrl(fileName: prefix + "/trimming.qml" )); |
147 | QObject *object = c.create(); |
148 | |
149 | QVERIFY(object != nullptr); |
150 | |
151 | QCOMPARE(object->property("tested" ).toString(), QString("" )); |
152 | int index = object->metaObject()->indexOfSignal(signal: "testMe(int,QString)" ); |
153 | QMetaMethod method = object->metaObject()->method(index); |
154 | method.invoke(object, |
155 | connectionType: Qt::DirectConnection, |
156 | Q_ARG(int, 5), |
157 | Q_ARG(QString, "worked" )); |
158 | QCOMPARE(object->property("tested" ).toString(), QString("worked5" )); |
159 | |
160 | delete object; |
161 | } |
162 | |
163 | // Confirm that target can be changed by one of our signal handlers |
164 | void tst_qqmlconnections::targetChanged() |
165 | { |
166 | QFETCH(QString, prefix); |
167 | QQmlEngine engine; |
168 | QQmlComponent c(&engine, testFileUrl(fileName: prefix + "/connection-targetchange.qml" )); |
169 | QQuickItem *item = qobject_cast<QQuickItem*>(object: c.create()); |
170 | QVERIFY(item != nullptr); |
171 | |
172 | QQmlConnections *connections = item->findChild<QQmlConnections*>(aName: "connections" ); |
173 | QVERIFY(connections); |
174 | |
175 | QQuickItem *item1 = item->findChild<QQuickItem*>(aName: "item1" ); |
176 | QVERIFY(item1); |
177 | |
178 | item1->setWidth(200); |
179 | |
180 | QQuickItem *item2 = item->findChild<QQuickItem*>(aName: "item2" ); |
181 | QVERIFY(item2); |
182 | QCOMPARE(connections->target(), item2); |
183 | |
184 | // If we don't crash then we're OK |
185 | |
186 | delete item; |
187 | } |
188 | |
189 | void tst_qqmlconnections::unknownSignals_data() |
190 | { |
191 | QTest::addColumn<QString>(name: "file" ); |
192 | QTest::addColumn<QString>(name: "error" ); |
193 | |
194 | QTest::newRow(dataTag: "functions/basic" ) << "functions/connection-unknownsignals.qml" << ":6:30: QML Connections: Detected function \"onFooBar\" in Connections element. This is probably intended to be a signal handler but no signal of the target matches the name." ; |
195 | QTest::newRow(dataTag: "functions/parent" ) << "functions/connection-unknownsignals-parent.qml" << ":4:30: QML Connections: Detected function \"onFooBar\" in Connections element. This is probably intended to be a signal handler but no signal of the target matches the name." ; |
196 | QTest::newRow(dataTag: "functions/ignored" ) << "functions/connection-unknownsignals-ignored.qml" << "" ; // should be NO error |
197 | QTest::newRow(dataTag: "functions/notarget" ) << "functions/connection-unknownsignals-notarget.qml" << "" ; // should be NO error |
198 | |
199 | QTest::newRow(dataTag: "bindings/basic" ) << "bindings/connection-unknownsignals.qml" << ":6:30: QML Connections: Cannot assign to non-existent property \"onFooBar\"" ; |
200 | QTest::newRow(dataTag: "bindings/parent" ) << "bindings/connection-unknownsignals-parent.qml" << ":4:30: QML Connections: Cannot assign to non-existent property \"onFooBar\"" ; |
201 | QTest::newRow(dataTag: "bindings/ignored" ) << "bindings/connection-unknownsignals-ignored.qml" << "" ; // should be NO error |
202 | QTest::newRow(dataTag: "bindings/notarget" ) << "bindings/connection-unknownsignals-notarget.qml" << "" ; // should be NO error |
203 | } |
204 | |
205 | void tst_qqmlconnections::unknownSignals() |
206 | { |
207 | QFETCH(QString, file); |
208 | QFETCH(QString, error); |
209 | |
210 | QUrl url = testFileUrl(fileName: file); |
211 | if (!error.isEmpty()) { |
212 | QTest::ignoreMessage(type: QtWarningMsg, message: (url.toString() + error).toLatin1()); |
213 | } else { |
214 | // QTest has no way to insist no message (i.e. fail) |
215 | } |
216 | |
217 | QQmlEngine engine; |
218 | QQmlComponent c(&engine, url); |
219 | QObject *object = c.create(); |
220 | QVERIFY(object != nullptr); |
221 | |
222 | // check that connection is created (they are all runtime errors) |
223 | QQmlConnections *connections = object->findChild<QQmlConnections*>(aName: "connections" ); |
224 | QVERIFY(connections); |
225 | |
226 | if (file == "connection-unknownsignals-ignored.qml" ) |
227 | QVERIFY(connections->ignoreUnknownSignals()); |
228 | |
229 | delete object; |
230 | } |
231 | |
232 | void tst_qqmlconnections::errors_data() |
233 | { |
234 | QTest::addColumn<QString>(name: "file" ); |
235 | QTest::addColumn<QString>(name: "error" ); |
236 | |
237 | QTest::newRow(dataTag: "no \"on\"" ) << "error-property.qml" << "Cannot assign to non-existent property \"fakeProperty\"" ; |
238 | QTest::newRow(dataTag: "3rd letter lowercase" ) << "error-property2.qml" << "Cannot assign to non-existent property \"onfakeProperty\"" ; |
239 | QTest::newRow(dataTag: "child object" ) << "error-object.qml" << "Connections: nested objects not allowed" ; |
240 | QTest::newRow(dataTag: "grouped object" ) << "error-syntax.qml" << "Connections: syntax error" ; |
241 | } |
242 | |
243 | void tst_qqmlconnections::errors() |
244 | { |
245 | QFETCH(QString, file); |
246 | QFETCH(QString, error); |
247 | |
248 | QUrl url = testFileUrl(fileName: file); |
249 | |
250 | QQmlEngine engine; |
251 | QQmlComponent c(&engine, url); |
252 | QVERIFY(c.isError()); |
253 | QList<QQmlError> errors = c.errors(); |
254 | QCOMPARE(errors.count(), 1); |
255 | QCOMPARE(errors.at(0).description(), error); |
256 | } |
257 | |
258 | class TestObject : public QObject |
259 | { |
260 | Q_OBJECT |
261 | Q_PROPERTY(bool ran READ ran WRITE setRan) |
262 | |
263 | public: |
264 | TestObject(QObject *parent = nullptr) : QObject(parent), m_ran(false) {} |
265 | ~TestObject() {} |
266 | |
267 | bool ran() const { return m_ran; } |
268 | void setRan(bool arg) { m_ran = arg; } |
269 | |
270 | signals: |
271 | void unnamedArgumentSignal(int a, qreal, QString c); |
272 | void signalWithGlobalName(int parseInt); |
273 | |
274 | private: |
275 | bool m_ran; |
276 | }; |
277 | |
278 | void tst_qqmlconnections::rewriteErrors() |
279 | { |
280 | QFETCH(QString, prefix); |
281 | qmlRegisterType<TestObject>(uri: "Test" , versionMajor: 1, versionMinor: 0, qmlName: "TestObject" ); |
282 | { |
283 | QQmlEngine engine; |
284 | QQmlComponent c(&engine, testFileUrl(fileName: prefix + "/rewriteError-unnamed.qml" )); |
285 | QTest::ignoreMessage(type: QtWarningMsg, message: (c.url().toString() + ":5:35: QML Connections: Signal uses unnamed parameter followed by named parameter." ).toLatin1()); |
286 | TestObject *obj = qobject_cast<TestObject*>(object: c.create()); |
287 | QVERIFY(obj != nullptr); |
288 | obj->unnamedArgumentSignal(a: 1, .5, c: "hello" ); |
289 | QCOMPARE(obj->ran(), false); |
290 | |
291 | delete obj; |
292 | } |
293 | |
294 | { |
295 | QQmlEngine engine; |
296 | QQmlComponent c(&engine, testFileUrl(fileName: prefix + "/rewriteError-global.qml" )); |
297 | QTest::ignoreMessage(type: QtWarningMsg, message: (c.url().toString() + ":5:35: QML Connections: Signal parameter \"parseInt\" hides global variable." ).toLatin1()); |
298 | TestObject *obj = qobject_cast<TestObject*>(object: c.create()); |
299 | QVERIFY(obj != nullptr); |
300 | |
301 | obj->signalWithGlobalName(parseInt: 10); |
302 | QCOMPARE(obj->ran(), false); |
303 | |
304 | delete obj; |
305 | } |
306 | } |
307 | |
308 | |
309 | class MyTestSingletonType : public QObject |
310 | { |
311 | Q_OBJECT |
312 | Q_PROPERTY(int intProp READ intProp WRITE setIntProp NOTIFY intPropChanged) |
313 | |
314 | public: |
315 | MyTestSingletonType(QObject *parent = nullptr) : QObject(parent), m_intProp(0), m_changeCount(0) {} |
316 | ~MyTestSingletonType() {} |
317 | |
318 | Q_INVOKABLE int otherMethod(int val) { return val + 4; } |
319 | |
320 | int intProp() const { return m_intProp; } |
321 | void setIntProp(int val) |
322 | { |
323 | if (++m_changeCount % 3 == 0) emit otherSignal(); |
324 | m_intProp = val; emit intPropChanged(); |
325 | } |
326 | |
327 | signals: |
328 | void intPropChanged(); |
329 | void otherSignal(); |
330 | |
331 | private: |
332 | int m_intProp; |
333 | int m_changeCount; |
334 | }; |
335 | |
336 | static QObject *module_api_factory(QQmlEngine *engine, QJSEngine *scriptEngine) |
337 | { |
338 | Q_UNUSED(engine) |
339 | Q_UNUSED(scriptEngine) |
340 | MyTestSingletonType *api = new MyTestSingletonType(); |
341 | return api; |
342 | } |
343 | |
344 | // QTBUG-20937 |
345 | void tst_qqmlconnections::singletonTypeTarget() |
346 | { |
347 | QFETCH(QString, prefix); |
348 | qmlRegisterSingletonType<MyTestSingletonType>(uri: "MyTestSingletonType" , versionMajor: 1, versionMinor: 0, typeName: "Api" , callback: module_api_factory); |
349 | QQmlComponent component(&engine, testFileUrl(fileName: prefix + "/singletontype-target.qml" )); |
350 | QObject *object = component.create(); |
351 | QVERIFY(object != nullptr); |
352 | |
353 | QCOMPARE(object->property("moduleIntPropChangedCount" ).toInt(), 0); |
354 | QCOMPARE(object->property("moduleOtherSignalCount" ).toInt(), 0); |
355 | |
356 | QMetaObject::invokeMethod(obj: object, member: "setModuleIntProp" ); |
357 | QCOMPARE(object->property("moduleIntPropChangedCount" ).toInt(), 1); |
358 | QCOMPARE(object->property("moduleOtherSignalCount" ).toInt(), 0); |
359 | |
360 | QMetaObject::invokeMethod(obj: object, member: "setModuleIntProp" ); |
361 | QCOMPARE(object->property("moduleIntPropChangedCount" ).toInt(), 2); |
362 | QCOMPARE(object->property("moduleOtherSignalCount" ).toInt(), 0); |
363 | |
364 | // the singleton Type emits otherSignal every 3 times the int property changes. |
365 | QMetaObject::invokeMethod(obj: object, member: "setModuleIntProp" ); |
366 | QCOMPARE(object->property("moduleIntPropChangedCount" ).toInt(), 3); |
367 | QCOMPARE(object->property("moduleOtherSignalCount" ).toInt(), 1); |
368 | |
369 | delete object; |
370 | } |
371 | |
372 | void tst_qqmlconnections::enableDisable_QTBUG_36350() |
373 | { |
374 | QFETCH(QString, prefix); |
375 | QQmlEngine engine; |
376 | QQmlComponent c(&engine, testFileUrl(fileName: prefix + "/test-connection.qml" )); |
377 | QQuickItem *item = qobject_cast<QQuickItem*>(object: c.create()); |
378 | QVERIFY(item != nullptr); |
379 | |
380 | QQmlConnections *connections = item->findChild<QQmlConnections*>(aName: "connections" ); |
381 | QVERIFY(connections); |
382 | |
383 | connections->setEnabled(false); |
384 | QCOMPARE(item->property("tested" ).toBool(), false); |
385 | QCOMPARE(item->width(), 50.); |
386 | emit item->setWidth(100.); |
387 | QCOMPARE(item->width(), 100.); |
388 | QCOMPARE(item->property("tested" ).toBool(), false); //Should not have received signal to change property |
389 | |
390 | connections->setEnabled(true); //Re-enable the connectSignals() |
391 | QCOMPARE(item->property("tested" ).toBool(), false); |
392 | QCOMPARE(item->width(), 100.); |
393 | emit item->setWidth(50.); |
394 | QCOMPARE(item->width(), 50.); |
395 | QCOMPARE(item->property("tested" ).toBool(), true); //Should have received signal to change property |
396 | |
397 | delete item; |
398 | } |
399 | |
400 | void tst_qqmlconnections::disabledAtStart() |
401 | { |
402 | QFETCH(QString, prefix); |
403 | QQmlEngine engine; |
404 | QQmlComponent c(&engine, testFileUrl(fileName: prefix + "/disabled-at-start.qml" )); |
405 | QObject * const object = c.create(); |
406 | |
407 | QVERIFY(object != nullptr); |
408 | |
409 | QCOMPARE(object->property("tested" ).toBool(), false); |
410 | const int index = object->metaObject()->indexOfSignal(signal: "testMe()" ); |
411 | const QMetaMethod method = object->metaObject()->method(index); |
412 | method.invoke(object, connectionType: Qt::DirectConnection); |
413 | QCOMPARE(object->property("tested" ).toBool(), false); |
414 | |
415 | delete object; |
416 | } |
417 | |
418 | //QTBUG-56499 |
419 | void tst_qqmlconnections::clearImplicitTarget() |
420 | { |
421 | QFETCH(QString, prefix); |
422 | QQmlEngine engine; |
423 | QQmlComponent c(&engine, testFileUrl(fileName: prefix + "/test-connection-implicit.qml" )); |
424 | QQuickItem *item = qobject_cast<QQuickItem*>(object: c.create()); |
425 | |
426 | QVERIFY(item != nullptr); |
427 | |
428 | // normal case: fire Connections |
429 | item->setWidth(100.); |
430 | QCOMPARE(item->property("tested" ).toBool(), true); |
431 | |
432 | item->setProperty(name: "tested" , value: false); |
433 | // clear the implicit target |
434 | QQmlConnections *connections = item->findChild<QQmlConnections*>(); |
435 | QVERIFY(connections); |
436 | connections->setTarget(nullptr); |
437 | |
438 | // target cleared: no longer fire Connections |
439 | item->setWidth(150.); |
440 | QCOMPARE(item->property("tested" ).toBool(), false); |
441 | |
442 | delete item; |
443 | } |
444 | |
445 | void tst_qqmlconnections::onWithoutASignal() |
446 | { |
447 | QQmlEngine engine; |
448 | QQmlComponent c(&engine, testFileUrl(fileName: "connection-no-signal-name.qml" )); |
449 | QVERIFY(c.isError()); // Cannot assign to non-existent property "on" expected |
450 | QScopedPointer<QQuickItem> item(qobject_cast<QQuickItem*>(object: c.create())); |
451 | QVERIFY(item == nullptr); // should parse error, and not give us an item (or crash). |
452 | } |
453 | |
454 | class Proxy : public QObject |
455 | { |
456 | Q_OBJECT |
457 | public: |
458 | enum MyEnum { EnumValue = 20, AnotherEnumValue }; |
459 | Q_ENUM(MyEnum) |
460 | |
461 | signals: |
462 | void someSignal(); |
463 | }; |
464 | |
465 | void tst_qqmlconnections::noAcceleratedGlobalLookup() |
466 | { |
467 | QFETCH(QString, prefix); |
468 | qRegisterMetaType<Proxy::MyEnum>(); |
469 | qmlRegisterType<Proxy>(uri: "test.proxy" , versionMajor: 1, versionMinor: 0, qmlName: "Proxy" ); |
470 | QQmlEngine engine; |
471 | QQmlComponent c(&engine, testFileUrl(fileName: prefix + "/override-proxy-type.qml" )); |
472 | QVERIFY(c.isReady()); |
473 | QScopedPointer<QObject> object(c.create()); |
474 | const QVariant val = object->property(name: "testEnum" ); |
475 | QCOMPARE(val.type(), QVariant::Int); |
476 | QCOMPARE(val.toInt(), int(Proxy::EnumValue)); |
477 | } |
478 | |
479 | void tst_qqmlconnections::bindToPropertyWithUnderscoreChangeHandler() |
480 | { |
481 | QQmlEngine engine; |
482 | QQmlComponent component(&engine, testFileUrl(fileName: "underscore.qml" )); |
483 | QScopedPointer<QObject> root {component.create()}; |
484 | QVERIFY(root); |
485 | QQmlProperty underscoreProperty(root.get(), "__underscore_property" ); |
486 | QVERIFY(underscoreProperty.isValid()); |
487 | underscoreProperty.write(42); |
488 | QVERIFY(root->property("sanityCheck" ).toBool()); |
489 | QVERIFY(root->property("success" ).toBool()); |
490 | } |
491 | |
492 | QTEST_MAIN(tst_qqmlconnections) |
493 | |
494 | #include "tst_qqmlconnections.moc" |
495 | |