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
38class tst_QQmlPropertyMap : public QQmlDataTest
39{
40 Q_OBJECT
41public:
42 tst_QQmlPropertyMap() {}
43
44private 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
67class 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)
73public:
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
86signals:
87 void someFixedPropertyChanged();
88
89private:
90 int value = 0;
91};
92
93class SimplePropertyMap: public QQmlPropertyMap
94{
95 Q_OBJECT
96public:
97 SimplePropertyMap() : QQmlPropertyMap(this, nullptr) {}
98};
99
100void 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
107void 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
156void 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
170void 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
186void 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
200void 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
230void 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
248class MyPropertyMap : public QQmlPropertyMap
249{
250 Q_OBJECT
251protected:
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
264void 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
290void 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
304void 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
321class MyEnhancedPropertyMap : public QQmlPropertyMap
322{
323 Q_OBJECT
324public:
325 MyEnhancedPropertyMap() : QQmlPropertyMap(this, nullptr) {}
326 bool testSlotCalled() const { return m_testSlotCalled; }
327
328signals:
329 void testSignal();
330
331public slots:
332 void testSlot() { m_testSlotCalled = true; }
333
334private:
335 bool m_testSlotCalled = false;
336};
337
338void 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
355void 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
383void 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
406void 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
429void 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
454void 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
475void 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
509void 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
519QTEST_MAIN(tst_QQmlPropertyMap)
520
521#include "tst_qqmlpropertymap.moc"
522

source code of qtdeclarative/tests/auto/qml/qqmlpropertymap/tst_qqmlpropertymap.cpp