/**************************************************************************** ** ** Copyright (C) 2016 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 #include #include "../../shared/util.h" Q_DECLARE_METATYPE(QJsonValue::Type) class JsonPropertyObject : public QObject { Q_OBJECT Q_PROPERTY(QJsonValue value READ value WRITE setValue) Q_PROPERTY(QJsonObject object READ object WRITE setObject) Q_PROPERTY(QJsonArray array READ array WRITE setArray) public: QJsonValue value() const { return m_value; } void setValue(const QJsonValue &v) { m_value = v; } QJsonObject object() const { return m_object; } void setObject(const QJsonObject &o) { m_object = o; } QJsonArray array() const { return m_array; } void setArray(const QJsonArray &a) { m_array = a; } private: QJsonValue m_value; QJsonObject m_object; QJsonArray m_array; }; class tst_qjsonbinding : public QQmlDataTest { Q_OBJECT public: tst_qjsonbinding() {} private slots: void cppJsConversion_data(); void cppJsConversion(); void readValueProperty_data(); void readValueProperty(); void readObjectOrArrayProperty_data(); void readObjectOrArrayProperty(); void writeValueProperty_data(); void writeValueProperty(); void writeObjectOrArrayProperty_data(); void writeObjectOrArrayProperty(); void writeProperty_incompatibleType_data(); void writeProperty_incompatibleType(); void writeProperty_javascriptExpression_data(); void writeProperty_javascriptExpression(); private: QByteArray readAsUtf8(const QString &fileName); static QJsonValue valueFromJson(const QByteArray &json); void addPrimitiveDataTestFiles(); void addObjectDataTestFiles(); void addArrayDataTestFiles(); }; QByteArray tst_qjsonbinding::readAsUtf8(const QString &fileName) { QFile file(testFile(fileName)); file.open(QIODevice::ReadOnly); QTextStream stream(&file); return stream.readAll().trimmed().toUtf8(); } QJsonValue tst_qjsonbinding::valueFromJson(const QByteArray &json) { if (json.isEmpty()) return QJsonValue(QJsonValue::Undefined); QJsonDocument doc = QJsonDocument::fromJson(json); if (!doc.isEmpty()) return doc.isObject() ? QJsonValue(doc.object()) : QJsonValue(doc.array()); // QJsonDocument::fromJson() only handles objects and arrays... // Wrap the JSON inside a dummy object and extract the value. QByteArray wrappedJson = "{\"prop\":" + json + '}'; doc = QJsonDocument::fromJson(wrappedJson); Q_ASSERT(doc.isObject()); return doc.object().value("prop"); } void tst_qjsonbinding::addPrimitiveDataTestFiles() { QTest::newRow("true") << "true.json"; QTest::newRow("false") << "false.json"; QTest::newRow("null") << "null.json"; QTest::newRow("number.0") << "number.0.json"; QTest::newRow("number.1") << "number.1.json"; QTest::newRow("string.0") << "string.0.json"; QTest::newRow("undefined") << "empty.json"; } void tst_qjsonbinding::addObjectDataTestFiles() { QTest::newRow("object.0") << "object.0.json"; QTest::newRow("object.1") << "object.1.json"; QTest::newRow("object.2") << "object.2.json"; QTest::newRow("object.3") << "object.3.json"; QTest::newRow("object.4") << "object.4.json"; } void tst_qjsonbinding::addArrayDataTestFiles() { QTest::newRow("array.0") << "array.0.json"; QTest::newRow("array.1") << "array.1.json"; QTest::newRow("array.2") << "array.2.json"; QTest::newRow("array.3") << "array.3.json"; QTest::newRow("array.4") << "array.4.json"; } void tst_qjsonbinding::cppJsConversion_data() { QTest::addColumn("fileName"); addPrimitiveDataTestFiles(); addObjectDataTestFiles(); addArrayDataTestFiles(); } void tst_qjsonbinding::cppJsConversion() { QFETCH(QString, fileName); QByteArray json = readAsUtf8(fileName); QJsonValue jsonValue = valueFromJson(json); QJSEngine eng; QJSValue stringify = eng.globalObject().property("JSON").property("stringify"); QVERIFY(stringify.isCallable()); { QJSValue jsValue = eng.toScriptValue(jsonValue); QVERIFY(!jsValue.isVariant()); switch (jsonValue.type()) { case QJsonValue::Null: QVERIFY(jsValue.isNull()); break; case QJsonValue::Bool: QVERIFY(jsValue.isBool()); QCOMPARE(jsValue.toBool(), jsonValue.toBool()); break; case QJsonValue::Double: QVERIFY(jsValue.isNumber()); QCOMPARE(jsValue.toNumber(), jsonValue.toDouble()); break; case QJsonValue::String: QVERIFY(jsValue.isString()); QCOMPARE(jsValue.toString(), jsonValue.toString()); break; case QJsonValue::Array: QVERIFY(jsValue.isArray()); break; case QJsonValue::Object: QVERIFY(jsValue.isObject()); break; case QJsonValue::Undefined: QVERIFY(jsValue.isUndefined()); break; } if (jsValue.isUndefined()) { QVERIFY(json.isEmpty()); } else { QJSValue stringified = stringify.call(QJSValueList() << jsValue); QVERIFY(!stringified.isError()); QCOMPARE(stringified.toString().toUtf8(), json); } QJsonValue roundtrip = qjsvalue_cast(jsValue); // Workarounds for QTBUG-25164 if (jsonValue.isObject() && jsonValue.toObject().isEmpty()) QVERIFY(roundtrip.isObject() && roundtrip.toObject().isEmpty()); else if (jsonValue.isArray() && jsonValue.toArray().isEmpty()) QVERIFY(roundtrip.isArray() && roundtrip.toArray().isEmpty()); else QCOMPARE(roundtrip, jsonValue); } if (jsonValue.isObject()) { QJsonObject jsonObject = jsonValue.toObject(); QJSValue jsObject = eng.toScriptValue(jsonObject); QVERIFY(!jsObject.isVariant()); QVERIFY(jsObject.isObject()); QJSValue stringified = stringify.call(QJSValueList() << jsObject); QVERIFY(!stringified.isError()); QCOMPARE(stringified.toString().toUtf8(), json); QJsonObject roundtrip = qjsvalue_cast(jsObject); QCOMPARE(roundtrip, jsonObject); } else if (jsonValue.isArray()) { QJsonArray jsonArray = jsonValue.toArray(); QJSValue jsArray = eng.toScriptValue(jsonArray); QVERIFY(!jsArray.isVariant()); QVERIFY(jsArray.isArray()); QJSValue stringified = stringify.call(QJSValueList() << jsArray); QVERIFY(!stringified.isError()); QCOMPARE(stringified.toString().toUtf8(), json); QJsonArray roundtrip = qjsvalue_cast(jsArray); QCOMPARE(roundtrip, jsonArray); } } void tst_qjsonbinding::readValueProperty_data() { cppJsConversion_data(); } void tst_qjsonbinding::readValueProperty() { QFETCH(QString, fileName); QByteArray json = readAsUtf8(fileName); QJsonValue jsonValue = valueFromJson(json); QJSEngine eng; JsonPropertyObject obj; obj.setValue(jsonValue); eng.globalObject().setProperty("obj", eng.newQObject(&obj)); QJSValue stringified = eng.evaluate( "var v = obj.value; (typeof v == 'undefined') ? '' : JSON.stringify(v)"); QVERIFY(!stringified.isError()); QCOMPARE(stringified.toString().toUtf8(), json); } void tst_qjsonbinding::readObjectOrArrayProperty_data() { QTest::addColumn("fileName"); addObjectDataTestFiles(); addArrayDataTestFiles(); } void tst_qjsonbinding::readObjectOrArrayProperty() { QFETCH(QString, fileName); QByteArray json = readAsUtf8(fileName); QJsonValue jsonValue = valueFromJson(json); QVERIFY(jsonValue.isObject() || jsonValue.isArray()); QJSEngine eng; JsonPropertyObject obj; if (jsonValue.isObject()) obj.setObject(jsonValue.toObject()); else obj.setArray(jsonValue.toArray()); eng.globalObject().setProperty("obj", eng.newQObject(&obj)); QJSValue stringified = eng.evaluate( QString::fromLatin1("JSON.stringify(obj.%0)").arg( jsonValue.isObject() ? "object" : "array")); QVERIFY(!stringified.isError()); QCOMPARE(stringified.toString().toUtf8(), json); } void tst_qjsonbinding::writeValueProperty_data() { readValueProperty_data(); } void tst_qjsonbinding::writeValueProperty() { QFETCH(QString, fileName); QByteArray json = readAsUtf8(fileName); QJsonValue jsonValue = valueFromJson(json); QJSEngine eng; JsonPropertyObject obj; eng.globalObject().setProperty("obj", eng.newQObject(&obj)); QJSValue fun = eng.evaluate( "(function(json) {" " void(obj.value = (json == '') ? undefined : JSON.parse(json));" "})"); QVERIFY(fun.isCallable()); QVERIFY(obj.value().isNull()); QVERIFY(fun.call(QJSValueList() << QString::fromUtf8(json)).isUndefined()); // Workarounds for QTBUG-25164 if (jsonValue.isObject() && jsonValue.toObject().isEmpty()) QVERIFY(obj.value().isObject() && obj.value().toObject().isEmpty()); else if (jsonValue.isArray() && jsonValue.toArray().isEmpty()) QVERIFY(obj.value().isArray() && obj.value().toArray().isEmpty()); else QCOMPARE(obj.value(), jsonValue); } void tst_qjsonbinding::writeObjectOrArrayProperty_data() { readObjectOrArrayProperty_data(); } void tst_qjsonbinding::writeObjectOrArrayProperty() { QFETCH(QString, fileName); QByteArray json = readAsUtf8(fileName); QJsonValue jsonValue = valueFromJson(json); QVERIFY(jsonValue.isObject() || jsonValue.isArray()); QJSEngine eng; JsonPropertyObject obj; eng.globalObject().setProperty("obj", eng.newQObject(&obj)); QJSValue fun = eng.evaluate( QString::fromLatin1( "(function(json) {" " void(obj.%0 = JSON.parse(json));" "})").arg(jsonValue.isObject() ? "object" : "array") ); QVERIFY(fun.isCallable()); QVERIFY(obj.object().isEmpty() && obj.array().isEmpty()); QVERIFY(fun.call(QJSValueList() << QString::fromUtf8(json)).isUndefined()); if (jsonValue.isObject()) QCOMPARE(obj.object(), jsonValue.toObject()); else QCOMPARE(obj.array(), jsonValue.toArray()); } void tst_qjsonbinding::writeProperty_incompatibleType_data() { QTest::addColumn("property"); QTest::addColumn("expression"); QTest::newRow("value=function") << "value" << "(function(){})"; QTest::newRow("object=undefined") << "object" << "undefined"; QTest::newRow("object=null") << "object" << "null"; QTest::newRow("object=false") << "object" << "false"; QTest::newRow("object=true") << "object" << "true"; QTest::newRow("object=123") << "object" << "123"; QTest::newRow("object=42.35") << "object" << "42.35"; QTest::newRow("object='foo'") << "object" << "'foo'"; QTest::newRow("object=[]") << "object" << "[]"; QTest::newRow("object=function") << "object" << "(function(){})"; QTest::newRow("array=undefined") << "array" << "undefined"; QTest::newRow("array=null") << "array" << "null"; QTest::newRow("array=false") << "array" << "false"; QTest::newRow("array=true") << "array" << "true"; QTest::newRow("array=123") << "array" << "123"; QTest::newRow("array=42.35") << "array" << "42.35"; QTest::newRow("array='foo'") << "array" << "'foo'"; QTest::newRow("array={}") << "array" << "{}"; QTest::newRow("array=function") << "array" << "(function(){})"; } void tst_qjsonbinding::writeProperty_incompatibleType() { QFETCH(QString, property); QFETCH(QString, expression); QJSEngine eng; JsonPropertyObject obj; eng.globalObject().setProperty("obj", eng.newQObject(&obj)); QJSValue ret = eng.evaluate(QString::fromLatin1("obj.%0 = %1") .arg(property).arg(expression)); QVERIFY(ret.isError()); QVERIFY(ret.toString().contains("Cannot assign")); } void tst_qjsonbinding::writeProperty_javascriptExpression_data() { QTest::addColumn("property"); QTest::addColumn("expression"); QTest::addColumn("expectedJson"); // Function properties should be omitted. QTest::newRow("value = object with function property") << "value" << "{ foo: function() {} }" << "{}"; QTest::newRow("object = object with function property") << "object" << "{ foo: function() {} }" << "{}"; QTest::newRow("array = array with function property") << "array" << "[function() {}]" << "[null]"; // Inherited properties should not be included. QTest::newRow("value = object with inherited property") << "value" << "{ __proto__: { proto_foo: 123 } }" << "{}"; QTest::newRow("value = object with inherited property 2") << "value" << "{ foo: 123, __proto__: { proto_foo: 456 } }" << "{\"foo\":123}"; QTest::newRow("value = array with inherited property") << "value" << "(function() { var a = []; a.__proto__ = { proto_foo: 123 }; return a; })()" << "[]"; QTest::newRow("value = array with inherited property 2") << "value" << "(function() { var a = [10, 20]; a.__proto__ = { proto_foo: 123 }; return a; })()" << "[10,20]"; QTest::newRow("object = object with inherited property") << "object" << "{ __proto__: { proto_foo: 123 } }" << "{}"; QTest::newRow("object = object with inherited property 2") << "object" << "{ foo: 123, __proto__: { proto_foo: 456 } }" << "{\"foo\":123}"; QTest::newRow("array = array with inherited property") << "array" << "(function() { var a = []; a.__proto__ = { proto_foo: 123 }; return a; })()" << "[]"; QTest::newRow("array = array with inherited property 2") << "array" << "(function() { var a = [10, 20]; a.__proto__ = { proto_foo: 123 }; return a; })()" << "[10,20]"; // Non-enumerable properties should not be included. QTest::newRow("value = object with non-enumerable property") << "value" << "Object.defineProperty({}, 'foo', { value: 123, enumerable: false })" << "{}"; QTest::newRow("object = object with non-enumerable property") << "object" << "Object.defineProperty({}, 'foo', { value: 123, enumerable: false })" << "{}"; // Cyclic data structures are permitted, but the cyclic links become // empty objects. QTest::newRow("value = cyclic object") << "value" << "(function() { var o = { foo: 123 }; o.o = o; return o; })()" << "{\"foo\":123,\"o\":{}}"; QTest::newRow("value = cyclic array") << "value" << "(function() { var a = [10, 20]; a.push(a); return a; })()" << "[10,20,[]]"; QTest::newRow("object = cyclic object") << "object" << "(function() { var o = { bar: true }; o.o = o; return o; })()" << "{\"bar\":true,\"o\":{}}"; QTest::newRow("array = cyclic array") << "array" << "(function() { var a = [30, 40]; a.unshift(a); return a; })()" << "[[],30,40]"; // Properties with undefined value are excluded. QTest::newRow("value = { foo: undefined }") << "value" << "{ foo: undefined }" << "{}"; QTest::newRow("value = { foo: undefined, bar: 123 }") << "value" << "{ foo: undefined, bar: 123 }" << "{\"bar\":123}"; QTest::newRow("value = { foo: 456, bar: undefined }") << "value" << "{ foo: 456, bar: undefined }" << "{\"foo\":456}"; QTest::newRow("object = { foo: undefined }") << "object" << "{ foo: undefined }" << "{}"; QTest::newRow("object = { foo: undefined, bar: 123 }") << "object" << "{ foo: undefined, bar: 123 }" << "{\"bar\":123}"; QTest::newRow("object = { foo: 456, bar: undefined }") << "object" << "{ foo: 456, bar: undefined }" << "{\"foo\":456}"; // QJsonArray::append() implicitly converts undefined values to null. QTest::newRow("value = [undefined]") << "value" << "[undefined]" << "[null]"; QTest::newRow("value = [undefined, 10]") << "value" << "[undefined, 10]" << "[null,10]"; QTest::newRow("value = [10, undefined, 20]") << "value" << "[10, undefined, 20]" << "[10,null,20]"; QTest::newRow("array = [undefined]") << "array" << "[undefined]" << "[null]"; QTest::newRow("array = [undefined, 10]") << "array" << "[undefined, 10]" << "[null,10]"; QTest::newRow("array = [10, undefined, 20]") << "array" << "[10, undefined, 20]" << "[10,null,20]"; } void tst_qjsonbinding::writeProperty_javascriptExpression() { QFETCH(QString, property); QFETCH(QString, expression); QFETCH(QString, expectedJson); QJSEngine eng; JsonPropertyObject obj; eng.globalObject().setProperty("obj", eng.newQObject(&obj)); QJSValue ret = eng.evaluate(QString::fromLatin1("obj.%0 = %1; JSON.stringify(obj.%0)") .arg(property).arg(expression)); QVERIFY(!ret.isError()); QCOMPARE(ret.toString(), expectedJson); } QTEST_MAIN(tst_qjsonbinding) #include "tst_qjsonbinding.moc"