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 "../../shared/util.h" |
30 | #include <QtQml/qqmlengine.h> |
31 | #include <QtQml/qqmlcontext.h> |
32 | #include <QtQml/qqmlpropertymap.h> |
33 | #include <QtQml/qqmlcomponent.h> |
34 | #include <QtQuick/private/qquicktext_p.h> |
35 | #include <QSignalSpy> |
36 | #include <QDebug> |
37 | |
38 | class tst_QQmlPropertyMap : public QQmlDataTest |
39 | { |
40 | Q_OBJECT |
41 | public: |
42 | tst_QQmlPropertyMap() {} |
43 | |
44 | private slots: |
45 | void initTestCase(); |
46 | |
47 | void insert(); |
48 | void operatorInsert(); |
49 | void operatorValue(); |
50 | void clear(); |
51 | void changed(); |
52 | void count(); |
53 | void controlledWrite(); |
54 | |
55 | void crashBug(); |
56 | void QTBUG_17868(); |
57 | void metaObjectAccessibility(); |
58 | void QTBUG_31226(); |
59 | void QTBUG_29836(); |
60 | void QTBUG_35233(); |
61 | void disallowExtending(); |
62 | void QTBUG_35906(); |
63 | void QTBUG_48136(); |
64 | void lookupsInSubTypes(); |
65 | }; |
66 | |
67 | class LazyPropertyMap : public QQmlPropertyMap, public QQmlParserStatus |
68 | { |
69 | Q_OBJECT |
70 | Q_INTERFACES(QQmlParserStatus) |
71 | |
72 | Q_PROPERTY(int someFixedProperty READ someFixedProperty WRITE setSomeFixedProperty NOTIFY someFixedPropertyChanged) |
73 | public: |
74 | LazyPropertyMap() |
75 | : QQmlPropertyMap(this, /*parent*/nullptr) |
76 | {} |
77 | |
78 | virtual void classBegin() {} |
79 | virtual void componentComplete() { |
80 | insert(QStringLiteral("lateProperty" ), QStringLiteral("lateValue" )); |
81 | } |
82 | |
83 | int someFixedProperty() const { return value; } |
84 | void setSomeFixedProperty(int v) { value = v; emit someFixedPropertyChanged(); } |
85 | |
86 | signals: |
87 | void someFixedPropertyChanged(); |
88 | |
89 | private: |
90 | int value = 0; |
91 | }; |
92 | |
93 | class SimplePropertyMap: public QQmlPropertyMap |
94 | { |
95 | Q_OBJECT |
96 | public: |
97 | SimplePropertyMap() : QQmlPropertyMap(this, nullptr) {} |
98 | }; |
99 | |
100 | void tst_QQmlPropertyMap::initTestCase() |
101 | { |
102 | QQmlDataTest::initTestCase(); |
103 | qmlRegisterType<LazyPropertyMap>(uri: "QTBUG_35233" , versionMajor: 1, versionMinor: 0, qmlName: "LazyPropertyMap" ); |
104 | qmlRegisterType<SimplePropertyMap>(uri: "Test" , versionMajor: 1, versionMinor: 0, qmlName: "SimplePropertyMap" ); |
105 | } |
106 | |
107 | void tst_QQmlPropertyMap::insert() |
108 | { |
109 | QQmlPropertyMap map; |
110 | map.insert(key: QLatin1String("key1" ),value: 100); |
111 | map.insert(key: QLatin1String("key2" ),value: 200); |
112 | QCOMPARE(map.keys().count(), 2); |
113 | QVERIFY(map.contains(QLatin1String("key1" ))); |
114 | |
115 | QCOMPARE(map.value(QLatin1String("key1" )), QVariant(100)); |
116 | QCOMPARE(map.value(QLatin1String("key2" )), QVariant(200)); |
117 | |
118 | map.insert(key: QLatin1String("key1" ),value: "Hello World" ); |
119 | QCOMPARE(map.value(QLatin1String("key1" )), QVariant("Hello World" )); |
120 | |
121 | //inserting property names same with existing method(signal, slot, method) names is not allowed |
122 | //QQmlPropertyMap has an invokable keys() method |
123 | QTest::ignoreMessage(type: QtWarningMsg, message: "Creating property with name \"keys\" is not permitted, conflicts with internal symbols." ); |
124 | map.insert(key: QLatin1String("keys" ), value: 1); |
125 | QCOMPARE(map.keys().count(), 2); |
126 | QVERIFY(!map.contains(QLatin1String("keys" ))); |
127 | QVERIFY(map.value(QLatin1String("keys" )).isNull()); |
128 | |
129 | //QQmlPropertyMap has a deleteLater() slot |
130 | QTest::ignoreMessage(type: QtWarningMsg, message: "Creating property with name \"deleteLater\" is not permitted, conflicts with internal symbols." ); |
131 | map.insert(key: QLatin1String("deleteLater" ), value: 1); |
132 | QCOMPARE(map.keys().count(), 2); |
133 | QVERIFY(!map.contains(QLatin1String("deleteLater" ))); |
134 | QVERIFY(map.value(QLatin1String("deleteLater" )).isNull()); |
135 | |
136 | //QQmlPropertyMap has an valueChanged() signal |
137 | QTest::ignoreMessage(type: QtWarningMsg, message: "Creating property with name \"valueChanged\" is not permitted, conflicts with internal symbols." ); |
138 | map.insert(key: QLatin1String("valueChanged" ), value: 1); |
139 | QCOMPARE(map.keys().count(), 2); |
140 | QVERIFY(!map.contains(QLatin1String("valueChanged" ))); |
141 | QVERIFY(map.value(QLatin1String("valueChanged" )).isNull()); |
142 | |
143 | //but 'valueChange' should be ok |
144 | map.insert(key: QLatin1String("valueChange" ), value: 1); |
145 | QCOMPARE(map.keys().count(), 3); |
146 | QVERIFY(map.contains(QLatin1String("valueChange" ))); |
147 | QCOMPARE(map.value(QLatin1String("valueChange" )), QVariant(1)); |
148 | |
149 | //'valueCHANGED' should be ok, too |
150 | map.insert(key: QLatin1String("valueCHANGED" ), value: 1); |
151 | QCOMPARE(map.keys().count(), 4); |
152 | QVERIFY(map.contains(QLatin1String("valueCHANGED" ))); |
153 | QCOMPARE(map.value(QLatin1String("valueCHANGED" )), QVariant(1)); |
154 | } |
155 | |
156 | void tst_QQmlPropertyMap::operatorInsert() |
157 | { |
158 | QQmlPropertyMap map; |
159 | map[QLatin1String("key1" )] = 100; |
160 | map[QLatin1String("key2" )] = 200; |
161 | QCOMPARE(map.keys().count(), 2); |
162 | |
163 | QCOMPARE(map.value(QLatin1String("key1" )), QVariant(100)); |
164 | QCOMPARE(map.value(QLatin1String("key2" )), QVariant(200)); |
165 | |
166 | map[QLatin1String("key1" )] = "Hello World" ; |
167 | QCOMPARE(map.value(QLatin1String("key1" )), QVariant("Hello World" )); |
168 | } |
169 | |
170 | void tst_QQmlPropertyMap::operatorValue() |
171 | { |
172 | QQmlPropertyMap map; |
173 | map.insert(key: QLatin1String("key1" ),value: 100); |
174 | map.insert(key: QLatin1String("key2" ),value: 200); |
175 | QCOMPARE(map.count(), 2); |
176 | QVERIFY(map.contains(QLatin1String("key1" ))); |
177 | |
178 | const QQmlPropertyMap &constMap = map; |
179 | |
180 | QCOMPARE(constMap.value(QLatin1String("key1" )), QVariant(100)); |
181 | QCOMPARE(constMap.value(QLatin1String("key2" )), QVariant(200)); |
182 | QCOMPARE(constMap[QLatin1String("key1" )], constMap.value(QLatin1String("key1" ))); |
183 | QCOMPARE(constMap[QLatin1String("key2" )], constMap.value(QLatin1String("key2" ))); |
184 | } |
185 | |
186 | void tst_QQmlPropertyMap::clear() |
187 | { |
188 | QQmlPropertyMap map; |
189 | map.insert(key: QLatin1String("key1" ),value: 100); |
190 | QCOMPARE(map.keys().count(), 1); |
191 | |
192 | QCOMPARE(map.value(QLatin1String("key1" )), QVariant(100)); |
193 | |
194 | map.clear(key: QLatin1String("key1" )); |
195 | QCOMPARE(map.keys().count(), 1); |
196 | QVERIFY(map.contains(QLatin1String("key1" ))); |
197 | QCOMPARE(map.value(QLatin1String("key1" )), QVariant()); |
198 | } |
199 | |
200 | void tst_QQmlPropertyMap::changed() |
201 | { |
202 | QQmlPropertyMap map; |
203 | QSignalSpy spy(&map, SIGNAL(valueChanged(QString,QVariant))); |
204 | map.insert(key: QLatin1String("key1" ),value: 100); |
205 | map.insert(key: QLatin1String("key2" ),value: 200); |
206 | QCOMPARE(spy.count(), 0); |
207 | |
208 | map.clear(key: QLatin1String("key1" )); |
209 | QCOMPARE(spy.count(), 0); |
210 | |
211 | //make changes in QML |
212 | QQmlEngine engine; |
213 | QQmlContext *ctxt = engine.rootContext(); |
214 | ctxt->setContextProperty(QLatin1String("testdata" ), &map); |
215 | QQmlComponent component(&engine); |
216 | component.setData("import QtQuick 2.0\nText { text: { testdata.key1 = 'Hello World'; 'X' } }" , |
217 | baseUrl: QUrl::fromLocalFile(localfile: "" )); |
218 | QVERIFY(component.isReady()); |
219 | QQuickText *txt = qobject_cast<QQuickText*>(object: component.create()); |
220 | QVERIFY(txt); |
221 | QCOMPARE(txt->text(), QString('X')); |
222 | QCOMPARE(spy.count(), 1); |
223 | QList<QVariant> arguments = spy.takeFirst(); |
224 | QCOMPARE(arguments.count(), 2); |
225 | QCOMPARE(arguments.at(0).toString(),QLatin1String("key1" )); |
226 | QCOMPARE(arguments.at(1).value<QVariant>(),QVariant("Hello World" )); |
227 | QCOMPARE(map.value(QLatin1String("key1" )), QVariant("Hello World" )); |
228 | } |
229 | |
230 | void tst_QQmlPropertyMap::count() |
231 | { |
232 | QQmlPropertyMap map; |
233 | QCOMPARE(map.isEmpty(), true); |
234 | map.insert(key: QLatin1String("key1" ),value: 100); |
235 | map.insert(key: QLatin1String("key2" ),value: 200); |
236 | QCOMPARE(map.count(), 2); |
237 | QCOMPARE(map.isEmpty(), false); |
238 | |
239 | map.insert(key: QLatin1String("key3" ),value: "Hello World" ); |
240 | QCOMPARE(map.count(), 3); |
241 | |
242 | //clearing doesn't remove the key |
243 | map.clear(key: QLatin1String("key3" )); |
244 | QCOMPARE(map.count(), 3); |
245 | QCOMPARE(map.size(), map.count()); |
246 | } |
247 | |
248 | class MyPropertyMap : public QQmlPropertyMap |
249 | { |
250 | Q_OBJECT |
251 | protected: |
252 | virtual QVariant updateValue(const QString &key, const QVariant &src) |
253 | { |
254 | if (key == QLatin1String("key1" )) { |
255 | // 'key1' must be all uppercase |
256 | const QString original(src.toString()); |
257 | return QVariant(original.toUpper()); |
258 | } |
259 | |
260 | return src; |
261 | } |
262 | }; |
263 | |
264 | void tst_QQmlPropertyMap::controlledWrite() |
265 | { |
266 | MyPropertyMap map; |
267 | QCOMPARE(map.isEmpty(), true); |
268 | |
269 | //make changes in QML |
270 | QQmlEngine engine; |
271 | QQmlContext *ctxt = engine.rootContext(); |
272 | ctxt->setContextProperty(QLatin1String("testdata" ), &map); |
273 | |
274 | const char *qmlSource = |
275 | "import QtQuick 2.0\n" |
276 | "Item { Component.onCompleted: { testdata.key1 = 'Hello World'; testdata.key2 = 'Goodbye' } }" ; |
277 | |
278 | QQmlComponent component(&engine); |
279 | component.setData(qmlSource, baseUrl: QUrl::fromLocalFile(localfile: "" )); |
280 | QVERIFY(component.isReady()); |
281 | |
282 | QObject *obj = component.create(); |
283 | QVERIFY(obj); |
284 | delete obj; |
285 | |
286 | QCOMPARE(map.value(QLatin1String("key1" )), QVariant("HELLO WORLD" )); |
287 | QCOMPARE(map.value(QLatin1String("key2" )), QVariant("Goodbye" )); |
288 | } |
289 | |
290 | void tst_QQmlPropertyMap::crashBug() |
291 | { |
292 | QQmlPropertyMap map; |
293 | |
294 | QQmlEngine engine; |
295 | QQmlContext context(&engine); |
296 | context.setContextProperty("map" , &map); |
297 | |
298 | QQmlComponent c(&engine); |
299 | c.setData("import QtQuick 2.0\nBinding { target: map; property: \"myProp\"; value: 10 + 23 }" ,baseUrl: QUrl()); |
300 | QObject *obj = c.create(context: &context); |
301 | delete obj; |
302 | } |
303 | |
304 | void tst_QQmlPropertyMap::QTBUG_17868() |
305 | { |
306 | QQmlPropertyMap map; |
307 | |
308 | QQmlEngine engine; |
309 | QQmlContext context(&engine); |
310 | context.setContextProperty("map" , &map); |
311 | map.insert(key: "key" , value: 1); |
312 | QQmlComponent c(&engine); |
313 | c.setData("import QtQuick 2.0\nItem {property bool error:false; Component.onCompleted: {try{ map.keys(); }catch(e) {error=true;}}}" ,baseUrl: QUrl()); |
314 | QObject *obj = c.create(context: &context); |
315 | QVERIFY(obj); |
316 | QVERIFY(!obj->property("error" ).toBool()); |
317 | delete obj; |
318 | |
319 | } |
320 | |
321 | class MyEnhancedPropertyMap : public QQmlPropertyMap |
322 | { |
323 | Q_OBJECT |
324 | public: |
325 | MyEnhancedPropertyMap() : QQmlPropertyMap(this, nullptr) {} |
326 | bool testSlotCalled() const { return m_testSlotCalled; } |
327 | |
328 | signals: |
329 | void testSignal(); |
330 | |
331 | public slots: |
332 | void testSlot() { m_testSlotCalled = true; } |
333 | |
334 | private: |
335 | bool m_testSlotCalled = false; |
336 | }; |
337 | |
338 | void tst_QQmlPropertyMap::metaObjectAccessibility() |
339 | { |
340 | QQmlTestMessageHandler messageHandler; |
341 | |
342 | QQmlEngine engine; |
343 | |
344 | MyEnhancedPropertyMap map; |
345 | |
346 | // Verify that signals and slots of QQmlPropertyMap-derived classes are accessible |
347 | QObject::connect(sender: &map, SIGNAL(testSignal()), receiver: &engine, SIGNAL(quit())); |
348 | QObject::connect(sender: &engine, SIGNAL(quit()), receiver: &map, SLOT(testSlot())); |
349 | |
350 | QCOMPARE(map.metaObject()->className(), "MyEnhancedPropertyMap" ); |
351 | |
352 | QVERIFY2(messageHandler.messages().isEmpty(), qPrintable(messageHandler.messageString())); |
353 | } |
354 | |
355 | void tst_QQmlPropertyMap::QTBUG_31226() |
356 | { |
357 | /* Instantiate a property map from QML, and verify that property changes |
358 | * made from C++ are visible from QML */ |
359 | QQmlEngine engine; |
360 | QQmlContext context(&engine); |
361 | qmlRegisterType<QQmlPropertyMap>(uri: "QTBUG_31226" , versionMajor: 1, versionMinor: 0, qmlName: "PropertyMap" ); |
362 | QQmlComponent c(&engine); |
363 | c.setData("import QtQuick 2.0\nimport QTBUG_31226 1.0\n" |
364 | "Item {\n" |
365 | " property string myProp\n" |
366 | " PropertyMap { id: qmlPropertyMap; objectName: \"qmlPropertyMap\" }\n" |
367 | " Timer { interval: 5; running: true; onTriggered: { myProp = qmlPropertyMap.greeting; } }\n" |
368 | "}" , |
369 | baseUrl: QUrl()); |
370 | QObject *obj = c.create(context: &context); |
371 | QVERIFY(obj); |
372 | |
373 | QQmlPropertyMap *qmlPropertyMap = obj->findChild<QQmlPropertyMap*>(aName: QString("qmlPropertyMap" )); |
374 | QVERIFY(qmlPropertyMap); |
375 | |
376 | qmlPropertyMap->insert(key: "greeting" , value: QString("Hello world!" )); |
377 | QTRY_COMPARE(obj->property("myProp" ).toString(), QString("Hello world!" )); |
378 | |
379 | delete obj; |
380 | |
381 | } |
382 | |
383 | void tst_QQmlPropertyMap::QTBUG_29836() |
384 | { |
385 | MyEnhancedPropertyMap map; |
386 | QCOMPARE(map.testSlotCalled(), false); |
387 | |
388 | QQmlEngine engine; |
389 | QQmlContext context(&engine); |
390 | context.setContextProperty("enhancedMap" , &map); |
391 | QQmlComponent c(&engine); |
392 | c.setData("import QtQuick 2.0\n" |
393 | "Item {\n" |
394 | " Timer { interval: 5; running: true; onTriggered: enhancedMap.testSlot() }\n" |
395 | "}" , |
396 | baseUrl: QUrl()); |
397 | QObject *obj = c.create(context: &context); |
398 | QVERIFY(obj); |
399 | |
400 | QTRY_COMPARE(map.testSlotCalled(), true); |
401 | |
402 | delete obj; |
403 | |
404 | } |
405 | |
406 | void tst_QQmlPropertyMap::QTBUG_35233() |
407 | { |
408 | QQmlEngine engine; |
409 | QQmlComponent component(&engine); |
410 | component.setData("import QtQml 2.0\n" |
411 | "import QTBUG_35233 1.0\n" |
412 | "QtObject {\n" |
413 | " property QtObject testMap: LazyPropertyMap {\n" |
414 | " id: map\n" |
415 | " }\n" |
416 | " property QtObject sibling: QtObject {\n" |
417 | " objectName: \"sibling\"\n" |
418 | " property string testValue: map.lateProperty\n" |
419 | " }\n" |
420 | "}" , baseUrl: QUrl()); |
421 | QScopedPointer<QObject> obj(component.create()); |
422 | QVERIFY(!obj.isNull()); |
423 | |
424 | QObject *sibling = obj->findChild<QObject*>(aName: "sibling" ); |
425 | QVERIFY(sibling); |
426 | QCOMPARE(sibling->property("testValue" ).toString(), QString("lateValue" )); |
427 | } |
428 | |
429 | void tst_QQmlPropertyMap::disallowExtending() |
430 | { |
431 | QQmlEngine engine; |
432 | QQmlComponent component(&engine); |
433 | component.setData("import QtQml 2.0\n" |
434 | "import QTBUG_35233 1.0\n" |
435 | "LazyPropertyMap {\n" |
436 | " id: blah\n" |
437 | " someFixedProperty: 42\n" |
438 | "}\n" , baseUrl: QUrl()); |
439 | QScopedPointer<QObject> obj(component.create()); |
440 | QVERIFY(!obj.isNull()); |
441 | |
442 | component.setData("import QtQml 2.0\n" |
443 | "import QTBUG_35233 1.0\n" |
444 | "LazyPropertyMap {\n" |
445 | " id: blah\n" |
446 | " property int someNewProperty;\n" |
447 | "}\n" , baseUrl: QUrl()); |
448 | obj.reset(other: component.create()); |
449 | QVERIFY(obj.isNull()); |
450 | QCOMPARE(component.errors().count(), 1); |
451 | QCOMPARE(component.errors().at(0).toString(), QStringLiteral("<Unknown File>:3:1: Fully dynamic types cannot declare new properties." )); |
452 | } |
453 | |
454 | void tst_QQmlPropertyMap::QTBUG_35906() |
455 | { |
456 | QQmlEngine engine; |
457 | QQmlComponent component(&engine); |
458 | component.setData("import QtQml 2.0\n" |
459 | "import QTBUG_35233 1.0\n" |
460 | "QtObject {\n" |
461 | " property int testValue: mapById.someFixedProperty\n" |
462 | "\n" |
463 | " property QtObject maProperty: LazyPropertyMap {\n" |
464 | " id: mapById\n" |
465 | " someFixedProperty: 42\n" |
466 | " }\n" |
467 | "}\n" , baseUrl: QUrl()); |
468 | QScopedPointer<QObject> obj(component.create()); |
469 | QVERIFY(!obj.isNull()); |
470 | QVariant value = obj->property(name: "testValue" ); |
471 | QCOMPARE(value.type(), QVariant::Int); |
472 | QCOMPARE(value.toInt(), 42); |
473 | } |
474 | |
475 | void tst_QQmlPropertyMap::QTBUG_48136() |
476 | { |
477 | static const char key[] = "mykey" ; |
478 | QQmlPropertyMap map; |
479 | |
480 | // |
481 | // Test that the notify signal is emitted correctly |
482 | // |
483 | |
484 | const int propIndex = map.metaObject()->indexOfProperty(name: key); |
485 | const QMetaProperty prop = map.metaObject()->property(index: propIndex); |
486 | QSignalSpy notifySpy(&map, QByteArray::number(QSIGNAL_CODE) + prop.notifySignal().methodSignature()); |
487 | |
488 | map.insert(key, value: 42); |
489 | QCOMPARE(notifySpy.count(), 1); |
490 | map.insert(key, value: 43); |
491 | QCOMPARE(notifySpy.count(), 2); |
492 | map.insert(key, value: 43); |
493 | QCOMPARE(notifySpy.count(), 2); |
494 | map.insert(key, value: 44); |
495 | QCOMPARE(notifySpy.count(), 3); |
496 | |
497 | // |
498 | // Test that the valueChanged signal is emitted correctly |
499 | // |
500 | QSignalSpy valueChangedSpy(&map, &QQmlPropertyMap::valueChanged); |
501 | map.setProperty(name: key, value: 44); |
502 | QCOMPARE(valueChangedSpy.count(), 0); |
503 | map.setProperty(name: key, value: 45); |
504 | QCOMPARE(valueChangedSpy.count(), 1); |
505 | map.setProperty(name: key, value: 45); |
506 | QCOMPARE(valueChangedSpy.count(), 1); |
507 | } |
508 | |
509 | void tst_QQmlPropertyMap::lookupsInSubTypes() |
510 | { |
511 | QQmlEngine engine; |
512 | QQmlComponent component(&engine, testFileUrl(fileName: "PropertyMapSubType.qml" )); |
513 | QTest::ignoreMessage(type: QtDebugMsg, message: "expected output" ); |
514 | QScopedPointer<QObject> object(component.create()); |
515 | QVERIFY(!object.isNull()); |
516 | QCOMPARE(object->property("newProperty" ).toInt(), 42); |
517 | } |
518 | |
519 | QTEST_MAIN(tst_QQmlPropertyMap) |
520 | |
521 | #include "tst_qqmlpropertymap.moc" |
522 | |