aboutsummaryrefslogtreecommitdiffstats
path: root/tests/auto/qml/qqmlxmllistmodel/tst_qqmlxmllistmodel.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/auto/qml/qqmlxmllistmodel/tst_qqmlxmllistmodel.cpp')
-rw-r--r--tests/auto/qml/qqmlxmllistmodel/tst_qqmlxmllistmodel.cpp693
1 files changed, 693 insertions, 0 deletions
diff --git a/tests/auto/qml/qqmlxmllistmodel/tst_qqmlxmllistmodel.cpp b/tests/auto/qml/qqmlxmllistmodel/tst_qqmlxmllistmodel.cpp
new file mode 100644
index 0000000000..eebe4a6c05
--- /dev/null
+++ b/tests/auto/qml/qqmlxmllistmodel/tst_qqmlxmllistmodel.cpp
@@ -0,0 +1,693 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+#include <QtQmlXmlListModel/private/qqmlxmllistmodel_p.h>
+#include <QtQuickTestUtils/private/qmlutils_p.h>
+
+#include <QtTest/qsignalspy.h>
+
+#include <QtQml/qqmlcomponent.h>
+#include <QtQml/qqmlengine.h>
+#include <QtQml/qqmlnetworkaccessmanagerfactory.h>
+
+#include <QtCore/qset.h>
+#include <QtCore/qsortfilterproxymodel.h>
+#include <QtCore/qtemporaryfile.h>
+#include <QtCore/qtimer.h>
+
+using namespace Qt::StringLiterals;
+
+typedef QList<QVariantList> QQmlXmlModelData;
+
+Q_DECLARE_METATYPE(QQmlXmlModelData)
+Q_DECLARE_METATYPE(QQmlXmlListModel::Status)
+
+class tst_QQmlXmlListModel : public QQmlDataTest
+
+{
+ Q_OBJECT
+public:
+ tst_QQmlXmlListModel() : QQmlDataTest(QT_QMLTEST_DATADIR) { }
+
+private slots:
+ void initTestCase() override
+ {
+ QQmlDataTest::initTestCase();
+ qRegisterMetaType<QQmlXmlListModel::Status>();
+ }
+
+ void buildModel();
+ void cdata();
+ void attributes();
+ void roles();
+ void elementErrors();
+ void uniqueRoleNames();
+ void headers();
+ void source();
+ void source_data();
+ void data();
+ void reload();
+ void threading();
+ void threading_data();
+ void propertyChanges();
+ void nestedElements();
+ void malformedData();
+ void malformedData_data();
+
+ void roleCrash();
+ void proxyCrash();
+
+private:
+ QString errorString(QAbstractItemModel *model)
+ {
+ QString ret;
+ QMetaObject::invokeMethod(model, "errorString", Q_RETURN_ARG(QString, ret));
+ return ret;
+ }
+
+ QString makeItemXmlAndData(const QString &data, QQmlXmlModelData *modelData = 0) const
+ {
+ if (modelData)
+ modelData->clear();
+ QString xml;
+
+ if (!data.isEmpty()) {
+ const QStringList items = data.split(QLatin1Char(';'));
+ for (const QString &item : items) {
+ if (item.isEmpty())
+ continue;
+ QVariantList variants;
+ xml += QLatin1String("<item>");
+ const QStringList fields = item.split(QLatin1Char(','));
+ for (const QString &field : fields) {
+ QStringList values = field.split(QLatin1Char('='));
+ if (values.size() != 2) {
+ qWarning() << "makeItemXmlAndData: invalid field:" << field;
+ continue;
+ }
+ xml += QString("<%1>%2</%1>").arg(values[0], values[1]);
+ if (!modelData)
+ continue;
+ bool isNum = false;
+ int number = values[1].toInt(&isNum);
+ if (isNum)
+ variants << number;
+ else
+ variants << values[1];
+ }
+ xml += QLatin1String("</item>");
+ if (modelData)
+ modelData->append(variants);
+ }
+ }
+
+ QString decl = "<?xml version=\"1.0\" encoding=\"iso-8859-1\" ?>";
+ return decl + QLatin1String("<data>") + xml + QLatin1String("</data>");
+ }
+
+ QQmlEngine engine;
+};
+
+class ScopedFile
+{
+public:
+ ScopedFile(const QString &fileName, const QByteArray &data) : m_fileName(fileName)
+ {
+ m_file.setFileName(fileName);
+ m_created = m_file.open(QIODevice::WriteOnly | QIODevice::Truncate);
+ if (m_created) {
+ const auto written = m_file.write(data);
+ m_created = written == data.size();
+ m_file.close();
+ }
+ }
+ ~ScopedFile() { QFile::remove(m_file.fileName()); }
+
+ bool isCreated() const { return m_created; }
+ QString fileName() const { return m_fileName; }
+
+private:
+ QFile m_file;
+ const QString m_fileName;
+ bool m_created = false;
+};
+
+class CustomNetworkAccessManagerFactory : public QObject, public QQmlNetworkAccessManagerFactory
+{
+ Q_OBJECT
+public:
+ QVariantMap lastSentHeaders;
+
+protected:
+ QNetworkAccessManager *create(QObject *parent) override;
+};
+
+class CustomNetworkAccessManager : public QNetworkAccessManager
+{
+ Q_OBJECT
+public:
+ CustomNetworkAccessManager(CustomNetworkAccessManagerFactory *factory, QObject *parent)
+ : QNetworkAccessManager(parent), m_factory(factory)
+ {
+ }
+
+protected:
+ QNetworkReply *createRequest(Operation op, const QNetworkRequest &req,
+ QIODevice *outgoingData = 0) override
+ {
+ if (m_factory) {
+ QVariantMap map;
+ const auto rawHeaderList = req.rawHeaderList();
+ for (const QByteArray &header : rawHeaderList)
+ map[header] = req.rawHeader(header);
+ m_factory->lastSentHeaders = map;
+ }
+ return QNetworkAccessManager::createRequest(op, req, outgoingData);
+ }
+
+ QPointer<CustomNetworkAccessManagerFactory> m_factory;
+};
+
+QNetworkAccessManager *CustomNetworkAccessManagerFactory::create(QObject *parent)
+{
+ return new CustomNetworkAccessManager(this, parent);
+}
+
+void tst_QQmlXmlListModel::buildModel()
+{
+ QQmlComponent component(&engine, testFileUrl("model.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 9);
+
+ QModelIndex index = model->index(3, 0);
+ QCOMPARE(model->data(index, Qt::UserRole).toString(), QLatin1String("Spot"));
+ QCOMPARE(model->data(index, Qt::UserRole + 1).toString(), QLatin1String("Dog"));
+ QCOMPARE(model->data(index, Qt::UserRole + 2).toInt(), 9);
+ QCOMPARE(model->data(index, Qt::UserRole + 3).toString(), QLatin1String("Medium"));
+}
+
+void tst_QQmlXmlListModel::cdata()
+{
+ QQmlComponent component(&engine, testFileUrl("recipes.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 5);
+
+ QVERIFY(model->data(model->index(2, 0), Qt::UserRole + 2)
+ .toString()
+ .startsWith(QLatin1String("<html>")));
+}
+
+void tst_QQmlXmlListModel::attributes()
+{
+ QQmlComponent component(&engine, testFileUrl("attributes.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 4);
+
+ const QList<QVariantList> desiredResults = { QVariantList { "Polly", "Parrot", 12, "Small" },
+ QVariantList { "Penny", "Turtle", 4, "Small" },
+ QVariantList { "Spot", "Dog", 9, "Medium" },
+ QVariantList { "Tiny", "Elephant", 15, "Large" } };
+
+ QVERIFY(model->rowCount() == desiredResults.size());
+
+ for (qsizetype idx = 0; idx < model->rowCount(); ++idx) {
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole).toString(),
+ desiredResults.at(idx).at(0).toString());
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole + 1).toString(),
+ desiredResults.at(idx).at(1).toString());
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole + 2).toInt(),
+ desiredResults.at(idx).at(2).toInt());
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole + 3).toString(),
+ desiredResults.at(idx).at(3).toString());
+ }
+}
+
+void tst_QQmlXmlListModel::roles()
+{
+ QQmlComponent component(&engine, testFileUrl("model.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 9);
+
+ QHash<int, QByteArray> roleNames = model->roleNames();
+ QCOMPARE(roleNames.size(), 4);
+ QVERIFY(roleNames.key("name", -1) >= 0);
+ QVERIFY(roleNames.key("type", -1) >= 0);
+ QVERIFY(roleNames.key("age", -1) >= 0);
+ QVERIFY(roleNames.key("size", -1) >= 0);
+
+ QSet<int> roles;
+ roles.insert(roleNames.key("name"));
+ roles.insert(roleNames.key("type"));
+ roles.insert(roleNames.key("age"));
+ roles.insert(roleNames.key("size"));
+ QCOMPARE(roles.size(), 4);
+}
+
+void tst_QQmlXmlListModel::elementErrors()
+{
+ QQmlComponent component(&engine, testFileUrl("elementErrors.qml"));
+ QTest::ignoreMessage(QtWarningMsg,
+ (testFileUrl("elementErrors.qml").toString()
+ + ":6:5: QML XmlListModelRole: An XML element must not start with '/'")
+ .toUtf8()
+ .constData());
+ QTest::ignoreMessage(QtWarningMsg,
+ (testFileUrl("elementErrors.qml").toString()
+ + ":7:5: QML XmlListModelRole: An XML element must not end with '/'")
+ .toUtf8()
+ .constData());
+ QTest::ignoreMessage(QtWarningMsg,
+ (testFileUrl("elementErrors.qml").toString()
+ + ":8:5: QML XmlListModelRole: An XML element must not contain \"//\"")
+ .toUtf8()
+ .constData());
+
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 9);
+
+ QModelIndex index = model->index(3, 0);
+ QCOMPARE(model->data(index, Qt::UserRole).toString(), QString());
+ QCOMPARE(model->data(index, Qt::UserRole + 1).toString(), QString());
+ QCOMPARE(model->data(index, Qt::UserRole + 2).toString(), QString());
+}
+
+void tst_QQmlXmlListModel::uniqueRoleNames()
+{
+ QQmlComponent component(&engine, testFileUrl("unique.qml"));
+ QTest::ignoreMessage(QtWarningMsg,
+ (testFileUrl("unique.qml").toString()
+ + ":7:5: QML XmlListModelRole: \"name\" duplicates a previous role name "
+ "and will be disabled.")
+ .toUtf8()
+ .constData());
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 9);
+
+ QHash<int, QByteArray> roleNames = model->roleNames();
+ QCOMPARE(roleNames.size(), 1);
+}
+
+void tst_QQmlXmlListModel::headers()
+{
+ // ensure the QNetworkAccessManagers created for this test are immediately deleted
+ QQmlEngine qmlEng;
+
+ CustomNetworkAccessManagerFactory factory;
+ qmlEng.setNetworkAccessManagerFactory(&factory);
+
+ QQmlComponent component(&qmlEng, testFileUrl("model.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(qvariant_cast<QQmlXmlListModel::Status>(model->property("status")),
+ QQmlXmlListModel::Ready);
+
+ // It doesn't do a network request for a local file
+ QCOMPARE(factory.lastSentHeaders.size(), 0);
+
+ model->setProperty("source", QUrl("http://localhost/filethatdoesnotexist.xml"));
+ QTRY_COMPARE_WITH_TIMEOUT(qvariant_cast<QQmlXmlListModel::Status>(model->property("status")),
+ QQmlXmlListModel::Error, 10000);
+
+ QVariantMap expectedHeaders;
+ expectedHeaders["Accept"] = "application/xml,*/*";
+
+ QCOMPARE(factory.lastSentHeaders.size(), expectedHeaders.size());
+ for (auto it = expectedHeaders.cbegin(), end = expectedHeaders.cend(); it != end; ++it) {
+ QVERIFY(factory.lastSentHeaders.contains(it.key()));
+ QCOMPARE(factory.lastSentHeaders[it.key()].toString(), it.value().toString());
+ }
+}
+
+void tst_QQmlXmlListModel::source()
+{
+ QFETCH(QUrl, source);
+ QFETCH(int, count);
+ QFETCH(QQmlXmlListModel::Status, status);
+
+ QQmlComponent component(&engine, testFileUrl("model.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QSignalSpy spy(model.get(), SIGNAL(statusChanged(QQmlXmlListModel::Status)));
+
+ QVERIFY(errorString(model.get()).isEmpty());
+ QCOMPARE(model->property("progress").toDouble(), qreal(1.0));
+ QCOMPARE(qvariant_cast<QQmlXmlListModel::Status>(model->property("status")),
+ QQmlXmlListModel::Loading);
+ QTRY_COMPARE(spy.size(), 1);
+ spy.clear();
+ QCOMPARE(qvariant_cast<QQmlXmlListModel::Status>(model->property("status")),
+ QQmlXmlListModel::Ready);
+ QVERIFY(errorString(model.get()).isEmpty());
+ QCOMPARE(model->property("progress").toDouble(), qreal(1.0));
+ QCOMPARE(model->rowCount(), 9);
+
+ model->setProperty("source", source);
+ if (model->property("source").toString().isEmpty())
+ QCOMPARE(qvariant_cast<QQmlXmlListModel::Status>(model->property("status")),
+ QQmlXmlListModel::Null);
+ qreal expectedProgress = (source.isLocalFile() || (source.scheme() == "qrc"_L1)) ? 1.0 : 0.0;
+ QCOMPARE(model->property("progress").toDouble(), expectedProgress);
+ QTRY_COMPARE(spy.size(), 1);
+ spy.clear();
+ QCOMPARE(qvariant_cast<QQmlXmlListModel::Status>(model->property("status")),
+ QQmlXmlListModel::Loading);
+ QVERIFY(errorString(model.get()).isEmpty());
+
+ QEventLoop loop;
+ QTimer timer;
+ timer.setSingleShot(true);
+ connect(model.get(), SIGNAL(statusChanged(QQmlXmlListModel::Status)), &loop, SLOT(quit()));
+ connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
+ timer.start(20000);
+ loop.exec();
+
+ if (spy.size() == 0 && status != QQmlXmlListModel::Ready) {
+ qWarning("QQmlXmlListModel invalid source test timed out");
+ } else {
+ QCOMPARE(spy.size(), 1);
+ spy.clear();
+ }
+
+ QCOMPARE(qvariant_cast<QQmlXmlListModel::Status>(model->property("status")), status);
+ QCOMPARE(model->rowCount(), count);
+
+ if (status == QQmlXmlListModel::Ready)
+ QCOMPARE(model->property("progress").toDouble(), qreal(1.0));
+
+ QCOMPARE(errorString(model.get()).isEmpty(), status == QQmlXmlListModel::Ready);
+}
+
+void tst_QQmlXmlListModel::source_data()
+{
+ QTest::addColumn<QUrl>("source");
+ QTest::addColumn<int>("count");
+ QTest::addColumn<QQmlXmlListModel::Status>("status");
+
+ QTest::newRow("valid") << testFileUrl("model2.xml") << 2 << QQmlXmlListModel::Ready;
+ QTest::newRow("invalid") << QUrl("http://blah.blah/blah.xml") << 0 << QQmlXmlListModel::Error;
+
+ // empty file
+ QTemporaryFile *temp = new QTemporaryFile(this);
+ if (temp->open())
+ QTest::newRow("empty file")
+ << QUrl::fromLocalFile(temp->fileName()) << 0 << QQmlXmlListModel::Ready;
+ temp->close();
+}
+
+void tst_QQmlXmlListModel::data()
+{
+ QQmlComponent component(&engine, testFileUrl("model.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+
+ for (int i = 0; i < 9; i++) {
+ QModelIndex index = model->index(i, 0);
+ for (int j = 0; j < model->roleNames().size(); j++) {
+ QCOMPARE(model->data(index, j), QVariant());
+ }
+ }
+ QTRY_COMPARE(model->rowCount(), 9);
+}
+
+void tst_QQmlXmlListModel::reload()
+{
+ // If no keys are used, the model should be rebuilt from scratch when
+ // reload() is called.
+
+ QQmlComponent component(&engine, testFileUrl("model.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 9);
+
+ QSignalSpy spyInsert(model.get(), SIGNAL(rowsInserted(QModelIndex,int,int)));
+ QSignalSpy spyRemove(model.get(), SIGNAL(rowsRemoved(QModelIndex,int,int)));
+ QSignalSpy spyCount(model.get(), SIGNAL(countChanged()));
+ // reload multiple times to test the xml query aborting
+ QMetaObject::invokeMethod(model.get(), "reload");
+ QMetaObject::invokeMethod(model.get(), "reload");
+ QCoreApplication::processEvents();
+ QMetaObject::invokeMethod(model.get(), "reload");
+ QMetaObject::invokeMethod(model.get(), "reload");
+ QTRY_COMPARE(spyCount.size(), 0);
+ QTRY_COMPARE(spyInsert.size(), 1);
+ QTRY_COMPARE(spyRemove.size(), 1);
+
+ QCOMPARE(spyInsert[0][1].toInt(), 0);
+ QCOMPARE(spyInsert[0][2].toInt(), 8);
+
+ QCOMPARE(spyRemove[0][1].toInt(), 0);
+ QCOMPARE(spyRemove[0][2].toInt(), 8);
+}
+
+void tst_QQmlXmlListModel::threading()
+{
+ QFETCH(int, xmlDataCount);
+
+ QQmlComponent component(&engine, testFileUrl("threading.qml"));
+
+ QScopedPointer<QAbstractItemModel> m1(qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(m1 != nullptr);
+ QScopedPointer<QAbstractItemModel> m2(qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(m2 != nullptr);
+ QScopedPointer<QAbstractItemModel> m3(qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(m3 != nullptr);
+
+ QTemporaryDir tempDir;
+
+ for (int dataCount = 0; dataCount < xmlDataCount; ++dataCount) {
+ QString data1, data2, data3;
+ for (int i = 0; i < dataCount; ++i) {
+ data1 += "name=A" + QString::number(i) + ",age=1" + QString::number(i)
+ + ",sport=Football;";
+ data2 += "name=B" + QString::number(i) + ",age=2" + QString::number(i)
+ + ",sport=Athletics;";
+ data3 += "name=C" + QString::number(i) + ",age=3" + QString::number(i)
+ + ",sport=Curling;";
+ }
+
+ ScopedFile f1(tempDir.filePath("file1.xml"), makeItemXmlAndData(data1).toLatin1());
+ ScopedFile f2(tempDir.filePath("file2.xml"), makeItemXmlAndData(data2).toLatin1());
+ ScopedFile f3(tempDir.filePath("file3.xml"), makeItemXmlAndData(data3).toLatin1());
+ QVERIFY(f1.isCreated() && f2.isCreated() && f3.isCreated());
+
+ m1->setProperty("source", QUrl::fromLocalFile(f1.fileName()));
+ m2->setProperty("source", QUrl::fromLocalFile(f2.fileName()));
+ m3->setProperty("source", QUrl::fromLocalFile(f3.fileName()));
+ QCoreApplication::processEvents();
+
+ QTRY_VERIFY(m1->rowCount() == dataCount && m2->rowCount() == dataCount
+ && m3->rowCount() == dataCount);
+
+ for (int i = 0; i < dataCount; ++i) {
+ QModelIndex index = m1->index(i, 0);
+ QList<int> roles = m1->roleNames().keys();
+ std::sort(roles.begin(), roles.end());
+ QCOMPARE(m1->data(index, roles.at(0)).toString(),
+ QLatin1Char('A') + QString::number(i));
+ QCOMPARE(m1->data(index, roles.at(1)).toString(),
+ QLatin1Char('1') + QString::number(i));
+ QCOMPARE(m1->data(index, roles.at(2)).toString(), QString("Football"));
+
+ index = m2->index(i, 0);
+ roles = m2->roleNames().keys();
+ std::sort(roles.begin(), roles.end());
+ QCOMPARE(m2->data(index, roles.at(0)).toString(),
+ QLatin1Char('B') + QString::number(i));
+ QCOMPARE(m2->data(index, roles.at(1)).toString(),
+ QLatin1Char('2') + QString::number(i));
+ QCOMPARE(m2->data(index, roles.at(2)).toString(), QString("Athletics"));
+
+ index = m3->index(i, 0);
+ roles = m3->roleNames().keys();
+ std::sort(roles.begin(), roles.end());
+ QCOMPARE(m3->data(index, roles.at(0)).toString(),
+ QLatin1Char('C') + QString::number(i));
+ QCOMPARE(m3->data(index, roles.at(1)).toString(),
+ QLatin1Char('3') + QString::number(i));
+ QCOMPARE(m3->data(index, roles.at(2)).toString(), QString("Curling"));
+ }
+
+ // clear sources, so that we could reuse same file names later
+ m1->setProperty("source", QUrl());
+ m2->setProperty("source", QUrl());
+ m3->setProperty("source", QUrl());
+ }
+}
+
+void tst_QQmlXmlListModel::threading_data()
+{
+ QTest::addColumn<int>("xmlDataCount");
+
+ QTest::newRow("1") << 1;
+ QTest::newRow("2") << 2;
+ QTest::newRow("10") << 10;
+}
+
+void tst_QQmlXmlListModel::propertyChanges()
+{
+ QQmlComponent component(&engine, testFileUrl("propertychanges.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 9);
+
+ QObject *role = model->findChild<QObject *>("role");
+ QVERIFY(role);
+
+ QSignalSpy nameSpy(role, SIGNAL(nameChanged()));
+ QSignalSpy elementSpy(role, SIGNAL(elementNameChanged()));
+
+ role->setProperty("name", "size");
+ role->setProperty("elementName", "size");
+
+ QCOMPARE(role->property("name").toString(), QString("size"));
+ QCOMPARE(role->property("elementName").toString(), QString("size"));
+
+ QCOMPARE(nameSpy.size(), 1);
+ QCOMPARE(elementSpy.size(), 1);
+
+ role->setProperty("name", "size");
+ role->setProperty("elementName", "size");
+
+ QCOMPARE(nameSpy.size(), 1);
+ QCOMPARE(elementSpy.size(), 1);
+
+ QSignalSpy sourceSpy(model.get(), SIGNAL(sourceChanged()));
+ QSignalSpy modelQuerySpy(model.get(), SIGNAL(queryChanged()));
+
+ model->setProperty("source", QUrl("model2.xml"));
+ model->setProperty("query", "/Pets");
+
+ QCOMPARE(model->property("source").toUrl(), QUrl("model2.xml"));
+ QCOMPARE(model->property("query").toString(), QString("/Pets"));
+
+ QTRY_COMPARE(model->rowCount(), 1);
+
+ QCOMPARE(sourceSpy.size(), 1);
+ QCOMPARE(modelQuerySpy.size(), 1);
+
+ model->setProperty("source", QUrl("model2.xml"));
+ model->setProperty("query", "/Pets");
+
+ QCOMPARE(sourceSpy.size(), 1);
+ QCOMPARE(modelQuerySpy.size(), 1);
+
+ QTRY_COMPARE(model->rowCount(), 1);
+}
+
+void tst_QQmlXmlListModel::nestedElements()
+{
+ QQmlComponent component(&engine, testFileUrl("nestedElements.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ QTRY_COMPARE(model->rowCount(), 4);
+
+ const QList<QVariantList> desiredResults = { QVariantList { "Polly", "Parrot", 12, "Small" },
+ QVariantList { "Penny", "Turtle", 4, "Small" },
+ QVariantList { "Spot", "Dog", 9, "Medium" },
+ QVariantList { "Tiny", "Elephant", 15, "Large" } };
+
+ QVERIFY(model->rowCount() == desiredResults.size());
+
+ for (qsizetype idx = 0; idx < model->rowCount(); ++idx) {
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole).toString(),
+ desiredResults.at(idx).at(0).toString());
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole + 1).toString(),
+ desiredResults.at(idx).at(1).toString());
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole + 2).toInt(),
+ desiredResults.at(idx).at(2).toInt());
+ QCOMPARE(model->data(model->index(idx, 0), Qt::UserRole + 3).toString(),
+ desiredResults.at(idx).at(3).toString());
+ }
+}
+
+void tst_QQmlXmlListModel::malformedData()
+{
+ QFETCH(QUrl, fileName);
+ QFETCH(QString, errorMessage);
+
+ // In this test we check that malformed xml document would not cause
+ // infinite loop while parsing, and that the errors will be reported.
+
+ QTest::ignoreMessage(
+ QtWarningMsg,
+ (testFileUrl("malformedData.qml").toString() + errorMessage).toUtf8().constData());
+
+ QQmlComponent component(&engine, testFileUrl("malformedData.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+ model->setProperty("source", fileName);
+ QTRY_VERIFY(model->rowCount() != 0);
+}
+
+void tst_QQmlXmlListModel::malformedData_data()
+{
+ QTest::addColumn<QUrl>("fileName");
+ QTest::addColumn<QString>("errorMessage");
+
+ QTest::addRow("tag mismatch top level")
+ << testFileUrl("malformedTagTopLevel.xml")
+ << QStringLiteral(
+ ":3:1: QML XmlListModel: Query error: \"Opening and ending tag mismatch.\"");
+ QTest::addRow("missing tag nested level")
+ << testFileUrl("malformedTagNestedLevel.xml")
+ << QStringLiteral(
+ ":3:1: QML XmlListModel: Query error: \"Opening and ending tag mismatch.\"");
+ QTest::addRow("invalid attribute name")
+ << testFileUrl("malformedAttribute.xml")
+ << QStringLiteral(":3:1: QML XmlListModel: Query error: \"Expected '>' or '/', but got "
+ "'[0-9]'.\"");
+}
+
+void tst_QQmlXmlListModel::roleCrash()
+{
+ // don't crash
+ QQmlComponent component(&engine, testFileUrl("roleCrash.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+}
+
+class SortFilterProxyModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+ Q_PROPERTY(QObject *source READ source WRITE setSource)
+
+public:
+ SortFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) { sort(0); }
+ QObject *source() const { return sourceModel(); }
+ void setSource(QObject *source) { setSourceModel(qobject_cast<QAbstractItemModel *>(source)); }
+};
+
+void tst_QQmlXmlListModel::proxyCrash()
+{
+ qmlRegisterType<SortFilterProxyModel>("SortFilterProxyModel", 1, 0, "SortFilterProxyModel");
+
+ // don't crash
+ QQmlComponent component(&engine, testFileUrl("proxyCrash.qml"));
+ QScopedPointer<QAbstractItemModel> model(
+ qobject_cast<QAbstractItemModel *>(component.create()));
+ QVERIFY(model != nullptr);
+}
+
+QTEST_MAIN(tst_QQmlXmlListModel)
+
+#include "tst_qqmlxmllistmodel.moc"