/**************************************************************************** ** ** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). ** All rights reserved. ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** This file is part of the test suite of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** GNU Lesser General Public License Usage ** This file may be used under the terms of the GNU Lesser General Public ** License version 2.1 as published by the Free Software Foundation and ** appearing in the file LICENSE.LGPL included in the packaging of this ** file. Please review the following information to ensure the GNU Lesser ** General Public License version 2.1 requirements will be met: ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Nokia gives you certain additional ** rights. These rights are described in the Nokia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU General ** Public License version 3.0 as published by the Free Software Foundation ** and appearing in the file LICENSE.GPL included in the packaging of this ** file. Please review the following information to ensure the GNU General ** Public License version 3.0 requirements will be met: ** http://www.gnu.org/copyleft/gpl.html. ** ** Other Usage ** Alternatively, this file may be used in accordance with the terms and ** conditions contained in a signed written agreement between you and Nokia. ** ** ** ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include "../../../shared/util.h" Q_DECLARE_METATYPE(QList) Q_DECLARE_METATYPE(QList) class tst_qdeclarativelistmodel : public QObject { Q_OBJECT public: tst_qdeclarativelistmodel() {} private: int roleFromName(const QDeclarativeListModel *model, const QString &roleName); QSGItem *createWorkerTest(QDeclarativeEngine *eng, QDeclarativeComponent *component, QDeclarativeListModel *model); void waitForWorker(QSGItem *item); private slots: void static_types(); void static_types_data(); void static_i18n(); void static_i18n_data(); void static_nestedElements(); void static_nestedElements_data(); void dynamic_data(); void dynamic(); void dynamic_worker_data(); void dynamic_worker(); void dynamic_worker_sync_data(); void dynamic_worker_sync(); void convertNestedToFlat_fail(); void convertNestedToFlat_fail_data(); void convertNestedToFlat_ok(); void convertNestedToFlat_ok_data(); void enumerate(); void error_data(); void error(); void syncError(); void set(); void get(); void get_data(); void get_worker(); void get_worker_data(); void get_nested(); void get_nested_data(); void crash_model_with_multiple_roles(); void set_model_cache(); void property_changes(); void property_changes_data(); void property_changes_worker(); void property_changes_worker_data(); void clear(); void signal_handlers(); }; int tst_qdeclarativelistmodel::roleFromName(const QDeclarativeListModel *model, const QString &roleName) { QList roles = model->roles(); for (int i=0; itoString(roles[i]) == roleName) return roles[i]; } return -1; } QSGItem *tst_qdeclarativelistmodel::createWorkerTest(QDeclarativeEngine *eng, QDeclarativeComponent *component, QDeclarativeListModel *model) { QSGItem *item = qobject_cast(component->create()); QDeclarativeEngine::setContextForObject(model, eng->rootContext()); if (item) item->setProperty("model", qVariantFromValue(model)); return item; } void tst_qdeclarativelistmodel::waitForWorker(QSGItem *item) { QEventLoop loop; QTimer timer; timer.setSingleShot(true); connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); QDeclarativeProperty prop(item, "done"); QVERIFY(prop.isValid()); QVERIFY(prop.connectNotifySignal(&loop, SLOT(quit()))); timer.start(10000); loop.exec(); QVERIFY(timer.isActive()); } void tst_qdeclarativelistmodel::static_types_data() { QTest::addColumn("qml"); QTest::addColumn("value"); QTest::newRow("string") << "ListElement { foo: \"bar\" }" << QVariant(QString("bar")); QTest::newRow("real") << "ListElement { foo: 10.5 }" << QVariant(10.5); QTest::newRow("real0") << "ListElement { foo: 0 }" << QVariant(double(0)); QTest::newRow("bool") << "ListElement { foo: false }" << QVariant(false); QTest::newRow("bool") << "ListElement { foo: true }" << QVariant(true); QTest::newRow("enum") << "ListElement { foo: Text.AlignHCenter }" << QVariant(double(QSGText::AlignHCenter)); QTest::newRow("Qt enum") << "ListElement { foo: Qt.AlignBottom }" << QVariant(double(Qt::AlignBottom)); } void tst_qdeclarativelistmodel::static_types() { QFETCH(QString, qml); QFETCH(QVariant, value); qml = "import QtQuick 2.0\nItem { property variant test: model.get(0).foo; ListModel { id: model; " + qml + " } }"; QDeclarativeEngine engine; QDeclarativeComponent component(&engine); component.setData(qml.toUtf8(), QUrl::fromLocalFile(QString("dummy.qml"))); QVERIFY(!component.isError()); QObject *obj = component.create(); QVERIFY(obj != 0); QVariant actual = obj->property("test"); QCOMPARE(actual, value); QCOMPARE(actual.toString(), value.toString()); delete obj; } void tst_qdeclarativelistmodel::static_i18n_data() { QTest::addColumn("qml"); QTest::addColumn("value"); QTest::addColumn("error"); QTest::newRow("QT_TR_NOOP") << QString::fromUtf8("ListElement { foo: QT_TR_NOOP(\"na\303\257ve\") }") << QVariant(QString::fromUtf8("na\303\257ve")) << QString(); QTest::newRow("QT_TRANSLATE_NOOP") << "ListElement { foo: QT_TRANSLATE_NOOP(\"MyListModel\", \"hello\") }" << QVariant(QString("hello")) << QString(); QTest::newRow("QT_TRID_NOOP") << QString::fromUtf8("ListElement { foo: QT_TRID_NOOP(\"qtn_1st_text\") }") << QVariant(QString("qtn_1st_text")) << QString(); QTest::newRow("QT_TR_NOOP extra param") << QString::fromUtf8("ListElement { foo: QT_TR_NOOP(\"hello\",\"world\") }") << QVariant(QString()) << QString("ListElement: improperly specified QT_TR_NOOP"); QTest::newRow("QT_TRANSLATE_NOOP missing params") << "ListElement { foo: QT_TRANSLATE_NOOP() }" << QVariant(QString()) << QString("ListElement: improperly specified QT_TRANSLATE_NOOP"); QTest::newRow("QT_TRID_NOOP missing param") << QString::fromUtf8("ListElement { foo: QT_TRID_NOOP() }") << QVariant(QString()) << QString("ListElement: improperly specified QT_TRID_NOOP"); } void tst_qdeclarativelistmodel::static_i18n() { QFETCH(QString, qml); QFETCH(QVariant, value); QFETCH(QString, error); qml = "import QtQuick 2.0\nItem { property variant test: model.get(0).foo; ListModel { id: model; " + qml + " } }"; QDeclarativeEngine engine; QDeclarativeComponent component(&engine); component.setData(qml.toUtf8(), QUrl::fromLocalFile(QString("dummy.qml"))); if (!error.isEmpty()) { QVERIFY(component.isError()); QCOMPARE(component.errors().at(0).description(), error); return; } QVERIFY(!component.isError()); QObject *obj = component.create(); QVERIFY(obj != 0); QVariant actual = obj->property("test"); QCOMPARE(actual, value); QCOMPARE(actual.toString(), value.toString()); delete obj; } void tst_qdeclarativelistmodel::static_nestedElements() { QFETCH(int, elementCount); QStringList elements; for (int i=0; iproperty("count"); QCOMPARE(count.type(), QVariant::Int); QCOMPARE(count.toInt(), elementCount); delete obj; } void tst_qdeclarativelistmodel::static_nestedElements_data() { QTest::addColumn("elementCount"); QTest::newRow("0 items") << 0; QTest::newRow("1 item") << 1; QTest::newRow("2 items") << 2; QTest::newRow("many items") << 5; } void tst_qdeclarativelistmodel::dynamic_data() { QTest::addColumn("script"); QTest::addColumn("result"); QTest::addColumn("warning"); // Simple flat model QTest::newRow("count") << "count" << 0 << ""; QTest::newRow("get1") << "{get(0) === undefined}" << 1 << ""; QTest::newRow("get2") << "{get(-1) === undefined}" << 1 << ""; QTest::newRow("get3") << "{append({'foo':123});get(0) != undefined}" << 1 << ""; QTest::newRow("get4") << "{append({'foo':123});get(0).foo}" << 123 << ""; QTest::newRow("get-modify1") << "{append({'foo':123,'bar':456});get(0).foo = 333;get(0).foo}" << 333 << ""; QTest::newRow("get-modify2") << "{append({'z':1});append({'foo':123,'bar':456});get(1).bar = 999;get(1).bar}" << 999 << ""; QTest::newRow("append1") << "{append({'foo':123});count}" << 1 << ""; QTest::newRow("append2") << "{append({'foo':123,'bar':456});count}" << 1 << ""; QTest::newRow("append3a") << "{append({'foo':123});append({'foo':456});get(0).foo}" << 123 << ""; QTest::newRow("append3b") << "{append({'foo':123});append({'foo':456});get(1).foo}" << 456 << ""; QTest::newRow("append4a") << "{append(123)}" << 0 << ": QML ListModel: append: value is not an object"; QTest::newRow("append4b") << "{append([1,2,3])}" << 0 << ": QML ListModel: append: value is not an object"; QTest::newRow("clear1") << "{append({'foo':456});clear();count}" << 0 << ""; QTest::newRow("clear2") << "{append({'foo':123});append({'foo':456});clear();count}" << 0 << ""; QTest::newRow("clear3") << "{append({'foo':123});clear()}" << 0 << ""; QTest::newRow("remove1") << "{append({'foo':123});remove(0);count}" << 0 << ""; QTest::newRow("remove2a") << "{append({'foo':123});append({'foo':456});remove(0);count}" << 1 << ""; QTest::newRow("remove2b") << "{append({'foo':123});append({'foo':456});remove(0);get(0).foo}" << 456 << ""; QTest::newRow("remove2c") << "{append({'foo':123});append({'foo':456});remove(1);get(0).foo}" << 123 << ""; QTest::newRow("remove3") << "{append({'foo':123});remove(0)}" << 0 << ""; QTest::newRow("remove3a") << "{append({'foo':123});remove(-1);count}" << 1 << ": QML ListModel: remove: index -1 out of range"; QTest::newRow("remove4a") << "{remove(0)}" << 0 << ": QML ListModel: remove: index 0 out of range"; QTest::newRow("remove4b") << "{append({'foo':123});remove(0);remove(0);count}" << 0 << ": QML ListModel: remove: index 0 out of range"; QTest::newRow("remove4c") << "{append({'foo':123});remove(1);count}" << 1 << ": QML ListModel: remove: index 1 out of range"; QTest::newRow("insert1") << "{insert(0,{'foo':123});count}" << 1 << ""; QTest::newRow("insert2") << "{insert(1,{'foo':123});count}" << 0 << ": QML ListModel: insert: index 1 out of range"; QTest::newRow("insert3a") << "{append({'foo':123});insert(1,{'foo':456});count}" << 2 << ""; QTest::newRow("insert3b") << "{append({'foo':123});insert(1,{'foo':456});get(0).foo}" << 123 << ""; QTest::newRow("insert3c") << "{append({'foo':123});insert(1,{'foo':456});get(1).foo}" << 456 << ""; QTest::newRow("insert3d") << "{append({'foo':123});insert(0,{'foo':456});get(0).foo}" << 456 << ""; QTest::newRow("insert3e") << "{append({'foo':123});insert(0,{'foo':456});get(1).foo}" << 123 << ""; QTest::newRow("insert4") << "{append({'foo':123});insert(-1,{'foo':456});count}" << 1 << ": QML ListModel: insert: index -1 out of range"; QTest::newRow("insert5a") << "{insert(0,123)}" << 0 << ": QML ListModel: insert: value is not an object"; QTest::newRow("insert5b") << "{insert(0,[1,2,3])}" << 0 << ": QML ListModel: insert: value is not an object"; QTest::newRow("set1") << "{append({'foo':123});set(0,{'foo':456});count}" << 1 << ""; QTest::newRow("set2") << "{append({'foo':123});set(0,{'foo':456});get(0).foo}" << 456 << ""; QTest::newRow("set3a") << "{append({'foo':123,'bar':456});set(0,{'foo':999});get(0).foo}" << 999 << ""; QTest::newRow("set3b") << "{append({'foo':123,'bar':456});set(0,{'foo':999});get(0).bar}" << 456 << ""; QTest::newRow("set4a") << "{set(0,{'foo':456});count}" << 1 << ""; QTest::newRow("set4c") << "{set(-1,{'foo':456})}" << 0 << ": QML ListModel: set: index -1 out of range"; QTest::newRow("set5a") << "{append({'foo':123,'bar':456});set(0,123);count}" << 1 << ": QML ListModel: set: value is not an object"; QTest::newRow("set5b") << "{append({'foo':123,'bar':456});set(0,[1,2,3]);count}" << 1 << ": QML ListModel: set: value is not an object"; QTest::newRow("set6") << "{append({'foo':123});set(1,{'foo':456});count}" << 2 << ""; QTest::newRow("setprop1") << "{append({'foo':123});setProperty(0,'foo',456);count}" << 1 << ""; QTest::newRow("setprop2") << "{append({'foo':123});setProperty(0,'foo',456);get(0).foo}" << 456 << ""; QTest::newRow("setprop3a") << "{append({'foo':123,'bar':456});setProperty(0,'foo',999);get(0).foo}" << 999 << ""; QTest::newRow("setprop3b") << "{append({'foo':123,'bar':456});setProperty(0,'foo',999);get(0).bar}" << 456 << ""; QTest::newRow("setprop4a") << "{setProperty(0,'foo',456)}" << 0 << ": QML ListModel: set: index 0 out of range"; QTest::newRow("setprop4b") << "{setProperty(-1,'foo',456)}" << 0 << ": QML ListModel: set: index -1 out of range"; QTest::newRow("setprop4c") << "{append({'foo':123,'bar':456});setProperty(1,'foo',456);count}" << 1 << ": QML ListModel: set: index 1 out of range"; QTest::newRow("setprop5") << "{append({'foo':123,'bar':456});append({'foo':111});setProperty(1,'bar',222);get(1).bar}" << 222 << ""; QTest::newRow("move1a") << "{append({'foo':123});append({'foo':456});move(0,1,1);count}" << 2 << ""; QTest::newRow("move1b") << "{append({'foo':123});append({'foo':456});move(0,1,1);get(0).foo}" << 456 << ""; QTest::newRow("move1c") << "{append({'foo':123});append({'foo':456});move(0,1,1);get(1).foo}" << 123 << ""; QTest::newRow("move1d") << "{append({'foo':123});append({'foo':456});move(1,0,1);get(0).foo}" << 456 << ""; QTest::newRow("move1e") << "{append({'foo':123});append({'foo':456});move(1,0,1);get(1).foo}" << 123 << ""; QTest::newRow("move2a") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);count}" << 3 << ""; QTest::newRow("move2b") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);get(0).foo}" << 789 << ""; QTest::newRow("move2c") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);get(1).foo}" << 123 << ""; QTest::newRow("move2d") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);get(2).foo}" << 456 << ""; QTest::newRow("move3a") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(1,0,3);count}" << 3 << ": QML ListModel: move: out of range"; QTest::newRow("move3b") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(1,-1,1);count}" << 3 << ": QML ListModel: move: out of range"; QTest::newRow("move3c") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(1,0,-1);count}" << 3 << ": QML ListModel: move: out of range"; QTest::newRow("move3d") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,3,1);count}" << 3 << ": QML ListModel: move: out of range"; // Nested models QTest::newRow("nested-append1") << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]});count}" << 1 << ""; QTest::newRow("nested-append2") << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]});get(0).bars.get(1).a}" << 2 << ""; QTest::newRow("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 << ""; QTest::newRow("nested-insert") << "{append({'foo':123});insert(0,{'bars':[{'a':1},{'b':2},{'c':3}]});get(0).bars.get(0).a}" << 1 << ""; QTest::newRow("nested-set") << "{append({'foo':[{'x':1}]});set(0,{'foo':[{'x':123}]});get(0).foo.get(0).x}" << 123 << ""; QTest::newRow("nested-count") << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]}); get(0).bars.count}" << 3 << ""; QTest::newRow("nested-clear") << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]}); get(0).bars.clear(); get(0).bars.count}" << 0 << ""; } void tst_qdeclarativelistmodel::dynamic() { QFETCH(QString, script); QFETCH(int, result); QFETCH(QString, warning); QDeclarativeEngine engine; QDeclarativeListModel model; QDeclarativeEngine::setContextForObject(&model,engine.rootContext()); engine.rootContext()->setContextObject(&model); QDeclarativeExpression e(engine.rootContext(), &model, script); if (!warning.isEmpty()) QTest::ignoreMessage(QtWarningMsg, warning.toLatin1()); QSignalSpy spyCount(&model, SIGNAL(countChanged())); int actual = e.evaluate().toInt(); if (e.hasError()) qDebug() << e.error(); // errors not expected QCOMPARE(actual,result); if (model.count() > 0) QVERIFY(spyCount.count() > 0); } void tst_qdeclarativelistmodel::dynamic_worker_data() { dynamic_data(); } void tst_qdeclarativelistmodel::dynamic_worker() { QFETCH(QString, script); QFETCH(int, result); QFETCH(QString, warning); if (QByteArray(QTest::currentDataTag()).startsWith("nested")) return; // This is same as dynamic() except it applies the test to a ListModel called // from a WorkerScript (i.e. testing the internal FlatListModel that is created // by the WorkerListModelAgent) QDeclarativeListModel model; QDeclarativeEngine eng; QDeclarativeComponent component(&eng, QUrl::fromLocalFile(SRCDIR "/data/model.qml")); QSGItem *item = createWorkerTest(&eng, &component, &model); QVERIFY(item != 0); QSignalSpy spyCount(&model, SIGNAL(countChanged())); if (script[0] == QLatin1Char('{') && script[script.length()-1] == QLatin1Char('}')) script = script.mid(1, script.length() - 2); QVariantList operations; foreach (const QString &s, script.split(';')) { if (!s.isEmpty()) operations << s; } if (!warning.isEmpty()) QTest::ignoreMessage(QtWarningMsg, warning.toLatin1()); QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker", Q_ARG(QVariant, operations))); waitForWorker(item); QCOMPARE(QDeclarativeProperty(item, "result").read().toInt(), result); if (model.count() > 0) QVERIFY(spyCount.count() > 0); delete item; qApp->processEvents(); } void tst_qdeclarativelistmodel::dynamic_worker_sync_data() { dynamic_data(); } void tst_qdeclarativelistmodel::dynamic_worker_sync() { QFETCH(QString, script); QFETCH(int, result); QFETCH(QString, warning); // This is the same as dynamic_worker() except that it executes a set of list operations // from the worker script, calls sync(), and tests the changes are reflected in the // list in the main thread QDeclarativeListModel model; QDeclarativeEngine eng; QDeclarativeComponent component(&eng, QUrl::fromLocalFile(SRCDIR "/data/model.qml")); QSGItem *item = createWorkerTest(&eng, &component, &model); QVERIFY(item != 0); if (script[0] == QLatin1Char('{') && script[script.length()-1] == QLatin1Char('}')) script = script.mid(1, script.length() - 2); QVariantList operations; foreach (const QString &s, script.split(';')) { if (!s.isEmpty()) operations << s; } if (!warning.isEmpty()) QTest::ignoreMessage(QtWarningMsg, warning.toLatin1()); // execute a set of commands on the worker list model, then check the // changes are reflected in the list model in the main thread if (QByteArray(QTest::currentDataTag()).startsWith("nested")) QTest::ignoreMessage(QtWarningMsg, ": QML ListModel: Cannot add list-type data when modifying or after modification from a worker script"); if (QByteArray(QTest::currentDataTag()).startsWith("nested-set")) QTest::ignoreMessage(QtWarningMsg, ": QML ListModel: Cannot add list-type data when modifying or after modification from a worker script"); QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker", Q_ARG(QVariant, operations.mid(0, operations.length()-1)))); waitForWorker(item); QDeclarativeExpression e(eng.rootContext(), &model, operations.last().toString()); if (!QByteArray(QTest::currentDataTag()).startsWith("nested")) QCOMPARE(e.evaluate().toInt(), result); delete item; qApp->processEvents(); } #define RUNEVAL(object, string) \ QVERIFY(QMetaObject::invokeMethod(object, "runEval", Q_ARG(QVariant, QString(string)))); inline QVariant runexpr(QDeclarativeEngine *engine, const QString &str) { QDeclarativeExpression expr(engine->rootContext(), 0, str); return expr.evaluate(); } #define RUNEXPR(string) runexpr(&engine, QString(string)) void tst_qdeclarativelistmodel::convertNestedToFlat_fail() { // If a model has nested data, it cannot be used at all from a worker script QFETCH(QString, script); QDeclarativeListModel model; QDeclarativeEngine eng; QDeclarativeComponent component(&eng, QUrl::fromLocalFile(SRCDIR "/data/model.qml")); QSGItem *item = createWorkerTest(&eng, &component, &model); QVERIFY(item != 0); RUNEVAL(item, "model.append({foo: 123})"); RUNEVAL(item, "model.append({foo: [{}, {}]})"); QCOMPARE(model.count(), 2); QTest::ignoreMessage(QtWarningMsg, ": QML ListModel: List contains list-type data and cannot be used from a worker script"); QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker", Q_ARG(QVariant, script))); waitForWorker(item); QCOMPARE(model.count(), 2); delete item; qApp->processEvents(); } void tst_qdeclarativelistmodel::convertNestedToFlat_fail_data() { QTest::addColumn("script"); QTest::newRow("clear") << "clear()"; QTest::newRow("remove") << "remove(0)"; QTest::newRow("append") << "append({'x':1})"; QTest::newRow("insert") << "insert(0, {'x':1})"; QTest::newRow("set") << "set(0, {'foo':1})"; QTest::newRow("setProperty") << "setProperty(0, 'foo', 1})"; QTest::newRow("move") << "move(0, 1, 1})"; QTest::newRow("get") << "get(0)"; } void tst_qdeclarativelistmodel::convertNestedToFlat_ok() { // If a model only has plain data, it can be modified from a worker script. However, // once the model is used from a worker script, it no longer accepts nested data QFETCH(QString, script); QDeclarativeListModel model; QDeclarativeEngine eng; QDeclarativeComponent component(&eng, QUrl::fromLocalFile(SRCDIR "/data/model.qml")); QSGItem *item = createWorkerTest(&eng, &component, &model); QVERIFY(item != 0); RUNEVAL(item, "model.append({foo: 123})"); QCOMPARE(model.count(), 1); QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker", Q_ARG(QVariant, script))); waitForWorker(item); // can still add plain data int count = model.count(); RUNEVAL(item, "model.append({foo: 123})"); QCOMPARE(model.count(), count+1); const char *warning = ": QML ListModel: Cannot add list-type data when modifying or after modification from a worker script"; QTest::ignoreMessage(QtWarningMsg, warning); RUNEVAL(item, "model.append({foo: [{}, {}]})"); QTest::ignoreMessage(QtWarningMsg, warning); RUNEVAL(item, "model.insert(0, {foo: [{}, {}]})"); QTest::ignoreMessage(QtWarningMsg, warning); RUNEVAL(item, "model.set(0, {foo: [{}, {}]})"); QCOMPARE(model.count(), count+1); delete item; qApp->processEvents(); } void tst_qdeclarativelistmodel::convertNestedToFlat_ok_data() { convertNestedToFlat_fail_data(); } void tst_qdeclarativelistmodel::enumerate() { QDeclarativeEngine eng; QDeclarativeComponent component(&eng, QUrl::fromLocalFile(SRCDIR "/data/enumerate.qml")); QVERIFY(!component.isError()); QSGItem *item = qobject_cast(component.create()); QVERIFY(item != 0); QStringList r = item->property("result").toString().split(":"); QCOMPARE(r[0],QLatin1String("val1=1Y")); QCOMPARE(r[1],QLatin1String("val2=2Y")); QCOMPARE(r[2],QLatin1String("val3=strY")); QCOMPARE(r[3],QLatin1String("val4=falseN")); QCOMPARE(r[4],QLatin1String("val5=trueY")); delete item; } void tst_qdeclarativelistmodel::error_data() { QTest::addColumn("qml"); QTest::addColumn("error"); QTest::newRow("id not allowed in ListElement") << "import QtQuick 2.0\nListModel { ListElement { id: fred } }" << "ListElement: cannot use reserved \"id\" property"; QTest::newRow("id allowed in ListModel") << "import QtQuick 2.0\nListModel { id:model }" << ""; QTest::newRow("random properties not allowed in ListModel") << "import QtQuick 2.0\nListModel { foo:123 }" << "ListModel: undefined property 'foo'"; QTest::newRow("random properties allowed in ListElement") << "import QtQuick 2.0\nListModel { ListElement { foo:123 } }" << ""; QTest::newRow("bindings not allowed in ListElement") << "import QtQuick 2.0\nRectangle { id: rect; ListModel { ListElement { foo: rect.color } } }" << "ListElement: cannot use script for property value"; QTest::newRow("random object list properties allowed in ListElement") << "import QtQuick 2.0\nListModel { ListElement { foo: [ ListElement { bar: 123 } ] } }" << ""; QTest::newRow("default properties not allowed in ListElement") << "import QtQuick 2.0\nListModel { ListElement { Item { } } }" << "ListElement: cannot contain nested elements"; QTest::newRow("QML elements not allowed in ListElement") << "import QtQuick 2.0\nListModel { ListElement { a: Item { } } }" << "ListElement: cannot contain nested elements"; QTest::newRow("qualified ListElement supported") << "import QtQuick 2.0 as Foo\nFoo.ListModel { Foo.ListElement { a: 123 } }" << ""; QTest::newRow("qualified ListElement required") << "import QtQuick 2.0 as Foo\nFoo.ListModel { ListElement { a: 123 } }" << "ListElement is not a type"; QTest::newRow("unknown qualified ListElement not allowed") << "import QtQuick 2.0\nListModel { Foo.ListElement { a: 123 } }" << "Foo.ListElement - Foo is not a namespace"; } void tst_qdeclarativelistmodel::error() { QFETCH(QString, qml); QFETCH(QString, error); QDeclarativeEngine engine; QDeclarativeComponent component(&engine); component.setData(qml.toUtf8(), QUrl::fromLocalFile(QString("dummy.qml"))); if (error.isEmpty()) { QVERIFY(!component.isError()); } else { QVERIFY(component.isError()); QList errors = component.errors(); QCOMPARE(errors.count(),1); QCOMPARE(errors.at(0).description(),error); } } void tst_qdeclarativelistmodel::syncError() { QString qml = "import QtQuick 2.0\nListModel { id: lm; Component.onCompleted: lm.sync() }"; QString error = "file:dummy.qml:2:1: QML ListModel: List sync() can only be called from a WorkerScript"; QDeclarativeEngine engine; QDeclarativeComponent component(&engine); component.setData(qml.toUtf8(), QUrl::fromLocalFile(QString("dummy.qml"))); QTest::ignoreMessage(QtWarningMsg,error.toUtf8()); QObject *obj = component.create(); QVERIFY(obj); delete obj; } /* Test model changes from set() are available to the view */ void tst_qdeclarativelistmodel::set() { QDeclarativeEngine engine; QDeclarativeListModel model; QDeclarativeEngine::setContextForObject(&model,engine.rootContext()); engine.rootContext()->setContextProperty("model", &model); RUNEXPR("model.append({test:false})"); RUNEXPR("model.set(0, {test:true})"); QCOMPARE(RUNEXPR("model.get(0).test").toBool(), true); // triggers creation of model cache QCOMPARE(model.data(0, model.roles()[0]), qVariantFromValue(true)); RUNEXPR("model.set(0, {test:false})"); QCOMPARE(RUNEXPR("model.get(0).test").toBool(), false); // tests model cache is updated QCOMPARE(model.data(0, model.roles()[0]), qVariantFromValue(false)); } /* Test model changes on values returned by get() are available to the view */ void tst_qdeclarativelistmodel::get() { QFETCH(QString, expression); QFETCH(int, index); QFETCH(QString, roleName); QFETCH(QVariant, roleValue); QDeclarativeEngine engine; QDeclarativeComponent component(&engine); component.setData( "import QtQuick 2.0\n" "ListModel { \n" "ListElement { roleA: 100 }\n" "ListElement { roleA: 200; roleB: 400 } \n" "ListElement { roleA: 200; roleB: 400 } \n" "}", QUrl()); QDeclarativeListModel *model = qobject_cast(component.create()); int role = roleFromName(model, roleName); QVERIFY(role >= 0); QSignalSpy spy(model, SIGNAL(itemsChanged(int, int, QList))); QDeclarativeExpression expr(engine.rootContext(), model, expression); expr.evaluate(); QVERIFY(!expr.hasError()); QCOMPARE(model->data(index, role), roleValue); QCOMPARE(spy.count(), 1); QList spyResult = spy.takeFirst(); QCOMPARE(spyResult.at(0).toInt(), index); QCOMPARE(spyResult.at(1).toInt(), 1); // only 1 item is modified at a time QCOMPARE(spyResult.at(2).value >(), (QList() << role)); delete model; } void tst_qdeclarativelistmodel::get_data() { QTest::addColumn("expression"); QTest::addColumn("index"); QTest::addColumn("roleName"); QTest::addColumn("roleValue"); QTest::newRow("simple value") << "get(0).roleA = 500" << 0 << "roleA" << QVariant(500); QTest::newRow("simple value 2") << "get(1).roleB = 500" << 1 << "roleB" << QVariant(500); QVariantMap map; map["zzz"] = 123; QTest::newRow("object value") << "get(1).roleB = {'zzz':123}" << 1 << "roleB" << QVariant::fromValue(map); QVariantList list; map.clear(); map["a"] = 50; map["b"] = 500; list << map; map.clear(); map["c"] = 1000; list << map; QTest::newRow("list of objects") << "get(2).roleB = [{'a': 50, 'b': 500}, {'c': 1000}]" << 2 << "roleB" << QVariant::fromValue(list); } void tst_qdeclarativelistmodel::get_worker() { QFETCH(QString, expression); QFETCH(int, index); QFETCH(QString, roleName); QFETCH(QVariant, roleValue); QDeclarativeListModel model; QDeclarativeEngine eng; QDeclarativeComponent component(&eng, QUrl::fromLocalFile(SRCDIR "/data/model.qml")); QSGItem *item = createWorkerTest(&eng, &component, &model); QVERIFY(item != 0); // Add some values like get() test RUNEVAL(item, "model.append({roleA: 100})"); RUNEVAL(item, "model.append({roleA: 200, roleB: 400})"); RUNEVAL(item, "model.append({roleA: 200, roleB: 400})"); int role = roleFromName(&model, roleName); QVERIFY(role >= 0); const char *warning = ": QML ListModel: Cannot add list-type data when modifying or after modification from a worker script"; if (roleValue.type() == QVariant::List || roleValue.type() == QVariant::Map) QTest::ignoreMessage(QtWarningMsg, warning); QSignalSpy spy(&model, SIGNAL(itemsChanged(int, int, QList))); // in the worker thread, change the model data and call sync() QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker", Q_ARG(QVariant, QStringList(expression)))); waitForWorker(item); // see if we receive the model changes in the main thread's model if (roleValue.type() == QVariant::List || roleValue.type() == QVariant::Map) { QVERIFY(model.data(index, role) != roleValue); QCOMPARE(spy.count(), 0); } else { QCOMPARE(model.data(index, role), roleValue); QCOMPARE(spy.count(), 1); QList spyResult = spy.takeFirst(); QCOMPARE(spyResult.at(0).toInt(), index); QCOMPARE(spyResult.at(1).toInt(), 1); // only 1 item is modified at a time QVERIFY(spyResult.at(2).value >().contains(role)); } } void tst_qdeclarativelistmodel::get_worker_data() { get_data(); } /* Test that the tests run in get() also work for nested list data */ void tst_qdeclarativelistmodel::get_nested() { QFETCH(QString, expression); QFETCH(int, index); QFETCH(QString, roleName); QFETCH(QVariant, roleValue); QDeclarativeEngine eng; QDeclarativeComponent component(&eng); component.setData( "import QtQuick 2.0\n" "ListModel { \n" "ListElement {\n" "listRoleA: [\n" "ListElement { roleA: 100 },\n" "ListElement { roleA: 200; roleB: 400 },\n" "ListElement { roleA: 200; roleB: 400 } \n" "]\n" "}\n" "ListElement {\n" "listRoleA: [\n" "ListElement { roleA: 100 },\n" "ListElement { roleA: 200; roleB: 400 },\n" "ListElement { roleA: 200; roleB: 400 } \n" "]\n" "listRoleB: [\n" "ListElement { roleA: 100 },\n" "ListElement { roleA: 200; roleB: 400 },\n" "ListElement { roleA: 200; roleB: 400 } \n" "]\n" "listRoleC: [\n" "ListElement { roleA: 100 },\n" "ListElement { roleA: 200; roleB: 400 },\n" "ListElement { roleA: 200; roleB: 400 } \n" "]\n" "}\n" "}", QUrl()); QDeclarativeListModel *model = qobject_cast(component.create()); QVERIFY(component.errorString().isEmpty()); QDeclarativeListModel *childModel; // Test setting the inner list data for: // get(0).listRoleA // get(1).listRoleA // get(1).listRoleB // get(1).listRoleC QList > testData; testData << qMakePair(0, QString("listRoleA")); testData << qMakePair(1, QString("listRoleA")); testData << qMakePair(1, QString("listRoleB")); testData << qMakePair(1, QString("listRoleC")); for (int i=0; i= 0); childModel = qobject_cast(model->data(outerListIndex, outerListRole).value()); QVERIFY(childModel); QString extendedExpression = QString("get(%1).%2.%3").arg(outerListIndex).arg(outerListRoleName).arg(expression); QDeclarativeExpression expr(eng.rootContext(), model, extendedExpression); QSignalSpy spy(childModel, SIGNAL(itemsChanged(int, int, QList))); expr.evaluate(); QVERIFY(!expr.hasError()); int role = roleFromName(childModel, roleName); QVERIFY(role >= 0); QCOMPARE(childModel->data(index, role), roleValue); QCOMPARE(spy.count(), 1); QList spyResult = spy.takeFirst(); QCOMPARE(spyResult.at(0).toInt(), index); QCOMPARE(spyResult.at(1).toInt(), 1); // only 1 item is modified at a time QCOMPARE(spyResult.at(2).value >(), (QList() << role)); } delete model; } void tst_qdeclarativelistmodel::get_nested_data() { get_data(); } //QTBUG-13754 void tst_qdeclarativelistmodel::crash_model_with_multiple_roles() { QDeclarativeEngine eng; QDeclarativeComponent component(&eng, QUrl::fromLocalFile(SRCDIR "/data/multipleroles.qml")); QObject *rootItem = component.create(); QVERIFY(component.errorString().isEmpty()); QVERIFY(rootItem != 0); QDeclarativeListModel *model = rootItem->findChild("listModel"); QVERIFY(model != 0); // used to cause a crash in QDeclarativeVisualDataModel model->setProperty(0, "black", true); delete rootItem; } //QTBUG-15190 void tst_qdeclarativelistmodel::set_model_cache() { QDeclarativeEngine eng; QDeclarativeComponent component(&eng, QUrl::fromLocalFile(SRCDIR "/data/setmodelcachelist.qml")); QObject *model = component.create(); QVERIFY2(component.errorString().isEmpty(), QTest::toString(component.errorString())); QVERIFY(model != 0); QVERIFY(model->property("ok").toBool()); delete model; } void tst_qdeclarativelistmodel::property_changes() { QFETCH(QString, script_setup); QFETCH(QString, script_change); QFETCH(QString, roleName); QFETCH(int, listIndex); QFETCH(bool, itemsChanged); QFETCH(QString, testExpression); QDeclarativeEngine engine; QDeclarativeListModel model; QDeclarativeEngine::setContextForObject(&model, engine.rootContext()); engine.rootContext()->setContextObject(&model); QDeclarativeExpression expr(engine.rootContext(), &model, script_setup); expr.evaluate(); QVERIFY2(!expr.hasError(), QTest::toString(expr.error().toString())); QString signalHandler = "on" + QString(roleName[0].toUpper()) + roleName.mid(1, roleName.length()) + "Changed:"; QString qml = "import QtQuick 2.0\n" "Connections {\n" "property bool gotSignal: false\n" "target: model.get(" + QString::number(listIndex) + ")\n" + signalHandler + " gotSignal = true\n" "}\n"; QDeclarativeComponent component(&engine); component.setData(qml.toUtf8(), QUrl::fromLocalFile("")); engine.rootContext()->setContextProperty("model", &model); QObject *connectionsObject = component.create(); QVERIFY2(component.errorString().isEmpty(), QTest::toString(component.errorString())); QSignalSpy spyItemsChanged(&model, SIGNAL(itemsChanged(int, int, QList))); expr.setExpression(script_change); expr.evaluate(); QVERIFY2(!expr.hasError(), QTest::toString(expr.error())); // test the object returned by get() emits the correct signals QCOMPARE(connectionsObject->property("gotSignal").toBool(), itemsChanged); // test itemsChanged() is emitted correctly if (itemsChanged) { QCOMPARE(spyItemsChanged.count(), 1); QCOMPARE(spyItemsChanged.at(0).at(0).toInt(), listIndex); QCOMPARE(spyItemsChanged.at(0).at(1).toInt(), 1); } else { QCOMPARE(spyItemsChanged.count(), 0); } expr.setExpression(testExpression); QCOMPARE(expr.evaluate().toBool(), true); delete connectionsObject; } void tst_qdeclarativelistmodel::property_changes_data() { QTest::addColumn("script_setup"); QTest::addColumn("script_change"); QTest::addColumn("roleName"); QTest::addColumn("listIndex"); QTest::addColumn("itemsChanged"); QTest::addColumn("testExpression"); QTest::newRow("set: plain") << "append({'a':123, 'b':456, 'c':789});" << "set(0,{'b':123});" << "b" << 0 << true << "get(0).b == 123"; QTest::newRow("setProperty: plain") << "append({'a':123, 'b':456, 'c':789});" << "setProperty(0, 'b', 123);" << "b" << 0 << true << "get(0).b == 123"; QTest::newRow("set: plain, no changes") << "append({'a':123, 'b':456, 'c':789});" << "set(0,{'b':456});" << "b" << 0 << false << "get(0).b == 456"; QTest::newRow("setProperty: plain, no changes") << "append({'a':123, 'b':456, 'c':789});" << "setProperty(0, 'b', 456);" << "b" << 0 << false << "get(0).b == 456"; QTest::newRow("set: inserted item") << "{append({'a':123, 'b':456, 'c':789}); get(0); insert(0, {'a':0, 'b':0, 'c':0});}" << "set(1, {'a':456});" << "a" << 1 << true << "get(1).a == 456"; QTest::newRow("setProperty: inserted item") << "{append({'a':123, 'b':456, 'c':789}); get(0); insert(0, {'a':0, 'b':0, 'c':0});}" << "setProperty(1, 'a', 456);" << "a" << 1 << true << "get(1).a == 456"; QTest::newRow("get: inserted item") << "{append({'a':123, 'b':456, 'c':789}); get(0); insert(0, {'a':0, 'b':0, 'c':0});}" << "get(1).a = 456;" << "a" << 1 << true << "get(1).a == 456"; QTest::newRow("set: removed item") << "{append({'a':0, 'b':0, 'c':0}); append({'a':123, 'b':456, 'c':789}); get(1); remove(0);}" << "set(0, {'a':456});" << "a" << 0 << true << "get(0).a == 456"; QTest::newRow("setProperty: removed item") << "{append({'a':0, 'b':0, 'c':0}); append({'a':123, 'b':456, 'c':789}); get(1); remove(0);}" << "setProperty(0, 'a', 456);" << "a" << 0 << true << "get(0).a == 456"; QTest::newRow("get: removed item") << "{append({'a':0, 'b':0, 'c':0}); append({'a':123, 'b':456, 'c':789}); get(1); remove(0);}" << "get(0).a = 456;" << "a" << 0 << true << "get(0).a == 456"; // Following tests only call set() since setProperty() only allows plain // values, not lists, as the argument. // Note that when a list is changed, itemsChanged() is currently always // emitted regardless of whether it actually changed or not. QTest::newRow("nested-set: list, new size") << "append({'a':123, 'b':[{'a':1},{'a':2},{'a':3}], 'c':789});" << "set(0,{'b':[{'a':1},{'a':2}]});" << "b" << 0 << true << "get(0).b.get(0).a == 1 && get(0).b.get(1).a == 2"; QTest::newRow("nested-set: list, empty -> non-empty") << "append({'a':123, 'b':[], 'c':789});" << "set(0,{'b':[{'a':1},{'a':2},{'a':3}]});" << "b" << 0 << true << "get(0).b.get(0).a == 1 && get(0).b.get(1).a == 2 && get(0).b.get(2).a == 3"; QTest::newRow("nested-set: list, non-empty -> empty") << "append({'a':123, 'b':[{'a':1},{'a':2},{'a':3}], 'c':789});" << "set(0,{'b':[]});" << "b" << 0 << true << "get(0).b.count == 0"; QTest::newRow("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}]});" << "b" << 0 << true << "get(0).b.get(0).a == 1 && get(0).b.get(1).a == 222 && get(0).b.get(2).a == 3"; QTest::newRow("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}]});" << "b" << 0 << true << "get(0).b.get(0).a == 1 && get(0).b.get(1).a == 2 && get(0).b.get(2).a == 3"; QTest::newRow("nested-set: list, no changes, empty") << "append({'a':123, 'b':[], 'c':789});" << "set(0,{'b':[]});" << "b" << 0 << true << "get(0).b.count == 0"; } void tst_qdeclarativelistmodel::property_changes_worker() { // nested models are not supported when WorkerScript is involved if (QByteArray(QTest::currentDataTag()).startsWith("nested-")) return; QFETCH(QString, script_setup); QFETCH(QString, script_change); QFETCH(QString, roleName); QFETCH(int, listIndex); QFETCH(bool, itemsChanged); QDeclarativeListModel model; QDeclarativeEngine engine; QDeclarativeComponent component(&engine, QUrl::fromLocalFile(SRCDIR "/data/model.qml")); QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8()); QSGItem *item = createWorkerTest(&engine, &component, &model); QVERIFY(item != 0); QDeclarativeExpression expr(engine.rootContext(), &model, script_setup); expr.evaluate(); QVERIFY2(!expr.hasError(), QTest::toString(expr.error().toString())); QSignalSpy spyItemsChanged(&model, SIGNAL(itemsChanged(int, int, QList))); QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker", Q_ARG(QVariant, QStringList(script_change)))); waitForWorker(item); // test itemsChanged() is emitted correctly if (itemsChanged) { QCOMPARE(spyItemsChanged.count(), 1); QCOMPARE(spyItemsChanged.at(0).at(0).toInt(), listIndex); QCOMPARE(spyItemsChanged.at(0).at(1).toInt(), 1); } else { QCOMPARE(spyItemsChanged.count(), 0); } delete item; qApp->processEvents(); } void tst_qdeclarativelistmodel::property_changes_worker_data() { property_changes_data(); } void tst_qdeclarativelistmodel::clear() { QDeclarativeEngine engine; QDeclarativeListModel model; QDeclarativeEngine::setContextForObject(&model, engine.rootContext()); engine.rootContext()->setContextProperty("model", &model); model.clear(); QCOMPARE(model.count(), 0); RUNEXPR("model.append({propertyA: \"value a\", propertyB: \"value b\"})"); QCOMPARE(model.count(), 1); model.clear(); QCOMPARE(model.count(), 0); RUNEXPR("model.append({propertyA: \"value a\", propertyB: \"value b\"})"); RUNEXPR("model.append({propertyA: \"value a\", propertyB: \"value b\"})"); QCOMPARE(model.count(), 2); model.clear(); QCOMPARE(model.count(), 0); // clearing does not remove the roles RUNEXPR("model.append({propertyA: \"value a\", propertyB: \"value b\", propertyC: \"value c\"})"); QList roles = model.roles(); model.clear(); QCOMPARE(model.count(), 0); QCOMPARE(model.roles(), roles); QCOMPARE(model.toString(roles[0]), QString("propertyA")); QCOMPARE(model.toString(roles[1]), QString("propertyB")); QCOMPARE(model.toString(roles[2]), QString("propertyC")); } void tst_qdeclarativelistmodel::signal_handlers() { QDeclarativeEngine eng; QDeclarativeComponent component(&eng, QUrl::fromLocalFile(SRCDIR "/data/signalhandlers.qml")); QObject *model = component.create(); QVERIFY2(component.errorString().isEmpty(), QTest::toString(component.errorString())); QVERIFY(model != 0); QVERIFY(model->property("ok").toBool()); delete model; } QTEST_MAIN(tst_qdeclarativelistmodel) #include "tst_qdeclarativelistmodel.moc"