| 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 | |