diff options
Diffstat (limited to 'tests')
33 files changed, 2598 insertions, 58 deletions
diff --git a/tests/auto/qml/ecmascripttests/TestExpectations b/tests/auto/qml/ecmascripttests/TestExpectations index cf7bf3d8ba..0e0d70845b 100644 --- a/tests/auto/qml/ecmascripttests/TestExpectations +++ b/tests/auto/qml/ecmascripttests/TestExpectations @@ -211,7 +211,6 @@ built-ins/Promise/prototype/then/ctor-throws.js fails built-ins/Promise/race/ctx-ctor.js fails built-ins/Proxy/ownKeys/return-duplicate-entries-throws.js fails built-ins/Proxy/ownKeys/return-duplicate-symbol-entries-throws.js fails -built-ins/RegExp/S15.10.2.12_A2_T1.js fails built-ins/RegExp/prototype/Symbol.match/builtin-success-u-return-val-groups.js fails built-ins/RegExp/prototype/Symbol.split/species-ctor.js fails built-ins/RegExp/prototype/exec/S15.10.6.2_A5_T3.js fails @@ -219,7 +218,6 @@ built-ins/RegExp/prototype/exec/failure-lastindex-access.js fails built-ins/RegExp/prototype/exec/success-lastindex-access.js fails built-ins/RegExp/prototype/source/value-line-terminator.js fails built-ins/RegExp/prototype/test/S15.10.6.3_A1_T22.js fails -built-ins/RegExp/u180e.js fails built-ins/RegExp/unicode_restricted_brackets.js fails built-ins/RegExp/unicode_restricted_character_class_escape.js fails built-ins/RegExp/unicode_restricted_identity_escape.js fails diff --git a/tests/auto/qml/qjsengine/tst_qjsengine.cpp b/tests/auto/qml/qjsengine/tst_qjsengine.cpp index fb9c7b0152..01b9465f58 100644 --- a/tests/auto/qml/qjsengine/tst_qjsengine.cpp +++ b/tests/auto/qml/qjsengine/tst_qjsengine.cpp @@ -66,6 +66,10 @@ private slots: void newArray(); void newArray_HooliganTask218092(); void newArray_HooliganTask233836(); + void toScriptValue_data(); + void toScriptValue(); + void toScriptValuenotroundtripped_data(); + void toScriptValuenotroundtripped(); void newVariant(); void newVariant_valueOfToString(); void newVariant_valueOfEnum(); @@ -94,6 +98,7 @@ private slots: void valueConversion_basic2(); void valueConversion_dateTime(); void valueConversion_regExp(); + void valueConversion_RegularExpression(); void castWithMultipleInheritance(); void collectGarbage(); void gcWithNestedDataStructure(); @@ -135,6 +140,8 @@ private slots: void qRegExpInport_data(); void qRegExpInport(); + void qRegularExpressionImport_data(); + void qRegularExpressionImport(); void dateRoundtripJSQtJS(); void dateRoundtripQtJSQt(); void dateConversionJSQt(); @@ -478,17 +485,97 @@ void tst_QJSEngine::newArray_HooliganTask233836() } } +void tst_QJSEngine::toScriptValue_data() +{ + QTest::addColumn<QVariant>("input"); + + QTest::newRow("UnknownType") << QVariant(int(QMetaType::UnknownType), nullptr); + QTest::newRow("Nullptr") << QVariant(int(QMetaType::Nullptr), nullptr); + QTest::newRow("true") << QVariant(true); + QTest::newRow("false") << QVariant(false); + QTest::newRow("int") << QVariant(int(42)); + QTest::newRow("uint") << QVariant(uint(42)); + QTest::newRow("longlong") << QVariant(qlonglong(4242)); + QTest::newRow("ulonglong") << QVariant(qulonglong(4242)); + QTest::newRow("double") << QVariant(double(42.42)); + QTest::newRow("float") << QVariant(float(42.42)); + QTest::newRow("qstring") << QVariant(QString::fromLatin1("hello")); + QTest::newRow("qbytearray") << QVariant(QByteArray("hello")); + QTest::newRow("short") << QVariant(short('r')); + QTest::newRow("ushort") << QVariant(short('b')); + QTest::newRow("char") << QVariant(char('r')); + QTest::newRow("uchar") << QVariant(uchar('b')); + QTest::newRow("qchar") << QVariant(QString::fromUtf8("å").at(0)); + QTest::newRow("qdate") << QVariant(QDate(1925, 5, 8)); + QTest::newRow("qtime") << QVariant(QTime(4, 5, 6)); + QTest::newRow("qregularexpression") << QVariant(QRegularExpression(".*")); + QTest::newRow("qpointf") << QVariant(QPointF(42, 24)); + QTest::newRow("qvariantlist") << QVariant(QVariantList() << 42.24 << 5 << "hello"); + QTest::newRow("qvariantlist_point") << QVariant(QVariantList() << 42.24 << QPointF(42.24, 24.42) << QPointF(24.42, 42.24)); + QVariantMap vm; vm.insert("test", 55); vm.insert("abc", 42.42);; + QTest::newRow("qvariantmap") << QVariant(vm); + vm.clear(); vm.insert("point1", QPointF(42.24, 24.42)); vm.insert("point2", QPointF(42.24, 24.42)); + QTest::newRow("qvariantmap_point") << QVariant(vm); + QTest::newRow("qvariant") << QVariant(QVariant(42)); + QTest::newRow("QList<int>") << QVariant::fromValue(QList<int>() << 1 << 2 << 3 << 4); + QTest::newRow("QVector<int>") << QVariant::fromValue(QVector<int>() << 1 << 2 << 3 << 4); + QTest::newRow("QList<QString>") << QVariant::fromValue(QVector<QString>() << "1" << "2" << "3" << "4"); + QTest::newRow("QStringList") << QVariant::fromValue(QStringList() << "1" << "2" << "3" << "4"); + QTest::newRow("QMap<QString, QString>") << QVariant::fromValue(QMap<QString, QString>{{ "1", "2" }, { "3", "4" }}); + QTest::newRow("QHash<QString, QString>") << QVariant::fromValue(QHash<QString, QString>{{ "1", "2" }, { "3", "4" }}); + QTest::newRow("QMap<QString, QPointF>") << QVariant::fromValue(QMap<QString, QPointF>{{ "1", { 42.24, 24.42 } }, { "3", { 24.42, 42.24 } }}); + QTest::newRow("QHash<QString, QPointF>") << QVariant::fromValue(QHash<QString, QPointF>{{ "1", { 42.24, 24.42 } }, { "3", { 24.42, 42.24 } }}); +} + +void tst_QJSEngine::toScriptValue() +{ + QFETCH(QVariant, input); + + QJSEngine engine; + QJSValue outputJS = engine.toScriptValue(input); + QVariant output = engine.fromScriptValue<QVariant>(outputJS); + + QCOMPARE(input, output); +} + +void tst_QJSEngine::toScriptValuenotroundtripped_data() +{ + QTest::addColumn<QVariant>("input"); + QTest::addColumn<QVariant>("output"); + + QTest::newRow("QList<QObject*>") << QVariant::fromValue(QList<QObject*>() << this) << QVariant(QVariantList() << QVariant::fromValue(this)); + QTest::newRow("QObjectList") << QVariant::fromValue(QObjectList() << this) << QVariant(QVariantList() << QVariant::fromValue(this)); + QTest::newRow("QList<QPoint>") << QVariant::fromValue(QList<QPointF>() << QPointF(42.24, 24.42) << QPointF(42.24, 24.42)) << QVariant(QVariantList() << QPointF(42.24, 24.42) << QPointF(42.24, 24.42)); + QTest::newRow("QVector<QPoint>") << QVariant::fromValue(QVector<QPointF>() << QPointF(42.24, 24.42) << QPointF(42.24, 24.42)) << QVariant(QVariantList() << QPointF(42.24, 24.42) << QPointF(42.24, 24.42)); + QTest::newRow("VoidStar") << QVariant(int(QMetaType::VoidStar), nullptr) << QVariant(int(QMetaType::Nullptr), nullptr); + QTest::newRow("qregex") << QVariant(QRegExp(".*", Qt::CaseSensitive, QRegExp::RegExp2)) << QVariant(QRegularExpression(".*")); +} + +// This is almost the same as toScriptValue, but the inputs don't roundtrip to +// exactly the same value. +void tst_QJSEngine::toScriptValuenotroundtripped() +{ + QFETCH(QVariant, input); + QFETCH(QVariant, output); + + QJSEngine engine; + QJSValue outputJS = engine.toScriptValue(input); + QVariant actualOutput = engine.fromScriptValue<QVariant>(outputJS); + + QCOMPARE(actualOutput, output); +} + void tst_QJSEngine::newVariant() { QJSEngine eng; { QJSValue opaque = eng.toScriptValue(QVariant(QPoint(1, 2))); QVERIFY(!opaque.isUndefined()); - QCOMPARE(opaque.isVariant(), true); + QCOMPARE(opaque.isVariant(), false); QVERIFY(!opaque.isCallable()); QCOMPARE(opaque.isObject(), true); QVERIFY(!opaque.prototype().isUndefined()); - QCOMPARE(opaque.prototype().isVariant(), true); + QCOMPARE(opaque.prototype().isVariant(), false); QVERIFY(opaque.property("valueOf").callWithInstance(opaque).equals(opaque)); } } @@ -502,7 +589,7 @@ void tst_QJSEngine::newVariant_valueOfToString() QJSValue value = object.property("valueOf").callWithInstance(object); QVERIFY(value.isObject()); QVERIFY(value.strictlyEquals(object)); - QCOMPARE(object.toString(), QString::fromLatin1("QVariant(QPoint, QPoint(10,20))")); + QCOMPARE(object.toString(), QString::fromLatin1("QPoint(10, 20)")); } } @@ -520,22 +607,27 @@ void tst_QJSEngine::newVariant_valueOfEnum() void tst_QJSEngine::newRegExp() { QJSEngine eng; - QJSValue rexp = eng.toScriptValue(QRegExp("foo")); - QVERIFY(!rexp.isUndefined()); - QCOMPARE(rexp.isRegExp(), true); - QCOMPARE(rexp.isObject(), true); - QCOMPARE(rexp.isCallable(), false); - // prototype should be RegExp.prototype - QVERIFY(!rexp.prototype().isUndefined()); - QCOMPARE(rexp.prototype().isObject(), true); - // Get [[Class]] internal property of RegExp Prototype Object. - // See ECMA-262 Section 8.6.2, "Object Internal Properties and Methods". - // See ECMA-262 Section 15.10.6, "Properties of the RegExp Prototype Object". - QJSValue r = eng.evaluate("Object.prototype.toString.call(RegExp.prototype)"); - QCOMPARE(r.toString(), QString::fromLatin1("[object Object]")); - QCOMPARE(rexp.prototype().strictlyEquals(eng.evaluate("RegExp.prototype")), true); + QJSValue rexps[] = { + eng.toScriptValue(QRegularExpression("foo")), + eng.toScriptValue(QRegExp("foo")) + }; + for (const auto &rexp : rexps) { + QVERIFY(!rexp.isUndefined()); + QCOMPARE(rexp.isRegExp(), true); + QCOMPARE(rexp.isObject(), true); + QCOMPARE(rexp.isCallable(), false); + // prototype should be RegExp.prototype + QVERIFY(!rexp.prototype().isUndefined()); + QCOMPARE(rexp.prototype().isObject(), true); + // Get [[Class]] internal property of RegExp Prototype Object. + // See ECMA-262 Section 8.6.2, "Object Internal Properties and Methods". + // See ECMA-262 Section 15.10.6, "Properties of the RegExp Prototype Object". + QJSValue r = eng.evaluate("Object.prototype.toString.call(RegExp.prototype)"); + QCOMPARE(r.toString(), QString::fromLatin1("[object Object]")); + QCOMPARE(rexp.prototype().strictlyEquals(eng.evaluate("RegExp.prototype")), true); - QCOMPARE(qjsvalue_cast<QRegExp>(rexp).pattern(), QRegExp("foo").pattern()); + QCOMPARE(qjsvalue_cast<QRegExp>(rexp).pattern(), QRegExp("foo").pattern()); + } } void tst_QJSEngine::jsRegExp() @@ -1495,7 +1587,7 @@ void tst_QJSEngine::valueConversion_QVariant() { QVariant var = qVariantFromValue(QPoint(123, 456)); QJSValue val = eng.toScriptValue(var); - QVERIFY(val.isVariant()); + QVERIFY(!val.isVariant()); QCOMPARE(val.toVariant(), var); } @@ -1601,6 +1693,28 @@ void tst_QJSEngine::valueConversion_regExp() } } +void tst_QJSEngine::valueConversion_RegularExpression() +{ + QJSEngine eng; + { + QRegularExpression in = QRegularExpression("foo"); + QJSValue val = eng.toScriptValue(in); + QVERIFY(val.isRegExp()); + QRegularExpression out = qjsvalue_cast<QRegularExpression>(val); + QCOMPARE(out.pattern(), in.pattern()); + QCOMPARE(out.patternOptions(), in.patternOptions()); + } + { + QRegularExpression in = QRegularExpression("foo", + QRegularExpression::CaseInsensitiveOption); + QJSValue val = eng.toScriptValue(in); + QVERIFY(val.isRegExp()); + QCOMPARE(qjsvalue_cast<QRegularExpression>(val), in); + QRegularExpression out = qjsvalue_cast<QRegularExpression>(val); + QCOMPARE(out.patternOptions(), in.patternOptions()); + } +} + Q_DECLARE_METATYPE(QGradient) Q_DECLARE_METATYPE(QGradient*) Q_DECLARE_METATYPE(QLinearGradient) @@ -2950,6 +3064,8 @@ void tst_QJSEngine::reentrancy_objectCreation() QJSValue r2 = eng2.evaluate("new RegExp('foo', 'gim')"); QCOMPARE(qjsvalue_cast<QRegExp>(r1), qjsvalue_cast<QRegExp>(r2)); QCOMPARE(qjsvalue_cast<QRegExp>(r2), qjsvalue_cast<QRegExp>(r1)); + QCOMPARE(qjsvalue_cast<QRegularExpression>(r1), qjsvalue_cast<QRegularExpression>(r2)); + QCOMPARE(qjsvalue_cast<QRegularExpression>(r2), qjsvalue_cast<QRegularExpression>(r1)); } { QJSValue o1 = eng1.newQObject(temp); @@ -3145,6 +3261,56 @@ void tst_QJSEngine::qRegExpInport() } } +void tst_QJSEngine::qRegularExpressionImport_data() +{ + QTest::addColumn<QRegularExpression>("rx"); + QTest::addColumn<QString>("string"); + QTest::addColumn<QString>("matched"); + + QTest::newRow("normal") << QRegularExpression("(test|foo)") << "test _ foo _ test _ Foo"; + QTest::newRow("normal2") << QRegularExpression("(Test|Foo)") << "test _ foo _ test _ Foo"; + QTest::newRow("case insensitive") << QRegularExpression("(test|foo)", QRegularExpression::CaseInsensitiveOption) << "test _ foo _ test _ Foo"; + QTest::newRow("case insensitive2") << QRegularExpression("(Test|Foo)", QRegularExpression::CaseInsensitiveOption) << "test _ foo _ test _ Foo"; + QTest::newRow("b(a*)(b*)") << QRegularExpression("b(a*)(b*)", QRegularExpression::CaseInsensitiveOption) << "aaabbBbaAabaAaababaaabbaaab"; + QTest::newRow("greedy") << QRegularExpression("a*(a*)", QRegularExpression::CaseInsensitiveOption) << "aaaabaaba"; + QTest::newRow("wildcard") << QRegularExpression(".*\\.txt") << "file.txt"; + QTest::newRow("wildcard 2") << QRegularExpression("a.b\\.txt") << "ab.txt abb.rtc acb.txt"; + QTest::newRow("slash") << QRegularExpression("g/.*/s", QRegularExpression::CaseInsensitiveOption) << "string/string/string"; + QTest::newRow("slash2") << QRegularExpression("g / .* / s", QRegularExpression::CaseInsensitiveOption) << "string / string / string"; + QTest::newRow("fixed") << QRegularExpression("a\\*aa\\.a\\(ba\\)\\*a\\\\ba", QRegularExpression::CaseInsensitiveOption) << "aa*aa.a(ba)*a\\ba"; + QTest::newRow("fixed insensitive") << QRegularExpression("A\\*A", QRegularExpression::CaseInsensitiveOption) << "a*A A*a A*A a*a"; + QTest::newRow("fixed sensitive") << QRegularExpression("A\\*A") << "a*A A*a A*A a*a"; + QTest::newRow("html") << QRegularExpression("<b>(.*)</b>") << "<b>bold</b><i>italic</i><b>bold</b>"; + QTest::newRow("html minimal") << QRegularExpression("^<b>(.*)</b>$") << "<b>bold</b><i>italic</i><b>bold</b>"; + QTest::newRow("aaa") << QRegularExpression("a{2,5}") << "aAaAaaaaaAa"; + QTest::newRow("aaa minimal") << QRegularExpression("^a{2,5}$") << "aAaAaaaaaAa"; + QTest::newRow("minimal") << QRegularExpression("^.*\\} [*8]$") << "}?} ?} *"; + QTest::newRow(".? minimal") << QRegularExpression("^.?$") << ".?"; + QTest::newRow(".+ minimal") << QRegularExpression("^.+$") << ".+"; + QTest::newRow("[.?] minimal") << QRegularExpression("^[.?]$") << ".?"; + QTest::newRow("[.+] minimal") << QRegularExpression("^[.+]$") << ".+"; +} + +void tst_QJSEngine::qRegularExpressionImport() +{ + QFETCH(QRegularExpression, rx); + QFETCH(QString, string); + + QJSEngine eng; + QJSValue rexp; + rexp = eng.toScriptValue(rx); + + QCOMPARE(rexp.isRegExp(), true); + QCOMPARE(rexp.isCallable(), false); + + QJSValue func = eng.evaluate("(function(string, regexp) { return string.match(regexp); })"); + QJSValue result = func.call(QJSValueList() << string << rexp); + + const QRegularExpressionMatch match = rx.match(string); + for (int i = 0; i <= match.lastCapturedIndex(); i++) + QCOMPARE(result.property(i).toString(), match.captured(i)); +} + // QScriptValue::toDateTime() returns a local time, whereas JS dates // are always stored as UTC. Qt Script must respect the current time // zone, and correctly adjust for daylight saving time that may be in diff --git a/tests/auto/qml/qjsvalue/tst_qjsvalue.cpp b/tests/auto/qml/qjsvalue/tst_qjsvalue.cpp index b58cd98d1e..2b80970559 100644 --- a/tests/auto/qml/qjsvalue/tst_qjsvalue.cpp +++ b/tests/auto/qml/qjsvalue/tst_qjsvalue.cpp @@ -404,8 +404,8 @@ void tst_QJSValue::toString() // variant should use internal valueOf(), then fall back to QVariant::toString(), // then fall back to "QVariant(typename)" QJSValue variant = eng.toScriptValue(QPoint(10, 20)); - QVERIFY(variant.isVariant()); - QCOMPARE(variant.toString(), QString::fromLatin1("QVariant(QPoint, QPoint(10,20))")); + QVERIFY(!variant.isVariant()); + QCOMPARE(variant.toString(), QString::fromLatin1("QPoint(10, 20)")); variant = eng.toScriptValue(QUrl()); QVERIFY(variant.isVariant()); QVERIFY(variant.toString().isEmpty()); @@ -1027,6 +1027,20 @@ void tst_QJSValue::toVariant() QJSValue rxObject = eng.toScriptValue(rx); QVERIFY(rxObject.isRegExp()); QVariant var = rxObject.toVariant(); + + // We can't roundtrip a QRegExp this way, as toVariant() has no information on whether we + // want QRegExp or QRegularExpression. It will always create a QRegularExpression. + QCOMPARE(var.type(), QMetaType::QRegularExpression); + QRegularExpression result = var.toRegularExpression(); + QCOMPARE(result.pattern(), rx.pattern()); + QCOMPARE(result.patternOptions() & QRegularExpression::CaseInsensitiveOption, 0); + } + + { + QRegularExpression rx = QRegularExpression("[0-9a-z]+"); + QJSValue rxObject = eng.toScriptValue(rx); + QVERIFY(rxObject.isRegExp()); + QVariant var = rxObject.toVariant(); QCOMPARE(var, QVariant(rx)); } @@ -1194,6 +1208,32 @@ void tst_QJSValue::toRegExp() QVERIFY(qjsvalue_cast<QRegExp>(eng.toScriptValue(QVariant())).isEmpty()); } +void tst_QJSValue::toRegularExpression() +{ + QJSEngine eng; + { + QRegularExpression rx = qjsvalue_cast<QRegularExpression>(eng.evaluate("/foo/")); + QVERIFY(rx.isValid()); + QCOMPARE(rx.pattern(), QString::fromLatin1("foo")); + QVERIFY(!(rx.patternOptions() & QRegularExpression::CaseInsensitiveOption)); + } + { + QRegularExpression rx = qjsvalue_cast<QRegularExpression>(eng.evaluate("/bar/gi")); + QVERIFY(rx.isValid()); + QCOMPARE(rx.pattern(), QString::fromLatin1("bar")); + QVERIFY(rx.patternOptions() & QRegularExpression::CaseInsensitiveOption); + } + + QVERIFY(qjsvalue_cast<QRegularExpression>(eng.evaluate("[]")).pattern().isEmpty()); + QVERIFY(qjsvalue_cast<QRegularExpression>(eng.evaluate("{}")).pattern().isEmpty()); + QVERIFY(qjsvalue_cast<QRegularExpression>(eng.globalObject()).pattern().isEmpty()); + QVERIFY(qjsvalue_cast<QRegularExpression>(QJSValue()).pattern().isEmpty()); + QVERIFY(qjsvalue_cast<QRegularExpression>(QJSValue(123)).pattern().isEmpty()); + QVERIFY(qjsvalue_cast<QRegularExpression>(QJSValue(false)).pattern().isEmpty()); + QVERIFY(qjsvalue_cast<QRegularExpression>(eng.evaluate("null")).pattern().isEmpty()); + QVERIFY(qjsvalue_cast<QRegularExpression>(eng.toScriptValue(QVariant())).pattern().isEmpty()); +} + void tst_QJSValue::isArray_data() { newEngine(); @@ -2248,8 +2288,8 @@ void tst_QJSValue::strictlyEquals() { QJSValue var1 = eng.toScriptValue(QVariant(QStringList() << "a")); QJSValue var2 = eng.toScriptValue(QVariant(QStringList() << "a")); - QVERIFY(var1.isArray()); - QVERIFY(var2.isArray()); + QVERIFY(!var1.isArray()); + QVERIFY(!var2.isArray()); QVERIFY(!var1.strictlyEquals(var2)); } { diff --git a/tests/auto/qml/qjsvalue/tst_qjsvalue.h b/tests/auto/qml/qjsvalue/tst_qjsvalue.h index b8b9f4403c..9532b1f10e 100644 --- a/tests/auto/qml/qjsvalue/tst_qjsvalue.h +++ b/tests/auto/qml/qjsvalue/tst_qjsvalue.h @@ -76,6 +76,7 @@ private slots: void toQObject(); void toDateTime(); void toRegExp(); + void toRegularExpression(); void isArray_data(); void isArray(); void isDate(); diff --git a/tests/auto/qml/qml.pro b/tests/auto/qml/qml.pro index 5448088ee5..86f36286d9 100644 --- a/tests/auto/qml/qml.pro +++ b/tests/auto/qml/qml.pro @@ -70,6 +70,7 @@ PRIVATETESTS += \ qqmltranslation \ qqmlimport \ qqmlobjectmodel \ + qqmltablemodel \ qv4assembler \ qv4mm \ qv4identifiertable \ diff --git a/tests/auto/qml/qqmlecmascript/data/regularExpression.2.qml b/tests/auto/qml/qqmlecmascript/data/regularExpression.2.qml new file mode 100644 index 0000000000..b22f8ab71e --- /dev/null +++ b/tests/auto/qml/qqmlecmascript/data/regularExpression.2.qml @@ -0,0 +1,7 @@ +import Qt.test 1.0 + +MyQmlObject{ + id: obj + objectName: "obj" + regularExpression: "[a-zA-z]" +} diff --git a/tests/auto/qml/qqmlecmascript/data/regularExpression.qml b/tests/auto/qml/qqmlecmascript/data/regularExpression.qml new file mode 100644 index 0000000000..6f31ffd305 --- /dev/null +++ b/tests/auto/qml/qqmlecmascript/data/regularExpression.qml @@ -0,0 +1,7 @@ +import Qt.test 1.0 + +MyQmlObject{ + id: obj + objectName: "obj" + regularExpression: /[a-zA-z]/ +} diff --git a/tests/auto/qml/qqmlecmascript/testtypes.h b/tests/auto/qml/qqmlecmascript/testtypes.h index 4547a74470..32120ee5b7 100644 --- a/tests/auto/qml/qqmlecmascript/testtypes.h +++ b/tests/auto/qml/qqmlecmascript/testtypes.h @@ -33,6 +33,7 @@ #include <QtQml/qqmlexpression.h> #include <QtCore/qpoint.h> #include <QtCore/qsize.h> +#include <QtCore/qregularexpression.h> #include <QtQml/qqmllist.h> #include <QtCore/qrect.h> #include <QtGui/qmatrix.h> @@ -101,6 +102,7 @@ class MyQmlObject : public QObject Q_PROPERTY(QQmlListProperty<QObject> objectListProperty READ objectListProperty CONSTANT) Q_PROPERTY(int resettableProperty READ resettableProperty WRITE setResettableProperty RESET resetProperty) Q_PROPERTY(QRegExp regExp READ regExp WRITE setRegExp) + Q_PROPERTY(QRegularExpression regularExpression READ regularExpression WRITE setRegularExpression) Q_PROPERTY(int nonscriptable READ nonscriptable WRITE setNonscriptable SCRIPTABLE false) Q_PROPERTY(int intProperty READ intProperty WRITE setIntProperty NOTIFY intChanged) Q_PROPERTY(QJSValue qjsvalue READ qjsvalue WRITE setQJSValue NOTIFY qjsvalueChanged) @@ -170,6 +172,12 @@ public: QRegExp regExp() { return m_regExp; } void setRegExp(const QRegExp ®Exp) { m_regExp = regExp; } + QRegularExpression regularExpression() { return m_regularExpression; } + void setRegularExpression(const QRegularExpression ®ularExpression) + { + m_regularExpression = regularExpression; + } + int console() const { return 11; } int nonscriptable() const { return 0; } @@ -270,6 +278,7 @@ private: int m_value; int m_resetProperty; QRegExp m_regExp; + QRegularExpression m_regularExpression; QVariant m_variant; QJSValue m_qjsvalue; int m_intProperty; diff --git a/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp b/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp index 3cd70a0d1b..69609fad61 100644 --- a/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp +++ b/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp @@ -2420,6 +2420,13 @@ void tst_qqmlecmascript::regExpBug() delete object; } + { + QQmlComponent component(&engine, testFileUrl("regularExpression.qml")); + QScopedPointer<MyQmlObject> object(qobject_cast<MyQmlObject*>(component.create())); + QVERIFY(!object.isNull()); + QCOMPARE(object->regularExpression().pattern(), QLatin1String("[a-zA-z]")); + } + //QTBUG-23068 { QString err = QString(QLatin1String("%1:6 Invalid property assignment: regular expression expected; use /pattern/ syntax\n")).arg(testFileUrl("regExp.2.qml").toString()); @@ -2429,6 +2436,18 @@ void tst_qqmlecmascript::regExpBug() QVERIFY(!object); QCOMPARE(component.errorString(), err); } + + { + const QString err = QString::fromLatin1("%1:6 Invalid property assignment: " + "regular expression expected; " + "use /pattern/ syntax\n") + .arg(testFileUrl("regularExpression.2.qml").toString()); + QQmlComponent component(&engine, testFileUrl("regularExpression.2.qml")); + QTest::ignoreMessage(QtWarningMsg, "QQmlComponent: Component is not ready"); + MyQmlObject *object = qobject_cast<MyQmlObject*>(component.create()); + QVERIFY(!object); + QCOMPARE(component.errorString(), err); + } } static inline bool evaluate_error(QV4::ExecutionEngine *v4, const QV4::Value &o, const char *source) @@ -2483,7 +2502,7 @@ static inline bool evaluate_value(QV4::ExecutionEngine *v4, const QV4::Value &o, scope.engine->catchException(); return false; } - return QV4::Runtime::method_strictEqual(value, result); + return QV4::Runtime::StrictEqual::call(value, result); } static inline QV4::ReturnedValue evaluate(QV4::ExecutionEngine *v4, const QV4::Value &o, @@ -3117,7 +3136,7 @@ void tst_qqmlecmascript::resolveClashingProperties() QString key = name->toQStringNoThrow(); if (key == QLatin1String("clashes")) { value = v; - QV4::ScopedValue typeString(scope, QV4::Runtime::method_typeofValue(engine, value)); + QV4::ScopedValue typeString(scope, QV4::Runtime::TypeofValue::call(engine, value)); QString type = typeString->toQStringNoThrow(); if (type == QLatin1String("boolean")) { QVERIFY(!seenProperty); diff --git a/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp b/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp index 22ea3a89c3..bf3835d388 100644 --- a/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp +++ b/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp @@ -3805,7 +3805,7 @@ void tst_qqmllanguage::scopedEnumsWithNameClash() { auto typeId = qmlRegisterUncreatableType<ScopedEnumsWithNameClash>("ScopedEnumsWithNameClashTest", 1, 0, "ScopedEnum", "Dummy reason"); auto registryGuard = qScopeGuard([typeId]() { - qmlUnregisterType(typeId); + QQmlMetaType::unregisterType(typeId); }); QQmlEngine engine; @@ -3824,7 +3824,7 @@ void tst_qqmllanguage::scopedEnumsWithResolvedNameClash() { auto typeId = qmlRegisterUncreatableType<ScopedEnumsWithResolvedNameClash>("ScopedEnumsWithResolvedNameClashTest", 1, 0, "ScopedEnum", "Dummy reason"); auto registryGuard = qScopeGuard([typeId]() { - qmlUnregisterType(typeId); + QQmlMetaType::unregisterType(typeId); }); QQmlEngine engine; diff --git a/tests/auto/qml/qqmlmetatype/tst_qqmlmetatype.cpp b/tests/auto/qml/qqmlmetatype/tst_qqmlmetatype.cpp index ce72f40dcc..ac75eeab26 100644 --- a/tests/auto/qml/qqmlmetatype/tst_qqmlmetatype.cpp +++ b/tests/auto/qml/qqmlmetatype/tst_qqmlmetatype.cpp @@ -36,7 +36,6 @@ #include <private/qqmlmetatype_p.h> #include <private/qqmlpropertyvalueinterceptor_p.h> #include <private/qqmlengine_p.h> -#include <private/qhashedstring_p.h> #include "../../shared/util.h" class tst_qqmlmetatype : public QQmlDataTest @@ -398,7 +397,7 @@ void tst_qqmlmetatype::unregisterCustomType() QCOMPARE(enumVal.type(), QVariant::Int); QCOMPARE(enumVal.toInt(), 1); } - qmlUnregisterType(controllerId); + QQmlMetaType::unregisterType(controllerId); { QQmlEngine engine; QQmlType type = QQmlMetaType::qmlType(QString("Controller"), QString("mytypes"), 1, 0); @@ -421,7 +420,7 @@ void tst_qqmlmetatype::unregisterCustomType() QCOMPARE(enumVal.type(), QVariant::Int); QCOMPARE(enumVal.toInt(), 111); } - qmlUnregisterType(controllerId); + QQmlMetaType::unregisterType(controllerId); { QQmlEngine engine; QQmlType type = QQmlMetaType::qmlType(QString("Controller"), QString("mytypes"), 1, 0); @@ -490,7 +489,7 @@ void tst_qqmlmetatype::unregisterCustomSingletonType() QCOMPARE(stringVal.type(), QVariant::String); QCOMPARE(stringVal.toString(), QStringLiteral("StaticProvider #1")); } - qmlUnregisterType(staticProviderId); + QQmlMetaType::unregisterType(staticProviderId); { QQmlEngine engine; staticProviderId = qmlRegisterSingletonType<StaticProvider2>("mytypes", 1, 0, "StaticProvider", createStaticProvider2); @@ -506,7 +505,7 @@ void tst_qqmlmetatype::unregisterCustomSingletonType() QCOMPARE(stringVal.type(), QVariant::String); QCOMPARE(stringVal.toString(), QStringLiteral("StaticProvider #2")); } - qmlUnregisterType(staticProviderId); + QQmlMetaType::unregisterType(staticProviderId); { QQmlEngine engine; staticProviderId = qmlRegisterSingletonType<StaticProvider1>("mytypes", 1, 0, "StaticProvider", createStaticProvider1); @@ -532,7 +531,7 @@ void tst_qqmlmetatype::normalizeUrls() QVERIFY(QQmlMetaType::qmlType(url, /*includeNonFileImports=*/true).isValid()); QUrl normalizedURL("qrc:/tstqqmlmetatype/data/CompositeType.qml"); QVERIFY(QQmlMetaType::qmlType(normalizedURL, /*includeNonFileImports=*/true).isValid()); - qmlUnregisterType(registrationId); + QQmlMetaType::unregisterType(registrationId); QVERIFY(!QQmlMetaType::qmlType(url, /*includeNonFileImports=*/true).isValid()); } diff --git a/tests/auto/qml/qqmltablemodel/data/TestModel.qml b/tests/auto/qml/qqmltablemodel/data/TestModel.qml new file mode 100644 index 0000000000..7aeb5d03f4 --- /dev/null +++ b/tests/auto/qml/qqmltablemodel/data/TestModel.qml @@ -0,0 +1,47 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import Qt.labs.qmlmodels 1.0 + +import "TestUtils.js" as TestUtils + +TableModel { + id: testModel + objectName: "testModel" + roleDataProvider: TestUtils.testModelRoleDataProvider + rows: [ + [ + { name: "John" }, + { age: 22 } + ], + [ + { name: "Oliver" }, + { age: 33 } + ] + ] +} diff --git a/tests/auto/qml/qqmltablemodel/data/TestUtils.js b/tests/auto/qml/qqmltablemodel/data/TestUtils.js new file mode 100644 index 0000000000..0b92a377bb --- /dev/null +++ b/tests/auto/qml/qqmltablemodel/data/TestUtils.js @@ -0,0 +1,45 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +function testModelRoleDataProvider(index, role, cellData) { + switch (role) { + case "display": + switch (index.column) { + case 0: + return cellData.name + case 1: + return cellData.age + } + break + case "name": + return cellData.name + case "age": + return cellData.age + } + return cellData +} diff --git a/tests/auto/qml/qqmltablemodel/data/builtInRoles.qml b/tests/auto/qml/qqmltablemodel/data/builtInRoles.qml new file mode 100644 index 0000000000..d9882e4dea --- /dev/null +++ b/tests/auto/qml/qqmltablemodel/data/builtInRoles.qml @@ -0,0 +1,47 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import Qt.labs.qmlmodels 1.0 + +import "TestUtils.js" as TestUtils + +TableModel { + id: testModel + objectName: "testModel" + roleDataProvider: TestUtils.testModelRoleDataProvider + rows: [ + [ + { name: "John", someOtherRole1: "foo" }, // column 0 + { age: 22, someOtherRole2: "foo" } // column 1 + ], + [ + { name: "Oliver", someOtherRole1: "foo" }, // column 0 + { age: 33, someOtherRole2: "foo" } // column 1 + ] + ] +} diff --git a/tests/auto/qml/qqmltablemodel/data/common.qml b/tests/auto/qml/qqmltablemodel/data/common.qml new file mode 100644 index 0000000000..aec796bd4f --- /dev/null +++ b/tests/auto/qml/qqmltablemodel/data/common.qml @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.12 +import Qt.labs.qmlmodels 1.0 + +Item { + id: root + width: 200 + height: 200 + + property alias testModel: testModel + property alias tableView: tableView + + function appendRow(personName, personAge) { + testModel.appendRow([ + { name: personName }, + { age: personAge } + ]) + } + + function appendRowInvalid1() { + testModel.appendRow([ + { name: "Foo" }, + { age: 99 }, + { nonExistentRole: 123 } + ]) + } + + function appendRowInvalid2() { + testModel.appendRow(123) + } + + function appendRowInvalid3() { + testModel.appendRow([ + { name: "Foo" }, + { age: [] } + ]) + } + + function insertRow(personName, personAge, rowIndex) { + testModel.insertRow(rowIndex, [ + { name: personName }, + { age: personAge }] + ) + } + + function insertRowInvalid1() { + testModel.insertRow(0, [ + { name: "Foo" }, + { age: 99 }, + { nonExistentRole: 123 } + ]) + } + + function insertRowInvalid2() { + testModel.insertRow(0, 123) + } + + function insertRowInvalid3() { + testModel.insertRow(0, [ + { name: "Foo" }, + { age: [] } + ]) + } + + function setRow(rowIndex, personName, personAge) { + testModel.setRow(rowIndex, [ + { name: personName }, + { age: personAge }] + ) + } + + function setRowInvalid1() { + testModel.setRow(0, [ + { name: "Foo" }, + { age: 99 }, + { nonExistentRole: 123 } + ]) + } + + function setRowInvalid2() { + testModel.setRow(0, 123) + } + + function setRowInvalid3() { + testModel.setRow(0, [ + { name: "Foo" }, + { age: [] } + ]) + } + + TableView { + id: tableView + anchors.fill: parent + model: TestModel { + id: testModel + } + delegate: Text { + text: model.display + } + } +} diff --git a/tests/auto/qml/qqmltablemodel/data/dataAndSetData.qml b/tests/auto/qml/qqmltablemodel/data/dataAndSetData.qml new file mode 100644 index 0000000000..d61c50ba2c --- /dev/null +++ b/tests/auto/qml/qqmltablemodel/data/dataAndSetData.qml @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.12 +import Qt.labs.qmlmodels 1.0 + +TableView { + width: 200; height: 200 + model: TableModel { + id: testModel + objectName: "testModel" + rows: [ + [ + { name: "John", someOtherRole1: "foo" }, // column 0 + { age: 22, someOtherRole2: "foo" } // column 1 + ], + [ + { name: "Oliver", someOtherRole1: "foo" }, // column 0 + { age: 33, someOtherRole2: "foo" } // column 1 + ] + ] + + // This is silly: in real life, store the birthdate instead of the age, + // and let the delegate calculate the age, so it won't need updating + function happyBirthday(dude) { + var row = -1; + for (var r = 0; row < 0 && r < testModel.rowCount; ++r) + if (testModel.data(testModel.index(r, 0), "name") === dude) + row = r; + var index = testModel.index(row, 1) + testModel.setData(index, "age", testModel.data(index, "age") + 1) + } + } + delegate: Text { + id: textItem + text: model.display + TapHandler { + onTapped: testModel.happyBirthday(testModel.data(testModel.index(row, 0), "name")) + } + } +} diff --git a/tests/auto/qml/qqmltablemodel/data/empty.qml b/tests/auto/qml/qqmltablemodel/data/empty.qml new file mode 100644 index 0000000000..6e66b99145 --- /dev/null +++ b/tests/auto/qml/qqmltablemodel/data/empty.qml @@ -0,0 +1,68 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.12 +import Qt.labs.qmlmodels 1.0 + +import "TestUtils.js" as TestUtils + +Item { + id: root + width: 200 + height: 200 + + property alias testModel: testModel + property alias tableView: tableView + + function setRows() { + testModel.rows = [ + [ + { name: "John" }, + { age: 22 } + ], + [ + { name: "Oliver" }, + { age: 33 } + ] + ] + } + + TableModel { + id: testModel + objectName: "testModel" + roleDataProvider: TestUtils.testModelRoleDataProvider + } + TableView { + id: tableView + anchors.fill: parent + model: testModel + delegate: Text { + text: model.display + } + } +} diff --git a/tests/auto/qml/qqmltablemodel/data/explicitDisplayRole.qml b/tests/auto/qml/qqmltablemodel/data/explicitDisplayRole.qml new file mode 100644 index 0000000000..510a62e74b --- /dev/null +++ b/tests/auto/qml/qqmltablemodel/data/explicitDisplayRole.qml @@ -0,0 +1,41 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import Qt.labs.qmlmodels 1.0 + +import "TestUtils.js" as TestUtils + +TableModel { + id: testModel + rows: [ + [ + { name: "John", display: "foo" }, + { age: 22, display: "bar" } + ] + ] +} diff --git a/tests/auto/qml/qqmltablemodel/data/roleDataProvider.qml b/tests/auto/qml/qqmltablemodel/data/roleDataProvider.qml new file mode 100644 index 0000000000..2706ea54fd --- /dev/null +++ b/tests/auto/qml/qqmltablemodel/data/roleDataProvider.qml @@ -0,0 +1,65 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.12 +import Qt.labs.qmlmodels 1.0 + +Item { + id: root + width: 200 + height: 200 + + property alias testModel: testModel + + TableModel { + id: testModel + objectName: "testModel" + rows: [ + [ { name: "Rex" }, { age: 3 } ], + [ { name: "Buster" }, { age: 5 } ] + ] + roleDataProvider: function(index, role, cellData) { + if (role === "display") { + // Age will now be in dog years + if (cellData.hasOwnProperty("age")) + return (cellData.age * 7); + else if (index.column === 0) + return (cellData.name); + } + return cellData; + } + } + TableView { + anchors.fill: parent + model: testModel + delegate: Text { + id: textItem + text: model.display + } + } +} diff --git a/tests/auto/qml/qqmltablemodel/data/setDataThroughDelegate.qml b/tests/auto/qml/qqmltablemodel/data/setDataThroughDelegate.qml new file mode 100644 index 0000000000..5f849c3350 --- /dev/null +++ b/tests/auto/qml/qqmltablemodel/data/setDataThroughDelegate.qml @@ -0,0 +1,99 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.12 +import Qt.labs.qmlmodels 1.0 + +Item { + id: root + width: 200 + height: 200 + + property alias testModel: testModel + + signal shouldModify() + signal shouldModifyInvalidRole() + signal shouldModifyInvalidType() + + function modify() { + shouldModify(); + } + + function modifyInvalidRole() { + shouldModifyInvalidRole(); + } + + function modifyInvalidType() { + shouldModifyInvalidType() + } + + TableView { + anchors.fill: parent + model: TableModel { + id: testModel + objectName: "testModel" + rows: [ + [ + { name: "John" }, + { age: 22 } + ], + [ + { name: "Oliver" }, + { age: 33 } + ] + ] + } + + delegate: Text { + id: textItem + // TODO: this is currently random when no roleDataProvider handles it + // we should allow roleDataProvider to be used to handle specific roles only + text: model.display + + Connections { + target: root + enabled: column === 1 + onShouldModify: model.age = 18 + } + + Connections { + target: root + enabled: column === 0 + // Invalid: should be "name". + onShouldModifyInvalidRole: model.age = 100 + } + + Connections { + target: root + enabled: column === 1 + // Invalid: should be string. + onShouldModifyInvalidType: model.age = "Whoops" + } + } + } +} diff --git a/tests/auto/qml/qqmltablemodel/data/setRowsMultipleTimes.qml b/tests/auto/qml/qqmltablemodel/data/setRowsMultipleTimes.qml new file mode 100644 index 0000000000..6aaf79f2d4 --- /dev/null +++ b/tests/auto/qml/qqmltablemodel/data/setRowsMultipleTimes.qml @@ -0,0 +1,84 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.12 +import Qt.labs.qmlmodels 1.0 + +Item { + id: root + width: 200 + height: 200 + + property alias testModel: testModel + property alias tableView: tableView + + function setRowsValid() { + testModel.rows = [ + [ + { name: "Max" }, + { age: 20 } + ], + [ + { name: "Imum" }, + { age: 41 } + ], + [ + { name: "Power" }, + { age: 89 } + ] + ] + } + + function setRowsInvalid() { + testModel.rows = [ + [ + { nope: "Nope" }, + { age: 20 } + ], + [ + { nope: "Nah" }, + { age: 41 } + ], + [ + { nope: "No" }, + { age: 89 } + ] + ] + } + + TableView { + id: tableView + anchors.fill: parent + model: TestModel { + id: testModel + } + delegate: Text { + text: model.display + } + } +} diff --git a/tests/auto/qml/qqmltablemodel/qqmltablemodel.pro b/tests/auto/qml/qqmltablemodel/qqmltablemodel.pro new file mode 100644 index 0000000000..11b11132aa --- /dev/null +++ b/tests/auto/qml/qqmltablemodel/qqmltablemodel.pro @@ -0,0 +1,10 @@ +CONFIG += testcase +TARGET = tst_qqmltablemodel + +SOURCES += tst_qqmltablemodel.cpp + +include (../../shared/util.pri) + +TESTDATA = data/* + +QT += core gui qml-private qml quick-private quick testlib diff --git a/tests/auto/qml/qqmltablemodel/tst_qqmltablemodel.cpp b/tests/auto/qml/qqmltablemodel/tst_qqmltablemodel.cpp new file mode 100644 index 0000000000..059ce082d9 --- /dev/null +++ b/tests/auto/qml/qqmltablemodel/tst_qqmltablemodel.cpp @@ -0,0 +1,947 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtTest/qtest.h> +#include <QtTest/qsignalspy.h> +#include <QtCore/qregularexpression.h> +#include <QtQml/private/qqmlengine_p.h> +#include <QtQml/private/qqmltablemodel_p.h> +#include <QtQml/qqmlcomponent.h> +#include <QtQuick/qquickitem.h> +#include <QtQuick/qquickview.h> +#include <QtQuick/private/qquicktableview_p.h> + +#include "../../shared/util.h" + +class tst_QQmlTableModel : public QQmlDataTest +{ + Q_OBJECT + +public: + tst_QQmlTableModel() {} + +private slots: + void appendRemoveRow(); + void clear(); + void getRow(); + void insertRow(); + void moveRow(); + void setRow(); + void setDataThroughDelegate(); + void setRowsImperatively(); + void setRowsMultipleTimes(); + void builtInRoles_data(); + void builtInRoles(); + void explicitDisplayRole(); + void roleDataProvider(); + void dataAndEditing(); +}; + +static const int builtInRoleCount = 6; + +void tst_QQmlTableModel::appendRemoveRow() +{ + QQuickView view(testFileUrl("common.qml")); + QCOMPARE(view.status(), QQuickView::Ready); + view.show(); + QVERIFY(QTest::qWaitForWindowActive(&view)); + + QQmlTableModel *model = view.rootObject()->property("testModel").value<QQmlTableModel*>(); + QVERIFY(model); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + + QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged())); + QVERIFY(columnCountSpy.isValid()); + + QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged())); + QVERIFY(rowCountSpy.isValid()); + int heightSignalEmissions = 0; + + const QHash<int, QByteArray> roleNames = model->roleNames(); + QCOMPARE(roleNames.size(), 2 + builtInRoleCount); + QVERIFY(roleNames.values().contains("name")); + QVERIFY(roleNames.values().contains("age")); + QVERIFY(roleNames.values().contains("display")); + QVERIFY(roleNames.values().contains("decoration")); + QVERIFY(roleNames.values().contains("edit")); + QVERIFY(roleNames.values().contains("toolTip")); + QVERIFY(roleNames.values().contains("statusTip")); + QVERIFY(roleNames.values().contains("whatsThis")); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + + // Call remove() with a negative rowIndex. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*removeRow\\(\\): \"rowIndex\" cannot be negative")); + QVERIFY(QMetaObject::invokeMethod(model, "removeRow", Q_ARG(int, -1))); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + + // Call remove() with an rowIndex that is too large. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*removeRow\\(\\): \"rowIndex\" 2 is greater than or equal to rowCount\\(\\) of 2")); + QVERIFY(QMetaObject::invokeMethod(model, "removeRow", Q_ARG(int, 2))); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + + // Call remove() with a valid rowIndex but negative rows. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*removeRow\\(\\): \"rows\" is less than or equal to zero")); + QVERIFY(QMetaObject::invokeMethod(model, "removeRow", Q_ARG(int, 0), Q_ARG(int, -1))); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + + // Call remove() with a valid rowIndex but excessive rows. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*removeRow\\(\\): \"rows\" 3 exceeds available rowCount\\(\\) of 2 when removing from \"rowIndex\" 0")); + QVERIFY(QMetaObject::invokeMethod(model, "removeRow", Q_ARG(int, 0), Q_ARG(int, 3))); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + + // Call remove() without specifying the number of rows to remove; it should remove one row. + QVERIFY(QMetaObject::invokeMethod(model, "removeRow", Q_ARG(int, 0))); + QCOMPARE(model->rowCount(), 1); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions); + + // Call append() with a row that has a new (and hence unexpected) role. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*appendRow\\(\\): expected 2 columns, but got 3")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRowInvalid1")); + // Nothing should change. + QCOMPARE(model->rowCount(), 1); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + + // Call append() with a row that is not an array. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*appendRow\\(\\): expected \"row\" argument to be an array, but got int instead")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRowInvalid2")); + // Nothing should change. + QCOMPARE(model->rowCount(), 1); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + + // Call append() with a row with a role that is of the wrong type. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*appendRow\\(\\): expected property with type int at column index 1, but got QVariantList instead")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRowInvalid3")); + // Nothing should change. + QCOMPARE(model->rowCount(), 1); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + + // Call append() to insert one row. + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRow", Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40))); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions); + + // Call remove() and specify rowIndex and rows, removing all remaining rows. + QVERIFY(QMetaObject::invokeMethod(model, "removeRow", Q_ARG(int, 0), Q_ARG(int, 2))); + QCOMPARE(model->rowCount(), 0); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")), QVariant()); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")), QVariant()); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions); +} + +void tst_QQmlTableModel::clear() +{ + QQuickView view(testFileUrl("common.qml")); + QCOMPARE(view.status(), QQuickView::Ready); + view.show(); + QVERIFY(QTest::qWaitForWindowActive(&view)); + + QQmlTableModel *model = view.rootObject()->property("testModel").value<QQmlTableModel*>(); + QVERIFY(model); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + + QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged())); + QVERIFY(columnCountSpy.isValid()); + + QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged())); + QVERIFY(rowCountSpy.isValid()); + + const QHash<int, QByteArray> roleNames = model->roleNames(); + QVERIFY(roleNames.values().contains("name")); + QVERIFY(roleNames.values().contains("age")); + QCOMPARE(roleNames.size(), 2 + builtInRoleCount); + + QQuickTableView *tableView = view.rootObject()->property("tableView").value<QQuickTableView*>(); + QVERIFY(tableView); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + QVERIFY(QMetaObject::invokeMethod(model, "clear")); + QCOMPARE(model->rowCount(), 0); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")), QVariant()); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")), QVariant()); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), 1); + // Wait until updatePolish() gets called, which is where the size is recalculated. + QTRY_COMPARE(tableView->rows(), 0); + QCOMPARE(tableView->columns(), 2); +} + +void tst_QQmlTableModel::getRow() +{ + QQuickView view(testFileUrl("common.qml")); + QCOMPARE(view.status(), QQuickView::Ready); + view.show(); + QVERIFY(QTest::qWaitForWindowActive(&view)); + + QQmlTableModel *model = view.rootObject()->property("testModel").value<QQmlTableModel*>(); + QVERIFY(model); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + + // Call get() with a negative row index. + QVariant returnValue; + QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*getRow\\(\\): \"rowIndex\" cannot be negative")); + QVERIFY(QMetaObject::invokeMethod(model, "getRow", Q_RETURN_ARG(QVariant, returnValue), Q_ARG(int, -1))); + QVERIFY(!returnValue.isValid()); + + // Call get() with a row index that is too large. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*getRow\\(\\): \"rowIndex\" 2 is greater than or equal to rowCount\\(\\) of 2")); + QVERIFY(QMetaObject::invokeMethod(model, "getRow", Q_RETURN_ARG(QVariant, returnValue), Q_ARG(int, 2))); + QVERIFY(!returnValue.isValid()); + + // Call get() with a valid row index. + QVERIFY(QMetaObject::invokeMethod(model, "getRow", Q_RETURN_ARG(QVariant, returnValue), Q_ARG(int, 0))); + const QVariantList rowAsVariantList = returnValue.toList(); + QCOMPARE(rowAsVariantList.at(0).toMap().value(QLatin1String("name")), QLatin1String("John")); + QCOMPARE(rowAsVariantList.at(1).toMap().value(QLatin1String("age")), 22); +} + +void tst_QQmlTableModel::insertRow() +{ + QQuickView view(testFileUrl("common.qml")); + QCOMPARE(view.status(), QQuickView::Ready); + view.show(); + QVERIFY(QTest::qWaitForWindowActive(&view)); + + QQmlTableModel *model = view.rootObject()->property("testModel").value<QQmlTableModel*>(); + QVERIFY(model); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + + QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged())); + QVERIFY(columnCountSpy.isValid()); + + QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged())); + QVERIFY(rowCountSpy.isValid()); + int heightSignalEmissions = 0; + + QQuickTableView *tableView = view.rootObject()->property("tableView").value<QQuickTableView*>(); + QVERIFY(tableView); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Try to insert with a negative index. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*insertRow\\(\\): \"rowIndex\" cannot be negative")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "insertRow", + Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40), Q_ARG(QVariant, -1))); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->rowCount(), 2); + const QHash<int, QByteArray> roleNames = model->roleNames(); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Try to insert past the last allowed index. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*insertRow\\(\\): \"rowIndex\" 3 is greater than rowCount\\(\\) of 2")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "insertRow", + Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40), Q_ARG(QVariant, 3))); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Try to insert a row that has a new (and hence unexpected) role. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*insertRow\\(\\): expected 2 columns, but got 3")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "insertRowInvalid1")); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Try to insert a row that is not an array. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*insertRow\\(\\): expected \"row\" argument to be an array, but got int instead")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "insertRowInvalid2")); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Try to insert a row with a role that is of the wrong type. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*insertRow\\(\\): expected property with type int at column index 1, but got QVariantList instead")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "insertRowInvalid3")); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Insert a row at the bottom of the table. + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "insertRow", + Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40), Q_ARG(QVariant, 2))); + QCOMPARE(model->rowCount(), 3); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions); + QTRY_COMPARE(tableView->rows(), 3); + QCOMPARE(tableView->columns(), 2); + + // Insert a row in the middle of the table. + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "insertRow", + Q_ARG(QVariant, QLatin1String("Daisy")), Q_ARG(QVariant, 30), Q_ARG(QVariant, 1))); + QCOMPARE(model->rowCount(), 4); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Daisy")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 30); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions); + QTRY_COMPARE(tableView->rows(), 4); + QCOMPARE(tableView->columns(), 2); +} + +void tst_QQmlTableModel::moveRow() +{ + QQuickView view(testFileUrl("common.qml")); + QCOMPARE(view.status(), QQuickView::Ready); + view.show(); + QVERIFY(QTest::qWaitForWindowActive(&view)); + + QQmlTableModel *model = view.rootObject()->property("testModel").value<QQmlTableModel*>(); + QVERIFY(model); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->rowCount(), 2); + + QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged())); + QVERIFY(columnCountSpy.isValid()); + + QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged())); + QVERIFY(rowCountSpy.isValid()); + int heightSignalEmissions = 0; + + const QHash<int, QByteArray> roleNames = model->roleNames(); + + // Append some rows. + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRow", Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40))); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRow", Q_ARG(QVariant, QLatin1String("Daisy")), Q_ARG(QVariant, 30))); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "appendRow", Q_ARG(QVariant, QLatin1String("Trev")), Q_ARG(QVariant, 48))); + QCOMPARE(model->rowCount(), 5); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); + QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Daisy")); + QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("age")).toInt(), 30); + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); + heightSignalEmissions = 3; + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + + // Try to move with a fromRowIndex that is negative. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*moveRow\\(\\): \"fromRowIndex\" cannot be negative")); + QVERIFY(QMetaObject::invokeMethod(model, "moveRow", Q_ARG(int, -1), Q_ARG(int, 1))); + // Shouldn't have changed. + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + + // Try to move with a fromRowIndex that is too large. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*moveRow\\(\\): \"fromRowIndex\" 5 is greater than or equal to rowCount\\(\\)")); + QVERIFY(QMetaObject::invokeMethod(model, "moveRow", Q_ARG(int, 5), Q_ARG(int, 1))); + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + + // Try to move with a toRowIndex that is negative. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*moveRow\\(\\): \"toRowIndex\" cannot be negative")); + QVERIFY(QMetaObject::invokeMethod(model, "moveRow", Q_ARG(int, 0), Q_ARG(int, -1))); + // Shouldn't have changed. + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + + // Try to move with a toRowIndex that is too large. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*moveRow\\(\\): \"toRowIndex\" 5 is greater than or equal to rowCount\\(\\)")); + QVERIFY(QMetaObject::invokeMethod(model, "moveRow", Q_ARG(int, 0), Q_ARG(int, 5))); + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + + // Move the first row to the end. + QVERIFY(QMetaObject::invokeMethod(model, "moveRow", Q_ARG(int, 0), Q_ARG(int, 4))); + // The counts shouldn't have changed. + QCOMPARE(model->rowCount(), 5); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Daisy")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 30); + QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + + // Move it back again. + QVERIFY(QMetaObject::invokeMethod(model, "moveRow", Q_ARG(int, 4), Q_ARG(int, 0))); + QCOMPARE(model->rowCount(), 5); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); + QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Daisy")); + QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("age")).toInt(), 30); + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + + // Move the first row down one by one row. + QVERIFY(QMetaObject::invokeMethod(model, "moveRow", Q_ARG(int, 0), Q_ARG(int, 1))); + QCOMPARE(model->rowCount(), 5); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); + QCOMPARE(model->data(model->index(3, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Daisy")); + QCOMPARE(model->data(model->index(3, 1, QModelIndex()), roleNames.key("age")).toInt(), 30); + QCOMPARE(model->data(model->index(4, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Trev")); + QCOMPARE(model->data(model->index(4, 1, QModelIndex()), roleNames.key("age")).toInt(), 48); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); +} + +void tst_QQmlTableModel::setRow() +{ + QQuickView view(testFileUrl("common.qml")); + QCOMPARE(view.status(), QQuickView::Ready); + view.show(); + QVERIFY(QTest::qWaitForWindowActive(&view)); + + QQmlTableModel *model = view.rootObject()->property("testModel").value<QQmlTableModel*>(); + QVERIFY(model); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->rowCount(), 2); + + QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged())); + QVERIFY(columnCountSpy.isValid()); + + QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged())); + QVERIFY(rowCountSpy.isValid()); + int heightSignalEmissions = 0; + + QQuickTableView *tableView = view.rootObject()->property("tableView").value<QQuickTableView*>(); + QVERIFY(tableView); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Try to insert with a negative index. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*setRow\\(\\): \"rowIndex\" cannot be negative")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRow", + Q_ARG(QVariant, -1), Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40))); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + const QHash<int, QByteArray> roleNames = model->roleNames(); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Try to insert past the last allowed index. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*setRow\\(\\): \"rowIndex\" 3 is greater than rowCount\\(\\) of 2")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRow", + Q_ARG(QVariant, 3), Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40))); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Try to insert a row that has a new (and hence unexpected) role. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*setRow\\(\\): expected 2 columns, but got 3")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRowInvalid1")); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Try to insert a row that is not an array. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*setRow\\(\\): expected \"row\" argument to be an array, but got int instead")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRowInvalid2")); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Try to insert a row with a role that is of the wrong type. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*setRow\\(\\): expected property with type int at column index 1, but got QVariantList instead")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRowInvalid3")); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Set the first row. + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRow", + Q_ARG(QVariant, 0), Q_ARG(QVariant, QLatin1String("Max")), Q_ARG(QVariant, 40))); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Set the last row. + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRow", + Q_ARG(QVariant, 1), Q_ARG(QVariant, QLatin1String("Daisy")), Q_ARG(QVariant, 30))); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Daisy")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 30); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), heightSignalEmissions); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Append a row by passing an index that is equal to rowCount(). + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRow", + Q_ARG(QVariant, 2), Q_ARG(QVariant, QLatin1String("Wot")), Q_ARG(QVariant, 99))); + QCOMPARE(model->rowCount(), 3); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 40); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Daisy")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 30); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Wot")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 99); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), ++heightSignalEmissions); + QTRY_COMPARE(tableView->rows(), 3); + QCOMPARE(tableView->columns(), 2); +} + +void tst_QQmlTableModel::setDataThroughDelegate() +{ + QQuickView view(testFileUrl("setDataThroughDelegate.qml")); + QCOMPARE(view.status(), QQuickView::Ready); + view.show(); + QVERIFY(QTest::qWaitForWindowActive(&view)); + + QQmlTableModel *model = view.rootObject()->property("testModel").value<QQmlTableModel*>(); + QVERIFY(model); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + + QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged())); + QVERIFY(columnCountSpy.isValid()); + + QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged())); + QVERIFY(rowCountSpy.isValid()); + + const QHash<int, QByteArray> roleNames = model->roleNames(); + QCOMPARE(roleNames.size(), 2 + builtInRoleCount); + QVERIFY(roleNames.values().contains("name")); + QVERIFY(roleNames.values().contains("age")); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), 0); + + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "modify")); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 18); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 18); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), 0); + + // Test setting a role that doesn't exist for a certain column. + const auto invalidRoleRegEx = QRegularExpression(".*setData\\(\\): no role named \"age\" at column index 0. " \ + "The available roles for that column are:[\r\n] - \"name\" \\(QString\\)"); + // There are two rows, so two delegates respond to the signal, which means we need to ignore two warnings. + QTest::ignoreMessage(QtWarningMsg, invalidRoleRegEx); + QTest::ignoreMessage(QtWarningMsg, invalidRoleRegEx); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "modifyInvalidRole")); + // Should be unchanged. + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 18); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 18); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), 0); + + // Test setting a role with a value of the wrong type. + // There are two rows, so two delegates respond to the signal, which means we need to ignore two warnings. + QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*setData\\(\\): failed converting value QVariant\\(QString, \"Whoops\"\\) " \ + "set at row 0 column 1 with role \"age\" to \"int\"")); + QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*setData\\(\\): failed converting value QVariant\\(QString, \"Whoops\"\\) " \ + "set at row 1 column 1 with role \"age\" to \"int\"")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "modifyInvalidType")); + // Should be unchanged. + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 18); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 18); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), 0); +} + +// Start off with empty rows and append to test widthChanged(). +void tst_QQmlTableModel::setRowsImperatively() +{ + QQuickView view(testFileUrl("empty.qml")); + QCOMPARE(view.status(), QQuickView::Ready); + view.show(); + QVERIFY(QTest::qWaitForWindowActive(&view)); + + QQmlTableModel *model = view.rootObject()->property("testModel").value<QQmlTableModel*>(); + QVERIFY(model); + QCOMPARE(model->columnCount(), 0); + QCOMPARE(model->rowCount(), 0); + + QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged())); + QVERIFY(columnCountSpy.isValid()); + + QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged())); + QVERIFY(rowCountSpy.isValid()); + + QQuickTableView *tableView = view.rootObject()->property("tableView").value<QQuickTableView*>(); + QVERIFY(tableView); + QCOMPARE(tableView->rows(), 0); + QCOMPARE(tableView->columns(), 0); + + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRows")); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + const QHash<int, QByteArray> roleNames = model->roleNames(); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 33); + QCOMPARE(columnCountSpy.count(), 1); + QCOMPARE(rowCountSpy.count(), 1); + QTRY_COMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); +} + +void tst_QQmlTableModel::setRowsMultipleTimes() +{ + QQuickView view(testFileUrl("setRowsMultipleTimes.qml")); + QCOMPARE(view.status(), QQuickView::Ready); + view.show(); + QVERIFY(QTest::qWaitForWindowActive(&view)); + + QQmlTableModel *model = view.rootObject()->property("testModel").value<QQmlTableModel*>(); + QVERIFY(model); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + + QSignalSpy columnCountSpy(model, SIGNAL(columnCountChanged())); + QVERIFY(columnCountSpy.isValid()); + + QSignalSpy rowCountSpy(model, SIGNAL(rowCountChanged())); + QVERIFY(rowCountSpy.isValid()); + + QQuickTableView *tableView = view.rootObject()->property("tableView").value<QQuickTableView*>(); + QVERIFY(tableView); + QCOMPARE(tableView->rows(), 2); + QCOMPARE(tableView->columns(), 2); + + // Set valid rows after they've already been declared. + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRowsValid")); + QCOMPARE(model->rowCount(), 3); + QCOMPARE(model->columnCount(), 2); + const QHash<int, QByteArray> roleNames = model->roleNames(); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 20); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Imum")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 41); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Power")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 89); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), 1); + QTRY_COMPARE(tableView->rows(), 3); + QCOMPARE(tableView->columns(), 2); + + // Set invalid rows; we should get a warning and nothing should change. + // TODO: add quotes to the warning message + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + ".*setRows\\(\\): expected property named name at column index 0, but got nope instead")); + QVERIFY(QMetaObject::invokeMethod(view.rootObject(), "setRowsInvalid")); + QCOMPARE(model->rowCount(), 3); + QCOMPARE(model->columnCount(), 2); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Max")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("age")).toInt(), 20); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Imum")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("age")).toInt(), 41); + QCOMPARE(model->data(model->index(2, 0, QModelIndex()), roleNames.key("name")).toString(), QLatin1String("Power")); + QCOMPARE(model->data(model->index(2, 1, QModelIndex()), roleNames.key("age")).toInt(), 89); + QCOMPARE(columnCountSpy.count(), 0); + QCOMPARE(rowCountSpy.count(), 1); + QCOMPARE(tableView->rows(), 3); + QCOMPARE(tableView->columns(), 2); +} + +void tst_QQmlTableModel::builtInRoles_data() +{ + QTest::addColumn<int>("row"); + QTest::addColumn<int>("column"); + QTest::addColumn<QByteArray>("roleName"); + QTest::addColumn<QVariant>("expectedValue"); + + const QByteArray displayRole = "display"; + + QTest::addRow("display(0,0)") << 0 << 0 << displayRole << QVariant(QLatin1String("John")); + QTest::addRow("display(0,1)") << 0 << 1 << displayRole << QVariant(QLatin1String("22")); + QTest::addRow("display(1,0)") << 1 << 0 << displayRole << QVariant(QLatin1String("Oliver")); + QTest::addRow("display(1,1)") << 1 << 1 << displayRole << QVariant(QLatin1String("33")); +} + +void tst_QQmlTableModel::builtInRoles() +{ + QFETCH(int, row); + QFETCH(int, column); + QFETCH(QByteArray, roleName); + QFETCH(QVariant, expectedValue); + + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("builtInRoles.qml")); + QCOMPARE(component.status(), QQmlComponent::Ready); + + QScopedPointer<QQmlTableModel> model(qobject_cast<QQmlTableModel*>(component.create())); + QVERIFY(model); + QCOMPARE(model->rowCount(), 2); + QCOMPARE(model->columnCount(), 2); + + const QHash<int, QByteArray> roleNames = model->roleNames(); + QCOMPARE(roleNames.size(), 4 + builtInRoleCount); + QVERIFY(roleNames.values().contains("display")); + QVERIFY(roleNames.values().contains("decoration")); + QVERIFY(roleNames.values().contains("edit")); + QVERIFY(roleNames.values().contains("toolTip")); + QVERIFY(roleNames.values().contains("statusTip")); + QVERIFY(roleNames.values().contains("whatsThis")); + QVERIFY(roleNames.values().contains("name")); + QVERIFY(roleNames.values().contains("age")); + QVERIFY(roleNames.values().contains("someOtherRole1")); + QVERIFY(roleNames.values().contains("someOtherRole2")); + QCOMPARE(model->data(model->index(row, column, QModelIndex()), roleNames.key(roleName)), expectedValue); +} + +void tst_QQmlTableModel::explicitDisplayRole() +{ + QQmlEngine engine; + QQmlComponent component(&engine, testFileUrl("explicitDisplayRole.qml")); + QCOMPARE(component.status(), QQmlComponent::Ready); + + QScopedPointer<QQmlTableModel> model(qobject_cast<QQmlTableModel*>(component.create())); + QVERIFY(model); + QCOMPARE(model->rowCount(), 1); + QCOMPARE(model->columnCount(), 2); + const QHash<int, QByteArray> roleNames = model->roleNames(); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("foo")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("bar")); +} + +void tst_QQmlTableModel::roleDataProvider() +{ + QQuickView view(testFileUrl("roleDataProvider.qml")); + QCOMPARE(view.status(), QQuickView::Ready); + view.show(); + QVERIFY(QTest::qWaitForWindowActive(&view)); + + QQmlTableModel *model = view.rootObject()->property("testModel").value<QQmlTableModel*>(); + QVERIFY(model); + + const QHash<int, QByteArray> roleNames = model->roleNames(); + QVERIFY(roleNames.values().contains("display")); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Rex")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 3 * 7); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Buster")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 5 * 7); +} + +void tst_QQmlTableModel::dataAndEditing() +{ + QQuickView view(testFileUrl("dataAndSetData.qml")); + QCOMPARE(view.status(), QQuickView::Ready); + view.show(); + QVERIFY(QTest::qWaitForWindowActive(&view)); + + QQmlTableModel *model = view.rootObject()->property("model").value<QQmlTableModel*>(); + QVERIFY(model); + + const QHash<int, QByteArray> roleNames = model->roleNames(); + QVERIFY(roleNames.values().contains("display")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 33); + QVERIFY(QMetaObject::invokeMethod(model, "happyBirthday", Q_ARG(QVariant, QLatin1String("Oliver")))); + QCOMPARE(model->data(model->index(0, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("John")); + QCOMPARE(model->data(model->index(0, 1, QModelIndex()), roleNames.key("display")).toInt(), 22); + QCOMPARE(model->data(model->index(1, 0, QModelIndex()), roleNames.key("display")).toString(), QLatin1String("Oliver")); + QCOMPARE(model->data(model->index(1, 1, QModelIndex()), roleNames.key("display")).toInt(), 34); +} + +QTEST_MAIN(tst_QQmlTableModel) + +#include "tst_qqmltablemodel.moc" diff --git a/tests/auto/qml/qqmlvaluetypes/tst_qqmlvaluetypes.cpp b/tests/auto/qml/qqmlvaluetypes/tst_qqmlvaluetypes.cpp index 6ccfc77c25..8a01524b5b 100644 --- a/tests/auto/qml/qqmlvaluetypes/tst_qqmlvaluetypes.cpp +++ b/tests/auto/qml/qqmlvaluetypes/tst_qqmlvaluetypes.cpp @@ -1710,7 +1710,7 @@ void tst_qqmlvaluetypes::sequences() QJSValue value = engine.toScriptValue(qcharVector); QCOMPARE(value.property("length").toInt(), qcharVector.length()); for (int i = 0; i < qcharVector.length(); ++i) - QCOMPARE(value.property(i).toInt(), qcharVector.at(i)); + QCOMPARE(value.property(i).toString(), qcharVector.at(i)); } { MyTypeObject a, b, c; diff --git a/tests/auto/qml/qquickworkerscript/tst_qquickworkerscript.cpp b/tests/auto/qml/qquickworkerscript/tst_qquickworkerscript.cpp index 4ad58ba56c..dfaeca67f1 100644 --- a/tests/auto/qml/qquickworkerscript/tst_qquickworkerscript.cpp +++ b/tests/auto/qml/qquickworkerscript/tst_qquickworkerscript.cpp @@ -30,6 +30,7 @@ #include <QtCore/qtimer.h> #include <QtCore/qdir.h> #include <QtCore/qfileinfo.h> +#include <QtCore/qregularexpression.h> #include <QtQml/qjsengine.h> #include <QtQml/qqmlcomponent.h> @@ -118,7 +119,18 @@ void tst_QQuickWorkerScript::messaging() QVariant response = mo->property(mo->indexOfProperty("response")).read(worker).value<QVariant>(); if (response.userType() == qMetaTypeId<QJSValue>()) response = response.value<QJSValue>().toVariant(); - QCOMPARE(response, value); + + if (value.type() == QMetaType::QRegExp && response.type() == QMetaType::QRegularExpression) { + // toVariant() doesn't know if we want QRegExp or QRegularExpression. It always creates + // a QRegularExpression from a JavaScript regular expression. + const QRegularExpression responseRegExp = response.toRegularExpression(); + const QRegExp valueRegExp = value.toRegExp(); + QCOMPARE(responseRegExp.pattern(), valueRegExp.pattern()); + QCOMPARE(bool(responseRegExp.patternOptions() & QRegularExpression::CaseInsensitiveOption), + bool(valueRegExp.caseSensitivity() == Qt::CaseInsensitive)); + } else { + QCOMPARE(response, value); + } qApp->processEvents(); delete worker; @@ -135,10 +147,10 @@ void tst_QQuickWorkerScript::messaging_data() QTest::newRow("string") << qVariantFromValue(QString("More cheeeese, Gromit!")); QTest::newRow("variant list") << qVariantFromValue((QVariantList() << "a" << "b" << "c")); QTest::newRow("date time") << qVariantFromValue(QDateTime::currentDateTime()); -#ifndef QT_NO_REGEXP - // Qt Script's QScriptValue -> QRegExp uses RegExp2 pattern syntax - QTest::newRow("regexp") << qVariantFromValue(QRegExp("^\\d\\d?$", Qt::CaseInsensitive, QRegExp::RegExp2)); -#endif + QTest::newRow("regexp") << qVariantFromValue(QRegExp("^\\d\\d?$", Qt::CaseInsensitive, + QRegExp::RegExp2)); + QTest::newRow("regularexpression") << qVariantFromValue(QRegularExpression( + "^\\d\\d?$", QRegularExpression::CaseInsensitiveOption)); } void tst_QQuickWorkerScript::messaging_sendQObjectList() diff --git a/tests/auto/quick/examples/tst_examples.cpp b/tests/auto/quick/examples/tst_examples.cpp index 9b3fa8fd2c..fdefa855e4 100644 --- a/tests/auto/quick/examples/tst_examples.cpp +++ b/tests/auto/quick/examples/tst_examples.cpp @@ -74,6 +74,7 @@ tst_examples::tst_examples() { // Add files to exclude here excludedFiles << "snippets/qml/listmodel/listmodel.qml"; //Just a ListModel, no root QQuickItem + excludedFiles << "snippets/qml/tablemodel/fruit-example-delegatechooser.qml"; // Requires QtQuick.Controls import. // Add directories you want excluded here excludedDirs << "shared"; //Not an example diff --git a/tests/auto/quick/propertyrequirements/tst_propertyrequirements.cpp b/tests/auto/quick/propertyrequirements/tst_propertyrequirements.cpp index 34be4d98b4..5781a007b6 100644 --- a/tests/auto/quick/propertyrequirements/tst_propertyrequirements.cpp +++ b/tests/auto/quick/propertyrequirements/tst_propertyrequirements.cpp @@ -28,7 +28,6 @@ #include <qtest.h> #include <QtQml/qqmlcomponent.h> #include <QtQml/qqmlengine.h> -#include <QtQml/private/qhashedstring_p.h> #include <QtQml/private/qqmlmetatype_p.h> #include <QtCore/QDebug> #include <QtCore/QHash> diff --git a/tests/benchmarks/qml/librarymetrics_performance/tst_librarymetrics_performance.cpp b/tests/benchmarks/qml/librarymetrics_performance/tst_librarymetrics_performance.cpp index d7c54703ad..7db01180be 100644 --- a/tests/benchmarks/qml/librarymetrics_performance/tst_librarymetrics_performance.cpp +++ b/tests/benchmarks/qml/librarymetrics_performance/tst_librarymetrics_performance.cpp @@ -35,6 +35,16 @@ // This benchmark produces performance statistics // for the standard set of elements, properties and expressions which // are provided in the QtDeclarative library (QtQml and QtQuick). +// +// Note that we have hand-rolled our own benchmark harness, rather +// than directly using QBENCHMARK, in order to avoid contaminating +// the benchmark results with unwanted initialization or side-effects +// and allow us to more completely isolate the required specific area +// (compilation, instantiation, or cached type-data instantiation) +// that we wish to benchmark. + +#define AVERAGE_OVER_N 10 +#define IGNORE_N_OUTLIERS 2 class ModuleApi : public QObject { @@ -197,6 +207,18 @@ void tst_librarymetrics_performance::metrics_data() QTest::newRow("062) positioning - binding (with grid) positioning") << testFileUrl("data/bindingwithgridpositioning.qml"); } +// This method is intended to benchmark the amount of time / cpu cycles +// required to compile the given QML input. +// Note that this deliberately does NOT include the time taken to +// construct the QML engine. +// Also note that between each iteration, we attempt to clean the +// engine state (destroying and reconstructing the engine, clearing +// the QML type registrations, etc) to simulate a cold-start environment. +// +// The benchmark result is expected to be dominated by QML parsing, +// compilation, and optimization paths, and since the compiled component +// is never instantiated, no construction or binding evaluation should +// occur. void tst_librarymetrics_performance::compilation() { QFETCH(QUrl, qmlfile); @@ -211,37 +233,148 @@ void tst_librarymetrics_performance::compilation() } } - QBENCHMARK { + QList<qint64> nResults; + + // generate AVERAGE_OVER_N results + for (int i = 0; i < AVERAGE_OVER_N; ++i) { cleanState(&e); - QQmlComponent c(e, this); - c.loadUrl(qmlfile); // just compile. + { + QElapsedTimer et; + et.start(); + // BEGIN benchmarked code block + QQmlComponent c(e, this); + c.loadUrl(qmlfile); + // END benchmarked code block + qint64 etime = et.nsecsElapsed(); + nResults.append(etime); + } } + + // sort the list + qSort(nResults); + + // remove IGNORE_N_OUTLIERS*2 from ONLY the worst end (remove gc interference) + for (int i = 0; i < IGNORE_N_OUTLIERS; ++i) { + if (!nResults.isEmpty()) nResults.removeLast(); + if (!nResults.isEmpty()) nResults.removeLast(); + } + + // now generate an average + qint64 totaltime = 0; + if (nResults.size() == 0) nResults.append(9999); + for (int i = 0; i < nResults.size(); ++i) + totaltime += nResults.at(i); + double average = ((double)totaltime) / nResults.count(); + + // and return it as the result + QTest::setBenchmarkResult(average, QTest::WalltimeNanoseconds); + QTest::setBenchmarkResult(average, QTest::WalltimeNanoseconds); // twice to workaround bug in QTestLib } +// This method is intended to benchmark the amount of time / cpu cycles +// required to compile and instantiate the given QML input, +// where the QML state has NOT been cleared in between each iteration. +// Thus, cached type data should be used when instantiating the object. +// +// The benchmark result is expected to be dominated by QObject +// hierarchy construction and first-time binding evaluation. void tst_librarymetrics_performance::instantiation_cached() { QFETCH(QUrl, qmlfile); + cleanState(&e); + QList<qint64> nResults; - QBENCHMARK { + // generate AVERAGE_OVER_N results + for (int i = 0; i < AVERAGE_OVER_N; ++i) { + QElapsedTimer et; + et.start(); + // BEGIN benchmarked code block QQmlComponent c(e, this); - c.loadUrl(qmlfile); // just compile. + c.loadUrl(qmlfile); QObject *o = c.create(); + // END benchmarked code block + qint64 etime = et.nsecsElapsed(); + nResults.append(etime); delete o; } + + // sort the list + qSort(nResults); + + // remove IGNORE_N_OUTLIERS*2 from ONLY the worst end (remove gc interference) + for (int i = 0; i < IGNORE_N_OUTLIERS; ++i) { + if (!nResults.isEmpty()) nResults.removeLast(); + if (!nResults.isEmpty()) nResults.removeLast(); + } + + // now generate an average + qint64 totaltime = 0; + if (nResults.size() == 0) nResults.append(9999); + for (int i = 0; i < nResults.size(); ++i) + totaltime += nResults.at(i); + double average = ((double)totaltime) / nResults.count(); + + // and return it as the result + QTest::setBenchmarkResult(average, QTest::WalltimeNanoseconds); + QTest::setBenchmarkResult(average, QTest::WalltimeNanoseconds); // twice to workaround bug in QTestLib } +// This method is intended to benchmark the amount of time / cpu cycles +// required to compile and instantiate the given QML input, +// where the QML state has been cleared in between each iteration. +// This will cause the engine to parse, compile and optimize the +// input QML in each iteration. After compilation, the component +// will be instantiated, and so QObject hierarchy construction and +// first time binding evaluation will contribute to the result. +// +// The compilation phase is expected to dominate the result, however +// in some cases (complex positioning via expensive bindings, or +// other pathological cases) instantiation may dominate. Those +// cases are prime candidates for further investigation... void tst_librarymetrics_performance::instantiation() { QFETCH(QUrl, qmlfile); - QBENCHMARK { + cleanState(&e); + QList<qint64> nResults; + + // generate AVERAGE_OVER_N results + for (int i = 0; i < AVERAGE_OVER_N; ++i) { cleanState(&e); - QQmlComponent c(e, this); - c.loadUrl(qmlfile); // just compile. - QObject *o = c.create(); - delete o; + { + QElapsedTimer et; + et.start(); + // BEGIN benchmarked code block + QQmlComponent c(e, this); + c.loadUrl(qmlfile); + QObject *o = c.create(); + // END benchmarked code block + qint64 etime = et.nsecsElapsed(); + nResults.append(etime); + delete o; + } + } + + // sort the list + qSort(nResults); + + // remove IGNORE_N_OUTLIERS*2 from ONLY the worst end (remove gc interference) + for (int i = 0; i < IGNORE_N_OUTLIERS; ++i) { + if (!nResults.isEmpty()) nResults.removeLast(); + if (!nResults.isEmpty()) nResults.removeLast(); } + + // now generate an average + qint64 totaltime = 0; + if (nResults.size() == 0) nResults.append(9999); + for (int i = 0; i < nResults.size(); ++i) + totaltime += nResults.at(i); + double average = ((double)totaltime) / nResults.count(); + + // and return it as the result + QTest::setBenchmarkResult(average, QTest::WalltimeNanoseconds); + QTest::setBenchmarkResult(average, QTest::WalltimeNanoseconds); // twice to workaround bug in QTestLib } void tst_librarymetrics_performance::positioners_data() @@ -256,19 +389,58 @@ void tst_librarymetrics_performance::positioners_data() QTest::newRow("07) positioning - binding (with grid) positioning") << testFileUrl("data/bindingwithgridpositioning.2.qml"); } -// this test triggers repositioning a large number of times, +// This method triggers repositioning a large number of times, // so we can track the cost of different repositioning methods. +// Note that the engine state is cleared before every iteration, +// so the benchmark result will include the cost of compilation +// as well as instantiation and first-time binding evaluation. +// +// The repositioning triggered within the QML is expected +// to dominate the benchmark time, especially if slow-path +// binding evaluation occurs. void tst_librarymetrics_performance::positioners() { QFETCH(QUrl, qmlfile); - QBENCHMARK { + cleanState(&e); + QList<qint64> nResults; + + // generate AVERAGE_OVER_N results + for (int i = 0; i < AVERAGE_OVER_N; ++i) { cleanState(&e); - QQmlComponent c(e, this); - c.loadUrl(qmlfile); // just compile. - QObject *o = c.create(); - delete o; + { + QElapsedTimer et; + et.start(); + // BEGIN benchmarked code block + QQmlComponent c(e, this); + c.loadUrl(qmlfile); + QObject *o = c.create(); + // END benchmarked code block + qint64 etime = et.nsecsElapsed(); + nResults.append(etime); + delete o; + } } + + // sort the list + qSort(nResults); + + // remove IGNORE_N_OUTLIERS*2 from ONLY the worst end (remove gc interference) + for (int i = 0; i < IGNORE_N_OUTLIERS; ++i) { + if (!nResults.isEmpty()) nResults.removeLast(); + if (!nResults.isEmpty()) nResults.removeLast(); + } + + // now generate an average + qint64 totaltime = 0; + if (nResults.size() == 0) nResults.append(9999); + for (int i = 0; i < nResults.size(); ++i) + totaltime += nResults.at(i); + double average = ((double)totaltime) / nResults.count(); + + // and return it as the result + QTest::setBenchmarkResult(average, QTest::WalltimeNanoseconds); + QTest::setBenchmarkResult(average, QTest::WalltimeNanoseconds); // twice to workaround bug in QTestLib } QTEST_MAIN(tst_librarymetrics_performance) diff --git a/tests/manual/tableview/tablemodel/form/RowForm.qml b/tests/manual/tableview/tablemodel/form/RowForm.qml new file mode 100644 index 0000000000..428682008a --- /dev/null +++ b/tests/manual/tableview/tablemodel/form/RowForm.qml @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.12 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.11 + +ScrollView { + clip: true + + function inputAsRow() { + return [ + { checkable: checkableCheckBox.checked, checked: checkedCheckBox.checked }, + { amount: amountSpinBox.value }, + { fruitType: fruitTypeTextField.text }, + { fruitName: fruitNameTextField.text }, + { fruitPrice: parseFloat(fruitPriceTextField.text) }, + ] + } + + default property alias content: gridLayout.children + + GridLayout { + id: gridLayout + columns: 2 + + RowLayout { + Layout.columnSpan: 2 + + Label { + text: "checkable" + } + CheckBox { + id: checkableCheckBox + checked: true + } + + Label { + text: "checked" + } + CheckBox { + id: checkedCheckBox + } + } + + Label { + text: "amount" + } + SpinBox { + id: amountSpinBox + value: 1 + } + + Label { + text: "fruitType" + } + TextField { + id: fruitTypeTextField + text: "Pear" + } + + Label { + text: "fruitName" + } + TextField { + id: fruitNameTextField + text: "Williams" + } + + Label { + text: "fruitPrice" + } + TextField { + id: fruitPriceTextField + text: "1.50" + } + } +} diff --git a/tests/manual/tableview/tablemodel/form/form.pro b/tests/manual/tableview/tablemodel/form/form.pro new file mode 100644 index 0000000000..ba6f7a91b1 --- /dev/null +++ b/tests/manual/tableview/tablemodel/form/form.pro @@ -0,0 +1,10 @@ +TEMPLATE = app +TARGET = form +QT += qml quick +SOURCES += main.cpp +RESOURCES += main.qml RowForm.qml + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target diff --git a/tests/manual/tableview/tablemodel/form/main.cpp b/tests/manual/tableview/tablemodel/form/main.cpp new file mode 100644 index 0000000000..2a3b90d392 --- /dev/null +++ b/tests/manual/tableview/tablemodel/form/main.cpp @@ -0,0 +1,52 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QGuiApplication> +#include <QQmlApplicationEngine> + +int main(int argc, char *argv[]) +{ + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); + + return app.exec(); +} diff --git a/tests/manual/tableview/tablemodel/form/main.qml b/tests/manual/tableview/tablemodel/form/main.qml new file mode 100644 index 0000000000..21ecd8edbb --- /dev/null +++ b/tests/manual/tableview/tablemodel/form/main.qml @@ -0,0 +1,284 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.12 +import QtQuick.Controls 2.5 +import QtQuick.Layouts 1.12 +import QtQuick.Window 2.12 +import Qt.labs.qmlmodels 1.0 + +ApplicationWindow { + id: window + width: 800 + height: 800 + visible: true + + ColumnLayout { + anchors.fill: parent + + TableView { + id: tableView + boundsBehavior: Flickable.StopAtBounds + + ScrollBar.horizontal: ScrollBar {} + ScrollBar.vertical: ScrollBar {} + + Layout.minimumHeight: window.height / 2 + Layout.fillWidth: true + Layout.fillHeight: true + + model: TableModel { + // One row = one type of fruit that can be ordered + rows: [ + [ + // Each object (line) is one cell/column, + // and each property in that object is a role. + { checked: false, checkable: true }, + { amount: 1 }, + { fruitType: "Apple" }, + { fruitName: "Granny Smith" }, + { fruitPrice: 1.50 } + ], + [ + { checked: true, checkable: true }, + { amount: 4 }, + { fruitType: "Orange" }, + { fruitName: "Navel" }, + { fruitPrice: 2.50 } + ], + [ + { checked: false, checkable: true }, + { amount: 1 }, + { fruitType: "Banana" }, + { fruitName: "Cavendish" }, + { fruitPrice: 3.50 } + ] + ] + } + + delegate: DelegateChooser { + DelegateChoice { + column: 0 + delegate: CheckBox { + objectName: "tableViewCheckBoxDelegate" + checked: model.checked + onToggled: model.checked = checked + } + } + DelegateChoice { + column: 1 + delegate: SpinBox { + objectName: "tableViewSpinBoxDelegate" + value: model.amount + onValueModified: model.amount = value + } + } + DelegateChoice { + delegate: TextField { + objectName: "tableViewTextFieldDelegate" + text: model.display + selectByMouse: true + implicitWidth: 140 + onAccepted: model.display = text + } + } + } + } + + TabBar { + id: operationTabBar + + Layout.fillWidth: true + Layout.preferredHeight: 40 + + TabButton { + text: "Append" + } + TabButton { + text: "Clear" + } + TabButton { + text: "Insert" + } + TabButton { + text: "Move" + } + TabButton { + text: "Remove" + } + TabButton { + text: "Set" + } + } + + StackLayout { + currentIndex: operationTabBar.currentIndex + + ColumnLayout { + RowForm { + id: appendRowForm + + Layout.fillHeight: true + } + + Button { + text: "Append" + + Layout.alignment: Qt.AlignRight + + onClicked: tableView.model.appendRow(appendRowForm.inputAsRow()) + } + } + ColumnLayout { + Button { + text: "Clear" + enabled: tableView.rows > 0 + + onClicked: tableView.model.clear() + } + } + ColumnLayout { + RowForm { + id: insertRowForm + + Layout.fillHeight: true + + Label { + text: "Insert index" + } + SpinBox { + id: insertIndexSpinBox + from: 0 + to: tableView.rows + } + } + + Button { + text: "Insert" + + Layout.alignment: Qt.AlignRight + + onClicked: tableView.model.insertRow(insertIndexSpinBox.value, insertRowForm.inputAsRow()) + } + } + GridLayout { + columns: 2 + + Label { + text: "Move from index" + } + SpinBox { + id: moveFromIndexSpinBox + from: 0 + to: tableView.rows > 0 ? tableView.rows - 1 : 0 + } + + Label { + text: "Move to index" + } + SpinBox { + id: moveToIndexSpinBox + from: 0 + to: tableView.rows > 0 ? tableView.rows - 1 : 0 + } + + Label { + text: "Rows to move" + } + SpinBox { + id: rowsToMoveSpinBox + from: 1 + to: tableView.rows + } + + Button { + text: "Move" + enabled: tableView.rows > 0 + + Layout.alignment: Qt.AlignRight + Layout.columnSpan: 2 + + onClicked: tableView.model.moveRow(moveFromIndexSpinBox.value, moveToIndexSpinBox.value, rowsToMoveSpinBox.value) + } + } + GridLayout { + Label { + text: "Remove index" + } + SpinBox { + id: removeIndexSpinBox + from: 0 + to: tableView.rows > 0 ? tableView.rows - 1 : 0 + } + + Button { + text: "Remove" + enabled: tableView.rows > 0 + + Layout.alignment: Qt.AlignRight + Layout.columnSpan: 2 + + onClicked: tableView.model.removeRow(removeIndexSpinBox.value) + } + } + ColumnLayout { + RowForm { + id: setRowForm + + Layout.fillHeight: true + + Label { + text: "Set index" + } + SpinBox { + id: setIndexSpinBox + from: 0 + to: tableView.rows > 0 ? tableView.rows - 1 : 0 + } + } + + Button { + text: "Set" + + onClicked: tableView.model.setRow(setIndexSpinBox.value, setRowForm.inputAsRow()); + } + } + } + } +} diff --git a/tests/manual/tableview/tablemodel/tablemodel.pro b/tests/manual/tableview/tablemodel/tablemodel.pro new file mode 100644 index 0000000000..4e4eba7653 --- /dev/null +++ b/tests/manual/tableview/tablemodel/tablemodel.pro @@ -0,0 +1,2 @@ +TEMPLATE = subdirs +SUBDIRS += form |