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 | |
44 | Q_DECLARE_METATYPE(QList<int>) |
45 | Q_DECLARE_METATYPE(QList<QVariantHash>) |
46 | |
47 | #define RUNEVAL(object, string) \ |
48 | QVERIFY(QMetaObject::invokeMethod(object, "runEval", Q_ARG(QVariant, QString(string)))); |
49 | |
50 | inline 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 | |
58 | static 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 | |
72 | class tst_qqmllistmodel : public QQmlDataTest |
73 | { |
74 | Q_OBJECT |
75 | public: |
76 | tst_qqmllistmodel() |
77 | { |
78 | qRegisterMetaType<QVector<int> >(); |
79 | } |
80 | |
81 | private: |
82 | int roleFromName(const QQmlListModel *model, const QString &roleName); |
83 | |
84 | static bool compareVariantList(const QVariantList &testList, QVariant object); |
85 | |
86 | private 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 | |
136 | bool 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 | |
182 | int tst_qqmllistmodel::roleFromName(const QQmlListModel *model, const QString &roleName) |
183 | { |
184 | return model->roleNames().key(value: roleName.toUtf8(), defaultKey: -1); |
185 | } |
186 | |
187 | void 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 | |
244 | void 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 | |
276 | void 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 | |
313 | void 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 | |
345 | void 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 | |
362 | void 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 | } |
393 | void 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 | |
431 | void 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 | |
441 | void 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 | |
585 | void 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 | |
616 | void 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 | |
657 | void 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 | |
707 | void 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 | |
726 | void 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 | */ |
744 | void 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 | |
752 | void 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 | */ |
782 | void 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 | |
830 | void 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 | */ |
857 | void 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 | |
955 | void tst_qqmllistmodel::get_nested_data() |
956 | { |
957 | get_data(); |
958 | } |
959 | |
960 | //QTBUG-13754 |
961 | void 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 | |
977 | void 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 |
993 | void 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 |
1084 | void 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 | |
1096 | void 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 | |
1154 | void 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 | |
1227 | void 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 | |
1235 | void 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 | |
1272 | void 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 | |
1280 | void 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 | |
1297 | void 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 | |
1311 | void 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 | |
1332 | void 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 | |
1345 | void 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 | |
1359 | void 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 | |
1380 | void 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 | |
1396 | void 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 | |
1415 | class RowTester : public QObject |
1416 | { |
1417 | Q_OBJECT |
1418 | public: |
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 | |
1459 | private 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 | |
1498 | private: |
1499 | QAbstractItemModel *model; |
1500 | }; |
1501 | |
1502 | void 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 | |
1543 | void 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 | |
1573 | void 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 | |
1585 | void 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 | |
1607 | void 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 | |
1634 | void 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 | |
1655 | void 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 | |
1673 | void 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 |
1700 | void 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 |
1728 | void 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 | |
1749 | QTEST_MAIN(tst_qqmllistmodel) |
1750 | |
1751 | #include "tst_qqmllistmodel.moc" |
1752 | |