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 <QtQuick/private/qquickitem_p.h>
30#include <QtQuick/private/qquicktext_p.h>
31#include <QtQuick/private/qquickanimation_p.h>
32#include <QtQml/private/qqmlengine_p.h>
33#include <QtQmlModels/private/qqmllistmodel_p.h>
34#include <QtQml/private/qqmlexpression_p.h>
35#include <QQmlComponent>
36
37#include <QtCore/qtimer.h>
38#include <QtCore/qdebug.h>
39#include <QtCore/qtranslator.h>
40#include <QSignalSpy>
41
42#include "../../shared/util.h"
43
44Q_DECLARE_METATYPE(QList<int>)
45Q_DECLARE_METATYPE(QList<QVariantHash>)
46
47#define RUNEVAL(object, string) \
48 QVERIFY(QMetaObject::invokeMethod(object, "runEval", Q_ARG(QVariant, QString(string))));
49
50inline QVariant runexpr(QQmlEngine *engine, const QString &str)
51{
52 QQmlExpression expr(engine->rootContext(), nullptr, str);
53 return expr.evaluate();
54}
55
56#define RUNEXPR(string) runexpr(&engine, QString(string))
57
58static bool isValidErrorMessage(const QString &msg, bool dynamicRoleTest)
59{
60 bool valid = true;
61
62 if (msg.isEmpty()) {
63 valid = false;
64 } else if (dynamicRoleTest) {
65 if (msg.contains(s: "Can't assign to existing role") || msg.contains(s: "Can't create role for unsupported data type"))
66 valid = false;
67 }
68
69 return valid;
70}
71
72class tst_qqmllistmodel : public QQmlDataTest
73{
74 Q_OBJECT
75public:
76 tst_qqmllistmodel()
77 {
78 qRegisterMetaType<QVector<int> >();
79 }
80
81private:
82 int roleFromName(const QQmlListModel *model, const QString &roleName);
83
84 static bool compareVariantList(const QVariantList &testList, QVariant object);
85
86private slots:
87 void static_types();
88 void static_types_data();
89 void static_i18n();
90 void static_i18n_data();
91 void dynamic_i18n();
92 void dynamic_i18n_data();
93 void static_nestedElements();
94 void static_nestedElements_data();
95 void dynamic_data();
96 void dynamic();
97 void enumerate();
98 void error_data();
99 void error();
100 void syncError();
101 void get();
102 void set_data();
103 void set();
104 void get_data();
105 void get_nested();
106 void get_nested_data();
107 void crash_model_with_multiple_roles();
108 void crash_model_with_unknown_roles();
109 void crash_model_with_dynamic_roles();
110 void set_model_cache();
111 void property_changes();
112 void property_changes_data();
113 void clear_data();
114 void clear();
115 void signal_handlers_data();
116 void signal_handlers();
117 void role_mode_data();
118 void role_mode();
119 void string_to_list_crash();
120 void empty_element_warning();
121 void empty_element_warning_data();
122 void datetime();
123 void datetime_data();
124 void about_to_be_signals();
125 void modify_through_delegate();
126 void bindingsOnGetResult();
127 void stringifyModelEntry();
128 void qobjectTrackerForDynamicModelObjects();
129 void crash_append_empty_array();
130 void dynamic_roles_crash_QTBUG_38907();
131 void nestedListModelIteration();
132 void undefinedAppendShouldCauseError();
133 void nullPropertyCrash();
134};
135
136bool tst_qqmllistmodel::compareVariantList(const QVariantList &testList, QVariant object)
137{
138 bool allOk = true;
139
140 QQmlListModel *model = qobject_cast<QQmlListModel *>(object: object.value<QObject *>());
141 if (model == nullptr)
142 return false;
143
144 if (model->count() != testList.count())
145 return false;
146
147 for (int i=0 ; i < testList.count() ; ++i) {
148 const QVariant &testVariant = testList.at(i);
149 if (testVariant.type() != QVariant::Map)
150 return false;
151 const QVariantMap &map = testVariant.toMap();
152
153 const QHash<int, QByteArray> roleNames = model->roleNames();
154
155 QVariantMap::const_iterator it = map.begin();
156 QVariantMap::const_iterator end = map.end();
157
158 while (it != end) {
159 const QString &testKey = it.key();
160 const QVariant &testData = it.value();
161
162 int roleIndex = roleNames.key(value: testKey.toUtf8(), defaultKey: -1);
163 if (roleIndex == -1)
164 return false;
165
166 const QVariant &modelData = model->data(index: i, role: roleIndex);
167
168 if (testData.type() == QVariant::List) {
169 const QVariantList &subList = testData.toList();
170 allOk = allOk && compareVariantList(testList: subList, object: modelData);
171 } else {
172 allOk = allOk && (testData == modelData);
173 }
174
175 ++it;
176 }
177 }
178
179 return allOk;
180}
181
182int tst_qqmllistmodel::roleFromName(const QQmlListModel *model, const QString &roleName)
183{
184 return model->roleNames().key(value: roleName.toUtf8(), defaultKey: -1);
185}
186
187void tst_qqmllistmodel::static_types_data()
188{
189 QTest::addColumn<QString>(name: "qml");
190 QTest::addColumn<QVariant>(name: "value");
191 QTest::addColumn<QString>(name: "error");
192
193 QTest::newRow(dataTag: "string")
194 << "ListElement { foo: \"bar\" }"
195 << QVariant(QString("bar"))
196 << QString();
197
198 QTest::newRow(dataTag: "real")
199 << "ListElement { foo: 10.5 }"
200 << QVariant(10.5)
201 << QString();
202
203 QTest::newRow(dataTag: "real0")
204 << "ListElement { foo: 0 }"
205 << QVariant(double(0))
206 << QString();
207
208 QTest::newRow(dataTag: "bool")
209 << "ListElement { foo: false }"
210 << QVariant(false)
211 << QString();
212
213 QTest::newRow(dataTag: "bool")
214 << "ListElement { foo: true }"
215 << QVariant(true)
216 << QString();
217
218 QTest::newRow(dataTag: "enum")
219 << "ListElement { foo: Text.AlignHCenter }"
220 << QVariant(double(QQuickText::AlignHCenter))
221 << QString();
222
223 QTest::newRow(dataTag: "Qt enum")
224 << "ListElement { foo: Qt.AlignBottom }"
225 << QVariant(double(Qt::AlignBottom))
226 << QString();
227
228 QTest::newRow(dataTag: "negative enum")
229 << "ListElement { foo: Animation.Infinite }"
230 << QVariant(double(QQuickAbstractAnimation::Infinite))
231 << QString();
232
233 QTest::newRow(dataTag: "role error")
234 << "ListElement { foo: 1 } ListElement { foo: 'string' }"
235 << QVariant()
236 << QString("<Unknown File>: Can't assign to existing role 'foo' of different type [String -> Number]");
237
238 QTest::newRow(dataTag: "list type error")
239 << "ListElement { foo: 1 } ListElement { foo: ListElement { bar: 1 } }"
240 << QVariant()
241 << QString("<Unknown File>: Can't assign to existing role 'foo' of different type [List -> Number]");
242}
243
244void tst_qqmllistmodel::static_types()
245{
246 QFETCH(QString, qml);
247 QFETCH(QVariant, value);
248 QFETCH(QString, error);
249
250 qml = "import QtQuick 2.0\nItem { property variant test: model.get(0).foo; ListModel { id: model; " + qml + " } }";
251
252 if (!error.isEmpty()) {
253 QTest::ignoreMessage(type: QtWarningMsg, message: error.toLatin1());
254 }
255
256 QQmlEngine engine;
257 QQmlComponent component(&engine);
258 component.setData(qml.toUtf8(),
259 baseUrl: QUrl::fromLocalFile(localfile: QString("dummy.qml")));
260
261 QVERIFY(!component.isError());
262
263 QObject *obj = component.create();
264 QVERIFY(obj != nullptr);
265
266 if (error.isEmpty()) {
267 QVariant actual = obj->property(name: "test");
268
269 QCOMPARE(actual, value);
270 QCOMPARE(actual.toString(), value.toString());
271 }
272
273 delete obj;
274}
275
276void tst_qqmllistmodel::static_i18n_data()
277{
278 QTest::addColumn<QString>(name: "qml");
279 QTest::addColumn<QVariant>(name: "value");
280 QTest::addColumn<QString>(name: "error");
281
282 QTest::newRow(dataTag: "QT_TR_NOOP")
283 << QString::fromUtf8(str: "ListElement { foo: QT_TR_NOOP(\"na\303\257ve\") }")
284 << QVariant(QString::fromUtf8(str: "na\303\257ve"))
285 << QString();
286
287 QTest::newRow(dataTag: "QT_TRANSLATE_NOOP")
288 << "ListElement { foo: QT_TRANSLATE_NOOP(\"MyListModel\", \"hello\") }"
289 << QVariant(QString("hello"))
290 << QString();
291
292 QTest::newRow(dataTag: "QT_TRID_NOOP")
293 << QString::fromUtf8(str: "ListElement { foo: QT_TRID_NOOP(\"qtn_1st_text\") }")
294 << QVariant(QString("qtn_1st_text"))
295 << QString();
296
297 QTest::newRow(dataTag: "QT_TR_NOOP extra param")
298 << QString::fromUtf8(str: "ListElement { foo: QT_TR_NOOP(\"hello\",\"world\") }")
299 << QVariant(QString())
300 << QString("ListElement: cannot use script for property value");
301
302 QTest::newRow(dataTag: "QT_TRANSLATE_NOOP missing params")
303 << "ListElement { foo: QT_TRANSLATE_NOOP() }"
304 << QVariant(QString())
305 << QString("ListElement: cannot use script for property value");
306
307 QTest::newRow(dataTag: "QT_TRID_NOOP missing param")
308 << QString::fromUtf8(str: "ListElement { foo: QT_TRID_NOOP() }")
309 << QVariant(QString())
310 << QString("ListElement: cannot use script for property value");
311}
312
313void tst_qqmllistmodel::static_i18n()
314{
315 QFETCH(QString, qml);
316 QFETCH(QVariant, value);
317 QFETCH(QString, error);
318
319 qml = "import QtQuick 2.0\nItem { property variant test: model.get(0).foo; ListModel { id: model; " + qml + " } }";
320
321 QQmlEngine engine;
322 QQmlComponent component(&engine);
323 component.setData(qml.toUtf8(),
324 baseUrl: QUrl::fromLocalFile(localfile: QString("dummy.qml")));
325
326 if (!error.isEmpty()) {
327 QVERIFY(component.isError());
328 QCOMPARE(component.errors().at(0).description(), error);
329 return;
330 }
331
332 QVERIFY(!component.isError());
333
334 QObject *obj = component.create();
335 QVERIFY(obj != nullptr);
336
337 QVariant actual = obj->property(name: "test");
338
339 QCOMPARE(actual, value);
340 QCOMPARE(actual.toString(), value.toString());
341
342 delete obj;
343}
344
345void tst_qqmllistmodel::dynamic_i18n_data()
346{
347 QTest::addColumn<QString>(name: "qml");
348 QTest::addColumn<QVariant>(name: "value");
349 QTest::addColumn<QString>(name: "error");
350
351 QTest::newRow(dataTag: "qsTr")
352 << QString::fromUtf8(str: "ListElement { foo: qsTr(\"test\") }")
353 << QVariant(QString::fromUtf8(str: "test"))
354 << QString();
355
356 QTest::newRow(dataTag: "qsTrId")
357 << "ListElement { foo: qsTrId(\"qtn_test\") }"
358 << QVariant(QString("qtn_test"))
359 << QString();
360}
361
362void tst_qqmllistmodel::dynamic_i18n()
363{
364 QFETCH(QString, qml);
365 QFETCH(QVariant, value);
366 QFETCH(QString, error);
367
368 qml = "import QtQuick 2.0\nItem { property variant test: model.get(0).foo; ListModel { id: model; " + qml + " } }";
369
370 QQmlEngine engine;
371 QQmlComponent component(&engine);
372 component.setData(qml.toUtf8(),
373 baseUrl: QUrl::fromLocalFile(localfile: QString("dummy.qml")));
374
375 if (!error.isEmpty()) {
376 QVERIFY(component.isError());
377 QCOMPARE(component.errors().at(0).description(), error);
378 return;
379 }
380
381 QVERIFY(!component.isError());
382
383 QObject *obj = component.create();
384 QVERIFY(obj != nullptr);
385
386 QVariant actual = obj->property(name: "test");
387
388 QCOMPARE(actual, value);
389 QCOMPARE(actual.toString(), value.toString());
390
391 delete obj;
392}
393void tst_qqmllistmodel::static_nestedElements()
394{
395 QFETCH(int, elementCount);
396
397 QStringList elements;
398 for (int i=0; i<elementCount; i++)
399 elements.append(t: "ListElement { a: 1; b: 2 }");
400 QString elementsStr = elements.join(sep: ",\n") + "\n";
401
402 QString componentStr =
403 "import QtQuick 2.0\n"
404 "Item {\n"
405 " property variant count: model.get(0).attributes.count\n"
406 " ListModel {\n"
407 " id: model\n"
408 " ListElement {\n"
409 " attributes: [\n";
410 componentStr += elementsStr.toUtf8().constData();
411 componentStr +=
412 " ]\n"
413 " }\n"
414 " }\n"
415 "}";
416
417 QQmlEngine engine;
418 QQmlComponent component(&engine);
419 component.setData(componentStr.toUtf8(), baseUrl: QUrl::fromLocalFile(localfile: ""));
420
421 QObject *obj = component.create();
422 QVERIFY(obj != nullptr);
423
424 QVariant count = obj->property(name: "count");
425 QCOMPARE(count.type(), QVariant::Int);
426 QCOMPARE(count.toInt(), elementCount);
427
428 delete obj;
429}
430
431void tst_qqmllistmodel::static_nestedElements_data()
432{
433 QTest::addColumn<int>(name: "elementCount");
434
435 QTest::newRow(dataTag: "0 items") << 0;
436 QTest::newRow(dataTag: "1 item") << 1;
437 QTest::newRow(dataTag: "2 items") << 2;
438 QTest::newRow(dataTag: "many items") << 5;
439}
440
441void tst_qqmllistmodel::dynamic_data()
442{
443 QTest::addColumn<QString>(name: "script");
444 QTest::addColumn<int>(name: "result");
445 QTest::addColumn<QString>(name: "warning");
446 QTest::addColumn<bool>(name: "dynamicRoles");
447
448 for (int i=0 ; i < 2 ; ++i) {
449 bool dr = (i != 0);
450
451 // Simple flat model
452 QTest::newRow(dataTag: "count") << "count" << 0 << "" << dr;
453
454 QTest::newRow(dataTag: "get1") << "{get(0) === undefined}" << 1 << "" << dr;
455 QTest::newRow(dataTag: "get2") << "{get(-1) === undefined}" << 1 << "" << dr;
456 QTest::newRow(dataTag: "get3") << "{append({'foo':123});get(0) != undefined}" << 1 << "" << dr;
457 QTest::newRow(dataTag: "get4") << "{append({'foo':123});get(0).foo}" << 123 << "" << dr;
458 QTest::newRow(dataTag: "get5") << "{append({'foo':123});get(0) == get(0)}" << 1 << "" << dr;
459 QTest::newRow(dataTag: "get-modify1") << "{append({'foo':123,'bar':456});get(0).foo = 333;get(0).foo}" << 333 << "" << dr;
460 QTest::newRow(dataTag: "get-modify2") << "{append({'z':1});append({'foo':123,'bar':456});get(1).bar = 999;get(1).bar}" << 999 << "" << dr;
461
462 QTest::newRow(dataTag: "append1") << "{append({'foo':123});count}" << 1 << "" << dr;
463 QTest::newRow(dataTag: "append2") << "{append({'foo':123,'bar':456});count}" << 1 << "" << dr;
464 QTest::newRow(dataTag: "append3a") << "{append({'foo':123});append({'foo':456});get(0).foo}" << 123 << "" << dr;
465 QTest::newRow(dataTag: "append3b") << "{append({'foo':123});append({'foo':456});get(1).foo}" << 456 << "" << dr;
466 QTest::newRow(dataTag: "append4a") << "{append(123)}" << 0 << "<Unknown File>: QML ListModel: append: value is not an object" << dr;
467 QTest::newRow(dataTag: "append4b") << "{append([{'foo':123},{'foo':456},{'foo':789}]);count}" << 3 << "" << dr;
468 QTest::newRow(dataTag: "append4c") << "{append([{'foo':123},{'foo':456},{'foo':789}]);get(1).foo}" << 456 << "" << dr;
469
470 QTest::newRow(dataTag: "clear1") << "{append({'foo':456});clear();count}" << 0 << "" << dr;
471 QTest::newRow(dataTag: "clear2") << "{append({'foo':123});append({'foo':456});clear();count}" << 0 << "" << dr;
472 QTest::newRow(dataTag: "clear3") << "{append({'foo':123});clear()}" << 0 << "" << dr;
473
474 QTest::newRow(dataTag: "remove1") << "{append({'foo':123});remove(0);count}" << 0 << "" << dr;
475 QTest::newRow(dataTag: "remove2a") << "{append({'foo':123});append({'foo':456});remove(0);count}" << 1 << "" << dr;
476 QTest::newRow(dataTag: "remove2b") << "{append({'foo':123});append({'foo':456});remove(0);get(0).foo}" << 456 << "" << dr;
477 QTest::newRow(dataTag: "remove2c") << "{append({'foo':123});append({'foo':456});remove(1);get(0).foo}" << 123 << "" << dr;
478 QTest::newRow(dataTag: "remove3") << "{append({'foo':123});remove(0)}" << 0 << "" << dr;
479 QTest::newRow(dataTag: "remove3a") << "{append({'foo':123});remove(-1);count}" << 1 << "<Unknown File>: QML ListModel: remove: indices [-1 - 0] out of range [0 - 1]" << dr;
480 QTest::newRow(dataTag: "remove4a") << "{remove(0)}" << 0 << "<Unknown File>: QML ListModel: remove: indices [0 - 1] out of range [0 - 0]" << dr;
481 QTest::newRow(dataTag: "remove4b") << "{append({'foo':123});remove(0);remove(0);count}" << 0 << "<Unknown File>: QML ListModel: remove: indices [0 - 1] out of range [0 - 0]" << dr;
482 QTest::newRow(dataTag: "remove4c") << "{append({'foo':123});remove(1);count}" << 1 << "<Unknown File>: QML ListModel: remove: indices [1 - 2] out of range [0 - 1]" << dr;
483 QTest::newRow(dataTag: "remove5a") << "{append({'foo':123});append({'foo':456});remove(0,2);count}" << 0 << "" << dr;
484 QTest::newRow(dataTag: "remove5b") << "{append({'foo':123});append({'foo':456});remove(0,1);count}" << 1 << "" << dr;
485 QTest::newRow(dataTag: "remove5c") << "{append({'foo':123});append({'foo':456});remove(1,1);count}" << 1 << "" << dr;
486 QTest::newRow(dataTag: "remove5d") << "{append({'foo':123});append({'foo':456});remove(0,1);get(0).foo}" << 456 << "" << dr;
487 QTest::newRow(dataTag: "remove5e") << "{append({'foo':123});append({'foo':456});remove(1,1);get(0).foo}" << 123 << "" << dr;
488 QTest::newRow(dataTag: "remove5f") << "{append({'foo':123});append({'foo':456});append({'foo':789});remove(0,1);remove(1,1);get(0).foo}" << 456 << "" << dr;
489 QTest::newRow(dataTag: "remove6a") << "{remove();count}" << 0 << "<Unknown File>: QML ListModel: remove: incorrect number of arguments" << dr;
490 QTest::newRow(dataTag: "remove6b") << "{remove(1,2,3);count}" << 0 << "<Unknown File>: QML ListModel: remove: incorrect number of arguments" << dr;
491 QTest::newRow(dataTag: "remove7a") << "{append({'foo':123});remove(0,0);count}" << 1 << "<Unknown File>: QML ListModel: remove: indices [0 - 0] out of range [0 - 1]" << dr;
492 QTest::newRow(dataTag: "remove7b") << "{append({'foo':123});remove(0,-1);count}" << 1 << "<Unknown File>: QML ListModel: remove: indices [0 - -1] out of range [0 - 1]" << dr;
493
494 QTest::newRow(dataTag: "insert1") << "{insert(0,{'foo':123});count}" << 1 << "" << dr;
495 QTest::newRow(dataTag: "insert2") << "{insert(1,{'foo':123});count}" << 0 << "<Unknown File>: QML ListModel: insert: index 1 out of range" << dr;
496 QTest::newRow(dataTag: "insert3a") << "{append({'foo':123});insert(1,{'foo':456});count}" << 2 << "" << dr;
497 QTest::newRow(dataTag: "insert3b") << "{append({'foo':123});insert(1,{'foo':456});get(0).foo}" << 123 << "" << dr;
498 QTest::newRow(dataTag: "insert3c") << "{append({'foo':123});insert(1,{'foo':456});get(1).foo}" << 456 << "" << dr;
499 QTest::newRow(dataTag: "insert3d") << "{append({'foo':123});insert(0,{'foo':456});get(0).foo}" << 456 << "" << dr;
500 QTest::newRow(dataTag: "insert3e") << "{append({'foo':123});insert(0,{'foo':456});get(1).foo}" << 123 << "" << dr;
501 QTest::newRow(dataTag: "insert4") << "{append({'foo':123});insert(-1,{'foo':456});count}" << 1 << "<Unknown File>: QML ListModel: insert: index -1 out of range" << dr;
502 QTest::newRow(dataTag: "insert5a") << "{insert(0,123)}" << 0 << "<Unknown File>: QML ListModel: insert: value is not an object" << dr;
503 QTest::newRow(dataTag: "insert5b") << "{insert(0,[{'foo':11},{'foo':22},{'foo':33}]);count}" << 3 << "" << dr;
504 QTest::newRow(dataTag: "insert5c") << "{insert(0,[{'foo':11},{'foo':22},{'foo':33}]);get(2).foo}" << 33 << "" << dr;
505
506 QTest::newRow(dataTag: "set1") << "{append({'foo':123});set(0,{'foo':456});count}" << 1 << "" << dr;
507 QTest::newRow(dataTag: "set2") << "{append({'foo':123});set(0,{'foo':456});get(0).foo}" << 456 << "" << dr;
508 QTest::newRow(dataTag: "set3a") << "{append({'foo':123,'bar':456});set(0,{'foo':999});get(0).foo}" << 999 << "" << dr;
509 QTest::newRow(dataTag: "set3b") << "{append({'foo':123,'bar':456});set(0,{'foo':999});get(0).bar}" << 456 << "" << dr;
510 QTest::newRow(dataTag: "set4a") << "{set(0,{'foo':456});count}" << 1 << "" << dr;
511 QTest::newRow(dataTag: "set4c") << "{set(-1,{'foo':456})}" << 0 << "<Unknown File>: QML ListModel: set: index -1 out of range" << dr;
512 QTest::newRow(dataTag: "set5a") << "{append({'foo':123,'bar':456});set(0,123);count}" << 1 << "<Unknown File>: QML ListModel: set: value is not an object" << dr;
513 QTest::newRow(dataTag: "set5b") << "{append({'foo':123,'bar':456});set(0,[1,2,3]);count}" << 1 << "" << dr;
514 QTest::newRow(dataTag: "set6") << "{append({'foo':123});set(1,{'foo':456});count}" << 2 << "" << dr;
515
516 QTest::newRow(dataTag: "setprop1") << "{append({'foo':123});setProperty(0,'foo',456);count}" << 1 << "" << dr;
517 QTest::newRow(dataTag: "setprop2") << "{append({'foo':123});setProperty(0,'foo',456);get(0).foo}" << 456 << "" << dr;
518 QTest::newRow(dataTag: "setprop3a") << "{append({'foo':123,'bar':456});setProperty(0,'foo',999);get(0).foo}" << 999 << "" << dr;
519 QTest::newRow(dataTag: "setprop3b") << "{append({'foo':123,'bar':456});setProperty(0,'foo',999);get(0).bar}" << 456 << "" << dr;
520 QTest::newRow(dataTag: "setprop4a") << "{setProperty(0,'foo',456)}" << 0 << "<Unknown File>: QML ListModel: set: index 0 out of range" << dr;
521 QTest::newRow(dataTag: "setprop4b") << "{setProperty(-1,'foo',456)}" << 0 << "<Unknown File>: QML ListModel: set: index -1 out of range" << dr;
522 QTest::newRow(dataTag: "setprop4c") << "{append({'foo':123,'bar':456});setProperty(1,'foo',456);count}" << 1 << "<Unknown File>: QML ListModel: set: index 1 out of range" << dr;
523 QTest::newRow(dataTag: "setprop5") << "{append({'foo':123,'bar':456});append({'foo':111});setProperty(1,'bar',222);get(1).bar}" << 222 << "" << dr;
524
525 QTest::newRow(dataTag: "move1a") << "{append({'foo':123});append({'foo':456});move(0,1,1);count}" << 2 << "" << dr;
526 QTest::newRow(dataTag: "move1b") << "{append({'foo':123});append({'foo':456});move(0,1,1);get(0).foo}" << 456 << "" << dr;
527 QTest::newRow(dataTag: "move1c") << "{append({'foo':123});append({'foo':456});move(0,1,1);get(1).foo}" << 123 << "" << dr;
528 QTest::newRow(dataTag: "move1d") << "{append({'foo':123});append({'foo':456});move(1,0,1);get(0).foo}" << 456 << "" << dr;
529 QTest::newRow(dataTag: "move1e") << "{append({'foo':123});append({'foo':456});move(1,0,1);get(1).foo}" << 123 << "" << dr;
530 QTest::newRow(dataTag: "move2a") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);count}" << 3 << "" << dr;
531 QTest::newRow(dataTag: "move2b") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);get(0).foo}" << 789 << "" << dr;
532 QTest::newRow(dataTag: "move2c") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);get(1).foo}" << 123 << "" << dr;
533 QTest::newRow(dataTag: "move2d") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);get(2).foo}" << 456 << "" << dr;
534 QTest::newRow(dataTag: "move3a") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(1,0,3);count}" << 3 << "<Unknown File>: QML ListModel: move: out of range" << dr;
535 QTest::newRow(dataTag: "move3b") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(1,-1,1);count}" << 3 << "<Unknown File>: QML ListModel: move: out of range" << dr;
536 QTest::newRow(dataTag: "move3c") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(1,0,-1);count}" << 3 << "<Unknown File>: QML ListModel: move: out of range" << dr;
537 QTest::newRow(dataTag: "move3d") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,3,1);count}" << 3 << "<Unknown File>: QML ListModel: move: out of range" << dr;
538
539 QTest::newRow(dataTag: "large1") << "{append({'a':1,'b':2,'c':3,'d':4,'e':5,'f':6,'g':7,'h':8});get(0).h}" << 8 << "" << dr;
540
541 QTest::newRow(dataTag: "datatypes1") << "{append({'a':1});append({'a':'string'});}" << 0 << "<Unknown File>: Can't assign to existing role 'a' of different type [String -> Number]" << dr;
542
543 QTest::newRow(dataTag: "null") << "{append({'a':null});}" << 0 << "" << dr;
544 QTest::newRow(dataTag: "setNull") << "{append({'a':1});set(0, {'a':null});}" << 0 << "" << dr;
545 QTest::newRow(dataTag: "setString") << "{append({'a':'hello'});set(0, {'a':'world'});get(0).a == 'world'}" << 1 << "" << dr;
546 QTest::newRow(dataTag: "setInt") << "{append({'a':5});set(0, {'a':10});get(0).a}" << 10 << "" << dr;
547 QTest::newRow(dataTag: "setNumber") << "{append({'a':6});set(0, {'a':5.5});get(0).a < 5.6}" << 1 << "" << dr;
548 QTest::newRow(dataTag: "badType0") << "{append({'a':'hello'});set(0, {'a':1});}" << 0 << "<Unknown File>: Can't assign to existing role 'a' of different type [Number -> String]" << dr;
549 QTest::newRow(dataTag: "invalidInsert0") << "{insert(0);}" << 0 << "<Unknown File>: QML ListModel: insert: value is not an object" << dr;
550 QTest::newRow(dataTag: "invalidAppend0") << "{append();}" << 0 << "<Unknown File>: QML ListModel: append: value is not an object" << dr;
551 QTest::newRow(dataTag: "invalidInsert1") << "{insert(0, 34);}" << 0 << "<Unknown File>: QML ListModel: insert: value is not an object" << dr;
552 QTest::newRow(dataTag: "invalidAppend1") << "{append(37);}" << 0 << "<Unknown File>: QML ListModel: append: value is not an object" << dr;
553
554 // QObjects
555 QTest::newRow(dataTag: "qobject0") << "{append({'a':dummyItem0});}" << 0 << "" << dr;
556 QTest::newRow(dataTag: "qobject1") << "{append({'a':dummyItem0});set(0,{'a':dummyItem1});get(0).a == dummyItem1;}" << 1 << "" << dr;
557 QTest::newRow(dataTag: "qobject2") << "{append({'a':dummyItem0});get(0).a == dummyItem0;}" << 1 << "" << dr;
558 QTest::newRow(dataTag: "qobject3") << "{append({'a':dummyItem0});append({'b':1});}" << 0 << "" << dr;
559
560 // JS objects
561 QTest::newRow(dataTag: "js1") << "{append({'foo':{'prop':1}});count}" << 1 << "" << dr;
562 QTest::newRow(dataTag: "js2") << "{append({'foo':{'prop':27}});get(0).foo.prop}" << 27 << "" << dr;
563 QTest::newRow(dataTag: "js3") << "{append({'foo':{'prop':27}});append({'bar':1});count}" << 2 << "" << dr;
564 QTest::newRow(dataTag: "js4") << "{append({'foo':{'prop':27}});append({'bar':1});set(0, {'foo':{'prop':28}});get(0).foo.prop}" << 28 << "" << dr;
565 QTest::newRow(dataTag: "js5") << "{append({'foo':{'prop':27}});append({'bar':1});set(1, {'foo':{'prop':33}});get(1).foo.prop}" << 33 << "" << dr;
566 QTest::newRow(dataTag: "js6") << "{append({'foo':{'prop':27}});clear();count}" << 0 << "" << dr;
567 QTest::newRow(dataTag: "js7") << "{append({'foo':{'prop':27}});set(0, {'foo':null});count}" << 1 << "" << dr;
568 QTest::newRow(dataTag: "js8") << "{append({'foo':{'prop':27}});set(0, {'foo':{'prop2':31}});get(0).foo.prop2}" << 31 << "" << dr;
569
570 // Nested models
571 QTest::newRow(dataTag: "nested-append1") << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]});count}" << 1 << "" << dr;
572 QTest::newRow(dataTag: "nested-append2") << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]});get(0).bars.get(1).a}" << 2 << "" << dr;
573 QTest::newRow(dataTag: "nested-append3") << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]});get(0).bars.append({'a':4});get(0).bars.get(3).a}" << 4 << "" << dr;
574
575 QTest::newRow(dataTag: "nested-insert") << "{append({'foo':123});insert(0,{'bars':[{'a':1},{'b':2},{'c':3}]});get(0).bars.get(0).a}" << 1 << "" << dr;
576 QTest::newRow(dataTag: "nested-set") << "{append({'foo':[{'x':1}]});set(0,{'foo':[{'x':123}]});get(0).foo.get(0).x}" << 123 << "" << dr;
577
578 QTest::newRow(dataTag: "nested-count") << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]}); get(0).bars.count}" << 3 << "" << dr;
579 QTest::newRow(dataTag: "nested-clear") << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]}); get(0).bars.clear(); get(0).bars.count}" << 0 << "" << dr;
580 }
581
582 QTest::newRow(dataTag: "jsarray") << "{append({'foo':['1', '2', '3']});get(0).foo.get(0)}" << 0 << "" << false;
583}
584
585void tst_qqmllistmodel::dynamic()
586{
587 QFETCH(QString, script);
588 QFETCH(int, result);
589 QFETCH(QString, warning);
590 QFETCH(bool, dynamicRoles);
591
592 QQuickItem dummyItem0, dummyItem1;
593 QQmlEngine engine;
594 QQmlListModel model;
595 model.setDynamicRoles(dynamicRoles);
596 QQmlEngine::setContextForObject(&model,engine.rootContext());
597 engine.rootContext()->setContextObject(&model);
598 engine.rootContext()->setContextProperty("dummyItem0", QVariant::fromValue(value: &dummyItem0));
599 engine.rootContext()->setContextProperty("dummyItem1", QVariant::fromValue(value: &dummyItem1));
600 QQmlExpression e(engine.rootContext(), &model, script);
601 if (isValidErrorMessage(msg: warning, dynamicRoleTest: dynamicRoles))
602 QTest::ignoreMessage(type: QtWarningMsg, message: warning.toLatin1());
603
604 QSignalSpy spyCount(&model, SIGNAL(countChanged()));
605
606 int actual = e.evaluate().toInt();
607 if (e.hasError())
608 qDebug() << e.error(); // errors not expected
609
610 QCOMPARE(actual,result);
611
612 if (model.count() > 0)
613 QVERIFY(spyCount.count() > 0);
614}
615
616void tst_qqmllistmodel::enumerate()
617{
618 QQmlEngine eng;
619 QQmlComponent component(&eng, testFileUrl(fileName: "enumerate.qml"));
620 QVERIFY(!component.isError());
621 QQuickItem *item = qobject_cast<QQuickItem*>(object: component.create());
622 QVERIFY(item != nullptr);
623
624 QLatin1String expectedStrings[] = {
625 QLatin1String("val1=1Y"),
626 QLatin1String("val2=2Y"),
627 QLatin1String("val3=strY"),
628 QLatin1String("val4=falseN"),
629 QLatin1String("val5=trueY")
630 };
631
632 int expectedStringCount = sizeof(expectedStrings) / sizeof(expectedStrings[0]);
633
634 QStringList r = item->property(name: "result").toString().split(sep: QLatin1Char(':'));
635
636 int matchCount = 0;
637 for (int i=0 ; i < expectedStringCount ; ++i) {
638 const QLatin1String &expectedString = expectedStrings[i];
639
640 QStringList::const_iterator it = r.begin();
641 QStringList::const_iterator end = r.end();
642
643 while (it != end) {
644 if (it->compare(other: expectedString) == 0) {
645 ++matchCount;
646 break;
647 }
648 ++it;
649 }
650 }
651
652 QCOMPARE(matchCount, expectedStringCount);
653
654 delete item;
655}
656
657void tst_qqmllistmodel::error_data()
658{
659 QTest::addColumn<QString>(name: "qml");
660 QTest::addColumn<QString>(name: "error");
661
662 QTest::newRow(dataTag: "id not allowed in ListElement")
663 << "import QtQuick 2.0\nListModel { ListElement { id: fred } }"
664 << "ListElement: cannot use reserved \"id\" property";
665
666 QTest::newRow(dataTag: "id allowed in ListModel")
667 << "import QtQuick 2.0\nListModel { id:model }"
668 << "";
669
670 QTest::newRow(dataTag: "random properties not allowed in ListModel")
671 << "import QtQuick 2.0\nListModel { foo:123 }"
672 << "ListModel: undefined property 'foo'";
673
674 QTest::newRow(dataTag: "random properties allowed in ListElement")
675 << "import QtQuick 2.0\nListModel { ListElement { foo:123 } }"
676 << "";
677
678 QTest::newRow(dataTag: "bindings not allowed in ListElement")
679 << "import QtQuick 2.0\nRectangle { id: rect; ListModel { ListElement { foo: rect.color } } }"
680 << "ListElement: cannot use script for property value";
681
682 QTest::newRow(dataTag: "random object list properties allowed in ListElement")
683 << "import QtQuick 2.0\nListModel { ListElement { foo: [ ListElement { bar: 123 } ] } }"
684 << "";
685
686 QTest::newRow(dataTag: "default properties not allowed in ListElement")
687 << "import QtQuick 2.0\nListModel { ListElement { Item { } } }"
688 << "ListElement: cannot contain nested elements";
689
690 QTest::newRow(dataTag: "QML elements not allowed in ListElement")
691 << "import QtQuick 2.0\nListModel { ListElement { a: Item { } } }"
692 << "ListElement: cannot contain nested elements";
693
694 QTest::newRow(dataTag: "qualified ListElement supported")
695 << "import QtQuick 2.0 as Foo\nFoo.ListModel { Foo.ListElement { a: 123 } }"
696 << "";
697
698 QTest::newRow(dataTag: "qualified ListElement required")
699 << "import QtQuick 2.0 as Foo\nFoo.ListModel { ListElement { a: 123 } }"
700 << "ListElement is not a type";
701
702 QTest::newRow(dataTag: "unknown qualified ListElement not allowed")
703 << "import QtQuick 2.0\nListModel { Foo.ListElement { a: 123 } }"
704 << "Foo.ListElement - Foo is neither a type nor a namespace";
705}
706
707void tst_qqmllistmodel::error()
708{
709 QFETCH(QString, qml);
710 QFETCH(QString, error);
711
712 QQmlEngine engine;
713 QQmlComponent component(&engine);
714 component.setData(qml.toUtf8(),
715 baseUrl: QUrl::fromLocalFile(localfile: QString("dummy.qml")));
716 if (error.isEmpty()) {
717 QVERIFY(!component.isError());
718 } else {
719 QVERIFY(component.isError());
720 QList<QQmlError> errors = component.errors();
721 QCOMPARE(errors.count(),1);
722 QCOMPARE(errors.at(0).description(),error);
723 }
724}
725
726void tst_qqmllistmodel::syncError()
727{
728 QString qml = "import QtQuick 2.0\nListModel { id: lm; Component.onCompleted: lm.sync() }";
729 QString error = "file:dummy.qml:2:1: QML ListModel: List sync() can only be called from a WorkerScript";
730
731 QQmlEngine engine;
732 QQmlComponent component(&engine);
733 component.setData(qml.toUtf8(),
734 baseUrl: QUrl::fromLocalFile(localfile: QString("dummy.qml")));
735 QTest::ignoreMessage(type: QtWarningMsg,message: error.toUtf8());
736 QObject *obj = component.create();
737 QVERIFY(obj);
738 delete obj;
739}
740
741/*
742 Test model changes from set() are available to the view
743*/
744void tst_qqmllistmodel::set_data()
745{
746 QTest::addColumn<bool>(name: "dynamicRoles");
747
748 QTest::newRow(dataTag: "staticRoles") << false;
749 QTest::newRow(dataTag: "dynamicRoles") << true;
750}
751
752void tst_qqmllistmodel::set()
753{
754 QFETCH(bool, dynamicRoles);
755
756 QQmlEngine engine;
757 QQmlListModel model;
758 model.setDynamicRoles(dynamicRoles);
759 QQmlEngine::setContextForObject(&model,engine.rootContext());
760 engine.rootContext()->setContextProperty("model", &model);
761
762 RUNEXPR("model.append({test:false})");
763 RUNEXPR("model.set(0, {test:true})");
764
765 QCOMPARE(RUNEXPR("model.get(0).test").toBool(), true); // triggers creation of model cache
766 QCOMPARE(model.data(0, 0), QVariant::fromValue(true));
767
768 RUNEXPR("model.set(0, {test:false})");
769 QCOMPARE(RUNEXPR("model.get(0).test").toBool(), false); // tests model cache is updated
770 QCOMPARE(model.data(0, 0), QVariant::fromValue(false));
771
772 QString warning = QString::fromLatin1(str: "<Unknown File>: Can't create role for unsupported data type");
773 if (isValidErrorMessage(msg: warning, dynamicRoleTest: dynamicRoles))
774 QTest::ignoreMessage(type: QtWarningMsg, message: warning.toLatin1());
775 QVariant invalidData = QColor();
776 model.setProperty(index: 0, property: "test", value: invalidData);
777}
778
779/*
780 Test model changes on values returned by get() are available to the view
781*/
782void tst_qqmllistmodel::get()
783{
784 QFETCH(QString, expression);
785 QFETCH(int, index);
786 QFETCH(QString, roleName);
787 QFETCH(QVariant, roleValue);
788 QFETCH(bool, dynamicRoles);
789
790 QQmlEngine engine;
791 QQmlComponent component(&engine);
792 component.setData(
793 "import QtQuick 2.0\n"
794 "ListModel {}\n", baseUrl: QUrl());
795 QQmlListModel *model = qobject_cast<QQmlListModel*>(object: component.create());
796 model->setDynamicRoles(dynamicRoles);
797 engine.rootContext()->setContextProperty("model", model);
798
799 RUNEXPR("model.append({roleA: 100})");
800 RUNEXPR("model.append({roleA: 200, roleB: 400})");
801 RUNEXPR("model.append({roleA: 200, roleB: 400})");
802 RUNEXPR("model.append({roleC: {} })");
803 RUNEXPR("model.append({roleD: [ { a:1, b:2 }, { c: 3 } ] })");
804
805 QSignalSpy spy(model, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)));
806 QQmlExpression expr(engine.rootContext(), model, expression);
807 expr.evaluate();
808 QVERIFY(!expr.hasError());
809
810 int role = roleFromName(model, roleName);
811 QVERIFY(role >= 0);
812
813 if (roleValue.type() == QVariant::List) {
814 const QVariantList &list = roleValue.toList();
815 QVERIFY(compareVariantList(list, model->data(index, role)));
816 } else {
817 QCOMPARE(model->data(index, role), roleValue);
818 }
819
820 QCOMPARE(spy.count(), 1);
821
822 QList<QVariant> spyResult = spy.takeFirst();
823 QCOMPARE(spyResult.at(0).value<QModelIndex>(), model->index(index, 0, QModelIndex()));
824 QCOMPARE(spyResult.at(1).value<QModelIndex>(), model->index(index, 0, QModelIndex())); // only 1 item is modified at a time
825 QCOMPARE(spyResult.at(2).value<QVector<int> >(), (QVector<int>() << role));
826
827 delete model;
828}
829
830void tst_qqmllistmodel::get_data()
831{
832 QTest::addColumn<QString>(name: "expression");
833 QTest::addColumn<int>(name: "index");
834 QTest::addColumn<QString>(name: "roleName");
835 QTest::addColumn<QVariant>(name: "roleValue");
836 QTest::addColumn<bool>(name: "dynamicRoles");
837
838 for (int i=0 ; i < 2 ; ++i) {
839 bool dr = (i != 0);
840
841 QTest::newRow(dataTag: "simple value") << "get(0).roleA = 500" << 0 << "roleA" << QVariant(500) << dr;
842 QTest::newRow(dataTag: "simple value 2") << "get(1).roleB = 500" << 1 << "roleB" << QVariant(500) << dr;
843
844 QVariantMap map;
845 QVariantList list;
846 map.clear(); map["a"] = 50; map["b"] = 500;
847 list << map;
848 map.clear(); map["c"] = 1000;
849 list << map;
850 QTest::newRow(dataTag: "list of objects") << "get(2).roleD = [{'a': 50, 'b': 500}, {'c': 1000}]" << 2 << "roleD" << QVariant::fromValue(value: list) << dr;
851 }
852}
853
854/*
855 Test that the tests run in get() also work for nested list data
856*/
857void tst_qqmllistmodel::get_nested()
858{
859 QFETCH(QString, expression);
860 QFETCH(int, index);
861 QFETCH(QString, roleName);
862 QFETCH(QVariant, roleValue);
863 QFETCH(bool, dynamicRoles);
864
865 if (roleValue.type() == QVariant::Map)
866 return;
867
868 QQmlEngine engine;
869 QQmlComponent component(&engine);
870 component.setData(
871 "import QtQuick 2.0\n"
872 "ListModel {}", baseUrl: QUrl());
873 QQmlListModel *model = qobject_cast<QQmlListModel*>(object: component.create());
874 model->setDynamicRoles(dynamicRoles);
875 QVERIFY(component.errorString().isEmpty());
876 QQmlListModel *childModel;
877 engine.rootContext()->setContextProperty("model", model);
878
879 RUNEXPR("model.append({ listRoleA: [\n"
880 "{ roleA: 100 },\n"
881 "{ roleA: 200, roleB: 400 },\n"
882 "{ roleA: 200, roleB: 400 }, \n"
883 "{ roleC: {} }, \n"
884 "{ roleD: [ { a: 1, b:2 }, { c: 3 } ] } \n"
885 "] })\n");
886
887 RUNEXPR("model.append({ listRoleA: [\n"
888 "{ roleA: 100 },\n"
889 "{ roleA: 200, roleB: 400 },\n"
890 "{ roleA: 200, roleB: 400 }, \n"
891 "{ roleC: {} }, \n"
892 "{ roleD: [ { a: 1, b:2 }, { c: 3 } ] } \n"
893 "],\n"
894 "listRoleB: [\n"
895 "{ roleA: 100 },\n"
896 "{ roleA: 200, roleB: 400 },\n"
897 "{ roleA: 200, roleB: 400 }, \n"
898 "{ roleC: {} }, \n"
899 "{ roleD: [ { a: 1, b:2 }, { c: 3 } ] } \n"
900 "],\n"
901 "listRoleC: [\n"
902 "{ roleA: 100 },\n"
903 "{ roleA: 200, roleB: 400 },\n"
904 "{ roleA: 200, roleB: 400 }, \n"
905 "{ roleC: {} }, \n"
906 "{ roleD: [ { a: 1, b:2 }, { c: 3 } ] } \n"
907 "] })\n");
908
909 // Test setting the inner list data for:
910 // get(0).listRoleA
911 // get(1).listRoleA
912 // get(1).listRoleB
913 // get(1).listRoleC
914
915 QList<QPair<int, QString> > testData;
916 testData << qMakePair(x: 0, y: QString("listRoleA"));
917 testData << qMakePair(x: 1, y: QString("listRoleA"));
918 testData << qMakePair(x: 1, y: QString("listRoleB"));
919 testData << qMakePair(x: 1, y: QString("listRoleC"));
920
921 for (int i=0; i<testData.count(); i++) {
922 int outerListIndex = testData[i].first;
923 QString outerListRoleName = testData[i].second;
924 int outerListRole = roleFromName(model, roleName: outerListRoleName);
925 QVERIFY(outerListRole >= 0);
926
927 childModel = qobject_cast<QQmlListModel*>(object: model->data(index: outerListIndex, role: outerListRole).value<QObject*>());
928 QVERIFY(childModel);
929
930 QString extendedExpression = QString("get(%1).%2.%3").arg(a: outerListIndex).arg(a: outerListRoleName).arg(a: expression);
931 QQmlExpression expr(engine.rootContext(), model, extendedExpression);
932
933 QSignalSpy spy(childModel, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)));
934 expr.evaluate();
935 QVERIFY(!expr.hasError());
936
937 int role = roleFromName(model: childModel, roleName);
938 QVERIFY(role >= 0);
939 if (roleValue.type() == QVariant::List) {
940 QVERIFY(compareVariantList(roleValue.toList(), childModel->data(index, role)));
941 } else {
942 QCOMPARE(childModel->data(index, role), roleValue);
943 }
944 QCOMPARE(spy.count(), 1);
945
946 QList<QVariant> spyResult = spy.takeFirst();
947 QCOMPARE(spyResult.at(0).value<QModelIndex>(), childModel->index(index, 0, QModelIndex()));
948 QCOMPARE(spyResult.at(1).value<QModelIndex>(), childModel->index(index, 0, QModelIndex())); // only 1 item is modified at a time
949 QCOMPARE(spyResult.at(2).value<QVector<int> >(), (QVector<int>() << role));
950 }
951
952 delete model;
953}
954
955void tst_qqmllistmodel::get_nested_data()
956{
957 get_data();
958}
959
960//QTBUG-13754
961void tst_qqmllistmodel::crash_model_with_multiple_roles()
962{
963 QQmlEngine eng;
964 QQmlComponent component(&eng, testFileUrl(fileName: "multipleroles.qml"));
965 QObject *rootItem = component.create();
966 QVERIFY(component.errorString().isEmpty());
967 QVERIFY(rootItem != nullptr);
968 QQmlListModel *model = rootItem->findChild<QQmlListModel*>(aName: "listModel");
969 QVERIFY(model != nullptr);
970
971 // used to cause a crash
972 model->setProperty(index: 0, property: "black", value: true);
973
974 delete rootItem;
975}
976
977void tst_qqmllistmodel::crash_model_with_unknown_roles()
978{
979 QQmlEngine eng;
980 QQmlComponent component(&eng, testFileUrl(fileName: "multipleroles.qml"));
981 QScopedPointer<QObject> rootItem(component.create());
982 QVERIFY(component.errorString().isEmpty());
983 QVERIFY(rootItem != nullptr);
984 QQmlListModel *model = rootItem->findChild<QQmlListModel*>(aName: "listModel");
985 QVERIFY(model != nullptr);
986
987 // used to cause a crash in debug builds
988 model->index(row: 0, column: 0, parent: QModelIndex()).data(arole: Qt::DisplayRole);
989 model->index(row: 0, column: 0, parent: QModelIndex()).data(arole: Qt::UserRole);
990}
991
992//QTBUG-35639
993void tst_qqmllistmodel::crash_model_with_dynamic_roles()
994{
995 {
996 // setting a dynamic role to a QObject value, then triggering dtor
997 QQmlEngine eng;
998 QQmlComponent component(&eng, testFileUrl(fileName: "dynamicroles.qml"));
999 QObject *rootItem = component.create();
1000 qWarning() << component.errorString();
1001 QVERIFY(component.errorString().isEmpty());
1002 QVERIFY(rootItem != 0);
1003 QQmlListModel *model = rootItem->findChild<QQmlListModel*>(aName: "listModel");
1004 QVERIFY(model != 0);
1005
1006 QMetaObject::invokeMethod(obj: model, member: "appendNewElement");
1007
1008 QObject *testObj = new QObject;
1009 model->setProperty(index: 0, property: "obj", value: QVariant::fromValue<QObject*>(value: testObj));
1010 delete testObj;
1011
1012 // delete the root item, which will cause the model dtor to run
1013 // previously, this would crash as it attempted to delete testObj.
1014 delete rootItem;
1015 }
1016
1017 {
1018 // setting a dynamic role to a QObject value, then triggering
1019 // DynamicRoleModelNode::updateValues() to trigger unsafe qobject_cast
1020 QQmlEngine eng;
1021 QQmlComponent component(&eng, testFileUrl(fileName: "dynamicroles.qml"));
1022 QObject *rootItem = component.create();
1023 qWarning() << component.errorString();
1024 QVERIFY(component.errorString().isEmpty());
1025 QVERIFY(rootItem != 0);
1026 QQmlListModel *model = rootItem->findChild<QQmlListModel*>(aName: "listModel");
1027 QVERIFY(model != 0);
1028
1029 QMetaObject::invokeMethod(obj: model, member: "appendNewElement");
1030
1031 QObject *testObj = new QObject;
1032 model->setProperty(index: 0, property: "obj", value: QVariant::fromValue<QObject*>(value: testObj));
1033 delete testObj;
1034
1035 QMetaObject::invokeMethod(obj: model, member: "setElementAgain");
1036
1037 delete rootItem;
1038 }
1039
1040 {
1041 // setting a dynamic role to a QObject value, then triggering
1042 // DynamicRoleModelNodeMetaObject::propertyWrite()
1043
1044 /*
1045 XXX TODO: I couldn't reproduce this one simply - I think it
1046 requires a WorkerScript sync() call, and that's non-trivial.
1047 I thought I could do it simply via:
1048
1049 QQmlEngine eng;
1050 QQmlComponent component(&eng, testFileUrl("dynamicroles.qml"));
1051 QObject *rootItem = component.create();
1052 qWarning() << component.errorString();
1053 QVERIFY(component.errorString().isEmpty());
1054 QVERIFY(rootItem != 0);
1055 QQmlListModel *model = rootItem->findChild<QQmlListModel*>("listModel");
1056 QVERIFY(model != 0);
1057
1058 QMetaObject::invokeMethod(model, "appendNewElement");
1059
1060 QObject *testObj = new QObject;
1061 model->setProperty(0, "obj", QVariant::fromValue<QObject*>(testObj));
1062 delete testObj;
1063 QObject *testObj2 = new QObject;
1064 model->setProperty(0, "obj", QVariant::fromValue<QObject*>(testObj2));
1065
1066 But it turns out that that doesn't work (the setValue() call within
1067 setProperty() doesn't seem to trigger the right codepath, for some
1068 reason), and you can't trigger it manually via:
1069
1070 QObject *testObj2 = new QObject;
1071 void *a[] = { testObj2, 0 };
1072 QMetaObject::metacall(dynamicNodeModel, QMetaObject::WriteProperty, 0, a);
1073
1074 because the dynamicNodeModel for that index cannot be retrieved
1075 using the public API.
1076
1077 But, anyway, I think the above two test cases are sufficient to
1078 show that QObject* values should be guarded internally.
1079 */
1080 }
1081}
1082
1083//QTBUG-15190
1084void tst_qqmllistmodel::set_model_cache()
1085{
1086 QQmlEngine eng;
1087 QQmlComponent component(&eng, testFileUrl(fileName: "setmodelcachelist.qml"));
1088 QObject *model = component.create();
1089 QVERIFY2(component.errorString().isEmpty(), QTest::toString(component.errorString()));
1090 QVERIFY(model != nullptr);
1091 QVERIFY(model->property("ok").toBool());
1092
1093 delete model;
1094}
1095
1096void tst_qqmllistmodel::property_changes()
1097{
1098 QFETCH(QString, script_setup);
1099 QFETCH(QString, script_change);
1100 QFETCH(QString, roleName);
1101 QFETCH(int, listIndex);
1102 QFETCH(bool, itemsChanged);
1103 QFETCH(QString, testExpression);
1104 QFETCH(bool, dynamicRoles);
1105
1106 QQmlEngine engine;
1107 QQmlListModel model;
1108 model.setDynamicRoles(dynamicRoles);
1109 QQmlEngine::setContextForObject(&model, engine.rootContext());
1110 engine.rootContext()->setContextObject(&model);
1111
1112 QQmlExpression expr(engine.rootContext(), &model, script_setup);
1113 expr.evaluate();
1114 QVERIFY2(!expr.hasError(), QTest::toString(expr.error().toString()));
1115
1116 QString signalHandler = "on" + QString(roleName[0].toUpper()) + roleName.mid(position: 1, n: roleName.length()) + "Changed:";
1117 QString qml = "import QtQuick 2.0\n"
1118 "Connections {\n"
1119 "property bool gotSignal: false\n"
1120 "target: model.get(" + QString::number(listIndex) + ")\n"
1121 + signalHandler + " gotSignal = true\n"
1122 "}\n";
1123
1124 QQmlComponent component(&engine);
1125 component.setData(qml.toUtf8(), baseUrl: QUrl::fromLocalFile(localfile: ""));
1126 engine.rootContext()->setContextProperty("model", &model);
1127 QObject *connectionsObject = component.create();
1128 QVERIFY2(component.errorString().isEmpty(), QTest::toString(component.errorString()));
1129
1130 QSignalSpy spyItemsChanged(&model, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)));
1131
1132 expr.setExpression(script_change);
1133 expr.evaluate();
1134 QVERIFY2(!expr.hasError(), QTest::toString(expr.error().toString()));
1135
1136 // test the object returned by get() emits the correct signals
1137 QCOMPARE(connectionsObject->property("gotSignal").toBool(), itemsChanged);
1138
1139 // test itemsChanged() is emitted correctly
1140 if (itemsChanged) {
1141 QCOMPARE(spyItemsChanged.count(), 1);
1142 QCOMPARE(spyItemsChanged.at(0).at(0).value<QModelIndex>(), model.index(listIndex, 0, QModelIndex()));
1143 QCOMPARE(spyItemsChanged.at(0).at(1).value<QModelIndex>(), model.index(listIndex, 0, QModelIndex()));
1144 } else {
1145 QCOMPARE(spyItemsChanged.count(), 0);
1146 }
1147
1148 expr.setExpression(testExpression);
1149 QCOMPARE(expr.evaluate().toBool(), true);
1150
1151 delete connectionsObject;
1152}
1153
1154void tst_qqmllistmodel::property_changes_data()
1155{
1156 QTest::addColumn<QString>(name: "script_setup");
1157 QTest::addColumn<QString>(name: "script_change");
1158 QTest::addColumn<QString>(name: "roleName");
1159 QTest::addColumn<int>(name: "listIndex");
1160 QTest::addColumn<bool>(name: "itemsChanged");
1161 QTest::addColumn<QString>(name: "testExpression");
1162 QTest::addColumn<bool>(name: "dynamicRoles");
1163
1164 for (int i=0 ; i < 2 ; ++i) {
1165 bool dr = (i != 0);
1166
1167 QTest::newRow(dataTag: "set: plain") << "append({'a':123, 'b':456, 'c':789});" << "set(0,{'b':123});"
1168 << "b" << 0 << true << "get(0).b == 123" << dr;
1169 QTest::newRow(dataTag: "setProperty: plain") << "append({'a':123, 'b':456, 'c':789});" << "setProperty(0, 'b', 123);"
1170 << "b" << 0 << true << "get(0).b == 123" << dr;
1171
1172 QTest::newRow(dataTag: "set: plain, no changes") << "append({'a':123, 'b':456, 'c':789});" << "set(0,{'b':456});"
1173 << "b" << 0 << false << "get(0).b == 456" << dr;
1174 QTest::newRow(dataTag: "setProperty: plain, no changes") << "append({'a':123, 'b':456, 'c':789});" << "setProperty(0, 'b', 456);"
1175 << "b" << 0 << false << "get(0).b == 456" << dr;
1176
1177 QTest::newRow(dataTag: "set: inserted item")
1178 << "{append({'a':123, 'b':456, 'c':789}); get(0); insert(0, {'a':0, 'b':0, 'c':0});}"
1179 << "set(1, {'a':456});"
1180 << "a" << 1 << true << "get(1).a == 456" << dr;
1181 QTest::newRow(dataTag: "setProperty: inserted item")
1182 << "{append({'a':123, 'b':456, 'c':789}); get(0); insert(0, {'a':0, 'b':0, 'c':0});}"
1183 << "setProperty(1, 'a', 456);"
1184 << "a" << 1 << true << "get(1).a == 456" << dr;
1185 QTest::newRow(dataTag: "get: inserted item")
1186 << "{append({'a':123, 'b':456, 'c':789}); get(0); insert(0, {'a':0, 'b':0, 'c':0});}"
1187 << "get(1).a = 456;"
1188 << "a" << 1 << true << "get(1).a == 456" << dr;
1189 QTest::newRow(dataTag: "set: removed item")
1190 << "{append({'a':0, 'b':0, 'c':0}); append({'a':123, 'b':456, 'c':789}); get(1); remove(0);}"
1191 << "set(0, {'a':456});"
1192 << "a" << 0 << true << "get(0).a == 456" << dr;
1193 QTest::newRow(dataTag: "setProperty: removed item")
1194 << "{append({'a':0, 'b':0, 'c':0}); append({'a':123, 'b':456, 'c':789}); get(1); remove(0);}"
1195 << "setProperty(0, 'a', 456);"
1196 << "a" << 0 << true << "get(0).a == 456" << dr;
1197 QTest::newRow(dataTag: "get: removed item")
1198 << "{append({'a':0, 'b':0, 'c':0}); append({'a':123, 'b':456, 'c':789}); get(1); remove(0);}"
1199 << "get(0).a = 456;"
1200 << "a" << 0 << true << "get(0).a == 456" << dr;
1201
1202 // Following tests only call set() since setProperty() only allows plain
1203 // values, not lists, as the argument.
1204 // Note that when a list is changed, itemsChanged() is currently always
1205 // emitted regardless of whether it actually changed or not.
1206
1207 QTest::newRow(dataTag: "nested-set: list, new size") << "append({'a':123, 'b':[{'a':1},{'a':2},{'a':3}], 'c':789});" << "set(0,{'b':[{'a':1},{'a':2}]});"
1208 << "b" << 0 << true << "get(0).b.get(0).a == 1 && get(0).b.get(1).a == 2" << dr;
1209
1210 QTest::newRow(dataTag: "nested-set: list, empty -> non-empty") << "append({'a':123, 'b':[], 'c':789});" << "set(0,{'b':[{'a':1},{'a':2},{'a':3}]});"
1211 << "b" << 0 << true << "get(0).b.get(0).a == 1 && get(0).b.get(1).a == 2 && get(0).b.get(2).a == 3" << dr;
1212
1213 QTest::newRow(dataTag: "nested-set: list, non-empty -> empty") << "append({'a':123, 'b':[{'a':1},{'a':2},{'a':3}], 'c':789});" << "set(0,{'b':[]});"
1214 << "b" << 0 << true << "get(0).b.count == 0" << dr;
1215
1216 QTest::newRow(dataTag: "nested-set: list, same size, different values") << "append({'a':123, 'b':[{'a':1},{'a':2},{'a':3}], 'c':789});" << "set(0,{'b':[{'a':1},{'a':222},{'a':3}]});"
1217 << "b" << 0 << true << "get(0).b.get(0).a == 1 && get(0).b.get(1).a == 222 && get(0).b.get(2).a == 3" << dr;
1218
1219 QTest::newRow(dataTag: "nested-set: list, no changes") << "append({'a':123, 'b':[{'a':1},{'a':2},{'a':3}], 'c':789});" << "set(0,{'b':[{'a':1},{'a':2},{'a':3}]});"
1220 << "b" << 0 << true << "get(0).b.get(0).a == 1 && get(0).b.get(1).a == 2 && get(0).b.get(2).a == 3" << dr;
1221
1222 QTest::newRow(dataTag: "nested-set: list, no changes, empty") << "append({'a':123, 'b':[], 'c':789});" << "set(0,{'b':[]});"
1223 << "b" << 0 << true << "get(0).b.count == 0" << dr;
1224 }
1225}
1226
1227void tst_qqmllistmodel::clear_data()
1228{
1229 QTest::addColumn<bool>(name: "dynamicRoles");
1230
1231 QTest::newRow(dataTag: "staticRoles") << false;
1232 QTest::newRow(dataTag: "dynamicRoles") << true;
1233}
1234
1235void tst_qqmllistmodel::clear()
1236{
1237 QFETCH(bool, dynamicRoles);
1238
1239 QQmlEngine engine;
1240 QQmlListModel model;
1241 model.setDynamicRoles(dynamicRoles);
1242 QQmlEngine::setContextForObject(&model, engine.rootContext());
1243 engine.rootContext()->setContextProperty("model", &model);
1244
1245 model.clear();
1246 QCOMPARE(model.count(), 0);
1247
1248 RUNEXPR("model.append({propertyA: \"value a\", propertyB: \"value b\"})");
1249 QCOMPARE(model.count(), 1);
1250
1251 model.clear();
1252 QCOMPARE(model.count(), 0);
1253
1254 RUNEXPR("model.append({propertyA: \"value a\", propertyB: \"value b\"})");
1255 RUNEXPR("model.append({propertyA: \"value a\", propertyB: \"value b\"})");
1256 QCOMPARE(model.count(), 2);
1257
1258 model.clear();
1259 QCOMPARE(model.count(), 0);
1260
1261 // clearing does not remove the roles
1262 RUNEXPR("model.append({propertyA: \"value a\", propertyB: \"value b\", propertyC: \"value c\"})");
1263 QHash<int, QByteArray> roleNames = model.roleNames();
1264 model.clear();
1265 QCOMPARE(model.count(), 0);
1266 QCOMPARE(model.roleNames(), roleNames);
1267 QCOMPARE(roleNames[0], QByteArray("propertyA"));
1268 QCOMPARE(roleNames[1], QByteArray("propertyB"));
1269 QCOMPARE(roleNames[2], QByteArray("propertyC"));
1270}
1271
1272void tst_qqmllistmodel::signal_handlers_data()
1273{
1274 QTest::addColumn<bool>(name: "dynamicRoles");
1275
1276 QTest::newRow(dataTag: "staticRoles") << false;
1277 QTest::newRow(dataTag: "dynamicRoles") << true;
1278}
1279
1280void tst_qqmllistmodel::signal_handlers()
1281{
1282 QFETCH(bool, dynamicRoles);
1283
1284 QQmlEngine eng;
1285 QQmlComponent component(&eng, testFileUrl(fileName: "signalhandlers.qml"));
1286 QObject *model = component.create();
1287 QQmlListModel *lm = qobject_cast<QQmlListModel *>(object: model);
1288 QVERIFY(lm != nullptr);
1289 lm->setDynamicRoles(dynamicRoles);
1290 QVERIFY2(component.errorString().isEmpty(), QTest::toString(component.errorString()));
1291 QVERIFY(model != nullptr);
1292 QVERIFY(model->property("ok").toBool());
1293
1294 delete model;
1295}
1296
1297void tst_qqmllistmodel::role_mode_data()
1298{
1299 QTest::addColumn<QString>(name: "script");
1300 QTest::addColumn<int>(name: "result");
1301 QTest::addColumn<QString>(name: "warning");
1302
1303 QTest::newRow(dataTag: "default0") << "{dynamicRoles}" << 0 << "";
1304 QTest::newRow(dataTag: "default1") << "{append({'a':1});dynamicRoles}" << 0 << "";
1305
1306 QTest::newRow(dataTag: "enableDynamic0") << "{dynamicRoles=true;dynamicRoles}" << 1 << "";
1307 QTest::newRow(dataTag: "enableDynamic1") << "{append({'a':1});dynamicRoles=true;dynamicRoles}" << 0 << "<Unknown File>: QML ListModel: unable to enable dynamic roles as this model is not empty";
1308 QTest::newRow(dataTag: "enableDynamic2") << "{dynamicRoles=true;append({'a':1});dynamicRoles=false;dynamicRoles}" << 1 << "<Unknown File>: QML ListModel: unable to enable static roles as this model is not empty";
1309}
1310
1311void tst_qqmllistmodel::role_mode()
1312{
1313 QFETCH(QString, script);
1314 QFETCH(int, result);
1315 QFETCH(QString, warning);
1316
1317 QQmlEngine engine;
1318 QQmlListModel model;
1319 QQmlEngine::setContextForObject(&model,engine.rootContext());
1320 engine.rootContext()->setContextObject(&model);
1321 QQmlExpression e(engine.rootContext(), &model, script);
1322 if (!warning.isEmpty())
1323 QTest::ignoreMessage(type: QtWarningMsg, message: warning.toLatin1());
1324
1325 int actual = e.evaluate().toInt();
1326 if (e.hasError())
1327 qDebug() << e.error(); // errors not expected
1328
1329 QCOMPARE(actual,result);
1330}
1331
1332void tst_qqmllistmodel::string_to_list_crash()
1333{
1334 QQmlEngine engine;
1335 QQmlListModel model;
1336 QQmlEngine::setContextForObject(&model,engine.rootContext());
1337 engine.rootContext()->setContextObject(&model);
1338 QString script = QLatin1String("{append({'a':'data'});get(0).a = [{'x':123}]}");
1339 QTest::ignoreMessage(type: QtWarningMsg, message: "<Unknown File>: Can't assign to existing role 'a' of different type [String -> List]");
1340 QQmlExpression e(engine.rootContext(), &model, script);
1341 // Don't crash!
1342 e.evaluate();
1343}
1344
1345void tst_qqmllistmodel::empty_element_warning_data()
1346{
1347 QTest::addColumn<QString>(name: "qml");
1348 QTest::addColumn<bool>(name: "warning");
1349
1350 QTest::newRow(dataTag: "empty") << "import QtQuick 2.0\nListModel {}" << false;
1351 QTest::newRow(dataTag: "withid") << "import QtQuick 2.0\nListModel { id: model }" << false;
1352 QTest::newRow(dataTag: "emptyElement") << "import QtQuick 2.0\nListModel { ListElement {} }" << true;
1353 QTest::newRow(dataTag: "emptyElements") << "import QtQuick 2.0\nListModel { ListElement {} ListElement {} }" << true;
1354 QTest::newRow(dataTag: "role1") << "import QtQuick 2.0\nListModel { ListElement {a:1} }" << false;
1355 QTest::newRow(dataTag: "role2") << "import QtQuick 2.0\nListModel { ListElement {} ListElement {a:1} ListElement {} }" << false;
1356 QTest::newRow(dataTag: "role3") << "import QtQuick 2.0\nListModel { ListElement {} ListElement {a:1} ListElement {b:2} }" << false;
1357}
1358
1359void tst_qqmllistmodel::empty_element_warning()
1360{
1361 QFETCH(QString, qml);
1362 QFETCH(bool, warning);
1363
1364 if (warning) {
1365 QString warningString = QLatin1String("file:dummy.qml:2:1: QML ListModel: All ListElement declarations are empty, no roles can be created unless dynamicRoles is set.");
1366 QTest::ignoreMessage(type: QtWarningMsg, message: warningString.toLatin1());
1367 }
1368
1369 QQmlEngine engine;
1370 QQmlComponent component(&engine);
1371 component.setData(qml.toUtf8(), baseUrl: QUrl::fromLocalFile(localfile: QString("dummy.qml")));
1372 QVERIFY(!component.isError());
1373
1374 QObject *obj = component.create();
1375 QVERIFY(obj != nullptr);
1376
1377 delete obj;
1378}
1379
1380void tst_qqmllistmodel::datetime_data()
1381{
1382 QTest::addColumn<QString>(name: "qml");
1383 QTest::addColumn<QDateTime>(name: "expected");
1384
1385 QDateTime dt;
1386 QDateTime dt0(QDate(1900, 1, 2), QTime( 8, 14));
1387 QDateTime dt1(QDate(2000, 11, 22), QTime(10, 45));
1388
1389 QTest::newRow(dataTag: "dt0") << "{append({'date':dt0});get(0).date}" << dt0;
1390 QTest::newRow(dataTag: "dt1") << "{append({'date':dt0});get(0).date=dt1;get(0).date}" << dt1;
1391 QTest::newRow(dataTag: "dt2") << "{append({'date':dt0});set(0,{'date':dt1});get(0).date}" << dt1;
1392 QTest::newRow(dataTag: "dt3") << "{append({'date':dt0});get(0).date=undefined;get(0).date}" << dt;
1393 QTest::newRow(dataTag: "dt4") << "{append({'date':dt0});setProperty(0,'date',dt1);get(0).date}" << dt1;
1394}
1395
1396void tst_qqmllistmodel::datetime()
1397{
1398 QFETCH(QString, qml);
1399 QFETCH(QDateTime, expected);
1400
1401 QQmlEngine engine;
1402 QQmlListModel model;
1403 QQmlEngine::setContextForObject(&model,engine.rootContext());
1404 QDateTime dt0(QDate(1900, 1, 2), QTime( 8, 14));
1405 QDateTime dt1(QDate(2000, 11, 22), QTime(10, 45));
1406 engine.rootContext()->setContextProperty("dt0", dt0);
1407 engine.rootContext()->setContextProperty("dt1", dt1);
1408 engine.rootContext()->setContextObject(&model);
1409 QQmlExpression e(engine.rootContext(), &model, qml);
1410 QVariant result = e.evaluate();
1411 QDateTime dtResult = result.toDateTime();
1412 QCOMPARE(expected, dtResult);
1413}
1414
1415class RowTester : public QObject
1416{
1417 Q_OBJECT
1418public:
1419 RowTester(QAbstractItemModel *model) : QObject(model), model(model)
1420 {
1421 reset();
1422 connect(sender: model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), receiver: this, SLOT(rowsAboutToBeInserted()));
1423 connect(sender: model, SIGNAL(rowsInserted(QModelIndex,int,int)), receiver: this, SLOT(rowsInserted()));
1424 connect(sender: model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), receiver: this, SLOT(rowsAboutToBeRemoved()));
1425 connect(sender: model, SIGNAL(rowsRemoved(QModelIndex,int,int)), receiver: this, SLOT(rowsRemoved()));
1426 connect(sender: model, SIGNAL(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int)), receiver: this, SLOT(rowsAboutToBeMoved()));
1427 connect(sender: model, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)), receiver: this, SLOT(rowsMoved()));
1428 }
1429
1430 void reset()
1431 {
1432 rowsAboutToBeInsertedCalls = 0;
1433 rowsAboutToBeInsertedCount = 0;
1434 rowsInsertedCalls = 0;
1435 rowsInsertedCount = 0;
1436 rowsAboutToBeRemovedCalls = 0;
1437 rowsAboutToBeRemovedCount = 0;
1438 rowsRemovedCalls = 0;
1439 rowsRemovedCount = 0;
1440 rowsAboutToBeMovedCalls = 0;
1441 rowsAboutToBeMovedData.clear();
1442 rowsMovedCalls = 0;
1443 rowsMovedData.clear();
1444 }
1445
1446 int rowsAboutToBeInsertedCalls;
1447 int rowsAboutToBeInsertedCount;
1448 int rowsInsertedCalls;
1449 int rowsInsertedCount;
1450 int rowsAboutToBeRemovedCalls;
1451 int rowsAboutToBeRemovedCount;
1452 int rowsRemovedCalls;
1453 int rowsRemovedCount;
1454 int rowsAboutToBeMovedCalls;
1455 QVariantList rowsAboutToBeMovedData;
1456 int rowsMovedCalls;
1457 QVariantList rowsMovedData;
1458
1459private slots:
1460 void rowsAboutToBeInserted()
1461 {
1462 rowsAboutToBeInsertedCalls++;
1463 rowsAboutToBeInsertedCount = model->rowCount();
1464 }
1465
1466 void rowsInserted()
1467 {
1468 rowsInsertedCalls++;
1469 rowsInsertedCount = model->rowCount();
1470 }
1471
1472 void rowsAboutToBeRemoved()
1473 {
1474 rowsAboutToBeRemovedCalls++;
1475 rowsAboutToBeRemovedCount = model->rowCount();
1476 }
1477
1478 void rowsRemoved()
1479 {
1480 rowsRemovedCalls++;
1481 rowsRemovedCount = model->rowCount();
1482 }
1483
1484 void rowsAboutToBeMoved()
1485 {
1486 rowsAboutToBeMovedCalls++;
1487 for (int i = 0; i < model->rowCount(); ++i)
1488 rowsAboutToBeMovedData += model->data(index: model->index(row: i, column: 0), role: 0);
1489 }
1490
1491 void rowsMoved()
1492 {
1493 rowsMovedCalls++;
1494 for (int i = 0; i < model->rowCount(); ++i)
1495 rowsMovedData += model->data(index: model->index(row: i, column: 0), role: 0);
1496 }
1497
1498private:
1499 QAbstractItemModel *model;
1500};
1501
1502void tst_qqmllistmodel::about_to_be_signals()
1503{
1504 QQmlEngine engine;
1505 QQmlListModel model;
1506 QQmlEngine::setContextForObject(&model,engine.rootContext());
1507
1508 RowTester tester(&model);
1509
1510 QQmlExpression e1(engine.rootContext(), &model, "{append({'test':0})}");
1511 e1.evaluate();
1512
1513 QCOMPARE(tester.rowsAboutToBeInsertedCalls, 1);
1514 QCOMPARE(tester.rowsAboutToBeInsertedCount, 0);
1515 QCOMPARE(tester.rowsInsertedCalls, 1);
1516 QCOMPARE(tester.rowsInsertedCount, 1);
1517
1518 QQmlExpression e2(engine.rootContext(), &model, "{append({'test':1})}");
1519 e2.evaluate();
1520
1521 QCOMPARE(tester.rowsAboutToBeInsertedCalls, 2);
1522 QCOMPARE(tester.rowsAboutToBeInsertedCount, 1);
1523 QCOMPARE(tester.rowsInsertedCalls, 2);
1524 QCOMPARE(tester.rowsInsertedCount, 2);
1525
1526 QQmlExpression e3(engine.rootContext(), &model, "{move(0, 1, 1)}");
1527 e3.evaluate();
1528
1529 QCOMPARE(tester.rowsAboutToBeMovedCalls, 1);
1530 QCOMPARE(tester.rowsAboutToBeMovedData, QVariantList() << 0.0 << 1.0);
1531 QCOMPARE(tester.rowsMovedCalls, 1);
1532 QCOMPARE(tester.rowsMovedData, QVariantList() << 1.0 << 0.0);
1533
1534 QQmlExpression e4(engine.rootContext(), &model, "{remove(0, 2)}");
1535 e4.evaluate();
1536
1537 QCOMPARE(tester.rowsAboutToBeRemovedCalls, 1);
1538 QCOMPARE(tester.rowsAboutToBeRemovedCount, 2);
1539 QCOMPARE(tester.rowsRemovedCalls, 1);
1540 QCOMPARE(tester.rowsRemovedCount, 0);
1541}
1542
1543void tst_qqmllistmodel::modify_through_delegate()
1544{
1545 QQmlEngine engine;
1546 QQmlComponent component(&engine);
1547 component.setData(
1548 "import QtQuick 2.0\n"
1549 "Item {\n"
1550 " ListModel {\n"
1551 " id: testModel\n"
1552 " objectName: \"testModel\"\n"
1553 " ListElement { name: \"Joe\"; age: 22 }\n"
1554 " ListElement { name: \"Doe\"; age: 33 }\n"
1555 " }\n"
1556 " ListView {\n"
1557 " model: testModel\n"
1558 " delegate: Item {\n"
1559 " Component.onCompleted: model.age = 18;\n"
1560 " }\n"
1561 " }\n"
1562 "}\n", baseUrl: QUrl());
1563
1564 QObject *scene = component.create();
1565 QQmlListModel *model = scene->findChild<QQmlListModel*>(aName: "testModel");
1566
1567 const QHash<int, QByteArray> roleNames = model->roleNames();
1568
1569 QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("age")).toInt(), 18);
1570 QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("age")).toInt(), 18);
1571}
1572
1573void tst_qqmllistmodel::bindingsOnGetResult()
1574{
1575 QQmlEngine engine;
1576 QQmlComponent component(&engine, testFileUrl(fileName: "bindingsOnGetResult.qml"));
1577 QVERIFY2(!component.isError(), qPrintable(component.errorString()));
1578
1579 QScopedPointer<QObject> obj(component.create());
1580 QVERIFY(!obj.isNull());
1581
1582 QVERIFY(obj->property("success").toBool());
1583}
1584
1585void tst_qqmllistmodel::stringifyModelEntry()
1586{
1587 QQmlEngine engine;
1588 QQmlComponent component(&engine);
1589 component.setData(
1590 "import QtQuick 2.0\n"
1591 "Item {\n"
1592 " ListModel {\n"
1593 " id: testModel\n"
1594 " objectName: \"testModel\"\n"
1595 " ListElement { name: \"Joe\"; age: 22 }\n"
1596 " }\n"
1597 "}\n", baseUrl: QUrl());
1598 QScopedPointer<QObject> scene(component.create());
1599 QQmlListModel *model = scene->findChild<QQmlListModel*>(aName: "testModel");
1600 QQmlExpression expr(engine.rootContext(), model, "JSON.stringify(get(0));");
1601 QVariant v = expr.evaluate();
1602 QVERIFY2(!expr.hasError(), QTest::toString(expr.error().toString()));
1603 const QString expectedString = QStringLiteral("{\"age\":22,\"name\":\"Joe\"}");
1604 QCOMPARE(v.toString(), expectedString);
1605}
1606
1607void tst_qqmllistmodel::qobjectTrackerForDynamicModelObjects()
1608{
1609 QQmlEngine engine;
1610 QQmlComponent component(&engine);
1611 component.setData(
1612 "import QtQuick 2.0\n"
1613 "Item {\n"
1614 " ListModel {\n"
1615 " id: testModel\n"
1616 " objectName: \"testModel\"\n"
1617 " ListElement { name: \"Joe\"; age: 22 }\n"
1618 " }\n"
1619 "}\n", baseUrl: QUrl());
1620 QScopedPointer<QObject> scene(component.create());
1621 QQmlListModel *model = scene->findChild<QQmlListModel*>(aName: "testModel");
1622 QQmlExpression expr(engine.rootContext(), model, "get(0);");
1623 QVariant v = expr.evaluate();
1624 QVERIFY2(!expr.hasError(), QTest::toString(expr.error().toString()));
1625
1626 QObject *obj = v.value<QObject*>();
1627 QVERIFY(obj);
1628
1629 QQmlData *ddata = QQmlData::get(object: obj, /*create*/false);
1630 QVERIFY(ddata);
1631 QVERIFY(!ddata->jsWrapper.isNullOrUndefined());
1632}
1633
1634void tst_qqmllistmodel::crash_append_empty_array()
1635{
1636 QQmlEngine engine;
1637 QQmlComponent component(&engine);
1638 component.setData(
1639 "import QtQuick 2.0\n"
1640 "Item {\n"
1641 " ListModel {\n"
1642 " id: testModel\n"
1643 " objectName: \"testModel\""
1644 " }\n"
1645 "}\n", baseUrl: QUrl());
1646 QScopedPointer<QObject> scene(component.create());
1647 QQmlListModel *model = scene->findChild<QQmlListModel*>(aName: "testModel");
1648 QSignalSpy spy(model, &QQmlListModel::rowsAboutToBeInserted);
1649 QQmlExpression expr(engine.rootContext(), model, "append(new Array())");
1650 expr.evaluate();
1651 QVERIFY2(!expr.hasError(), QTest::toString(expr.error().toString()));
1652 QCOMPARE(spy.count(), 0);
1653}
1654
1655void tst_qqmllistmodel::dynamic_roles_crash_QTBUG_38907()
1656{
1657 QQmlEngine eng;
1658 QQmlComponent component(&eng, testFileUrl(fileName: "qtbug38907.qml"));
1659 QVERIFY(!component.isError());
1660 QScopedPointer<QQuickItem> item(qobject_cast<QQuickItem*>(object: component.create()));
1661 QVERIFY(item != 0);
1662
1663 QVariant retVal;
1664
1665 QMetaObject::invokeMethod(obj: item.data(),
1666 member: "exec",
1667 Qt::DirectConnection,
1668 Q_RETURN_ARG(QVariant, retVal));
1669
1670 QVERIFY(retVal.toBool());
1671}
1672
1673void tst_qqmllistmodel::nestedListModelIteration()
1674{
1675 QQmlEngine engine;
1676 QQmlComponent component(&engine);
1677 QTest::ignoreMessage(type: QtMsgType::QtDebugMsg ,message: R"({"subItems":[{"a":1,"b":0,"c":0},{"a":0,"b":2,"c":0},{"a":0,"b":0,"c":3}]})");
1678 component.setData(
1679 R"(import QtQuick 2.5
1680 Item {
1681 visible: true
1682 width: 640
1683 height: 480
1684 ListModel {
1685 id : model
1686 }
1687 Component.onCompleted: {
1688 var tempData = {
1689 subItems: [{a: 1}, {b: 2}, {c: 3}]
1690 }
1691 model.insert(0, tempData)
1692 console.log(JSON.stringify(model.get(0)))
1693 }
1694 })",
1695 baseUrl: QUrl());
1696 QScopedPointer<QObject>(component.create());
1697}
1698
1699// QTBUG-63569
1700void tst_qqmllistmodel::undefinedAppendShouldCauseError()
1701{
1702 QQmlEngine engine;
1703 QQmlComponent component(&engine);
1704 component.setData(
1705 R"(import QtQuick 2.5
1706 Item {
1707 width: 640
1708 height: 480
1709 ListModel {
1710 id : model
1711 }
1712 Component.onCompleted: {
1713 var tempData = {
1714 faulty: undefined
1715 }
1716 model.insert(0, tempData)
1717 tempData.faulty = null
1718 model.insert(0, tempData)
1719 }
1720 })",
1721 baseUrl: QUrl());
1722 QTest::ignoreMessage(type: QtMsgType::QtWarningMsg, message: "<Unknown File>: faulty is undefined. Adding an object with a undefined member does not create a role for it.");
1723 QTest::ignoreMessage(type: QtMsgType::QtWarningMsg, message: "<Unknown File>: faulty is null. Adding an object with a null member does not create a role for it.");
1724 QScopedPointer<QObject>(component.create());
1725}
1726
1727// QTBUG-89173
1728void tst_qqmllistmodel::nullPropertyCrash()
1729{
1730 QQmlEngine engine;
1731 QQmlComponent component(&engine);
1732 component.setData(
1733 R"(import QtQuick 2.15
1734 ListView {
1735 model: ListModel { id: listModel }
1736
1737 delegate: Item {}
1738
1739 Component.onCompleted: {
1740 listModel.append({"a": "value1", "b":[{"c":"value2"}]})
1741 listModel.append({"a": "value2", "b":[{"c":null}]})
1742 }
1743 })",
1744 baseUrl: QUrl());
1745 QTest::ignoreMessage(type: QtMsgType::QtWarningMsg, message: "<Unknown File>: c is null. Adding an object with a null member does not create a role for it.");
1746 QScopedPointer<QObject>(component.create());
1747}
1748
1749QTEST_MAIN(tst_qqmllistmodel)
1750
1751#include "tst_qqmllistmodel.moc"
1752

source code of qtdeclarative/tests/auto/qml/qqmllistmodel/tst_qqmllistmodel.cpp