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
36class tst_qqmlconnections : public QQmlDataTest
37{
38 Q_OBJECT
39public:
40 tst_qqmlconnections();
41
42private 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
82private:
83 QQmlEngine engine;
84 void prefixes();
85};
86
87tst_qqmlconnections::tst_qqmlconnections()
88{
89}
90
91void 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
98void 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
110void 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
124void 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
142void 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
164void 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
189void 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
205void 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
232void 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
243void 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
258class TestObject : public QObject
259{
260 Q_OBJECT
261 Q_PROPERTY(bool ran READ ran WRITE setRan)
262
263public:
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
270signals:
271 void unnamedArgumentSignal(int a, qreal, QString c);
272 void signalWithGlobalName(int parseInt);
273
274private:
275 bool m_ran;
276};
277
278void 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
309class MyTestSingletonType : public QObject
310{
311Q_OBJECT
312Q_PROPERTY(int intProp READ intProp WRITE setIntProp NOTIFY intPropChanged)
313
314public:
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
327signals:
328 void intPropChanged();
329 void otherSignal();
330
331private:
332 int m_intProp;
333 int m_changeCount;
334};
335
336static 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
345void 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
372void 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
400void 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
419void 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
445void 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
454class Proxy : public QObject
455{
456 Q_OBJECT
457public:
458 enum MyEnum { EnumValue = 20, AnotherEnumValue };
459 Q_ENUM(MyEnum)
460
461signals:
462 void someSignal();
463};
464
465void 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
479void 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
492QTEST_MAIN(tst_qqmlconnections)
493
494#include "tst_qqmlconnections.moc"
495

source code of qtdeclarative/tests/auto/qml/qqmlconnections/tst_qqmlconnections.cpp