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
29#include <QtTest/QtTest>
30#include <QtGlobal>
31#include <math.h>
32#include <QMetaObject>
33#include <qtest.h>
34#include <QtTest/qsignalspy.h>
35#include <QtQml/qqmlnetworkaccessmanagerfactory.h>
36#include <QtNetwork/qnetworkaccessmanager.h>
37#include <QtNetwork/qnetworkrequest.h>
38#include <QtCore/qtimer.h>
39#include <QtCore/qfile.h>
40#include <QtCore/qtemporaryfile.h>
41#include <QtCore/qsortfilterproxymodel.h>
42#include "../../shared/util.h"
43#include <private/qqmlengine_p.h>
44
45#include <QtQml/qqmlengine.h>
46#include <QtQml/qqmlcomponent.h>
47#include "../../../src/imports/xmllistmodel/qqmlxmllistmodel_p.h"
48
49#include <algorithm>
50
51typedef QPair<int, int> QQuickXmlListRange;
52typedef QList<QVariantList> QQmlXmlModelData;
53
54Q_DECLARE_METATYPE(QList<QQuickXmlListRange>)
55Q_DECLARE_METATYPE(QQmlXmlModelData)
56Q_DECLARE_METATYPE(QQuickXmlListModel::Status)
57
58class tst_qquickxmllistmodel : public QQmlDataTest
59
60{
61 Q_OBJECT
62public:
63 tst_qquickxmllistmodel() {}
64
65private slots:
66 void initTestCase() {
67 QQmlDataTest::initTestCase();
68 qRegisterMetaType<QQuickXmlListModel::Status>();
69 }
70
71 void buildModel();
72 void testTypes();
73 void testTypes_data();
74 void cdata();
75 void attributes();
76 void roles();
77 void roleErrors();
78 void uniqueRoleNames();
79 void headers();
80 void xml();
81 void xml_data();
82 void source();
83 void source_data();
84 void data();
85 void get();
86 void reload();
87 void useKeys();
88 void useKeys_data();
89 void noKeysValueChanges();
90 void keysChanged();
91 void threading();
92 void threading_data();
93 void propertyChanges();
94 void selectAncestor();
95
96 void roleCrash();
97 void proxyCrash();
98
99private:
100 QString errorString(QAbstractItemModel *model) {
101 QString ret;
102 QMetaObject::invokeMethod(obj: model, member: "errorString", Q_RETURN_ARG(QString, ret));
103 return ret;
104 }
105
106 QString makeItemXmlAndData(const QString &data, QQmlXmlModelData *modelData = 0) const
107 {
108 if (modelData)
109 modelData->clear();
110 QString xml;
111
112 if (!data.isEmpty()) {
113 const QStringList items = data.split(sep: QLatin1Char(';'));
114 for (const QString &item : items) {
115 if (item.isEmpty())
116 continue;
117 QVariantList variants;
118 xml += QLatin1String("<item>");
119 const QStringList fields = item.split(sep: QLatin1Char(','));
120 for (const QString &field : fields) {
121 QStringList values = field.split(sep: QLatin1Char('='));
122 if (values.count() != 2) {
123 qWarning() << "makeItemXmlAndData: invalid field:" << field;
124 continue;
125 }
126 xml += QString("<%1>%2</%1>").arg(args&: values[0], args&: values[1]);
127 if (!modelData)
128 continue;
129 bool isNum = false;
130 int number = values[1].toInt(ok: &isNum);
131 if (isNum)
132 variants << number;
133 else
134 variants << values[1];
135 }
136 xml += QLatin1String("</item>");
137 if (modelData)
138 modelData->append(t: variants);
139 }
140 }
141
142 QString decl = "<?xml version=\"1.0\" encoding=\"iso-8859-1\" ?>";
143 return decl + QLatin1String("<data>") + xml + QLatin1String("</data>");
144 }
145
146 QQmlEngine engine;
147};
148
149class CustomNetworkAccessManagerFactory : public QObject, public QQmlNetworkAccessManagerFactory
150{
151 Q_OBJECT
152public:
153 QVariantMap lastSentHeaders;
154
155protected:
156 QNetworkAccessManager *create(QObject *parent);
157};
158
159class CustomNetworkAccessManager : public QNetworkAccessManager
160{
161 Q_OBJECT
162public:
163 CustomNetworkAccessManager(CustomNetworkAccessManagerFactory *factory, QObject *parent)
164 : QNetworkAccessManager(parent), m_factory(factory) {}
165
166protected:
167 QNetworkReply *createRequest(Operation op, const QNetworkRequest &req, QIODevice * outgoingData = 0)
168 {
169 if (m_factory) {
170 QVariantMap map;
171 const auto rawHeaderList = req.rawHeaderList();
172 for (const QString &header : rawHeaderList)
173 map[header] = req.rawHeader(headerName: header.toUtf8());
174 m_factory->lastSentHeaders = map;
175 }
176 return QNetworkAccessManager::createRequest(op, request: req, outgoingData);
177 }
178
179 QPointer<CustomNetworkAccessManagerFactory> m_factory;
180};
181
182QNetworkAccessManager *CustomNetworkAccessManagerFactory::create(QObject *parent)
183{
184 return new CustomNetworkAccessManager(this, parent);
185}
186
187
188void tst_qquickxmllistmodel::buildModel()
189{
190 QQmlComponent component(&engine, testFileUrl(fileName: "model.qml"));
191 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
192 QVERIFY(model != 0);
193 QTRY_COMPARE(model->rowCount(), 9);
194
195 QModelIndex index = model->index(row: 3, column: 0);
196 QCOMPARE(model->data(index, Qt::UserRole).toString(), QLatin1String("Spot"));
197 QCOMPARE(model->data(index, Qt::UserRole+1).toString(), QLatin1String("Dog"));
198 QCOMPARE(model->data(index, Qt::UserRole+2).toInt(), 9);
199 QCOMPARE(model->data(index, Qt::UserRole+3).toString(), QLatin1String("Medium"));
200
201 delete model;
202}
203
204void tst_qquickxmllistmodel::testTypes()
205{
206 QFETCH(QString, xml);
207 QFETCH(QString, roleName);
208 QFETCH(QVariant, expectedValue);
209
210 QQmlComponent component(&engine, testFileUrl(fileName: "testtypes.qml"));
211 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
212 QVERIFY(model != 0);
213 model->setProperty(name: "xml",value: xml.toUtf8());
214 QMetaObject::invokeMethod(obj: model, member: "reload");
215 QTRY_COMPARE(model->rowCount(), 1);
216
217 int role = model->roleNames().key(value: roleName.toUtf8(), defaultKey: -1);
218 QVERIFY(role >= 0);
219
220 QModelIndex index = model->index(row: 0, column: 0);
221 if (expectedValue.toString() == "nan")
222 QVERIFY(qIsNaN(model->data(index, role).toDouble()));
223 else
224 QCOMPARE(model->data(index, role), expectedValue);
225
226 delete model;
227}
228
229void tst_qquickxmllistmodel::testTypes_data()
230{
231 QTest::addColumn<QString>(name: "xml");
232 QTest::addColumn<QString>(name: "roleName");
233 QTest::addColumn<QVariant>(name: "expectedValue");
234
235 QTest::newRow(dataTag: "missing string field") << "<data></data>"
236 << "stringValue" << QVariant("");
237 QTest::newRow(dataTag: "empty string") << "<data><a-string></a-string></data>"
238 << "stringValue" << QVariant("");
239 QTest::newRow(dataTag: "1-char string") << "<data><a-string>5</a-string></data>"
240 << "stringValue" << QVariant("5");
241 QTest::newRow(dataTag: "string ok") << "<data><a-string>abc def g</a-string></data>"
242 << "stringValue" << QVariant("abc def g");
243
244 QTest::newRow(dataTag: "missing number field") << "<data></data>"
245 << "numberValue" << QVariant("");
246 double nan = qQNaN();
247 QTest::newRow(dataTag: "empty number field") << "<data><a-number></a-number></data>"
248 << "numberValue" << QVariant(nan);
249 QTest::newRow(dataTag: "number field with string") << "<data><a-number>a string</a-number></data>"
250 << "numberValue" << QVariant(nan);
251 QTest::newRow(dataTag: "-1") << "<data><a-number>-1</a-number></data>"
252 << "numberValue" << QVariant("-1");
253 QTest::newRow(dataTag: "-1.5") << "<data><a-number>-1.5</a-number></data>"
254 << "numberValue" << QVariant("-1.5");
255 QTest::newRow(dataTag: "0") << "<data><a-number>0</a-number></data>"
256 << "numberValue" << QVariant("0");
257 QTest::newRow(dataTag: "+1") << "<data><a-number>1</a-number></data>"
258 << "numberValue" << QVariant("1");
259 QTest::newRow(dataTag: "+1.5") << "<data><a-number>1.5</a-number></data>"
260 << "numberValue" << QVariant("1.5");
261}
262
263void tst_qquickxmllistmodel::cdata()
264{
265 QQmlComponent component(&engine, testFileUrl(fileName: "recipes.qml"));
266 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
267 QVERIFY(model != 0);
268 QTRY_COMPARE(model->rowCount(), 5);
269
270 QVERIFY(model->data(model->index(2, 0), Qt::UserRole+2).toString().startsWith(QLatin1String("<html>")));
271
272 delete model;
273}
274
275void tst_qquickxmllistmodel::attributes()
276{
277 QQmlComponent component(&engine, testFileUrl(fileName: "recipes.qml"));
278 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
279 QVERIFY(model != 0);
280 QTRY_COMPARE(model->rowCount(), 5);
281 QCOMPARE(model->data(model->index(2, 0), Qt::UserRole).toString(), QLatin1String("Vegetable Soup"));
282
283 delete model;
284}
285
286void tst_qquickxmllistmodel::roles()
287{
288 QQmlComponent component(&engine, testFileUrl(fileName: "model.qml"));
289 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
290 QVERIFY(model != 0);
291 QTRY_COMPARE(model->rowCount(), 9);
292
293 QHash<int, QByteArray> roleNames = model->roleNames();
294 QCOMPARE(roleNames.count(), 4);
295 QVERIFY(roleNames.key("name", -1) >= 0);
296 QVERIFY(roleNames.key("type", -1) >= 0);
297 QVERIFY(roleNames.key("age", -1) >= 0);
298 QVERIFY(roleNames.key("size", -1) >= 0);
299
300 QSet<int> roles;
301 roles.insert(value: roleNames.key(value: "name"));
302 roles.insert(value: roleNames.key(value: "type"));
303 roles.insert(value: roleNames.key(value: "age"));
304 roles.insert(value: roleNames.key(value: "size"));
305 QCOMPARE(roles.count(), 4);
306
307 delete model;
308}
309
310void tst_qquickxmllistmodel::roleErrors()
311{
312 QQmlComponent component(&engine, testFileUrl(fileName: "roleErrors.qml"));
313 QTest::ignoreMessage(type: QtWarningMsg, message: (testFileUrl(fileName: "roleErrors.qml").toString() + ":7:5: QML XmlRole: An XmlRole query must not start with '/'").toUtf8().constData());
314 QTest::ignoreMessage(type: QtWarningMsg, message: (testFileUrl(fileName: "roleErrors.qml").toString() + ":10:5: QML XmlRole: invalid query: \"age/\"").toUtf8().constData());
315
316 //### make sure we receive all expected warning messages.
317 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
318 QVERIFY(model != 0);
319 QTRY_COMPARE(model->rowCount(), 9);
320
321 QModelIndex index = model->index(row: 3, column: 0);
322 //### should any of these return valid values?
323 QCOMPARE(model->data(index, Qt::UserRole), QVariant());
324 QCOMPARE(model->data(index, Qt::UserRole+1), QVariant());
325 QCOMPARE(model->data(index, Qt::UserRole+2), QVariant());
326
327 QEXPECT_FAIL("", "QTBUG-10797", Continue);
328 QCOMPARE(model->data(index, Qt::UserRole+3), QVariant());
329
330 delete model;
331}
332
333void tst_qquickxmllistmodel::uniqueRoleNames()
334{
335 QQmlComponent component(&engine, testFileUrl(fileName: "unique.qml"));
336 QTest::ignoreMessage(type: QtWarningMsg, message: (testFileUrl(fileName: "unique.qml").toString() + ":8:5: QML XmlRole: \"name\" duplicates a previous role name and will be disabled.").toUtf8().constData());
337 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
338 QVERIFY(model != 0);
339 QTRY_COMPARE(model->rowCount(), 9);
340
341 QHash<int, QByteArray> roleNames = model->roleNames();
342 QCOMPARE(roleNames.count(), 1);
343
344 delete model;
345}
346
347
348void tst_qquickxmllistmodel::xml()
349{
350 QFETCH(QString, xml);
351 QFETCH(int, count);
352
353 QQmlComponent component(&engine, testFileUrl(fileName: "model.qml"));
354 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
355
356 QSignalSpy spy(model, SIGNAL(statusChanged(QQuickXmlListModel::Status)));
357 QVERIFY(errorString(model).isEmpty());
358 QCOMPARE(model->property("progress").toDouble(), qreal(1.0));
359 QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
360 QQuickXmlListModel::Loading);
361 QTRY_COMPARE(spy.count(), 1); spy.clear();
362 QTest::qWait(ms: 50);
363 QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
364 QQuickXmlListModel::Ready);
365 QVERIFY(errorString(model).isEmpty());
366 QCOMPARE(model->property("progress").toDouble(), qreal(1.0));
367 QCOMPARE(model->rowCount(), 9);
368
369 // if xml is empty (i.e. clearing) it won't have any effect if a source is set
370 if (xml.isEmpty())
371 model->setProperty(name: "source",value: QUrl());
372 model->setProperty(name: "xml",value: xml);
373 QCOMPARE(model->property("progress").toDouble(), qreal(1.0)); // immediately goes to 1.0 if using setXml()
374 QTRY_COMPARE(spy.count(), 1); spy.clear();
375 QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
376 QQuickXmlListModel::Loading);
377 QTRY_COMPARE(spy.count(), 1); spy.clear();
378 if (xml.isEmpty())
379 QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
380 QQuickXmlListModel::Null);
381 else
382 QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
383 QQuickXmlListModel::Ready);
384 QVERIFY(errorString(model).isEmpty());
385 QCOMPARE(model->rowCount(), count);
386
387 delete model;
388}
389
390void tst_qquickxmllistmodel::xml_data()
391{
392 QTest::addColumn<QString>(name: "xml");
393 QTest::addColumn<int>(name: "count");
394
395 QTest::newRow(dataTag: "xml with no items") << "<Pets></Pets>" << 0;
396 QTest::newRow(dataTag: "empty xml") << "" << 0;
397 QTest::newRow(dataTag: "one item") << "<Pets><Pet><name>Hobbes</name><type>Tiger</type><age>7</age><size>Large</size></Pet></Pets>" << 1;
398}
399
400void tst_qquickxmllistmodel::headers()
401{
402 // ensure the QNetworkAccessManagers created for this test are immediately deleted
403 QQmlEngine qmlEng;
404
405 CustomNetworkAccessManagerFactory factory;
406 qmlEng.setNetworkAccessManagerFactory(&factory);
407
408 QQmlComponent component(&qmlEng, testFileUrl(fileName: "model.qml"));
409 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
410 QVERIFY(model != 0);
411 QTRY_COMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
412 QQuickXmlListModel::Ready);
413
414 // It doesn't do a network request for a local file
415 QCOMPARE(factory.lastSentHeaders.count(), 0);
416
417 model->setProperty(name: "source", value: QUrl("http://localhost/filethatdoesnotexist.xml"));
418 QTRY_COMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
419 QQuickXmlListModel::Error);
420
421 QVariantMap expectedHeaders;
422 expectedHeaders["Accept"] = "application/xml,*/*";
423
424 QCOMPARE(factory.lastSentHeaders.count(), expectedHeaders.count());
425 for (auto it = expectedHeaders.cbegin(), end = expectedHeaders.cend(); it != end; ++it) {
426 QVERIFY(factory.lastSentHeaders.contains(it.key()));
427 QCOMPARE(factory.lastSentHeaders[it.key()].toString(), it.value().toString());
428 }
429
430 delete model;
431}
432
433void tst_qquickxmllistmodel::source()
434{
435 QFETCH(QUrl, source);
436 QFETCH(int, count);
437 QFETCH(QQuickXmlListModel::Status, status);
438
439 QQmlComponent component(&engine, testFileUrl(fileName: "model.qml"));
440 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
441 QSignalSpy spy(model, SIGNAL(statusChanged(QQuickXmlListModel::Status)));
442
443 QVERIFY(errorString(model).isEmpty());
444 QCOMPARE(model->property("progress").toDouble(), qreal(1.0));
445 QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
446 QQuickXmlListModel::Loading);
447 QTRY_COMPARE(spy.count(), 1); spy.clear();
448 QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
449 QQuickXmlListModel::Ready);
450 QVERIFY(errorString(model).isEmpty());
451 QCOMPARE(model->property("progress").toDouble(), qreal(1.0));
452 QCOMPARE(model->rowCount(), 9);
453
454 model->setProperty(name: "source",value: source);
455 if (model->property(name: "source").toString().isEmpty())
456 QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
457 QQuickXmlListModel::Null);
458 QCOMPARE(model->property("progress").toDouble(), qreal(source.isLocalFile() ? 1.0 : 0.0));
459 QTRY_COMPARE(spy.count(), 1); spy.clear();
460 QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")),
461 QQuickXmlListModel::Loading);
462 QVERIFY(errorString(model).isEmpty());
463
464 QEventLoop loop;
465 QTimer timer;
466 timer.setSingleShot(true);
467 connect(sender: model, SIGNAL(statusChanged(QQuickXmlListModel::Status)), receiver: &loop, SLOT(quit()));
468 connect(sender: &timer, SIGNAL(timeout()), receiver: &loop, SLOT(quit()));
469 timer.start(msec: 20000);
470 loop.exec();
471
472 if (spy.count() == 0 && status != QQuickXmlListModel::Ready) {
473 qWarning(msg: "QQuickXmlListModel invalid source test timed out");
474 } else {
475 QCOMPARE(spy.count(), 1); spy.clear();
476 }
477
478 QCOMPARE(qvariant_cast<QQuickXmlListModel::Status>(model->property("status")), status);
479 QCOMPARE(model->rowCount(), count);
480
481 if (status == QQuickXmlListModel::Ready)
482 QCOMPARE(model->property("progress").toDouble(), qreal(1.0));
483
484 QCOMPARE(errorString(model).isEmpty(), status == QQuickXmlListModel::Ready);
485
486 delete model;
487}
488
489void tst_qquickxmllistmodel::source_data()
490{
491 QTest::addColumn<QUrl>(name: "source");
492 QTest::addColumn<int>(name: "count");
493 QTest::addColumn<QQuickXmlListModel::Status>(name: "status");
494
495 QTest::newRow(dataTag: "valid") << testFileUrl(fileName: "model2.xml") << 2
496 << QQuickXmlListModel::Ready;
497 QTest::newRow(dataTag: "invalid") << QUrl("http://blah.blah/blah.xml") << 0
498 << QQuickXmlListModel::Error;
499
500 // empty file
501 QTemporaryFile *temp = new QTemporaryFile(this);
502 if (temp->open())
503 QTest::newRow(dataTag: "empty file") << QUrl::fromLocalFile(localfile: temp->fileName()) << 0
504 << QQuickXmlListModel::Ready;
505 temp->close();
506}
507
508void tst_qquickxmllistmodel::data()
509{
510 QQmlComponent component(&engine, testFileUrl(fileName: "model.qml"));
511 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
512 QVERIFY(model != 0);
513
514 for (int i=0; i<9; i++) {
515 QModelIndex index = model->index(row: i, column: 0);
516 for (int j=0; j<model->roleNames().count(); j++) {
517 QCOMPARE(model->data(index, j), QVariant());
518 }
519 }
520 QTRY_COMPARE(model->rowCount(), 9);
521
522 delete model;
523}
524
525void tst_qquickxmllistmodel::get()
526{
527 QQmlComponent component(&engine, testFileUrl(fileName: "get.qml"));
528 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
529
530 QVERIFY(model != 0);
531
532 QVERIFY(QMetaObject::invokeMethod(model, "runPreTest"));
533 QCOMPARE(model->property("preTest").toBool(), true);
534
535 QTRY_COMPARE(model->rowCount(), 9);
536
537 QVERIFY(QMetaObject::invokeMethod(model, "runPostTest"));
538 QCOMPARE(model->property("postTest").toBool(), true);
539
540 delete model;
541}
542
543void tst_qquickxmllistmodel::reload()
544{
545 // If no keys are used, the model should be rebuilt from scratch when
546 // reload() is called.
547
548 QQmlComponent component(&engine, testFileUrl(fileName: "model.qml"));
549 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
550 QVERIFY(model != 0);
551 QTRY_COMPARE(model->rowCount(), 9);
552
553 QSignalSpy spyInsert(model, SIGNAL(rowsInserted(QModelIndex,int,int)));
554 QSignalSpy spyRemove(model, SIGNAL(rowsRemoved(QModelIndex,int,int)));
555 QSignalSpy spyCount(model, SIGNAL(countChanged()));
556 //reload multiple times to test the xml query aborting
557 QMetaObject::invokeMethod(obj: model, member: "reload");
558 QMetaObject::invokeMethod(obj: model, member: "reload");
559 QCoreApplication::processEvents();
560 QMetaObject::invokeMethod(obj: model, member: "reload");
561 QMetaObject::invokeMethod(obj: model, member: "reload");
562 QTRY_COMPARE(spyCount.count(), 0);
563 QTRY_COMPARE(spyInsert.count(), 1);
564 QTRY_COMPARE(spyRemove.count(), 1);
565
566 QCOMPARE(spyInsert[0][1].toInt(), 0);
567 QCOMPARE(spyInsert[0][2].toInt(), 8);
568
569 QCOMPARE(spyRemove[0][1].toInt(), 0);
570 QCOMPARE(spyRemove[0][2].toInt(), 8);
571
572 delete model;
573}
574
575void tst_qquickxmllistmodel::useKeys()
576{
577 // If using incremental updates through keys, the model should only
578 // insert & remove some of the items, instead of throwing everything
579 // away and causing the view to repaint the whole view.
580
581 QFETCH(QString, oldXml);
582 QFETCH(int, oldCount);
583 QFETCH(QString, newXml);
584 QFETCH(QQmlXmlModelData, newData);
585 QFETCH(QList<QQuickXmlListRange>, insertRanges);
586 QFETCH(QList<QQuickXmlListRange>, removeRanges);
587
588 QQmlComponent component(&engine, testFileUrl(fileName: "roleKeys.qml"));
589 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
590 QVERIFY(model != 0);
591
592 model->setProperty(name: "xml",value: oldXml);
593 QTRY_COMPARE(model->rowCount(), oldCount);
594
595 QSignalSpy spyInsert(model, SIGNAL(rowsInserted(QModelIndex,int,int)));
596 QSignalSpy spyRemove(model, SIGNAL(rowsRemoved(QModelIndex,int,int)));
597 QSignalSpy spyCount(model, SIGNAL(countChanged()));
598
599 model->setProperty(name: "xml",value: newXml);
600
601 if (oldCount != newData.count()) {
602 QTRY_COMPARE(model->rowCount(), newData.count());
603 QCOMPARE(spyCount.count(), 1);
604 } else {
605 QTRY_VERIFY(spyInsert.count() > 0 || spyRemove.count() > 0);
606 QCOMPARE(spyCount.count(), 0);
607 }
608
609 QList<int> roles = model->roleNames().keys();
610 std::sort(first: roles.begin(), last: roles.end());
611 for (int i=0; i<model->rowCount(); i++) {
612 QModelIndex index = model->index(row: i, column: 0);
613 for (int j=0; j<roles.count(); j++)
614 QCOMPARE(model->data(index, roles.at(j)), newData[i][j]);
615 }
616
617 QCOMPARE(spyInsert.count(), insertRanges.count());
618 for (int i=0; i<spyInsert.count(); i++) {
619 QCOMPARE(spyInsert[i][1].toInt(), insertRanges[i].first);
620 QCOMPARE(spyInsert[i][2].toInt(), insertRanges[i].first + insertRanges[i].second - 1);
621 }
622
623 QCOMPARE(spyRemove.count(), removeRanges.count());
624 for (int i=0; i<spyRemove.count(); i++) {
625 QCOMPARE(spyRemove[i][1].toInt(), removeRanges[i].first);
626 QCOMPARE(spyRemove[i][2].toInt(), removeRanges[i].first + removeRanges[i].second - 1);
627 }
628
629 delete model;
630}
631
632void tst_qquickxmllistmodel::useKeys_data()
633{
634 QTest::addColumn<QString>(name: "oldXml");
635 QTest::addColumn<int>(name: "oldCount");
636 QTest::addColumn<QString>(name: "newXml");
637 QTest::addColumn<QQmlXmlModelData>(name: "newData");
638 QTest::addColumn<QList<QQuickXmlListRange> >(name: "insertRanges");
639 QTest::addColumn<QList<QQuickXmlListRange> >(name: "removeRanges");
640
641 QQmlXmlModelData modelData;
642
643 QTest::newRow(dataTag: "append 1")
644 << makeItemXmlAndData(data: "name=A,age=25,sport=Football") << 1
645 << makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=B,age=35,sport=Athletics", modelData: &modelData)
646 << modelData
647 << (QList<QQuickXmlListRange>() << qMakePair(x: 1, y: 1))
648 << QList<QQuickXmlListRange>();
649
650 QTest::newRow(dataTag: "append multiple")
651 << makeItemXmlAndData(data: "name=A,age=25,sport=Football") << 1
652 << makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling", modelData: &modelData)
653 << modelData
654 << (QList<QQuickXmlListRange>() << qMakePair(x: 1, y: 2))
655 << QList<QQuickXmlListRange>();
656
657 QTest::newRow(dataTag: "insert in different spots")
658 << makeItemXmlAndData(data: "name=B,age=35,sport=Athletics") << 1
659 << makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling;name=D,age=55,sport=Golf", modelData: &modelData)
660 << modelData
661 << (QList<QQuickXmlListRange>() << qMakePair(x: 0, y: 1) << qMakePair(x: 2,y: 2))
662 << QList<QQuickXmlListRange>();
663
664 QTest::newRow(dataTag: "insert in middle")
665 << makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=D,age=55,sport=Golf") << 2
666 << makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling;name=D,age=55,sport=Golf", modelData: &modelData)
667 << modelData
668 << (QList<QQuickXmlListRange>() << qMakePair(x: 1, y: 2))
669 << QList<QQuickXmlListRange>();
670
671 QTest::newRow(dataTag: "remove first")
672 << makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=B,age=35,sport=Athletics") << 2
673 << makeItemXmlAndData(data: "name=B,age=35,sport=Athletics", modelData: &modelData)
674 << modelData
675 << QList<QQuickXmlListRange>()
676 << (QList<QQuickXmlListRange>() << qMakePair(x: 0, y: 1));
677
678 QTest::newRow(dataTag: "remove last")
679 << makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=B,age=35,sport=Athletics") << 2
680 << makeItemXmlAndData(data: "name=A,age=25,sport=Football", modelData: &modelData)
681 << modelData
682 << QList<QQuickXmlListRange>()
683 << (QList<QQuickXmlListRange>() << qMakePair(x: 1, y: 1));
684
685 QTest::newRow(dataTag: "remove from multiple spots")
686 << makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling;name=D,age=55,sport=Golf;name=E,age=65,sport=Fencing") << 5
687 << makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=C,age=45,sport=Curling", modelData: &modelData)
688 << modelData
689 << QList<QQuickXmlListRange>()
690 << (QList<QQuickXmlListRange>() << qMakePair(x: 1, y: 1) << qMakePair(x: 3,y: 2));
691
692 QTest::newRow(dataTag: "remove all")
693 << makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling") << 3
694 << makeItemXmlAndData(data: "", modelData: &modelData)
695 << modelData
696 << QList<QQuickXmlListRange>()
697 << (QList<QQuickXmlListRange>() << qMakePair(x: 0, y: 3));
698
699 QTest::newRow(dataTag: "replace item")
700 << makeItemXmlAndData(data: "name=A,age=25,sport=Football") << 1
701 << makeItemXmlAndData(data: "name=ZZZ,age=25,sport=Football", modelData: &modelData)
702 << modelData
703 << (QList<QQuickXmlListRange>() << qMakePair(x: 0, y: 1))
704 << (QList<QQuickXmlListRange>() << qMakePair(x: 0, y: 1));
705
706 QTest::newRow(dataTag: "add and remove simultaneously, in different spots")
707 << makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling;name=D,age=55,sport=Golf") << 4
708 << makeItemXmlAndData(data: "name=B,age=35,sport=Athletics;name=E,age=65,sport=Fencing", modelData: &modelData)
709 << modelData
710 << (QList<QQuickXmlListRange>() << qMakePair(x: 1, y: 1))
711 << (QList<QQuickXmlListRange>() << qMakePair(x: 0, y: 1) << qMakePair(x: 2,y: 2));
712
713 QTest::newRow(dataTag: "insert at start, remove at end i.e. rss feed")
714 << makeItemXmlAndData(data: "name=C,age=45,sport=Curling;name=D,age=55,sport=Golf;name=E,age=65,sport=Fencing") << 3
715 << makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling", modelData: &modelData)
716 << modelData
717 << (QList<QQuickXmlListRange>() << qMakePair(x: 0, y: 2))
718 << (QList<QQuickXmlListRange>() << qMakePair(x: 1, y: 2));
719
720 QTest::newRow(dataTag: "remove at start, insert at end")
721 << makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=B,age=35,sport=Athletics;name=C,age=45,sport=Curling") << 3
722 << makeItemXmlAndData(data: "name=C,age=45,sport=Curling;name=D,age=55,sport=Golf;name=E,age=65,sport=Fencing", modelData: &modelData)
723 << modelData
724 << (QList<QQuickXmlListRange>() << qMakePair(x: 1, y: 2))
725 << (QList<QQuickXmlListRange>() << qMakePair(x: 0, y: 2));
726
727 QTest::newRow(dataTag: "all data has changed")
728 << makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=B,age=35") << 2
729 << makeItemXmlAndData(data: "name=C,age=45,sport=Curling;name=D,age=55,sport=Golf", modelData: &modelData)
730 << modelData
731 << (QList<QQuickXmlListRange>() << qMakePair(x: 0, y: 2))
732 << (QList<QQuickXmlListRange>() << qMakePair(x: 0, y: 2));
733}
734
735void tst_qquickxmllistmodel::noKeysValueChanges()
736{
737 // The 'key' roles are 'name' and 'age', as defined in roleKeys.qml.
738 // If a 'sport' value is changed, the model should not be reloaded,
739 // since 'sport' is not marked as a key.
740
741 QQmlComponent component(&engine, testFileUrl(fileName: "roleKeys.qml"));
742 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
743 QVERIFY(model != 0);
744
745 QString xml;
746
747 xml = makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=B,age=35,sport=Athletics");
748 model->setProperty(name: "xml",value: xml);
749 QTRY_COMPARE(model->rowCount(), 2);
750
751 model->setProperty(name: "xml",value: "");
752
753 QSignalSpy spyInsert(model, SIGNAL(rowsInserted(QModelIndex,int,int)));
754 QSignalSpy spyRemove(model, SIGNAL(rowsRemoved(QModelIndex,int,int)));
755 QSignalSpy spyCount(model, SIGNAL(countChanged()));
756
757 xml = makeItemXmlAndData(data: "name=A,age=25,sport=AussieRules;name=B,age=35,sport=Athletics");
758 model->setProperty(name: "xml",value: xml);
759
760 QList<int> roles = model->roleNames().keys();
761 std::sort(first: roles.begin(), last: roles.end());
762 // wait for the new xml data to be set, and verify no signals were emitted
763 QTRY_VERIFY(model->data(model->index(0, 0), roles.at(2)).toString() != QLatin1String("Football"));
764 QCOMPARE(model->data(model->index(0, 0), roles.at(2)).toString(), QLatin1String("AussieRules"));
765
766 QCOMPARE(spyInsert.count(), 0);
767 QCOMPARE(spyRemove.count(), 0);
768 QCOMPARE(spyCount.count(), 0);
769
770 QCOMPARE(model->rowCount(), 2);
771
772 delete model;
773}
774
775void tst_qquickxmllistmodel::keysChanged()
776{
777 // If the key roles change, the next time the data is reloaded, it should
778 // delete all its data and build a clean model (i.e. same behavior as
779 // if no keys are set).
780
781 QQmlComponent component(&engine, testFileUrl(fileName: "roleKeys.qml"));
782 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
783 QVERIFY(model != 0);
784
785 QString xml = makeItemXmlAndData(data: "name=A,age=25,sport=Football;name=B,age=35,sport=Athletics");
786 model->setProperty(name: "xml",value: xml);
787 QTRY_COMPARE(model->rowCount(), 2);
788
789 model->setProperty(name: "xml",value: "");
790
791 QSignalSpy spyInsert(model, SIGNAL(rowsInserted(QModelIndex,int,int)));
792 QSignalSpy spyRemove(model, SIGNAL(rowsRemoved(QModelIndex,int,int)));
793 QSignalSpy spyCount(model, SIGNAL(countChanged()));
794
795 QVERIFY(QMetaObject::invokeMethod(model, "disableNameKey"));
796 model->setProperty(name: "xml",value: xml);
797
798 QTRY_VERIFY(spyInsert.count() > 0 && spyRemove.count() > 0);
799
800 QCOMPARE(spyInsert.count(), 1);
801 QCOMPARE(spyInsert[0][1].toInt(), 0);
802 QCOMPARE(spyInsert[0][2].toInt(), 1);
803
804 QCOMPARE(spyRemove.count(), 1);
805 QCOMPARE(spyRemove[0][1].toInt(), 0);
806 QCOMPARE(spyRemove[0][2].toInt(), 1);
807
808 QCOMPARE(spyCount.count(), 0);
809
810 delete model;
811}
812
813void tst_qquickxmllistmodel::threading()
814{
815 QFETCH(int, xmlDataCount);
816
817 QQmlComponent component(&engine, testFileUrl(fileName: "roleKeys.qml"));
818
819 QAbstractItemModel *m1 = qobject_cast<QAbstractItemModel *>(object: component.create());
820 QVERIFY(m1 != 0);
821 QAbstractItemModel *m2 = qobject_cast<QAbstractItemModel *>(object: component.create());
822 QVERIFY(m2 != 0);
823 QAbstractItemModel *m3 = qobject_cast<QAbstractItemModel *>(object: component.create());
824 QVERIFY(m3 != 0);
825
826 for (int dataCount=0; dataCount<xmlDataCount; dataCount++) {
827
828 QString data1, data2, data3;
829 for (int i=0; i<dataCount; i++) {
830 data1 += "name=A" + QString::number(i) + ",age=1" + QString::number(i) + ",sport=Football;";
831 data2 += "name=B" + QString::number(i) + ",age=2" + QString::number(i) + ",sport=Athletics;";
832 data3 += "name=C" + QString::number(i) + ",age=3" + QString::number(i) + ",sport=Curling;";
833 }
834
835 //Set the xml data multiple times with randomized order and mixed with multiple event loops
836 //to test the xml query reloading/aborting, the result should be stable.
837 m1->setProperty(name: "xml",value: makeItemXmlAndData(data: data1));
838 m2->setProperty(name: "xml",value: makeItemXmlAndData(data: data2));
839 m3->setProperty(name: "xml",value: makeItemXmlAndData(data: data3));
840 QCoreApplication::processEvents();
841 m2->setProperty(name: "xml",value: makeItemXmlAndData(data: data2));
842 m1->setProperty(name: "xml",value: makeItemXmlAndData(data: data1));
843 m2->setProperty(name: "xml",value: makeItemXmlAndData(data: data2));
844 QCoreApplication::processEvents();
845 m3->setProperty(name: "xml",value: makeItemXmlAndData(data: data3));
846 QCoreApplication::processEvents();
847 m2->setProperty(name: "xml",value: makeItemXmlAndData(data: data2));
848 m1->setProperty(name: "xml",value: makeItemXmlAndData(data: data1));
849 m2->setProperty(name: "xml",value: makeItemXmlAndData(data: data2));
850 m3->setProperty(name: "xml",value: makeItemXmlAndData(data: data3));
851 QCoreApplication::processEvents();
852 m2->setProperty(name: "xml",value: makeItemXmlAndData(data: data2));
853 m3->setProperty(name: "xml",value: makeItemXmlAndData(data: data3));
854 m3->setProperty(name: "xml",value: makeItemXmlAndData(data: data3));
855 QCoreApplication::processEvents();
856
857 QTRY_VERIFY(m1->rowCount() == dataCount && m2->rowCount() == dataCount && m3->rowCount() == dataCount);
858
859 for (int i=0; i<dataCount; i++) {
860 QModelIndex index = m1->index(row: i, column: 0);
861 QList<int> roles = m1->roleNames().keys();
862 std::sort(first: roles.begin(), last: roles.end());
863 QCOMPARE(m1->data(index, roles.at(0)).toString(), QLatin1Char('A') + QString::number(i));
864 QCOMPARE(m1->data(index, roles.at(1)).toString(), QLatin1Char('1') + QString::number(i));
865 QCOMPARE(m1->data(index, roles.at(2)).toString(), QString("Football"));
866
867 index = m2->index(row: i, column: 0);
868 roles = m2->roleNames().keys();
869 std::sort(first: roles.begin(), last: roles.end());
870 QCOMPARE(m2->data(index, roles.at(0)).toString(), QLatin1Char('B') + QString::number(i));
871 QCOMPARE(m2->data(index, roles.at(1)).toString(), QLatin1Char('2') + QString::number(i));
872 QCOMPARE(m2->data(index, roles.at(2)).toString(), QString("Athletics"));
873
874 index = m3->index(row: i, column: 0);
875 roles = m3->roleNames().keys();
876 std::sort(first: roles.begin(), last: roles.end());
877 QCOMPARE(m3->data(index, roles.at(0)).toString(), QLatin1Char('C') + QString::number(i));
878 QCOMPARE(m3->data(index, roles.at(1)).toString(), QLatin1Char('3') + QString::number(i));
879 QCOMPARE(m3->data(index, roles.at(2)).toString(), QString("Curling"));
880 }
881 }
882
883 delete m1;
884 delete m2;
885 delete m3;
886}
887
888void tst_qquickxmllistmodel::threading_data()
889{
890 QTest::addColumn<int>(name: "xmlDataCount");
891
892 QTest::newRow(dataTag: "1") << 1;
893 QTest::newRow(dataTag: "2") << 2;
894 QTest::newRow(dataTag: "10") << 10;
895}
896
897void tst_qquickxmllistmodel::propertyChanges()
898{
899 QQmlComponent component(&engine, testFileUrl(fileName: "propertychanges.qml"));
900 QAbstractItemModel *model = qobject_cast<QAbstractItemModel*>(object: component.create());
901 QVERIFY(model != 0);
902 QTRY_COMPARE(model->rowCount(), 9);
903
904 QObject *role = model->findChild<QObject*>(aName: "role");
905 QVERIFY(role);
906
907 QSignalSpy nameSpy(role, SIGNAL(nameChanged()));
908 QSignalSpy querySpy(role, SIGNAL(queryChanged()));
909 QSignalSpy isKeySpy(role, SIGNAL(isKeyChanged()));
910
911 role->setProperty(name: "name",value: "size");
912 role->setProperty(name: "query",value: "size/string()");
913 role->setProperty(name: "isKey",value: true);
914
915 QCOMPARE(role->property("name").toString(), QString("size"));
916 QCOMPARE(role->property("query").toString(), QString("size/string()"));
917 QVERIFY(role->property("isKey").toBool());
918
919 QCOMPARE(nameSpy.count(),1);
920 QCOMPARE(querySpy.count(),1);
921 QCOMPARE(isKeySpy.count(),1);
922
923 role->setProperty(name: "name",value: "size");
924 role->setProperty(name: "query",value: "size/string()");
925 role->setProperty(name: "isKey",value: true);
926
927 QCOMPARE(nameSpy.count(),1);
928 QCOMPARE(querySpy.count(),1);
929 QCOMPARE(isKeySpy.count(),1);
930
931 QSignalSpy sourceSpy(model, SIGNAL(sourceChanged()));
932 QSignalSpy xmlSpy(model, SIGNAL(xmlChanged()));
933 QSignalSpy modelQuerySpy(model, SIGNAL(queryChanged()));
934 QSignalSpy namespaceDeclarationsSpy(model, SIGNAL(namespaceDeclarationsChanged()));
935
936 model->setProperty(name: "source",value: QUrl(""));
937 model->setProperty(name: "xml",value: "<Pets><Pet><name>Polly</name><type>Parrot</type><age>12</age><size>Small</size></Pet></Pets>");
938 model->setProperty(name: "query",value: "/Pets");
939 model->setProperty(name: "namespaceDeclarations",value: "declare namespace media=\"http://search.yahoo.com/mrss/\";");
940
941 QCOMPARE(model->property("source").toUrl(), QUrl(""));
942 QCOMPARE(model->property("xml").toString(), QString("<Pets><Pet><name>Polly</name><type>Parrot</type><age>12</age><size>Small</size></Pet></Pets>"));
943 QCOMPARE(model->property("query").toString(), QString("/Pets"));
944 QCOMPARE(model->property("namespaceDeclarations").toString(), QString("declare namespace media=\"http://search.yahoo.com/mrss/\";"));
945
946 QTRY_COMPARE(model->rowCount(), 1);
947
948 QCOMPARE(sourceSpy.count(),1);
949 QCOMPARE(xmlSpy.count(),1);
950 QCOMPARE(modelQuerySpy.count(),1);
951 QCOMPARE(namespaceDeclarationsSpy.count(),1);
952
953 model->setProperty(name: "source",value: QUrl(""));
954 model->setProperty(name: "xml",value: "<Pets><Pet><name>Polly</name><type>Parrot</type><age>12</age><size>Small</size></Pet></Pets>");
955 model->setProperty(name: "query",value: "/Pets");
956 model->setProperty(name: "namespaceDeclarations",value: "declare namespace media=\"http://search.yahoo.com/mrss/\";");
957
958 QCOMPARE(sourceSpy.count(),1);
959 QCOMPARE(xmlSpy.count(),1);
960 QCOMPARE(modelQuerySpy.count(),1);
961 QCOMPARE(namespaceDeclarationsSpy.count(),1);
962
963 QTRY_COMPARE(model->rowCount(), 1);
964 delete model;
965}
966
967void tst_qquickxmllistmodel::selectAncestor()
968{
969 QQmlComponent component(&engine, testFileUrl(fileName: "groups.qml"));
970 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
971 QVERIFY(model != 0);
972 QTRY_COMPARE(model->rowCount(), 1);
973
974 QModelIndex index = model->index(row: 0, column: 0);
975 QCOMPARE(model->data(index, Qt::UserRole).toInt(), 12);
976 QCOMPARE(model->data(index, Qt::UserRole+1).toString(), QLatin1String("cats"));
977}
978
979void tst_qquickxmllistmodel::roleCrash()
980{
981 // don't crash
982 QQmlComponent component(&engine, testFileUrl(fileName: "roleCrash.qml"));
983 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
984 QVERIFY(model != 0);
985 delete model;
986}
987
988class SortFilterProxyModel : public QSortFilterProxyModel
989{
990 Q_OBJECT
991 Q_PROPERTY(QObject *source READ source WRITE setSource)
992
993public:
994 SortFilterProxyModel(QObject *parent = 0) : QSortFilterProxyModel(parent) { sort(column: 0); }
995 QObject *source() const { return sourceModel(); }
996 void setSource(QObject *source) { setSourceModel(qobject_cast<QAbstractItemModel *>(object: source)); }
997};
998
999void tst_qquickxmllistmodel::proxyCrash()
1000{
1001 qmlRegisterType<SortFilterProxyModel>(uri: "SortFilterProxyModel", versionMajor: 1, versionMinor: 0, qmlName: "SortFilterProxyModel");
1002
1003 // don't crash
1004 QQmlComponent component(&engine, testFileUrl(fileName: "proxyCrash.qml"));
1005 QAbstractItemModel *model = qobject_cast<QAbstractItemModel *>(object: component.create());
1006 QVERIFY(model != 0);
1007 delete model;
1008}
1009
1010QTEST_MAIN(tst_qquickxmllistmodel)
1011
1012#include "tst_qquickxmllistmodel.moc"
1013

source code of qtxmlpatterns/tests/auto/qquickxmllistmodel/tst_qquickxmllistmodel.cpp