| 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 <QtQml/private/qqmlengine_p.h> |
| 32 | #include <QtQmlModels/private/qqmllistmodel_p.h> |
| 33 | #include <QtQml/private/qqmlexpression_p.h> |
| 34 | #include <QQmlComponent> |
| 35 | |
| 36 | #include <QtCore/qtimer.h> |
| 37 | #include <QtCore/qdebug.h> |
| 38 | #include <QtCore/qtranslator.h> |
| 39 | #include <QSignalSpy> |
| 40 | |
| 41 | #include "../../shared/util.h" |
| 42 | |
| 43 | Q_DECLARE_METATYPE(QList<int>) |
| 44 | Q_DECLARE_METATYPE(QList<QVariantHash>) |
| 45 | |
| 46 | #define RUNEVAL(object, string) \ |
| 47 | QVERIFY(QMetaObject::invokeMethod(object, "runEval", Q_ARG(QVariant, QString(string)))); |
| 48 | |
| 49 | inline QVariant runexpr(QQmlEngine *engine, const QString &str) |
| 50 | { |
| 51 | QQmlExpression expr(engine->rootContext(), nullptr, str); |
| 52 | return expr.evaluate(); |
| 53 | } |
| 54 | |
| 55 | #define RUNEXPR(string) runexpr(&engine, QString(string)) |
| 56 | |
| 57 | static bool isValidErrorMessage(const QString &msg, bool dynamicRoleTest) |
| 58 | { |
| 59 | bool valid = true; |
| 60 | |
| 61 | if (msg.isEmpty()) { |
| 62 | valid = false; |
| 63 | } else if (dynamicRoleTest) { |
| 64 | if (msg.contains(s: "Can't assign to existing role" ) || msg.contains(s: "Can't create role for unsupported data type" )) |
| 65 | valid = false; |
| 66 | } |
| 67 | |
| 68 | return valid; |
| 69 | } |
| 70 | |
| 71 | class tst_qqmllistmodelworkerscript : public QQmlDataTest |
| 72 | { |
| 73 | Q_OBJECT |
| 74 | public: |
| 75 | tst_qqmllistmodelworkerscript() |
| 76 | { |
| 77 | qRegisterMetaType<QVector<int> >(); |
| 78 | } |
| 79 | |
| 80 | private: |
| 81 | int roleFromName(const QQmlListModel *model, const QString &roleName); |
| 82 | QQuickItem *createWorkerTest(QQmlEngine *eng, QQmlComponent *component, QQmlListModel *model); |
| 83 | void waitForWorker(QQuickItem *item); |
| 84 | |
| 85 | static bool compareVariantList(const QVariantList &testList, QVariant object); |
| 86 | |
| 87 | private slots: |
| 88 | void dynamic_data(); |
| 89 | void dynamic_worker_data(); |
| 90 | void dynamic_worker(); |
| 91 | void dynamic_worker_sync_data(); |
| 92 | void dynamic_worker_sync(); |
| 93 | void get_data(); |
| 94 | void get_worker(); |
| 95 | void get_worker_data(); |
| 96 | void property_changes_data(); |
| 97 | void property_changes_worker(); |
| 98 | void property_changes_worker_data(); |
| 99 | void worker_sync_data(); |
| 100 | void worker_sync(); |
| 101 | void worker_remove_element_data(); |
| 102 | void worker_remove_element(); |
| 103 | void worker_remove_list_data(); |
| 104 | void worker_remove_list(); |
| 105 | void dynamic_role_data(); |
| 106 | void dynamic_role(); |
| 107 | void correctMoves(); |
| 108 | }; |
| 109 | |
| 110 | bool tst_qqmllistmodelworkerscript::compareVariantList(const QVariantList &testList, QVariant object) |
| 111 | { |
| 112 | bool allOk = true; |
| 113 | |
| 114 | QQmlListModel *model = qobject_cast<QQmlListModel *>(object: object.value<QObject *>()); |
| 115 | if (model == nullptr) |
| 116 | return false; |
| 117 | |
| 118 | if (model->count() != testList.count()) |
| 119 | return false; |
| 120 | |
| 121 | for (int i=0 ; i < testList.count() ; ++i) { |
| 122 | const QVariant &testVariant = testList.at(i); |
| 123 | if (testVariant.type() != QVariant::Map) |
| 124 | return false; |
| 125 | const QVariantMap &map = testVariant.toMap(); |
| 126 | |
| 127 | const QHash<int, QByteArray> roleNames = model->roleNames(); |
| 128 | |
| 129 | QVariantMap::const_iterator it = map.begin(); |
| 130 | QVariantMap::const_iterator end = map.end(); |
| 131 | |
| 132 | while (it != end) { |
| 133 | const QString &testKey = it.key(); |
| 134 | const QVariant &testData = it.value(); |
| 135 | |
| 136 | int roleIndex = roleNames.key(value: testKey.toUtf8(), defaultKey: -1); |
| 137 | if (roleIndex == -1) |
| 138 | return false; |
| 139 | |
| 140 | const QVariant &modelData = model->data(index: model->index(row: i, column: 0, parent: QModelIndex()), role: roleIndex); |
| 141 | |
| 142 | if (testData.type() == QVariant::List) { |
| 143 | const QVariantList &subList = testData.toList(); |
| 144 | allOk = allOk && compareVariantList(testList: subList, object: modelData); |
| 145 | } else { |
| 146 | allOk = allOk && (testData == modelData); |
| 147 | } |
| 148 | |
| 149 | ++it; |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | return allOk; |
| 154 | } |
| 155 | |
| 156 | int tst_qqmllistmodelworkerscript::roleFromName(const QQmlListModel *model, const QString &roleName) |
| 157 | { |
| 158 | return model->roleNames().key(value: roleName.toUtf8(), defaultKey: -1); |
| 159 | } |
| 160 | |
| 161 | QQuickItem *tst_qqmllistmodelworkerscript::createWorkerTest(QQmlEngine *eng, QQmlComponent *component, QQmlListModel *model) |
| 162 | { |
| 163 | QQuickItem *item = qobject_cast<QQuickItem*>(object: component->create()); |
| 164 | QQmlEngine::setContextForObject(model, eng->rootContext()); |
| 165 | if (item) |
| 166 | item->setProperty(name: "model" , value: QVariant::fromValue(value: model)); |
| 167 | return item; |
| 168 | } |
| 169 | |
| 170 | void tst_qqmllistmodelworkerscript::waitForWorker(QQuickItem *item) |
| 171 | { |
| 172 | QQmlProperty prop(item, "done" ); |
| 173 | QVERIFY(prop.isValid()); |
| 174 | if (prop.read().toBool()) |
| 175 | return; // already finished |
| 176 | |
| 177 | QEventLoop loop; |
| 178 | QTimer timer; |
| 179 | timer.setSingleShot(true); |
| 180 | connect(sender: &timer, SIGNAL(timeout()), receiver: &loop, SLOT(quit())); |
| 181 | |
| 182 | QVERIFY(prop.connectNotifySignal(&loop, SLOT(quit()))); |
| 183 | timer.start(msec: 10000); |
| 184 | loop.exec(); |
| 185 | QVERIFY(timer.isActive()); |
| 186 | QVERIFY(prop.read().toBool()); |
| 187 | } |
| 188 | |
| 189 | void tst_qqmllistmodelworkerscript::dynamic_data() |
| 190 | { |
| 191 | QTest::addColumn<QString>(name: "script" ); |
| 192 | QTest::addColumn<int>(name: "result" ); |
| 193 | QTest::addColumn<QString>(name: "warning" ); |
| 194 | QTest::addColumn<bool>(name: "dynamicRoles" ); |
| 195 | |
| 196 | for (int i = 0; i < 2; ++i) { |
| 197 | bool dr = (i != 0); |
| 198 | |
| 199 | // Simple flat model |
| 200 | QTest::newRow(dataTag: "count" ) << "count" << 0 << "" << dr; |
| 201 | |
| 202 | QTest::newRow(dataTag: "get1" ) << "{get(0) === undefined}" << 1 << "" << dr; |
| 203 | QTest::newRow(dataTag: "get2" ) << "{get(-1) === undefined}" << 1 << "" << dr; |
| 204 | QTest::newRow(dataTag: "get3" ) << "{append({'foo':123});get(0) != undefined}" << 1 << "" << dr; |
| 205 | QTest::newRow(dataTag: "get4" ) << "{append({'foo':123});get(0).foo}" << 123 << "" << dr; |
| 206 | QTest::newRow(dataTag: "get-modify1" ) << "{append({'foo':123,'bar':456});get(0).foo = 333;get(0).foo}" << 333 << "" << dr; |
| 207 | QTest::newRow(dataTag: "get-modify2" ) << "{append({'z':1});append({'foo':123,'bar':456});get(1).bar = 999;get(1).bar}" << 999 << "" << dr; |
| 208 | QTest::newRow(dataTag: "get-set" ) << "{append({'foo':123});get(0).foo;setProperty(0, 'foo', 999);get(0).foo}" << 999 << "" << dr; |
| 209 | |
| 210 | QTest::newRow(dataTag: "append1" ) << "{append({'foo':123});count}" << 1 << "" << dr; |
| 211 | QTest::newRow(dataTag: "append2" ) << "{append({'foo':123,'bar':456});count}" << 1 << "" << dr; |
| 212 | QTest::newRow(dataTag: "append3a" ) << "{append({'foo':123});append({'foo':456});get(0).foo}" << 123 << "" << dr; |
| 213 | QTest::newRow(dataTag: "append3b" ) << "{append({'foo':123});append({'foo':456});get(1).foo}" << 456 << "" << dr; |
| 214 | QTest::newRow(dataTag: "append4a" ) << "{append(123)}" << 0 << "<Unknown File>: QML ListModel: append: value is not an object" << dr; |
| 215 | QTest::newRow(dataTag: "append4b" ) << "{append([{'foo':123},{'foo':456},{'foo':789}]);count}" << 3 << "" << dr; |
| 216 | QTest::newRow(dataTag: "append4c" ) << "{append([{'foo':123},{'foo':456},{'foo':789}]);get(1).foo}" << 456 << "" << dr; |
| 217 | |
| 218 | QTest::newRow(dataTag: "clear1" ) << "{append({'foo':456});clear();count}" << 0 << "" << dr; |
| 219 | QTest::newRow(dataTag: "clear2" ) << "{append({'foo':123});append({'foo':456});clear();count}" << 0 << "" << dr; |
| 220 | QTest::newRow(dataTag: "clear3" ) << "{append({'foo':123});clear()}" << 0 << "" << dr; |
| 221 | |
| 222 | QTest::newRow(dataTag: "remove1" ) << "{append({'foo':123});remove(0);count}" << 0 << "" << dr; |
| 223 | QTest::newRow(dataTag: "remove2a" ) << "{append({'foo':123});append({'foo':456});remove(0);count}" << 1 << "" << dr; |
| 224 | QTest::newRow(dataTag: "remove2b" ) << "{append({'foo':123});append({'foo':456});remove(0);get(0).foo}" << 456 << "" << dr; |
| 225 | QTest::newRow(dataTag: "remove2c" ) << "{append({'foo':123});append({'foo':456});remove(1);get(0).foo}" << 123 << "" << dr; |
| 226 | QTest::newRow(dataTag: "remove3" ) << "{append({'foo':123});remove(0)}" << 0 << "" << dr; |
| 227 | 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; |
| 228 | QTest::newRow(dataTag: "remove4a" ) << "{remove(0)}" << 0 << "<Unknown File>: QML ListModel: remove: indices [0 - 1] out of range [0 - 0]" << dr; |
| 229 | 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; |
| 230 | 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; |
| 231 | QTest::newRow(dataTag: "remove5a" ) << "{append({'foo':123});append({'foo':456});remove(0,2);count}" << 0 << "" << dr; |
| 232 | QTest::newRow(dataTag: "remove5b" ) << "{append({'foo':123});append({'foo':456});remove(0,1);count}" << 1 << "" << dr; |
| 233 | QTest::newRow(dataTag: "remove5c" ) << "{append({'foo':123});append({'foo':456});remove(1,1);count}" << 1 << "" << dr; |
| 234 | QTest::newRow(dataTag: "remove5d" ) << "{append({'foo':123});append({'foo':456});remove(0,1);get(0).foo}" << 456 << "" << dr; |
| 235 | QTest::newRow(dataTag: "remove5e" ) << "{append({'foo':123});append({'foo':456});remove(1,1);get(0).foo}" << 123 << "" << dr; |
| 236 | QTest::newRow(dataTag: "remove5f" ) << "{append({'foo':123});append({'foo':456});append({'foo':789});remove(0,1);remove(1,1);get(0).foo}" << 456 << "" << dr; |
| 237 | QTest::newRow(dataTag: "remove6a" ) << "{remove();count}" << 0 << "<Unknown File>: QML ListModel: remove: incorrect number of arguments" << dr; |
| 238 | QTest::newRow(dataTag: "remove6b" ) << "{remove(1,2,3);count}" << 0 << "<Unknown File>: QML ListModel: remove: incorrect number of arguments" << dr; |
| 239 | 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; |
| 240 | 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; |
| 241 | |
| 242 | QTest::newRow(dataTag: "insert1" ) << "{insert(0,{'foo':123});count}" << 1 << "" << dr; |
| 243 | QTest::newRow(dataTag: "insert2" ) << "{insert(1,{'foo':123});count}" << 0 << "<Unknown File>: QML ListModel: insert: index 1 out of range" << dr; |
| 244 | QTest::newRow(dataTag: "insert3a" ) << "{append({'foo':123});insert(1,{'foo':456});count}" << 2 << "" << dr; |
| 245 | QTest::newRow(dataTag: "insert3b" ) << "{append({'foo':123});insert(1,{'foo':456});get(0).foo}" << 123 << "" << dr; |
| 246 | QTest::newRow(dataTag: "insert3c" ) << "{append({'foo':123});insert(1,{'foo':456});get(1).foo}" << 456 << "" << dr; |
| 247 | QTest::newRow(dataTag: "insert3d" ) << "{append({'foo':123});insert(0,{'foo':456});get(0).foo}" << 456 << "" << dr; |
| 248 | QTest::newRow(dataTag: "insert3e" ) << "{append({'foo':123});insert(0,{'foo':456});get(1).foo}" << 123 << "" << dr; |
| 249 | QTest::newRow(dataTag: "insert4" ) << "{append({'foo':123});insert(-1,{'foo':456});count}" << 1 << "<Unknown File>: QML ListModel: insert: index -1 out of range" << dr; |
| 250 | QTest::newRow(dataTag: "insert5a" ) << "{insert(0,123)}" << 0 << "<Unknown File>: QML ListModel: insert: value is not an object" << dr; |
| 251 | QTest::newRow(dataTag: "insert5b" ) << "{insert(0,[{'foo':11},{'foo':22},{'foo':33}]);count}" << 3 << "" << dr; |
| 252 | QTest::newRow(dataTag: "insert5c" ) << "{insert(0,[{'foo':11},{'foo':22},{'foo':33}]);get(2).foo}" << 33 << "" << dr; |
| 253 | |
| 254 | QTest::newRow(dataTag: "set1" ) << "{append({'foo':123});set(0,{'foo':456});count}" << 1 << "" << dr; |
| 255 | QTest::newRow(dataTag: "set2" ) << "{append({'foo':123});set(0,{'foo':456});get(0).foo}" << 456 << "" << dr; |
| 256 | QTest::newRow(dataTag: "set3a" ) << "{append({'foo':123,'bar':456});set(0,{'foo':999});get(0).foo}" << 999 << "" << dr; |
| 257 | QTest::newRow(dataTag: "set3b" ) << "{append({'foo':123,'bar':456});set(0,{'foo':999});get(0).bar}" << 456 << "" << dr; |
| 258 | QTest::newRow(dataTag: "set4a" ) << "{set(0,{'foo':456});count}" << 1 << "" << dr; |
| 259 | QTest::newRow(dataTag: "set4c" ) << "{set(-1,{'foo':456})}" << 0 << "<Unknown File>: QML ListModel: set: index -1 out of range" << dr; |
| 260 | 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; |
| 261 | QTest::newRow(dataTag: "set5b" ) << "{append({'foo':123,'bar':456});set(0,[1,2,3]);count}" << 1 << "" << dr; |
| 262 | QTest::newRow(dataTag: "set6" ) << "{append({'foo':123});set(1,{'foo':456});count}" << 2 << "" << dr; |
| 263 | |
| 264 | QTest::newRow(dataTag: "setprop1" ) << "{append({'foo':123});setProperty(0,'foo',456);count}" << 1 << "" << dr; |
| 265 | QTest::newRow(dataTag: "setprop2" ) << "{append({'foo':123});setProperty(0,'foo',456);get(0).foo}" << 456 << "" << dr; |
| 266 | QTest::newRow(dataTag: "setprop3a" ) << "{append({'foo':123,'bar':456});setProperty(0,'foo',999);get(0).foo}" << 999 << "" << dr; |
| 267 | QTest::newRow(dataTag: "setprop3b" ) << "{append({'foo':123,'bar':456});setProperty(0,'foo',999);get(0).bar}" << 456 << "" << dr; |
| 268 | QTest::newRow(dataTag: "setprop4a" ) << "{setProperty(0,'foo',456)}" << 0 << "<Unknown File>: QML ListModel: set: index 0 out of range" << dr; |
| 269 | QTest::newRow(dataTag: "setprop4b" ) << "{setProperty(-1,'foo',456)}" << 0 << "<Unknown File>: QML ListModel: set: index -1 out of range" << dr; |
| 270 | 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; |
| 271 | QTest::newRow(dataTag: "setprop5" ) << "{append({'foo':123,'bar':456});append({'foo':111});setProperty(1,'bar',222);get(1).bar}" << 222 << "" << dr; |
| 272 | |
| 273 | QTest::newRow(dataTag: "move1a" ) << "{append({'foo':123});append({'foo':456});move(0,1,1);count}" << 2 << "" << dr; |
| 274 | QTest::newRow(dataTag: "move1b" ) << "{append({'foo':123});append({'foo':456});move(0,1,1);get(0).foo}" << 456 << "" << dr; |
| 275 | QTest::newRow(dataTag: "move1c" ) << "{append({'foo':123});append({'foo':456});move(0,1,1);get(1).foo}" << 123 << "" << dr; |
| 276 | QTest::newRow(dataTag: "move1d" ) << "{append({'foo':123});append({'foo':456});move(1,0,1);get(0).foo}" << 456 << "" << dr; |
| 277 | QTest::newRow(dataTag: "move1e" ) << "{append({'foo':123});append({'foo':456});move(1,0,1);get(1).foo}" << 123 << "" << dr; |
| 278 | QTest::newRow(dataTag: "move2a" ) << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);count}" << 3 << "" << dr; |
| 279 | QTest::newRow(dataTag: "move2b" ) << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);get(0).foo}" << 789 << "" << dr; |
| 280 | QTest::newRow(dataTag: "move2c" ) << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);get(1).foo}" << 123 << "" << dr; |
| 281 | QTest::newRow(dataTag: "move2d" ) << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);get(2).foo}" << 456 << "" << dr; |
| 282 | 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; |
| 283 | 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; |
| 284 | 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; |
| 285 | 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; |
| 286 | |
| 287 | 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; |
| 288 | |
| 289 | 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; |
| 290 | |
| 291 | QTest::newRow(dataTag: "null" ) << "{append({'a':null});}" << 0 << "" << dr; |
| 292 | QTest::newRow(dataTag: "setNull" ) << "{append({'a':1});set(0, {'a':null});}" << 0 << "" << dr; |
| 293 | QTest::newRow(dataTag: "setString" ) << "{append({'a':'hello'});set(0, {'a':'world'});get(0).a == 'world'}" << 1 << "" << dr; |
| 294 | QTest::newRow(dataTag: "setInt" ) << "{append({'a':5});set(0, {'a':10});get(0).a}" << 10 << "" << dr; |
| 295 | QTest::newRow(dataTag: "setNumber" ) << "{append({'a':6});set(0, {'a':5.5});get(0).a < 5.6}" << 1 << "" << dr; |
| 296 | 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; |
| 297 | QTest::newRow(dataTag: "invalidInsert0" ) << "{insert(0);}" << 0 << "<Unknown File>: QML ListModel: insert: value is not an object" << dr; |
| 298 | QTest::newRow(dataTag: "invalidAppend0" ) << "{append();}" << 0 << "<Unknown File>: QML ListModel: append: value is not an object" << dr; |
| 299 | QTest::newRow(dataTag: "invalidInsert1" ) << "{insert(0, 34);}" << 0 << "<Unknown File>: QML ListModel: insert: value is not an object" << dr; |
| 300 | QTest::newRow(dataTag: "invalidAppend1" ) << "{append(37);}" << 0 << "<Unknown File>: QML ListModel: append: value is not an object" << dr; |
| 301 | |
| 302 | // QObjects |
| 303 | QTest::newRow(dataTag: "qobject0" ) << "{append({'a':dummyItem0});}" << 0 << "" << dr; |
| 304 | QTest::newRow(dataTag: "qobject1" ) << "{append({'a':dummyItem0});set(0,{'a':dummyItem1});get(0).a == dummyItem1;}" << 1 << "" << dr; |
| 305 | QTest::newRow(dataTag: "qobject2" ) << "{append({'a':dummyItem0});get(0).a == dummyItem0;}" << 1 << "" << dr; |
| 306 | QTest::newRow(dataTag: "qobject3" ) << "{append({'a':dummyItem0});append({'b':1});}" << 0 << "" << dr; |
| 307 | |
| 308 | // JS objects |
| 309 | QTest::newRow(dataTag: "js1" ) << "{append({'foo':{'prop':1}});count}" << 1 << "" << dr; |
| 310 | QTest::newRow(dataTag: "js2" ) << "{append({'foo':{'prop':27}});get(0).foo.prop}" << 27 << "" << dr; |
| 311 | QTest::newRow(dataTag: "js3" ) << "{append({'foo':{'prop':27}});append({'bar':1});count}" << 2 << "" << dr; |
| 312 | QTest::newRow(dataTag: "js4" ) << "{append({'foo':{'prop':27}});append({'bar':1});set(0, {'foo':{'prop':28}});get(0).foo.prop}" << 28 << "" << dr; |
| 313 | QTest::newRow(dataTag: "js5" ) << "{append({'foo':{'prop':27}});append({'bar':1});set(1, {'foo':{'prop':33}});get(1).foo.prop}" << 33 << "" << dr; |
| 314 | QTest::newRow(dataTag: "js6" ) << "{append({'foo':{'prop':27}});clear();count}" << 0 << "" << dr; |
| 315 | QTest::newRow(dataTag: "js7" ) << "{append({'foo':{'prop':27}});set(0, {'foo':null});count}" << 1 << "" << dr; |
| 316 | QTest::newRow(dataTag: "js8" ) << "{append({'foo':{'prop':27}});set(0, {'foo':{'prop2':31}});get(0).foo.prop2}" << 31 << "" << dr; |
| 317 | |
| 318 | // Nested models |
| 319 | QTest::newRow(dataTag: "nested-append1" ) << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]});count}" << 1 << "" << dr; |
| 320 | QTest::newRow(dataTag: "nested-append2" ) << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]});get(0).bars.get(1).a}" << 2 << "" << dr; |
| 321 | 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; |
| 322 | |
| 323 | 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; |
| 324 | QTest::newRow(dataTag: "nested-set" ) << "{append({'foo':[{'x':1}]});set(0,{'foo':[{'x':123}]});get(0).foo.get(0).x}" << 123 << "" << dr; |
| 325 | |
| 326 | QTest::newRow(dataTag: "nested-count" ) << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]}); get(0).bars.count}" << 3 << "" << dr; |
| 327 | 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; |
| 328 | } |
| 329 | } |
| 330 | |
| 331 | void tst_qqmllistmodelworkerscript::dynamic_worker_data() |
| 332 | { |
| 333 | dynamic_data(); |
| 334 | } |
| 335 | |
| 336 | void tst_qqmllistmodelworkerscript::dynamic_worker() |
| 337 | { |
| 338 | QFETCH(QString, script); |
| 339 | QFETCH(int, result); |
| 340 | QFETCH(QString, warning); |
| 341 | QFETCH(bool, dynamicRoles); |
| 342 | |
| 343 | if (QByteArray(QTest::currentDataTag()).startsWith(c: "qobject" )) |
| 344 | return; |
| 345 | |
| 346 | // This is same as dynamic() except it applies the test to a ListModel called |
| 347 | // from a WorkerScript. |
| 348 | |
| 349 | QQmlListModel model; |
| 350 | model.setDynamicRoles(dynamicRoles); |
| 351 | QQmlEngine eng; |
| 352 | QQmlComponent component(&eng, testFileUrl(fileName: "model.qml" )); |
| 353 | QQuickItem *item = createWorkerTest(eng: &eng, component: &component, model: &model); |
| 354 | QVERIFY(item != nullptr); |
| 355 | |
| 356 | QSignalSpy spyCount(&model, SIGNAL(countChanged())); |
| 357 | |
| 358 | if (script[0] == QLatin1Char('{') && script[script.length()-1] == QLatin1Char('}')) |
| 359 | script = script.mid(position: 1, n: script.length() - 2); |
| 360 | QVariantList operations; |
| 361 | foreach (const QString &s, script.split(';')) { |
| 362 | if (!s.isEmpty()) |
| 363 | operations << s; |
| 364 | } |
| 365 | |
| 366 | if (isValidErrorMessage(msg: warning, dynamicRoleTest: dynamicRoles)) |
| 367 | QTest::ignoreMessage(type: QtWarningMsg, message: warning.toLatin1()); |
| 368 | |
| 369 | QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker" , |
| 370 | Q_ARG(QVariant, operations))); |
| 371 | waitForWorker(item); |
| 372 | QCOMPARE(QQmlProperty(item, "result" ).read().toInt(), result); |
| 373 | |
| 374 | if (model.count() > 0) |
| 375 | QVERIFY(spyCount.count() > 0); |
| 376 | |
| 377 | delete item; |
| 378 | qApp->processEvents(); |
| 379 | } |
| 380 | |
| 381 | void tst_qqmllistmodelworkerscript::dynamic_worker_sync_data() |
| 382 | { |
| 383 | dynamic_data(); |
| 384 | } |
| 385 | |
| 386 | void tst_qqmllistmodelworkerscript::dynamic_worker_sync() |
| 387 | { |
| 388 | QFETCH(QString, script); |
| 389 | QFETCH(int, result); |
| 390 | QFETCH(QString, warning); |
| 391 | QFETCH(bool, dynamicRoles); |
| 392 | |
| 393 | if (QByteArray(QTest::currentDataTag()).startsWith(c: "qobject" )) |
| 394 | return; |
| 395 | |
| 396 | // This is the same as dynamic_worker() except that it executes a set of list operations |
| 397 | // from the worker script, calls sync(), and tests the changes are reflected in the |
| 398 | // list in the main thread |
| 399 | |
| 400 | QQmlListModel model; |
| 401 | model.setDynamicRoles(dynamicRoles); |
| 402 | QQmlEngine eng; |
| 403 | QQmlComponent component(&eng, testFileUrl(fileName: "model.qml" )); |
| 404 | QQuickItem *item = createWorkerTest(eng: &eng, component: &component, model: &model); |
| 405 | QVERIFY(item != nullptr); |
| 406 | |
| 407 | if (script[0] == QLatin1Char('{') && script[script.length()-1] == QLatin1Char('}')) |
| 408 | script = script.mid(position: 1, n: script.length() - 2); |
| 409 | QVariantList operations; |
| 410 | foreach (const QString &s, script.split(';')) { |
| 411 | if (!s.isEmpty()) |
| 412 | operations << s; |
| 413 | } |
| 414 | |
| 415 | if (isValidErrorMessage(msg: warning, dynamicRoleTest: dynamicRoles)) |
| 416 | QTest::ignoreMessage(type: QtWarningMsg, message: warning.toLatin1()); |
| 417 | |
| 418 | // execute a set of commands on the worker list model, then check the |
| 419 | // changes are reflected in the list model in the main thread |
| 420 | QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker" , |
| 421 | Q_ARG(QVariant, operations.mid(0, operations.length()-1)))); |
| 422 | waitForWorker(item); |
| 423 | |
| 424 | QQmlExpression e(eng.rootContext(), &model, operations.last().toString()); |
| 425 | QCOMPARE(e.evaluate().toInt(), result); |
| 426 | |
| 427 | delete item; |
| 428 | qApp->processEvents(); |
| 429 | } |
| 430 | |
| 431 | void tst_qqmllistmodelworkerscript::get_data() |
| 432 | { |
| 433 | QTest::addColumn<QString>(name: "expression" ); |
| 434 | QTest::addColumn<int>(name: "index" ); |
| 435 | QTest::addColumn<QString>(name: "roleName" ); |
| 436 | QTest::addColumn<QVariant>(name: "roleValue" ); |
| 437 | QTest::addColumn<bool>(name: "dynamicRoles" ); |
| 438 | |
| 439 | for (int i =0; i < 2; ++i) { |
| 440 | bool dr = (i != 0); |
| 441 | |
| 442 | QTest::newRow(dataTag: "simple value" ) << "get(0).roleA = 500" << 0 << "roleA" << QVariant(500) << dr; |
| 443 | QTest::newRow(dataTag: "simple value 2" ) << "get(1).roleB = 500" << 1 << "roleB" << QVariant(500) << dr; |
| 444 | |
| 445 | QVariantMap map; |
| 446 | QVariantList list; |
| 447 | map.clear(); map["a" ] = 50; map["b" ] = 500; |
| 448 | list << map; |
| 449 | map.clear(); map["c" ] = 1000; |
| 450 | list << map; |
| 451 | QTest::newRow(dataTag: "list of objects" ) << "get(2).roleD = [{'a': 50, 'b': 500}, {'c': 1000}]" << 2 << "roleD" << QVariant::fromValue(value: list) << dr; |
| 452 | } |
| 453 | } |
| 454 | |
| 455 | void tst_qqmllistmodelworkerscript::get_worker() |
| 456 | { |
| 457 | QFETCH(QString, expression); |
| 458 | QFETCH(int, index); |
| 459 | QFETCH(QString, roleName); |
| 460 | QFETCH(QVariant, roleValue); |
| 461 | QFETCH(bool, dynamicRoles); |
| 462 | |
| 463 | QQmlListModel model; |
| 464 | model.setDynamicRoles(dynamicRoles); |
| 465 | QQmlEngine eng; |
| 466 | QQmlComponent component(&eng, testFileUrl(fileName: "model.qml" )); |
| 467 | QQuickItem *item = createWorkerTest(eng: &eng, component: &component, model: &model); |
| 468 | QVERIFY(item != nullptr); |
| 469 | |
| 470 | // Add some values like get() test |
| 471 | RUNEVAL(item, "model.append({roleA: 100})" ); |
| 472 | RUNEVAL(item, "model.append({roleA: 200, roleB: 400})" ); |
| 473 | RUNEVAL(item, "model.append({roleA: 200, roleB: 400})" ); |
| 474 | RUNEVAL(item, "model.append({roleC: {} })" ); |
| 475 | RUNEVAL(item, "model.append({roleD: [ { a:1, b:2 }, { c: 3 } ] })" ); |
| 476 | |
| 477 | int role = roleFromName(model: &model, roleName); |
| 478 | QVERIFY(role >= 0); |
| 479 | |
| 480 | QSignalSpy spy(&model, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>))); |
| 481 | |
| 482 | // in the worker thread, change the model data and call sync() |
| 483 | QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker" , |
| 484 | Q_ARG(QVariant, QStringList(expression)))); |
| 485 | waitForWorker(item); |
| 486 | |
| 487 | // see if we receive the model changes in the main thread's model |
| 488 | if (roleValue.type() == QVariant::List) { |
| 489 | const QVariantList &list = roleValue.toList(); |
| 490 | QVERIFY(compareVariantList(list, model.data(index, role))); |
| 491 | } else { |
| 492 | QCOMPARE(model.data(index, role), roleValue); |
| 493 | } |
| 494 | |
| 495 | QCOMPARE(spy.count(), 1); |
| 496 | |
| 497 | QList<QVariant> spyResult = spy.takeFirst(); |
| 498 | QCOMPARE(spyResult.at(0).value<QModelIndex>(), model.index(index, 0, QModelIndex())); |
| 499 | QCOMPARE(spyResult.at(1).value<QModelIndex>(), model.index(index, 0, QModelIndex())); // only 1 item is modified at a time |
| 500 | QVERIFY(spyResult.at(2).value<QVector<int> >().contains(role)); |
| 501 | |
| 502 | delete item; |
| 503 | } |
| 504 | |
| 505 | void tst_qqmllistmodelworkerscript::get_worker_data() |
| 506 | { |
| 507 | get_data(); |
| 508 | } |
| 509 | |
| 510 | void tst_qqmllistmodelworkerscript::property_changes_data() |
| 511 | { |
| 512 | QTest::addColumn<QString>(name: "script_setup" ); |
| 513 | QTest::addColumn<QString>(name: "script_change" ); |
| 514 | QTest::addColumn<QString>(name: "roleName" ); |
| 515 | QTest::addColumn<int>(name: "listIndex" ); |
| 516 | QTest::addColumn<bool>(name: "itemsChanged" ); |
| 517 | QTest::addColumn<QString>(name: "testExpression" ); |
| 518 | QTest::addColumn<bool>(name: "dynamicRoles" ); |
| 519 | |
| 520 | for (int i=1 ; i < 2 ; ++i) { |
| 521 | bool dr = (i != 0); |
| 522 | |
| 523 | QTest::newRow(dataTag: "set: plain" ) << "append({'a':123, 'b':456, 'c':789});" << "set(0,{'b':123});" |
| 524 | << "b" << 0 << true << "get(0).b == 123" << dr; |
| 525 | QTest::newRow(dataTag: "setProperty: plain" ) << "append({'a':123, 'b':456, 'c':789});" << "setProperty(0, 'b', 123);" |
| 526 | << "b" << 0 << true << "get(0).b == 123" << dr; |
| 527 | |
| 528 | QTest::newRow(dataTag: "set: plain, no changes" ) << "append({'a':123, 'b':456, 'c':789});" << "set(0,{'b':456});" |
| 529 | << "b" << 0 << false << "get(0).b == 456" << dr; |
| 530 | QTest::newRow(dataTag: "setProperty: plain, no changes" ) << "append({'a':123, 'b':456, 'c':789});" << "setProperty(0, 'b', 456);" |
| 531 | << "b" << 0 << false << "get(0).b == 456" << dr; |
| 532 | |
| 533 | QTest::newRow(dataTag: "set: inserted item" ) |
| 534 | << "{append({'a':123, 'b':456, 'c':789}); get(0); insert(0, {'a':0, 'b':0, 'c':0});}" |
| 535 | << "set(1, {'a':456});" |
| 536 | << "a" << 1 << true << "get(1).a == 456" << dr; |
| 537 | QTest::newRow(dataTag: "setProperty: inserted item" ) |
| 538 | << "{append({'a':123, 'b':456, 'c':789}); get(0); insert(0, {'a':0, 'b':0, 'c':0});}" |
| 539 | << "setProperty(1, 'a', 456);" |
| 540 | << "a" << 1 << true << "get(1).a == 456" << dr; |
| 541 | QTest::newRow(dataTag: "get: inserted item" ) |
| 542 | << "{append({'a':123, 'b':456, 'c':789}); get(0); insert(0, {'a':0, 'b':0, 'c':0});}" |
| 543 | << "get(1).a = 456;" |
| 544 | << "a" << 1 << true << "get(1).a == 456" << dr; |
| 545 | QTest::newRow(dataTag: "set: removed item" ) |
| 546 | << "{append({'a':0, 'b':0, 'c':0}); append({'a':123, 'b':456, 'c':789}); get(1); remove(0);}" |
| 547 | << "set(0, {'a':456});" |
| 548 | << "a" << 0 << true << "get(0).a == 456" << dr; |
| 549 | QTest::newRow(dataTag: "setProperty: removed item" ) |
| 550 | << "{append({'a':0, 'b':0, 'c':0}); append({'a':123, 'b':456, 'c':789}); get(1); remove(0);}" |
| 551 | << "setProperty(0, 'a', 456);" |
| 552 | << "a" << 0 << true << "get(0).a == 456" << dr; |
| 553 | QTest::newRow(dataTag: "get: removed item" ) |
| 554 | << "{append({'a':0, 'b':0, 'c':0}); append({'a':123, 'b':456, 'c':789}); get(1); remove(0);}" |
| 555 | << "get(0).a = 456;" |
| 556 | << "a" << 0 << true << "get(0).a == 456" << dr; |
| 557 | |
| 558 | // Following tests only call set() since setProperty() only allows plain |
| 559 | // values, not lists, as the argument. |
| 560 | // Note that when a list is changed, itemsChanged() is currently always |
| 561 | // emitted regardless of whether it actually changed or not. |
| 562 | |
| 563 | 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}]});" |
| 564 | << "b" << 0 << true << "get(0).b.get(0).a == 1 && get(0).b.get(1).a == 2" << dr; |
| 565 | |
| 566 | QTest::newRow(dataTag: "nested-set: list, empty -> non-empty" ) << "append({'a':123, 'b':[], 'c':789});" << "set(0,{'b':[{'a':1},{'a':2},{'a':3}]});" |
| 567 | << "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; |
| 568 | |
| 569 | QTest::newRow(dataTag: "nested-set: list, non-empty -> empty" ) << "append({'a':123, 'b':[{'a':1},{'a':2},{'a':3}], 'c':789});" << "set(0,{'b':[]});" |
| 570 | << "b" << 0 << true << "get(0).b.count == 0" << dr; |
| 571 | |
| 572 | 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}]});" |
| 573 | << "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; |
| 574 | |
| 575 | 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}]});" |
| 576 | << "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; |
| 577 | |
| 578 | QTest::newRow(dataTag: "nested-set: list, no changes, empty" ) << "append({'a':123, 'b':[], 'c':789});" << "set(0,{'b':[]});" |
| 579 | << "b" << 0 << false << "get(0).b.count == 0" << dr; |
| 580 | } |
| 581 | } |
| 582 | |
| 583 | void tst_qqmllistmodelworkerscript::property_changes_worker() |
| 584 | { |
| 585 | QFETCH(QString, script_setup); |
| 586 | QFETCH(QString, script_change); |
| 587 | QFETCH(QString, roleName); |
| 588 | QFETCH(int, listIndex); |
| 589 | QFETCH(bool, itemsChanged); |
| 590 | QFETCH(bool, dynamicRoles); |
| 591 | |
| 592 | QQmlListModel model; |
| 593 | model.setDynamicRoles(dynamicRoles); |
| 594 | QQmlEngine engine; |
| 595 | QQmlComponent component(&engine, testFileUrl(fileName: "model.qml" )); |
| 596 | QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); |
| 597 | QQuickItem *item = createWorkerTest(eng: &engine, component: &component, model: &model); |
| 598 | QVERIFY(item != nullptr); |
| 599 | |
| 600 | QQmlExpression expr(engine.rootContext(), &model, script_setup); |
| 601 | expr.evaluate(); |
| 602 | QVERIFY2(!expr.hasError(), QTest::toString(expr.error().toString())); |
| 603 | |
| 604 | QSignalSpy spyItemsChanged(&model, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>))); |
| 605 | |
| 606 | QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker" , |
| 607 | Q_ARG(QVariant, QStringList(script_change)))); |
| 608 | waitForWorker(item); |
| 609 | |
| 610 | // test itemsChanged() is emitted correctly |
| 611 | if (itemsChanged) { |
| 612 | QCOMPARE(spyItemsChanged.count(), 1); |
| 613 | QCOMPARE(spyItemsChanged.at(0).at(0).value<QModelIndex>(), model.index(listIndex, 0, QModelIndex())); |
| 614 | QCOMPARE(spyItemsChanged.at(0).at(1).value<QModelIndex>(), model.index(listIndex, 0, QModelIndex())); |
| 615 | } else { |
| 616 | QCOMPARE(spyItemsChanged.count(), 0); |
| 617 | } |
| 618 | |
| 619 | delete item; |
| 620 | qApp->processEvents(); |
| 621 | } |
| 622 | |
| 623 | void tst_qqmllistmodelworkerscript::property_changes_worker_data() |
| 624 | { |
| 625 | property_changes_data(); |
| 626 | } |
| 627 | |
| 628 | void tst_qqmllistmodelworkerscript::worker_sync_data() |
| 629 | { |
| 630 | QTest::addColumn<bool>(name: "dynamicRoles" ); |
| 631 | |
| 632 | QTest::newRow(dataTag: "staticRoles" ) << false; |
| 633 | QTest::newRow(dataTag: "dynamicRoles" ) << true; |
| 634 | } |
| 635 | |
| 636 | void tst_qqmllistmodelworkerscript::worker_sync() |
| 637 | { |
| 638 | QFETCH(bool, dynamicRoles); |
| 639 | |
| 640 | QQmlListModel model; |
| 641 | model.setDynamicRoles(dynamicRoles); |
| 642 | QQmlEngine eng; |
| 643 | QQmlComponent component(&eng, testFileUrl(fileName: "workersync.qml" )); |
| 644 | QQuickItem *item = createWorkerTest(eng: &eng, component: &component, model: &model); |
| 645 | QVERIFY(item != nullptr); |
| 646 | |
| 647 | QCOMPARE(model.count(), 0); |
| 648 | |
| 649 | QVERIFY(QMetaObject::invokeMethod(item, "addItem0" )); |
| 650 | |
| 651 | QCOMPARE(model.count(), 2); |
| 652 | QVariant childData = model.data(index: 0, role: 0); |
| 653 | QQmlListModel *childModel = qobject_cast<QQmlListModel *>(object: childData.value<QObject *>()); |
| 654 | QVERIFY(childModel); |
| 655 | QCOMPARE(childModel->count(), 1); |
| 656 | |
| 657 | QSignalSpy spyModelInserted(&model, SIGNAL(rowsInserted(QModelIndex,int,int))); |
| 658 | QSignalSpy spyChildInserted(childModel, SIGNAL(rowsInserted(QModelIndex,int,int))); |
| 659 | |
| 660 | QVERIFY(QMetaObject::invokeMethod(item, "addItemViaWorker" )); |
| 661 | waitForWorker(item); |
| 662 | |
| 663 | QCOMPARE(model.count(), 2); |
| 664 | QCOMPARE(childModel->count(), 1); |
| 665 | QCOMPARE(spyModelInserted.count(), 0); |
| 666 | QCOMPARE(spyChildInserted.count(), 0); |
| 667 | |
| 668 | QVERIFY(QMetaObject::invokeMethod(item, "doSync" )); |
| 669 | waitForWorker(item); |
| 670 | |
| 671 | QCOMPARE(model.count(), 2); |
| 672 | QCOMPARE(childModel->count(), 2); |
| 673 | QCOMPARE(spyModelInserted.count(), 0); |
| 674 | QCOMPARE(spyChildInserted.count(), 1); |
| 675 | |
| 676 | QVERIFY(QMetaObject::invokeMethod(item, "addItemViaWorker" )); |
| 677 | waitForWorker(item); |
| 678 | |
| 679 | QCOMPARE(model.count(), 2); |
| 680 | QCOMPARE(childModel->count(), 2); |
| 681 | QCOMPARE(spyModelInserted.count(), 0); |
| 682 | QCOMPARE(spyChildInserted.count(), 1); |
| 683 | |
| 684 | QVERIFY(QMetaObject::invokeMethod(item, "doSync" )); |
| 685 | waitForWorker(item); |
| 686 | |
| 687 | QCOMPARE(model.count(), 2); |
| 688 | QCOMPARE(childModel->count(), 3); |
| 689 | QCOMPARE(spyModelInserted.count(), 0); |
| 690 | QCOMPARE(spyChildInserted.count(), 2); |
| 691 | |
| 692 | delete item; |
| 693 | qApp->processEvents(); |
| 694 | } |
| 695 | |
| 696 | void tst_qqmllistmodelworkerscript::worker_remove_element_data() |
| 697 | { |
| 698 | worker_sync_data(); |
| 699 | } |
| 700 | |
| 701 | void tst_qqmllistmodelworkerscript::worker_remove_element() |
| 702 | { |
| 703 | QFETCH(bool, dynamicRoles); |
| 704 | |
| 705 | QQmlListModel model; |
| 706 | model.setDynamicRoles(dynamicRoles); |
| 707 | QQmlEngine eng; |
| 708 | QQmlComponent component(&eng, testFileUrl(fileName: "workerremoveelement.qml" )); |
| 709 | QQuickItem *item = createWorkerTest(eng: &eng, component: &component, model: &model); |
| 710 | QVERIFY(item != nullptr); |
| 711 | |
| 712 | QSignalSpy spyModelRemoved(&model, SIGNAL(rowsRemoved(QModelIndex,int,int))); |
| 713 | |
| 714 | QCOMPARE(model.count(), 0); |
| 715 | QCOMPARE(spyModelRemoved.count(), 0); |
| 716 | |
| 717 | QVERIFY(QMetaObject::invokeMethod(item, "addItem" )); |
| 718 | |
| 719 | QCOMPARE(model.count(), 1); |
| 720 | |
| 721 | QVERIFY(QMetaObject::invokeMethod(item, "removeItemViaWorker" )); |
| 722 | waitForWorker(item); |
| 723 | |
| 724 | QCOMPARE(model.count(), 1); |
| 725 | QCOMPARE(spyModelRemoved.count(), 0); |
| 726 | |
| 727 | QVERIFY(QMetaObject::invokeMethod(item, "doSync" )); |
| 728 | waitForWorker(item); |
| 729 | |
| 730 | QCOMPARE(model.count(), 0); |
| 731 | QCOMPARE(spyModelRemoved.count(), 1); |
| 732 | |
| 733 | delete item; |
| 734 | qApp->processEvents(); |
| 735 | |
| 736 | { |
| 737 | //don't crash if model was deleted earlier |
| 738 | QQmlListModel* model = new QQmlListModel; |
| 739 | model->setDynamicRoles(dynamicRoles); |
| 740 | QQmlEngine eng; |
| 741 | QQmlComponent component(&eng, testFileUrl(fileName: "workerremoveelement.qml" )); |
| 742 | QQuickItem *item = createWorkerTest(eng: &eng, component: &component, model); |
| 743 | QVERIFY(item != nullptr); |
| 744 | |
| 745 | QVERIFY(QMetaObject::invokeMethod(item, "addItem" )); |
| 746 | |
| 747 | QCOMPARE(model->count(), 1); |
| 748 | |
| 749 | QVERIFY(QMetaObject::invokeMethod(item, "removeItemViaWorker" )); |
| 750 | QVERIFY(QMetaObject::invokeMethod(item, "doSync" )); |
| 751 | delete model; |
| 752 | qApp->processEvents(); //must not crash here |
| 753 | waitForWorker(item); |
| 754 | |
| 755 | delete item; |
| 756 | } |
| 757 | } |
| 758 | |
| 759 | void tst_qqmllistmodelworkerscript::worker_remove_list_data() |
| 760 | { |
| 761 | worker_sync_data(); |
| 762 | } |
| 763 | |
| 764 | void tst_qqmllistmodelworkerscript::worker_remove_list() |
| 765 | { |
| 766 | QFETCH(bool, dynamicRoles); |
| 767 | |
| 768 | QQmlListModel model; |
| 769 | model.setDynamicRoles(dynamicRoles); |
| 770 | QQmlEngine eng; |
| 771 | QQmlComponent component(&eng, testFileUrl(fileName: "workerremovelist.qml" )); |
| 772 | QQuickItem *item = createWorkerTest(eng: &eng, component: &component, model: &model); |
| 773 | QVERIFY(item != nullptr); |
| 774 | |
| 775 | QSignalSpy spyModelRemoved(&model, SIGNAL(rowsRemoved(QModelIndex,int,int))); |
| 776 | |
| 777 | QCOMPARE(model.count(), 0); |
| 778 | QCOMPARE(spyModelRemoved.count(), 0); |
| 779 | |
| 780 | QVERIFY(QMetaObject::invokeMethod(item, "addList" )); |
| 781 | |
| 782 | QCOMPARE(model.count(), 1); |
| 783 | |
| 784 | QVERIFY(QMetaObject::invokeMethod(item, "removeListViaWorker" )); |
| 785 | waitForWorker(item); |
| 786 | |
| 787 | QCOMPARE(model.count(), 1); |
| 788 | QCOMPARE(spyModelRemoved.count(), 0); |
| 789 | |
| 790 | QVERIFY(QMetaObject::invokeMethod(item, "doSync" )); |
| 791 | waitForWorker(item); |
| 792 | |
| 793 | QCOMPARE(model.count(), 0); |
| 794 | QCOMPARE(spyModelRemoved.count(), 1); |
| 795 | |
| 796 | delete item; |
| 797 | qApp->processEvents(); |
| 798 | } |
| 799 | |
| 800 | void tst_qqmllistmodelworkerscript::dynamic_role_data() |
| 801 | { |
| 802 | QTest::addColumn<QString>(name: "preamble" ); |
| 803 | QTest::addColumn<QString>(name: "script" ); |
| 804 | QTest::addColumn<int>(name: "result" ); |
| 805 | |
| 806 | QTest::newRow(dataTag: "sync1" ) << "{append({'a':[{'b':1},{'b':2}]})}" << "{get(0).a = 'string';count}" << 1; |
| 807 | } |
| 808 | |
| 809 | void tst_qqmllistmodelworkerscript::dynamic_role() |
| 810 | { |
| 811 | QFETCH(QString, preamble); |
| 812 | QFETCH(QString, script); |
| 813 | QFETCH(int, result); |
| 814 | |
| 815 | QQmlListModel model; |
| 816 | model.setDynamicRoles(true); |
| 817 | QQmlEngine engine; |
| 818 | QQmlComponent component(&engine, testFileUrl(fileName: "model.qml" )); |
| 819 | QQuickItem *item = createWorkerTest(eng: &engine, component: &component, model: &model); |
| 820 | QVERIFY(item != nullptr); |
| 821 | |
| 822 | QQmlExpression preExp(engine.rootContext(), &model, preamble); |
| 823 | QCOMPARE(preExp.evaluate().toInt(), 0); |
| 824 | |
| 825 | if (script[0] == QLatin1Char('{') && script[script.length()-1] == QLatin1Char('}')) |
| 826 | script = script.mid(position: 1, n: script.length() - 2); |
| 827 | QVariantList operations; |
| 828 | foreach (const QString &s, script.split(';')) { |
| 829 | if (!s.isEmpty()) |
| 830 | operations << s; |
| 831 | } |
| 832 | |
| 833 | // execute a set of commands on the worker list model, then check the |
| 834 | // changes are reflected in the list model in the main thread |
| 835 | QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker" , |
| 836 | Q_ARG(QVariant, operations.mid(0, operations.length()-1)))); |
| 837 | waitForWorker(item); |
| 838 | |
| 839 | QQmlExpression e(engine.rootContext(), &model, operations.last().toString()); |
| 840 | QCOMPARE(e.evaluate().toInt(), result); |
| 841 | |
| 842 | delete item; |
| 843 | qApp->processEvents(); |
| 844 | } |
| 845 | |
| 846 | void tst_qqmllistmodelworkerscript::correctMoves() |
| 847 | { |
| 848 | QQmlEngine engine; |
| 849 | QQmlComponent component(&engine, testFileUrl(fileName: "listmodel_async_sort/main.qml" )); |
| 850 | QScopedPointer<QObject> root {component.create()}; |
| 851 | QVERIFY2(root, qPrintable(component.errorString())); |
| 852 | bool ok =QMetaObject::invokeMethod(obj: root.get(), member: "doSort" ); |
| 853 | QVERIFY(ok); |
| 854 | auto check = [&](){ |
| 855 | bool success = false; |
| 856 | QMetaObject::invokeMethod(obj: root.get(), member: "verify" , Q_RETURN_ARG(bool, success)); |
| 857 | return success; |
| 858 | }; |
| 859 | QTRY_VERIFY(check()); |
| 860 | } |
| 861 | |
| 862 | QTEST_MAIN(tst_qqmllistmodelworkerscript) |
| 863 | |
| 864 | #include "tst_qqmllistmodelworkerscript.moc" |
| 865 | |