538 lines
18 KiB
C++
538 lines
18 KiB
C++
// Copyright (C) 2016 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
|
#include <QtTest/QTest>
|
|
#include <QtQml/QtQml>
|
|
#include <QtQuickTestUtils/private/qmlutils_p.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() : QQmlDataTest(QT_QMLTEST_DATADIR) {}
|
|
|
|
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();
|
|
|
|
void cyclicStringify();
|
|
void recursiveStringify();
|
|
|
|
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));
|
|
if (!file.open(QIODevice::ReadOnly))
|
|
qFatal("Cannot open file %s", qPrintable(fileName));
|
|
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);
|
|
#if QT_DEPRECATED_SINCE(6, 9)
|
|
QT_WARNING_PUSH QT_WARNING_DISABLE_DEPRECATED
|
|
QVERIFY(!jsValue.isVariant());
|
|
QT_WARNING_POP
|
|
#endif
|
|
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.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.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);
|
|
}
|
|
|
|
void tst_qjsonbinding::cyclicStringify()
|
|
{
|
|
QJSEngine e;
|
|
const QString program = QStringLiteral(R"(
|
|
var a = {};
|
|
a.a = a;
|
|
JSON.stringify(a);
|
|
)");
|
|
|
|
QJSValue result = e.evaluate(program);
|
|
QVERIFY(result.isError());
|
|
QCOMPARE(result.errorType(), QJSValue::TypeError);
|
|
QVERIFY(result.toString().contains(QLatin1String("Cannot convert circular structure to JSON")));
|
|
}
|
|
|
|
void tst_qjsonbinding::recursiveStringify()
|
|
{
|
|
QJSEngine e;
|
|
const QString program = QStringLiteral(R"(
|
|
function create() {
|
|
var a = {}
|
|
Object.defineProperty(a, "a", {
|
|
enumerable: true,
|
|
get: function() { return create(); }
|
|
});
|
|
return a;
|
|
}
|
|
|
|
JSON.stringify(create());
|
|
)");
|
|
|
|
QJSValue result = e.evaluate(program);
|
|
QVERIFY(result.isError());
|
|
QCOMPARE(result.errorType(), QJSValue::RangeError);
|
|
QVERIFY(result.toString().contains(QLatin1String("Maximum call stack size exceeded")));
|
|
}
|
|
|
|
QTEST_MAIN(tst_qjsonbinding)
|
|
|
|
#include "tst_qjsonbinding.moc"
|