blob: 05e1ce8b6b58d35991214f639f2a5590b20c6e5f [file] [log] [blame]
/****************************************************************************
**
** 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 <QtTest/QtTest>
#include <QtQml/QtQml>
#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<QString>("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<QJsonValue>(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<QJsonObject>(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<QJsonArray>(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<QString>("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<QString>("property");
QTest::addColumn<QString>("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<QString>("property");
QTest::addColumn<QString>("expression");
QTest::addColumn<QString>("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"